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",
"nom 7.1.1",
"nom_locate",
"pretty_assertions",
"serde_json",
"strum",
]

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
use air_test_framework::TestExecutor;
use air_test_utils::prelude::*;
#[test]
@ -27,24 +28,17 @@ fn issue_221() {
let peer_1_value = "peer_1_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#"
(seq
(seq
(seq
;; 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
(par
(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
;; with help of ap instruction
(ap value $stream)
@ -61,8 +55,8 @@ fn issue_221() {
;; appropriate way and state for (1) is returned
(par
(par
(call "{join_1_id}" ("" "") [iterator])
(call "{join_2_id}" ("" "") [iterator])
(call "{join_1_id}" ("" "") [iterator]) ; behaviour = echo
(call "{join_2_id}" ("" "") [iterator]) ; behaviour = echo
)
(next iterator)
)
@ -72,18 +66,20 @@ fn issue_221() {
)
"#);
let result = checked_call_vm!(set_variable, <_>::default(), &script, "", "");
let peer_1_result = checked_call_vm!(peer_1, <_>::default(), &script, "", result.data.clone());
let peer_2_result = checked_call_vm!(peer_2, <_>::default(), &script, "", result.data.clone());
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(),
let executor = TestExecutor::new(
TestRunParameters::from_init_peer_id("set_variable_id"),
vec![],
vec![peer_1_id, peer_2_id].into_iter().map(Into::into),
&script,
join_1_result.data,
peer_2_result.data.clone()
); // before 0.20.9 it fails here
)
.expect("Invalid annotated AIR script");
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 expected_trace = vec![
executed_state::scalar(json!([peer_1_id, peer_2_id])),

View File

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

View File

@ -36,11 +36,14 @@ pub enum ServiceDefinition {
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>),
#[strum_discriminants(strum(serialize = "seq_ok"))]
SeqOk(HashMap<String, JValue>),
#[strum_discriminants(strum(serialize = "seq_error"))]
SeqError(HashMap<String, CallServiceResult>),
/// Some known service by name: "echo", "unit" (more to follow).
#[strum_discriminants(strum(serialize = "behaviour"))]
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 crate::services::JValue;
use crate::transform::parser::delim_ws;
use air_test_utils::CallServiceResult;
use nom::{error::VerboseError, IResult, InputTakeAtPosition, Parser};
use nom::{error::VerboseError, IResult};
use std::{collections::HashMap, str::FromStr};
@ -50,8 +51,10 @@ pub fn parse_kw(inp: &str) -> IResult<&str, ServiceDefinition, ParseError> {
alt((
tag(ServiceTagName::Ok.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::Map.as_ref()),
)),
equal(),
cut(context(
@ -68,33 +71,26 @@ pub fn parse_kw(inp: &str) -> IResult<&str, ServiceDefinition, ParseError> {
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::SeqOk) => {
serde_json::from_str(value).map(ServiceDefinition::SeqOk)
}
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::Map) => serde_json::from_str(value).map(ServiceDefinition::Map),
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::*;
use pretty_assertions::assert_eq;
use serde_json::json;
#[test]
fn test_parse_empty() {
@ -151,13 +147,13 @@ mod tests {
}
#[test]
fn test_seq_result() {
fn test_seq_ok() {
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!(
res,
Ok(ServiceDefinition::SeqResult(maplit::hashmap! {
Ok(ServiceDefinition::SeqOk(maplit::hashmap! {
"default".to_owned() => json!(42),
"1".to_owned() => json!(true),
"3".to_owned() => json!([]),
@ -166,15 +162,45 @@ mod tests {
}
#[test]
fn test_seq_result_malformed() {
let res = ServiceDefinition::from_str(r#"seq_result={"default": 42, "1": true, "3": ]}"#);
fn test_seq_ok_malformed() {
let res = ServiceDefinition::from_str(r#"seq_ok={"default": 42, "1": true, "3": ]}"#);
assert!(res.is_err());
}
#[test]
fn test_seq_result_invalid() {
fn test_seq_ok_invalid() {
// 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());
}
@ -183,4 +209,16 @@ mod tests {
let res = ServiceDefinition::from_str(r#"behaviour=echo"#);
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)]
mod tests {
use air_test_utils::prelude::*;
use super::*;
use air_test_utils::prelude::*;
use pretty_assertions::assert_eq;
use std::ops::Deref;
#[test]
fn test_execution() {
let exec = TestExecutor::new(
@ -248,7 +251,7 @@ mod tests {
}
#[test]
fn test_seq_result() {
fn test_seq_ok() {
let exec = TestExecutor::new(
TestRunParameters::from_init_peer_id("init_peer_id"),
vec![],
@ -260,7 +263,7 @@ mod tests {
(ap 1 k)
(fold var i
(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)))))
(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]
fn test_echo() {
let exec = TestExecutor::new(

View File

@ -21,8 +21,9 @@ use air_test_utils::{
prelude::{echo_call_service, unit_call_service},
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 {
results: HashMap<u32, CallServiceClosure>,
@ -37,8 +38,10 @@ impl TryInto<CallServiceClosure> for ServiceDefinition {
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::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::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);
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 {
pub(crate) fn new(results: HashMap<u32, ServiceDefinition>) -> Result<Self, String> {
Ok(Self {

View File

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

View File

@ -15,16 +15,16 @@
*/
use super::{Call, Sexp, Triplet};
use crate::asserts::{parser::delim_ws, ServiceDefinition};
use crate::asserts::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::multi::{many0, many1, many1_count, separated_list0};
use nom::sequence::{delimited, pair, preceded, separated_pair, terminated};
use nom::IResult;
use nom::{IResult, InputTakeAtPosition};
use nom_locate::LocatedSpan;
use std::str::FromStr;
@ -87,11 +87,11 @@ fn parse_sexp_list(inp: Input<'_>) -> IResult<Input<'_>, Sexp, ParseError<'_>> {
context(
"within generic list",
preceded(
terminated(tag("("), multispace0),
terminated(tag("("), sexp_multispace0),
cut(terminated(
map(separated_list0(multispace1, parse_sexp), Sexp::list),
map(separated_list0(sexp_multispace1, parse_sexp), Sexp::list),
preceded(
multispace0,
sexp_multispace0,
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
pair(
terminated(
opt(preceded(multispace1, map(parse_sexp_symbol, Box::new))),
preceded(multispace0, tag(")")),
opt(preceded(sexp_multispace1, map(parse_sexp_symbol, Box::new))),
preceded(sexp_multispace0, tag(")")),
),
alt((
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(
separated_pair(
context("triplet peer_id", parse_sexp),
multispace0,
sexp_multispace0,
delimited(
delim_ws(tag("(")),
separated_pair(
context("triplet service name", parse_sexp_string),
multispace0,
sexp_multispace0,
context("triplet function name", parse_sexp),
),
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)
}
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)]
mod tests {
use super::*;
use crate::asserts::ServiceDefinition;
use pretty_assertions::assert_eq;
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]
fn test_symbol() {
@ -516,4 +627,37 @@ mod tests {
let res = Sexp::from_str(sexp_str);
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"),
]),
])]))
);
}
}