use std::{
collections::{BTreeMap, BTreeSet},
path::PathBuf,
};
use crate::{
config::Input,
descriptor::EnvValue,
id::{DataId, NodeId},
metadata::MetadataParameters,
};
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct IntegrationTestInput {
pub id: NodeId,
pub name: Option<String>,
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub args: Option<String>,
pub env: Option<BTreeMap<String, EnvValue>>,
#[serde(default)]
pub outputs: BTreeSet<DataId>,
#[serde(default)]
pub inputs: BTreeMap<DataId, Input>,
#[serde(skip_serializing_if = "Option::is_none")]
pub send_stdout_as: Option<String>,
pub events: Vec<TimedIncomingEvent>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recording_status: Option<Box<RecordingStatus>>,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "state", rename_all = "lowercase")]
pub enum RecordingStatus {
Clean,
Poisoned {
first_failure_event_index: usize,
first_failure_time_offset_secs: f64,
first_failure_error: String,
additional_failures: u64,
},
}
impl IntegrationTestInput {
pub fn new(id: NodeId, events: Vec<TimedIncomingEvent>) -> Self {
Self {
id,
name: None,
description: None,
args: None,
env: None,
outputs: BTreeSet::new(),
inputs: BTreeMap::new(),
send_stdout_as: None,
events,
recording_status: None,
}
}
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct TimedIncomingEvent {
pub time_offset_secs: f64,
#[serde(flatten)]
pub event: IncomingEvent,
}
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
pub enum IncomingEvent {
Stop,
Input {
id: DataId,
metadata: Option<MetadataParameters>,
#[serde(flatten)]
data: Option<Box<InputData>>,
},
InputClosed {
id: DataId,
},
AllInputsClosed,
}
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
pub enum InputData {
JsonObject {
data: serde_json::Value,
data_type: Option<serde_json::Value>,
},
ArrowFile {
path: PathBuf,
#[serde(default)]
batch_index: usize,
column: Option<String>,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn integration_test_input_without_recording_status_deserializes() {
let json = r#"{
"id": "test-node",
"events": []
}"#;
let v: IntegrationTestInput = serde_json::from_str(json).expect("deserialize legacy form");
assert!(v.recording_status.is_none());
}
#[test]
fn integration_test_input_with_clean_recording_status_deserializes() {
let json = r#"{
"id": "test-node",
"events": [],
"recording_status": { "state": "clean" }
}"#;
let v: IntegrationTestInput = serde_json::from_str(json).expect("deserialize clean form");
assert_eq!(v.recording_status.as_deref(), Some(&RecordingStatus::Clean));
}
#[test]
fn integration_test_input_with_poisoned_recording_status_deserializes() {
let json = r#"{
"id": "test-node",
"events": [],
"recording_status": {
"state": "poisoned",
"first_failure_event_index": 7,
"first_failure_time_offset_secs": 1.25,
"first_failure_error": "Arrow conversion failed: bad type",
"additional_failures": 3
}
}"#;
let v: IntegrationTestInput =
serde_json::from_str(json).expect("deserialize poisoned form");
match v.recording_status.map(|b| *b) {
Some(RecordingStatus::Poisoned {
first_failure_event_index,
first_failure_time_offset_secs,
first_failure_error,
additional_failures,
}) => {
assert_eq!(first_failure_event_index, 7);
assert_eq!(first_failure_time_offset_secs, 1.25);
assert_eq!(first_failure_error, "Arrow conversion failed: bad type");
assert_eq!(additional_failures, 3);
}
other => panic!("expected Poisoned, got {other:?}"),
}
}
}