diff --git a/.github/workflows/rust-build.yml b/.github/workflows/rust-build.yml index 8b2e29e..95ed81f 100644 --- a/.github/workflows/rust-build.yml +++ b/.github/workflows/rust-build.yml @@ -98,6 +98,7 @@ jobs: cargo build -p testangel --bin testangel --release cargo build -p testangel --bin testangel-executor --no-default-features --features cli --release cargo build -p testangel-evidence --release + cargo build -p testangel-rand --release cargo build -p testangel-user-interaction --release # Prepare output dir mkdir -p build || exit 1 @@ -106,6 +107,7 @@ jobs: # Prepare engines mkdir -p build/engines || exit 1 cp target/release/libtestangel_evidence.so build/engines + cp target/release/libtestangel_rand.so build/engines cp target/release/libtestangel_user_interaction.so build/engines - name: Save Cargo cache @@ -217,6 +219,7 @@ jobs: cargo build -p testangel --bin testangel --release cargo build -p testangel --bin testangel-executor --no-default-features --features cli --release cargo build -p testangel-evidence --release + cargo build -p testangel-rand --release cargo build -p testangel-user-interaction --release mkdir build copy target/release/testangel.exe build/ @@ -226,6 +229,7 @@ jobs: copy C:\gtk-build\gtk\x64\release\bin\*.dll build/ mkdir build/engines copy target/release/testangel_evidence.dll build/engines/ + copy target/release/testangel_rand.dll build/engines/ copy target/release/testangel_user_interaction.dll build/engines/ # GSchemas for FileChooser diff --git a/Cargo.lock b/Cargo.lock index 1ef2495..9f6216e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2677,6 +2677,16 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_regex" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bfbd599a8c757f89100e3ae559fb1ef9efa1cfd9276136862e3089dec627b31" +dependencies = [ + "rand", + "regex-syntax", +] + [[package]] name = "raw-window-handle" version = "0.5.2" @@ -3460,7 +3470,7 @@ dependencies = [ [[package]] name = "testangel" -version = "0.21.0-pre.5" +version = "0.21.0-rc.6" dependencies = [ "base64 0.22.0", "chrono", @@ -3496,7 +3506,7 @@ dependencies = [ [[package]] name = "testangel-engine" -version = "0.21.0-pre.5" +version = "0.21.0-rc.6" dependencies = [ "testangel-engine-macros", "testangel-ipc", @@ -3504,11 +3514,11 @@ dependencies = [ [[package]] name = "testangel-engine-macros" -version = "0.21.0-pre.5" +version = "0.21.0-rc.6" [[package]] name = "testangel-evidence" -version = "0.21.0-pre.5" +version = "0.21.0-rc.6" dependencies = [ "lazy_static", "testangel-engine", @@ -3516,16 +3526,36 @@ dependencies = [ [[package]] name = "testangel-ipc" -version = "0.21.0-pre.5" +version = "0.21.0-rc.6" dependencies = [ "schemars", "serde", "serde_json", ] +[[package]] +name = "testangel-rand" +version = "0.21.0-rc.6" +dependencies = [ + "lazy_static", + "rand", + "rand_regex", + "testangel-engine", + "thiserror", +] + +[[package]] +name = "testangel-time" +version = "0.21.0-rc.6" +dependencies = [ + "lazy_static", + "testangel-engine", + "thiserror", +] + [[package]] name = "testangel-user-interaction" -version = "0.21.0-pre.5" +version = "0.21.0-rc.6" dependencies = [ "lazy_static", "rfd", diff --git a/Cargo.toml b/Cargo.toml index 6638287..29fba52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.21.0-pre.5" +version = "0.21.0-rc.6" edition = "2021" [workspace] @@ -9,6 +9,8 @@ members = [ "testangel-engine", "testangel-engine-macros", "testangel-evidence", + "testangel-rand", + "testangel-time", "testangel-ipc", "testangel-user-interaction", ] diff --git a/testangel-rand/Cargo.toml b/testangel-rand/Cargo.toml new file mode 100644 index 0000000..0628756 --- /dev/null +++ b/testangel-rand/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "testangel-rand" +authors = ["Lily Hopkins "] +description = "A randomisation engine plugin for testangel." +version.workspace = true +edition.workspace = true + +[lib] +crate-type = ["cdylib"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lazy_static = "1.4.0" +rand = "0.8.5" +rand_regex = "0.17.0" +testangel-engine = { path = "../testangel-engine" } +thiserror = "1.0" diff --git a/testangel-rand/src/lib.rs b/testangel-rand/src/lib.rs new file mode 100644 index 0000000..99f67cc --- /dev/null +++ b/testangel-rand/src/lib.rs @@ -0,0 +1,41 @@ +use std::sync::Mutex; + +use lazy_static::lazy_static; +use rand::Rng; +use testangel_engine::*; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum EngineError { + #[error("Couldn't build expression.")] + CouldntBuildExpression(#[from] rand_regex::Error), +} + +lazy_static! { + static ref ENGINE: Mutex> = Mutex::new( + Engine::new("Random", "Random", env!("CARGO_PKG_VERSION")).with_instruction( + Instruction::new( + "rand-string", + "StringByRegex", + "Random String by Regex", + "Generate a random string given the regular expression-like format you provide.", + ) + .with_parameter("regex", "Regular Expression", ParameterKind::String) + .with_output("result", "Result", ParameterKind::String), + |_state, params, output, _evidence| { + let regex = params["regex"].value_string(); + + let expr = rand_regex::Regex::compile(®ex, 32) + .map_err(EngineError::CouldntBuildExpression)?; + output.insert( + "result".to_string(), + ParameterValue::String(rand::thread_rng().sample(&expr)), + ); + + Ok(()) + } + ) + ); +} + +expose_engine!(ENGINE); diff --git a/testangel-time/Cargo.toml b/testangel-time/Cargo.toml new file mode 100644 index 0000000..6fed8e2 --- /dev/null +++ b/testangel-time/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "testangel-time" +authors = ["Lily Hopkins "] +description = "A time engine plugin for testangel." +version.workspace = true +edition.workspace = true + +[lib] +crate-type = ["cdylib"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lazy_static = "1.4.0" +testangel-engine = { path = "../testangel-engine" } +thiserror = "1.0" diff --git a/testangel-time/src/lib.rs b/testangel-time/src/lib.rs new file mode 100644 index 0000000..8fb078b --- /dev/null +++ b/testangel-time/src/lib.rs @@ -0,0 +1,35 @@ +use std::{sync::Mutex, thread::sleep, time::Duration}; + +use lazy_static::lazy_static; +use testangel_engine::*; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum EngineError { + #[error("Duration cannot be negative.")] + CantWaitNegative, +} + +lazy_static! { + static ref ENGINE: Mutex> = Mutex::new( + Engine::new("Time", "Time", env!("CARGO_PKG_VERSION")).with_instruction( + Instruction::new( + "time-wait", + "Wait", + "Wait", + "Wait for a specified number of milliseconds.", + ) + .with_parameter("duration", "Duration (ms)", ParameterKind::Decimal), + |_state, params, _output, _evidence| { + let duration = params["duration"].value_i32(); + if duration < 0 { + return Err(Box::new(EngineError::CantWaitNegative)); + } + sleep(Duration::from_millis(duration as u64)); + Ok(()) + } + ) + ); +} + +expose_engine!(ENGINE); diff --git a/testangel/src/types/action_v1.rs b/testangel/src/types/action_v1.rs index 1dcb13e..de3fca1 100644 --- a/testangel/src/types/action_v1.rs +++ b/testangel/src/types/action_v1.rs @@ -67,7 +67,7 @@ impl ActionV1 { line.push_str(&match &step.run_if { InstructionParameterSource::Literal => String::new(), InstructionParameterSource::FromOutput(step, name) => { - format!("if s{step}_{} then ", name.to_case(Case::Snake)) + format!("if s{}_{} then ", step + 1, name.to_case(Case::Snake)) } InstructionParameterSource::FromParameter(param) => format!( "if {} then ", diff --git a/testangel/src/types/mod.rs b/testangel/src/types/mod.rs index 3d672f0..8f39866 100644 --- a/testangel/src/types/mod.rs +++ b/testangel/src/types/mod.rs @@ -130,7 +130,6 @@ pub enum FlowError { IPCFailure(IpcError), ActionDidntReturnCorrectArgumentCount, ActionDidntReturnValidArguments, - InstructionCalledWithUnsupportedVarType, InstructionCalledWithWrongNumberOfParams, InstructionCalledWithInvalidParamType, } @@ -152,10 +151,6 @@ impl fmt::Display for FlowError { Self::ActionDidntReturnValidArguments => { write!(f, "The action didn't return valid values.") } - Self::InstructionCalledWithUnsupportedVarType => write!( - f, - "An instruction was called with an unsupported variable type." - ), Self::InstructionCalledWithWrongNumberOfParams => write!( f, "An instruction was called with the wrong number of parameters." @@ -258,37 +253,65 @@ impl ActionConfiguration { )); } - // Convert to TA params - let mut params = vec![]; - for param in &args { - match param { - mlua::Value::Boolean(b) => params.push(ParameterValue::Boolean(*b)), - mlua::Value::String(s) => params - .push(ParameterValue::String(s.to_str().unwrap().to_owned())), - mlua::Value::Integer(i) => params.push(ParameterValue::Integer(*i)), - mlua::Value::Number(n) => { - params.push(ParameterValue::Decimal(*n as f32)) - } - _ => { - return Err(mlua::Error::external( - FlowError::InstructionCalledWithUnsupportedVarType, - )) - } - } - } - // Check we have the correct parameter types and convert to parameter map let mut param_map = HashMap::new(); - for (value, param_id) in - std::iter::zip(params, instruction.parameter_order()) - { + for (idx, param_id) in instruction.parameter_order().iter().enumerate() { if let Some((_name, kind)) = instruction.parameters().get(param_id) { - if *kind != value.kind() { - return Err(mlua::Error::external( - FlowError::InstructionCalledWithInvalidParamType, - )); + // Get argument and coerce + let arg = args[idx].clone(); + match kind { + ParameterKind::Boolean => { + if let mlua::Value::Boolean(b) = arg { + param_map.insert( + param_id.clone(), + ParameterValue::Boolean(b), + ); + } else { + return Err(mlua::Error::external( + FlowError::InstructionCalledWithInvalidParamType, + )); + } + } + ParameterKind::String => { + let maybe_str = lua.coerce_string(arg)?; + if let Some(s) = maybe_str { + param_map.insert( + param_id.clone(), + ParameterValue::String(s.to_str()?.to_string()), + ); + } else { + return Err(mlua::Error::external( + FlowError::InstructionCalledWithInvalidParamType, + )); + } + } + ParameterKind::Decimal => { + let maybe_dec = lua.coerce_number(arg)?; + if let Some(d) = maybe_dec { + param_map.insert( + param_id.clone(), + ParameterValue::Decimal(d as f32), + ); + } else { + return Err(mlua::Error::external( + FlowError::InstructionCalledWithInvalidParamType, + )); + } + } + ParameterKind::Integer => { + let maybe_int = lua.coerce_integer(arg)?; + if let Some(i) = maybe_int { + param_map.insert( + param_id.clone(), + ParameterValue::Integer(i), + ); + } else { + return Err(mlua::Error::external( + FlowError::InstructionCalledWithInvalidParamType, + )); + } + } } - param_map.insert(param_id.clone(), value); } } @@ -318,21 +341,25 @@ impl ActionConfiguration { let o = output[0][output_id].clone(); match o { ParameterValue::Boolean(b) => { + log::debug!("Boolean {b} returned to Lua"); outputs.push(mlua::Value::Boolean(b)) } ParameterValue::String(s) => { + log::debug!("String {s:?} returned to Lua"); outputs.push(mlua::Value::String(lua.create_string(s)?)) } ParameterValue::Integer(i) => { + log::debug!("Integer {i} returned to Lua"); outputs.push(mlua::Value::Integer(i)) } ParameterValue::Decimal(n) => { + log::debug!("Decimal {n} returned to Lua"); outputs.push(mlua::Value::Number(n as f64)) } } } - Ok(outputs) + Ok(mlua::MultiValue::from_vec(outputs)) } Response::Error { kind, reason } => { Err(mlua::Error::external(FlowError::FromInstruction { diff --git a/testangel/src/ui/actions/mod.rs b/testangel/src/ui/actions/mod.rs index 878fe95..c63b0a9 100644 --- a/testangel/src/ui/actions/mod.rs +++ b/testangel/src/ui/actions/mod.rs @@ -577,7 +577,14 @@ impl Component for ActionsModel { use convert_case::{Case, Casing}; let (param_name, _param_kind) = instruction.parameters().get(param_id).unwrap(); - params.push_str(&format!("{}, ", param_name.to_case(Case::Snake))); + // remove invalid chars + let mut sanitised_name = String::new(); + for c in param_name.chars() { + if c.is_ascii_alphanumeric() || c.is_ascii_whitespace() { + sanitised_name.push(c); + } + } + params.push_str(&format!("{}, ", sanitised_name.to_case(Case::Snake))); } // remove last ", " let _ = params.pop();