diff --git a/rust_slim/readme.md b/rust_slim/readme.md index 8539e3b..ead09ba 100644 --- a/rust_slim/readme.md +++ b/rust_slim/readme.md @@ -2,12 +2,9 @@ Develop Slim Fixtures for rust applications. Based on the [Slim Protocol](https://fitnesse.org/FitNesse/UserGuide/WritingAcceptanceTests/SliM/SlimProtocol.html) of fitnesse [fitnesse](https://fitnesse.org) . -This is not 100% compliant with the slim protocol right now. Here are some of the features that are known to not be implemented: - -It can only handle string symbols and it cannot. -It does not support a SUT for features. -It does not support Actors. -It does not support using the STOUD and STDIN for comunication. +This is not 100% compliant with the SliM protocol yet. Remaining work includes +instruction timeouts, stop and ignore batch control, standard input/output +transport with fixture-output tunneling, and the optional HTML hash converter. This is currently in an unstable version. The general API can change in the next versions. @@ -21,3 +18,9 @@ argument and `IntoSlimValue` to convert the method result. These conversions preserve protocol lists, null, void, and in-process object handles. The old `ToSlimResultString` trait remains available as a string conversion helper, but it does not preserve those structured values. + +Manual fixtures must also implement `Constructor::construct` and return +`Result`. The `#[fixture]` macro generates a zero-argument +constructor for `Default` fixtures. Mark one or more associated functions with +`#[slim(constructor)]` to expose typed constructors of different arities, and +mark an `&mut self` accessor with `#[slim(sut)]` to provide a System Under Test. diff --git a/rust_slim/src/lib.rs b/rust_slim/src/lib.rs index 662dd57..aa8f704 100644 --- a/rust_slim/src/lib.rs +++ b/rust_slim/src/lib.rs @@ -20,6 +20,11 @@ //! //! #[fixture] //! impl Calculator { +//! #[slim(constructor)] +//! pub fn new() -> Self { +//! Self::default() +//! } +//! //! pub fn set_a(&mut self, a: i64) { //! self.a = a //! } @@ -68,6 +73,11 @@ //! # //! # #[fixture] //! # impl Calculator { +//! # #[slim(constructor)] +//! # pub fn new() -> Self { +//! # Self::default() +//! # } +//! # //! # pub fn set_a(&mut self, a: i64) { //! # self.a = a //! # } @@ -136,6 +146,20 @@ pub trait SlimFixture { method: &str, args: Vec, ) -> Result; + + /// Tries the fixture's System Under Test after a method is not found on + /// the fixture itself. Fixtures without an SUT keep the default, which + /// deliberately looks like a missing method to the dispatcher. + fn execute_system_under_test( + &mut self, + method: &str, + _args: Vec, + ) -> Result { + Err(ExecuteMethodError::MethodNotFound { + method: method.into(), + class: "SystemUnderTest".into(), + }) + } } /// ClassPath that will be used in the construction of the fixture. @@ -155,18 +179,23 @@ pub trait ClassPath { fn class_path() -> String; } -/// Trait used to construct the fixture. It is auto-implemented for fixtures that also implement Default. +/// Trait used to construct a fixture from SliM constructor arguments. +/// +/// Implementations must report an arity mismatch as [`ConstructorError::NoConstructor`], +/// conversion failures as [`ConstructorError::ArgumentParsingError`], and an +/// error from the constructor itself as [`ConstructorError::CouldNotInvoke`]. pub trait Constructor { - fn construct(args: Vec) -> Self; + fn construct(args: Vec) -> Result + where + Self: Sized; } -impl Constructor for T -where - T: Default, -{ - fn construct(_args: Vec) -> T { - T::default() - } +/// Failure while selecting, converting for, or invoking a fixture constructor. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum ConstructorError { + NoConstructor, + ArgumentParsingError(String), + CouldNotInvoke(String), } /// Error that can happen while trying to execute a method in a feature. diff --git a/rust_slim/src/server.rs b/rust_slim/src/server.rs index 04f92c6..7296e89 100644 --- a/rust_slim/src/server.rs +++ b/rust_slim/src/server.rs @@ -1,7 +1,10 @@ -use crate::{ClassPath, Constructor, SlimFixture, SlimValue}; +use crate::{ + ClassPath, Constructor, ConstructorError, ExecuteMethodError, SlimFixture, SlimObject, + SlimValue, +}; use convert_case::{Case, Casing}; use slim_protocol::{ - ByeOrSlimInstructions, ExceptionMessage, FromSlimReader, FromSlimReaderError, Instruction, + ByeOrSlimInstructions, ExceptionMessage, FromSlimReader, FromSlimReaderError, Id, Instruction, InstructionResult, InstructionResultValue, SlimValue as WireSlimValue, ToSlimString, }; use std::{ @@ -10,7 +13,9 @@ use std::{ }; use thiserror::Error; -/// Error that can happen while executing an SlimServer +pub type SlimClosureConstructor = + Box) -> Result>; + #[derive(Debug, Error)] pub enum SlimServerError { #[error(transparent)] @@ -19,13 +24,13 @@ pub enum SlimServerError { FromSlimReaderError(#[from] FromSlimReaderError), } -pub type SlimClosureConstructor = Box) -> Box>; - -/// The SlimServer responsible to get the Slim commands and execute against the Fixtures. +/// The mutable execution context required by the SliM instruction set. pub struct SlimServer { fixtures: HashMap, - instances: HashMap>, - libraries: HashMap>, + instances: HashMap, + /// Kept as a vector because library lookup is a stack, not a map. + libraries: Vec<(String, SlimObject)>, + actors: Vec, symbols: HashMap, imports: Vec, reader: BufReader, @@ -33,37 +38,38 @@ pub struct SlimServer { } impl SlimServer { - /// Create a new SlimServer pub fn new(reader: R, writer: W) -> Self { Self { fixtures: HashMap::new(), instances: HashMap::new(), - libraries: HashMap::new(), + libraries: Vec::new(), + actors: Vec::new(), symbols: HashMap::new(), - reader: BufReader::new(reader), imports: Vec::new(), + reader: BufReader::new(reader), writer, } } - /// Add a new fixture pub fn add_fixture(&mut self) { self.fixtures.insert( T::class_path(), - Box::new(|args: Vec| Box::new(T::construct(args)) as Box) - as Box) -> Box>, + Box::new(|args| { + T::construct(args).map(|fixture| SlimObject::fixture(fixture, T::class_path())) + }), ); } - /// Run the server pub fn run(mut self) -> Result<(), SlimServerError> { self.writer.write_all(b"Slim -- V0.5\n")?; + self.writer.flush()?; loop { match ByeOrSlimInstructions::from_reader(&mut self.reader)? { ByeOrSlimInstructions::Bye => break, ByeOrSlimInstructions::Instructions(instructions) => { let result = self.execute_instructions(instructions); self.writer.write_all(result.to_slim_string().as_bytes())?; + self.writer.flush()?; } } } @@ -71,140 +77,250 @@ impl SlimServer { } fn execute_instructions(&mut self, instructions: Vec) -> Vec { - let mut results = Vec::new(); - for instruction in instructions { - match instruction { - Instruction::Malformed { id, fields } => { - results.push(InstructionResult::exception( - id, - ExceptionMessage::new(format!( - "MALFORMED_INSTRUCTION [{}]", - fields.join(",") - )), - )); + instructions + .into_iter() + .map(|instruction| self.execute_instruction(instruction)) + .collect() + } + + fn execute_instruction(&mut self, instruction: Instruction) -> InstructionResult { + match instruction { + Instruction::Malformed { id, fields } => { + exception(id, format!("MALFORMED_INSTRUCTION [{}]", fields.join(","))) + } + Instruction::Import { id, path } => { + self.imports.push(path); + InstructionResult::ok(id) + } + Instruction::Make { + id, + instance, + class, + args, + } => self.make(id, instance, class, args), + Instruction::Call { + id, + instance, + function, + args, + } => self.call(id, None, instance, function, args), + Instruction::CallAndAssign { + id, + symbol, + instance, + function, + args, + } => { + if !is_symbol_name(&symbol) { + let mut fields = vec![ + id.to_string(), + "callAndAssign".into(), + symbol, + instance, + function, + ]; + fields.extend(args.iter().map(wire_value_text)); + return malformed_instruction(id, fields); } - Instruction::Import { id, path } => { - self.imports.push(path); - results.push(InstructionResult::ok(id)) + self.call(id, Some(symbol), instance, function, args) + } + Instruction::Assign { id, symbol, value } => { + if !is_symbol_name(&symbol) { + let fields = vec![ + id.to_string(), + "assign".into(), + symbol, + wire_value_text(&value), + ]; + return malformed_instruction(id, fields); } - Instruction::Make { - id, - instance, - class, - args, - } => { - let class = self.parse_symbol(class); - let Some(fixture) = self.find_fixture(&class) else { - results.push(InstructionResult::exception( - id, - ExceptionMessage::new(format!("NO CLASS: {class}")), - )); - continue; - }; - let args = self.parse_wire_values(args); - if instance.starts_with("library") { - self.libraries.insert(instance, fixture(args)); - } else { - self.instances.insert(instance, fixture(args)); - } - results.push(InstructionResult::ok(id)) + self.symbols + .insert(symbol, runtime_value_without_symbols(value)); + InstructionResult::ok(id) + } + } + } + + fn make( + &mut self, + id: Id, + instance: String, + class: String, + args: Vec, + ) -> InstructionResult { + if let Some(SlimValue::Object(object)) = self.whole_symbol(&class) { + self.insert_instance(instance, object); + return InstructionResult::ok(id); + } + let class = self.replace_symbols(&class); + let Some(constructor) = self.find_fixture(&class) else { + return exception(id, format!("NO_CLASS {class}")); + }; + let args = self.parse_wire_values(args); + match constructor(args) { + Ok(fixture) => { + self.insert_instance(instance, fixture); + InstructionResult::ok(id) + } + Err(ConstructorError::NoConstructor) => { + exception(id, format!("NO_CONSTRUCTOR {class}")) + } + Err(ConstructorError::ArgumentParsingError(argument)) => { + exception(id, format!("NO_CONVERTER_FOR_ARGUMENT_NUMBER {argument}")) + } + Err(ConstructorError::CouldNotInvoke(message)) => exception( + id, + format!("COULD_NOT_INVOKE_CONSTRUCTOR {class} message:<<{message}>>"), + ), + } + } + + fn insert_instance(&mut self, name: String, object: SlimObject) { + self.instances.insert(name.clone(), object.clone()); + if name.starts_with("library") { + self.libraries.push((name, object)); + } + } + + fn call( + &mut self, + id: Id, + assigned_symbol: Option, + instance: String, + function: String, + args: Vec, + ) -> InstructionResult { + let protocol_function = function; + let function = protocol_function.to_case(Case::Snake); + let args = self.parse_wire_values(args); + let result = self.dispatch(&instance, &function, args); + match result { + Ok(value) => { + if let Some(symbol) = assigned_symbol { + self.symbols.insert(symbol, value.clone()); } - Instruction::Call { - id, - instance, - function, - args, - } => { - let args = self.parse_wire_values(args); - let instances = if instance.starts_with("library") { - &mut self.libraries - } else { - &mut self.instances - }; - let Some(instance) = instances.get_mut(&instance) else { - results.push(InstructionResult::exception( - id, - ExceptionMessage::new(format!("NO_INSTANCE: {instance}")), - )); - continue; - }; - let function = function.to_case(Case::Snake); - - match instance.execute_method(&function, args) { - Ok(value) => results.push(instruction_result_for_value(id, value)), - Err(error) => results.push(InstructionResult::exception( - id, - ExceptionMessage::new(error.to_string()), - )), - } + instruction_result_for_value(id, value) + } + Err(ExecuteMethodError::MethodNotFound { class, .. }) => exception( + id, + ExecuteMethodError::MethodNotFound { + method: protocol_function, + class, } - Instruction::CallAndAssign { - id, - symbol, - instance, - function, - args, - } => { - let args = self.parse_wire_values(args); - let instances = if instance.starts_with("library") { - &mut self.libraries - } else { - &mut self.instances - }; - let Some(instance) = instances.get_mut(&instance) else { - results.push(InstructionResult::exception( - id, - ExceptionMessage::new(format!("NO_INSTANCE: {instance}")), - )); - continue; - }; - let function = function.to_case(Case::Snake); - let symbol = symbol.strip_prefix('$').unwrap_or(&symbol).into(); - match instance.execute_method(&function, args) { - Ok(value) => { - results.push(instruction_result_for_value(id, value.clone())); - self.symbols.insert(symbol, value); - } - Err(error) => results.push(InstructionResult::exception( - id, - ExceptionMessage::new(error.to_string()), - )), - } + .to_string(), + ), + Err(error) => exception(id, error.to_string()), + } + } + + fn dispatch( + &mut self, + instance_name: &str, + method: &str, + args: Vec, + ) -> Result { + let instance = self.instances.get(instance_name).cloned(); + let direct = if let Some(instance) = &instance { + let result = invoke(instance, method, args.clone()); + if !is_missing_method(&result) { + return result; + } + result + } else if instance_name == "SlimHelperLibrary" { + if let Some(result) = self.helper_call(method, args.clone()) { + if !is_missing_method(&result) { + return result; } - Instruction::Assign { id, symbol, value } => { - let symbol = symbol.strip_prefix('$').unwrap_or(&symbol).into(); - let value = self.parse_wire_value(value); - self.symbols.insert(symbol, value); - results.push(InstructionResult::ok(id)) + } + Err(ExecuteMethodError::MethodNotFound { + method: method.into(), + class: "SlimHelperLibrary".into(), + }) + } else { + Err(ExecuteMethodError::ExecutionError(format!( + "NO_INSTANCE {instance_name}" + ))) + }; + if let Some(instance) = instance { + if let Some(fixture) = instance.as_fixture() { + let sut = fixture + .borrow_mut() + .execute_system_under_test(method, args.clone()); + if !is_missing_method(&sut) { + return sut; } } } - results + for (_, library) in self.libraries.iter().rev() { + let result = invoke(library, method, args.clone()); + if !is_missing_method(&result) { + return result; + } + } + self.helper_call(method, args).unwrap_or(direct) } - fn find_fixture(&self, class: &str) -> Option<&SlimClosureConstructor> { - if let Some(fixture) = self.fixtures.get(class) { - return Some(fixture); - } - for class_path in self.imports.iter() { - let class = format!("{class_path}.{class}"); - if let Some(fixture) = self.fixtures.get(&class) { - return Some(fixture); + fn helper_call( + &mut self, + method: &str, + args: Vec, + ) -> Option> { + let result = match method { + "get_fixture" if args.is_empty() => self.actor_fixture().map(SlimValue::Object), + "push_fixture" if args.is_empty() => self.actor_fixture().map(|fixture| { + self.actors.push(fixture); + SlimValue::Void + }), + "pop_fixture" if args.is_empty() => self + .actors + .pop() + .ok_or_else(|| { + ExecuteMethodError::ExecutionError( + "ACTOR_STACK_EMPTY message:<>".into(), + ) + }) + .map(|fixture| { + self.instances.insert("scriptTableActor".into(), fixture); + SlimValue::Void + }), + "clone_symbol" if args.len() == 1 => { + Ok(args.into_iter().next().expect("one argument checked")) } - } - None + _ => return None, + }; + Some(result) + } + + fn actor_fixture(&self) -> Result { + self.instances + .get("scriptTableActor") + .cloned() + .ok_or_else(|| { + ExecuteMethodError::ExecutionError("NO_INSTANCE scriptTableActor".into()) + }) + } + + fn find_fixture(&self, class: &str) -> Option<&SlimClosureConstructor> { + self.fixtures.get(class).or_else(|| { + self.imports + .iter() + .rev() + .find_map(|path| self.fixtures.get(&format!("{path}.{class}"))) + }) } fn parse_wire_values(&self, args: Vec) -> Vec { args.into_iter() - .map(|arg| self.parse_wire_value(arg)) + .map(|value| self.parse_wire_value(value)) .collect() } fn parse_wire_value(&self, value: WireSlimValue) -> SlimValue { match value { WireSlimValue::String(value) if value == "null" => SlimValue::Null, - WireSlimValue::String(value) => SlimValue::String(self.parse_symbol(value)), + WireSlimValue::String(value) => self + .whole_symbol(&value) + .unwrap_or_else(|| SlimValue::String(self.replace_symbols(&value))), WireSlimValue::List(values) => SlimValue::List( values .into_iter() @@ -214,33 +330,106 @@ impl SlimServer { } } - fn parse_symbol(&self, mut value: String) -> String { - while let Some((before, after)) = value.split_once('$') { - if let Some((name, rest)) = after.split_once(' ') { - let mut new_value = String::from(before); - new_value += &self - .symbols - .get(name) - .map(SlimValue::as_text) - .unwrap_or_default(); - new_value += " "; - new_value += rest; - value = new_value; + fn whole_symbol(&self, value: &str) -> Option { + value + .strip_prefix('$') + .filter(|name| is_symbol_name(name)) + .and_then(|name| self.symbols.get(name).cloned()) + } + + fn replace_symbols(&self, value: &str) -> String { + let mut result = String::with_capacity(value.len()); + let mut characters = value.char_indices().peekable(); + while let Some((_, character)) = characters.next() { + if character != '$' { + result.push(character); + continue; + } + let start = characters.peek().map_or(value.len(), |(index, _)| *index); + let mut end = start; + while let Some((index, character)) = characters.peek().copied() { + if !character.is_ascii_alphabetic() { + break; + } + end = index + character.len_utf8(); + characters.next(); + } + if end == start { + result.push('$'); } else { - let mut new_value = String::from(before); - new_value += &self - .symbols - .get(after) - .map(SlimValue::as_text) - .unwrap_or_default(); - value = new_value; + let name = &value[start..end]; + match self.symbols.get(name) { + Some(symbol) => result.push_str(&symbol.as_text()), + None => { + result.push('$'); + result.push_str(name); + } + } } } - value + result } } -fn instruction_result_for_value(id: slim_protocol::Id, value: SlimValue) -> InstructionResult { +fn invoke( + object: &SlimObject, + method: &str, + args: Vec, +) -> Result { + let Some(fixture) = object.as_fixture() else { + return Err(ExecuteMethodError::MethodNotFound { + method: method.into(), + class: object.display_value().into(), + }); + }; + let result = fixture.borrow_mut().execute_method(method, args); + result +} + +fn is_missing_method(result: &Result) -> bool { + matches!(result, Err(ExecuteMethodError::MethodNotFound { .. })) +} + +fn is_symbol_name(name: &str) -> bool { + !name.is_empty() && name.bytes().all(|byte| byte.is_ascii_alphabetic()) +} + +fn exception(id: Id, value: String) -> InstructionResult { + InstructionResult::exception(id, ExceptionMessage::new(value)) +} + +fn malformed_instruction(id: Id, fields: Vec) -> InstructionResult { + exception(id, format!("MALFORMED_INSTRUCTION [{}]", fields.join(","))) +} + +fn wire_value_text(value: &WireSlimValue) -> String { + match value { + WireSlimValue::String(value) => value.clone(), + WireSlimValue::List(values) => format!( + "[{}]", + values + .iter() + .map(wire_value_text) + .collect::>() + .join(", ") + ), + } +} + +fn runtime_value_without_symbols(value: WireSlimValue) -> SlimValue { + match value { + WireSlimValue::String(value) if value == "null" => SlimValue::Null, + WireSlimValue::String(value) => SlimValue::String(value), + WireSlimValue::List(values) => SlimValue::List( + values + .into_iter() + .map(runtime_value_without_symbols) + .collect(), + ), + } +} + +fn instruction_result_for_value(id: Id, value: SlimValue) -> InstructionResult { match value { SlimValue::Void => InstructionResult::void(id), value => InstructionResult::new(id, instruction_result_value(value)), @@ -261,496 +450,703 @@ fn instruction_result_value(value: SlimValue) -> InstructionResultValue { #[cfg(test)] mod tests { - use slim_protocol::{FromSlimReader, Id, ToSlimString}; - use std::error::Error; + use super::*; use std::io::Cursor; - use super::*; + struct Fixture { + value: String, + sut: Sut, + } + struct Sut; + struct Library { + value: &'static str, + } + struct Actor(String); + struct FailingFixture; - #[test] - fn execute_import() -> Result<(), Box> { - let mut vec = Vec::new(); - let reader = Cursor::new(&mut vec); - let mut vec = Vec::new(); - let writer = Cursor::new(&mut vec); - let mut slim_server = SlimServer::new(reader, writer); - let result = slim_server.execute_instructions(vec![ - Instruction::Import { - id: Id::from("id_1"), - path: "ExamplePath1".into(), - }, - Instruction::Import { - id: Id::from("id_2"), - path: "ExamplePath2".into(), - }, - ]); + impl SlimFixture for Fixture { + fn execute_method( + &mut self, + method: &str, + args: Vec, + ) -> Result { + match method { + "echo" => Ok(args + .into_iter() + .next() + .unwrap_or(SlimValue::String(self.value.clone()))), + "object" => Ok(SlimValue::Object(SlimObject::fixture(Sut, "chained"))), + "actor" => { + let label = args + .into_iter() + .next() + .map_or_else(|| "actor".into(), |value| value.as_text()); + Ok(SlimValue::Object(SlimObject::fixture( + Actor(label.clone()), + label, + ))) + } + _ => Err(ExecuteMethodError::MethodNotFound { + method: method.into(), + class: "Fixture".into(), + }), + } + } + fn execute_system_under_test( + &mut self, + method: &str, + args: Vec, + ) -> Result { + self.sut.execute_method(method, args) + } + } + impl SlimFixture for Sut { + fn execute_method( + &mut self, + method: &str, + _args: Vec, + ) -> Result { + match method { + "sut" | "from_sut" | "shared" => Ok(SlimValue::String("sut".into())), + "echo" => Ok(SlimValue::String("sut-echo".into())), + _ => Err(ExecuteMethodError::MethodNotFound { + method: method.into(), + class: "Sut".into(), + }), + } + } + } + impl SlimFixture for Library { + fn execute_method( + &mut self, + method: &str, + _args: Vec, + ) -> Result { + if matches!(method, "library" | "shared") { + Ok(SlimValue::String(self.value.into())) + } else { + Err(ExecuteMethodError::MethodNotFound { + method: method.into(), + class: "Library".into(), + }) + } + } + } + impl SlimFixture for Actor { + fn execute_method( + &mut self, + method: &str, + _args: Vec, + ) -> Result { + match method { + "read" => Ok(SlimValue::String(self.0.clone())), + _ => Err(ExecuteMethodError::MethodNotFound { + method: method.into(), + class: "Actor".into(), + }), + } + } + } + impl SlimFixture for FailingFixture { + fn execute_method( + &mut self, + method: &str, + _args: Vec, + ) -> Result { + Err(ExecuteMethodError::MethodNotFound { + method: method.into(), + class: "FailingFixture".into(), + }) + } + } + impl ClassPath for Fixture { + fn class_path() -> String { + "Fixture".into() + } + } + impl ClassPath for Library { + fn class_path() -> String { + "Library".into() + } + } + impl ClassPath for FailingFixture { + fn class_path() -> String { + "FailingFixture".into() + } + } + impl Constructor for Fixture { + fn construct(args: Vec) -> Result { + if args.len() > 1 { + return Err(ConstructorError::NoConstructor); + } + Ok(Self { + value: args + .into_iter() + .next() + .map_or_else(|| "fixture".into(), |value| value.as_text()), + sut: Sut, + }) + } + } + impl Constructor for Library { + fn construct(args: Vec) -> Result { + let [SlimValue::String(value)] = args.as_slice() else { + return Err(ConstructorError::NoConstructor); + }; + Ok(Self { + value: if value == "new" { "newest" } else { "oldest" }, + }) + } + } + impl Constructor for FailingFixture { + fn construct(args: Vec) -> Result { + match args.as_slice() { + [SlimValue::String(value)] if value == "conversion" => { + Err(ConstructorError::ArgumentParsingError("i64".into())) + } + [SlimValue::String(value)] if value == "invocation" => Err( + ConstructorError::CouldNotInvoke("constructor failed".into()), + ), + _ => Err(ConstructorError::NoConstructor), + } + } + } + + fn server() -> SlimServer>, Cursor>> { + let mut server = SlimServer::new(Cursor::new(Vec::new()), Cursor::new(Vec::new())); + server.add_fixture::(); + server.add_fixture::(); + server.add_fixture::(); + server + } + fn call(id: &str, instance: &str, function: &str, args: Vec) -> Instruction { + Instruction::Call { + id: id.into(), + instance: instance.into(), + function: function.into(), + args, + } + } + #[test] + fn symbols_are_letter_only_and_preserve_undefined_punctuation_and_values() { + let mut server = server(); + server + .symbols + .insert("A".into(), SlimValue::String("one".into())); + server + .symbols + .insert("B".into(), SlimValue::String("two".into())); assert_eq!( - vec!["ExamplePath1".to_string(), "ExamplePath2".to_string()], - slim_server.imports + "$missing.one-two$1", + server.replace_symbols("$missing.$A-$B$1") ); - assert_eq!( - vec![ - InstructionResult::ok(Id::from("id_1")), - InstructionResult::ok(Id::from("id_2")) - ], - result + assert!( + matches!(server.parse_wire_value("$A".into()), SlimValue::String(value) if value == "one") ); - Ok(()) + assert_eq!("$not_valid", server.replace_symbols("$not_valid")); } #[test] - fn execute_make() -> Result<(), Box> { - let mut vec = Vec::new(); - let reader = Cursor::new(&mut vec); - let mut vec = Vec::new(); - let writer = Cursor::new(&mut vec); - let mut slim_server = SlimServer::new(reader, writer); - slim_server.add_fixture::(); - let result = slim_server.execute_instructions(vec![ + fn dispatches_fixture_then_sut_then_newest_library_and_helpers() { + let mut server = server(); + let result = server.execute_instructions(vec![ Instruction::Make { - id: Id::from("m_1"), - instance: "Instance1".into(), - class: "Test.TestFixture".into(), - args: Vec::new(), + id: "one".into(), + instance: "fixture".into(), + class: "Fixture".into(), + args: vec![], }, Instruction::Make { - id: Id::from("m_2"), - instance: "Instance2".into(), - class: "Test.TestFixture".into(), - args: Vec::new(), + id: "two".into(), + instance: "libraryOne".into(), + class: "Library".into(), + args: vec!["old".into()], }, Instruction::Make { - id: Id::from("m_3"), - instance: "libraryInstance".into(), - class: "Test.TestFixture".into(), - args: Vec::new(), + id: "three".into(), + instance: "libraryTwo".into(), + class: "Library".into(), + args: vec!["new".into()], }, + call("four", "fixture", "fromSut", vec![]), + call("five", "fixture", "library", vec![]), + call("six", "fixture", "echo", vec![]), + call("seven", "fixture", "shared", vec![]), ]); - - assert_eq!(2, slim_server.instances.len()); - assert!(slim_server.instances.contains_key("Instance1")); - assert!(slim_server.instances.contains_key("Instance2")); - assert_eq!(1, slim_server.libraries.len()); - assert!(slim_server.libraries.contains_key("libraryInstance")); assert_eq!( - vec![ - InstructionResult::ok(Id::from("m_1")), - InstructionResult::ok(Id::from("m_2")), - InstructionResult::ok(Id::from("m_3")), - ], - result + InstructionResult::string("four".into(), "sut".into()), + result[3] + ); + assert_eq!( + InstructionResult::string("five".into(), "newest".into()), + result[4] + ); + assert_eq!( + InstructionResult::string("six".into(), "fixture".into()), + result[5] + ); + assert_eq!( + InstructionResult::string("seven".into(), "sut".into()), + result[6] + ); + assert!( + matches!(server.dispatch("fixture", "get_fixture", vec![]), Err(ExecuteMethodError::ExecutionError(value)) if value == "NO_INSTANCE scriptTableActor") ); - Ok(()) } #[test] - fn execute_call() -> Result<(), Box> { - let mut vec = Vec::new(); - let reader = Cursor::new(&mut vec); - let mut vec = Vec::new(); - let writer = Cursor::new(&mut vec); - let mut slim_server = SlimServer::new(reader, writer); - slim_server.add_fixture::(); - let result = slim_server.execute_instructions(vec![ - Instruction::Make { - id: Id::from("m_1"), - instance: "Instance".into(), - class: "Test.TestFixture".into(), - args: Vec::new(), - }, + fn call_and_assign_preserves_objects_for_fixture_chaining_and_actors() { + let mut server = server(); + let result = server.execute_instructions(vec![ Instruction::Make { - id: Id::from("m_2"), - instance: "libraryInstance".into(), - class: "Test.TestFixture".into(), - args: Vec::new(), + id: "make".into(), + instance: "fixture".into(), + class: "Fixture".into(), + args: vec![], }, - Instruction::Call { - id: Id::from("c_1"), - instance: "Instance".into(), - function: "echo".into(), - args: vec!["Arg".into()], + Instruction::CallAndAssign { + id: "assign".into(), + symbol: "Actor".into(), + instance: "fixture".into(), + function: "object".into(), + args: vec![], }, - Instruction::Call { - id: Id::from("c_2"), - instance: "libraryInstance".into(), - function: "echo".into(), - args: vec!["Arg1".into(), "Arg2".into()], + Instruction::Make { + id: "copy".into(), + instance: "scriptTableActor".into(), + class: "$Actor".into(), + args: vec!["ignored".into()], }, + call("push", "fixture", "pushFixture", vec![]), + call("pop", "fixture", "popFixture", vec![]), + call("chained", "scriptTableActor", "sut", vec![]), ]); + assert!(matches!( + server.symbols.get("Actor"), + Some(SlimValue::Object(_)) + )); + assert_eq!( + InstructionResult::string("chained".into(), "sut".into()), + result[5] + ); + } + + #[test] + fn make_copies_opaque_object_symbols_without_calling_a_constructor() { + let mut server = server(); + let object = SlimObject::new(42_i64, "opaque"); + server + .symbols + .insert("Opaque".into(), SlimValue::Object(object.clone())); + + let result = server.execute_instructions(vec![Instruction::Make { + id: "copy".into(), + instance: "copied".into(), + class: "$Opaque".into(), + args: vec!["ignored".into()], + }]); + assert_eq!(InstructionResult::ok("copy".into()), result[0]); assert_eq!( - vec![ - InstructionResult::ok(Id::from("m_1")), - InstructionResult::ok(Id::from("m_2")), - InstructionResult::string(Id::from("c_1"), "Arg".into()), - InstructionResult::string(Id::from("c_2"), "Arg1,Arg2".into()), - ], - result + SlimValue::Object(object), + SlimValue::Object(server.instances.get("copied").unwrap().clone()) ); - Ok(()) } #[test] - fn execute_call_and_assign() -> Result<(), Box> { - let mut vec = Vec::new(); - let reader = Cursor::new(&mut vec); - let mut vec = Vec::new(); - let writer = Cursor::new(&mut vec); - let mut slim_server = SlimServer::new(reader, writer); - slim_server.add_fixture::(); - let result = slim_server.execute_instructions(vec![ + fn actor_helpers_restore_fixture_identity_and_clone_object_symbols() { + let mut server = server(); + let result = server.execute_instructions(vec![ Instruction::Make { - id: Id::from("m_1"), - instance: "Instance".into(), - class: "Test.TestFixture".into(), - args: Vec::new(), + id: "fixture".into(), + instance: "fixture".into(), + class: "Fixture".into(), + args: vec![], + }, + Instruction::CallAndAssign { + id: "first-object".into(), + symbol: "First".into(), + instance: "fixture".into(), + function: "actor".into(), + args: vec!["first".into()], }, Instruction::Make { - id: Id::from("m_2"), - instance: "libraryInstance".into(), - class: "Test.TestFixture".into(), - args: Vec::new(), + id: "first-actor".into(), + instance: "scriptTableActor".into(), + class: "$First".into(), + args: vec!["ignored".into()], }, + call("push", "fixture", "pushFixture", vec![]), Instruction::CallAndAssign { - id: Id::from("ca_1"), - symbol: "$Symbol1".into(), - instance: "Instance".into(), - function: "echo".into(), - args: vec!["Arg".into()], + id: "second-object".into(), + symbol: "Second".into(), + instance: "fixture".into(), + function: "actor".into(), + args: vec!["second".into()], }, - Instruction::Call { - id: Id::from("c_1"), - instance: "Instance".into(), - function: "echo".into(), - args: vec!["$Symbol1 in symbol".into()], + Instruction::Make { + id: "second-actor".into(), + instance: "scriptTableActor".into(), + class: "$Second".into(), + args: vec![], }, + call("second", "scriptTableActor", "read", vec![]), + call("pop", "fixture", "popFixture", vec![]), + call("first", "scriptTableActor", "read", vec![]), Instruction::CallAndAssign { - id: Id::from("ca_2"), - symbol: "$Symbol2".into(), - instance: "libraryInstance".into(), - function: "echo".into(), - args: vec!["LibraryArg".into()], + id: "clone".into(), + symbol: "Clone".into(), + instance: "fixture".into(), + function: "cloneSymbol".into(), + args: vec!["$First".into()], }, - Instruction::Call { - id: Id::from("c_2"), - instance: "libraryInstance".into(), - function: "echo".into(), - args: vec!["$Symbol2".into(), "Arg2".into()], + Instruction::Make { + id: "copy".into(), + instance: "copiedActor".into(), + class: "$Clone".into(), + args: vec![], }, + call("copied", "copiedActor", "read", vec![]), ]); assert_eq!( - vec![ - InstructionResult::ok(Id::from("m_1")), - InstructionResult::ok(Id::from("m_2")), - InstructionResult::string(Id::from("ca_1"), "Arg".into()), - InstructionResult::string(Id::from("c_1"), "Arg in symbol".into()), - InstructionResult::string(Id::from("ca_2"), "LibraryArg".into()), - InstructionResult::string(Id::from("c_2"), "LibraryArg,Arg2".into()), - ], - result + InstructionResult::string("second".into(), "second".into()), + result[6] + ); + assert_eq!( + InstructionResult::string("first".into(), "first".into()), + result[8] + ); + assert_eq!( + InstructionResult::string("copied".into(), "first".into()), + result[11] ); - Ok(()) } #[test] - fn calls_preserve_nested_lists_and_call_and_assign_keeps_the_typed_value( - ) -> Result<(), Box> { - let reader = Cursor::new(Vec::new()); - let writer = Cursor::new(Vec::new()); - let mut slim_server = SlimServer::new(reader, writer); - slim_server.add_fixture::(); - let nested = WireSlimValue::List(vec![ - WireSlimValue::String("one".into()), - WireSlimValue::List(vec![WireSlimValue::String("two".into())]), + fn pop_fixture_reports_an_empty_actor_stack_separately() { + let mut server = server(); + let result = server.execute_instructions(vec![ + Instruction::Make { + id: "fixture".into(), + instance: "scriptTableActor".into(), + class: "Fixture".into(), + args: vec![], + }, + call("pop", "scriptTableActor", "popFixture", vec![]), ]); - let result = slim_server.execute_instructions(vec![ + + assert_eq!( + InstructionResult::exception( + "pop".into(), + ExceptionMessage::new("ACTOR_STACK_EMPTY message:<>".into()) + ), + result[1] + ); + } + + #[test] + fn reports_standard_constructor_and_lookup_errors() { + let mut server = server(); + let result = server.execute_instructions(vec![ Instruction::Make { - id: Id::from("make"), - instance: "fixture".into(), - class: "Test.TestFixture".into(), - args: Vec::new(), + id: "class".into(), + instance: "x".into(), + class: "Nope".into(), + args: vec![], }, - Instruction::CallAndAssign { - id: Id::from("call"), - symbol: "$nested".into(), - instance: "fixture".into(), - function: "nested".into(), - args: vec![nested], + Instruction::Make { + id: "ctor".into(), + instance: "x".into(), + class: "Library".into(), + args: vec![], }, - Instruction::Call { - id: Id::from("embedded"), - instance: "fixture".into(), - function: "echo".into(), - args: vec!["prefix $nested suffix".into()], + call("instance", "missing", "x", vec![]), + Instruction::Make { + id: "conversion".into(), + instance: "x".into(), + class: "FailingFixture".into(), + args: vec!["conversion".into()], + }, + Instruction::Make { + id: "invocation".into(), + instance: "x".into(), + class: "FailingFixture".into(), + args: vec!["invocation".into()], }, ]); - assert_eq!( - InstructionResult::list( - Id::from("call"), - vec![InstructionResultValue::List(vec![ - InstructionResultValue::String("one".into()), - InstructionResultValue::List(vec![InstructionResultValue::String( - "two".into() - )]), - ])], + InstructionResult::exception( + "class".into(), + ExceptionMessage::new("NO_CLASS Nope".into()) + ), + result[0] + ); + assert_eq!( + InstructionResult::exception( + "ctor".into(), + ExceptionMessage::new("NO_CONSTRUCTOR Library".into()) ), result[1] ); - assert!(matches!( - slim_server.symbols.get("nested"), - Some(SlimValue::List(_)) - )); assert_eq!( - InstructionResult::string(Id::from("embedded"), "prefix [[one, [two]]] suffix".into()), + InstructionResult::exception( + "instance".into(), + ExceptionMessage::new("NO_INSTANCE missing".into()) + ), result[2] ); - Ok(()) + assert_eq!( + InstructionResult::exception( + "conversion".into(), + ExceptionMessage::new("NO_CONVERTER_FOR_ARGUMENT_NUMBER i64".into()) + ), + result[3] + ); + assert_eq!( + InstructionResult::exception( + "invocation".into(), + ExceptionMessage::new( + "COULD_NOT_INVOKE_CONSTRUCTOR FailingFixture message:<>" + .into() + ) + ), + result[4] + ); } #[test] - fn execute_assign() -> Result<(), Box> { - let mut vec = Vec::new(); - let reader = Cursor::new(&mut vec); - let mut vec = Vec::new(); - let writer = Cursor::new(&mut vec); - let mut slim_server = SlimServer::new(reader, writer); - slim_server.add_fixture::(); - let result = slim_server.execute_instructions(vec![ + fn missing_targets_still_fall_back_to_libraries_and_preserve_method_spelling() { + let mut server = server(); + let result = server.execute_instructions(vec![ Instruction::Make { - id: Id::from("m_1"), - instance: "Instance".into(), - class: "Test.TestFixture".into(), - args: Vec::new(), - }, - Instruction::Assign { - id: Id::from("a_1"), - symbol: "$Symbol".into(), - value: "Value".into(), + id: "fixture".into(), + instance: "fixture".into(), + class: "Fixture".into(), + args: vec![], }, - Instruction::Call { - id: Id::from("c_1"), - instance: "Instance".into(), - function: "echo".into(), - args: vec!["$Symbol in symbol".into()], + Instruction::Make { + id: "library".into(), + instance: "libraryOne".into(), + class: "Library".into(), + args: vec!["new".into()], }, + call("fallback", "missing", "library", vec![]), + call("method", "fixture", "missingMethod", vec![]), + call( + "helper", + "SlimHelperLibrary", + "cloneSymbol", + vec!["value".into()], + ), ]); assert_eq!( - vec![ - InstructionResult::ok(Id::from("m_1")), - InstructionResult::ok(Id::from("a_1")), - InstructionResult::string(Id::from("c_1"), "Value in symbol".into()), - ], - result + InstructionResult::string("fallback".into(), "newest".into()), + result[2] ); - Ok(()) - } - - #[test] - fn integration() -> Result<(), Box> { - let mut vec = - Vec::from(b"000068:[000001:000051:[000003:000004:id_1:000006:import:000008:TestPath:]:]000003:bye".as_slice()); - let reader: Cursor<&mut Vec> = Cursor::new(&mut vec); - let mut output = Vec::new(); - let writer = Cursor::new(&mut output); - let slim_server = SlimServer::new(reader, writer); - slim_server.run()?; assert_eq!( - "Slim -- V0.5\n000048:[000001:000031:[000002:000004:id_1:000002:OK:]:]", - String::from_utf8_lossy(&output) + InstructionResult::exception( + "method".into(), + ExceptionMessage::new("NO_METHOD_IN_CLASS missingMethod Fixture".into()) + ), + result[3] + ); + assert_eq!( + InstructionResult::string("helper".into(), "value".into()), + result[4] ); - Ok(()) } #[test] - fn malformed_instruction_returns_an_exception_and_keeps_the_batch_running( - ) -> Result<(), Box> { - let mut input = vec![ - vec!["bad_id", "unknown", "field"], - vec!["good_id", "import", "TestPath"], - ] - .to_slim_string() - .as_bytes() - .to_vec(); - input.extend_from_slice("bye".to_slim_string().as_bytes()); - - let mut output = Vec::new(); - SlimServer::new(Cursor::new(input), Cursor::new(&mut output)).run()?; + fn invalid_assignment_names_are_malformed_without_invoking_the_call() { + let mut server = server(); + let result = server.execute_instructions(vec![ + Instruction::Make { + id: "fixture".into(), + instance: "fixture".into(), + class: "Fixture".into(), + args: vec![], + }, + Instruction::CallAndAssign { + id: "bad".into(), + symbol: "Bad1".into(), + instance: "fixture".into(), + function: "echo".into(), + args: vec!["value".into()], + }, + ]); - let mut response = Cursor::new(&output[b"Slim -- V0.5\n".len()..]); assert_eq!( - vec![ - InstructionResult::exception( - Id::from("bad_id"), - ExceptionMessage::new("MALFORMED_INSTRUCTION [bad_id,unknown,field]".into()), - ), - InstructionResult::ok(Id::from("good_id")), - ], - Vec::::from_reader(&mut response)? + InstructionResult::exception( + "bad".into(), + ExceptionMessage::new( + "MALFORMED_INSTRUCTION [bad,callAndAssign,Bad1,fixture,echo,value]".into() + ) + ), + result[1] ); - Ok(()) + assert!(!server.symbols.contains_key("Bad1")); } #[test] - fn parse_symbol_with_no_symbol() -> Result<(), Box> { - let mut vec = Vec::new(); - let reader = Cursor::new(&mut vec); - let mut vec = Vec::new(); - let writer = Cursor::new(&mut vec); - let slim_server = SlimServer::new(reader, writer); - assert_eq!( - "No symbol", - slim_server.parse_symbol(String::from("No symbol")) + fn symbol_substitution_preserves_typed_whole_values_and_walks_nested_lists() { + let mut server = server(); + server + .symbols + .insert("Text".into(), SlimValue::String("value".into())); + server.symbols.insert("Nothing".into(), SlimValue::Null); + server.symbols.insert( + "Items".into(), + SlimValue::List(vec![SlimValue::String("one".into())]), ); - assert_eq!("", slim_server.parse_symbol(String::from(""))); - Ok(()) - } - #[test] - fn parse_symbol_with_symbol_not_in_symbol_list() -> Result<(), Box> { - let mut vec = Vec::new(); - let reader = Cursor::new(&mut vec); - let mut vec = Vec::new(); - let writer = Cursor::new(&mut vec); - let slim_server = SlimServer::new(reader, writer); - assert_eq!("", slim_server.parse_symbol(String::from("$symbol"))); + assert_eq!(SlimValue::Null, server.parse_wire_value("$Nothing".into())); assert_eq!( - "Test ", - slim_server.parse_symbol(String::from("Test $symbol")) + SlimValue::List(vec![SlimValue::String("one".into())]), + server.parse_wire_value("$Items".into()) + ); + assert_eq!( + SlimValue::List(vec![ + SlimValue::String("value".into()), + SlimValue::List(vec![SlimValue::Null, SlimValue::String("$Missing!".into()),]), + ]), + server.parse_wire_value(WireSlimValue::List(vec![ + "$Text".into(), + WireSlimValue::List(vec!["$Nothing".into(), "$Missing!".into()]), + ])) + ); + assert_eq!( + "before null after", + server.replace_symbols("before $Nothing after") ); - Ok(()) } #[test] - fn parse_symbol() -> Result<(), Box> { - let mut vec = Vec::new(); - let reader = Cursor::new(&mut vec); - let mut vec = Vec::new(); - let writer = Cursor::new(&mut vec); - let mut slim_server = SlimServer::new(reader, writer); - slim_server + fn assign_preserves_literal_symbols_while_retaining_lists_and_null() { + let mut server = server(); + server .symbols - .insert("symbol".into(), "Symbol Value".into()); - slim_server - .symbols - .insert("symbol2".into(), "Symbol Value 2".into()); - assert_eq!( - "Symbol Value", - slim_server.parse_symbol(String::from("$symbol")) - ); + .insert("Existing".into(), SlimValue::String("expanded".into())); + let result = server.execute_instructions(vec![ + Instruction::Assign { + id: "literal".into(), + symbol: "Literal".into(), + value: "$Existing".into(), + }, + Instruction::Assign { + id: "list".into(), + symbol: "List".into(), + value: WireSlimValue::List(vec!["$Existing".into(), "null".into()]), + }, + ]); + + assert_eq!(InstructionResult::ok("literal".into()), result[0]); assert_eq!( - "Test Symbol Value", - slim_server.parse_symbol(String::from("Test $symbol")) + Some(&SlimValue::String("$Existing".into())), + server.symbols.get("Literal") ); assert_eq!( - "Test Symbol Value and another Symbol Value 2", - slim_server.parse_symbol(String::from("Test $symbol and another $symbol2")) + Some(&SlimValue::List(vec![ + SlimValue::String("$Existing".into()), + SlimValue::Null, + ])), + server.symbols.get("List") ); - Ok(()) } #[test] - fn find_fixture_should_prioritize_an_exact_match() { - let mut vec = Vec::new(); - let reader = Cursor::new(&mut vec); - let mut vec = Vec::new(); - let writer = Cursor::new(&mut vec); - let mut slim_server = SlimServer::new(reader, writer); - add_test_fixture_with_path(&mut slim_server, "ExamplePathFixutre", Ok("First".into())); - add_test_fixture_with_path( - &mut slim_server, - "Namespace.ExamplePathFixutre", - Ok("Second".into()), - ); - slim_server.imports.push("Namespace".into()); - - let mut result = slim_server.find_fixture("ExamplePathFixutre").unwrap()(Vec::new()); + fn malformed_instructions_keep_the_batch_running() { + let mut server = server(); + let result = server.execute_instructions(vec![ + Instruction::Malformed { + id: "bad".into(), + fields: vec!["bad".into(), "unknown".into()], + }, + Instruction::Import { + id: "good".into(), + path: "Fixtures".into(), + }, + ]); + assert_eq!( - Ok(SlimValue::String("First".to_string())), - result.execute_method("", Vec::new()) + InstructionResult::exception( + "bad".into(), + ExceptionMessage::new("MALFORMED_INSTRUCTION [bad,unknown]".into()) + ), + result[0] ); + assert_eq!(InstructionResult::ok("good".into()), result[1]); + assert_eq!(vec!["Fixtures"], server.imports); } #[test] - fn find_fixture_should_use_the_imports() { - let mut vec = Vec::new(); - let reader = Cursor::new(&mut vec); - let mut vec = Vec::new(); - let writer = Cursor::new(&mut vec); - let mut slim_server = SlimServer::new(reader, writer); - add_test_fixture_with_path( - &mut slim_server, - "Namespace1.ExamplePathFixutre", - Ok("First".into()), - ); - add_test_fixture_with_path( - &mut slim_server, - "Namespace2.ExamplePathFixutre", - Ok("Second".into()), - ); - slim_server.imports.push("Namespace2".into()); - slim_server.imports.push("Namespace1".into()); - - let mut result = slim_server.find_fixture("ExamplePathFixutre").unwrap()(Vec::new()); + fn exact_class_names_win_and_the_newest_import_is_searched_first() { + let mut server = server(); + for (class, value) in [("First.Thing", "oldest"), ("Second.Thing", "newest")] { + server.fixtures.insert( + class.into(), + Box::new(move |_| Ok(SlimObject::fixture(Library { value }, class))), + ); + } + let result = server.execute_instructions(vec![ + Instruction::Import { + id: "first-import".into(), + path: "First".into(), + }, + Instruction::Import { + id: "second-import".into(), + path: "Second".into(), + }, + Instruction::Make { + id: "short".into(), + instance: "short".into(), + class: "Thing".into(), + args: vec![], + }, + call("short-call", "short", "library", vec![]), + Instruction::Make { + id: "exact".into(), + instance: "exact".into(), + class: "First.Thing".into(), + args: vec![], + }, + call("exact-call", "exact", "library", vec![]), + ]); + assert_eq!( - Ok(SlimValue::String("Second".to_string())), - result.execute_method("", Vec::new()) + InstructionResult::string("short-call".into(), "newest".into()), + result[3] ); - } - - fn add_test_fixture_with_path( - server: &mut SlimServer, - class_path: impl Into, - return_value: Result, - ) { - server.fixtures.insert( - class_path.into(), - Box::new(move |_: Vec| { - Box::new(TestFixture { - return_value: return_value.clone(), - }) as Box - }) as Box) -> Box>, + assert_eq!( + InstructionResult::string("exact-call".into(), "oldest".into()), + result[5] ); } - struct TestFixture { - return_value: Result, - } - - impl SlimFixture for TestFixture { - fn execute_method( - &mut self, - method: &str, - parms: Vec, - ) -> Result { - match method { - "echo" => Ok(SlimValue::String( - parms - .into_iter() - .map(|value| value.as_text()) - .collect::>() - .join(","), - )), - "nested" => Ok(SlimValue::List(parms)), - _ => self.return_value.clone(), - } - } - } + #[test] + fn run_writes_a_handshake_response_and_stops_at_bye() { + let mut input = vec![Instruction::Import { + id: "import".into(), + path: "Fixtures".into(), + }] + .to_slim_string() + .as_bytes() + .to_vec(); + input.extend_from_slice("bye".to_slim_string().as_bytes()); + let mut output = Vec::new(); - impl ClassPath for TestFixture { - fn class_path() -> String { - "Test.TestFixture".into() - } - } + SlimServer::new(Cursor::new(input), Cursor::new(&mut output)) + .run() + .unwrap(); - impl Constructor for TestFixture { - fn construct(_: Vec) -> Self { - Self { - return_value: Ok(SlimValue::String("Value".to_string())), - } - } + let mut expected = b"Slim -- V0.5\n".to_vec(); + expected.extend_from_slice( + vec![InstructionResult::ok("import".into())] + .to_slim_string() + .as_bytes(), + ); + assert_eq!(expected, output); } } diff --git a/rust_slim/tests/fixture_macro.rs b/rust_slim/tests/fixture_macro.rs index 5e9e952..ed97b41 100644 --- a/rust_slim/tests/fixture_macro.rs +++ b/rust_slim/tests/fixture_macro.rs @@ -1,7 +1,8 @@ #![cfg(feature = "macros")] use rust_slim::{ - fixture, ClassPath, ExecuteMethodError, FromSlimValue, IntoSlimValue, SlimFixture, SlimValue, + fixture, ClassPath, Constructor, ConstructorError, ExecuteMethodError, FromSlimValue, + IntoSlimValue, SlimFixture, SlimValue, }; use std::marker::PhantomData; @@ -10,7 +11,22 @@ struct MacroFixture; struct Word(String); +#[derive(Default)] struct GenericFixture(PhantomData); +#[derive(Default)] +struct GenericDefaultFixture(PhantomData); +#[derive(Default)] +struct BoundedDefaultFixture(T); + +#[derive(Debug, PartialEq)] +struct Constructed(i64); +struct SutFixture { + sut: Sut, +} +#[derive(Default)] +struct Sut; +#[derive(Debug, PartialEq)] +struct MultiConstructor(i64); #[fixture] impl GenericFixture { @@ -19,6 +35,23 @@ impl GenericFixture { } } +#[fixture] +impl GenericDefaultFixture { + pub fn value(&self) -> &'static str { + "default" + } +} + +#[fixture] +impl BoundedDefaultFixture +where + T: Default, +{ + pub fn value(&self) -> &'static str { + "bounded default" + } +} + impl FromSlimValue for Word { fn from_slim_value(value: SlimValue) -> Result { match value { @@ -36,12 +69,65 @@ impl IntoSlimValue for Word { } } +#[fixture("Test.Constructed")] +impl Constructed { + #[slim(constructor)] + pub fn new(number: i64) -> Result { + if number >= 0 { + Ok(Self(number)) + } else { + Err("negative") + } + } + + pub fn value(&self) -> i64 { + self.0 + } +} + +#[fixture] +impl MultiConstructor { + #[slim(constructor)] + pub fn empty() -> Self { + Self(0) + } + + #[slim(constructor)] + pub fn with_value(value: i64) -> Self { + Self(value) + } +} + +#[fixture] +impl Sut { + pub fn answer(&self) -> i64 { + 42 + } +} + +#[fixture] +impl SutFixture { + #[slim(constructor)] + pub fn new() -> Self { + Self { sut: Sut } + } + + #[slim(sut)] + pub fn system_under_test(&mut self) -> &mut Sut { + &mut self.sut + } +} + #[fixture("Test.MacroFixture")] impl MacroFixture { pub fn helper() -> &'static str { "associated functions are not fixture methods" } + pub fn generic_helper(value: T) -> T { + value + } + pub fn nested(&self, values: Vec>) -> Vec> { values } @@ -97,6 +183,7 @@ fn macro_checks_arity_before_accessing_arguments() { "associated functions are not fixture methods", MacroFixture::helper() ); + assert_eq!(42, MacroFixture::generic_helper(42)); } #[test] @@ -117,3 +204,54 @@ fn macro_supports_generic_fixture_types() { ); assert!(GenericFixture::::class_path().ends_with("GenericFixture")); } + +#[test] +fn macro_generates_typed_fallible_constructors() { + assert_eq!( + Ok(7), + Constructed::construct(vec![SlimValue::String("7".into())]).map(|value| value.0) + ); + assert_eq!( + Err(ConstructorError::NoConstructor), + Constructed::construct(Vec::new()) + ); + assert_eq!( + Err(ConstructorError::CouldNotInvoke("negative".into())), + Constructed::construct(vec![SlimValue::String("-1".into())]) + ); + assert_eq!( + Err(ConstructorError::ArgumentParsingError("i64".into())), + Constructed::construct(vec![SlimValue::String("not a number".into())]) + ); +} + +#[test] +fn macro_forwards_missing_methods_to_the_annotated_sut() { + let mut fixture = SutFixture::construct(Vec::new()).unwrap(); + assert_eq!( + Ok(SlimValue::String("42".into())), + fixture.execute_system_under_test("answer", Vec::new()) + ); +} + +#[test] +fn macro_selects_marked_constructors_by_arity_and_keeps_default_construction() { + assert_eq!( + MultiConstructor(0), + MultiConstructor::construct(Vec::new()).unwrap() + ); + assert_eq!( + MultiConstructor(9), + MultiConstructor::construct(vec![SlimValue::String("9".into())]).unwrap() + ); + assert_eq!( + Err(ConstructorError::NoConstructor), + MultiConstructor::construct(vec![ + SlimValue::String("1".into()), + SlimValue::String("2".into()) + ]) + ); + assert!(MacroFixture::construct(Vec::new()).is_ok()); + assert!(GenericDefaultFixture::>::construct(Vec::new()).is_ok()); + assert!(BoundedDefaultFixture::::construct(Vec::new()).is_ok()); +} diff --git a/rust_slim/tests/ui/fixture_conflicting_markers.rs b/rust_slim/tests/ui/fixture_conflicting_markers.rs new file mode 100644 index 0000000..b8d951c --- /dev/null +++ b/rust_slim/tests/ui/fixture_conflicting_markers.rs @@ -0,0 +1,14 @@ +use rust_slim::fixture; + +struct Fixture; + +#[fixture] +impl Fixture { + #[slim(constructor)] + #[slim(sut)] + pub fn conflicting() -> Self { + Self + } +} + +fn main() {} diff --git a/rust_slim/tests/ui/fixture_conflicting_markers.stderr b/rust_slim/tests/ui/fixture_conflicting_markers.stderr new file mode 100644 index 0000000..6b389c9 --- /dev/null +++ b/rust_slim/tests/ui/fixture_conflicting_markers.stderr @@ -0,0 +1,5 @@ +error: a fixture method may carry only one `#[slim(...)]` marker + --> tests/ui/fixture_conflicting_markers.rs:8:5 + | +8 | #[slim(sut)] + | ^^^^^^^^^^^^ diff --git a/rust_slim/tests/ui/fixture_constructor_receiver.rs b/rust_slim/tests/ui/fixture_constructor_receiver.rs new file mode 100644 index 0000000..cf8687e --- /dev/null +++ b/rust_slim/tests/ui/fixture_constructor_receiver.rs @@ -0,0 +1,13 @@ +use rust_slim::fixture; + +struct Fixture; + +#[fixture] +impl Fixture { + #[slim(constructor)] + pub fn new(&self) -> Self { + Self + } +} + +fn main() {} diff --git a/rust_slim/tests/ui/fixture_constructor_receiver.stderr b/rust_slim/tests/ui/fixture_constructor_receiver.stderr new file mode 100644 index 0000000..b1d2fe4 --- /dev/null +++ b/rust_slim/tests/ui/fixture_constructor_receiver.stderr @@ -0,0 +1,5 @@ +error: a `#[slim(constructor)]` method must be an associated function + --> tests/ui/fixture_constructor_receiver.rs:8:9 + | +8 | pub fn new(&self) -> Self { + | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/rust_slim/tests/ui/fixture_constructor_return.rs b/rust_slim/tests/ui/fixture_constructor_return.rs new file mode 100644 index 0000000..5129ca1 --- /dev/null +++ b/rust_slim/tests/ui/fixture_constructor_return.rs @@ -0,0 +1,13 @@ +use rust_slim::fixture; + +struct Fixture; + +#[fixture] +impl Fixture { + #[slim(constructor)] + pub fn new() -> i64 { + 42 + } +} + +fn main() {} diff --git a/rust_slim/tests/ui/fixture_constructor_return.stderr b/rust_slim/tests/ui/fixture_constructor_return.stderr new file mode 100644 index 0000000..6a32ef6 --- /dev/null +++ b/rust_slim/tests/ui/fixture_constructor_return.stderr @@ -0,0 +1,5 @@ +error: a `#[slim(constructor)]` method must return `Self` or `Result` + --> tests/ui/fixture_constructor_return.rs:8:18 + | +8 | pub fn new() -> i64 { + | ^^^^^^ diff --git a/rust_slim/tests/ui/fixture_duplicate_constructor_arity.rs b/rust_slim/tests/ui/fixture_duplicate_constructor_arity.rs new file mode 100644 index 0000000..6b14328 --- /dev/null +++ b/rust_slim/tests/ui/fixture_duplicate_constructor_arity.rs @@ -0,0 +1,18 @@ +use rust_slim::fixture; + +struct Fixture; + +#[fixture] +impl Fixture { + #[slim(constructor)] + pub fn one(_value: i64) -> Self { + Self + } + + #[slim(constructor)] + pub fn another(_other: String) -> Self { + Self + } +} + +fn main() {} diff --git a/rust_slim/tests/ui/fixture_duplicate_constructor_arity.stderr b/rust_slim/tests/ui/fixture_duplicate_constructor_arity.stderr new file mode 100644 index 0000000..6de7db2 --- /dev/null +++ b/rust_slim/tests/ui/fixture_duplicate_constructor_arity.stderr @@ -0,0 +1,5 @@ +error: a fixture may declare only one constructor with 1 arguments + --> tests/ui/fixture_duplicate_constructor_arity.rs:13:9 + | +13 | pub fn another(_other: String) -> Self { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/rust_slim/tests/ui/fixture_sut_shared_reference.rs b/rust_slim/tests/ui/fixture_sut_shared_reference.rs new file mode 100644 index 0000000..5a2de45 --- /dev/null +++ b/rust_slim/tests/ui/fixture_sut_shared_reference.rs @@ -0,0 +1,19 @@ +use rust_slim::fixture; + +#[derive(Default)] +struct Fixture { + sut: Sut, +} + +#[derive(Default)] +struct Sut; + +#[fixture] +impl Fixture { + #[slim(sut)] + pub fn system_under_test(&self) -> &Sut { + &self.sut + } +} + +fn main() {} diff --git a/rust_slim/tests/ui/fixture_sut_shared_reference.stderr b/rust_slim/tests/ui/fixture_sut_shared_reference.stderr new file mode 100644 index 0000000..839b6d9 --- /dev/null +++ b/rust_slim/tests/ui/fixture_sut_shared_reference.stderr @@ -0,0 +1,5 @@ +error: a `#[slim(sut)]` method must have an &mut self receiver and no arguments + --> tests/ui/fixture_sut_shared_reference.rs:14:30 + | +14 | pub fn system_under_test(&self) -> &Sut { + | ^^^^^ diff --git a/rust_slim/tests/ui/fixture_sut_signature.rs b/rust_slim/tests/ui/fixture_sut_signature.rs new file mode 100644 index 0000000..e82bcc0 --- /dev/null +++ b/rust_slim/tests/ui/fixture_sut_signature.rs @@ -0,0 +1,13 @@ +use rust_slim::fixture; + +struct Fixture; + +#[fixture] +impl Fixture { + #[slim(sut)] + pub fn system_under_test() -> Fixture { + Fixture + } +} + +fn main() {} diff --git a/rust_slim/tests/ui/fixture_sut_signature.stderr b/rust_slim/tests/ui/fixture_sut_signature.stderr new file mode 100644 index 0000000..5c3cba7 --- /dev/null +++ b/rust_slim/tests/ui/fixture_sut_signature.stderr @@ -0,0 +1,5 @@ +error: a `#[slim(sut)]` method must have an &mut self receiver + --> tests/ui/fixture_sut_signature.rs:8:9 + | +8 | pub fn system_under_test() -> Fixture { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/rust_slim_macros/src/lib.rs b/rust_slim_macros/src/lib.rs index e03780d..241209b 100644 --- a/rust_slim_macros/src/lib.rs +++ b/rust_slim_macros/src/lib.rs @@ -1,14 +1,13 @@ use proc_macro::TokenStream; use quote::quote; use syn::{ - parse::Parser, parse_macro_input, Error, FnArg, ImplItem, ItemImpl, LitStr, Result, Type, - Visibility, + parse::Parser, parse_macro_input, parse_quote, Attribute, Error, FnArg, GenericArgument, + ImplItem, ImplItemFn, ItemImpl, LitStr, PathArguments, Result, ReturnType, Type, Visibility, }; -/// Exposes the public instance methods in an implementation block as SliM -/// fixture methods. -/// -/// Unsupported signatures produce compile errors at the method declaration. +/// Exposes public instance methods in an implementation block as SliM fixture +/// methods. `#[slim(constructor)]` marks associated constructors and +/// `#[slim(sut)]` marks the zero-argument System Under Test accessor. #[proc_macro_attribute] pub fn fixture(args: TokenStream, input: TokenStream) -> TokenStream { let impl_fixture = parse_macro_input!(input as ItemImpl); @@ -18,128 +17,332 @@ pub fn fixture(args: TokenStream, input: TokenStream) -> TokenStream { } } -fn expand_fixture(args: TokenStream, impl_fixture: ItemImpl) -> Result { - if impl_fixture.trait_.is_some() { +fn expand_fixture(args: TokenStream, mut item: ItemImpl) -> Result { + if item.trait_.is_some() { return Err(Error::new_spanned( - &impl_fixture, + &item, "fixture must be applied to an inherent impl block", )); } - let (impl_generics, _type_generics, where_clause) = impl_fixture.generics.split_for_impl(); - let ty = &impl_fixture.self_ty; + let ty = &item.self_ty; let class_path = get_class_path(args, ty)?; - let impl_methods = impl_slim_fixture_methods(ty, &class_path, &impl_fixture.items)?; + let (constructors, sut) = extract_markers(&mut item.items)?; + let methods = impl_slim_fixture_methods(ty, &class_path, &item.items, sut)?; + let (impl_generics, _, where_clause) = item.generics.split_for_impl(); + let constructor = impl_constructor(ty, &item.generics, constructors)?; Ok(quote! { - #impl_fixture - - impl #impl_generics ::rust_slim::SlimFixture for #ty #where_clause { - #impl_methods + #item + impl #impl_generics ::rust_slim::SlimFixture for #ty #where_clause { #methods } + impl #impl_generics ::rust_slim::ClassPath for #ty #where_clause { + fn class_path() -> String { #class_path } } + #constructor + }) +} - impl #impl_generics ::rust_slim::ClassPath for #ty #where_clause { - fn class_path() -> String { - #class_path +fn extract_markers(items: &mut [ImplItem]) -> Result<(Vec, Option)> { + let mut constructors = Vec::new(); + let mut sut = None; + for item in items { + let ImplItem::Fn(method) = item else { continue }; + let mut retained = Vec::new(); + let mut marker = None; + for attribute in std::mem::take(&mut method.attrs) { + if attribute.path().is_ident("slim") { + let name = slim_marker(&attribute)?; + if marker.replace(name).is_some() { + return Err(Error::new_spanned( + attribute, + "a fixture method may carry only one `#[slim(...)]` marker", + )); + } + } else { + retained.push(attribute); } } - }) + method.attrs = retained; + match marker.as_deref() { + Some("constructor") => constructors.push(method.clone()), + Some("sut") if sut.replace(method.clone()).is_some() => { + return Err(Error::new_spanned( + method, + "a fixture may declare only one `sut` method", + )); + } + Some("sut") => {} + Some(name) => { + return Err(Error::new_spanned( + method, + format!("unknown SliM fixture marker `{name}`"), + )); + } + None => {} + } + } + Ok((constructors, sut)) +} + +fn slim_marker(attribute: &Attribute) -> Result { + Ok(attribute.parse_args::()?.to_string()) +} + +fn validate_signature(method: &ImplItemFn) -> Result<()> { + if method.sig.constness.is_some() + || method.sig.asyncness.is_some() + || method.sig.unsafety.is_some() + || method.sig.abi.is_some() + || method.sig.variadic.is_some() + { + return Err(Error::new_spanned( + &method.sig, + "const, async, unsafe, extern, and variadic fixture methods are not supported", + )); + } + if !method.sig.generics.params.is_empty() { + return Err(Error::new_spanned( + &method.sig.generics, + "generic fixture methods are not supported", + )); + } + Ok(()) } fn impl_slim_fixture_methods( ty: &Type, class_path: &proc_macro2::TokenStream, items: &[ImplItem], + sut: Option, ) -> Result { + let sut_name = sut.as_ref().map(|method| method.sig.ident.to_string()); let mut methods = Vec::new(); for item in items { - let ImplItem::Fn(impl_fn) = item else { - continue; - }; - if !matches!(impl_fn.vis, Visibility::Public(_)) { - continue; - } - - if impl_fn.sig.constness.is_some() - || impl_fn.sig.asyncness.is_some() - || impl_fn.sig.unsafety.is_some() - || impl_fn.sig.abi.is_some() - || impl_fn.sig.variadic.is_some() + let ImplItem::Fn(method) = item else { continue }; + if !matches!(method.vis, Visibility::Public(_)) + || sut_name.as_deref() == Some(&method.sig.ident.to_string()) { - return Err(Error::new_spanned( - &impl_fn.sig, - "const, async, unsafe, extern, and variadic fixture methods are not supported", - )); + continue; } - - let Some(FnArg::Receiver(receiver)) = impl_fn.sig.inputs.first() else { - // Associated functions are not callable SliM methods. Constructor - // annotations will process them separately when constructor - // support is added. + let Some(FnArg::Receiver(receiver)) = method.sig.inputs.first() else { continue; }; + validate_signature(method)?; if receiver.reference.is_none() || receiver.colon_token.is_some() { return Err(Error::new_spanned( receiver, "fixture methods must use an &self or &mut self receiver", )); } - if !impl_fn.sig.generics.params.is_empty() { + methods.push(invocation_arm(ty, class_path, method)); + } + let sut_method = if let Some(sut) = sut { + validate_signature(&sut)?; + let Some(FnArg::Receiver(receiver)) = sut.sig.inputs.first() else { + return Err(Error::new_spanned( + &sut.sig, + "a `#[slim(sut)]` method must have an &mut self receiver", + )); + }; + if receiver.reference.is_none() + || receiver.mutability.is_none() + || receiver.colon_token.is_some() + || sut.sig.inputs.len() != 1 + { + return Err(Error::new_spanned( + receiver, + "a `#[slim(sut)]` method must have an &mut self receiver and no arguments", + )); + } + if !matches!(&sut.sig.output, ReturnType::Type(_, output) if matches!(output.as_ref(), Type::Reference(reference) if reference.mutability.is_some())) + { return Err(Error::new_spanned( - &impl_fn.sig.generics, - "generic fixture methods are not supported", + &sut.sig.output, + "a `#[slim(sut)]` method must return a mutable fixture reference", )); } + let ident = &sut.sig.ident; + quote! { + fn execute_system_under_test(&mut self, method: &str, args: ::std::vec::Vec<::rust_slim::SlimValue>) + -> ::std::result::Result<::rust_slim::SlimValue, ::rust_slim::ExecuteMethodError> { + ::rust_slim::SlimFixture::execute_method(<#ty>::#ident(self), method, args) + } + } + } else { + quote! {} + }; + Ok(quote! { + fn execute_method(&mut self, method: &str, args: ::std::vec::Vec<::rust_slim::SlimValue>) + -> ::std::result::Result<::rust_slim::SlimValue, ::rust_slim::ExecuteMethodError> { + match method { + #(#methods,)* + _ => Err(::rust_slim::ExecuteMethodError::MethodNotFound { method: method.to_string(), class: #class_path }), + } + } + #sut_method + }) +} - let arity = impl_fn.sig.inputs.len() - 1; - let method = impl_fn.sig.ident.to_string(); - let ident = &impl_fn.sig.ident; - let arguments = impl_fn - .sig - .inputs - .iter() - .skip(1) - .enumerate() - .map(|(index, argument)| { - let FnArg::Typed(argument) = argument else { - unreachable!("only the first fixture argument can be a receiver") - }; - let ty = &argument.ty; - quote! { - <#ty as ::rust_slim::FromSlimValue>::from_slim_value( - supplied_args.next().expect("argument count checked before conversion"), - ).map_err(|error| error.for_argument(#index))? - } - }); - methods.push(quote! { - #method => { - if args.len() != #arity { - return Err(::rust_slim::ExecuteMethodError::MethodNotFound { - method: method.to_string(), - class: #class_path, - }); +fn invocation_arm( + ty: &Type, + class_path: &proc_macro2::TokenStream, + method: &ImplItemFn, +) -> proc_macro2::TokenStream { + let arity = method.sig.inputs.len() - 1; + let name = method.sig.ident.to_string(); + let ident = &method.sig.ident; + let arguments = method.sig.inputs.iter().skip(1).map(|argument| { + let FnArg::Typed(argument) = argument else { unreachable!() }; + let ty = &argument.ty; + quote! { + <#ty as ::rust_slim::FromSlimValue>::from_slim_value(supplied.next().expect("argument count checked")) + .map_err(|error| match error { + ::rust_slim::ExecuteMethodError::ArgumentParsingError(_) => + ::rust_slim::ExecuteMethodError::ArgumentParsingError(stringify!(#ty).into()), + error => error, + })? + } + }); + quote! { + #name => { + if args.len() != #arity { + return Err(::rust_slim::ExecuteMethodError::MethodNotFound { method: method.to_string(), class: #class_path }); + } + let mut supplied = args.into_iter(); + ::rust_slim::IntoSlimValue::into_slim_value(<#ty>::#ident(self, #(#arguments),*)) + } + } +} + +fn impl_constructor( + ty: &Type, + impl_generics: &syn::Generics, + constructors: Vec, +) -> Result { + if constructors.is_empty() { + let mut default_generics = impl_generics.clone(); + default_generics + .make_where_clause() + .predicates + .push(parse_quote!(#ty: ::std::default::Default)); + let (impl_generics, _, where_clause) = default_generics.split_for_impl(); + return Ok(quote! { + impl #impl_generics ::rust_slim::Constructor for #ty #where_clause { + fn construct(args: ::std::vec::Vec<::rust_slim::SlimValue>) + -> ::std::result::Result { + if args.is_empty() { Ok(::std::default::Default::default()) } + else { Err(::rust_slim::ConstructorError::NoConstructor) } } - let mut supplied_args = args.into_iter(); - ::rust_slim::IntoSlimValue::into_slim_value(<#ty>::#ident(self, #(#arguments),*)) } }); } + + let mut arities = std::collections::BTreeSet::new(); + let mut arms = Vec::new(); + for method in constructors { + validate_signature(&method)?; + if method + .sig + .inputs + .first() + .is_some_and(|argument| matches!(argument, FnArg::Receiver(_))) + { + return Err(Error::new_spanned( + &method.sig, + "a `#[slim(constructor)]` method must be an associated function", + )); + } + if !constructor_returns_fixture(&method.sig.output, ty) { + return Err(Error::new_spanned( + &method.sig.output, + "a `#[slim(constructor)]` method must return `Self` or `Result`", + )); + } + let arity = method.sig.inputs.len(); + if !arities.insert(arity) { + return Err(Error::new_spanned( + &method.sig, + format!("a fixture may declare only one constructor with {arity} arguments"), + )); + } + arms.push(constructor_arm(ty, &method)); + } + let (impl_generics, _, where_clause) = impl_generics.split_for_impl(); Ok(quote! { - fn execute_method( - &mut self, - method: &str, - args: ::std::vec::Vec<::rust_slim::SlimValue>, - ) -> ::std::result::Result<::rust_slim::SlimValue, ::rust_slim::ExecuteMethodError> { - match method { - #(#methods,)* - _ => Err(::rust_slim::ExecuteMethodError::MethodNotFound { - method: method.to_string(), - class: #class_path, - }), + impl #impl_generics ::rust_slim::Constructor for #ty #where_clause { + fn construct(args: ::std::vec::Vec<::rust_slim::SlimValue>) + -> ::std::result::Result { + match args.len() { + #(#arms,)* + _ => Err(::rust_slim::ConstructorError::NoConstructor), + } } } }) } +fn constructor_arm(ty: &Type, method: &ImplItemFn) -> proc_macro2::TokenStream { + let ident = &method.sig.ident; + let arity = method.sig.inputs.len(); + let arguments = method.sig.inputs.iter().map(|argument| { + let FnArg::Typed(argument) = argument else { unreachable!() }; + let ty = &argument.ty; + quote! { + <#ty as ::rust_slim::FromSlimValue>::from_slim_value(supplied.next().expect("argument count checked")) + .map_err(|error| match error { + ::rust_slim::ExecuteMethodError::ArgumentParsingError(_) => + ::rust_slim::ConstructorError::ArgumentParsingError(stringify!(#ty).into()), + error => ::rust_slim::ConstructorError::CouldNotInvoke(error.to_string()), + })? + } + }); + let invoke = quote! { <#ty>::#ident(#(#arguments),*) }; + let result = if returns_result(&method.sig.output) { + quote! { #invoke.map_err(|error| ::rust_slim::ConstructorError::CouldNotInvoke(error.to_string())) } + } else { + quote! { Ok(#invoke) } + }; + quote! { + #arity => { + let mut supplied = args.into_iter(); + #result + } + } +} + +fn returns_result(return_type: &ReturnType) -> bool { + let ReturnType::Type(_, ty) = return_type else { + return false; + }; + matches!(ty.as_ref(), Type::Path(path) if path.path.segments.last().is_some_and(|segment| segment.ident == "Result")) +} + +fn constructor_returns_fixture(return_type: &ReturnType, fixture_type: &Type) -> bool { + let ReturnType::Type(_, output) = return_type else { + return false; + }; + if is_fixture_type(output, fixture_type) { + return true; + } + let Type::Path(path) = output.as_ref() else { + return false; + }; + let Some(segment) = path.path.segments.last() else { + return false; + }; + if segment.ident != "Result" { + return false; + } + let PathArguments::AngleBracketed(arguments) = &segment.arguments else { + return false; + }; + matches!(arguments.args.first(), Some(GenericArgument::Type(ok)) if is_fixture_type(ok, fixture_type)) +} + +fn is_fixture_type(candidate: &Type, fixture_type: &Type) -> bool { + matches!(candidate, Type::Path(path) if path.qself.is_none() && path.path.is_ident("Self")) + || quote!(#candidate).to_string() == quote!(#fixture_type).to_string() +} + fn get_class_path(args: TokenStream, ty: &Type) -> Result { let parser = syn::punctuated::Punctuated::::parse_terminated; let args = parser.parse(args)?; @@ -152,7 +355,6 @@ fn get_class_path(args: TokenStream, ty: &Type) -> Result Result) -> Result>()?, }), - "callAndAssign" if fields.len() >= 5 => Ok(Instruction::CallAndAssign { - id, - symbol: fields[2].clone(), - instance: fields[3].clone(), - function: fields[4].clone(), - args: fields[5..] - .iter() - .cloned() - .map(parse_slim_value) - .collect::>()?, - }), - "assign" if fields.len() == 4 => Ok(Instruction::Assign { + "callAndAssign" if fields.len() >= 5 && is_symbol_name(&fields[2]) => { + Ok(Instruction::CallAndAssign { + id, + symbol: fields[2].clone(), + instance: fields[3].clone(), + function: fields[4].clone(), + args: fields[5..] + .iter() + .cloned() + .map(parse_slim_value) + .collect::>()?, + }) + } + "assign" if fields.len() == 4 && is_symbol_name(&fields[2]) => Ok(Instruction::Assign { id, symbol: fields[2].clone(), value: parse_slim_value(fields[3].clone())?, @@ -207,6 +209,10 @@ fn instruction_from_values(fields: Vec) -> Result bool { + !value.is_empty() && value.bytes().all(|byte| byte.is_ascii_alphabetic()) +} + /// Reads a complete message. The outer length is counted in UTF-8 bytes. fn read_outer_payload(reader: &mut impl BufRead) -> Result { let length = read_length_prefix(reader)?; @@ -436,6 +442,24 @@ mod test { Ok(()) } + #[test] + fn symbol_assignment_instruction_names_are_letters_only() -> Result<(), Box> { + for fields in [ + vec!["bad", "assign", "has_underscore", "value"], + vec!["bad", "callAndAssign", "has1digit", "fixture", "method"], + ] { + let wire = fields.to_slim_string(); + assert_eq!( + Instruction::Malformed { + id: Id::from("bad"), + fields: fields.into_iter().map(str::to_owned).collect(), + }, + Instruction::from_reader(&mut Cursor::new(wire.as_bytes()))? + ); + } + Ok(()) + } + #[test] fn all_instruction_forms_round_trip() -> Result<(), Box> { let instructions = vec![ diff --git a/temoc/examples/calculator.rs b/temoc/examples/calculator.rs index 25f6125..9047956 100644 --- a/temoc/examples/calculator.rs +++ b/temoc/examples/calculator.rs @@ -16,6 +16,11 @@ mod fixtures { #[fixture] impl CalculatorFixture { + #[slim(constructor)] + pub fn new() -> Self { + Self::default() + } + pub fn set_a(&mut self, a: i64) { self.a = a }