tests and optional arguments

This commit is contained in:
Robin Appelman 2019-04-03 23:10:45 +02:00
commit b8cd60fff5
10 changed files with 356 additions and 37 deletions

View file

@ -53,11 +53,11 @@ fn export_fn(item: ItemFn) -> TokenStream {
let arg_ident = Ident::new(name, span.clone()); let arg_ident = Ident::new(name, span.clone());
quote!( quote!(
let #arg_ident: #ty = { let #arg_ident: #ty = {
let opt: Option<#ty> = args.next().unwrap().into(); let result: Result<#ty, ::ivory::CastError> = args.next().unwrap().into();
match opt { match result {
Some(val) => val, Ok(val) => val,
None => { Err(err) => {
::ivory::externs::error(::ivory::externs::ErrorLevel::Error, "invalid argument type,"); ::ivory::externs::error(::ivory::externs::ErrorLevel::Error, format!("{}", err));
return; return;
} }
} }
@ -69,7 +69,9 @@ fn export_fn(item: ItemFn) -> TokenStream {
#[no_mangle] #[no_mangle]
pub extern "C" fn #name(data: *const ::ivory::zend::ExecuteData, retval: *mut ::ivory::zend::ZVal) { pub extern "C" fn #name(data: *const ::ivory::zend::ExecuteData, retval: *mut ::ivory::zend::ZVal) {
let data: &::ivory::zend::ExecuteData = unsafe { data.as_ref() }.unwrap(); let data: &::ivory::zend::ExecuteData = unsafe { data.as_ref() }.unwrap();
if data.num_args() != #arg_count { // the less than case is handled during argument casting
// this is needed for optional arguments
if data.num_args() > #arg_count {
::ivory::externs::error(::ivory::externs::ErrorLevel::Error, format!("unexpected number of arguments, expected {}, got {}", #arg_count, data.num_args())); ::ivory::externs::error(::ivory::externs::ErrorLevel::Error, format!("unexpected number of arguments, expected {}, got {}", #arg_count, data.num_args()));
return; return;
} }

View file

@ -4,4 +4,5 @@ pub mod macros;
pub mod externs; pub mod externs;
pub mod info; pub mod info;
pub mod zend; pub mod zend;
pub use crate::zend::{ArgError, ArrayKey, CastError, PhpVal};
pub use ivory_macro::{ivory_export, ivory_module}; pub use ivory_macro::{ivory_export, ivory_module};

View file

@ -1,7 +1,7 @@
pub use self::module::*;
pub use self::function::*; pub use self::function::*;
pub use self::zval::{ExecuteData, ZVal, PhpVal}; pub use self::module::*;
pub use self::zval::{ArgError, ArrayKey, CastError, ExecuteData, PhpVal, ZVal};
mod module;
mod function; mod function;
mod module;
mod zval; mod zval;

View file

@ -1,10 +1,13 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::error::Error;
use std::fmt;
use std::fmt::Display;
use std::intrinsics::transmute; use std::intrinsics::transmute;
use std::mem::size_of; use std::mem::size_of;
use std::os::raw::c_char; use std::os::raw::c_char;
use std::str; use std::str;
use ivory_sys::{zend_execute_data, zval, zend_string}; use ivory_sys::{zend_execute_data, zend_string, zval};
#[repr(transparent)] #[repr(transparent)]
pub struct ExecuteData(zend_execute_data); pub struct ExecuteData(zend_execute_data);
@ -17,9 +20,7 @@ impl ExecuteData {
fn get_arg_base(&self) -> *const ZVal { fn get_arg_base(&self) -> *const ZVal {
let offset = (size_of::<zend_execute_data>() + size_of::<zval>() - 1) / size_of::<zval>(); let offset = (size_of::<zend_execute_data>() + size_of::<zval>() - 1) / size_of::<zval>();
let self_ptr: *const zend_execute_data = &self.0; let self_ptr: *const zend_execute_data = &self.0;
unsafe { unsafe { transmute::<_, *const ZVal>(self_ptr).add(offset) }
transmute::<_, *const ZVal>(self_ptr).add(offset)
}
} }
pub unsafe fn get_arg(&self, i: u32) -> &ZVal { pub unsafe fn get_arg(&self, i: u32) -> &ZVal {
@ -52,7 +53,7 @@ impl Iterator for IntoArgIterator {
self.item += 1; self.item += 1;
Some(val) Some(val)
} else { } else {
None Some(PhpVal::Undef)
} }
} }
} }
@ -99,7 +100,7 @@ impl ZVal {
let val: PhpVal = ZVal(elem.val).as_php_val(); let val: PhpVal = ZVal(elem.val).as_php_val();
match val { match val {
PhpVal::Undef => {} PhpVal::Undef => {}
_ => result.push((key, val)) _ => result.push((key, val)),
} }
} }
result result
@ -115,7 +116,7 @@ impl ZVal {
ZValType::Double => PhpVal::Double(unsafe { self.as_f64() }), ZValType::Double => PhpVal::Double(unsafe { self.as_f64() }),
ZValType::String => PhpVal::String(unsafe { self.as_str() }), ZValType::String => PhpVal::String(unsafe { self.as_str() }),
ZValType::Array => PhpVal::Array(unsafe { self.as_array() }), ZValType::Array => PhpVal::Array(unsafe { self.as_array() }),
_ => PhpVal::Undef _ => PhpVal::Undef,
} }
} }
} }
@ -136,6 +137,24 @@ pub enum ZValType {
Reference = 10, Reference = 10,
} }
impl Display for ZValType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ZValType::Undef => write!(f, "undefined"),
ZValType::Null => write!(f, "null"),
ZValType::False => write!(f, "bool"),
ZValType::True => write!(f, "bool"),
ZValType::Long => write!(f, "long"),
ZValType::Double => write!(f, "double"),
ZValType::String => write!(f, "string"),
ZValType::Array => write!(f, "array"),
ZValType::Object => write!(f, "object"),
ZValType::Resource => write!(f, "resource"),
ZValType::Reference => write!(f, "reference"),
}
}
}
impl From<u8> for ZValType { impl From<u8> for ZValType {
fn from(val: u8) -> Self { fn from(val: u8) -> Self {
if val > 10 { if val > 10 {
@ -151,6 +170,24 @@ pub enum ArrayKey {
Int(u64), Int(u64),
} }
impl From<String> for ArrayKey {
fn from(input: String) -> Self {
ArrayKey::String(input)
}
}
impl From<u64> for ArrayKey {
fn from(input: u64) -> Self {
ArrayKey::Int(input)
}
}
impl From<usize> for ArrayKey {
fn from(input: usize) -> Self {
ArrayKey::Int(input as u64)
}
}
#[derive(Debug)] #[derive(Debug)]
pub enum PhpVal { pub enum PhpVal {
Undef, Undef,
@ -165,44 +202,164 @@ pub enum PhpVal {
Reference(), Reference(),
} }
impl PhpVal {
pub fn get_type(&self) -> ZValType {
match self {
PhpVal::Undef => ZValType::Undef,
PhpVal::Null => ZValType::Null,
PhpVal::Bool(true) => ZValType::True,
PhpVal::Bool(false) => ZValType::False,
PhpVal::Long(_) => ZValType::Long,
PhpVal::Double(_) => ZValType::Double,
PhpVal::String(_) => ZValType::String,
PhpVal::Array(_) => ZValType::Array,
PhpVal::Object(_) => ZValType::Object,
PhpVal::Resource(_) => ZValType::Resource,
PhpVal::Reference() => ZValType::Reference,
}
}
}
impl Default for PhpVal { impl Default for PhpVal {
fn default() -> Self { fn default() -> Self {
PhpVal::Undef PhpVal::Undef
} }
} }
impl From<PhpVal> for Option<i64> { impl From<PhpVal> for Result<PhpVal, CastError> {
fn from(val: PhpVal) -> Self { fn from(val: PhpVal) -> Self {
match val { Ok(val)
PhpVal::Long(val) => Some(val), }
_ => None }
macro_rules! impl_from_phpval {
($type:ty, $variant:ident) => {
// non nullable version
impl From<PhpVal> for Result<$type, CastError> {
fn from(val: PhpVal) -> Self {
match val {
PhpVal::$variant(val) => Ok(val),
_ => Err(CastError {
actual: val.get_type(),
}),
}
}
}
// nullable version
impl From<PhpVal> for Result<Option<$type>, CastError> {
fn from(val: PhpVal) -> Self {
match val {
PhpVal::Null => Ok(None),
PhpVal::Undef => Ok(None),
PhpVal::$variant(val) => Ok(Some(val)),
_ => Err(CastError {
actual: val.get_type(),
}),
}
}
}
};
}
impl_from_phpval!(i64, Long);
impl_from_phpval!(f64, Double);
impl_from_phpval!(bool, Bool);
impl_from_phpval!(String, String);
impl From<i64> for PhpVal {
fn from(input: i64) -> Self {
PhpVal::Long(input)
}
}
impl From<String> for PhpVal {
fn from(input: String) -> Self {
PhpVal::String(input)
}
}
impl From<bool> for PhpVal {
fn from(input: bool) -> Self {
PhpVal::Bool(input)
}
}
impl<T: Into<PhpVal>> From<Option<T>> for PhpVal {
fn from(input: Option<T>) -> Self {
match input {
Some(inner) => inner.into(),
None => PhpVal::Null,
} }
} }
} }
impl From<PhpVal> for Option<f64> { impl<T: Into<PhpVal>> From<Vec<T>> for PhpVal {
fn from(val: PhpVal) -> Self { fn from(input: Vec<T>) -> Self {
match val { PhpVal::Array(
PhpVal::Double(val) => Some(val), input
_ => None .into_iter()
.enumerate()
.map(|(key, value)| (key.into(), value.into()))
.collect(),
)
}
}
impl<K: Into<ArrayKey>, T: Into<PhpVal>> From<Vec<(K, T)>> for PhpVal {
fn from(input: Vec<(K, T)>) -> Self {
PhpVal::Array(
input
.into_iter()
.map(|(key, value)| (key.into(), value.into()))
.collect(),
)
}
}
#[derive(Debug)]
pub struct CastError {
actual: ZValType,
}
#[derive(Debug)]
pub enum ArgError {
CastError(CastError),
NotEnoughArguments,
}
impl From<CastError> for ArgError {
fn from(from: CastError) -> Self {
ArgError::CastError(from)
}
}
impl Display for CastError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Incorrect variable type, got {}", self.actual)
}
}
impl Display for ArgError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ArgError::CastError(err) => err.fmt(f),
ArgError::NotEnoughArguments => write!(f, "Not enough arugments"),
} }
} }
} }
impl From<PhpVal> for Option<bool> { impl Error for CastError {
fn from(val: PhpVal) -> Self { fn cause(&self) -> Option<&Error> {
match val { None
PhpVal::Bool(val) => Some(val),
_ => None
}
} }
} }
impl From<PhpVal> for Option<String> { impl Error for ArgError {
fn from(val: PhpVal) -> Self { fn cause(&self) -> Option<&Error> {
match val { match self {
PhpVal::String(val) => Some(val), ArgError::CastError(err) => Some(err),
_ => None _ => None,
} }
} }
} }

View file

16
tests/Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "ivory-tests"
version = "0.1.0"
authors = ["Robin Appelman <robin@icewind.nl>"]
edition = "2018"
[dependencies]
ivory = { path = "../ivory", version = "0.1.0" }
[dev-dependencies]
maplit = "1.0"
pretty_assertions = "0.6"
[lib]
name = "tests"
crate-type = ["dylib"]

1
tests/README.md Normal file
View file

@ -0,0 +1 @@
# Tests

53
tests/src/lib.rs Normal file
View file

@ -0,0 +1,53 @@
use std::fmt::Debug;
use ivory::externs::printf;
use ivory::PhpVal;
use ivory::{ivory_export, ivory_module};
fn dump<T: Debug>(arg: T) {
printf(format!("{:?}", arg));
}
#[ivory_export]
fn dump_arg(arg: PhpVal) {
dump(arg);
}
#[ivory_export]
fn expect_long(arg: i64) {
dump(arg);
}
#[ivory_export]
fn expect_double(arg: f64) {
dump(arg);
}
#[ivory_export]
fn expect_string(arg: String) {
dump(arg);
}
#[ivory_export]
fn expect_bool(arg: bool) {
dump(arg);
}
#[ivory_export]
fn expect_option_bool(arg: Option<bool>) {
dump(arg);
}
ivory_module!({
name: "tests",
version: "0.0.1",
functions: &[
dump_arg,
expect_long,
expect_double,
expect_string,
expect_bool,
expect_option_bool
],
info: &[("test extension", "enabled")]
});

89
tests/tests/tests.rs Normal file
View file

@ -0,0 +1,89 @@
use std::collections::HashMap;
use std::process::Command;
use ivory::{ArrayKey, PhpVal};
use maplit::hashmap;
use pretty_assertions::assert_eq;
use std::fmt::Debug;
#[test]
fn zval_parsing() {
let inputs: HashMap<&str, PhpVal> = hashmap! {
"1" => PhpVal::Long(1),
"1.1" => PhpVal::Double(1.1),
"\"test\"" => PhpVal::String("test".into()),
"true" => PhpVal::Bool(true),
"false" => PhpVal::Bool(false),
"null" => PhpVal::Null,
"[1,2,3]" => vec![1, 2, 3].into(),
"[1,2,\"foo\"]" => vec![
PhpVal::Long(1),
PhpVal::Long(2),
PhpVal::String("foo".into())
].into(),
"[1,2, 4 => 3]" => vec![
(0u64, 1),
(1, 2),
(4, 3)
].into(),
"[1,2, \"foo\" => 3]" => vec![
(ArrayKey::from(0u64), 1),
(ArrayKey::from(1u64), 2),
(ArrayKey::from("foo".to_string()), 3)
].into(),
};
for (input, expected) in inputs {
let code = format!("dump_arg({})", input);
let result = run_php(&code).unwrap();
assert_debug_eq(expected, &result);
}
}
macro_rules! test_cast {
($name:ident, $method:expr, $in:expr, $fail:expr) => {
#[test]
fn $name() {
let result = run_php(&format!("{}({})", $method, $in)).unwrap();
assert_debug_eq($in, &result);
assert_eq!(true, run_php(&format!("{}({})", $method, $fail)).is_err());
assert_eq!(true, run_php(&format!("{}(null)", $method)).is_err());
assert_eq!(true, run_php(&format!("{}()", $method)).is_err());
}
};
}
test_cast!(test_cast_long, "expect_long", 1, false);
test_cast!(test_cast_double, "expect_double", 1.1, false);
test_cast!(test_cast_string, "expect_string", "foo".to_string(), false);
test_cast!(test_cast_bool, "expect_bool", true, 17);
#[test]
fn test_cast_option() {
let result = run_php("expect_option_bool(true)").unwrap();
assert_debug_eq(Some(true), &result);
assert_eq!(true, run_php("expect_option_bool(17)").is_err());
let result = run_php("expect_option_bool(null)").unwrap();
assert_debug_eq::<Option<bool>>(None, &result);
let result = run_php("expect_option_bool()").unwrap();
assert_debug_eq::<Option<bool>>(None, &result);
}
/// Test that the result is the debug formatting of expected
fn assert_debug_eq<T: Debug>(expected: T, result: &str) {
assert_eq!(format!("{:?}", expected), result);
}
/// Run some php code and return it's output
fn run_php(code: &str) -> Result<String, String> {
let code = format!("{};", code);
let output = Command::new("php")
.args(&["-d", "extension=target/debug/libtests.so", "-r", &code])
.output()
.expect("Failed to run php script");
if output.status.success() {
Ok(String::from_utf8(output.stdout).expect("invalid utf8"))
} else {
Err(String::from_utf8(output.stderr).expect("invalid utf8"))
}
}