use serde::{Deserialize, Serialize};
pub const PROTOCOL_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TestNodeInfo {
pub node_id: String,
pub file_path: String,
pub lineno: Option<u32>,
pub name: String,
pub class_name: Option<String>,
pub markers: Vec<String>,
pub skip: bool,
pub xfail: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Request {
InitContext {
protocol_version: u32,
repo_path: String,
python_path: Option<String>,
#[serde(default)]
execution_mode: Option<String>,
},
Collect {
context_id: String,
force: bool,
},
Run {
context_id: String,
node_ids: Vec<String>,
workers: Option<u32>,
maxfail: Option<u32>,
},
List {
context_id: String,
keyword: Option<String>,
marker: Option<String>,
},
GetInventory {
context_id: String,
},
GetWorkerStatus {
context_id: String,
},
ConfigureWorkers {
context_id: String,
num_workers: u32,
},
Shutdown {
context_id: Option<String>,
},
Ping,
RunStream {
context_id: String,
node_ids: Vec<String>,
workers: Option<u32>,
maxfail: Option<u32>,
},
GetRunProgress {
context_id: String,
run_id: String,
},
GetFlakinessReport {
context_id: String,
},
GetTestFlakiness {
context_id: String,
node_id: String,
},
ConfigureRerun {
context_id: String,
enabled: bool,
max_reruns: u32,
only_flaky: bool,
delay_ms: u32,
},
GetRerunConfig {
context_id: String,
},
RunWithRerun {
context_id: String,
node_ids: Vec<String>,
workers: Option<u32>,
maxfail: Option<u32>,
},
ConfigureFixtureReuse {
context_id: String,
enabled: bool,
max_age_seconds: f64,
teardown_on_conftest_change: bool,
},
GetFixtureConfig {
context_id: String,
},
GetSessionStatus {
context_id: String,
},
GetShard {
context_id: String,
node_ids: Vec<String>,
shard_index: u32,
total_shards: u32,
strategy: String,
},
GetShardInfo {
context_id: String,
node_ids: Vec<String>,
total_shards: u32,
strategy: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Response {
ContextReady {
protocol_version: u32,
context_id: String,
inventory_hash: String,
},
CollectionComplete {
node_count: usize,
duration_ms: u64,
},
TestList {
node_ids: Vec<String>,
},
InventoryData {
hash: String,
collected_at: u64,
nodes: Vec<TestNodeInfo>,
},
RunComplete {
total: usize,
passed: usize,
failed: usize,
skipped: usize,
errors: usize,
duration_ms: u64,
},
WorkerStatus {
active_workers: u32,
idle_workers: u32,
tests_executed: u64,
avg_test_duration_ms: u64,
},
WorkerConfigAck {
num_workers: u32,
},
ShutdownAck,
Pong,
RunStarted {
run_id: String,
total_tests: usize,
},
RunProgress {
run_id: String,
total: usize,
completed: usize,
running: usize,
done: bool,
results: Vec<TestResultInfo>,
},
Error {
code: ErrorCode,
message: String,
},
FlakinessReport {
flaky_tests: Vec<FlakinessInfo>,
unstable_tests: Vec<FlakinessInfo>,
stable_count: usize,
total_tracked: usize,
},
TestFlakiness {
node_id: String,
failure_rate: f64,
is_flaky: bool,
flaky_streak: u32,
consecutive_failures: u32,
consecutive_passes: u32,
total_runs: u32,
recent_outcomes: Vec<String>,
},
RerunConfig {
enabled: bool,
max_reruns: u32,
only_flaky: bool,
delay_ms: u32,
},
FixtureConfig {
enabled: bool,
max_fixture_age_seconds: f64,
teardown_on_conftest_change: bool,
teardown_on_test_file_change: bool,
scopes_to_reuse: Vec<String>,
},
SessionStatus {
session_id: String,
repo_path: String,
created_at: f64,
last_run_at: f64,
total_runs: u32,
enabled: bool,
},
ShardedTests {
shard_index: u32,
total_shards: u32,
node_ids: Vec<String>,
},
ShardInfo {
strategy: String,
total_shards: u32,
total_tests: usize,
shard_test_counts: Vec<usize>,
shard_durations_ms: Vec<u64>,
count_imbalance_percent: f64,
duration_imbalance_percent: f64,
estimated_wall_time_ms: u64,
},
ConfigAck {
config_type: String,
config: serde_json::Value,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TestResultInfo {
pub node_id: String,
pub outcome: String,
pub duration_ms: u64,
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FlakinessInfo {
pub node_id: String,
pub failure_rate: f64,
pub flaky_streak: u32,
pub total_runs: u32,
pub consecutive_failures: u32,
pub consecutive_passes: u32,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCode {
ContextNotFound,
CollectionFailed,
InvalidRequest,
InternalError,
Timeout,
PythonNotFound,
VersionMismatch,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_roundtrip() {
let requests = vec![
Request::InitContext {
protocol_version: PROTOCOL_VERSION,
repo_path: "/path/to/repo".to_string(),
python_path: Some("/usr/bin/python3".to_string()),
execution_mode: Some("auto".to_string()),
},
Request::Collect {
context_id: "ctx-123".to_string(),
force: true,
},
Request::Run {
context_id: "ctx-123".to_string(),
node_ids: vec!["test_foo.py::test_bar".to_string()],
workers: Some(4),
maxfail: Some(1),
},
Request::List {
context_id: "ctx-123".to_string(),
keyword: Some("auth".to_string()),
marker: None,
},
Request::GetInventory {
context_id: "ctx-123".to_string(),
},
Request::Shutdown {
context_id: Some("ctx-123".to_string()),
},
Request::Ping,
Request::RunStream {
context_id: "ctx-123".to_string(),
node_ids: vec!["test_foo.py::test_bar".to_string()],
workers: Some(4),
maxfail: None,
},
Request::GetRunProgress {
context_id: "ctx-123".to_string(),
run_id: "run-123".to_string(),
},
];
for req in requests {
let encoded = rmp_serde::to_vec(&req).unwrap();
let decoded: Request = rmp_serde::from_slice(&encoded).unwrap();
assert_eq!(req, decoded);
}
}
#[test]
fn response_roundtrip() {
let responses = vec![
Response::ContextReady {
protocol_version: PROTOCOL_VERSION,
context_id: "ctx-123".to_string(),
inventory_hash: "abc123".to_string(),
},
Response::CollectionComplete {
node_count: 42,
duration_ms: 150,
},
Response::TestList {
node_ids: vec!["test_a".to_string(), "test_b".to_string()],
},
Response::InventoryData {
hash: "abc123".to_string(),
collected_at: 1234567890,
nodes: vec![TestNodeInfo {
node_id: "test.py::test_func".to_string(),
file_path: "test.py".to_string(),
lineno: Some(10),
name: "test_func".to_string(),
class_name: None,
markers: vec!["slow".to_string()],
skip: false,
xfail: false,
}],
},
Response::RunComplete {
total: 10,
passed: 8,
failed: 1,
skipped: 1,
errors: 0,
duration_ms: 5000,
},
Response::ShutdownAck,
Response::Pong,
Response::RunStarted {
run_id: "run-123".to_string(),
total_tests: 10,
},
Response::RunProgress {
run_id: "run-123".to_string(),
total: 10,
completed: 5,
running: 2,
done: false,
results: vec![TestResultInfo {
node_id: "test.py::test_foo".to_string(),
outcome: "passed".to_string(),
duration_ms: 100,
message: None,
}],
},
Response::Error {
code: ErrorCode::ContextNotFound,
message: "Context not found".to_string(),
},
];
for resp in responses {
let encoded = rmp_serde::to_vec(&resp).unwrap();
let decoded: Response = rmp_serde::from_slice(&encoded).unwrap();
assert_eq!(resp, decoded);
}
}
}