From 13a74d7f593cbb3b4511f5f538465b8c6de76437 Mon Sep 17 00:00:00 2001 From: Xenira <1288524+Xenira@users.noreply.github.com> Date: Wed, 7 May 2025 22:23:14 +0200 Subject: [PATCH 1/2] fix(class)!: generate correct stubs for extends and implements BREAKING CHANGE: `extends` and `implements` attributes now require the `stub` property containing the class/interface name to be used in stubs. Refs: #326 --- crates/macros/src/class.rs | 37 ++++++++++--- crates/macros/src/lib.rs | 31 +++++------ guide/src/macros/classes.md | 14 ++--- guide/src/migration-guides/v0.14.md | 11 +++- src/builders/class.rs | 26 +++++---- src/builders/module.rs | 8 +-- src/class.rs | 8 ++- src/closure.rs | 8 +-- src/describe/mod.rs | 11 +++- src/exception.rs | 16 +++++- src/macros.rs | 6 +- tests/src/integration/class/class.php | 12 ++++ tests/src/integration/class/mod.rs | 80 ++++++++++++++++++++++++++- 13 files changed, 206 insertions(+), 62 deletions(-) diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index 0b84ca512a..e4c05bfcee 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -20,12 +20,18 @@ pub struct StructAttributes { modifier: Option, /// An expression of `ClassFlags` to be applied to the class. flags: Option, - extends: Option, + extends: Option, #[darling(multiple)] - implements: Vec, + implements: Vec, attrs: Vec, } +#[derive(FromMeta, Debug)] +pub struct ClassEntryAttribute { + ce: syn::Expr, + stub: String, +} + pub fn parser(mut input: ItemStruct) -> Result { let attr = StructAttributes::from_attributes(&input.attrs)?; let ident = &input.ident; @@ -111,14 +117,13 @@ fn generate_registered_class_impl( ident: &syn::Ident, class_name: &str, modifier: Option<&syn::Ident>, - extends: Option<&syn::Expr>, - implements: &[syn::Expr], + extends: Option<&ClassEntryAttribute>, + implements: &[ClassEntryAttribute], fields: &[Property], flags: Option<&syn::Expr>, docs: &[String], ) -> TokenStream { let modifier = modifier.option_tokens(); - let extends = extends.option_tokens(); let fields = fields.iter().map(|prop| { let name = prop.name(); @@ -149,6 +154,24 @@ fn generate_registered_class_impl( #(#docs)* }; + let extends = if let Some(extends) = extends { + let ce = &extends.ce; + let stub = &extends.stub; + quote! { + Some((#ce, #stub)) + } + } else { + quote! { None } + }; + + let implements = implements.iter().map(|imp| { + let ce = &imp.ce; + let stub = &imp.stub; + quote! { + (#ce, #stub) + } + }); + quote! { impl ::ext_php_rs::class::RegisteredClass for #ident { const CLASS_NAME: &'static str = #class_name; @@ -156,9 +179,9 @@ fn generate_registered_class_impl( fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder > = #modifier; const EXTENDS: ::std::option::Option< - fn() -> &'static ::ext_php_rs::zend::ClassEntry + ::ext_php_rs::class::ClassEntryInfo > = #extends; - const IMPLEMENTS: &'static [fn() -> &'static ::ext_php_rs::zend::ClassEntry] = &[ + const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[ #(#implements,)* ]; const FLAGS: ::ext_php_rs::flags::ClassFlags = #flags; diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index b7458137d3..4fb3d4379d 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -27,21 +27,19 @@ extern crate proc_macro; /// /// ## Options /// -/// The attribute takes some options to modify the output of the class: +/// There are additional macros that modify the class. These macros **must** be +/// placed underneath the `#[php_class]` attribute. /// /// - `name` - Changes the name of the class when exported to PHP. The Rust /// struct name is kept the same. If no name is given, the name of the struct /// is used. Useful for namespacing classes. -/// -/// There are also additional macros that modify the class. These macros -/// **must** be placed underneath the `#[php_class]` attribute. -/// -/// - `#[php(extends = ce)]` - Sets the parent class of the class. Can only be -/// used once. `ce` must be a function with the signature `fn() -> &'static -/// ClassEntry`. -/// - `#[php(implements = ce)]` - Implements the given interface on the class. -/// Can be used multiple times. `ce` must be a valid function with the -/// signature `fn() -> &'static ClassEntry`. +/// - `rename` - Changes the case of the class name when exported to PHP. +/// - `#[php(extends(ce = ce_fn, stub = "ParentClass"))]` - Sets the parent +/// class of the class. Can only be used once. `ce_fn` must be a function with +/// the signature `fn() -> &'static ClassEntry`. +/// - `#[php(implements(ce = ce_fn, stub = "InterfaceName"))]` - Implements the +/// given interface on the class. Can be used multiple times. `ce_fn` must be +/// a valid function with the signature `fn() -> &'static ClassEntry`. /// /// You may also use the `#[php(prop)]` attribute on a struct field to use the /// field as a PHP property. By default, the field will be accessible from PHP @@ -122,8 +120,9 @@ extern crate proc_macro; /// zend::ce /// }; /// -/// #[php_class(name = "Redis\\Exception\\RedisException")] -/// #[php(extends = ce::exception)] +/// #[php_class] +/// #[php(name = "Redis\\Exception\\RedisException")] +/// #[php(extends(ce = ce::exception, stub = "\\Exception"))] /// #[derive(Default)] /// pub struct RedisException; /// @@ -144,8 +143,8 @@ extern crate proc_macro; /// /// ## Implementing an Interface /// -/// To implement an interface, use `#[php(implements = ce)]` where `ce` is an -/// function returning a `ClassEntry`. The following example implements [`ArrayAccess`](https://www.php.net/manual/en/class.arrayaccess.php): +/// To implement an interface, use `#[php(implements(ce = ce_fn, stub = +/// "InterfaceName")]` where `ce_fn` is an function returning a `ClassEntry`. The following example implements [`ArrayAccess`](https://www.php.net/manual/en/class.arrayaccess.php): /// /// ````rust,no_run,ignore /// # #![cfg_attr(windows, feature(abi_vectorcall))] @@ -158,7 +157,7 @@ extern crate proc_macro; /// }; /// /// #[php_class] -/// #[php(implements = ce::arrayaccess)] +/// #[php(implements(ce = ce::arrayaccess, stub = "\\ArrayAccess"))] /// #[derive(Default)] /// pub struct EvenNumbersArray; /// diff --git a/guide/src/macros/classes.md b/guide/src/macros/classes.md index 1e1f9d0eb0..008581af98 100644 --- a/guide/src/macros/classes.md +++ b/guide/src/macros/classes.md @@ -13,10 +13,10 @@ placed underneath the `#[php_class]` attribute. name is kept the same. If no name is given, the name of the struct is used. Useful for namespacing classes. - `rename` - Changes the case of the class name when exported to PHP. -- `#[php(extends = ce)]` - Sets the parent class of the class. Can only be used once. - `ce` must be a function with the signature `fn() -> &'static ClassEntry`. -- `#[php(implements = ce)]` - Implements the given interface on the class. Can be used - multiple times. `ce` must be a valid function with the signature +- `#[php(extends(ce = ce_fn, stub = "ParentClass"))]` - Sets the parent class of the class. Can only be used once. + `ce_fn` must be a function with the signature `fn() -> &'static ClassEntry`. +- `#[php(implements(ce = ce_fn, stub = "InterfaceName"))]` - Implements the given interface on the class. Can be used + multiple times. `ce_fn` must be a valid function with the signature `fn() -> &'static ClassEntry`. You may also use the `#[php(prop)]` attribute on a struct field to use the field as a @@ -97,7 +97,7 @@ use ext_php_rs::{ #[php_class] #[php(name = "Redis\\Exception\\RedisException")] -#[php(extends = ce::exception)] +#[php(extends(ce = ce::exception, stub = "\\Exception"))] #[derive(Default)] pub struct RedisException; @@ -118,7 +118,7 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { ## Implementing an Interface -To implement an interface, use `#[php(implements = ce)]` where `ce` is an function returning a `ClassEntry`. +To implement an interface, use `#[php(implements(ce = ce_fn, stub = "InterfaceName")]` where `ce_fn` is an function returning a `ClassEntry`. The following example implements [`ArrayAccess`](https://www.php.net/manual/en/class.arrayaccess.php): ````rust,no_run @@ -132,7 +132,7 @@ use ext_php_rs::{ }; #[php_class] -#[php(implements = ce::arrayaccess)] +#[php(implements(ce = ce::arrayaccess, stub = "\\ArrayAccess"))] #[derive(Default)] pub struct EvenNumbersArray; diff --git a/guide/src/migration-guides/v0.14.md b/guide/src/migration-guides/v0.14.md index cda917c6bd..8cd8f3151a 100644 --- a/guide/src/migration-guides/v0.14.md +++ b/guide/src/migration-guides/v0.14.md @@ -106,8 +106,8 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { - `#[php(name = "NEW_NAME")]` - Renames the class - `#[php(rename = case)]` - Changes the case of the class name - `#[php(vis = "public")]` - Changes the visibility of the class -- `#[php(extends = "ParentClass")]` - Extends a parent class -- `#[php(implements = "Interface")]` - Implements an interface +- `#[php(extends(ce = ce_fn, stub = "ParentClass")]` - Extends a parent class +- `#[php(implements(ce = ce_fn, stub = "Interface"))]` - Implements an interface - `#[php(prop)]` - Marks a field as a property **Supported `#[php]` attributes (`impl`):** @@ -116,6 +116,13 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { For elements in the `#[php_impl]` block see the respective function and constant attributes. +#### Extends and Implements + +Extends and implements are now taking a second parameter which is the +`stub` name. This is the name of the class or interface in PHP. + +This value is only used for stub generation and is not used for the class name in Rust. + ### Constants Mostly unchanged in terms of constant definition, however you now need to diff --git a/src/builders/class.rs b/src/builders/class.rs index 746c0bc1dc..ee5164e609 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -2,7 +2,7 @@ use std::{ffi::CString, mem::MaybeUninit, ptr, rc::Rc}; use crate::{ builders::FunctionBuilder, - class::{ConstructorMeta, ConstructorResult, RegisteredClass}, + class::{ClassEntryInfo, ConstructorMeta, ConstructorResult, RegisteredClass}, convert::{IntoZval, IntoZvalDyn}, describe::DocComments, error::{Error, Result}, @@ -24,8 +24,8 @@ type ConstantEntry = (String, Box Result>, DocComments); pub struct ClassBuilder { pub(crate) name: String, ce: ClassEntry, - extends: Option<&'static ClassEntry>, - interfaces: Vec<&'static ClassEntry>, + pub(crate) extends: Option, + pub(crate) interfaces: Vec, pub(crate) methods: Vec<(FunctionBuilder<'static>, MethodFlags)>, object_override: Option *mut ZendObject>, pub(crate) properties: Vec<(String, PropertyFlags, DocComments)>, @@ -63,7 +63,7 @@ impl ClassBuilder { /// # Parameters /// /// * `parent` - The parent class to extend. - pub fn extends(mut self, parent: &'static ClassEntry) -> Self { + pub fn extends(mut self, parent: ClassEntryInfo) -> Self { self.extends = Some(parent); self } @@ -77,11 +77,7 @@ impl ClassBuilder { /// # Panics /// /// Panics when the given class entry `interface` is not an interface. - pub fn implements(mut self, interface: &'static ClassEntry) -> Self { - assert!( - interface.is_interface(), - "Given class entry was not an interface." - ); + pub fn implements(mut self, interface: ClassEntryInfo) -> Self { self.interfaces.push(interface); self } @@ -309,7 +305,7 @@ impl ClassBuilder { zend_register_internal_class_ex( &mut self.ce, match self.extends { - Some(ptr) => ptr::from_ref(ptr).cast_mut(), + Some((ptr, _)) => ptr::from_ref(ptr()).cast_mut(), None => std::ptr::null_mut(), }, ) @@ -329,8 +325,14 @@ impl ClassBuilder { } } - for iface in self.interfaces { - unsafe { zend_do_implement_interface(class, ptr::from_ref(iface).cast_mut()) }; + for (iface, _) in self.interfaces { + let interface = iface(); + assert!( + interface.is_interface(), + "Given class entry was not an interface." + ); + + unsafe { zend_do_implement_interface(class, ptr::from_ref(interface).cast_mut()) }; } for (name, flags, _) in self.properties { diff --git a/src/builders/module.rs b/src/builders/module.rs index 78721f3a44..1119cdfa2c 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -179,11 +179,11 @@ impl ModuleBuilder<'_> { for (method, flags) in T::method_builders() { builder = builder.method(method, flags); } - if let Some(extends) = T::EXTENDS { - builder = builder.extends(extends()); + if let Some(parent) = T::EXTENDS { + builder = builder.extends(parent); } - for iface in T::IMPLEMENTS { - builder = builder.implements(iface()); + for interface in T::IMPLEMENTS { + builder = builder.implements(*interface); } for (name, value, docs) in T::constants() { builder = builder diff --git a/src/class.rs b/src/class.rs index ad7571ed31..42673ac8e5 100644 --- a/src/class.rs +++ b/src/class.rs @@ -18,6 +18,10 @@ use crate::{ zend::{ClassEntry, ExecuteData, ZendObjectHandlers}, }; +/// A type alias for a tuple containing a function pointer to a class entry +/// and a string representing the class name used in stubs. +pub type ClassEntryInfo = (fn() -> &'static ClassEntry, &'static str); + /// Implemented on Rust types which are exported to PHP. Allows users to get and /// set PHP properties on the object. pub trait RegisteredClass: Sized + 'static { @@ -29,10 +33,10 @@ pub trait RegisteredClass: Sized + 'static { const BUILDER_MODIFIER: Option ClassBuilder>; /// Parent class entry. Optional. - const EXTENDS: Option &'static ClassEntry>; + const EXTENDS: Option; /// Interfaces implemented by the class. - const IMPLEMENTS: &'static [fn() -> &'static ClassEntry]; + const IMPLEMENTS: &'static [ClassEntryInfo]; /// PHP flags applied to the class. const FLAGS: ClassFlags = ClassFlags::empty(); diff --git a/src/closure.rs b/src/closure.rs index b50205ae9d..10d4ab2279 100644 --- a/src/closure.rs +++ b/src/closure.rs @@ -5,14 +5,14 @@ use std::collections::HashMap; use crate::{ args::{Arg, ArgParser}, builders::{ClassBuilder, FunctionBuilder}, - class::{ClassMetadata, RegisteredClass}, + class::{ClassEntryInfo, ClassMetadata, RegisteredClass}, convert::{FromZval, IntoZval}, describe::DocComments, exception::PhpException, flags::{DataType, MethodFlags}, internal::property::PropertyInfo, types::Zval, - zend::{ClassEntry, ExecuteData}, + zend::ExecuteData, zend_fastcall, }; @@ -150,8 +150,8 @@ impl RegisteredClass for Closure { const CLASS_NAME: &'static str = "RustClosure"; const BUILDER_MODIFIER: Option ClassBuilder> = None; - const EXTENDS: Option &'static ClassEntry> = None; - const IMPLEMENTS: &'static [fn() -> &'static ClassEntry] = &[]; + const EXTENDS: Option = None; + const IMPLEMENTS: &'static [ClassEntryInfo] = &[]; fn get_metadata() -> &'static ClassMetadata { &CLOSURE_META diff --git a/src/describe/mod.rs b/src/describe/mod.rs index f0971fb53d..4ac4ebbd9b 100644 --- a/src/describe/mod.rs +++ b/src/describe/mod.rs @@ -185,8 +185,13 @@ impl From for Class { .collect::>() .into(), ), - extends: abi::Option::None, // TODO: Implement extends #326 - implements: vec![].into(), // TODO: Implement implements #326 + extends: val.extends.map(|(_, stub)| stub.into()).into(), + implements: val + .interfaces + .into_iter() + .map(|(_, stub)| stub.into()) + .collect::>() + .into(), properties: val .properties .into_iter() @@ -241,7 +246,7 @@ impl From<(String, PropertyFlags, DocComments)> for Property { // TODO: Implement nullable #376 let nullable = false; let docs = docs.into(); - println!("Property: {name:?}"); + Self { name: name.into(), docs, diff --git a/src/exception.rs b/src/exception.rs index 6ff7ab3763..13c4e0bc0f 100644 --- a/src/exception.rs +++ b/src/exception.rs @@ -82,6 +82,20 @@ impl PhpException { self.object = object; } + /// Builder function that sets the Zval object for the exception. + /// + /// Exceptions can be based of instantiated Zval objects when you are + /// throwing a custom exception with stateful properties. + /// + /// # Parameters + /// + /// * `object` - The Zval object. + #[must_use] + pub fn with_object(mut self, object: Zval) -> Self { + self.object = Some(object); + self + } + /// Throws the exception, returning nothing inside a result if successful /// and an error otherwise. /// @@ -213,7 +227,7 @@ pub fn throw_with_code(ex: &ClassEntry, code: i32, message: &str) -> Result<()> /// use crate::ext_php_rs::convert::IntoZval; /// /// #[php_class] -/// #[php(extends = ext_php_rs::zend::ce::exception)] +/// #[php(extends(ce = ext_php_rs::zend::ce::exception, stub = "\\Exception"))] /// pub struct JsException { /// #[php(prop, flags = ext_php_rs::flags::PropertyFlags::Public)] /// message: String, diff --git a/src/macros.rs b/src/macros.rs index ae295a7275..6c96e25908 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -200,7 +200,7 @@ macro_rules! throw { /// # Examples /// /// ``` -/// # use ext_php_rs::{convert::{IntoZval, FromZval, IntoZvalDyn}, types::{Zval, ZendObject}, class::{RegisteredClass, ConstructorMeta}, builders::{ClassBuilder, FunctionBuilder}, zend::ClassEntry, flags::{ClassFlags, MethodFlags}, internal::property::PropertyInfo, describe::DocComments}; +/// # use ext_php_rs::{convert::{IntoZval, FromZval, IntoZvalDyn}, types::{Zval, ZendObject}, class::{RegisteredClass, ConstructorMeta, ClassEntryInfo}, builders::{ClassBuilder, FunctionBuilder}, zend::ClassEntry, flags::{ClassFlags, MethodFlags}, internal::property::PropertyInfo, describe::DocComments}; /// use ext_php_rs::class_derives; /// /// struct Test { @@ -212,8 +212,8 @@ macro_rules! throw { /// const CLASS_NAME: &'static str = "Test"; /// /// const BUILDER_MODIFIER: Option ClassBuilder> = None; -/// const EXTENDS: Option &'static ClassEntry> = None; -/// const IMPLEMENTS: &'static [fn() -> &'static ClassEntry] = &[]; +/// const EXTENDS: Option = None; +/// const IMPLEMENTS: &'static [ClassEntryInfo] = &[]; /// const FLAGS: ClassFlags = ClassFlags::empty(); /// const DOC_COMMENTS: DocComments = &[]; /// diff --git a/tests/src/integration/class/class.php b/tests/src/integration/class/class.php index a6295f6e0e..48bdb945a4 100644 --- a/tests/src/integration/class/class.php +++ b/tests/src/integration/class/class.php @@ -1,5 +1,7 @@ throw $ex); +assert_exception_thrown(fn() => throw_exception()); + +$arrayAccess = new TestClassArrayAccess(); +assert_exception_thrown(fn() => $arrayAccess[0] = 'foo'); +assert_exception_thrown(fn() => $arrayAccess['foo']); +assert($arrayAccess[0] === true); +assert($arrayAccess[1] === false); diff --git a/tests/src/integration/class/mod.rs b/tests/src/integration/class/mod.rs index c94f5e7761..4d01af67ca 100644 --- a/tests/src/integration/class/mod.rs +++ b/tests/src/integration/class/mod.rs @@ -1,4 +1,5 @@ -use ext_php_rs::prelude::*; +#![allow(clippy::unused_self)] +use ext_php_rs::{convert::IntoZval, prelude::*, types::Zval, zend::ce}; #[php_class] pub struct TestClass { @@ -44,10 +45,87 @@ pub fn test_class(string: String, number: i32) -> TestClass { } } +#[php_class] +#[php(implements(ce = ce::arrayaccess, stub = "ArrayAccess"))] +pub struct TestClassArrayAccess {} + +#[php_impl] +impl TestClassArrayAccess { + pub fn __construct() -> Self { + Self {} + } + + // We need to use `Zval` because ArrayAccess needs $offset to be a `mixed` + pub fn offset_exists(&self, offset: &'_ Zval) -> bool { + offset.is_long() + } + pub fn offset_get(&self, offset: &'_ Zval) -> PhpResult { + let integer_offset = offset.long().ok_or("Expected integer offset")?; + Ok(integer_offset % 2 == 0) + } + pub fn offset_set(&mut self, _offset: &'_ Zval, _value: &'_ Zval) -> PhpResult { + Err("Setting values is not supported".into()) + } + pub fn offset_unset(&mut self, _offset: &'_ Zval) -> PhpResult { + Err("Setting values is not supported".into()) + } +} + +#[php_class] +#[php(extends(ce = ce::exception, stub = "\\Exception"))] +#[derive(Default)] +pub struct TestClassExtends; + +#[php_impl] +impl TestClassExtends { + pub fn __construct() -> Self { + Self {} + } +} + +#[php_function] +pub fn throw_exception() -> PhpResult { + Err( + PhpException::from_class::("Not good!".into()) + .with_object(TestClassExtends.into_zval(false)?), + ) +} + +#[php_class] +#[php(implements(ce = ce::arrayaccess, stub = "ArrayAccess"))] +#[php(extends(ce = ce::exception, stub = "\\Exception"))] +pub struct TestClassExtendsImpl; + +#[php_impl] +impl TestClassExtendsImpl { + pub fn __construct() -> Self { + Self {} + } + + // We need to use `Zval` because ArrayAccess needs $offset to be a `mixed` + pub fn offset_exists(&self, offset: &'_ Zval) -> bool { + offset.is_long() + } + pub fn offset_get(&self, offset: &'_ Zval) -> PhpResult { + let integer_offset = offset.long().ok_or("Expected integer offset")?; + Ok(integer_offset % 2 == 0) + } + pub fn offset_set(&mut self, _offset: &'_ Zval, _value: &'_ Zval) -> PhpResult { + Err("Setting values is not supported".into()) + } + pub fn offset_unset(&mut self, _offset: &'_ Zval) -> PhpResult { + Err("Setting values is not supported".into()) + } +} + pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { builder .class::() + .class::() + .class::() + .class::() .function(wrap_function!(test_class)) + .function(wrap_function!(throw_exception)) } #[cfg(test)] From 4211637d1cac3226b1567475c199dd03b8cf41ea Mon Sep 17 00:00:00 2001 From: Xenira <1288524+Xenira@users.noreply.github.com> Date: Fri, 9 May 2025 19:36:48 +0200 Subject: [PATCH 2/2] test(class): add extends and implements tests Refs: #326 --- src/args.rs | 270 ++++++++++++++++++++++++++++++++++++++++- src/builders/class.rs | 97 +++++++++++++++ src/builders/module.rs | 84 +++++++++++++ src/describe/abi.rs | 32 +++++ src/describe/mod.rs | 269 +++++++++++++++++++++++++++++++++++++--- src/exception.rs | 139 +++++++++++++++++++++ src/lib.rs | 2 + src/test/mod.rs | 32 +++++ src/zend/ce.rs | 159 ++++++++++++++++++++++++ 9 files changed, 1062 insertions(+), 22 deletions(-) create mode 100644 src/test/mod.rs diff --git a/src/args.rs b/src/args.rs index 211c8937de..832f7b8e2b 100644 --- a/src/args.rs +++ b/src/args.rs @@ -177,7 +177,7 @@ impl<'a> Arg<'a> { impl From> for _zend_expected_type { fn from(arg: Arg) -> Self { - let err = match arg.r#type { + let type_id = match arg.r#type { DataType::False | DataType::True => _zend_expected_type_Z_EXPECTED_BOOL, DataType::Long => _zend_expected_type_Z_EXPECTED_LONG, DataType::Double => _zend_expected_type_Z_EXPECTED_DOUBLE, @@ -189,9 +189,9 @@ impl From> for _zend_expected_type { }; if arg.allow_null { - err + 1 + type_id + 1 } else { - err + type_id } } } @@ -302,3 +302,267 @@ impl<'a, 'b> ArgParser<'a, 'b> { Ok(()) } } + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + + use super::*; + + #[test] + fn test_new() { + let arg = Arg::new("test", DataType::Long); + assert_eq!(arg.name, "test"); + assert_eq!(arg.r#type, DataType::Long); + assert!(!arg.as_ref); + assert!(!arg.allow_null); + assert!(!arg.variadic); + assert!(arg.default_value.is_none()); + assert!(arg.zval.is_none()); + assert!(arg.variadic_zvals.is_empty()); + } + + #[test] + fn test_as_ref() { + let arg = Arg::new("test", DataType::Long).as_ref(); + assert!(arg.as_ref); + } + + #[test] + fn test_is_variadic() { + let arg = Arg::new("test", DataType::Long).is_variadic(); + assert!(arg.variadic); + } + + #[test] + fn test_allow_null() { + let arg = Arg::new("test", DataType::Long).allow_null(); + assert!(arg.allow_null); + } + + #[test] + fn test_default() { + let arg = Arg::new("test", DataType::Long).default("default"); + assert_eq!(arg.default_value, Some("default".to_string())); + + // TODO: Validate type + } + + #[test] + fn test_consume_no_value() { + let arg = Arg::new("test", DataType::Long); + let result: Result = arg.consume(); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().name, "test"); + } + + #[test] + #[cfg(feature = "embed")] + fn test_consume() { + let mut arg = Arg::new("test", DataType::Long); + let mut zval = Zval::from(42); + arg.zval = Some(&mut zval); + + let result: Result = arg.consume(); + assert_eq!(result.unwrap(), 42); + } + + #[test] + fn test_val_no_value() { + let mut arg = Arg::new("test", DataType::Long); + let result: Option = arg.val(); + assert!(result.is_none()); + } + + #[test] + #[cfg(feature = "embed")] + fn test_val() { + let mut arg = Arg::new("test", DataType::Long); + let mut zval = Zval::from(42); + arg.zval = Some(&mut zval); + + let result: Option = arg.val(); + assert_eq!(result.unwrap(), 42); + } + + #[test] + #[cfg(feature = "embed")] + fn test_variadic_vals() { + let mut arg = Arg::new("test", DataType::Long).is_variadic(); + let mut zval1 = Zval::from(42); + let mut zval2 = Zval::from(43); + arg.variadic_zvals.push(Some(&mut zval1)); + arg.variadic_zvals.push(Some(&mut zval2)); + + let result: Vec = arg.variadic_vals(); + assert_eq!(result, vec![42, 43]); + } + + #[test] + fn test_zval_no_value() { + let mut arg = Arg::new("test", DataType::Long); + let result = arg.zval(); + assert!(result.is_none()); + } + + #[test] + #[cfg(feature = "embed")] + fn test_zval() { + let mut arg = Arg::new("test", DataType::Long); + let mut zval = Zval::from(42); + arg.zval = Some(&mut zval); + + let result = arg.zval(); + assert!(result.is_some()); + assert_eq!(result.unwrap().dereference_mut().long(), Some(42)); + } + + #[test] + fn test_try_call_no_value() { + let arg = Arg::new("test", DataType::Long); + let result = arg.try_call(vec![]); + assert!(result.is_err()); + } + + #[test] + #[cfg(feature = "embed")] + fn test_try_call_not_callable() { + let mut arg = Arg::new("test", DataType::Long); + let mut zval = Zval::from(42); + arg.zval = Some(&mut zval); + + let result = arg.try_call(vec![]); + assert!(result.is_err()); + } + + // TODO: Test the callable case + + #[test] + #[cfg(feature = "embed")] + fn test_as_arg_info() { + let arg = Arg::new("test", DataType::Long); + let arg_info = arg.as_arg_info(); + assert!(arg_info.is_ok()); + + let arg_info = arg_info.unwrap(); + assert!(arg_info.default_value.is_null()); + + let r#type = arg_info.type_; + assert_eq!(r#type.type_mask, 16); + } + + #[test] + #[cfg(feature = "embed")] + fn test_as_arg_info_with_default() { + let arg = Arg::new("test", DataType::Long).default("default"); + let arg_info = arg.as_arg_info(); + assert!(arg_info.is_ok()); + + let arg_info = arg_info.unwrap(); + assert!(!arg_info.default_value.is_null()); + + let r#type = arg_info.type_; + assert_eq!(r#type.type_mask, 16); + } + + #[test] + fn test_type_from_arg() { + let arg = Arg::new("test", DataType::Long); + let actual: _zend_expected_type = arg.into(); + assert_eq!(actual, 0); + + let arg = Arg::new("test", DataType::Long).allow_null(); + let actual: _zend_expected_type = arg.into(); + assert_eq!(actual, 1); + + let arg = Arg::new("test", DataType::False); + let actual: _zend_expected_type = arg.into(); + assert_eq!(actual, 2); + + let arg = Arg::new("test", DataType::False).allow_null(); + let actual: _zend_expected_type = arg.into(); + assert_eq!(actual, 3); + + let arg = Arg::new("test", DataType::True); + let actual: _zend_expected_type = arg.into(); + assert_eq!(actual, 2); + + let arg = Arg::new("test", DataType::True).allow_null(); + let actual: _zend_expected_type = arg.into(); + assert_eq!(actual, 3); + + let arg = Arg::new("test", DataType::String); + let actual: _zend_expected_type = arg.into(); + assert_eq!(actual, 4); + + let arg = Arg::new("test", DataType::String).allow_null(); + let actual: _zend_expected_type = arg.into(); + assert_eq!(actual, 5); + + let arg = Arg::new("test", DataType::Array); + let actual: _zend_expected_type = arg.into(); + assert_eq!(actual, 6); + + let arg = Arg::new("test", DataType::Array).allow_null(); + let actual: _zend_expected_type = arg.into(); + assert_eq!(actual, 7); + + let arg = Arg::new("test", DataType::Resource); + let actual: _zend_expected_type = arg.into(); + assert_eq!(actual, 14); + + let arg = Arg::new("test", DataType::Resource).allow_null(); + let actual: _zend_expected_type = arg.into(); + assert_eq!(actual, 15); + + let arg = Arg::new("test", DataType::Object(None)); + let actual: _zend_expected_type = arg.into(); + assert_eq!(actual, 18); + + let arg = Arg::new("test", DataType::Object(None)).allow_null(); + let actual: _zend_expected_type = arg.into(); + assert_eq!(actual, 19); + + let arg = Arg::new("test", DataType::Double); + let actual: _zend_expected_type = arg.into(); + assert_eq!(actual, 20); + + let arg = Arg::new("test", DataType::Double).allow_null(); + let actual: _zend_expected_type = arg.into(); + assert_eq!(actual, 21); + } + + #[test] + fn test_param_from_arg() { + let arg = Arg::new("test", DataType::Long) + .default("default") + .allow_null(); + let param: Parameter = arg.into(); + assert_eq!(param.name, "test".into()); + assert_eq!(param.ty, abi::Option::Some(DataType::Long)); + assert!(param.nullable); + assert_eq!(param.default, abi::Option::Some("default".into())); + } + + #[test] + fn test_arg_parser_new() { + let arg_zvals = vec![None, None]; + let parser = ArgParser::new(arg_zvals); + assert_eq!(parser.arg_zvals.len(), 2); + assert!(parser.args.is_empty()); + assert!(parser.min_num_args.is_none()); + } + + #[test] + fn test_arg_parser_arg() { + let arg_zvals = vec![None, None]; + let mut parser = ArgParser::new(arg_zvals); + let mut arg = Arg::new("test", DataType::Long); + parser = parser.arg(&mut arg); + assert_eq!(parser.args.len(), 1); + assert_eq!(parser.args[0].name, "test"); + assert_eq!(parser.args[0].r#type, DataType::Long); + } + + // TODO: test parse +} diff --git a/src/builders/class.rs b/src/builders/class.rs index ee5164e609..5ff1882055 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -372,3 +372,100 @@ impl ClassBuilder { Ok(()) } } + +#[cfg(test)] +mod tests { + use crate::test::test_function; + + use super::*; + + #[test] + fn test_new() { + let class = ClassBuilder::new("Foo"); + assert_eq!(class.name, "Foo"); + assert_eq!(class.extends, None); + assert_eq!(class.interfaces, vec![]); + assert_eq!(class.methods.len(), 0); + assert_eq!(class.object_override, None); + assert_eq!(class.properties, vec![]); + assert_eq!(class.constants.len(), 0); + assert_eq!(class.register, None); + assert_eq!(class.docs, &[] as DocComments); + } + + #[test] + fn test_extends() { + let extends: ClassEntryInfo = (|| todo!(), "Bar"); + let class = ClassBuilder::new("Foo").extends(extends); + assert_eq!(class.extends, Some(extends)); + } + + #[test] + fn test_implements() { + let implements: ClassEntryInfo = (|| todo!(), "Bar"); + let class = ClassBuilder::new("Foo").implements(implements); + assert_eq!(class.interfaces, vec![implements]); + } + + #[test] + fn test_method() { + let method = FunctionBuilder::new("foo", test_function); + let class = ClassBuilder::new("Foo").method(method, MethodFlags::Public); + assert_eq!(class.methods.len(), 1); + } + + #[test] + fn test_property() { + let class = ClassBuilder::new("Foo").property("bar", PropertyFlags::Public, &["Doc 1"]); + assert_eq!( + class.properties, + vec![( + "bar".to_string(), + PropertyFlags::Public, + &["Doc 1"] as DocComments + )] + ); + } + + #[test] + #[cfg(feature = "embed")] + fn test_constant() { + let class = ClassBuilder::new("Foo") + .constant("bar", 42, &["Doc 1"]) + .expect("Failed to create constant"); + assert_eq!(class.constants.len(), 1); + assert_eq!(class.constants[0].0, "bar"); + assert_eq!(class.constants[0].2, &["Doc 1"] as DocComments); + } + + #[test] + #[cfg(feature = "embed")] + fn test_dyn_constant() { + let class = ClassBuilder::new("Foo") + .dyn_constant("bar", &42, &["Doc 1"]) + .expect("Failed to create constant"); + assert_eq!(class.constants.len(), 1); + assert_eq!(class.constants[0].0, "bar"); + assert_eq!(class.constants[0].2, &["Doc 1"] as DocComments); + } + + #[test] + fn test_flags() { + let class = ClassBuilder::new("Foo").flags(ClassFlags::Abstract); + assert_eq!(class.ce.ce_flags, ClassFlags::Abstract.bits()); + } + + #[test] + fn test_registration() { + let class = ClassBuilder::new("Foo").registration(|_| {}); + assert!(class.register.is_some()); + } + + #[test] + fn test_docs() { + let class = ClassBuilder::new("Foo").docs(&["Doc 1"]); + assert_eq!(class.docs, &["Doc 1"] as DocComments); + } + + // TODO: Test the register function +} diff --git a/src/builders/module.rs b/src/builders/module.rs index 1119cdfa2c..4e5b2d8ee9 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -304,3 +304,87 @@ impl TryFrom> for (ModuleEntry, ModuleStartup) { )) } } + +#[cfg(test)] +mod tests { + use crate::test::{ + test_deactivate_function, test_function, test_info_function, test_startup_shutdown_function, + }; + + use super::*; + + #[test] + fn test_new() { + let builder = ModuleBuilder::new("test", "1.0"); + assert_eq!(builder.name, "test"); + assert_eq!(builder.version, "1.0"); + assert!(builder.functions.is_empty()); + assert!(builder.constants.is_empty()); + assert!(builder.classes.is_empty()); + assert!(builder.startup_func.is_none()); + assert!(builder.shutdown_func.is_none()); + assert!(builder.request_startup_func.is_none()); + assert!(builder.request_shutdown_func.is_none()); + assert!(builder.post_deactivate_func.is_none()); + assert!(builder.info_func.is_none()); + } + + #[test] + fn test_startup_function() { + let builder = + ModuleBuilder::new("test", "1.0").startup_function(test_startup_shutdown_function); + assert!(builder.startup_func.is_some()); + } + + #[test] + fn test_shutdown_function() { + let builder = + ModuleBuilder::new("test", "1.0").shutdown_function(test_startup_shutdown_function); + assert!(builder.shutdown_func.is_some()); + } + + #[test] + fn test_request_startup_function() { + let builder = ModuleBuilder::new("test", "1.0") + .request_startup_function(test_startup_shutdown_function); + assert!(builder.request_startup_func.is_some()); + } + + #[test] + fn test_request_shutdown_function() { + let builder = ModuleBuilder::new("test", "1.0") + .request_shutdown_function(test_startup_shutdown_function); + assert!(builder.request_shutdown_func.is_some()); + } + + #[test] + fn test_set_post_deactivate_function() { + let builder = + ModuleBuilder::new("test", "1.0").post_deactivate_function(test_deactivate_function); + assert!(builder.post_deactivate_func.is_some()); + } + + #[test] + fn test_set_info_function() { + let builder = ModuleBuilder::new("test", "1.0").info_function(test_info_function); + assert!(builder.info_func.is_some()); + } + + #[test] + fn test_add_function() { + let builder = + ModuleBuilder::new("test", "1.0").function(FunctionBuilder::new("test", test_function)); + assert_eq!(builder.functions.len(), 1); + } + + #[test] + #[cfg(feature = "embed")] + fn test_add_constant() { + let builder = + ModuleBuilder::new("test", "1.0").constant(("TEST_CONST", 42, DocComments::default())); + assert_eq!(builder.constants.len(), 1); + assert_eq!(builder.constants[0].0, "TEST_CONST"); + // TODO: Check if the value is 42 + assert_eq!(builder.constants[0].2, DocComments::default()); + } +} diff --git a/src/describe/abi.rs b/src/describe/abi.rs index dcf6519b1b..9134212157 100644 --- a/src/describe/abi.rs +++ b/src/describe/abi.rs @@ -17,6 +17,7 @@ use std::{fmt::Display, ops::Deref, vec::Vec as StdVec}; /// An immutable, ABI-stable [`Vec`][std::vec::Vec]. #[repr(C)] +#[derive(Debug)] pub struct Vec { ptr: *mut T, len: usize, @@ -48,8 +49,18 @@ impl From> for Vec { } } +impl PartialEq for Vec +where + T: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.len == other.len && self.as_ref() == other.as_ref() + } +} + /// An immutable, ABI-stable borrowed [`&'static str`][str]. #[repr(C)] +#[derive(Debug)] pub struct Str { ptr: *const u8, len: usize, @@ -86,8 +97,15 @@ impl Display for Str { } } +impl PartialEq for Str { + fn eq(&self, other: &Self) -> bool { + self.len == other.len && self.str() == other.str() + } +} + /// An ABI-stable String #[repr(C)] +#[derive(Debug, PartialEq)] pub struct RString { inner: Vec, } @@ -134,6 +152,7 @@ impl Display for RString { /// An ABI-stable [`Option`][std::option::Option]. #[repr(C, u8)] +#[derive(Debug)] pub enum Option { /// [`Option::Some`][std::option::Option::Some] variant. Some(T), @@ -149,3 +168,16 @@ impl From> for Option { } } } + +impl PartialEq for Option +where + T: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Some(a), Self::Some(b)) => a == b, + (Self::None, Self::None) => true, + _ => false, + } + } +} diff --git a/src/describe/mod.rs b/src/describe/mod.rs index 4ac4ebbd9b..8abf2592e1 100644 --- a/src/describe/mod.rs +++ b/src/describe/mod.rs @@ -1,6 +1,5 @@ //! Types used to describe downstream extensions. Used by the `cargo-php` //! CLI application to generate PHP stub files used by IDEs. -use bitflags::bitflags_match; use std::vec::Vec as StdVec; use crate::{ @@ -45,6 +44,7 @@ impl Description { /// Represents a set of comments on an export. #[repr(C)] +#[derive(Debug, PartialEq)] pub struct DocBlock(pub Vec); impl From<&'static [&'static str]> for DocBlock { @@ -143,6 +143,7 @@ impl From> for Function { /// Represents a parameter attached to an exported function or method. #[repr(C)] +#[derive(Debug, PartialEq)] pub struct Parameter { /// Name of the parameter. pub name: RString, @@ -217,6 +218,7 @@ impl From for Class { /// Represents a property attached to an exported class. #[repr(C)] +#[derive(Debug, PartialEq)] pub struct Property { /// Name of the property. pub name: RString, @@ -261,6 +263,7 @@ impl From<(String, PropertyFlags, DocComments)> for Property { /// Represents a method attached to an exported class. #[repr(C)] +#[derive(Debug, PartialEq)] pub struct Method { /// Name of the method. pub name: RString, @@ -314,6 +317,7 @@ impl From<(FunctionBuilder<'_>, MethodFlags)> for Method { /// Represents a value returned from a function or method. #[repr(C)] +#[derive(Debug, PartialEq)] pub struct Retval { /// Type of the return value. pub ty: DataType, @@ -323,7 +327,7 @@ pub struct Retval { /// Enumerator used to differentiate between methods. #[repr(C)] -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum MethodType { /// A member method. Member, @@ -335,18 +339,21 @@ pub enum MethodType { impl From for MethodType { fn from(value: MethodFlags) -> Self { - match value { - MethodFlags::Static => Self::Static, - MethodFlags::IsConstructor => Self::Constructor, - _ => Self::Member, + if value.contains(MethodFlags::IsConstructor) { + return Self::Constructor; } + if value.contains(MethodFlags::Static) { + return Self::Static; + } + + Self::Member } } /// Enumerator used to differentiate between different method and property /// visibilties. #[repr(C)] -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum Visibility { /// Private visibility. Private, @@ -358,23 +365,28 @@ pub enum Visibility { impl From for Visibility { fn from(value: PropertyFlags) -> Self { - bitflags_match!(value, { - PropertyFlags::Public => Visibility::Public, - PropertyFlags::Protected => Visibility::Protected, - PropertyFlags::Private => Visibility::Private, - _ => Visibility::Public, - }) + if value.contains(PropertyFlags::Protected) { + return Self::Protected; + } + if value.contains(PropertyFlags::Private) { + return Self::Private; + } + + Self::Public } } impl From for Visibility { fn from(value: MethodFlags) -> Self { - bitflags_match!(value, { - MethodFlags::Public => Self::Public, - MethodFlags::Protected => Self::Protected, - MethodFlags::Private => Self::Private, - _ => Self::Public, - }) + if value.contains(MethodFlags::Protected) { + return Self::Protected; + } + + if value.contains(MethodFlags::Private) { + return Self::Private; + } + + Self::Public } } @@ -410,3 +422,222 @@ impl From<(String, Box, DocComments)> for Constant { } } } + +#[cfg(test)] +mod tests { + #![cfg_attr(windows, feature(abi_vectorcall))] + use super::*; + + use crate::{args::Arg, test::test_function}; + + #[test] + fn test_new_description() { + let module = Module { + name: "test".into(), + functions: vec![].into(), + classes: vec![].into(), + constants: vec![].into(), + }; + + let description = Description::new(module); + assert_eq!(description.version, crate::VERSION); + assert_eq!(description.module.name, "test".into()); + } + + #[test] + fn test_doc_block_from() { + let docs: &'static [&'static str] = &["doc1", "doc2"]; + let docs: DocBlock = docs.into(); + assert_eq!(docs.0.len(), 2); + assert_eq!(docs.0[0], "doc1".into()); + assert_eq!(docs.0[1], "doc2".into()); + } + + #[test] + fn test_module_from() { + let builder = ModuleBuilder::new("test", "test_version") + .function(FunctionBuilder::new("test_function", test_function)); + let module: Module = builder.into(); + assert_eq!(module.name, "test".into()); + assert_eq!(module.functions.len(), 1); + assert_eq!(module.classes.len(), 0); + assert_eq!(module.constants.len(), 0); + } + + #[test] + fn test_function_from() { + let builder = FunctionBuilder::new("test_function", test_function) + .docs(&["doc1", "doc2"]) + .arg(Arg::new("foo", DataType::Long)) + .returns(DataType::Bool, true, true); + let function: Function = builder.into(); + assert_eq!(function.name, "test_function".into()); + assert_eq!(function.docs.0.len(), 2); + assert_eq!( + function.params, + vec![Parameter { + name: "foo".into(), + ty: Option::Some(DataType::Long), + nullable: false, + default: Option::None, + }] + .into() + ); + assert_eq!( + function.ret, + Option::Some(Retval { + ty: DataType::Bool, + nullable: true, + }) + ); + } + + #[test] + fn test_class_from() { + let builder = ClassBuilder::new("TestClass") + .docs(&["doc1", "doc2"]) + .extends((|| todo!(), "BaseClass")) + .implements((|| todo!(), "Interface1")) + .implements((|| todo!(), "Interface2")) + .property("prop1", PropertyFlags::Public, &["doc1"]) + .method( + FunctionBuilder::new("test_function", test_function), + MethodFlags::Protected, + ); + let class: Class = builder.into(); + + assert_eq!(class.name, "TestClass".into()); + assert_eq!(class.docs.0.len(), 2); + assert_eq!(class.extends, Option::Some("BaseClass".into())); + assert_eq!( + class.implements, + vec!["Interface1".into(), "Interface2".into()].into() + ); + assert_eq!(class.properties.len(), 1); + assert_eq!( + class.properties[0], + Property { + name: "prop1".into(), + docs: DocBlock(vec!["doc1".into()].into()), + ty: Option::None, + vis: Visibility::Public, + static_: false, + nullable: false, + default: Option::None, + } + ); + assert_eq!(class.methods.len(), 1); + assert_eq!( + class.methods[0], + Method { + name: "test_function".into(), + docs: DocBlock(vec![].into()), + ty: MethodType::Member, + params: vec![].into(), + retval: Option::None, + r#static: false, + visibility: Visibility::Protected, + } + ); + } + + #[test] + fn test_property_from() { + let docs: &'static [&'static str] = &["doc1", "doc2"]; + let property: Property = + ("test_property".to_string(), PropertyFlags::Protected, docs).into(); + assert_eq!(property.name, "test_property".into()); + assert_eq!(property.docs.0.len(), 2); + assert_eq!(property.vis, Visibility::Protected); + assert!(!property.static_); + assert!(!property.nullable); + } + + #[test] + fn test_method_from() { + let builder = FunctionBuilder::new("test_method", test_function) + .docs(&["doc1", "doc2"]) + .arg(Arg::new("foo", DataType::Long)) + .returns(DataType::Bool, true, true); + let method: Method = (builder, MethodFlags::Static | MethodFlags::Protected).into(); + assert_eq!(method.name, "test_method".into()); + assert_eq!(method.docs.0.len(), 2); + assert_eq!( + method.params, + vec![Parameter { + name: "foo".into(), + ty: Option::Some(DataType::Long), + nullable: false, + default: Option::None, + }] + .into() + ); + assert_eq!( + method.retval, + Option::Some(Retval { + ty: DataType::Bool, + nullable: true, + }) + ); + assert!(method.r#static); + assert_eq!(method.visibility, Visibility::Protected); + assert_eq!(method.ty, MethodType::Static); + } + + #[test] + fn test_ty_from() { + let r#static: MethodType = MethodFlags::Static.into(); + assert_eq!(r#static, MethodType::Static); + + let constructor: MethodType = MethodFlags::IsConstructor.into(); + assert_eq!(constructor, MethodType::Constructor); + + let member: MethodType = MethodFlags::Public.into(); + assert_eq!(member, MethodType::Member); + + let mixed: MethodType = (MethodFlags::Protected | MethodFlags::Static).into(); + assert_eq!(mixed, MethodType::Static); + + let both: MethodType = (MethodFlags::Static | MethodFlags::IsConstructor).into(); + assert_eq!(both, MethodType::Constructor); + + let empty: MethodType = MethodFlags::empty().into(); + assert_eq!(empty, MethodType::Member); + } + + #[test] + fn test_prop_visibility_from() { + let private: Visibility = PropertyFlags::Private.into(); + assert_eq!(private, Visibility::Private); + + let protected: Visibility = PropertyFlags::Protected.into(); + assert_eq!(protected, Visibility::Protected); + + let public: Visibility = PropertyFlags::Public.into(); + assert_eq!(public, Visibility::Public); + + let mixed: Visibility = (PropertyFlags::Protected | PropertyFlags::Static).into(); + assert_eq!(mixed, Visibility::Protected); + + let empty: Visibility = PropertyFlags::empty().into(); + assert_eq!(empty, Visibility::Public); + } + + #[test] + fn test_method_visibility_from() { + let private: Visibility = MethodFlags::Private.into(); + assert_eq!(private, Visibility::Private); + + let protected: Visibility = MethodFlags::Protected.into(); + assert_eq!(protected, Visibility::Protected); + + let public: Visibility = MethodFlags::Public.into(); + assert_eq!(public, Visibility::Public); + + let mixed: Visibility = (MethodFlags::Protected | MethodFlags::Static).into(); + assert_eq!(mixed, Visibility::Protected); + + let empty: Visibility = MethodFlags::empty().into(); + assert_eq!(empty, Visibility::Public); + } +} diff --git a/src/exception.rs b/src/exception.rs index 13c4e0bc0f..23b2a07865 100644 --- a/src/exception.rs +++ b/src/exception.rs @@ -250,3 +250,142 @@ pub fn throw_object(zval: Zval) -> Result<()> { unsafe { zend_throw_exception_object(core::ptr::addr_of_mut!(zv).cast()) }; Ok(()) } + +#[cfg(feature = "embed")] +#[cfg(test)] +mod tests { + #![allow(clippy::assertions_on_constants)] + use super::*; + use crate::embed::Embed; + + #[test] + fn test_new() { + Embed::run(|| { + let ex = PhpException::new("Test".into(), 0, ce::exception()); + assert_eq!(ex.message, "Test"); + assert_eq!(ex.code, 0); + assert_eq!(ex.ex, ce::exception()); + assert!(ex.object.is_none()); + }); + } + + #[test] + fn test_default() { + Embed::run(|| { + let ex = PhpException::default("Test".into()); + assert_eq!(ex.message, "Test"); + assert_eq!(ex.code, 0); + assert_eq!(ex.ex, ce::exception()); + assert!(ex.object.is_none()); + }); + } + + #[test] + fn test_set_object() { + Embed::run(|| { + let mut ex = PhpException::default("Test".into()); + assert!(ex.object.is_none()); + let obj = Zval::new(); + ex.set_object(Some(obj)); + assert!(ex.object.is_some()); + }); + } + + #[test] + fn test_with_object() { + Embed::run(|| { + let obj = Zval::new(); + let ex = PhpException::default("Test".into()).with_object(obj); + assert!(ex.object.is_some()); + }); + } + + #[test] + fn test_throw_code() { + Embed::run(|| { + let ex = PhpException::default("Test".into()); + assert!(ex.throw().is_ok()); + + assert!(false, "Should not reach here"); + }); + } + + #[test] + fn test_throw_object() { + Embed::run(|| { + let ex = PhpException::default("Test".into()).with_object(Zval::new()); + assert!(ex.throw().is_ok()); + + assert!(false, "Should not reach here"); + }); + } + + #[test] + fn test_from_string() { + Embed::run(|| { + let ex: PhpException = "Test".to_string().into(); + assert_eq!(ex.message, "Test"); + assert_eq!(ex.code, 0); + assert_eq!(ex.ex, ce::exception()); + assert!(ex.object.is_none()); + }); + } + + #[test] + fn test_from_str() { + Embed::run(|| { + let ex: PhpException = "Test str".into(); + assert_eq!(ex.message, "Test str"); + assert_eq!(ex.code, 0); + assert_eq!(ex.ex, ce::exception()); + assert!(ex.object.is_none()); + }); + } + + #[test] + fn test_from_anyhow() { + Embed::run(|| { + let ex: PhpException = anyhow::anyhow!("Test anyhow").into(); + assert_eq!(ex.message, "Test anyhow"); + assert_eq!(ex.code, 0); + assert_eq!(ex.ex, ce::exception()); + assert!(ex.object.is_none()); + }); + } + + #[test] + fn test_throw_ex() { + Embed::run(|| { + assert!(throw(ce::exception(), "Test").is_ok()); + + assert!(false, "Should not reach here"); + }); + } + + #[test] + fn test_throw_with_code() { + Embed::run(|| { + assert!(throw_with_code(ce::exception(), 1, "Test").is_ok()); + + assert!(false, "Should not reach here"); + }); + } + + // TODO: Test abstract class + #[test] + fn test_throw_with_code_interface() { + Embed::run(|| { + assert!(throw_with_code(ce::arrayaccess(), 0, "Test").is_err()); + }); + } + + #[test] + fn test_static_throw_object() { + Embed::run(|| { + let obj = Zval::new(); + assert!(throw_object(obj).is_ok()); + + assert!(false, "Should not reach here"); + }); + } +} diff --git a/src/lib.rs b/src/lib.rs index 8af7fdd333..4b44d6aed8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,8 @@ pub mod embed; pub mod internal; pub mod props; pub mod rc; +#[cfg(test)] +pub mod test; pub mod types; pub mod zend; diff --git a/src/test/mod.rs b/src/test/mod.rs new file mode 100644 index 0000000000..8f55250bea --- /dev/null +++ b/src/test/mod.rs @@ -0,0 +1,32 @@ +//! Utility functions for testing +#![allow(clippy::must_use_candidate)] +use crate::{ffi::_zend_execute_data, types::Zval, zend::ModuleEntry}; + +/// Dummy function for testing +#[cfg(not(windows))] +pub extern "C" fn test_function(_: &mut _zend_execute_data, _: &mut Zval) { + // Dummy function for testing +} + +/// Dummy function for testing on windows +#[cfg(windows)] +pub extern "vectorcall" fn test_function(_: &mut _zend_execute_data, _: &mut Zval) { + // Dummy function for testing +} + +/// Dummy function for testing +pub extern "C" fn test_startup_shutdown_function(_type: i32, _module_number: i32) -> i32 { + // Dummy function for testing + 0 +} + +/// Dummy function for testing +pub extern "C" fn test_info_function(_zend_module: *mut ModuleEntry) { + // Dummy function for testing +} + +/// Dummy function for testing +pub extern "C" fn test_deactivate_function() -> i32 { + // Dummy function for testing + 0 +} diff --git a/src/zend/ce.rs b/src/zend/ce.rs index 655819f3d1..34ea133631 100644 --- a/src/zend/ce.rs +++ b/src/zend/ce.rs @@ -183,3 +183,162 @@ pub fn countable() -> &'static ClassEntry { pub fn stringable() -> &'static ClassEntry { unsafe { zend_ce_stringable.as_ref() }.unwrap() } + +#[cfg(test)] +#[cfg(feature = "embed")] +mod tests { + use super::*; + use crate::embed::Embed; + + #[test] + fn test_stdclass() { + Embed::run(|| { + let stdclass = stdclass(); + assert_eq!(stdclass.name(), Some("stdClass")); + }); + } + + #[test] + fn test_throwable() { + Embed::run(|| { + let throwable = throwable(); + assert_eq!(throwable.name(), Some("Throwable")); + }); + } + + #[test] + fn test_exception() { + Embed::run(|| { + let exception = exception(); + assert_eq!(exception.name(), Some("Exception")); + }); + } + + #[test] + fn test_error_exception() { + Embed::run(|| { + let error_exception = error_exception(); + assert_eq!(error_exception.name(), Some("ErrorException")); + }); + } + + #[test] + fn test_compile_error() { + Embed::run(|| { + let compile_error = compile_error(); + assert_eq!(compile_error.name(), Some("CompileError")); + }); + } + + #[test] + fn test_parse_error() { + Embed::run(|| { + let parse_error = parse_error(); + assert_eq!(parse_error.name(), Some("ParseError")); + }); + } + + #[test] + fn test_type_error() { + Embed::run(|| { + let type_error = type_error(); + assert_eq!(type_error.name(), Some("TypeError")); + }); + } + + #[test] + fn test_argument_count_error() { + Embed::run(|| { + let argument_count_error = argument_count_error(); + assert_eq!(argument_count_error.name(), Some("ArgumentCountError")); + }); + } + + #[test] + fn test_value_error() { + Embed::run(|| { + let value_error = value_error(); + assert_eq!(value_error.name(), Some("ValueError")); + }); + } + + #[test] + fn test_arithmetic_error() { + Embed::run(|| { + let arithmetic_error = arithmetic_error(); + assert_eq!(arithmetic_error.name(), Some("ArithmeticError")); + }); + } + + #[test] + fn test_division_by_zero_error() { + Embed::run(|| { + let division_by_zero_error = division_by_zero_error(); + assert_eq!(division_by_zero_error.name(), Some("DivisionByZeroError")); + }); + } + + #[test] + fn test_unhandled_match_error() { + Embed::run(|| { + let unhandled_match_error = unhandled_match_error(); + assert_eq!(unhandled_match_error.name(), Some("UnhandledMatchError")); + }); + } + + #[test] + fn test_traversable() { + Embed::run(|| { + let traversable = traversable(); + assert_eq!(traversable.name(), Some("Traversable")); + }); + } + + #[test] + fn test_aggregate() { + Embed::run(|| { + let aggregate = aggregate(); + assert_eq!(aggregate.name(), Some("IteratorAggregate")); + }); + } + + #[test] + fn test_iterator() { + Embed::run(|| { + let iterator = iterator(); + assert_eq!(iterator.name(), Some("Iterator")); + }); + } + + #[test] + fn test_arrayaccess() { + Embed::run(|| { + let arrayaccess = arrayaccess(); + assert_eq!(arrayaccess.name(), Some("ArrayAccess")); + }); + } + + #[test] + fn test_serializable() { + Embed::run(|| { + let serializable = serializable(); + assert_eq!(serializable.name(), Some("Serializable")); + }); + } + + #[test] + fn test_countable() { + Embed::run(|| { + let countable = countable(); + assert_eq!(countable.name(), Some("Countable")); + }); + } + + #[test] + fn test_stringable() { + Embed::run(|| { + let stringable = stringable(); + assert_eq!(stringable.name(), Some("Stringable")); + }); + } +}