use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeStartPayload {
pub task_name: String,
pub runtime_name: String,
pub language: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeStdoutPayload {
pub task_name: String,
pub chunk: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeStderrPayload {
pub task_name: String,
pub chunk: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeEndPayload {
pub task_name: String,
pub exit_code: i32,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeErrorPayload {
pub task_name: String,
pub kind: String,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeErrorKind {
NotConfigured,
Timeout,
SandboxUnavailable,
OomKilled,
Cancelled,
Internal,
Unknown,
}
impl RuntimeErrorKind {
pub fn from_wire(s: &str) -> Self {
match s {
"NotConfigured" => Self::NotConfigured,
"Timeout" => Self::Timeout,
"SandboxUnavailable" => Self::SandboxUnavailable,
"OomKilled" => Self::OomKilled,
"Cancelled" => Self::Cancelled,
"Internal" => Self::Internal,
_ => Self::Unknown,
}
}
pub fn to_wire(self) -> Option<&'static str> {
match self {
Self::NotConfigured => Some("NotConfigured"),
Self::Timeout => Some("Timeout"),
Self::SandboxUnavailable => Some("SandboxUnavailable"),
Self::OomKilled => Some("OomKilled"),
Self::Cancelled => Some("Cancelled"),
Self::Internal => Some("Internal"),
Self::Unknown => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", content = "payload")]
pub enum RuntimeEvent {
RuntimeStart(RuntimeStartPayload),
RuntimeStdout(RuntimeStdoutPayload),
RuntimeStderr(RuntimeStderrPayload),
RuntimeEnd(RuntimeEndPayload),
RuntimeError(RuntimeErrorPayload),
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn runtime_start_roundtrips() {
let evt = RuntimeEvent::RuntimeStart(RuntimeStartPayload {
task_name: "analyse".into(),
runtime_name: "run_python".into(),
language: "python".into(),
});
let wire = serde_json::to_value(&evt).unwrap();
assert_eq!(wire["type"], "RuntimeStart");
assert_eq!(wire["payload"]["task_name"], "analyse");
assert_eq!(wire["payload"]["runtime_name"], "run_python");
assert_eq!(wire["payload"]["language"], "python");
let back: RuntimeEvent = serde_json::from_value(wire).unwrap();
assert_eq!(back, evt);
}
#[test]
fn runtime_stdout_roundtrips() {
let wire = json!({
"type": "RuntimeStdout",
"payload": {"task_name": "t", "chunk": "hello\n"},
});
let evt: RuntimeEvent = serde_json::from_value(wire.clone()).unwrap();
match &evt {
RuntimeEvent::RuntimeStdout(p) => {
assert_eq!(p.task_name, "t");
assert_eq!(p.chunk, "hello\n");
}
other => panic!("expected RuntimeStdout, got {other:?}"),
}
assert_eq!(serde_json::to_value(&evt).unwrap(), wire);
}
#[test]
fn runtime_stderr_roundtrips() {
let evt = RuntimeEvent::RuntimeStderr(RuntimeStderrPayload {
task_name: "t".into(),
chunk: "warn: deprecated\n".into(),
});
let wire = serde_json::to_value(&evt).unwrap();
assert_eq!(wire["type"], "RuntimeStderr");
let back: RuntimeEvent = serde_json::from_value(wire).unwrap();
assert_eq!(back, evt);
}
#[test]
fn runtime_end_roundtrips() {
let evt = RuntimeEvent::RuntimeEnd(RuntimeEndPayload {
task_name: "t".into(),
exit_code: 0,
duration_ms: 1234,
});
let wire = serde_json::to_value(&evt).unwrap();
assert_eq!(wire["type"], "RuntimeEnd");
assert_eq!(wire["payload"]["exit_code"], 0);
assert_eq!(wire["payload"]["duration_ms"], 1234);
let back: RuntimeEvent = serde_json::from_value(wire).unwrap();
assert_eq!(back, evt);
}
#[test]
fn runtime_end_negative_exit_code() {
let wire = json!({
"type": "RuntimeEnd",
"payload": {"task_name": "t", "exit_code": -9, "duration_ms": 50},
});
let evt: RuntimeEvent = serde_json::from_value(wire).unwrap();
match evt {
RuntimeEvent::RuntimeEnd(p) => assert_eq!(p.exit_code, -9),
_ => panic!("expected RuntimeEnd"),
}
}
#[test]
fn runtime_error_roundtrips() {
let evt = RuntimeEvent::RuntimeError(RuntimeErrorPayload {
task_name: "t".into(),
kind: "Timeout".into(),
message: "exceeded 30s budget".into(),
});
let wire = serde_json::to_value(&evt).unwrap();
assert_eq!(wire["type"], "RuntimeError");
let back: RuntimeEvent = serde_json::from_value(wire).unwrap();
assert_eq!(back, evt);
}
#[test]
fn runtime_error_kind_maps_known_variants() {
assert_eq!(
RuntimeErrorKind::from_wire("NotConfigured"),
RuntimeErrorKind::NotConfigured
);
assert_eq!(
RuntimeErrorKind::from_wire("Timeout"),
RuntimeErrorKind::Timeout
);
assert_eq!(
RuntimeErrorKind::from_wire("SandboxUnavailable"),
RuntimeErrorKind::SandboxUnavailable
);
assert_eq!(
RuntimeErrorKind::from_wire("OomKilled"),
RuntimeErrorKind::OomKilled
);
assert_eq!(
RuntimeErrorKind::from_wire("Cancelled"),
RuntimeErrorKind::Cancelled
);
assert_eq!(
RuntimeErrorKind::from_wire("Internal"),
RuntimeErrorKind::Internal
);
}
#[test]
fn runtime_error_kind_to_wire_round_trips() {
for s in [
"NotConfigured",
"Timeout",
"SandboxUnavailable",
"OomKilled",
"Cancelled",
"Internal",
] {
let kind = RuntimeErrorKind::from_wire(s);
assert_eq!(kind.to_wire(), Some(s), "wire round-trip for {s}");
}
assert_eq!(RuntimeErrorKind::Unknown.to_wire(), None);
}
#[test]
fn runtime_error_kind_unknown_falls_through() {
assert_eq!(
RuntimeErrorKind::from_wire("FutureKindFromNewerEngine"),
RuntimeErrorKind::Unknown
);
assert_eq!(RuntimeErrorKind::from_wire(""), RuntimeErrorKind::Unknown);
}
#[test]
fn unknown_runtime_type_fails_decode() {
let wire = json!({
"type": "RuntimeFoo",
"payload": {"task_name": "t"},
});
assert!(serde_json::from_value::<RuntimeEvent>(wire).is_err());
let other = json!({
"type": "TaskStart",
"payload": ["t", null],
});
assert!(serde_json::from_value::<RuntimeEvent>(other).is_err());
}
}