This is an automated email from the ASF dual-hosted git repository.
xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-opendal.git
The following commit(s) were added to refs/heads/main by this push:
new 321a2f1ee feat(binding/php): Add basic io (#2782)
321a2f1ee is described below
commit 321a2f1ee056870f38d531b37d9e43182459dbda
Author: Lianbo <[email protected]>
AuthorDate: Tue Aug 8 15:47:00 2023 +0800
feat(binding/php): Add basic io (#2782)
* Basic IO
* Finish basic io
* Add license
* Rebase
* chore:rename api
* feat: add invalid uft-8 test
* feat: binary safe io
* feat: memory test and binary safe for `read` api
* chore: format license
---
Cargo.lock | 186 +++++++++++++++++++++
Cargo.toml | 1 +
bindings/php/Cargo.toml | 2 +-
bindings/php/composer.json | 2 +-
bindings/php/opendal-php.stubs.php | 114 ++++++++++++-
bindings/php/src/lib.rs | 160 +++++++++++++++++-
bindings/php/tests/Feature/BasicIOTest.php | 133 +++++++++++++++
bindings/php/tests/Pest.php | 63 +++++++
.../{opendal-php.stubs.php => tests/TestCase.php} | 9 +-
bindings/php/tests/Unit/BasicTest.php | 41 ++++-
10 files changed, 697 insertions(+), 14 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 36cdbc7d7..b84f14289 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+[[package]]
+name = "aes"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
[[package]]
name = "ahash"
version = "0.7.6"
@@ -676,6 +687,16 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
+[[package]]
+name = "bzip2"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
+dependencies = [
+ "bzip2-sys",
+ "libc",
+]
+
[[package]]
name = "bzip2-sys"
version = "0.1.11+1.0.8"
@@ -842,6 +863,16 @@ dependencies = [
"half",
]
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
[[package]]
name = "clang-sys"
version = "1.6.1"
@@ -999,6 +1030,12 @@ dependencies = [
"tiny-keccak",
]
+[[package]]
+name = "constant_time_eq"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
+
[[package]]
name = "convert_case"
version = "0.6.0"
@@ -1555,6 +1592,41 @@ version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+[[package]]
+name = "ext-php-rs"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7801d7d8a7b4d1435a2a067bf5a5e321641c78ed1016e2f773d9a9296c6e80f"
+dependencies = [
+ "anyhow",
+ "bindgen 0.65.1",
+ "bitflags 2.3.3",
+ "cc",
+ "cfg-if",
+ "ext-php-rs-derive",
+ "native-tls",
+ "once_cell",
+ "parking_lot 0.12.1",
+ "skeptic",
+ "ureq",
+ "zip",
+]
+
+[[package]]
+name = "ext-php-rs-derive"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88979a9b357bdd9b8ffea208133f0bd0fa7e4d4e18bed3242335ed7b27033cf2"
+dependencies = [
+ "anyhow",
+ "darling",
+ "ident_case",
+ "lazy_static",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
[[package]]
name = "fail"
version = "0.4.0"
@@ -1587,6 +1659,16 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda653ca797810c02f7ca4b804b40b8b95ae046eb989d356bce17919a8c25499"
+[[package]]
+name = "flate2"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
[[package]]
name = "flexstr"
version = "0.9.2"
@@ -2276,6 +2358,15 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac"
+[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "instant"
version = "0.1.12"
@@ -3469,6 +3560,14 @@ dependencies = [
"opendal",
]
+[[package]]
+name = "opendal-php"
+version = "0.1.0"
+dependencies = [
+ "ext-php-rs",
+ "opendal",
+]
+
[[package]]
name = "opendal-python"
version = "0.39.0"
@@ -3869,12 +3968,35 @@ dependencies = [
"windows-targets 0.48.1",
]
+[[package]]
+name = "password-hash"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
+dependencies = [
+ "base64ct",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
[[package]]
name = "paste"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
+[[package]]
+name = "pbkdf2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
+dependencies = [
+ "digest",
+ "hmac",
+ "password-hash",
+ "sha2",
+]
+
[[package]]
name = "peeking_take_while"
version = "0.1.2"
@@ -6112,6 +6234,20 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+[[package]]
+name = "ureq"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9"
+dependencies = [
+ "base64 0.21.2",
+ "flate2",
+ "log",
+ "native-tls",
+ "once_cell",
+ "url",
+]
+
[[package]]
name = "url"
version = "2.4.0"
@@ -6627,3 +6763,53 @@ checksum =
"70b40401a28d86ce16a330b863b86fd7dbee4d7c940587ab09ab8c019f9e3fdf"
dependencies = [
"num-traits",
]
+
+[[package]]
+name = "zip"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
+dependencies = [
+ "aes",
+ "byteorder",
+ "bzip2",
+ "constant_time_eq",
+ "crc32fast",
+ "crossbeam-utils",
+ "flate2",
+ "hmac",
+ "pbkdf2",
+ "sha1",
+ "time 0.3.22",
+ "zstd",
+]
+
+[[package]]
+name = "zstd"
+version = "0.11.2+zstd.1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "5.0.2+zstd.1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
+dependencies = [
+ "libc",
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.8+zstd.1.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 2c99a9699..98a3389f5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -34,6 +34,7 @@ members = [
"bindings/lua",
"bindings/dotnet",
"bindings/ocaml",
+ "bindings/php",
"bin/oli",
"bin/oay",
diff --git a/bindings/php/Cargo.toml b/bindings/php/Cargo.toml
index ec9f0a2d3..8cb0de743 100644
--- a/bindings/php/Cargo.toml
+++ b/bindings/php/Cargo.toml
@@ -32,4 +32,4 @@ crate-type = ["cdylib"]
[dependencies]
ext-php-rs = "0.10.1"
-opendal = {version = "0.38.1", path = "../../core" }
+opendal.workspace = true
diff --git a/bindings/php/composer.json b/bindings/php/composer.json
index 1f8157703..2126c6ffb 100644
--- a/bindings/php/composer.json
+++ b/bindings/php/composer.json
@@ -15,7 +15,7 @@
},
"config": {
"allow-plugins": {
- "pestphp/pest-plugin": false
+ "pestphp/pest-plugin": true
}
}
}
diff --git a/bindings/php/opendal-php.stubs.php
b/bindings/php/opendal-php.stubs.php
index c8e44df26..8b78b8030 100644
--- a/bindings/php/opendal-php.stubs.php
+++ b/bindings/php/opendal-php.stubs.php
@@ -20,6 +20,116 @@
// Stubs for opendal-php
-namespace {
- function debug(): string {}
+namespace OpenDAL {
+ class Metadata {
+ public $content_disposition;
+
+ public $content_md5;
+
+ public $content_type;
+
+ public $content_length;
+
+ public $mode;
+
+ public $etag;
+
+ public function content_disposition(): ?string {}
+
+ /**
+ * Content length of this entry.
+ */
+ public function content_length(): int {}
+
+ /**
+ * Content MD5 of this entry.
+ */
+ public function content_md5(): ?string {}
+
+ /**
+ * Content Type of this entry.
+ */
+ public function content_type(): ?string {}
+
+ /**
+ * ETag of this entry.
+ */
+ public function etag(): ?string {}
+
+ /**
+ * mode represent this entry's mode.
+ */
+ public function mode(): \OpenDAL\EntryMode {}
+ }
+
+ class EntryMode {
+ public $is_file;
+
+ public $is_dir;
+
+ public function is_dir(): int {}
+
+ public function is_file(): int {}
+ }
+
+ class Operator {
+ public function __construct(string $scheme_str, array $config) {}
+
+ /**
+ * Write string into given path.
+ */
+ public function write(string $path, string $content): mixed {}
+
+ /**
+ * Write bytes into given path, binary safe.
+ */
+ public function write_binary(string $path, array $content): mixed {}
+
+ /**
+ * Read the whole path into bytes, binary safe.
+ */
+ public function read(string $path): string {}
+
+ /**
+ * Check if this path exists or not, return 1 if exists, 0 otherwise.
+ */
+ public function is_exist(string $path): int {}
+
+ /**
+ * Get current path's metadata **without cache** directly.
+ *
+ * # Notes
+ *
+ * Use `stat` if you:
+ *
+ * - Want detect the outside changes of path.
+ * - Don't want to read from cached metadata.
+ */
+ public function stat(string $path): \OpenDAL\Metadata {}
+
+ /**
+ * Delete given path.
+ *
+ * # Notes
+ *
+ * - Delete not existing error won't return errors.
+ */
+ public function delete(string $path): mixed {}
+
+ /**
+ * Create a dir at given path.
+ *
+ * # Notes
+ *
+ * To indicate that a path is a directory, it is compulsory to include
+ * a trailing / in the path. Failure to do so may result in
+ * `NotADirectory` error being returned by OpenDAL.
+ *
+ * # Behavior
+ *
+ * - Create on existing dir will succeed.
+ * - Create dir is always recursive, works like `mkdir -p`
+ */
+ public function create_dir(string $path): mixed {}
+ }
}
diff --git a/bindings/php/src/lib.rs b/bindings/php/src/lib.rs
index 288faeb33..dbb85a8e6 100644
--- a/bindings/php/src/lib.rs
+++ b/bindings/php/src/lib.rs
@@ -15,14 +15,164 @@
// specific language governing permissions and limitations
// under the License.
+use ::opendal as od;
+use ext_php_rs::binary::Binary;
+use ext_php_rs::convert::FromZval;
+use ext_php_rs::exception::PhpException;
+use ext_php_rs::flags::DataType;
use ext_php_rs::prelude::*;
-use opendal::{EntryMode, Metadata};
+use ext_php_rs::types::Zval;
+use std::collections::HashMap;
+use std::str::FromStr;
-#[php_function]
-pub fn debug() -> String {
- let metadata = Metadata::new(EntryMode::FILE);
+#[php_class(name = "OpenDAL\\Operator")]
+pub struct Operator(od::BlockingOperator);
- format!("{:?}", metadata)
+#[php_impl(rename_methods = "none")]
+impl Operator {
+ pub fn __construct(scheme_str: String, config: HashMap<String, String>) ->
PhpResult<Self> {
+ let scheme =
od::Scheme::from_str(&scheme_str).map_err(format_php_err)?;
+ let op = od::Operator::via_map(scheme,
config).map_err(format_php_err)?;
+
+ Ok(Operator(op.blocking()))
+ }
+
+ /// Write string into given path.
+ pub fn write(&self, path: &str, content: String) -> PhpResult<()> {
+ self.0.write(path, content).map_err(format_php_err)
+ }
+
+ /// Write bytes into given path, binary safe.
+ pub fn write_binary(&self, path: &str, content: Vec<u8>) -> PhpResult<()> {
+ self.0.write(path, content).map_err(format_php_err)
+ }
+
+ /// Read the whole path into bytes, binary safe.
+ pub fn read(&self, path: &str) -> PhpResult<Binary<u8>> {
+ self.0.read(path).map_err(format_php_err).map(Binary::from)
+ }
+
+ /// Check if this path exists or not, return 1 if exists, 0 otherwise.
+ pub fn is_exist(&self, path: &str) -> PhpResult<u8> {
+ self.0
+ .is_exist(path)
+ .map_err(format_php_err)
+ .map(|b| if b { 1 } else { 0 })
+ }
+
+ /// Get current path's metadata **without cache** directly.
+ ///
+ /// # Notes
+ ///
+ /// Use `stat` if you:
+ ///
+ /// - Want detect the outside changes of path.
+ /// - Don't want to read from cached metadata.
+ pub fn stat(&self, path: &str) -> PhpResult<Metadata> {
+ self.0.stat(path).map_err(format_php_err).map(Metadata)
+ }
+
+ /// Delete given path.
+ ///
+ /// # Notes
+ ///
+ /// - Delete not existing error won't return errors.
+ pub fn delete(&self, path: &str) -> PhpResult<()> {
+ self.0.delete(path).map_err(format_php_err)
+ }
+
+ /// Create a dir at given path.
+ ///
+ /// # Notes
+ ///
+ /// To indicate that a path is a directory, it is compulsory to include
+ /// a trailing / in the path. Failure to do so may result in
+ /// `NotADirectory` error being returned by OpenDAL.
+ ///
+ /// # Behavior
+ ///
+ /// - Create on existing dir will succeed.
+ /// - Create dir is always recursive, works like `mkdir -p`
+ pub fn create_dir(&self, path: &str) -> PhpResult<()> {
+ self.0.create_dir(path).map_err(format_php_err)
+ }
+}
+
+#[php_class(name = "OpenDAL\\Metadata")]
+pub struct Metadata(od::Metadata);
+
+#[php_impl(rename_methods = "none")]
+impl Metadata {
+ #[getter]
+ pub fn content_disposition(&self) -> Option<String> {
+ self.0.content_disposition().map(|s| s.to_string())
+ }
+
+ /// Content length of this entry.
+ #[getter]
+ pub fn content_length(&self) -> u64 {
+ self.0.content_length()
+ }
+
+ /// Content MD5 of this entry.
+ #[getter]
+ pub fn content_md5(&self) -> Option<String> {
+ self.0.content_md5().map(|s| s.to_string())
+ }
+
+ /// Content Type of this entry.
+ #[getter]
+ pub fn content_type(&self) -> Option<String> {
+ self.0.content_type().map(|s| s.to_string())
+ }
+
+ /// ETag of this entry.
+ #[getter]
+ pub fn etag(&self) -> Option<String> {
+ self.0.etag().map(|s| s.to_string())
+ }
+
+ /// mode represent this entry's mode.
+ #[getter]
+ pub fn mode(&self) -> EntryMode {
+ EntryMode(self.0.mode())
+ }
+}
+
+#[php_class(name = "OpenDAL\\EntryMode")]
+pub struct EntryMode(od::EntryMode);
+
+impl<'b> FromZval<'b> for EntryMode {
+ const TYPE: DataType = DataType::Object(Some("OpenDAL\\EntryMode"));
+
+ fn from_zval(zval: &'b Zval) -> Option<Self> {
+ zval.object().and_then(|obj| obj.get_property("mode").ok())
+ }
+}
+
+#[php_impl(rename_methods = "none")]
+impl EntryMode {
+ #[getter]
+ pub fn is_dir(&self) -> u8 {
+ match self.0.is_dir() {
+ true => 1,
+ false => 0,
+ }
+ }
+
+ #[getter]
+ pub fn is_file(&self) -> u8 {
+ match self.0.is_file() {
+ true => 1,
+ false => 0,
+ }
+ }
+}
+
+fn format_php_err(e: od::Error) -> PhpException {
+ // @todo use custom exception, we cannot use custom exception now,
+ // see https://github.com/davidcole1340/ext-php-rs/issues/262
+ PhpException::default(e.to_string())
}
#[php_module]
diff --git a/bindings/php/tests/Feature/BasicIOTest.php
b/bindings/php/tests/Feature/BasicIOTest.php
new file mode 100644
index 000000000..330e158e6
--- /dev/null
+++ b/bindings/php/tests/Feature/BasicIOTest.php
@@ -0,0 +1,133 @@
+<?php
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// describe does not support dataset yet
+
+describe('basic io with fs', function () {
+ $op = new \OpenDAL\Operator('fs', ['root' => '/tmp']);
+
+ it('ensure file not exist', function () use ($op) {
+ $op->delete('test.txt');
+ expect($op->is_exist('test.txt'))->toEqual(0);
+ });
+
+ it('write/read file', function () use ($op) {
+ $op->write('test.txt', 'hello world');
+ expect($op->is_exist('test.txt'))->toEqual(1)
+ ->and($op->read('test.txt'))->toEqual('hello world');
+ });
+
+ it('write/read file overwrite', function () use ($op) {
+ $op->write('test.txt', 'new content');
+ expect($op->is_exist('test.txt'))->toEqual(1)
+ ->and($op->read('test.txt'))->toEqual('new content');
+ });
+
+ it('file metadata', function () use ($op) {
+ $meta = $op->stat('test.txt');
+ expect($meta)->toBeInstanceOf(\OpenDAL\Metadata::class)
+ ->and($meta->content_length)->toEqual(11)
+ ->and($meta->mode)->toBeInstanceOf(\OpenDAL\EntryMode::class)
+ ->and($meta->mode->is_file)->toEqual(1)
+ ->and($meta->mode->is_dir)->toEqual(0);
+ });
+
+ it('delete file', function () use ($op) {
+ $op->delete('test.txt');
+ expect($op->is_exist('test.txt'))->toEqual(0);
+ });
+
+ it('create dir', function () use ($op) {
+ $op->create_dir('test/');
+ expect(is_dir('/tmp/test'))->toBeTrue();
+ });
+});
+
+describe('basic io with memory', function () {
+ $op = new \OpenDAL\Operator('memory', []);
+
+ it('ensure file not exist', function () use ($op) {
+ $op->delete('test.txt');
+ expect($op->is_exist('test.txt'))->toEqual(0);
+ });
+
+ it('write/read file', function () use ($op) {
+ $op->write('test.txt', 'hello world');
+ expect($op->is_exist('test.txt'))->toEqual(1)
+ ->and($op->read('test.txt'))->toEqual('hello world');
+ });
+
+ it('write/read file overwrite', function () use ($op) {
+ $op->write('test.txt', 'new content');
+ expect($op->is_exist('test.txt'))->toEqual(1)
+ ->and($op->read('test.txt'))->toEqual('new content');
+ });
+
+ it('file metadata', function () use ($op) {
+ $meta = $op->stat('test.txt');
+ expect($meta)->toBeInstanceOf(\OpenDAL\Metadata::class)
+ ->and($meta->content_length)->toEqual(11)
+ ->and($meta->mode)->toBeInstanceOf(\OpenDAL\EntryMode::class)
+ ->and($meta->mode->is_file)->toEqual(1)
+ ->and($meta->mode->is_dir)->toEqual(0);
+ });
+
+ it('delete file', function () use ($op) {
+ $op->delete('test.txt');
+ expect($op->is_exist('test.txt'))->toEqual(0);
+ });
+
+ it('create dir', function () use ($op) {
+ $op->create_dir('test/');
+ expect(is_dir('/tmp/test'))->toBeTrue();
+ });
+});
+
+describe('binary safe IO with fs', function () {
+ $op = new \OpenDAL\Operator('fs', ['root' => '/tmp']);
+
+ it('write & read invalid UTF-8', function () use ($op) {
+ $content = "hello 🌰 \x80\x80\x80 🍋";
+ $bytesArray = unpack('C*', $content);
+
+ expect($bytesArray)->toBeArray();
+
+ $op->write_binary('test.txt', $bytesArray);
+ $content = $op->read('test.txt');
+
+ expect($content)->toBeString()->toEqual($content);
+ });
+});
+
+describe('binary safe IO with memory', function () {
+ $op = new \OpenDAL\Operator('memory', []);
+
+ it('write & read invalid UTF-8', function () use ($op) {
+ $content = "hello 🌰 \x80\x80\x80 🍋";
+ $bytesArray = unpack('C*', $content);
+
+ expect($bytesArray)->toBeArray();
+
+ $op->write_binary('test.txt', $bytesArray);
+ $content = $op->read('test.txt');
+
+ expect($content)->toBeString()->toEqual($content);
+ });
+});
diff --git a/bindings/php/tests/Pest.php b/bindings/php/tests/Pest.php
new file mode 100644
index 000000000..5916d5704
--- /dev/null
+++ b/bindings/php/tests/Pest.php
@@ -0,0 +1,63 @@
+<?php
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+|--------------------------------------------------------------------------
+| Test Case
+|--------------------------------------------------------------------------
+|
+| The closure you provide to your test functions is always bound to a specific
PHPUnit test
+| case class. By default, that class is "PHPUnit\Framework\TestCase". Of
course, you may
+| need to change it using the "uses()" function to bind a different classes or
traits.
+|
+*/
+
+// uses(Tests\TestCase::class)->in('Feature');
+
+/*
+|--------------------------------------------------------------------------
+| Expectations
+|--------------------------------------------------------------------------
+|
+| When you're writing tests, you often need to check that values meet certain
conditions. The
+| "expect()" function gives you access to a set of "expectations" methods that
you can use
+| to assert different things. Of course, you may extend the Expectation API at
any time.
+|
+*/
+
+expect()->extend('toBeOne', function () {
+ return $this->toBe(1);
+});
+
+/*
+|--------------------------------------------------------------------------
+| Functions
+|--------------------------------------------------------------------------
+|
+| While Pest is very powerful out-of-the-box, you may have some testing code
specific to your
+| project that you don't want to repeat in every file. Here you can also
expose helpers as
+| global functions to help you to reduce the number of lines of code in your
test files.
+|
+*/
+
+function something()
+{
+ // ..
+}
diff --git a/bindings/php/opendal-php.stubs.php
b/bindings/php/tests/TestCase.php
similarity index 87%
copy from bindings/php/opendal-php.stubs.php
copy to bindings/php/tests/TestCase.php
index c8e44df26..552af5dd9 100644
--- a/bindings/php/opendal-php.stubs.php
+++ b/bindings/php/tests/TestCase.php
@@ -18,8 +18,11 @@
* under the License.
*/
-// Stubs for opendal-php
+namespace Tests;
-namespace {
- function debug(): string {}
+use PHPUnit\Framework\TestCase as BaseTestCase;
+
+abstract class TestCase extends BaseTestCase
+{
+ //
}
diff --git a/bindings/php/tests/Unit/BasicTest.php
b/bindings/php/tests/Unit/BasicTest.php
index 503f1c972..a9f04c495 100644
--- a/bindings/php/tests/Unit/BasicTest.php
+++ b/bindings/php/tests/Unit/BasicTest.php
@@ -22,6 +22,43 @@ it('opendal-php extension loaded', function () {
expect(extension_loaded('opendal-php'))->toBeTrue();
});
-it('debug function works', function () {
- expect(debug())->toStartWith('Metadata');
+it('class & methods exists', function ($class, $methods) {
+ expect(class_exists($class))->toBeTrue();
+ foreach ($methods as $method) {
+ expect(method_exists($class, $method))->toBeTrue();
+ }
+})->with([
+ ['OpenDAL\Operator', ['is_exist', 'read', 'write', 'delete', 'stat',
'create_dir', 'write_binary']],
+ ['OpenDAL\Metadata', []],
+ ['OpenDAL\EntryMode', []],
+]);
+
+describe('throw exception', function () {
+ it('invalid driver', function () {
+ new \OpenDAL\Operator('invalid', []);
+ })->throws('Exception');
+
+ it('unspecified root path', function () {
+ new \OpenDAL\Operator('fs', []);
+ })->throws('Exception');
+
+ it('read non-exist file', function () {
+ $op = new \OpenDAL\Operator('fs', ['root' => '/tmp']);
+ $op->read('non-exist.txt');
+ })->throws('Exception');
+});
+
+it('initialization OpenDAL', function () {
+ $op = new \OpenDAL\Operator('fs', ['root' => '/tmp']);
+
+ expect($op)
+ ->toBeInstanceOf(\OpenDAL\Operator::class)
+ ->not->toHaveProperty('op')
+ ->not->toThrow(Exception::class);
});
+
+it('invalid UTF-8 encoding', function () {
+ $op = new \OpenDAL\Operator('fs', ['root' => '/tmp']);
+
+ $op->write('test.txt', 'invalid UTF-8: '.chr(0x80));
+})->throws('Exception')->expectExceptionMessage('Invalid value given for
argument `content`.');