use crate::test_descriptor::ast::Statement;
use crate::timeline::{SensorsTimeline, Timeline};
use serde::{Deserialize, Serialize};
use serde_json::to_writer_pretty;
use std::collections::BTreeMap;
use std::fs::File;
use std::path::PathBuf;
use crate::result::CompilerResult;
use crate::test_descriptor::concrete_test::ConcreteTest;
#[derive(Serialize, Deserialize)]
pub struct TestIdentifier {
test: [String; 2],
config: [String; 2],
}
impl TestIdentifier {
pub fn new(test_hash: u32, test_name: &str, config_hash: u32, config_name: &str) -> Self {
TestIdentifier {
test: [test_hash.to_string(), test_name.into()],
config: [config_hash.to_string(), config_name.into()],
}
}
pub fn emit(&self, output_path: PathBuf) -> CompilerResult<()> {
let mut res = CompilerResult::status_only("Emit JSON test identifier for mission control");
let file = check!(res, File::create(output_path));
check!(
res,
to_writer_pretty(file, self),
"Serialize test identifer and write to output file"
);
res
}
}
struct TestSensorBounds {
sequences: BTreeMap<String, SequenceSensorBounds>,
}
#[derive(Serialize, Deserialize)]
#[serde(transparent)]
struct SequenceSensorBounds {
sensors: BTreeMap<String, SensorBounds>,
}
#[derive(Serialize, Deserialize)]
struct SensorBounds {
times: Vec<u32>,
bound_values: Vec<BoundValue>,
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum BoundValue {
Value { left: f64, right: f64 },
NoValue { left: (), right: () },
}
impl TestSensorBounds {
pub fn new() -> Self {
TestSensorBounds {
sequences: BTreeMap::new(),
}
}
pub fn try_add_sensor_bounds_sequence(
&mut self,
key: &str,
) -> CompilerResult<&mut SequenceSensorBounds> {
let mut res = CompilerResult::new(format!("Add sensor bound sequence with key {}", key));
if self.sequences.contains_key(key) {
res.error(format!(
"A sensor bound sequence with key: {} already exists",
key
));
return res;
}
let new_sequence = SequenceSensorBounds::new();
self.sequences.insert(key.into(), new_sequence);
res.with_value(self.sequences.get_mut(key).unwrap())
}
}
impl SequenceSensorBounds {
pub fn new() -> Self {
SequenceSensorBounds {
sensors: BTreeMap::new(),
}
}
pub fn add_bounds(&mut self, timeline: &SensorsTimeline) -> CompilerResult<()> {
let mut res = CompilerResult::status_only("Add sensor bounds to JSON struct");
for (sensor, timeline) in &timeline.sensor_timelines {
let mut sensor_bounds = SensorBounds::new();
sensor_bounds.times = timeline.endpoints.clone();
for constraints_vec in &timeline.range_constraints {
match constraints_vec.len() {
0 => {
sensor_bounds.bound_values.push(BoundValue::NoValue {
left: (),
right: (),
});
}
1 => {
match &constraints_vec[0].sensor_bound {
crate::test_descriptor::ast::SensorBound::Boolean(_) => {
sensor_bounds.bound_values.push(BoundValue::NoValue {
left: (),
right: (),
})
}
crate::test_descriptor::ast::SensorBound::Numeric(bound) => {
sensor_bounds.bound_values.push(BoundValue::Value {
left: bound.left,
right: bound.right,
});
}
}
}
_ => {
res.error("Constraints vector contained more than one constraint!");
}
}
}
self.sensors.insert(sensor.to_string(), sensor_bounds);
}
res
}
}
impl SensorBounds {
pub fn new() -> Self {
SensorBounds {
times: Vec::new(),
bound_values: Vec::new(),
}
}
}
struct TestTimestrips {
timestrips: BTreeMap<String, Timestrip>,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(transparent)]
struct Timestrip {
sections: Vec<TimestripSection>,
}
#[derive(Serialize, Deserialize, Clone)]
struct TimestripSection {
id: u32,
group: u32,
title: String,
start_time: u128,
end_time: u128,
}
impl TestTimestrips {
pub fn new() -> Self {
TestTimestrips {
timestrips: BTreeMap::new(),
}
}
pub fn try_add_timestrip(&mut self, key: &str) -> CompilerResult<&mut Timestrip> {
let mut res = CompilerResult::new(format!("Add timestrip with key {}", key));
if self.timestrips.contains_key(key) {
res.error(format!("A timestrip with key: {} already exists", key));
return res;
}
let test_body_timestrip = Timestrip::new();
self.timestrips.insert(key.into(), test_body_timestrip);
res.with_value(self.timestrips.get_mut(key).unwrap())
}
}
impl Timestrip {
pub fn new() -> Self {
Timestrip {
sections: Vec::new(),
}
}
pub fn new_section(&mut self, title: &str, start_time: u128, end_time: u128, depth: u32) {
let new_section = TimestripSection {
id: self.sections.len() as u32,
group: depth,
title: title.to_string(),
start_time,
end_time,
};
self.sections.push(new_section);
}
fn create_sections(
&mut self,
statements: &[Statement],
time_offset: u128,
depth: u32,
) -> CompilerResult<()> {
let mut res =
CompilerResult::status_only(format!("Create timestrip sections with depth: {}", depth));
for statement in statements {
match statement {
Statement::Section(section_statement) => {
let start_time = time_offset + section_statement.time.start;
let end_time = start_time + section_statement.time.duration;
self.new_section(§ion_statement.name, start_time, end_time, depth);
res.require(self.create_sections(
§ion_statement.statements,
start_time,
depth + 1,
));
}
_ => (),
}
}
res
}
}
pub fn emit_json_sensor_bounds(
test_body_timeline: &Timeline,
abort_timelines: &Vec<Timeline>,
output_path: PathBuf,
) -> CompilerResult<()> {
let mut res = CompilerResult::status_only("Emit JSON sensor bounds for mission control");
let mut test_sensor_bounds = TestSensorBounds::new();
let test_body_bounds = check!(
res,
test_sensor_bounds.try_add_sensor_bounds_sequence("test_body")
);
check!(
res,
test_body_bounds.add_bounds(&test_body_timeline.timeline_sensor)
);
for (idx, abort_timeline) in abort_timelines.into_iter().enumerate() {
let abort_bounds = check!(
res,
test_sensor_bounds.try_add_sensor_bounds_sequence(&(idx + 1).to_string())
);
check!(
res,
abort_bounds.add_bounds(&abort_timeline.timeline_sensor)
);
}
let file = check!(res, File::create(output_path));
check!(
res,
to_writer_pretty(file, &test_sensor_bounds.sequences),
"Serialize sensor bounds and write to output file"
);
res
}
pub fn emit_timestrip(concrete: &ConcreteTest, output_path: PathBuf) -> CompilerResult<()> {
let mut res = CompilerResult::status_only("Emit timestrip for mission control");
let mut test_timestrips = TestTimestrips::new();
let test_body_timestrip = check!(res, test_timestrips.try_add_timestrip("test_body"));
let statements = &concrete.test.test_body.statements;
test_body_timestrip.create_sections(statements, 0u128, 0);
for abort in &concrete.test.aborts {
let abort_id = check!(res, concrete.get_abort_id(&abort.name));
let abort_timestrip = check!(
res,
test_timestrips.try_add_timestrip(&abort_id.to_string())
);
abort_timestrip.create_sections(&abort.statements, 0u128, 0);
}
let file = check!(res, File::create(output_path));
check!(
res,
to_writer_pretty(file, &test_timestrips.timestrips),
"Serialize timestrip and write to output file"
);
return res;
}
pub fn emit_abort_json(concrete: &ConcreteTest, output: PathBuf) -> CompilerResult<()> {
let mut res = CompilerResult::status_only("Emit abort idx and name mapping");
let aborts = &concrete.test.aborts;
let mut abort_mapping: BTreeMap<u32, &str> = BTreeMap::new();
for x in aborts {
abort_mapping.insert(check!(res, concrete.get_abort_id(&x.name)), &x.name);
}
let file = check!(res, File::create(output));
check!(
res,
to_writer_pretty(file, &abort_mapping),
"Serialize abort mappings and write to output file"
);
res
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_builder::TestBuilder;
use std::{fs, io::Read};
use tempfile::NamedTempFile;
#[test]
pub fn test_emit_empty_timeline() {
let timeline_expected = r#"{
"1": [],
"test_body": []
}"#;
let mut testbuilder = TestBuilder::new();
testbuilder.with_relay("relay1").with_relay("relay2");
testbuilder.with_sensor("sensor1").with_sensor("sensor2");
let mut test_body = testbuilder.with_body();
test_body.at(10).set("relay1");
test_body.at(40).set("relay2");
test_body
.at(5)
.require("sensor1", 10_f64, 40_f64, "HARD_ABORT");
test_body
.at(40)
.require("sensor1", 10_f64, 40_f64, "HARD_ABORT");
test_body.at(60).unset("relay1");
test_body.at(60).unset("relay2");
let mut abort = testbuilder.with_abort("pressure_lost");
abort.at(20).unset("relay1");
abort.at(20).unset("relay2");
let concrete_test_opt =
ConcreteTest::try_new(testbuilder.env, testbuilder.test).to_option();
assert!(concrete_test_opt.is_some());
let timeline_output_path = NamedTempFile::new().unwrap().into_temp_path().to_path_buf();
emit_timestrip(&concrete_test_opt.unwrap(), timeline_output_path.clone());
let mut timeline_output = String::new();
fs::File::open(timeline_output_path)
.expect("Failed to open tmp file")
.read_to_string(&mut timeline_output)
.expect("Failed to read from tmp file");
assert_eq!(timeline_expected, timeline_output)
}
#[test]
pub fn test_emit_timeline() {
let timeline_expected = r#"{
"1": [
{
"id": 0,
"group": 0,
"title": "abort_section",
"start_time": 50,
"end_time": 100
}
],
"test_body": [
{
"id": 0,
"group": 0,
"title": "example_section",
"start_time": 2,
"end_time": 52
},
{
"id": 1,
"group": 0,
"title": "new_section",
"start_time": 52,
"end_time": 102
},
{
"id": 2,
"group": 1,
"title": "inner_section",
"start_time": 58,
"end_time": 68
}
]
}"#;
let mut testbuilder = TestBuilder::new();
testbuilder.with_relay("relay1").with_relay("relay2");
testbuilder.with_sensor("sensor1").with_sensor("sensor2");
let mut test_body = testbuilder.with_body();
let mut example_section = test_body.section_from(2, 50, "example_section");
example_section.at(10).set("relay1");
example_section.at(40).set("relay2");
let mut new_section = test_body.section_from(52, 50, "new_section");
new_section.at(5).unset("relay1");
let mut inner_section = new_section.section_from(6, 10, "inner_section");
inner_section
.at(5)
.require("sensor1", 10_f64, 40_f64, "HARD_ABORT");
new_section.at(20).unset("relay2");
new_section
.at(40)
.require("sensor1", 10_f64, 40_f64, "HARD_ABORT");
let mut abort = testbuilder.with_abort("test_abort");
abort.at(20).unset("relay1");
let mut abort_section = abort.section_from(50, 50, "abort_section");
abort_section.at(40).unset("relay2");
let concrete_test_opt =
ConcreteTest::try_new(testbuilder.env, testbuilder.test).to_option();
assert!(concrete_test_opt.is_some());
let timeline_output_path = NamedTempFile::new().unwrap().into_temp_path().to_path_buf();
emit_timestrip(&concrete_test_opt.unwrap(), timeline_output_path.clone());
let mut timeline_output = String::new();
fs::File::open(timeline_output_path)
.expect("Failed to open tmp file")
.read_to_string(&mut timeline_output)
.expect("Failed to read from tmp file");
assert_eq!(timeline_expected, timeline_output)
}
#[test]
pub fn test_abort_mapping() {
let expected: &str = r#"{
"1": "test1",
"2": "test2",
"3": "test3",
"4": "test4"
}"#;
let mut testbuilder = TestBuilder::new();
testbuilder.with_abort("test1");
testbuilder.with_abort("test2");
testbuilder.with_abort("test3");
testbuilder.with_abort("test4");
let concrete_test_opt =
ConcreteTest::try_new(testbuilder.env, testbuilder.test).to_option();
assert!(concrete_test_opt.is_some());
let output_path = NamedTempFile::new().unwrap().into_temp_path().to_path_buf();
emit_abort_json(&concrete_test_opt.unwrap(), output_path.clone());
let mut abort_output = String::new();
fs::File::open(output_path)
.expect("Failed to open tmp file")
.read_to_string(&mut abort_output)
.expect("Failed to read from tmp file");
assert_eq!(abort_output, expected);
}
#[test]
pub fn empty_abort_mapping() {
let expected: &str = r#"{}"#;
let testbuilder = TestBuilder::new();
let concrete_test_opt =
ConcreteTest::try_new(testbuilder.env, testbuilder.test).to_option();
assert!(concrete_test_opt.is_some());
let output_path = NamedTempFile::new().unwrap().into_temp_path().to_path_buf();
emit_abort_json(&concrete_test_opt.unwrap(), output_path.clone());
let mut abort_output = String::new();
fs::File::open(output_path)
.expect("Failed to open tmp file")
.read_to_string(&mut abort_output)
.expect("Failed to read from tmp file");
assert_eq!(abort_output, expected);
}
#[test]
pub fn full_abort_mapping() {
let expected: &str = r#"{
"1": "test1",
"2": "test2",
"3": "test3",
"4": "test4",
"5": "test5",
"6": "test6",
"7": "test7",
"8": "test8",
"9": "test9",
"10": "test10",
"11": "test11",
"12": "test12",
"13": "test13",
"14": "test14",
"15": "test15"
}"#;
let mut testbuilder = TestBuilder::new();
testbuilder.with_abort("test1");
testbuilder.with_abort("test2");
testbuilder.with_abort("test3");
testbuilder.with_abort("test4");
testbuilder.with_abort("test5");
testbuilder.with_abort("test6");
testbuilder.with_abort("test7");
testbuilder.with_abort("test8");
testbuilder.with_abort("test9");
testbuilder.with_abort("test10");
testbuilder.with_abort("test11");
testbuilder.with_abort("test12");
testbuilder.with_abort("test13");
testbuilder.with_abort("test14");
testbuilder.with_abort("test15");
let concrete_test_opt =
ConcreteTest::try_new(testbuilder.env, testbuilder.test).to_option();
assert!(concrete_test_opt.is_some());
let output_path = NamedTempFile::new().unwrap().into_temp_path().to_path_buf();
emit_abort_json(&concrete_test_opt.unwrap(), output_path.clone());
let mut abort_output = String::new();
fs::File::open(output_path)
.expect("Failed to open tmp file")
.read_to_string(&mut abort_output)
.expect("Failed to read from tmp file");
assert_eq!(abort_output, expected);
}
#[test]
pub fn realistic_abort_mapping() {
let expected: &str = r#"{
"1": "section1_fail",
"2": "section2_fail",
"3": "inner_section_fail"
}"#;
let mut testbuilder = TestBuilder::new();
testbuilder.with_relay("relay1").with_relay("relay2");
testbuilder.with_sensor("sensor1").with_sensor("sensor2");
let mut test_body = testbuilder.with_body();
let mut example_section = test_body.section_from(2, 50, "example_section");
example_section.at(10).set("relay1");
example_section.at(40).set("relay2");
let mut new_section = test_body.section_from(52, 50, "new_section");
new_section.at(5).unset("relay1");
let mut inner_section = new_section.section_from(6, 10, "inner_section");
inner_section
.at(5)
.require("sensor1", 10_f64, 40_f64, "HARD_ABORT");
new_section.at(20).unset("relay2");
new_section
.at(40)
.require("sensor1", 10_f64, 40_f64, "HARD_ABORT");
testbuilder.with_abort("section1_fail");
testbuilder.with_abort("section2_fail");
testbuilder.with_abort("inner_section_fail");
let concrete_test_opt =
ConcreteTest::try_new(testbuilder.env, testbuilder.test).to_option();
assert!(concrete_test_opt.is_some());
let output_path = NamedTempFile::new().unwrap().into_temp_path().to_path_buf();
emit_abort_json(&concrete_test_opt.unwrap(), output_path.clone());
let mut abort_output = String::new();
fs::File::open(output_path)
.expect("Failed to open tmp file")
.read_to_string(&mut abort_output)
.expect("Failed to read from tmp file");
assert_eq!(abort_output, expected);
}
}