feat(testing-framework): tetraplet behavior (#396)

`behavior=tetraplet` annotation crates a service that returns call's tetraplets
This commit is contained in:
Ivan Boldyrev 2022-12-01 13:33:59 +03:00 committed by GitHub
parent 9fe7afb897
commit e5837e9171
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 320 additions and 71 deletions

View File

@ -0,0 +1,129 @@
/*
* 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 air_test_utils::{
prelude::{echo_call_service, unit_call_service},
CallServiceResult,
};
use nom::IResult;
use serde_json::json;
use strum::{AsRefStr, EnumDiscriminants, EnumString};
#[derive(Debug, Clone, PartialEq, Eq, EnumDiscriminants)]
#[strum_discriminants(strum(serialize_all = "snake_case"))]
#[strum_discriminants(derive(AsRefStr, EnumString))]
#[strum_discriminants(name(BehaviorTagName))]
pub enum Behavior {
Echo,
Unit,
Service,
Function,
Arg(usize),
Tetraplet,
}
pub(crate) fn parse_behaviour(inp: &str) -> IResult<&str, Behavior, super::parser::ParseError> {
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::character::complete::u32 as u32_parse;
use nom::combinator::{map, value};
use nom::sequence::{pair, preceded};
alt((
value(Behavior::Echo, tag(BehaviorTagName::Echo.as_ref())),
value(Behavior::Unit, tag(BehaviorTagName::Unit.as_ref())),
value(Behavior::Function, tag(BehaviorTagName::Function.as_ref())),
value(Behavior::Service, tag(BehaviorTagName::Service.as_ref())),
map(
preceded(
pair(tag(BehaviorTagName::Arg.as_ref()), tag(".")),
u32_parse,
),
|n| Behavior::Arg(n as _),
),
value(
Behavior::Tetraplet,
tag(BehaviorTagName::Tetraplet.as_ref()),
),
))(inp)
}
impl Behavior {
pub(crate) fn call(&self, params: air_test_utils::CallRequestParams) -> CallServiceResult {
use Behavior::*;
match self {
Echo => echo_call_service()(params),
Unit => unit_call_service()(params),
Function => CallServiceResult::ok(params.function_name.into()),
Service => CallServiceResult::ok(params.service_id.into()),
Arg(n) => match params.arguments.get(*n) {
Some(val) => CallServiceResult::ok(val.clone()),
None => CallServiceResult::err(
// TODO test-utils uses just json!{ "default" } value.
42,
json!("not enough arguments"),
),
},
Tetraplet => CallServiceResult::ok(serde_json::to_value(&params.tetraplets).unwrap()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_behavior_echo() {
let res = parse_behaviour("echo");
assert_eq!(
res,
Ok(("", Behavior::Echo)),
"{:?}",
BehaviorTagName::Echo.as_ref()
);
}
#[test]
fn test_parse_behavior_unit() {
let res = parse_behaviour("unit");
assert_eq!(res, Ok(("", Behavior::Unit)));
}
#[test]
fn test_parse_behavior_service() {
let res = parse_behaviour("service");
assert_eq!(res, Ok(("", Behavior::Service)));
}
#[test]
fn test_parse_behavior_function() {
let res = parse_behaviour("function");
assert_eq!(res, Ok(("", Behavior::Function)));
}
#[test]
fn test_parse_behavior_arg() {
let res = parse_behaviour("arg.42");
assert_eq!(res, Ok(("", Behavior::Arg(42))));
}
#[test]
fn test_parse_behavior_tetraplet() {
let res = parse_behaviour("tetraplet");
assert_eq!(res, Ok(("", Behavior::Tetraplet)));
}
}

View File

@ -14,20 +14,20 @@
* limitations under the License.
*/
mod behavior;
mod json;
pub(crate) mod parser;
use crate::services::JValue;
use air_test_utils::{
prelude::{echo_call_service, unit_call_service},
CallRequestParams, CallServiceResult,
};
use air_test_utils::{CallRequestParams, CallServiceResult};
use serde_json::json;
use strum::{AsRefStr, EnumDiscriminants, EnumString};
use std::{borrow::Cow, cell::Cell, collections::HashMap};
use self::behavior::Behavior;
/// Service definition in the testing framework comment DSL.
#[derive(Debug, PartialEq, Eq, Clone, EnumDiscriminants)]
#[strum_discriminants(derive(AsRefStr, EnumString))]
@ -53,7 +53,7 @@ pub enum ServiceDefinition {
},
/// Some known service by name: "echo", "unit" (more to follow).
#[strum_discriminants(strum(serialize = "behaviour"))]
Behaviour(String),
Behaviour(Behavior),
/// Maps first argument to a value
#[strum_discriminants(strum(serialize = "map"))]
Map(HashMap<String, JValue>),
@ -82,8 +82,8 @@ impl ServiceDefinition {
}
}
pub fn behaviour(name: impl Into<String>) -> Self {
Self::Behaviour(name.into())
pub fn behaviour(name: Behavior) -> Self {
Self::Behaviour(name)
}
pub fn map(map: HashMap<String, JValue>) -> Self {
@ -102,7 +102,7 @@ impl ServiceDefinition {
ref call_number_seq,
call_map,
} => call_seq_error(call_number_seq, call_map),
ServiceDefinition::Behaviour(name) => call_named_service(name, params),
ServiceDefinition::Behaviour(name) => name.call(params),
ServiceDefinition::Map(map) => call_map_service(map, params),
}
}
@ -149,14 +149,6 @@ fn call_seq_error(
.clone()
}
fn call_named_service(name: &str, params: CallRequestParams) -> CallServiceResult {
match name {
"echo" => echo_call_service()(params),
"unit" => unit_call_service()(params),
_ => unreachable!("shoudn't be allowed by a parser"),
}
}
fn call_map_service(
map: &HashMap<String, serde_json::Value>,
args: CallRequestParams,

View File

@ -15,7 +15,6 @@
*/
use super::{ServiceDefinition, ServiceTagName};
use crate::services::JValue;
use crate::transform::parser::delim_ws;
use air_test_utils::CallServiceResult;
@ -23,7 +22,7 @@ use nom::{error::VerboseError, IResult};
use std::{collections::HashMap, str::FromStr};
type ParseError<'inp> = VerboseError<&'inp str>;
pub(crate) type ParseError<'inp> = VerboseError<&'inp str>;
impl FromStr for ServiceDefinition {
type Err = String;
@ -38,12 +37,12 @@ impl FromStr for ServiceDefinition {
// kw "=" val
// example: "id=firstcall"
pub fn parse_kw(inp: &str) -> IResult<&str, ServiceDefinition, ParseError> {
use super::behavior::parse_behaviour;
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::character::complete::alphanumeric1;
use nom::combinator::{cut, map_res, recognize};
use nom::combinator::{cut, map, map_res, recognize};
use nom::error::context;
use nom::sequence::separated_pair;
use nom::sequence::{pair, preceded};
let equal = || delim_ws(tag("="));
let json_value = || {
@ -59,45 +58,56 @@ pub fn parse_kw(inp: &str) -> IResult<&str, ServiceDefinition, ParseError> {
))
};
delim_ws(map_res(
alt((
separated_pair(tag(ServiceTagName::Ok.as_ref()), equal(), json_value()),
separated_pair(tag(ServiceTagName::Error.as_ref()), equal(), json_map()),
separated_pair(tag(ServiceTagName::SeqOk.as_ref()), equal(), json_map()),
separated_pair(tag(ServiceTagName::SeqError.as_ref()), equal(), json_map()),
separated_pair(
tag(ServiceTagName::Behaviour.as_ref()),
equal(),
cut(alphanumeric1),
delim_ws(alt((
map_res(
preceded(
pair(tag(ServiceTagName::Ok.as_ref()), equal()),
json_value(),
),
separated_pair(tag(ServiceTagName::Map.as_ref()), equal(), json_map()),
)),
|(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::SeqOk) => {
serde_json::from_str(value).map(ServiceDefinition::seq_ok)
}
Ok(ServiceTagName::SeqError) => {
serde_json::from_str::<HashMap<String, CallServiceResult>>(value)
.map(ServiceDefinition::seq_error)
}
Ok(ServiceTagName::Behaviour) => Ok(ServiceDefinition::behaviour(value)),
Ok(ServiceTagName::Map) => serde_json::from_str(value).map(ServiceDefinition::map),
Err(_) => unreachable!("unknown tag {:?}", tag),
}
},
))(inp)
|value| serde_json::from_str(value).map(ServiceDefinition::Ok),
),
map_res(
preceded(
pair(tag(ServiceTagName::Error.as_ref()), equal()),
json_map(),
),
|value| serde_json::from_str::<CallServiceResult>(value).map(ServiceDefinition::Error),
),
map_res(
preceded(
pair(tag(ServiceTagName::SeqOk.as_ref()), equal()),
json_map(),
),
|value| serde_json::from_str(value).map(ServiceDefinition::seq_ok),
),
map_res(
preceded(
pair(tag(ServiceTagName::SeqError.as_ref()), equal()),
json_map(),
),
|value| {
serde_json::from_str::<HashMap<String, CallServiceResult>>(value)
.map(ServiceDefinition::seq_error)
},
),
map(
preceded(
pair(tag(ServiceTagName::Behaviour.as_ref()), equal()),
cut(parse_behaviour),
),
ServiceDefinition::Behaviour,
),
map_res(
preceded(pair(tag(ServiceTagName::Map.as_ref()), equal()), json_map()),
|value| serde_json::from_str(value).map(ServiceDefinition::Map),
),
)))(inp)
}
#[cfg(test)]
mod tests {
use crate::asserts::behavior::Behavior;
use super::*;
use pretty_assertions::assert_eq;
use serde_json::json;
@ -217,7 +227,7 @@ mod tests {
#[test]
fn test_behaviour() {
let res = ServiceDefinition::from_str(r#"behaviour=echo"#);
assert_eq!(res, Ok(ServiceDefinition::Behaviour("echo".to_owned())),);
assert_eq!(res, Ok(ServiceDefinition::Behaviour(Behavior::Echo)),);
}
#[test]

View File

@ -542,10 +542,7 @@ mod tests {
impl MarineService for Service {
fn call(&self, _params: CallRequestParams) -> crate::services::FunctionOutcome {
let mut cell = self.state.borrow_mut();
crate::services::FunctionOutcome::ServiceResult(
CallServiceResult::ok(cell.next().unwrap()),
<_>::default(),
)
crate::services::FunctionOutcome::from_value(cell.next().unwrap())
}
}
let service = Service {
@ -606,4 +603,110 @@ mod tests {
}
}
}
#[test]
fn test_behaviour_service() {
let peer = "peer1";
let exec = AirScriptExecutor::new(
TestRunParameters::from_init_peer_id(peer),
vec![],
std::iter::empty(),
r#"(call "peer1" ("service" "func") [1 22] arg) ; behaviour=service"#,
)
.unwrap();
let result_init: Vec<_> = exec.execution_iter(peer).unwrap().collect();
assert_eq!(result_init.len(), 1);
let outcome = &result_init[0];
assert_eq!(outcome.ret_code, 0);
assert_eq!(outcome.error_message, "");
assert_eq!(
trace_from_result(outcome),
ExecutionTrace::from(vec![scalar_string("service"),]),
)
}
#[test]
fn test_behaviour_function() {
let peer = "peer1";
let exec = AirScriptExecutor::new(
TestRunParameters::from_init_peer_id(peer),
vec![],
std::iter::empty(),
r#"(call "peer1" ("service" "func") [1 22] arg) ; behaviour=function"#,
)
.unwrap();
let result_init: Vec<_> = exec.execution_iter(peer).unwrap().collect();
assert_eq!(result_init.len(), 1);
let outcome = &result_init[0];
assert_eq!(outcome.ret_code, 0);
assert_eq!(outcome.error_message, "");
assert_eq!(
trace_from_result(outcome),
ExecutionTrace::from(vec![scalar_string("func"),]),
)
}
#[test]
fn test_behaviour_arg() {
let peer = "peer1";
let exec = AirScriptExecutor::new(
TestRunParameters::from_init_peer_id(peer),
vec![],
std::iter::empty(),
r#"(call "peer1" ("service" "func") [1 22] arg) ; behaviour=arg.1"#,
)
.unwrap();
let result_init: Vec<_> = exec.execution_iter(peer).unwrap().collect();
assert_eq!(result_init.len(), 1);
let outcome = &result_init[0];
assert_eq!(outcome.ret_code, 0);
assert_eq!(outcome.error_message, "");
assert_eq!(
trace_from_result(outcome),
ExecutionTrace::from(vec![scalar_number(22),]),
)
}
#[test]
fn test_behaviour_tetraplet() {
let peer = "peer1";
let exec = AirScriptExecutor::new(
TestRunParameters::from_init_peer_id(peer),
vec![],
std::iter::empty(),
r#"(call "peer1" ("service" "func") [1 22] arg) ; behaviour=tetraplet"#,
)
.unwrap();
let result_init: Vec<_> = exec.execution_iter(peer).unwrap().collect();
assert_eq!(result_init.len(), 1);
let outcome = &result_init[0];
assert_eq!(outcome.ret_code, 0);
assert_eq!(outcome.error_message, "");
assert_eq!(
trace_from_result(outcome),
ExecutionTrace::from(vec![scalar(json!([[{
"function_name": "",
"json_path": "",
"peer_pk": "peer1",
"service_id": "",
}], [{
"function_name": "",
"json_path": "",
"peer_pk": "peer1",
"service_id": "",
}]]))]),
)
}
}

View File

@ -33,6 +33,16 @@ pub enum FunctionOutcome {
Empty,
}
impl FunctionOutcome {
pub fn from_service_result(service_result: CallServiceResult) -> Self {
FunctionOutcome::ServiceResult(service_result, Duration::ZERO)
}
pub fn from_value(value: JValue) -> Self {
Self::from_service_result(CallServiceResult::ok(value))
}
}
/// A mocked Marine service.
pub trait MarineService {
fn call(&self, params: CallRequestParams) -> FunctionOutcome;

View File

@ -19,7 +19,7 @@ use crate::asserts::ServiceDefinition;
use air_test_utils::CallRequestParams;
use std::{cell::RefCell, collections::HashMap, rc::Rc, time::Duration};
use std::{cell::RefCell, collections::HashMap, rc::Rc};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct ResultStore {
@ -36,18 +36,23 @@ impl ResultStore {
}
impl MarineService for ResultStore {
fn call(&self, params: CallRequestParams) -> FunctionOutcome {
fn call(&self, mut params: CallRequestParams) -> FunctionOutcome {
let results = self.results.borrow();
if let Some((_, suffix)) = params.service_id.split_once("..") {
if let Ok(key) = suffix.parse() {
let service_desc = results.get(&key).expect("Unknown result id");
let service_result = service_desc.call(params);
FunctionOutcome::ServiceResult(service_result, Duration::ZERO)
} else {
// Pass malformed service names further in a chain
FunctionOutcome::NotDefined
}
let (real_service_id, suffix) = match params.service_id.rsplit_once("..") {
Some(split) => split,
None => return FunctionOutcome::NotDefined,
};
if let Ok(result_id) = suffix.parse::<usize>() {
let service_desc = results
.get(&result_id)
.unwrap_or_else(|| panic!("failed to parse service name {:?}", params.service_id));
// hide the artificial service_id
params.service_id = real_service_id.to_owned();
FunctionOutcome::from_service_result(service_desc.call(params))
} else {
// Pass malformed service names further in a chain
FunctionOutcome::NotDefined
}
}