mirror of
https://github.com/fluencelabs/aquavm
synced 2024-12-04 07:10:18 +00:00
Testing framework, chapter I (#293)
Testing framework for AquaVM Its primary features are: 1. It generates services declaratively by annotation in the comments inserted just after calls. 2. Ephemeral network keeps each node's data and incoming data queue. The network can be also generated based on peer IDs featured in the script. 3. One can explicitly add additional peers and services. The example of the script annotations: ``` (seq (call "peer_1" ("service" "func") [] var) ; ok=42 (call "peer_2" ("service" "func") [var]) ; err={"ret_code": 1, "result":"no towel"} ) ``` Passing this script to `air_test_framework::TestExecutor::new(...)` will create a virtual network with peers "peer_1" and "peer_2" (and any peer you provide in the `TestRunParameters`), and the particular calls will return respective values. Please note that autogenerated services use modified service name as a side channel for finding a correct value: "..{N}" is added to each service name (two dots and serial number). Be careful with service names taken from a variable.
This commit is contained in:
parent
5072fba9d6
commit
513eb0e126
58
Cargo.lock
generated
58
Cargo.lock
generated
@ -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",
|
||||
|
@ -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",
|
||||
]
|
||||
|
@ -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"] }
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -24,7 +24,7 @@ pub type CallResults = HashMap<u32, CallServiceResult>;
|
||||
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,
|
||||
|
28
crates/testing-framework/Cargo.toml
Normal file
28
crates/testing-framework/Cargo.toml
Normal file
@ -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"] }
|
46
crates/testing-framework/src/asserts/mod.rs
Normal file
46
crates/testing-framework/src/asserts/mod.rs
Normal file
@ -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<String, JValue>),
|
||||
/// Some known service by name: "echo", "unit" (more to follow).
|
||||
#[strum_discriminants(strum(serialize = "behaviour"))]
|
||||
Behaviour(String),
|
||||
}
|
186
crates/testing-framework/src/asserts/parser.rs
Normal file
186
crates/testing-framework/src/asserts/parser.rs
Normal file
@ -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<Self, Self::Err> {
|
||||
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::<JValue>(value).map(ServiceDefinition::Ok)
|
||||
}
|
||||
Ok(ServiceTagName::Error) => {
|
||||
serde_json::from_str::<CallServiceResult>(value).map(ServiceDefinition::Error)
|
||||
}
|
||||
Ok(ServiceTagName::SeqResult) => {
|
||||
serde_json::from_str::<HashMap<String, JValue>>(value)
|
||||
.map(ServiceDefinition::SeqResult)
|
||||
}
|
||||
Ok(ServiceTagName::Behaviour) => Ok(ServiceDefinition::Behaviour(value.to_owned())),
|
||||
Err(_) => unreachable!("unknown tag {:?}", tag),
|
||||
}
|
||||
},
|
||||
))(inp)
|
||||
}
|
||||
|
||||
pub(crate) fn delim_ws<I, O, E, F>(f: F) -> impl FnMut(I) -> IResult<I, O, E>
|
||||
where
|
||||
F: Parser<I, O, E>,
|
||||
E: nom::error::ParseError<I>,
|
||||
I: InputTakeAtPosition,
|
||||
<I as 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())),);
|
||||
}
|
||||
}
|
258
crates/testing-framework/src/ephemeral/mod.rs
Normal file
258
crates/testing-framework/src/ephemeral/mod.rs
Normal file
@ -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<String>) -> Self {
|
||||
Self(peer_id.into())
|
||||
}
|
||||
}
|
||||
impl From<String> 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<str> for PeerId {
|
||||
fn borrow(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub type Data = Vec<u8>;
|
||||
|
||||
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<PeerId>, 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<String>,
|
||||
data: Data,
|
||||
test_run_params: TestRunParameters,
|
||||
) -> Result<RawAVMOutcome, String> {
|
||||
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<PeerId, Rc<RefCell<PeerEnv>>>,
|
||||
default_neighborhood: HashSet<PeerId>,
|
||||
}
|
||||
|
||||
impl Network {
|
||||
pub fn empty() -> Self {
|
||||
Self::new(std::iter::empty::<&str>())
|
||||
}
|
||||
|
||||
pub fn new(default_neiborhoud: impl Iterator<Item = impl Into<PeerId>>) -> Self {
|
||||
Self {
|
||||
peers: Default::default(),
|
||||
default_neighborhood: default_neiborhoud.map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_peers(nodes: Vec<Peer>) -> 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<Item = impl Into<PeerId>>,
|
||||
) -> &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<Id>(&mut self, peer_id: &Id, failed: bool)
|
||||
where
|
||||
PeerId: Borrow<Id>,
|
||||
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<Id>(&mut self, source_peer_id: &Id, target_peer_id: impl Into<PeerId>)
|
||||
where
|
||||
PeerId: Borrow<Id>,
|
||||
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<Id1, Id2>(&mut self, source_peer_id: &Id1, target_peer_id: &Id2)
|
||||
where
|
||||
PeerId: Borrow<Id1>,
|
||||
Id1: Hash + Eq + ?Sized,
|
||||
PeerId: Borrow<Id2>,
|
||||
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<Id>(&self, peer_id: &Id) -> Option<Rc<RefCell<PeerEnv>>>
|
||||
where
|
||||
PeerId: Borrow<Id>,
|
||||
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<impl Iterator<Item = RawAVMOutcome> + 's>
|
||||
where
|
||||
PeerId: Borrow<Id>,
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
395
crates/testing-framework/src/ephemeral/neighborhood.rs
Normal file
395
crates/testing-framework/src/ephemeral/neighborhood.rs
Normal file
@ -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<PeerId>;
|
||||
|
||||
#[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<PeerId, LinkState>,
|
||||
}
|
||||
|
||||
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<Item = &PeerId> {
|
||||
self.into_iter()
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, other_peer_id: impl Into<PeerId>) {
|
||||
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<Id>(&mut self, other_peer_id: &Id)
|
||||
where
|
||||
PeerId: Borrow<Id>,
|
||||
Id: Eq + Hash + ?Sized,
|
||||
{
|
||||
self.neighbors.remove(other_peer_id);
|
||||
}
|
||||
|
||||
pub fn set_target_unreachable(&mut self, target: impl Into<PeerId>) {
|
||||
*self.neighbors.get_mut(&target.into()).unwrap() = LinkState::Unreachable;
|
||||
}
|
||||
|
||||
pub fn unset_target_unreachable<Id>(&mut self, target: &Id)
|
||||
where
|
||||
PeerId: Borrow<Id>,
|
||||
Id: Eq + Hash + ?Sized,
|
||||
{
|
||||
*self.neighbors.get_mut(target).unwrap() = LinkState::Reachable;
|
||||
}
|
||||
|
||||
pub fn is_reachable(&self, target: impl Deref<Target = PeerId>) -> 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<Data>,
|
||||
}
|
||||
|
||||
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<Target = PeerId>) -> 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<Item = impl Into<PeerId>>) {
|
||||
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<Item = &'a Id>)
|
||||
where
|
||||
PeerId: std::borrow::Borrow<Id>,
|
||||
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<Item = &PeerId> {
|
||||
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<String>,
|
||||
network: &Network,
|
||||
test_parameters: &TestRunParameters,
|
||||
) -> Option<Result<air_test_utils::RawAVMOutcome, String>> {
|
||||
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::<PeerSet>(),
|
||||
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::<Vec<_>>(),
|
||||
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::<HashSet<_>>(),
|
||||
IntoIterator::into_iter(["one"])
|
||||
.map(PeerId::from)
|
||||
.collect::<HashSet<_>>()
|
||||
);
|
||||
}
|
||||
#[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));
|
||||
}
|
||||
}
|
358
crates/testing-framework/src/execution/mod.rs
Normal file
358
crates/testing-framework/src/execution/mod.rs
Normal file
@ -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<MarineServiceHandle>,
|
||||
extra_peers: impl IntoIterator<Item = PeerId>,
|
||||
annotated_air_script: &str,
|
||||
) -> Result<Self, String> {
|
||||
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, String> {
|
||||
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<impl Iterator<Item = RawAVMOutcome> + 's>
|
||||
where
|
||||
PeerId: Borrow<Id>,
|
||||
// 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<Id>(&self, peer_id: &Id) -> Option<Vec<RawAVMOutcome>>
|
||||
where
|
||||
PeerId: Borrow<Id>,
|
||||
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<Id>(&self, peer_id: &Id) -> Option<RawAVMOutcome>
|
||||
where
|
||||
PeerId: Borrow<Id>,
|
||||
Id: Eq + Hash + ?Sized,
|
||||
{
|
||||
self.execution_iter(peer_id)
|
||||
.map(|mut it| it.next().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
fn build_peers(
|
||||
common_services: Vec<MarineServiceHandle>,
|
||||
results: HashMap<u32, ServiceDefinition>,
|
||||
known_peers: std::collections::HashSet<PeerId>,
|
||||
init_peer_id: PeerId,
|
||||
extra_peers: impl IntoIterator<Item = PeerId>,
|
||||
) -> Result<Vec<Peer>, String> {
|
||||
let mut result_services: Vec<MarineServiceHandle> =
|
||||
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::<HashMap<_, _>>();
|
||||
|
||||
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"),]),
|
||||
)
|
||||
}
|
||||
}
|
23
crates/testing-framework/src/lib.rs
Normal file
23
crates/testing-framework/src/lib.rs
Normal file
@ -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;
|
70
crates/testing-framework/src/services/mod.rs
Normal file
70
crates/testing-framework/src/services/mod.rs
Normal file
@ -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<RefCell<Box<dyn MarineService>>>);
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
106
crates/testing-framework/src/services/results.rs
Normal file
106
crates/testing-framework/src/services/results.rs
Normal file
@ -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<u32, CallServiceClosure>,
|
||||
}
|
||||
|
||||
impl TryInto<CallServiceClosure> for ServiceDefinition {
|
||||
type Error = String;
|
||||
|
||||
fn try_into(self) -> Result<CallServiceClosure, String> {
|
||||
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<CallServiceClosure, String> {
|
||||
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<String, serde_json::Value>) -> 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<u32, ServiceDefinition>) -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
results: results
|
||||
.into_iter()
|
||||
.map(|(id, service_def)| {
|
||||
service_def
|
||||
.try_into()
|
||||
.map(move |s: CallServiceClosure| (id, s))
|
||||
})
|
||||
.collect::<Result<_, String>>()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
120
crates/testing-framework/src/transform/mod.rs
Normal file
120
crates/testing-framework/src/transform/mod.rs
Normal file
@ -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<Triplet>,
|
||||
args: Vec<Sexp>,
|
||||
var: Option<Box<Sexp>>,
|
||||
service_desc: Option<ServiceDefinition>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum Sexp {
|
||||
Call(Call),
|
||||
List(Vec<Sexp>),
|
||||
Symbol(String),
|
||||
String(String),
|
||||
}
|
||||
|
||||
impl Sexp {
|
||||
pub(crate) fn list(list: Vec<Self>) -> 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);
|
||||
}
|
||||
}
|
519
crates/testing-framework/src/transform/parser.rs
Normal file
519
crates/testing-framework/src/transform/parser.rs
Normal file
@ -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<Input<'inp>>;
|
||||
|
||||
impl FromStr for Sexp {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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<ParseError>) -> 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::<Vec<_>>();
|
||||
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<Input<'_>, Sexp, ParseError<'_>> {
|
||||
alt((
|
||||
parse_sexp_call,
|
||||
parse_sexp_list,
|
||||
parse_sexp_string,
|
||||
parse_sexp_symbol,
|
||||
))(inp)
|
||||
}
|
||||
|
||||
fn parse_sexp_list(inp: Input<'_>) -> IResult<Input<'_>, 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<Input<'_>, 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<Input<'_>, 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<Input<'_>, 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<Input<'_>, 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<Input<'_>, ServiceDefinition, ParseError<'_>> {
|
||||
map_res(
|
||||
is_not("\r\n"),
|
||||
|span: Input<'_>| -> Result<ServiceDefinition, ParseError<'_>> {
|
||||
Ok(ServiceDefinition::from_str(&span).unwrap())
|
||||
},
|
||||
)(inp)
|
||||
}
|
||||
|
||||
fn parse_sexp_call_triplet(inp: Input<'_>) -> IResult<Input<'_>, Box<Triplet>, 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<Input<'_>, Vec<Sexp>, 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);
|
||||
}
|
||||
}
|
180
crates/testing-framework/src/transform/walker.rs
Normal file
180
crates/testing-framework/src/transform/walker.rs
Normal file
@ -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<u32, ServiceDefinition>,
|
||||
pub(crate) peers: HashSet<PeerId>,
|
||||
}
|
||||
|
||||
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<_>>(),
|
||||
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")]),
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user