diff --git a/Cargo.lock b/Cargo.lock index e0711546..6b62b688 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,7 @@ dependencies = [ "air-log-targets", "air-parser", "air-test-utils", + "air-testing-framework", "air-trace-handler", "air-utils", "boolinator", @@ -175,6 +176,19 @@ dependencies = [ "serde_json", ] +[[package]] +name = "air-testing-framework" +version = "0.1.0" +dependencies = [ + "air-test-utils", + "itertools 0.10.3", + "maplit", + "nom 7.1.1", + "nom_locate", + "serde_json", + "strum", +] + [[package]] name = "air-trace" version = "0.2.0" @@ -383,6 +397,12 @@ version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +[[package]] +name = "bytecount" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" + [[package]] name = "byteorder" version = "1.4.3" @@ -918,7 +938,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "047f670b4807cab8872550a607b1515daff08b3e3bb7576ce8f45971fd811a4e" dependencies = [ "it-to-bytes", - "nom", + "nom 5.1.2", "serde", "variant_count", "wast", @@ -1371,7 +1391,7 @@ dependencies = [ "itertools 0.10.3", "marine-it-interfaces", "marine-module-interface", - "nom", + "nom 5.1.2", "semver 0.11.0", "serde", "thiserror", @@ -1439,7 +1459,7 @@ dependencies = [ "anyhow", "itertools 0.10.3", "marine-it-interfaces", - "nom", + "nom 5.1.2", "semver 0.11.0", "serde", "thiserror", @@ -1549,6 +1569,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "multimap" version = "0.8.3" @@ -1588,6 +1614,27 @@ dependencies = [ "version_check", ] +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom_locate" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37794436ca3029a3089e0b95d42da1f0b565ad271e4d3bb4bad0c7bb70b10605" +dependencies = [ + "bytecount", + "memchr", + "nom 7.1.1", +] + [[package]] name = "non-empty-vec" version = "0.2.3" @@ -2239,6 +2286,9 @@ name = "strum" version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" +dependencies = [ + "strum_macros", +] [[package]] name = "strum_macros" @@ -2722,7 +2772,7 @@ dependencies = [ "it-to-bytes", "itertools 0.10.3", "log", - "nom", + "nom 5.1.2", "safe-transmute", "semver 0.11.0", "serde", diff --git a/Cargo.toml b/Cargo.toml index b54e380f..189583a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "crates/air-lib/utils", "crates/beautifier", "crates/data-store", + "crates/testing-framework", "tools/cli/air-beautify", "tools/cli/air-trace", ] diff --git a/air/Cargo.toml b/air/Cargo.toml index 116337f1..cdd1ee7c 100644 --- a/air/Cargo.toml +++ b/air/Cargo.toml @@ -45,6 +45,7 @@ wasm-bindgen = "=0.2.65" [dev_dependencies] air-test-utils = { path = "../crates/air-lib/test-utils" } +air-testing-framework = { path = "../crates/testing-framework" } fluence-app-service = "0.18.0" marine-rs-sdk = { version = "0.7.0", features = ["logger"] } diff --git a/air/tests/test_module/issues/issue_178.rs b/air/tests/test_module/issues/issue_178.rs index 9d6aa1b9..76d5d4e9 100644 --- a/air/tests/test_module/issues/issue_178.rs +++ b/air/tests/test_module/issues/issue_178.rs @@ -16,45 +16,36 @@ use air_test_utils::prelude::*; -use std::collections::HashSet; - #[test] // https://github.com/fluencelabs/aquavm/issues/178 fn par_ap_behaviour() { let client_id = "client_id"; let relay_id = "relay_id"; let variable_setter_id = "variable_setter_id"; - let mut client = create_avm(unit_call_service(), client_id); - let mut relay = create_avm(unit_call_service(), relay_id); - let mut variable_setter = create_avm(unit_call_service(), variable_setter_id); + // ap doesn't affect the subgraph_complete flag let script = f!(r#" (par - (call "{variable_setter_id}" ("peer" "timeout") [] join_it) + (call "{variable_setter_id}" ("peer" "timeout") [] join_it) ; behaviour=unit (seq (par - (call "{relay_id}" ("peer" "timeout") [join_it] $result) - (ap "fast_result" $result) ;; ap doesn't affect the subgraph_complete flag + (call "{relay_id}" ("peer" "timeout") [join_it] $result) ; behaviour=unit + (ap "fast_result" $result) ) - (call "{client_id}" ("op" "return") [$result.$[0]]) + (call "{client_id}" ("op" "return") [$result.$[0]]) ; behaviour=unit ) ) "#); - let mut client_result_1 = checked_call_vm!(client, <_>::default(), &script, "", ""); - let actual_next_peers: HashSet<_> = client_result_1.next_peer_pks.drain(..).collect(); - let expected_next_peers: HashSet<_> = maplit::hashset!(relay_id.to_string(), variable_setter_id.to_string()); - assert_eq!(actual_next_peers, expected_next_peers); + let engine = air_test_framework::TestExecutor::simple(TestRunParameters::new("client_id", 0, 1), &script) + .expect("invalid test executor config"); - let setter_result = checked_call_vm!( - variable_setter, - <_>::default(), - &script, - "", - client_result_1.data.clone() - ); + let client_result_1 = engine.execute_one(client_id).unwrap(); + assert_next_pks!(&client_result_1.next_peer_pks, [relay_id, variable_setter_id]); + + let setter_result = engine.execute_one(variable_setter_id).unwrap(); assert!(setter_result.next_peer_pks.is_empty()); - let relay_result = checked_call_vm!(relay, <_>::default(), script, "", client_result_1.data); + let relay_result = engine.execute_one(relay_id).unwrap(); assert!(relay_result.next_peer_pks.is_empty()); } diff --git a/air/tests/test_module/issues/issue_211.rs b/air/tests/test_module/issues/issue_211.rs index 296925cc..b4dc95df 100644 --- a/air/tests/test_module/issues/issue_211.rs +++ b/air/tests/test_module/issues/issue_211.rs @@ -21,15 +21,6 @@ use air_test_utils::prelude::*; // On the versions < 0.20.1 it just crashes fn issue_211() { let peer_1_id = "peer_1_id"; - let variables_mapping = maplit::hashmap! { - "idx".to_string() => json!(2), - "nodes".to_string() => json!([1,2,3]), - }; - - let mut peer_1 = create_avm( - set_variables_call_service(variables_mapping, VariableOptionSource::FunctionName), - peer_1_id, - ); let script = f!(r#" (xor @@ -38,9 +29,9 @@ fn issue_211() { (seq (seq (null) - (call %init_peer_id% ("getDataSrv" "idx") [] idx) + (call %init_peer_id% ("getDataSrv" "idx") [] idx) ; ok=2 ) - (call %init_peer_id% ("getDataSrv" "nodes") [] nodes) + (call %init_peer_id% ("getDataSrv" "nodes") [] nodes) ; ok = [1,2,3] ) (new $nodes2 (seq @@ -54,20 +45,23 @@ fn issue_211() { ) (null) ) - (call %init_peer_id% ("op" "noop") [$nodes2.$.[idx]! nodes]) + (call %init_peer_id% ("op" "noop") [$nodes2.$.[idx] nodes]) ; ok="expected result" ) - (call %init_peer_id% ("op" "identity") [$nodes2] nodes2-fix) + (call %init_peer_id% ("op" "identity") [$nodes2] nodes2-fix) ; ok="expected result" ) ) ) (null) ) - (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2]) + (call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2]) ; ok="error" ) "#); let run_params = TestRunParameters::from_init_peer_id(peer_1_id); - let result = checked_call_vm!(peer_1, run_params, &script, "", ""); + + let engine = air_test_framework::TestExecutor::simple(run_params, &script).expect("invalid test executor config"); + + let result = engine.execute_one(peer_1_id).unwrap(); let expected_trace = vec![ executed_state::scalar_number(2), @@ -79,8 +73,8 @@ fn issue_211() { executed_state::ap(Some(0)), executed_state::par(1, 0), executed_state::ap(Some(0)), - executed_state::scalar_string("default result from set_variables_call_service"), - executed_state::scalar_string("default result from set_variables_call_service"), + executed_state::scalar_string("expected result"), + executed_state::scalar_string("expected result"), ]; let actual_trace = trace_from_result(&result); diff --git a/avm/interface/src/call_service_result.rs b/avm/interface/src/call_service_result.rs index faee5b52..c2827f9d 100644 --- a/avm/interface/src/call_service_result.rs +++ b/avm/interface/src/call_service_result.rs @@ -24,7 +24,7 @@ pub type CallResults = HashMap; pub const CALL_SERVICE_SUCCESS: i32 = 0; /// Represents an executed host function result. -#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct CallServiceResult { /// A error code service or builtin returned, where CALL_SERVICE_SUCCESS represents success. pub ret_code: i32, diff --git a/crates/testing-framework/Cargo.toml b/crates/testing-framework/Cargo.toml new file mode 100644 index 00000000..6ef80b01 --- /dev/null +++ b/crates/testing-framework/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "air-testing-framework" +version = "0.1.0" +description = "AquaVM testing framework" +authors = ["Fluence Labs"] +edition = "2018" +license = "Apache-2.0" +repository = "https://github.com/fluencelabs/aquavm/tree/master/crates/test-framework" +publish = false +keywords = ["fluence", "air", "test"] + +[lib] +name = "air_test_framework" +path = "src/lib.rs" + +[dependencies] +air-test-utils = { path = "../air-lib/test-utils" } + +itertools = "0.10.3" +strum = { version="0.21.0", features=["derive"] } +nom = "7.1.1" +nom_locate = "4.0.0" +serde_json = "1.0.61" + +[dev-dependencies] +maplit = "1.0.2" +# We do not want to depend on wasm binary path +air-test-utils = { path = "../air-lib/test-utils", features = ["test_with_native_code"] } diff --git a/crates/testing-framework/src/asserts/mod.rs b/crates/testing-framework/src/asserts/mod.rs new file mode 100644 index 00000000..550dc576 --- /dev/null +++ b/crates/testing-framework/src/asserts/mod.rs @@ -0,0 +1,46 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pub(crate) mod parser; + +use crate::services::JValue; + +use air_test_utils::CallServiceResult; +use strum::{AsRefStr, EnumDiscriminants, EnumString}; + +use std::collections::HashMap; + +/// Service definition in the testing framework comment DSL. +#[derive(Debug, PartialEq, Eq, Clone, EnumDiscriminants)] +#[strum_discriminants(derive(AsRefStr, EnumString))] +#[strum_discriminants(name(ServiceTagName))] +pub enum ServiceDefinition { + /// Simple service that returns same value + #[strum_discriminants(strum(serialize = "ok"))] + Ok(JValue), + /// Simple service that returns same call result (i.e. may return a error) + #[strum_discriminants(strum(serialize = "err"))] + Error(CallServiceResult), + /// Service that may return a new value on subsequent call. Its keys are either + /// call number string starting from "0", or "default". + // TODO We need to return error results too, so we need to define a call result + // for default and individual errors. + #[strum_discriminants(strum(serialize = "seq_result"))] + SeqResult(HashMap), + /// Some known service by name: "echo", "unit" (more to follow). + #[strum_discriminants(strum(serialize = "behaviour"))] + Behaviour(String), +} diff --git a/crates/testing-framework/src/asserts/parser.rs b/crates/testing-framework/src/asserts/parser.rs new file mode 100644 index 00000000..669875ec --- /dev/null +++ b/crates/testing-framework/src/asserts/parser.rs @@ -0,0 +1,186 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use super::{ServiceDefinition, ServiceTagName}; +use crate::services::JValue; + +use air_test_utils::CallServiceResult; +use nom::{error::VerboseError, IResult, InputTakeAtPosition, Parser}; + +use std::{collections::HashMap, str::FromStr}; + +type ParseError<'inp> = VerboseError<&'inp str>; + +impl FromStr for ServiceDefinition { + type Err = String; + + fn from_str(s: &str) -> Result { + nom::combinator::all_consuming(parse_kw)(s) + .map(|(_, service_definition)| service_definition) + .map_err(|e| e.to_string()) + } +} + +// kw "=" val +// example: "id=firstcall" +pub fn parse_kw(inp: &str) -> IResult<&str, ServiceDefinition, ParseError> { + use nom::branch::alt; + use nom::bytes::complete::tag; + use nom::combinator::{cut, map_res, rest}; + use nom::error::context; + use nom::sequence::separated_pair; + + let equal = || delim_ws(tag("=")); + + delim_ws(map_res( + separated_pair( + alt(( + tag(ServiceTagName::Ok.as_ref()), + tag(ServiceTagName::Error.as_ref()), + tag(ServiceTagName::SeqResult.as_ref()), + tag(ServiceTagName::Behaviour.as_ref()), + )), + equal(), + cut(context( + "result value is consumed to end and has to be a valid JSON", + rest, + )), + ), + |(tag, value): (&str, &str)| { + let value = value.trim(); + match ServiceTagName::from_str(tag) { + Ok(ServiceTagName::Ok) => { + serde_json::from_str::(value).map(ServiceDefinition::Ok) + } + Ok(ServiceTagName::Error) => { + serde_json::from_str::(value).map(ServiceDefinition::Error) + } + Ok(ServiceTagName::SeqResult) => { + serde_json::from_str::>(value) + .map(ServiceDefinition::SeqResult) + } + Ok(ServiceTagName::Behaviour) => Ok(ServiceDefinition::Behaviour(value.to_owned())), + Err(_) => unreachable!("unknown tag {:?}", tag), + } + }, + ))(inp) +} + +pub(crate) fn delim_ws(f: F) -> impl FnMut(I) -> IResult +where + F: Parser, + E: nom::error::ParseError, + I: InputTakeAtPosition, + ::Item: nom::AsChar + Clone, +{ + use nom::character::complete::multispace0; + use nom::sequence::delimited; + + delimited(multispace0, f, multispace0) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_empty() { + let res = ServiceDefinition::from_str(""); + assert!(res.is_err()); + } + + #[test] + fn test_parse_garbage0() { + let res = ServiceDefinition::from_str("garbage"); + assert!(res.is_err(), "{:?}", res); + } + + #[test] + fn test_result_service() { + use serde_json::json; + + let res = ServiceDefinition::from_str(r#"ok={"this":["is","value"]}"#); + assert_eq!( + res, + Ok(ServiceDefinition::Ok(json!({"this": ["is", "value"]}))), + ); + } + + #[test] + fn test_result_service_malformed() { + let res = ServiceDefinition::from_str(r#"ok={"this":["is","value"]"#); + assert!(res.is_err()); + } + + #[test] + fn test_call_result() { + use serde_json::json; + + let res = ServiceDefinition::from_str(r#"err={"ret_code": 0, "result": [1, 2, 3]}"#); + assert_eq!( + res, + Ok(ServiceDefinition::Error(CallServiceResult::ok(json!([ + 1, 2, 3 + ])))), + ); + } + + #[test] + fn test_call_result_malformed() { + let res = ServiceDefinition::from_str(r#"err={"retcode": 0, "result": [1, 2, 3]}"#); + assert!(res.is_err()); + } + + #[test] + fn test_call_result_invalid() { + let res = ServiceDefinition::from_str(r#"err={"ret_code": 0, "result": 1, 2, 3]}"#); + assert!(res.is_err()); + } + + #[test] + fn test_seq_result() { + use serde_json::json; + + let res = ServiceDefinition::from_str(r#"seq_result={"default": 42, "1": true, "3": []}"#); + assert_eq!( + res, + Ok(ServiceDefinition::SeqResult(maplit::hashmap! { + "default".to_owned() => json!(42), + "1".to_owned() => json!(true), + "3".to_owned() => json!([]), + })), + ); + } + + #[test] + fn test_seq_result_malformed() { + let res = ServiceDefinition::from_str(r#"seq_result={"default": 42, "1": true, "3": ]}"#); + assert!(res.is_err()); + } + + #[test] + fn test_seq_result_invalid() { + // TODO perhaps, we should support both arrays and maps + let res = ServiceDefinition::from_str(r#"seq_result=[42, 43]"#); + assert!(res.is_err()); + } + + #[test] + fn test_behaviour() { + let res = ServiceDefinition::from_str(r#"behaviour=echo"#); + assert_eq!(res, Ok(ServiceDefinition::Behaviour("echo".to_owned())),); + } +} diff --git a/crates/testing-framework/src/ephemeral/mod.rs b/crates/testing-framework/src/ephemeral/mod.rs new file mode 100644 index 00000000..6c674683 --- /dev/null +++ b/crates/testing-framework/src/ephemeral/mod.rs @@ -0,0 +1,258 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pub mod neighborhood; + +use self::neighborhood::{PeerEnv, PeerSet}; +use crate::services::{services_to_call_service_closure, MarineServiceHandle}; + +use air_test_utils::{ + test_runner::{create_avm, TestRunParameters, TestRunner}, + RawAVMOutcome, +}; + +use std::{ + borrow::Borrow, + cell::RefCell, + collections::{HashMap, HashSet}, + hash::Hash, + rc::Rc, +}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct PeerId(String); + +impl PeerId { + pub fn new(peer_id: impl Into) -> Self { + Self(peer_id.into()) + } +} +impl From for PeerId { + fn from(source: String) -> Self { + Self(source) + } +} + +impl From<&str> for PeerId { + fn from(source: &str) -> Self { + Self(source.to_owned()) + } +} + +impl Borrow for PeerId { + fn borrow(&self) -> &str { + &self.0 + } +} + +pub type Data = Vec; + +pub struct Peer { + peer_id: PeerId, + // We presume that only one particle is run over the network. + prev_data: Data, + runner: TestRunner, +} + +impl Peer { + pub fn new(peer_id: impl Into, services: Rc<[MarineServiceHandle]>) -> Self { + let peer_id = Into::into(peer_id); + let call_service = services_to_call_service_closure(services); + let runner = create_avm(call_service, &peer_id.0); + + Self { + peer_id, + prev_data: vec![], + runner, + } + } + + pub fn invoke( + &mut self, + air: impl Into, + data: Data, + test_run_params: TestRunParameters, + ) -> Result { + let mut prev_data = vec![]; + std::mem::swap(&mut prev_data, &mut self.prev_data); + let res = self.runner.call(air, prev_data, data, test_run_params); + if let Ok(outcome) = &res { + self.prev_data = outcome.data.clone(); + } + res + } +} + +impl std::fmt::Debug for Peer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Peer") + .field("peer_id", &self.peer_id) + .field("prev_data", &self.prev_data) + .field("services", &"...") + .finish() + } +} + +#[derive(Debug)] +pub struct Network { + peers: HashMap>>, + default_neighborhood: HashSet, +} + +impl Network { + pub fn empty() -> Self { + Self::new(std::iter::empty::<&str>()) + } + + pub fn new(default_neiborhoud: impl Iterator>) -> Self { + Self { + peers: Default::default(), + default_neighborhood: default_neiborhoud.map(Into::into).collect(), + } + } + + pub fn from_peers(nodes: Vec) -> Self { + let mut network = Self::empty(); + let neighborhood: PeerSet = nodes.iter().map(|peer| peer.peer_id.clone()).collect(); + for peer in nodes { + network.add_peer_env(peer, neighborhood.iter().cloned()); + } + network + } + + pub fn add_peer_env( + &mut self, + peer: Peer, + neighborhood: impl IntoIterator>, + ) -> &mut PeerEnv { + let peer_id = peer.peer_id.clone(); + let mut peer_env = PeerEnv::new(peer); + peer_env.extend_neighborhood(neighborhood.into_iter()); + self.insert_peer_env_entry(peer_id, peer_env) + } + + /// Add a peer with default neighborhood. + pub fn add_peer(&mut self, peer: Peer) -> &mut PeerEnv { + let peer_id = peer.peer_id.clone(); + let mut peer_env = PeerEnv::new(peer); + peer_env.extend_neighborhood(self.default_neighborhood.iter().cloned()); + self.insert_peer_env_entry(peer_id, peer_env) + } + + fn insert_peer_env_entry(&mut self, peer_id: PeerId, peer_env: PeerEnv) -> &mut PeerEnv { + let peer_env = Rc::new(peer_env.into()); + // It will be simplified with entry_insert stabilization + // https://github.com/rust-lang/rust/issues/65225 + let cell = match self.peers.entry(peer_id) { + std::collections::hash_map::Entry::Occupied(ent) => { + let cell = ent.into_mut(); + *cell = peer_env; + cell + } + std::collections::hash_map::Entry::Vacant(ent) => ent.insert(peer_env), + }; + // never panics because Rc have been just created and there's just single reference + Rc::get_mut(cell).unwrap().get_mut() + } + + pub fn set_peer_failed(&mut self, peer_id: &Id, failed: bool) + where + PeerId: Borrow, + Id: Hash + Eq + ?Sized, + { + self.peers + .get_mut(peer_id) + .expect("unknown peer") + .as_ref() + .borrow_mut() + .set_failed(failed); + } + + pub fn fail_peer_for(&mut self, source_peer_id: &Id, target_peer_id: impl Into) + where + PeerId: Borrow, + Id: Hash + Eq + ?Sized, + { + self.peers + .get_mut(source_peer_id) + .expect("unknown peer") + .as_ref() + .borrow_mut() + .get_neighborhood_mut() + .set_target_unreachable(target_peer_id); + } + + pub fn unfail_peer_for(&mut self, source_peer_id: &Id1, target_peer_id: &Id2) + where + PeerId: Borrow, + Id1: Hash + Eq + ?Sized, + PeerId: Borrow, + Id2: Hash + Eq + ?Sized, + { + self.peers + .get_mut(source_peer_id) + .expect("unknown peer") + .as_ref() + .borrow_mut() + .get_neighborhood_mut() + .unset_target_unreachable(target_peer_id); + } + + // TODO there is some kind of unsymmetry between these methods and the fail/unfail: + // the latters panic on unknown peer; perhaps, it's OK + pub fn get_peer_env(&self, peer_id: &Id) -> Option>> + where + PeerId: Borrow, + Id: Hash + Eq + ?Sized, + { + self.peers.get(peer_id).cloned() + } + + /// Iterator for handling al the queued data. It borrows peer env's `RefCell` only temporarily. + /// Following test-utils' call_vm macro, it panics on failed VM. + pub fn execution_iter<'s, Id>( + &'s self, + air: &'s str, + test_parameters: &'s TestRunParameters, + peer_id: &Id, + ) -> Option + 's> + where + PeerId: Borrow, + Id: Eq + Hash + ?Sized, + { + let peer_env = self.get_peer_env(peer_id); + + peer_env.map(|peer_env_cell| { + std::iter::from_fn(move || { + let mut peer_env = peer_env_cell.borrow_mut(); + peer_env + .execute_once(air, self, test_parameters) + .map(|r| r.unwrap_or_else(|err| panic!("VM call failed: {}", err))) + }) + }) + } + + pub fn distribute_to_peers(&self, peers: &[String], data: &Data) { + for peer_id in peers { + if let Some(peer_env_cell) = self.get_peer_env(peer_id.as_str()) { + peer_env_cell + .borrow_mut() + .data_queue + .push_back(data.clone()); + } + } + } +} diff --git a/crates/testing-framework/src/ephemeral/neighborhood.rs b/crates/testing-framework/src/ephemeral/neighborhood.rs new file mode 100644 index 00000000..8ab31b28 --- /dev/null +++ b/crates/testing-framework/src/ephemeral/neighborhood.rs @@ -0,0 +1,395 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use super::{Data, Network, Peer, PeerId}; + +use air_test_utils::test_runner::TestRunParameters; + +use std::{ + borrow::Borrow, + collections::{HashMap, HashSet, VecDeque}, + hash::Hash, + ops::Deref, +}; + +pub(crate) type PeerSet = HashSet; + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum LinkState { + Reachable, + Unreachable, +} + +/// Neighbors of particular node, including set of nodes unreachable from this one (but they might be +/// reachable from others). +#[derive(Debug, Default)] +pub struct Neighborhood { + // the value is true is link from this peer to neighbor is failng + neighbors: HashMap, +} + +impl Neighborhood { + pub fn new() -> Self { + Default::default() + } + + pub fn set_neighbors(&mut self, neighbors: PeerSet) { + self.neighbors = neighbors + .into_iter() + .map(|peer_id| (peer_id, LinkState::Reachable)) + .collect(); + } + + pub fn iter(&self) -> impl Iterator { + self.into_iter() + } + + pub fn insert(&mut self, other_peer_id: impl Into) { + let other_peer_id = other_peer_id.into(); + self.neighbors.insert(other_peer_id, LinkState::Reachable); + } + + /// Removes the other_peer_id from neighborhood, also removes unreachable status. + pub fn remove(&mut self, other_peer_id: &Id) + where + PeerId: Borrow, + Id: Eq + Hash + ?Sized, + { + self.neighbors.remove(other_peer_id); + } + + pub fn set_target_unreachable(&mut self, target: impl Into) { + *self.neighbors.get_mut(&target.into()).unwrap() = LinkState::Unreachable; + } + + pub fn unset_target_unreachable(&mut self, target: &Id) + where + PeerId: Borrow, + Id: Eq + Hash + ?Sized, + { + *self.neighbors.get_mut(target).unwrap() = LinkState::Reachable; + } + + pub fn is_reachable(&self, target: impl Deref) -> bool { + let target_peer_id = target.deref(); + self.neighbors.get(target_peer_id) == Some(&LinkState::Reachable) + } +} + +impl<'a> std::iter::IntoIterator for &'a Neighborhood { + type Item = &'a PeerId; + + type IntoIter = std::collections::hash_map::Keys<'a, PeerId, LinkState>; + + fn into_iter(self) -> Self::IntoIter { + self.neighbors.keys() + } +} + +#[derive(Debug)] +pub struct PeerEnv { + pub(crate) peer: Peer, + // failed for everyone + failed: bool, + neighborhood: Neighborhood, + pub(crate) data_queue: VecDeque, +} + +impl PeerEnv { + pub fn new(peer: Peer) -> Self { + Self { + peer, + failed: false, + neighborhood: Default::default(), + data_queue: Default::default(), + } + } + + pub fn is_failed(&self) -> bool { + self.failed + } + + pub fn set_failed(&mut self, failed: bool) { + self.failed = failed; + } + + pub fn is_reachable(&self, target: impl Deref) -> bool { + if self.is_failed() { + return false; + } + + let target_peer_id = target.deref(); + if &self.peer.peer_id == target_peer_id { + return true; + } + + self.neighborhood.is_reachable(target) + } + + pub fn extend_neighborhood(&mut self, peers: impl Iterator>) { + let peer_id = self.peer.peer_id.clone(); + for other_peer_id in peers + .map(Into::into) + .filter(|other_id| other_id != &peer_id) + { + self.neighborhood.insert(other_peer_id); + } + } + + pub fn remove_from_neighborhood<'a, Id>(&mut self, peers: impl Iterator) + where + PeerId: std::borrow::Borrow, + Id: Eq + Hash + ?Sized + 'a, + { + for peer_id in peers { + self.neighborhood.remove(peer_id); + } + } + + pub fn get_neighborhood(&self) -> &Neighborhood { + &self.neighborhood + } + + pub fn get_neighborhood_mut(&mut self) -> &mut Neighborhood { + &mut self.neighborhood + } + + pub fn iter(&self) -> impl Iterator { + self.neighborhood.iter() + } + + pub fn send_data(&mut self, data: Data) { + self.data_queue.push_back(data); + } + + pub fn execute_once( + &mut self, + air: impl Into, + network: &Network, + test_parameters: &TestRunParameters, + ) -> Option> { + let maybe_data = self.data_queue.pop_front(); + + maybe_data.map(|data| { + let res = self.peer.invoke(air, data, test_parameters.clone()); + + if let Ok(outcome) = &res { + network.distribute_to_peers(&outcome.next_peer_pks, &outcome.data) + } + + res + }) + } +} + +impl<'a> IntoIterator for &'a PeerEnv { + type Item = <&'a Neighborhood as IntoIterator>::Item; + type IntoIter = <&'a Neighborhood as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.neighborhood.into_iter() + } +} + +#[cfg(test)] +mod tests { + use std::{iter::FromIterator, rc::Rc}; + + use super::*; + + #[test] + fn test_empty_neighborhood() { + let peer_id: PeerId = "someone".into(); + let other_id: PeerId = "other".into(); + let pwn = PeerEnv::new(Peer::new(peer_id.clone(), Rc::from(vec![]))); + assert!(pwn.is_reachable(&peer_id)); + assert!(!pwn.is_reachable(&other_id)); + } + + #[test] + fn test_no_self_disconnect() { + let peer_id: PeerId = "someone".into(); + let other_id: PeerId = "other".into(); + let mut pwn = PeerEnv::new(Peer::new(peer_id.clone(), Rc::from(vec![]))); + let nei = pwn.get_neighborhood_mut(); + nei.insert(peer_id.clone()); + nei.remove(&peer_id); + assert!(pwn.is_reachable(&peer_id)); + assert!(!pwn.is_reachable(&other_id)); + } + + #[test] + fn test_set_neighborhood() { + let peer_id: PeerId = "someone".into(); + let other_id1: PeerId = "other1".into(); + let other_id2: PeerId = "other2".into(); + let mut pwn = PeerEnv::new(Peer::new(peer_id.clone(), Rc::from(vec![]))); + + // iter is empty + assert!(pwn.iter().next().is_none()); + + let expected_neighborhood = PeerSet::from([other_id1.clone(), other_id2.clone()]); + pwn.get_neighborhood_mut() + .set_neighbors(expected_neighborhood.clone()); + assert_eq!( + pwn.iter().cloned().collect::(), + expected_neighborhood + ); + } + + #[test] + fn test_insert() { + let peer_id: PeerId = "someone".into(); + let other_id1: PeerId = "other1".into(); + let other_id2: PeerId = "other2".into(); + let mut pwn = PeerEnv::new(Peer::new(peer_id.clone(), Rc::from(vec![]))); + + // iter is empty + assert!(pwn.iter().next().is_none()); + let nei = pwn.get_neighborhood_mut(); + + nei.insert(other_id1.clone()); + nei.insert(other_id2.clone()); + let expected_neighborhood = PeerSet::from([other_id1.clone(), other_id2.clone()]); + assert_eq!( + PeerSet::from_iter(pwn.iter().cloned()), + expected_neighborhood + ); + } + + #[test] + fn test_insert_insert() { + let peer_id: PeerId = "someone".into(); + let other_id1: PeerId = "other1".into(); + let mut pwn = PeerEnv::new(Peer::new(peer_id.clone(), Rc::from(vec![]))); + + // iter is empty + assert!(pwn.iter().next().is_none()); + + let nei = pwn.get_neighborhood_mut(); + nei.insert(other_id1.clone()); + nei.insert(other_id1.clone()); + let expected_neighborhood = vec![other_id1]; + assert_eq!( + pwn.iter().cloned().collect::>(), + expected_neighborhood + ); + } + + #[test] + fn test_extend_neighborhood() { + let peer_id: PeerId = "someone".into(); + let mut pwn = PeerEnv::new(Peer::new(peer_id.clone(), Rc::from(vec![]))); + pwn.get_neighborhood_mut().insert("zero"); + pwn.extend_neighborhood(IntoIterator::into_iter(["one", "two"])); + + assert_eq!( + PeerSet::from_iter(pwn.iter().cloned()), + PeerSet::from_iter(IntoIterator::into_iter(["zero", "one", "two"]).map(PeerId::from)), + ); + } + + #[test] + fn test_remove_from_neiborhood() { + let peer_id: PeerId = "someone".into(); + let mut pwn = PeerEnv::new(Peer::new(peer_id.clone(), Rc::from(vec![]))); + pwn.get_neighborhood_mut().insert("zero"); + pwn.extend_neighborhood(IntoIterator::into_iter(["one", "two"])); + pwn.remove_from_neighborhood(IntoIterator::into_iter(["zero", "two"])); + + assert_eq!( + pwn.iter().cloned().collect::>(), + IntoIterator::into_iter(["one"]) + .map(PeerId::from) + .collect::>() + ); + } + #[test] + fn test_fail() { + let peer_id: PeerId = "someone".into(); + let other_id: PeerId = "other".into(); + let mut pwn = PeerEnv::new(Peer::new(peer_id.clone(), Rc::from(vec![]))); + let nei = pwn.get_neighborhood_mut(); + nei.insert(other_id.clone()); + nei.set_target_unreachable(other_id.clone()); + + let expected_neighborhood = PeerSet::from([other_id.clone()]); + assert_eq!( + PeerSet::from_iter(pwn.iter().cloned()), + expected_neighborhood + ); + assert!(!pwn.is_reachable(&other_id)); + } + + #[test] + fn test_fail_remove() { + let peer_id: PeerId = "someone".into(); + let other_id: PeerId = "other".into(); + let mut pwn = PeerEnv::new(Peer::new(peer_id.clone(), Rc::from(vec![]))); + + let nei = pwn.get_neighborhood_mut(); + nei.insert(other_id.clone()); + nei.set_target_unreachable(other_id.clone()); + assert!(!pwn.is_reachable(&other_id)); + + let nei = pwn.get_neighborhood_mut(); + nei.remove(&other_id); + assert!(!pwn.is_reachable(&other_id)); + + let nei = pwn.get_neighborhood_mut(); + nei.insert(other_id.clone()); + assert!(pwn.is_reachable(&other_id)); + } + + #[test] + fn test_fail_unfail() { + let peer_id: PeerId = "someone".into(); + let other_id: PeerId = "other".into(); + let mut pwn = PeerEnv::new(Peer::new(peer_id.clone(), Rc::from(vec![]))); + + let nei = pwn.get_neighborhood_mut(); + nei.insert(other_id.clone()); + nei.set_target_unreachable(other_id.clone()); + assert!(!pwn.is_reachable(&other_id)); + + let nei = pwn.get_neighborhood_mut(); + nei.unset_target_unreachable(&other_id); + assert!(pwn.is_reachable(&other_id)); + } + + #[test] + fn test_failed() { + let peer_id: PeerId = "someone".into(); + let other_id: PeerId = "other".into(); + let remote_id: PeerId = "remote".into(); + let mut pwn = PeerEnv::new(Peer::new(peer_id.clone(), Rc::from(vec![]))); + pwn.get_neighborhood_mut().insert(other_id.clone()); + + assert!(pwn.is_reachable(&peer_id)); + assert!(pwn.is_reachable(&other_id)); + assert!(!pwn.is_reachable(&remote_id)); + + pwn.set_failed(true); + assert!(!pwn.is_reachable(&peer_id)); + assert!(!pwn.is_reachable(&other_id)); + assert!(!pwn.is_reachable(&remote_id)); + + pwn.set_failed(false); + assert!(pwn.is_reachable(&peer_id)); + assert!(pwn.is_reachable(&other_id)); + assert!(!pwn.is_reachable(&remote_id)); + } +} diff --git a/crates/testing-framework/src/execution/mod.rs b/crates/testing-framework/src/execution/mod.rs new file mode 100644 index 00000000..b5c5f84d --- /dev/null +++ b/crates/testing-framework/src/execution/mod.rs @@ -0,0 +1,358 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use crate::{ + asserts::ServiceDefinition, + ephemeral::{Network, Peer, PeerId}, + services::{results::ResultService, MarineService, MarineServiceHandle}, + transform::{walker::Transformer, Sexp}, +}; + +use air_test_utils::{test_runner::TestRunParameters, RawAVMOutcome}; + +use std::{borrow::Borrow, collections::HashMap, hash::Hash, rc::Rc, str::FromStr}; + +pub struct TestExecutor { + pub air_script: String, + pub network: Network, + pub test_parameters: TestRunParameters, +} + +impl TestExecutor { + /// Create execution from the annotated air script. + /// + /// `extra_peers` allows you to define peers that are not mentioned in the annotated script + /// explicitly, but are used, e.g. if their names are returned from a call. + pub fn new( + test_parameters: TestRunParameters, + common_services: Vec, + extra_peers: impl IntoIterator, + annotated_air_script: &str, + ) -> Result { + let mut sexp = Sexp::from_str(annotated_air_script)?; + let mut walker = Transformer::new(); + walker.transform(&mut sexp); + + let init_peer_id = test_parameters.init_peer_id.clone(); + let transformed_air_script = sexp.to_string(); + + let peers = build_peers( + common_services, + walker.results, + walker.peers, + PeerId::new(init_peer_id.clone()), + extra_peers, + )?; + + let network = Network::from_peers(peers); + // Seed execution + network.distribute_to_peers(&[init_peer_id], &vec![]); + + Ok(TestExecutor { + air_script: transformed_air_script, + network, + test_parameters, + }) + } + + /// Simple constructor where everything is generated from the annotated_air_script. + pub fn simple( + test_parameters: TestRunParameters, + annotated_air_script: &str, + ) -> Result { + Self::new( + test_parameters, + <_>::default(), + std::iter::empty(), + annotated_air_script, + ) + } + + /// Return Iterator for handling all the queued datas + /// for particular peer_id. + pub fn execution_iter<'s, Id>( + &'s self, + peer_id: &Id, + ) -> Option + 's> + where + PeerId: Borrow, + // TODO it's not clear why compiler requies + 's here, but not at Network::iter_execution + Id: Eq + Hash + ?Sized + 's, + { + self.network + .execution_iter(&self.air_script, &self.test_parameters, peer_id) + } + + /// Process all queued datas, panicing on error. + pub fn execute_all(&self, peer_id: &Id) -> Option> + where + PeerId: Borrow, + Id: Eq + Hash + ?Sized, + { + self.execution_iter(peer_id).map(|it| it.collect()) + } + + /// Process one queued data, panicing if it is unavalable or on error. + pub fn execute_one(&self, peer_id: &Id) -> Option + where + PeerId: Borrow, + Id: Eq + Hash + ?Sized, + { + self.execution_iter(peer_id) + .map(|mut it| it.next().unwrap()) + } +} + +fn build_peers( + common_services: Vec, + results: HashMap, + known_peers: std::collections::HashSet, + init_peer_id: PeerId, + extra_peers: impl IntoIterator, +) -> Result, String> { + let mut result_services: Vec = + Vec::with_capacity(1 + common_services.len()); + result_services.push(ResultService::new(results)?.to_handle()); + result_services.extend(common_services); + let result_services = Rc::<[_]>::from(result_services); + + let extra_peers_pairs = extra_peers + .into_iter() + .chain(std::iter::once(init_peer_id)) + .map(|peer_id| (peer_id.clone(), Peer::new(peer_id, result_services.clone()))); + let mut peers = extra_peers_pairs.collect::>(); + + let known_peers_pairs = known_peers + .into_iter() + .map(|peer_id| (peer_id.clone(), Peer::new(peer_id, result_services.clone()))); + peers.extend(known_peers_pairs); + + Ok(peers.into_values().collect()) +} + +#[cfg(test)] +mod tests { + use air_test_utils::prelude::*; + + use super::*; + + #[test] + fn test_execution() { + let exec = TestExecutor::new( + TestRunParameters::from_init_peer_id("init_peer_id"), + vec![], + std::iter::empty(), + r#"(seq +(call "peer1" ("service" "func") [] arg) ; ok=42 +(call "peer2" ("service" "func") [arg]) ; ok=43 +) +"#, + ) + .unwrap(); + + let result_init: Vec<_> = exec.execution_iter("init_peer_id").unwrap().collect(); + + assert_eq!(result_init.len(), 1); + let outcome = &result_init[0]; + assert_eq!(outcome.next_peer_pks, vec!["peer1".to_owned()]); + + assert!(exec.execution_iter("peer2").unwrap().next().is_none()); + let results1: Vec<_> = exec.execution_iter("peer1").unwrap().collect(); + assert_eq!(results1.len(), 1); + let outcome1 = &results1[0]; + assert_eq!(outcome1.ret_code, 0); + assert!(exec.execution_iter("peer1").unwrap().next().is_none()); + + let outcome2 = exec.execute_one("peer2").unwrap(); + assert_eq!(outcome2.ret_code, 0); + } + + #[test] + fn test_call_result_success() { + let exec = TestExecutor::new( + TestRunParameters::from_init_peer_id("init_peer_id"), + vec![], + std::iter::empty(), + r#"(seq +(call "peer1" ("service" "func") [] arg) ; err = {"ret_code":0,"result":42} +(call "peer2" ("service" "func") [arg]) ; ok = 43 +) +"#, + ) + .unwrap(); + + let result_init: Vec<_> = exec.execution_iter("init_peer_id").unwrap().collect(); + + assert_eq!(result_init.len(), 1); + let outcome1 = &result_init[0]; + assert_eq!(outcome1.ret_code, 0); + assert_eq!(outcome1.error_message, ""); + + assert!(exec.execution_iter("peer2").unwrap().next().is_none()); + let results1: Vec<_> = exec.execution_iter("peer1").unwrap().collect(); + assert_eq!(results1.len(), 1); + let outcome1 = &results1[0]; + assert_eq!(outcome1.ret_code, 0, "{:?}", outcome1); + assert!(exec.execution_iter("peer1").unwrap().next().is_none()); + } + + #[test] + fn test_call_result_error() { + let exec = TestExecutor::new( + TestRunParameters::from_init_peer_id("init_peer_id"), + vec![], + std::iter::empty(), + r#"(seq +(call "peer1" ("service" "func") [] arg) ; err = {"ret_code":12,"result":"ERROR MESSAGE"} +(call "peer2" ("service" "func") [arg]) ; ok = 43 +) +"#, + ) + .unwrap(); + + let result_init: Vec<_> = exec.execution_iter("init_peer_id").unwrap().collect(); + + assert_eq!(result_init.len(), 1); + let outcome1 = &result_init[0]; + assert_eq!(outcome1.ret_code, 0); + assert_eq!(outcome1.error_message, ""); + + assert!(exec.execution_iter("peer2").unwrap().next().is_none()); + let results1: Vec<_> = exec.execution_iter("peer1").unwrap().collect(); + assert_eq!(results1.len(), 1); + let outcome1 = &results1[0]; + assert_eq!(outcome1.ret_code, 10000, "{:?}", outcome1); + assert_eq!( + outcome1.error_message, + "Local service error, ret_code is 12, error message is '\"ERROR MESSAGE\"'", + "{:?}", + outcome1 + ); + assert!(exec.execution_iter("peer1").unwrap().next().is_none()); + + let results2: Vec<_> = exec.execution_iter("peer2").unwrap().collect(); + assert_eq!(results2.len(), 0); + } + + #[test] + fn test_seq_result() { + let exec = TestExecutor::new( + TestRunParameters::from_init_peer_id("init_peer_id"), + vec![], + IntoIterator::into_iter(["peer2", "peer3"]).map(Into::into), + r#"(seq + (seq + (call "peer1" ("service" "func") [] var) ; ok = [{"p":"peer2","v":2},{"p":"peer3","v":3}] + (seq + (ap 1 k) + (fold var i + (seq + (call i.$.p ("service" "func") [i k] k) ; seq_result = {"0":12,"default":42} + (next i))))) + (call "init_peer_id" ("a" "b") []) ; ok = 0 +)"#, + ) + .unwrap(); + + let result_init: Vec<_> = exec.execution_iter("init_peer_id").unwrap().collect(); + + assert_eq!(result_init.len(), 1); + let outcome1 = &result_init[0]; + assert_eq!(outcome1.ret_code, 0); + assert_eq!(outcome1.error_message, ""); + + assert!(exec.execution_iter("peer2").unwrap().next().is_none()); + { + let results1 = exec.execute_all("peer1").unwrap(); + assert_eq!(results1.len(), 1); + let outcome1 = &results1[0]; + assert_eq!(outcome1.ret_code, 0, "{:?}", outcome1); + assert!(exec.execution_iter("peer1").unwrap().next().is_none()); + assert_next_pks!(&outcome1.next_peer_pks, ["peer2"]); + } + + { + let results2: Vec<_> = exec.execute_all("peer2").unwrap(); + assert_eq!(results2.len(), 1); + let outcome2 = &results2[0]; + assert_eq!(outcome2.ret_code, 0, "{:?}", outcome2); + assert!(exec.execution_iter("peer2").unwrap().next().is_none()); + assert_next_pks!(&outcome2.next_peer_pks, ["peer3"]); + + let trace = trace_from_result(outcome2); + assert_eq!( + trace, + ExecutionTrace::from(vec![ + scalar(json!([{"p":"peer2","v":2},{"p":"peer3","v":3},])), + scalar_number(12), + request_sent_by("peer2"), + ]) + ); + } + + { + let results3: Vec<_> = exec.execute_all("peer3").unwrap(); + assert_eq!(results3.len(), 1); + let outcome3 = &results3[0]; + assert_eq!(outcome3.ret_code, 0, "{:?}", outcome3); + assert!(exec.execution_iter("peer3").unwrap().next().is_none()); + + let trace = trace_from_result(outcome3); + assert_eq!( + trace, + ExecutionTrace::from(vec![ + scalar(json!([{"p":"peer2","v":2},{"p":"peer3","v":3},])), + scalar_number(12), + request_sent_by("peer2"), + ]) + ); + } + } + + #[test] + fn test_echo() { + let exec = TestExecutor::new( + TestRunParameters::from_init_peer_id("init_peer_id"), + vec![], + std::iter::empty(), + r#"(seq +(call "peer1" ("service" "func") [1 22] arg) ; behaviour=echo +(call "peer2" ("service" "func") [arg]) ; ok = 43 +) +"#, + ) + .unwrap(); + + let result_init: Vec<_> = exec.execution_iter("init_peer_id").unwrap().collect(); + + assert_eq!(result_init.len(), 1); + let outcome0 = &result_init[0]; + assert_eq!(outcome0.ret_code, 0); + assert_eq!(outcome0.error_message, ""); + + assert!(exec.execution_iter("peer2").unwrap().next().is_none()); + let results1: Vec<_> = exec.execution_iter("peer1").unwrap().collect(); + assert_eq!(results1.len(), 1); + let outcome1 = &results1[0]; + assert_eq!(outcome1.ret_code, 0, "{:?}", outcome1); + assert!(exec.execution_iter("peer1").unwrap().next().is_none()); + + assert_eq!( + trace_from_result(outcome1), + ExecutionTrace::from(vec![scalar_number(1), request_sent_by("peer1"),]), + ) + } +} diff --git a/crates/testing-framework/src/lib.rs b/crates/testing-framework/src/lib.rs new file mode 100644 index 00000000..04dd56a2 --- /dev/null +++ b/crates/testing-framework/src/lib.rs @@ -0,0 +1,23 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pub mod asserts; +pub mod ephemeral; +pub mod execution; +pub mod services; +pub mod transform; + +pub use execution::TestExecutor; diff --git a/crates/testing-framework/src/services/mod.rs b/crates/testing-framework/src/services/mod.rs new file mode 100644 index 00000000..efd4c0c7 --- /dev/null +++ b/crates/testing-framework/src/services/mod.rs @@ -0,0 +1,70 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +pub(crate) mod results; + +use air_test_utils::{CallRequestParams, CallServiceClosure, CallServiceResult}; + +use std::{cell::RefCell, rc::Rc, time::Duration}; + +pub type JValue = serde_json::Value; + +/// Somewhat modified type from fluence. The Duration defines when the caller receives it, imitating +/// real execution time. +#[derive(Debug)] +pub enum FunctionOutcome { + ServiceResult(CallServiceResult, Duration), + NotDefined, + Empty, +} + +/// A mocked Marine service. +pub trait MarineService { + fn call(&self, params: CallRequestParams) -> FunctionOutcome; + + fn to_handle(self) -> MarineServiceHandle + where + Self: Sized + 'static, + { + MarineServiceHandle(Rc::new(RefCell::new(Box::new(self)))) + } +} + +#[derive(Clone)] +pub struct MarineServiceHandle(Rc>>); + +impl MarineService for MarineServiceHandle { + fn call(&self, params: CallRequestParams) -> FunctionOutcome { + let mut guard = self.0.borrow_mut(); + MarineService::call(guard.as_mut(), params) + } +} + +pub(crate) fn services_to_call_service_closure( + services: Rc<[MarineServiceHandle]>, +) -> CallServiceClosure { + Box::new(move |params: CallRequestParams| -> CallServiceResult { + for service_handler in services.as_ref() { + let outcome = service_handler.call(params.clone()); + match outcome { + FunctionOutcome::ServiceResult(result, _) => return result, + FunctionOutcome::NotDefined => continue, + FunctionOutcome::Empty => return CallServiceResult::ok(serde_json::Value::Null), + } + } + panic!("No function found for params {:?}", params) + }) +} diff --git a/crates/testing-framework/src/services/results.rs b/crates/testing-framework/src/services/results.rs new file mode 100644 index 00000000..c238991a --- /dev/null +++ b/crates/testing-framework/src/services/results.rs @@ -0,0 +1,106 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use super::{FunctionOutcome, MarineService}; +use crate::asserts::ServiceDefinition; + +use air_test_utils::{ + prelude::{echo_call_service, unit_call_service}, + CallRequestParams, CallServiceClosure, CallServiceResult, +}; + +use std::{cell::Cell, collections::HashMap, convert::TryInto, time::Duration}; + +pub struct ResultService { + results: HashMap, +} + +impl TryInto for ServiceDefinition { + type Error = String; + + fn try_into(self) -> Result { + match self { + ServiceDefinition::Ok(jvalue) => { + Ok(Box::new(move |_| CallServiceResult::ok(jvalue.clone()))) + } + ServiceDefinition::Error(call_result) => Ok(Box::new(move |_| call_result.clone())), + ServiceDefinition::SeqResult(call_map) => Ok(seq_result_closure(call_map)), + ServiceDefinition::Behaviour(name) => named_service_closure(name), + } + } +} + +fn named_service_closure(name: String) -> Result { + match name.as_str() { + "echo" => Ok(echo_call_service()), + "unit" => Ok(unit_call_service()), + _ => Err(format!("unknown service name: {:?}", name)), + } +} + +fn seq_result_closure(call_map: HashMap) -> CallServiceClosure { + let call_number_seq = Cell::new(0); + + Box::new(move |_| { + let call_number = call_number_seq.get(); + let call_num_str = call_number.to_string(); + call_number_seq.set(call_number + 1); + + CallServiceResult::ok( + call_map + .get(&call_num_str) + .or_else(|| call_map.get("default")) + .unwrap_or_else(|| { + panic!( + "neither value {} nor default value not found in the {:?}", + call_num_str, call_map + ) + }) + .clone(), + ) + }) +} + +impl ResultService { + pub(crate) fn new(results: HashMap) -> Result { + Ok(Self { + results: results + .into_iter() + .map(|(id, service_def)| { + service_def + .try_into() + .map(move |s: CallServiceClosure| (id, s)) + }) + .collect::>()?, + }) + } +} + +impl MarineService for ResultService { + fn call(&self, params: CallRequestParams) -> FunctionOutcome { + if let Some((_, suffix)) = params.service_id.split_once("..") { + if let Ok(key) = suffix.parse() { + let service_desc = self.results.get(&key).expect("Unknown result id"); + FunctionOutcome::ServiceResult(service_desc(params), Duration::ZERO) + } else { + // Pass malformed service names further in a chain + FunctionOutcome::NotDefined + } + } else { + FunctionOutcome::NotDefined + } + } +} diff --git a/crates/testing-framework/src/transform/mod.rs b/crates/testing-framework/src/transform/mod.rs new file mode 100644 index 00000000..448a4dcf --- /dev/null +++ b/crates/testing-framework/src/transform/mod.rs @@ -0,0 +1,120 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +mod parser; +pub(crate) mod walker; + +use crate::asserts::ServiceDefinition; + +type Triplet = (Sexp, Sexp, Sexp); + +#[derive(Debug, PartialEq)] +pub(crate) struct Call { + triplet: Box, + args: Vec, + var: Option>, + service_desc: Option, +} + +#[derive(Debug, PartialEq)] +pub(crate) enum Sexp { + Call(Call), + List(Vec), + Symbol(String), + String(String), +} + +impl Sexp { + pub(crate) fn list(list: Vec) -> Self { + Self::List(list) + } + + pub(crate) fn symbol(name: impl ToString) -> Self { + Self::Symbol(name.to_string()) + } + + pub(crate) fn string(value: impl ToString) -> Self { + Self::String(value.to_string()) + } +} + +impl std::fmt::Display for Sexp { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + use itertools::Itertools; + + match self { + Sexp::Call(call) => { + write!( + f, + "(call {peer_id} ({service} {func}) [{args}]{var})", + peer_id = call.triplet.0, + service = call.triplet.1, + func = call.triplet.2, + args = call.args.iter().format(" "), + var = match &call.var { + Some(var) => format!(" {}", var), + None => "".to_owned(), + } + ) + } + Sexp::List(items) => write!(f, "({})", items.iter().format(" ")), + Sexp::Symbol(symbol) => write!(f, "{}", symbol), + Sexp::String(string) => write!(f, r#""{}""#, string), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::str::FromStr; + + #[test] + fn test_parse_fmt_call() { + let sexp_str = r#"(call "my_id" ("serv" "function") [other_peer_id "other_arg"])"#; + let sexp = Sexp::from_str(sexp_str).unwrap(); + assert_eq!(format!("{}", sexp), sexp_str); + } + + #[test] + fn test_parse_fmt_call_var() { + let sexp_str = r#"(call "my_id" ("serv" "function") [other_peer_id "other_arg"] var)"#; + let sexp = Sexp::from_str(sexp_str).unwrap(); + assert_eq!(format!("{}", sexp), sexp_str); + } + + #[test] + fn test_parse_fmt_symbol() { + let sexp_str = "symbol"; + let sexp = Sexp::from_str(sexp_str).unwrap(); + assert_eq!(format!("{}", sexp), sexp_str); + } + + #[test] + fn test_parse_fmt_string() { + let sexp_str = r#""my_id""#; + let sexp = Sexp::from_str(sexp_str).unwrap(); + assert_eq!(format!("{}", sexp), sexp_str); + } + + #[test] + fn test_parse_fmt_sexp() { + let sexp_str = r#"(par (ap x y) (fold x y (next)))"#; + let sexp = Sexp::from_str(sexp_str).unwrap(); + assert_eq!(format!("{}", sexp), sexp_str); + } +} diff --git a/crates/testing-framework/src/transform/parser.rs b/crates/testing-framework/src/transform/parser.rs new file mode 100644 index 00000000..7e73d2df --- /dev/null +++ b/crates/testing-framework/src/transform/parser.rs @@ -0,0 +1,519 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use super::{Call, Sexp, Triplet}; +use crate::asserts::{parser::delim_ws, ServiceDefinition}; + +use nom::branch::alt; +use nom::bytes::complete::{is_not, tag}; +use nom::character::complete::{alphanumeric1, multispace0, multispace1, one_of, space1}; +use nom::combinator::{cut, map, map_res, opt, recognize, value}; +use nom::error::{context, VerboseError, VerboseErrorKind}; +use nom::multi::{many1_count, separated_list0}; +use nom::sequence::{delimited, pair, preceded, separated_pair, terminated}; +use nom::IResult; +use nom_locate::LocatedSpan; + +use std::str::FromStr; + +type Input<'inp> = LocatedSpan<&'inp str>; +type ParseError<'inp> = VerboseError>; + +impl FromStr for Sexp { + type Err = String; + + fn from_str(s: &str) -> Result { + use nom::combinator::all_consuming; + + let span = nom_locate::LocatedSpan::new(s); + cut(all_consuming(delim_ws(parse_sexp)))(span) + .map(|(_, v)| v) + .map_err(parse_error_to_message) + } +} + +pub(crate) fn parse_error_to_message(e: nom::Err) -> String { + let e = match e { + nom::Err::Failure(e) => e, + _ => panic!("shouldn't happen because of top-level cut"), + }; + let contexts = e + .errors + .iter() + .rev() + .filter_map(|(span, kind)| { + if let VerboseErrorKind::Context(c) = kind { + Some(format!( + " {}:{}: {}", + span.location_line(), + span.get_utf8_column(), + c + )) + } else { + None + } + }) + .collect::>(); + if contexts.is_empty() { + e.to_string() + } else { + format!("Failed to parse the script:\n{}", contexts.join("\n")) + } +} + +pub(crate) fn parse_sexp(inp: Input<'_>) -> IResult, Sexp, ParseError<'_>> { + alt(( + parse_sexp_call, + parse_sexp_list, + parse_sexp_string, + parse_sexp_symbol, + ))(inp) +} + +fn parse_sexp_list(inp: Input<'_>) -> IResult, Sexp, ParseError<'_>> { + context( + "within generic list", + preceded( + terminated(tag("("), multispace0), + cut(terminated( + map(separated_list0(multispace1, parse_sexp), Sexp::list), + preceded( + multispace0, + context("closing parentheses not found", tag(")")), + ), + )), + ), + )(inp) +} + +fn parse_sexp_string(inp: Input<'_>) -> IResult, Sexp, ParseError<'_>> { + // N.B. escape are rejected by AIR parser, but we simply treat backslash + // as any other character + map( + context( + "within string", + preceded( + tag("\""), + cut(terminated( + alt(( + is_not("\""), + // + tag(""), + )), + context("closing quotes not found", tag("\"")), + )), + ), + ), + Sexp::string, + )(inp) +} + +fn parse_sexp_symbol(inp: Input<'_>) -> IResult, Sexp, ParseError<'_>> { + map( + recognize(pair( + many1_count(alt((value((), alphanumeric1), value((), one_of("_-.$#%"))))), + opt(delimited(tag("["), parse_sexp_symbol, tag("]"))), + )), + Sexp::symbol, + )(inp) +} + +fn parse_sexp_call(inp: Input<'_>) -> IResult, Sexp, ParseError<'_>> { + preceded( + delim_ws(tag("(")), + preceded( + tag("call "), + context("within call list", cut(parse_sexp_call_content)), + ), + // call_content includes ")" and possible comment ^ + )(inp) +} + +fn parse_sexp_call_content(inp: Input<'_>) -> IResult, Sexp, ParseError<'_>> { + map( + pair( + // triplet and arguments + pair(parse_sexp_call_triplet, parse_sexp_call_arguments), + // possible variable, closing ")", possible annotation + pair( + terminated( + opt(preceded(multispace1, map(parse_sexp_symbol, Box::new))), + preceded(multispace0, tag(")")), + ), + alt(( + opt(preceded(pair(space1, tag("; ")), parse_annotation)), + value(None, multispace0), + )), + ), + ), + |((triplet, args), (var, annotation))| { + Sexp::Call(Call { + triplet, + args, + var, + service_desc: annotation, + }) + }, + )(inp) +} + +fn parse_annotation(inp: Input<'_>) -> IResult, ServiceDefinition, ParseError<'_>> { + map_res( + is_not("\r\n"), + |span: Input<'_>| -> Result> { + Ok(ServiceDefinition::from_str(&span).unwrap()) + }, + )(inp) +} + +fn parse_sexp_call_triplet(inp: Input<'_>) -> IResult, Box, ParseError<'_>> { + map( + separated_pair( + context("triplet peer_id", parse_sexp), + multispace0, + delimited( + delim_ws(tag("(")), + separated_pair( + context("triplet service name", parse_sexp_string), + multispace0, + context("triplet function name", parse_sexp), + ), + delim_ws(tag(")")), + ), + ), + |(peer_id, (service, function))| Box::new((peer_id, service, function)), + )(inp) +} + +fn parse_sexp_call_arguments(inp: Input<'_>) -> IResult, Vec, ParseError<'_>> { + delimited(tag("["), separated_list0(multispace1, parse_sexp), tag("]"))(inp) +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + use crate::asserts::ServiceDefinition; + + #[test] + fn test_symbol() { + let res = Sexp::from_str("symbol"); + assert_eq!(res, Ok(Sexp::symbol("symbol"))); + } + + #[test] + fn test_symbol_lambda() { + let res = Sexp::from_str("sym_bol.$.blabla"); + assert_eq!(res, Ok(Sexp::symbol("sym_bol.$.blabla"))); + } + + #[test] + fn test_symbol_stream() { + let res = Sexp::from_str("$stream"); + assert_eq!(res, Ok(Sexp::symbol("$stream"))); + } + + #[test] + fn test_symbol_canon() { + let res = Sexp::from_str("#canon"); + assert_eq!(res, Ok(Sexp::symbol("#canon"))); + } + + #[test] + fn test_symbol_lambda2() { + let res = Sexp::from_str(r#"$result.$[0]"#); + assert_eq!(res, Ok(Sexp::symbol(r#"$result.$[0]"#))); + } + + #[test] + fn test_string_empty() { + let res = Sexp::from_str(r#""""#); + assert_eq!(res, Ok(Sexp::string(""))); + } + + #[test] + fn test_string() { + let res = Sexp::from_str(r#""str ing""#); + assert_eq!(res, Ok(Sexp::string("str ing"))); + } + + #[test] + fn test_empty_list() { + let res = Sexp::from_str("()"); + assert_eq!(res, Ok(Sexp::List(vec![]))); + } + + #[test] + fn test_small_list() { + let res = Sexp::from_str("(null)"); + assert_eq!(res, Ok(Sexp::list(vec![Sexp::symbol("null")]))); + } + + #[test] + fn test_call_no_args() { + let res = Sexp::from_str(r#"(call peer_id ("serv" "func") [])"#); + assert_eq!( + res, + Ok(Sexp::Call(Call { + triplet: Box::new(( + Sexp::symbol("peer_id"), + Sexp::string("serv"), + Sexp::string("func"), + )), + args: vec![], + var: None, + service_desc: None, + })) + ); + } + + #[test] + fn test_call_after_call() { + let res = Sexp::from_str( + r#"(seq + (call peer_id ("serv" "func") []) + (call peer_id ("serv" "func") []) +)"#, + ); + assert_eq!( + res, + Ok(Sexp::list(vec![ + Sexp::symbol("seq"), + Sexp::Call(Call { + triplet: Box::new(( + Sexp::symbol("peer_id"), + Sexp::string("serv"), + Sexp::string("func"), + )), + args: vec![], + var: None, + service_desc: None, + }), + Sexp::Call(Call { + triplet: Box::new(( + Sexp::symbol("peer_id"), + Sexp::string("serv"), + Sexp::string("func"), + )), + args: vec![], + var: None, + service_desc: None, + }), + ])) + ); + } + + #[test] + fn test_call_annotation_newline() { + let res = Sexp::from_str( + r#"(seq (call peer_id ("serv" "func") []) +; result=42 +)"#, + ); + assert_eq!( + res, + Err("Failed to parse the script:\n 1:1: within generic list\n 2:1: closing parentheses not found".to_owned()) + ); + } + + #[test] + fn test_call_args1() { + let res = Sexp::from_str(r#"(call peer_id ("serv" "func") [a])"#); + assert_eq!( + res, + Ok(Sexp::Call(Call { + triplet: Box::new(( + Sexp::symbol("peer_id"), + Sexp::string("serv"), + Sexp::string("func"), + )), + args: vec![Sexp::symbol("a")], + var: None, + service_desc: None, + })) + ); + } + + #[test] + fn test_call_args2() { + let res = Sexp::from_str(r#"(call peer_id ("serv" "func") [a b])"#); + assert_eq!( + res, + Ok(Sexp::Call(Call { + triplet: Box::new(( + Sexp::symbol("peer_id"), + Sexp::string("serv"), + Sexp::string("func"), + )), + args: vec![Sexp::symbol("a"), Sexp::symbol("b")], + var: None, + service_desc: None, + })) + ); + } + + #[test] + fn test_call_var() { + let res = Sexp::from_str(r#"(call peer_id ("serv" "func") [a b] var)"#); + assert_eq!( + res, + Ok(Sexp::Call(Call { + triplet: Box::new(( + Sexp::Symbol("peer_id".to_owned()), + Sexp::String("serv".to_owned()), + Sexp::String("func".to_owned()), + )), + args: vec![Sexp::Symbol("a".to_owned()), Sexp::Symbol("b".to_owned())], + var: Some(Box::new(Sexp::Symbol("var".to_owned()))), + service_desc: None, + })) + ); + } + + #[test] + fn test_call_with_annotation() { + let res = Sexp::from_str(r#"(call peer_id ("serv" "func") [a b] var) ; ok=42 "#); + let expected_annotation = ServiceDefinition::Ok(json!(42)); + assert_eq!( + res, + Ok(Sexp::Call(Call { + triplet: Box::new(( + Sexp::symbol("peer_id"), + Sexp::string("serv"), + Sexp::string("func"), + )), + args: vec![Sexp::symbol("a"), Sexp::symbol("b")], + var: Some(Box::new(Sexp::symbol("var"))), + service_desc: Some(expected_annotation), + })) + ); + } + + #[test] + fn test_call_with_annotation2() { + let res = Sexp::from_str( + r#"(par + (call peerid ("serv" "func") [a b] var) ; ok=42 + (call peerid2 ("serv" "func") []))"#, + ); + assert!(res.is_ok(), "{:?}", res); + } + + #[test] + fn test_generic_sexp() { + let res = Sexp::from_str(" (fold i n ( par (null) (match y \"asdf\" (fail ))) )"); + assert_eq!( + res, + Ok(Sexp::list(vec![ + Sexp::symbol("fold"), + Sexp::symbol("i"), + Sexp::symbol("n"), + Sexp::list(vec![ + Sexp::symbol("par"), + Sexp::list(vec![Sexp::symbol("null")]), + Sexp::list(vec![ + Sexp::symbol("match"), + Sexp::symbol("y"), + Sexp::string("asdf"), + Sexp::list(vec![Sexp::symbol("fail"),]) + ]) + ]) + ])) + ); + } + + #[test] + fn test_trailing_error() { + let res = Sexp::from_str("(null))"); + assert!(res.is_err(), "{:?}", res); + } + + #[test] + fn test_incomplete_string() { + let err = Sexp::from_str( + r#"(seq + "string"#, + ) + .unwrap_err(); + assert_eq!( + err, + "Failed to parse the script: + 1:1: within generic list + 2:4: within string + 2:11: closing quotes not found" + ); + } + + #[test] + fn test_incomplete_list() { + let err = Sexp::from_str( + r#"(seq + "string" +"#, + ) + .unwrap_err(); + assert_eq!( + err, + "Failed to parse the script: + 1:1: within generic list + 3:1: closing parentheses not found" + ); + } + + #[test] + fn test_parse_fmt_call() { + let sexp_str = r#"(call "my_id" ("serv" "function") [other_peer_id "other_arg"])"#; + let sexp = Sexp::from_str(sexp_str).unwrap(); + assert_eq!(format!("{}", sexp), sexp_str); + } + + #[test] + fn test_parse_fmt_call_var() { + let sexp_str = r#"(call "my_id" ("serv" "function") [other_peer_id "other_arg"] var)"#; + let sexp = Sexp::from_str(sexp_str).unwrap(); + assert_eq!(format!("{}", sexp), sexp_str); + } + + #[test] + fn test_parse_fmt_symbol() { + let sexp_str = "symbol"; + let sexp = Sexp::from_str(sexp_str).unwrap(); + assert_eq!(format!("{}", sexp), sexp_str); + } + + #[test] + fn test_parse_fmt_string() { + let sexp_str = r#""my_id""#; + let sexp = Sexp::from_str(sexp_str).unwrap(); + assert_eq!(format!("{}", sexp), sexp_str); + } + + #[test] + fn test_parse_fmt_sexp() { + let sexp_str = r#"(par (ap x y) (fold x y (next)))"#; + let sexp = Sexp::from_str(sexp_str).unwrap(); + assert_eq!(format!("{}", sexp), sexp_str); + } + + #[test] + fn test_canon_syntax() { + let sexp_str = r#"(seq (canon peer_id $stream #canon) (fold #canon i (next)))"#; + let res = Sexp::from_str(sexp_str); + assert!(res.is_ok(), "{:?}", res); + } +} diff --git a/crates/testing-framework/src/transform/walker.rs b/crates/testing-framework/src/transform/walker.rs new file mode 100644 index 00000000..701107ca --- /dev/null +++ b/crates/testing-framework/src/transform/walker.rs @@ -0,0 +1,180 @@ +/* + * Copyright 2022 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use super::{Call, Sexp}; +use crate::{asserts::ServiceDefinition, ephemeral::PeerId}; + +use std::collections::{HashMap, HashSet}; + +#[derive(Debug, Default)] +pub(crate) struct Transformer { + cnt: u32, + pub(crate) results: HashMap, + pub(crate) peers: HashSet, +} + +impl Transformer { + pub(crate) fn new() -> Self { + Default::default() + } + + pub(crate) fn transform(&mut self, sexp: &mut Sexp) { + match sexp { + Sexp::Call(call) => self.handle_call(call), + Sexp::List(children) => { + for child in children.iter_mut().skip(1) { + self.transform(child); + } + } + Sexp::Symbol(_) | Sexp::String(_) => {} + } + } + + fn handle_call(&mut self, call: &mut Call) { + // collect peers... + if let Sexp::String(peer_id) = &call.triplet.0 { + self.peers.insert(peer_id.clone().into()); + } + + if let Some(service) = &call.service_desc { + // install a value + let call_id = self.cnt; + self.cnt += 1; + + self.results.insert(call_id, service.clone()); + + match &mut call.triplet.1 { + Sexp::String(ref mut value) => value.push_str(&format!("..{}", call_id)), + _ => panic!("Incorrect script: non-string service string not supported"), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{iter::FromIterator, str::FromStr}; + + #[test] + fn test_translate_null() { + let mut tree = Sexp::from_str("(null)").unwrap(); + let mut transformer = Transformer::new(); + transformer.transform(&mut tree); + assert_eq!(tree.to_string(), "(null)"); + } + + #[test] + fn test_translate_call_no_result() { + let script = r#"(call peer_id ("service_id" func) [])"#; + let mut tree = Sexp::from_str(script).unwrap(); + let mut transformer = Transformer::new(); + transformer.transform(&mut tree); + assert_eq!(tree.to_string(), script); + } + + #[test] + #[should_panic] + fn test_translate_call_no_string() { + // TODO rewrite to Result instead of panic? + let script = r#"(call "peer_id" (service_id func) [])"#; + let mut tree = Sexp::from_str(script).unwrap(); + let mut transformer = Transformer::new(); + transformer.transform(&mut tree); + assert_eq!(tree.to_string(), script); + } + + #[test] + fn test_translate_call_result() { + let script = r#"(call "peer_id" ("service_id" func) []) ; ok = 42"#; + let mut tree = Sexp::from_str(script).unwrap(); + let mut transformer = Transformer::new(); + transformer.transform(&mut tree); + assert_eq!( + tree.to_string(), + r#"(call "peer_id" ("service_id..0" func) [])"# + ); + + assert_eq!( + transformer.results, + maplit::hashmap! { + 0u32 => ServiceDefinition::Ok(serde_json::json!(42)), + } + ); + + assert_eq!( + transformer.peers.into_iter().collect::>(), + vec![PeerId::new("peer_id")], + ); + } + + #[test] + fn test_translate_multiple_calls() { + let script = r#"(seq + (call peer_id ("service_id" func) [a 11]) ; ok={"test":"me"} + (seq + (call peer_id ("service_id" func) [b]) + (call peer_id ("service_id" func) [1]) ; ok=true +))"#; + + let mut tree = Sexp::from_str(script).unwrap(); + let mut transformer = Transformer::new(); + transformer.transform(&mut tree); + assert_eq!( + tree.to_string(), + concat!( + "(seq ", + r#"(call peer_id ("service_id..0" func) [a 11])"#, + " (seq ", + r#"(call peer_id ("service_id" func) [b])"#, + " ", + r#"(call peer_id ("service_id..1" func) [1])"#, + "))", + ) + ); + + assert_eq!( + transformer.results, + maplit::hashmap! { + 0u32 => ServiceDefinition::Ok(serde_json::json!({"test":"me"})), + 1 => ServiceDefinition::Ok(serde_json::json!(true)), + } + ); + + assert!(transformer.peers.is_empty()); + } + + #[test] + fn test_peers() { + // this script is not correct AIR, but our parser handles it + let script = r#"(seq + (call "peer_id1" ("service_id" func) [a 11]) ; ok={"test":"me"} + (seq + (call "peer_id2" ("service_id" func) [b]) + (call "peer_id1" ("service_id" func) [1]) ; ok=true + (call peer_id3 ("service_id" func) [b]) +))"#; + + let mut tree = Sexp::from_str(script).unwrap(); + let mut transformer = Transformer::new(); + transformer.transform(&mut tree); + + assert_eq!( + transformer.peers, + HashSet::from_iter(vec![PeerId::new("peer_id1"), PeerId::new("peer_id2")]), + ) + } +}