feat(testing) Testing framework chapter 1, asserts and comments (#342)

* seq_result` -> `seq_ok`; add `seq_err`

`seq_ok` and `seq_err` are consistent with `ok` and `err`, but produce
results sequentially.

* Accept `;;` and longer comments in the sexp parser

Currently they are just dropped, and resulting AIR has different
character positions in the error messages.

* Add "map" assertion

Lookup result in a map by service's first argument.
This commit is contained in:
Ivan Boldyrev 2022-10-10 21:05:20 +03:00 committed by GitHub
parent 17a6409566
commit 076045124c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 447 additions and 72 deletions

1
Cargo.lock generated
View File

@ -189,6 +189,7 @@ dependencies = [
"maplit", "maplit",
"nom 7.1.1", "nom 7.1.1",
"nom_locate", "nom_locate",
"pretty_assertions",
"serde_json", "serde_json",
"strum", "strum",
] ]

View File

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
use air_test_framework::TestExecutor;
use air_test_utils::prelude::*; use air_test_utils::prelude::*;
#[test] #[test]
@ -27,24 +28,17 @@ fn issue_221() {
let peer_1_value = "peer_1_value"; let peer_1_value = "peer_1_value";
let peer_2_value = "peer_2_value"; let peer_2_value = "peer_2_value";
let mut peer_1 = create_avm(set_variable_call_service(json!(peer_1_value)), peer_1_id);
let mut peer_2 = create_avm(set_variable_call_service(json!(peer_2_value)), peer_2_id);
let mut join_1 = create_avm(echo_call_service(), join_1_id);
let mut set_variable = create_avm(
set_variable_call_service(json!([peer_1_id, peer_2_id])),
set_variable_id,
);
let script = f!(r#" let script = f!(r#"
(seq (seq
(seq (seq
(seq (seq
;; let's peers be an array of two values [peer_1_id, peer_2_id] ;; let's peers be an array of two values [peer_1_id, peer_2_id]
(call "{set_variable_id}" ("" "") [] peers) (call "{set_variable_id}" ("" "") [] peers) ; ok = ["{peer_1_id}", "{peer_2_id}"]
(fold peers peer (fold peers peer
(par (par
(seq (seq
(call peer ("" "") [] value) (call peer ("" "") [peer] value) ; map = {{"{peer_1_id}": "{peer_1_value}", "{peer_2_id}": "{peer_2_value}"}}
;; it's crucial to reproduce this bug to add value to stream ;; it's crucial to reproduce this bug to add value to stream
;; with help of ap instruction ;; with help of ap instruction
(ap value $stream) (ap value $stream)
@ -61,8 +55,8 @@ fn issue_221() {
;; appropriate way and state for (1) is returned ;; appropriate way and state for (1) is returned
(par (par
(par (par
(call "{join_1_id}" ("" "") [iterator]) (call "{join_1_id}" ("" "") [iterator]) ; behaviour = echo
(call "{join_2_id}" ("" "") [iterator]) (call "{join_2_id}" ("" "") [iterator]) ; behaviour = echo
) )
(next iterator) (next iterator)
) )
@ -72,18 +66,20 @@ fn issue_221() {
) )
"#); "#);
let result = checked_call_vm!(set_variable, <_>::default(), &script, "", ""); let executor = TestExecutor::new(
let peer_1_result = checked_call_vm!(peer_1, <_>::default(), &script, "", result.data.clone()); TestRunParameters::from_init_peer_id("set_variable_id"),
let peer_2_result = checked_call_vm!(peer_2, <_>::default(), &script, "", result.data.clone()); vec![],
vec![peer_1_id, peer_2_id].into_iter().map(Into::into),
let join_1_result = checked_call_vm!(join_1, <_>::default(), &script, "", peer_1_result.data.clone());
let join_1_result = checked_call_vm!(
join_1,
<_>::default(),
&script, &script,
join_1_result.data, )
peer_2_result.data.clone() .expect("Invalid annotated AIR script");
); // before 0.20.9 it fails here
let _result = executor.execute_one(set_variable_id).unwrap();
let _peer_1_result = executor.execute_one(peer_1_id).unwrap();
let _peer_2_result = executor.execute_one(peer_2_id).unwrap();
let _join_1_result = executor.execute_one(join_1_id).unwrap();
let join_1_result = executor.execute_one(join_1_id).unwrap(); // before 0.20.9 it fails here
let actual_trace = trace_from_result(&join_1_result); let actual_trace = trace_from_result(&join_1_result);
let expected_trace = vec![ let expected_trace = vec![
executed_state::scalar(json!([peer_1_id, peer_2_id])), executed_state::scalar(json!([peer_1_id, peer_2_id])),

View File

@ -24,5 +24,7 @@ serde_json = "1.0.85"
[dev-dependencies] [dev-dependencies]
maplit = "1.0.2" maplit = "1.0.2"
pretty_assertions = "0.6.1"
# We do not want to depend on wasm binary path # We do not want to depend on wasm binary path
air-test-utils = { path = "../air-lib/test-utils", features = ["test_with_native_code"] } air-test-utils = { path = "../air-lib/test-utils", features = ["test_with_native_code"] }

View File

@ -36,11 +36,14 @@ pub enum ServiceDefinition {
Error(CallServiceResult), Error(CallServiceResult),
/// Service that may return a new value on subsequent call. Its keys are either /// Service that may return a new value on subsequent call. Its keys are either
/// call number string starting from "0", or "default". /// call number string starting from "0", or "default".
// TODO We need to return error results too, so we need to define a call result #[strum_discriminants(strum(serialize = "seq_ok"))]
// for default and individual errors. SeqOk(HashMap<String, JValue>),
#[strum_discriminants(strum(serialize = "seq_result"))] #[strum_discriminants(strum(serialize = "seq_error"))]
SeqResult(HashMap<String, JValue>), SeqError(HashMap<String, CallServiceResult>),
/// Some known service by name: "echo", "unit" (more to follow). /// Some known service by name: "echo", "unit" (more to follow).
#[strum_discriminants(strum(serialize = "behaviour"))] #[strum_discriminants(strum(serialize = "behaviour"))]
Behaviour(String), Behaviour(String),
/// Maps first argument to a value
#[strum_discriminants(strum(serialize = "map"))]
Map(HashMap<String, JValue>),
} }

View File

@ -16,9 +16,10 @@
use super::{ServiceDefinition, ServiceTagName}; use super::{ServiceDefinition, ServiceTagName};
use crate::services::JValue; use crate::services::JValue;
use crate::transform::parser::delim_ws;
use air_test_utils::CallServiceResult; use air_test_utils::CallServiceResult;
use nom::{error::VerboseError, IResult, InputTakeAtPosition, Parser}; use nom::{error::VerboseError, IResult};
use std::{collections::HashMap, str::FromStr}; use std::{collections::HashMap, str::FromStr};
@ -50,8 +51,10 @@ pub fn parse_kw(inp: &str) -> IResult<&str, ServiceDefinition, ParseError> {
alt(( alt((
tag(ServiceTagName::Ok.as_ref()), tag(ServiceTagName::Ok.as_ref()),
tag(ServiceTagName::Error.as_ref()), tag(ServiceTagName::Error.as_ref()),
tag(ServiceTagName::SeqResult.as_ref()), tag(ServiceTagName::SeqOk.as_ref()),
tag(ServiceTagName::SeqError.as_ref()),
tag(ServiceTagName::Behaviour.as_ref()), tag(ServiceTagName::Behaviour.as_ref()),
tag(ServiceTagName::Map.as_ref()),
)), )),
equal(), equal(),
cut(context( cut(context(
@ -68,33 +71,26 @@ pub fn parse_kw(inp: &str) -> IResult<&str, ServiceDefinition, ParseError> {
Ok(ServiceTagName::Error) => { Ok(ServiceTagName::Error) => {
serde_json::from_str::<CallServiceResult>(value).map(ServiceDefinition::Error) serde_json::from_str::<CallServiceResult>(value).map(ServiceDefinition::Error)
} }
Ok(ServiceTagName::SeqResult) => { Ok(ServiceTagName::SeqOk) => {
serde_json::from_str::<HashMap<String, JValue>>(value) serde_json::from_str(value).map(ServiceDefinition::SeqOk)
.map(ServiceDefinition::SeqResult) }
Ok(ServiceTagName::SeqError) => {
serde_json::from_str::<HashMap<String, CallServiceResult>>(value)
.map(ServiceDefinition::SeqError)
} }
Ok(ServiceTagName::Behaviour) => Ok(ServiceDefinition::Behaviour(value.to_owned())), Ok(ServiceTagName::Behaviour) => Ok(ServiceDefinition::Behaviour(value.to_owned())),
Ok(ServiceTagName::Map) => serde_json::from_str(value).map(ServiceDefinition::Map),
Err(_) => unreachable!("unknown tag {:?}", tag), Err(_) => unreachable!("unknown tag {:?}", tag),
} }
}, },
))(inp) ))(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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
#[test] #[test]
fn test_parse_empty() { fn test_parse_empty() {
@ -151,13 +147,13 @@ mod tests {
} }
#[test] #[test]
fn test_seq_result() { fn test_seq_ok() {
use serde_json::json; use serde_json::json;
let res = ServiceDefinition::from_str(r#"seq_result={"default": 42, "1": true, "3": []}"#); let res = ServiceDefinition::from_str(r#"seq_ok={"default": 42, "1": true, "3": []}"#);
assert_eq!( assert_eq!(
res, res,
Ok(ServiceDefinition::SeqResult(maplit::hashmap! { Ok(ServiceDefinition::SeqOk(maplit::hashmap! {
"default".to_owned() => json!(42), "default".to_owned() => json!(42),
"1".to_owned() => json!(true), "1".to_owned() => json!(true),
"3".to_owned() => json!([]), "3".to_owned() => json!([]),
@ -166,15 +162,45 @@ mod tests {
} }
#[test] #[test]
fn test_seq_result_malformed() { fn test_seq_ok_malformed() {
let res = ServiceDefinition::from_str(r#"seq_result={"default": 42, "1": true, "3": ]}"#); let res = ServiceDefinition::from_str(r#"seq_ok={"default": 42, "1": true, "3": ]}"#);
assert!(res.is_err()); assert!(res.is_err());
} }
#[test] #[test]
fn test_seq_result_invalid() { fn test_seq_ok_invalid() {
// TODO perhaps, we should support both arrays and maps // TODO perhaps, we should support both arrays and maps
let res = ServiceDefinition::from_str(r#"seq_result=[42, 43]"#); let res = ServiceDefinition::from_str(r#"seq_ok=[42, 43]"#);
assert!(res.is_err());
}
#[test]
fn test_seq_error() {
use serde_json::json;
let res = ServiceDefinition::from_str(
r#"seq_error={"default": {"ret_code": 0, "result": 42}, "1": {"ret_code": 0, "result": true}, "3": {"ret_code": 1, "result": "error"}}"#,
);
assert_eq!(
res,
Ok(ServiceDefinition::SeqError(maplit::hashmap! {
"default".to_owned() => CallServiceResult::ok(json!(42)),
"1".to_owned() => CallServiceResult::ok(json!(true)),
"3".to_owned() => CallServiceResult::err(1, json!("error")),
})),
);
}
#[test]
fn test_seq_error_malformed() {
let res = ServiceDefinition::from_str(r#"seq_error={"default": 42, "1": true]}"#);
assert!(res.is_err());
}
#[test]
fn test_seq_error_invalid() {
// TODO perhaps, we should support both arrays and maps
let res = ServiceDefinition::from_str(r#"seq_error=[42, 43]"#);
assert!(res.is_err()); assert!(res.is_err());
} }
@ -183,4 +209,16 @@ mod tests {
let res = ServiceDefinition::from_str(r#"behaviour=echo"#); let res = ServiceDefinition::from_str(r#"behaviour=echo"#);
assert_eq!(res, Ok(ServiceDefinition::Behaviour("echo".to_owned())),); assert_eq!(res, Ok(ServiceDefinition::Behaviour("echo".to_owned())),);
} }
#[test]
fn test_map() {
let res = ServiceDefinition::from_str(r#"map = {"42": [], "a": 2}"#);
assert_eq!(
res,
Ok(ServiceDefinition::Map(maplit::hashmap! {
"42".to_owned() => json!([]),
"a".to_owned() => json!(2)
}))
);
}
} }

View File

@ -145,10 +145,13 @@ fn build_peers(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use air_test_utils::prelude::*;
use super::*; use super::*;
use air_test_utils::prelude::*;
use pretty_assertions::assert_eq;
use std::ops::Deref;
#[test] #[test]
fn test_execution() { fn test_execution() {
let exec = TestExecutor::new( let exec = TestExecutor::new(
@ -248,7 +251,7 @@ mod tests {
} }
#[test] #[test]
fn test_seq_result() { fn test_seq_ok() {
let exec = TestExecutor::new( let exec = TestExecutor::new(
TestRunParameters::from_init_peer_id("init_peer_id"), TestRunParameters::from_init_peer_id("init_peer_id"),
vec![], vec![],
@ -260,7 +263,7 @@ mod tests {
(ap 1 k) (ap 1 k)
(fold var i (fold var i
(seq (seq
(call i.$.p ("service" "func") [i k] k) ; seq_result = {"0":12,"default":42} (call i.$.p ("service" "func") [i k] k) ; seq_ok = {"0":12,"default":42}
(next i))))) (next i)))))
(call "init_peer_id" ("a" "b") []) ; ok = 0 (call "init_peer_id" ("a" "b") []) ; ok = 0
)"#, )"#,
@ -322,6 +325,152 @@ mod tests {
} }
} }
#[test]
fn test_map() {
let exec = TestExecutor::new(
TestRunParameters::from_init_peer_id("peer1"),
vec![],
IntoIterator::into_iter(["peer2", "peer3"]).map(Into::into),
r#"
(seq
(call "peer1" ("" "") [] peers) ; ok = ["peer2", "peer3"]
(fold peers p
(seq
(call p ("" "") [p]) ; map = {"peer2": 42, "peer3": 43}
(next p)
)))
"#,
)
.unwrap();
let result_init: Vec<_> = exec.execution_iter("peer1").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_next_pks!(&outcome1.next_peer_pks, ["peer2"]);
{
let results2 = 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 results3 = exec.execute_all("peer3").unwrap();
assert_eq!(results3.len(), 1);
let outcome3 = &results3[0];
assert_eq!(outcome3.ret_code, 0, "{:?}", outcome3);
assert_next_pks!(&outcome3.next_peer_pks, []);
let trace = trace_from_result(outcome3);
assert_eq!(
trace.deref(),
vec![
executed_state::scalar(json!(["peer2", "peer3"])),
executed_state::scalar(json!(42)),
executed_state::scalar(json!(43)),
]
);
}
}
#[test]
#[should_panic]
fn test_map_no_arg() {
let exec = TestExecutor::new(
TestRunParameters::from_init_peer_id("peer1"),
vec![],
IntoIterator::into_iter(["peer2", "peer3"]).map(Into::into),
r#"
(call "peer1" ("" "") [] p) ; map = {"any": "key"}
"#,
)
.unwrap();
let _result_init: Vec<_> = exec.execution_iter("peer1").unwrap().collect();
}
#[test]
fn test_seq_error() {
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}, {"p":"peer4"}]
(seq
(ap 1 k)
(fold var i
(seq
(call i.$.p ("service" "func") [i.$.v k] k) ; seq_error = {"0":{"ret_code":0,"result":12},"default":{"ret_code":1,"result":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},{"p":"peer4"}])),
scalar_number(12),
request_sent_by("peer2"),
])
);
}
{
let results3: Vec<_> = exec.execute_all("peer3").unwrap();
assert_eq!(results3.len(), 1);
// TODO why doesn't it fail?
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},{"p":"peer4"}])),
scalar_number(12),
request_sent_by("peer2"),
])
);
}
}
#[test] #[test]
fn test_echo() { fn test_echo() {
let exec = TestExecutor::new( let exec = TestExecutor::new(

View File

@ -21,8 +21,9 @@ use air_test_utils::{
prelude::{echo_call_service, unit_call_service}, prelude::{echo_call_service, unit_call_service},
CallRequestParams, CallServiceClosure, CallServiceResult, CallRequestParams, CallServiceClosure, CallServiceResult,
}; };
use serde_json::json;
use std::{cell::Cell, collections::HashMap, convert::TryInto, time::Duration}; use std::{borrow::Cow, cell::Cell, collections::HashMap, convert::TryInto, time::Duration};
pub struct ResultService { pub struct ResultService {
results: HashMap<u32, CallServiceClosure>, results: HashMap<u32, CallServiceClosure>,
@ -37,8 +38,10 @@ impl TryInto<CallServiceClosure> for ServiceDefinition {
Ok(Box::new(move |_| CallServiceResult::ok(jvalue.clone()))) Ok(Box::new(move |_| CallServiceResult::ok(jvalue.clone())))
} }
ServiceDefinition::Error(call_result) => Ok(Box::new(move |_| call_result.clone())), ServiceDefinition::Error(call_result) => Ok(Box::new(move |_| call_result.clone())),
ServiceDefinition::SeqResult(call_map) => Ok(seq_result_closure(call_map)), ServiceDefinition::SeqOk(call_map) => Ok(seq_ok_closure(call_map)),
ServiceDefinition::SeqError(call_map) => Ok(seq_error_closure(call_map)),
ServiceDefinition::Behaviour(name) => named_service_closure(name), ServiceDefinition::Behaviour(name) => named_service_closure(name),
ServiceDefinition::Map(map) => Ok(map_service_closure(map)),
} }
} }
} }
@ -51,7 +54,7 @@ fn named_service_closure(name: String) -> Result<CallServiceClosure, String> {
} }
} }
fn seq_result_closure(call_map: HashMap<String, serde_json::Value>) -> CallServiceClosure { fn seq_ok_closure(call_map: HashMap<String, serde_json::Value>) -> CallServiceClosure {
let call_number_seq = Cell::new(0); let call_number_seq = Cell::new(0);
Box::new(move |_| { Box::new(move |_| {
@ -74,6 +77,45 @@ fn seq_result_closure(call_map: HashMap<String, serde_json::Value>) -> CallServi
}) })
} }
fn seq_error_closure(call_map: HashMap<String, CallServiceResult>) -> 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);
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()
})
}
fn map_service_closure(map: HashMap<String, serde_json::Value>) -> CallServiceClosure {
Box::new(move |args| {
let key = args
.arguments
.get(0)
.expect("At least one arugment expected");
// Strings are looked up by value, other objects -- by string representation.
//
// For example, `"key"` is looked up as `"key"`, `5` is looked up as `"5"`, `["test"]` is looked up
// as `"[\"test\"]"`.
let key_repr = match key {
serde_json::Value::String(s) => Cow::Borrowed(s.as_str()),
val => Cow::Owned(val.to_string()),
};
CallServiceResult::ok(json!(map.get(key_repr.as_ref()).cloned()))
})
}
impl ResultService { impl ResultService {
pub(crate) fn new(results: HashMap<u32, ServiceDefinition>) -> Result<Self, String> { pub(crate) fn new(results: HashMap<u32, ServiceDefinition>) -> Result<Self, String> {
Ok(Self { Ok(Self {

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
mod parser; pub(crate) mod parser;
pub(crate) mod walker; pub(crate) mod walker;
use crate::asserts::ServiceDefinition; use crate::asserts::ServiceDefinition;

View File

@ -15,16 +15,16 @@
*/ */
use super::{Call, Sexp, Triplet}; use super::{Call, Sexp, Triplet};
use crate::asserts::{parser::delim_ws, ServiceDefinition}; use crate::asserts::ServiceDefinition;
use nom::branch::alt; use nom::branch::alt;
use nom::bytes::complete::{is_not, tag}; use nom::bytes::complete::{is_not, tag};
use nom::character::complete::{alphanumeric1, multispace0, multispace1, one_of, space1}; use nom::character::complete::{alphanumeric1, multispace0, multispace1, one_of, space1};
use nom::combinator::{cut, map, map_res, opt, recognize, value}; use nom::combinator::{cut, map, map_res, opt, recognize, value};
use nom::error::{context, VerboseError, VerboseErrorKind}; use nom::error::{context, VerboseError, VerboseErrorKind};
use nom::multi::{many1_count, separated_list0}; use nom::multi::{many0, many1, many1_count, separated_list0};
use nom::sequence::{delimited, pair, preceded, separated_pair, terminated}; use nom::sequence::{delimited, pair, preceded, separated_pair, terminated};
use nom::IResult; use nom::{IResult, InputTakeAtPosition};
use nom_locate::LocatedSpan; use nom_locate::LocatedSpan;
use std::str::FromStr; use std::str::FromStr;
@ -87,11 +87,11 @@ fn parse_sexp_list(inp: Input<'_>) -> IResult<Input<'_>, Sexp, ParseError<'_>> {
context( context(
"within generic list", "within generic list",
preceded( preceded(
terminated(tag("("), multispace0), terminated(tag("("), sexp_multispace0),
cut(terminated( cut(terminated(
map(separated_list0(multispace1, parse_sexp), Sexp::list), map(separated_list0(sexp_multispace1, parse_sexp), Sexp::list),
preceded( preceded(
multispace0, sexp_multispace0,
context("closing parentheses not found", tag(")")), context("closing parentheses not found", tag(")")),
), ),
)), )),
@ -150,12 +150,12 @@ fn parse_sexp_call_content(inp: Input<'_>) -> IResult<Input<'_>, Sexp, ParseErro
// possible variable, closing ")", possible annotation // possible variable, closing ")", possible annotation
pair( pair(
terminated( terminated(
opt(preceded(multispace1, map(parse_sexp_symbol, Box::new))), opt(preceded(sexp_multispace1, map(parse_sexp_symbol, Box::new))),
preceded(multispace0, tag(")")), preceded(sexp_multispace0, tag(")")),
), ),
alt(( alt((
opt(preceded(pair(space1, tag("; ")), parse_annotation)), opt(preceded(pair(space1, tag("; ")), parse_annotation)),
value(None, multispace0), value(None, sexp_multispace0),
)), )),
), ),
), ),
@ -183,12 +183,12 @@ fn parse_sexp_call_triplet(inp: Input<'_>) -> IResult<Input<'_>, Box<Triplet>, P
map( map(
separated_pair( separated_pair(
context("triplet peer_id", parse_sexp), context("triplet peer_id", parse_sexp),
multispace0, sexp_multispace0,
delimited( delimited(
delim_ws(tag("(")), delim_ws(tag("(")),
separated_pair( separated_pair(
context("triplet service name", parse_sexp_string), context("triplet service name", parse_sexp_string),
multispace0, sexp_multispace0,
context("triplet function name", parse_sexp), context("triplet function name", parse_sexp),
), ),
delim_ws(tag(")")), delim_ws(tag(")")),
@ -202,13 +202,124 @@ fn parse_sexp_call_arguments(inp: Input<'_>) -> IResult<Input<'_>, Vec<Sexp>, Pa
delimited(tag("["), separated_list0(multispace1, parse_sexp), tag("]"))(inp) delimited(tag("["), separated_list0(multispace1, parse_sexp), tag("]"))(inp)
} }
pub(crate) fn delim_ws<I, O, E, F>(f: F) -> impl FnMut(I) -> IResult<I, O, E>
where
F: nom::Parser<I, O, E>,
E: nom::error::ParseError<I>,
I: nom::InputTakeAtPosition
+ nom::InputLength
+ for<'a> nom::Compare<&'a str>
+ nom::InputTake
+ Clone,
<I as InputTakeAtPosition>::Item: nom::AsChar + Clone,
for<'a> &'a str: nom::FindToken<<I as InputTakeAtPosition>::Item>,
{
delimited(sexp_multispace0, f, sexp_multispace0)
}
pub(crate) fn sexp_multispace0<I, E>(inp: I) -> IResult<I, (), E>
where
E: nom::error::ParseError<I>,
I: InputTakeAtPosition
+ nom::InputLength
+ for<'a> nom::Compare<&'a str>
+ nom::InputTake
+ Clone,
<I as InputTakeAtPosition>::Item: nom::AsChar + Clone,
for<'a> &'a str: nom::FindToken<<I as InputTakeAtPosition>::Item>,
{
map(
opt(many0(pair(
// white space
multispace1,
// possible ;;, ;;; comment
opt(pair(tag(";;"), is_not("\r\n"))),
))),
|_| (),
)(inp)
}
pub(crate) fn sexp_multispace1(inp: Input<'_>) -> IResult<Input<'_>, (), ParseError<'_>> {
map(
// It's not the fastest implementation, but easiest to write.
// It passes initial whitespace two times.
many1(alt((
map(
pair(
// white space
multispace0,
// ;;, ;;;, etc comment
pair(tag(";;"), is_not("\r\n")),
),
|_| (),
),
map(multispace1, |_| ()),
))),
|_| (),
)(inp)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use crate::asserts::ServiceDefinition;
use pretty_assertions::assert_eq;
use serde_json::json; use serde_json::json;
use super::*; #[test]
fn test_multispace0_empty() {
let res = sexp_multispace0::<_, ()>("");
assert!(res.is_ok(), "{}", res.unwrap_err());
}
use crate::asserts::ServiceDefinition; #[test]
fn test_multispace0_spaces() {
let res = sexp_multispace0::<_, ()>(" ");
assert!(res.is_ok(), "{}", res.unwrap_err());
}
#[test]
fn test_multispace0_comment() {
let res = sexp_multispace0::<_, ()>(";; this is comment");
assert!(res.is_ok(), "{}", res.unwrap_err());
}
#[test]
fn test_multispace0_comment_with_space() {
let res = sexp_multispace0::<_, ()>(" ;; ");
assert!(res.is_ok(), "{}", res.unwrap_err());
}
#[test]
fn test_multispace0_multiline() {
let res = sexp_multispace0::<_, ()>(" ;; \n ;;;; \n ");
assert!(res.is_ok(), "{}", res.unwrap_err());
}
#[test]
fn test_multispace1_empty() {
let res = sexp_multispace1("".into());
assert!(res.is_err());
}
#[test]
fn test_multispace1_space() {
let res = sexp_multispace1(" ".into());
assert!(res.is_ok(), "{}", res.unwrap_err());
}
#[test]
fn test_multispace1_comment() {
let res = sexp_multispace1(" ;; ".into());
assert!(res.is_ok(), "{}", res.unwrap_err());
}
#[test]
fn test_multispace1_multiline() {
let res = sexp_multispace1(" ;; \n ;;;; \n ".into());
assert!(res.is_ok(), "{}", res.unwrap_err());
}
#[test] #[test]
fn test_symbol() { fn test_symbol() {
@ -516,4 +627,37 @@ mod tests {
let res = Sexp::from_str(sexp_str); let res = Sexp::from_str(sexp_str);
assert!(res.is_ok(), "{:?}", res); assert!(res.is_ok(), "{:?}", res);
} }
#[test]
fn test_comments() {
let sexp_str = r#" ;; One comment
( ;;; Second comment
;; The third one
(par ;;;; Comment comment comment
;;;; Comment comment comment
(null) ;;;;; Comment
(fail ;; Fails
1 ;; Retcode
"test" ;; Message
;; Nothing more
)
) ;;;;; Comment
;;;;; Comment
) ;;;;; Comment
;;; Comment
"#;
let res = Sexp::from_str(sexp_str);
assert_eq!(
res,
Ok(Sexp::list(vec![Sexp::list(vec![
Sexp::symbol("par"),
Sexp::list(vec![Sexp::symbol("null"),]),
Sexp::list(vec![
Sexp::symbol("fail"),
Sexp::symbol("1"),
Sexp::string("test"),
]),
])]))
);
}
} }