use crate::app::{AppHandle, AppSpec, AppStartError, AppStopError};
use crate::cx::Cx;
use crate::lab::config::LabConfig;
use crate::lab::dual_run::{DualRunScenarioIdentity, ReplayMetadata, SeedLineageRecord};
use crate::lab::runtime::{HarnessAttachmentRef, LabRuntime, SporkHarnessReport};
use crate::types::Budget;
use std::collections::BTreeMap;
const LAB_SPORK_HARNESS_ADAPTER: &str = "lab.spork_harness";
#[derive(Debug)]
pub enum HarnessError {
Start(AppStartError),
Stop(AppStopError),
}
impl std::fmt::Display for HarnessError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Start(e) => write!(f, "harness start failed: {e}"),
Self::Stop(e) => write!(f, "harness stop failed: {e}"),
}
}
}
impl std::error::Error for HarnessError {}
pub struct SporkAppHarness {
runtime: LabRuntime,
app_handle: Option<AppHandle>,
app_name: String,
attachments: Vec<HarnessAttachmentRef>,
cx: Cx,
}
impl SporkAppHarness {
pub fn new(config: LabConfig, app: AppSpec) -> Result<Self, HarnessError> {
let mut runtime = LabRuntime::new(config);
let cx = Cx::for_testing();
let root_region = runtime.state.create_root_region(Budget::INFINITE);
let app_handle = app
.start(&mut runtime.state, &cx, root_region)
.map_err(HarnessError::Start)?;
let app_name = app_handle.name().to_string();
Ok(Self {
runtime,
app_handle: Some(app_handle),
app_name,
attachments: Vec::new(),
cx,
})
}
pub fn with_seed(seed: u64, app: AppSpec) -> Result<Self, HarnessError> {
Self::new(LabConfig::new(seed), app)
}
#[must_use]
pub fn runtime(&self) -> &LabRuntime {
&self.runtime
}
pub fn runtime_mut(&mut self) -> &mut LabRuntime {
&mut self.runtime
}
#[must_use]
pub fn cx(&self) -> &Cx {
&self.cx
}
#[must_use]
pub fn app_handle(&self) -> Option<&AppHandle> {
self.app_handle.as_ref()
}
#[must_use]
pub fn app_name(&self) -> &str {
&self.app_name
}
pub fn attach(&mut self, attachment: HarnessAttachmentRef) {
self.attachments.push(attachment);
}
pub fn attach_all(&mut self, attachments: impl IntoIterator<Item = HarnessAttachmentRef>) {
self.attachments.extend(attachments);
}
pub fn run_until_idle(&mut self) -> u64 {
self.runtime.run_until_idle()
}
pub fn run_until_quiescent(&mut self) -> u64 {
self.runtime.run_until_quiescent()
}
pub fn stop_app(&mut self) -> Result<(), HarnessError> {
let stop_result = if let Some(mut handle) = self.app_handle.take() {
handle
.stop(&mut self.runtime.state)
.map_err(HarnessError::Stop)
.map(|_| ())
} else {
Ok(())
};
self.runtime.run_until_quiescent();
stop_result
}
pub fn run_to_report(mut self) -> Result<SporkHarnessReport, HarnessError> {
self.runtime.run_until_idle();
let stop_result = if let Some(mut handle) = self.app_handle.take() {
handle
.stop(&mut self.runtime.state)
.map(|_| ())
.map_err(HarnessError::Stop)
} else {
Ok(())
};
self.runtime.run_until_quiescent();
stop_result?;
let report = self
.runtime
.spork_report(&self.app_name, std::mem::take(&mut self.attachments));
Ok(report)
}
#[must_use]
pub fn snapshot_report(&mut self) -> SporkHarnessReport {
self.runtime
.spork_report(&self.app_name, self.attachments.clone())
}
#[must_use]
pub fn oracles_pass(&mut self) -> bool {
let now = self.runtime.now();
self.runtime.oracles.report(now).all_passed()
}
}
type ScenarioFactory = std::sync::Arc<dyn Fn(&SporkScenarioConfig) -> AppSpec + Send + Sync>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SporkScenarioConfig {
pub seed: u64,
pub worker_count: usize,
pub trace_capacity: usize,
pub max_steps: Option<u64>,
pub panic_on_obligation_leak: bool,
pub panic_on_futurelock: bool,
}
impl Default for SporkScenarioConfig {
fn default() -> Self {
Self {
seed: 42,
worker_count: 1,
trace_capacity: 4096,
max_steps: Some(100_000),
panic_on_obligation_leak: true,
panic_on_futurelock: true,
}
}
}
impl SporkScenarioConfig {
#[must_use]
pub fn to_lab_config(&self) -> LabConfig {
let mut config = LabConfig::new(self.seed)
.worker_count(self.worker_count)
.trace_capacity(self.trace_capacity)
.panic_on_leak(self.panic_on_obligation_leak)
.panic_on_futurelock(self.panic_on_futurelock);
config = if let Some(max_steps) = self.max_steps {
config.max_steps(max_steps)
} else {
config.no_step_limit()
};
config.with_default_replay_recording()
}
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
use serde_json::json;
json!({
"seed": self.seed,
"worker_count": self.worker_count,
"trace_capacity": self.trace_capacity,
"max_steps": self.max_steps,
"panic_on_obligation_leak": self.panic_on_obligation_leak,
"panic_on_futurelock": self.panic_on_futurelock,
})
}
}
#[derive(Clone)]
pub struct SporkScenarioSpec {
id: String,
description: Option<String>,
expected_invariants: Vec<String>,
default_config: SporkScenarioConfig,
surface_id: Option<String>,
surface_contract_version: Option<String>,
seed_lineage_id: Option<String>,
app_factory: ScenarioFactory,
}
impl std::fmt::Debug for SporkScenarioSpec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SporkScenarioSpec")
.field("id", &self.id)
.field("description", &self.description)
.field("expected_invariants", &self.expected_invariants)
.field("default_config", &self.default_config)
.field("surface_id", &self.surface_id)
.field("surface_contract_version", &self.surface_contract_version)
.field("seed_lineage_id", &self.seed_lineage_id)
.finish_non_exhaustive()
}
}
impl SporkScenarioSpec {
pub fn new<F>(id: impl Into<String>, app_factory: F) -> Self
where
F: Fn(&SporkScenarioConfig) -> AppSpec + Send + Sync + 'static,
{
Self {
id: id.into(),
description: None,
expected_invariants: Vec::new(),
default_config: SporkScenarioConfig::default(),
surface_id: None,
surface_contract_version: None,
seed_lineage_id: None,
app_factory: std::sync::Arc::new(app_factory),
}
}
#[must_use]
pub fn id(&self) -> &str {
&self.id
}
#[must_use]
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
#[must_use]
pub fn expected_invariants(&self) -> &[String] {
&self.expected_invariants
}
#[must_use]
pub fn default_config(&self) -> &SporkScenarioConfig {
&self.default_config
}
#[must_use]
pub fn surface_id(&self) -> Option<&str> {
self.surface_id.as_deref()
}
#[must_use]
pub fn surface_contract_version(&self) -> Option<&str> {
self.surface_contract_version.as_deref()
}
#[must_use]
pub fn seed_lineage_id(&self) -> Option<&str> {
self.seed_lineage_id.as_deref()
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn with_expected_invariants<I, S>(mut self, invariants: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.expected_invariants = invariants.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn with_default_config(mut self, config: SporkScenarioConfig) -> Self {
self.default_config = config;
self
}
#[must_use]
pub fn with_surface_id(mut self, surface_id: impl Into<String>) -> Self {
self.surface_id = Some(surface_id.into());
self
}
#[must_use]
pub fn with_surface_contract_version(mut self, version: impl Into<String>) -> Self {
self.surface_contract_version = Some(version.into());
self
}
#[must_use]
pub fn with_seed_lineage_id(mut self, seed_lineage_id: impl Into<String>) -> Self {
self.seed_lineage_id = Some(seed_lineage_id.into());
self
}
fn dual_run_identity(&self, config: &SporkScenarioConfig) -> DualRunScenarioIdentity {
let description = self.description.clone().unwrap_or_else(|| self.id.clone());
let mut identity = DualRunScenarioIdentity::phase1(
&self.id,
self.surface_id.clone().unwrap_or_else(|| self.id.clone()),
self.surface_contract_version
.clone()
.unwrap_or_else(|| format!("{}.v1", self.id)),
description,
config.seed,
);
if let Some(ref seed_lineage_id) = self.seed_lineage_id {
let mut seed_plan = identity.seed_plan.clone();
seed_plan.seed_lineage_id.clone_from(seed_lineage_id);
identity = identity.with_seed_plan(seed_plan);
}
identity
}
}
#[derive(Debug)]
pub enum ScenarioRunnerError {
InvalidScenarioId,
DuplicateScenarioId(String),
UnknownScenarioId(String),
Harness(HarnessError),
}
impl std::fmt::Display for ScenarioRunnerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidScenarioId => write!(f, "scenario id must be non-empty"),
Self::DuplicateScenarioId(id) => {
write!(f, "scenario `{id}` already registered")
}
Self::UnknownScenarioId(id) => write!(f, "unknown scenario `{id}`"),
Self::Harness(err) => write!(f, "{err}"),
}
}
}
impl std::error::Error for ScenarioRunnerError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Harness(err) => Some(err),
_ => None,
}
}
}
impl From<HarnessError> for ScenarioRunnerError {
fn from(value: HarnessError) -> Self {
Self::Harness(value)
}
}
#[derive(Debug, Clone)]
pub struct SporkScenarioResult {
pub schema_version: u32,
pub scenario_id: String,
pub description: Option<String>,
pub expected_invariants: Vec<String>,
pub config: SporkScenarioConfig,
pub report: SporkHarnessReport,
pub adapter: String,
pub replay_metadata: ReplayMetadata,
pub seed_lineage: SeedLineageRecord,
}
impl SporkScenarioResult {
pub const SCHEMA_VERSION: u32 = 1;
#[must_use]
fn from_parts(
scenario: &SporkScenarioSpec,
config: SporkScenarioConfig,
report: SporkHarnessReport,
) -> Self {
let identity = scenario.dual_run_identity(&config);
let mut replay_metadata = identity
.lab_replay_metadata()
.with_lab_report(
report.trace_fingerprint(),
report.run.trace_certificate.event_hash,
report.run.trace_certificate.event_count,
report.run.trace_certificate.schedule_hash,
report.run.steps_total,
)
.with_repro_command(format!(
"ASUPERSYNC_SEED=0x{:X} rch exec -- cargo test {} -- --nocapture",
config.seed, scenario.id
));
if let Some(crashpack_path) = report.crashpack_path() {
replay_metadata = replay_metadata.with_artifact_path(crashpack_path.to_string());
}
Self {
schema_version: Self::SCHEMA_VERSION,
scenario_id: scenario.id.clone(),
description: scenario.description.clone(),
expected_invariants: scenario.expected_invariants.clone(),
config,
report,
adapter: LAB_SPORK_HARNESS_ADAPTER.to_string(),
replay_metadata,
seed_lineage: identity.seed_lineage(),
}
}
#[must_use]
pub fn passed(&self) -> bool {
self.report.passed()
}
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
use serde_json::json;
json!({
"schema_version": self.schema_version,
"scenario_id": self.scenario_id,
"surface_id": self.replay_metadata.family.surface_id,
"surface_contract_version": self.replay_metadata.family.surface_contract_version,
"description": self.description,
"expected_invariants": self.expected_invariants,
"config": self.config.to_json(),
"report": self.report.to_json(),
"seed_lineage_id": self.seed_lineage.seed_lineage_id,
"adapter": self.adapter,
"execution_instance_id": self.replay_metadata.instance.key(),
"replay_metadata": &self.replay_metadata,
"seed_lineage": &self.seed_lineage,
})
}
}
#[derive(Debug, Default, Clone)]
pub struct SporkScenarioRunner {
scenarios: BTreeMap<String, SporkScenarioSpec>,
}
impl SporkScenarioRunner {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, mut scenario: SporkScenarioSpec) -> Result<(), ScenarioRunnerError> {
let id = scenario.id().trim().to_string();
if id.is_empty() {
return Err(ScenarioRunnerError::InvalidScenarioId);
}
if self.scenarios.contains_key(&id) {
return Err(ScenarioRunnerError::DuplicateScenarioId(id));
}
scenario.id.clone_from(&id);
self.scenarios.insert(id, scenario);
Ok(())
}
#[must_use]
pub fn scenario_ids(&self) -> Vec<&str> {
self.scenarios.keys().map(String::as_str).collect()
}
pub fn run(&self, scenario_id: &str) -> Result<SporkScenarioResult, ScenarioRunnerError> {
self.run_with_config(scenario_id, None)
}
pub fn run_with_config(
&self,
scenario_id: &str,
config_override: Option<SporkScenarioConfig>,
) -> Result<SporkScenarioResult, ScenarioRunnerError> {
let scenario = self
.scenarios
.get(scenario_id)
.ok_or_else(|| ScenarioRunnerError::UnknownScenarioId(scenario_id.to_string()))?;
let config = config_override.unwrap_or_else(|| scenario.default_config().clone());
let app = (scenario.app_factory)(&config);
let harness = SporkAppHarness::new(config.to_lab_config(), app)?;
let report = harness.run_to_report()?;
Ok(SporkScenarioResult::from_parts(scenario, config, report))
}
pub fn run_all(&self) -> Result<Vec<SporkScenarioResult>, ScenarioRunnerError> {
self.scenarios
.keys()
.map(|id| self.run(id))
.collect::<Result<Vec<_>, _>>()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lab::{SporkHarnessReport, SporkScenarioRunner};
#[test]
fn harness_empty_app_lifecycle() {
crate::test_utils::init_test_logging();
crate::test_phase!("harness_empty_app_lifecycle");
let app = AppSpec::new("empty_app");
let mut harness = SporkAppHarness::with_seed(99, app).unwrap();
harness.run_until_idle();
let report = harness.run_to_report().unwrap();
assert_eq!(report.schema_version, SporkHarnessReport::SCHEMA_VERSION);
assert_eq!(report.app, "empty_app");
crate::test_complete!("harness_empty_app_lifecycle");
}
#[test]
fn harness_deterministic_across_runs() {
crate::test_utils::init_test_logging();
crate::test_phase!("harness_deterministic_across_runs");
let report_a = {
let app = AppSpec::new("det_app");
let harness = SporkAppHarness::with_seed(42, app).unwrap();
harness.run_to_report().unwrap()
};
let report_b = {
let app = AppSpec::new("det_app");
let harness = SporkAppHarness::with_seed(42, app).unwrap();
harness.run_to_report().unwrap()
};
assert_eq!(
report_a.run.trace_fingerprint, report_b.run.trace_fingerprint,
"same seed must produce identical trace fingerprint"
);
assert_eq!(report_a.to_json(), report_b.to_json());
crate::test_complete!("harness_deterministic_across_runs");
}
#[test]
fn harness_attachments_in_report() {
crate::test_utils::init_test_logging();
crate::test_phase!("harness_attachments_in_report");
let app = AppSpec::new("attach_app");
let mut harness = SporkAppHarness::with_seed(7, app).unwrap();
harness.attach(HarnessAttachmentRef::trace("trace.json"));
harness.attach(HarnessAttachmentRef::crashpack("crash.tar"));
let report = harness.run_to_report().unwrap();
assert_eq!(report.attachments.len(), 2);
crate::test_complete!("harness_attachments_in_report");
}
#[test]
fn harness_snapshot_report_does_not_stop() {
crate::test_utils::init_test_logging();
crate::test_phase!("harness_snapshot_report_does_not_stop");
let app = AppSpec::new("snap_app");
let mut harness = SporkAppHarness::with_seed(1, app).unwrap();
harness.run_until_idle();
let snap = harness.snapshot_report();
assert_eq!(snap.app, "snap_app");
assert!(harness.app_handle().is_some());
let _final_report = harness.run_to_report().unwrap();
crate::test_complete!("harness_snapshot_report_does_not_stop");
}
#[test]
fn scenario_runner_register_and_run() {
crate::test_utils::init_test_logging();
crate::test_phase!("scenario_runner_register_and_run");
let mut runner = SporkScenarioRunner::new();
let scenario = SporkScenarioSpec::new("empty.lifecycle", |_| AppSpec::new("empty_app"))
.with_description("empty lifecycle smoke scenario")
.with_expected_invariants(["no_task_leaks", "quiescence_on_close"])
.with_default_config(SporkScenarioConfig {
seed: 777,
worker_count: 2,
trace_capacity: 2048,
max_steps: Some(50_000),
panic_on_obligation_leak: true,
panic_on_futurelock: true,
});
runner.register(scenario).unwrap();
let result = runner.run("empty.lifecycle").unwrap();
assert_eq!(result.schema_version, SporkScenarioResult::SCHEMA_VERSION);
assert_eq!(result.scenario_id, "empty.lifecycle");
assert_eq!(result.config.seed, 777);
assert_eq!(result.report.app, "empty_app");
assert_eq!(result.adapter, LAB_SPORK_HARNESS_ADAPTER);
assert_eq!(result.replay_metadata.family.surface_id, "empty.lifecycle");
assert_eq!(
result.replay_metadata.family.surface_contract_version,
"empty.lifecycle.v1"
);
assert!(
result
.expected_invariants
.iter()
.any(|i| i == "no_task_leaks")
);
crate::test_complete!("scenario_runner_register_and_run");
}
#[test]
fn scenario_runner_deterministic_for_same_seed() {
crate::test_utils::init_test_logging();
crate::test_phase!("scenario_runner_deterministic_for_same_seed");
let mut runner = SporkScenarioRunner::new();
runner
.register(
SporkScenarioSpec::new("det.seed", |_| AppSpec::new("deterministic_app"))
.with_default_config(SporkScenarioConfig {
seed: 4242,
worker_count: 1,
trace_capacity: 4096,
max_steps: Some(100_000),
panic_on_obligation_leak: true,
panic_on_futurelock: true,
}),
)
.unwrap();
let a = runner.run("det.seed").unwrap();
let b = runner.run("det.seed").unwrap();
assert_eq!(a.report.trace_fingerprint(), b.report.trace_fingerprint());
assert_eq!(a.to_json(), b.to_json());
crate::test_complete!("scenario_runner_deterministic_for_same_seed");
}
#[test]
fn scenario_runner_preserves_configured_dual_run_surface_metadata() {
crate::test_utils::init_test_logging();
crate::test_phase!("scenario_runner_preserves_configured_dual_run_surface_metadata");
let mut runner = SporkScenarioRunner::new();
runner
.register(
SporkScenarioSpec::new("cancel.race", |_| AppSpec::new("surface_app"))
.with_surface_id("cancel.race")
.with_surface_contract_version("cancel.race.v1")
.with_seed_lineage_id("seed.cancel.race.v1"),
)
.unwrap();
let result = runner.run("cancel.race").unwrap();
assert_eq!(result.replay_metadata.family.surface_id, "cancel.race");
assert_eq!(
result.replay_metadata.family.surface_contract_version,
"cancel.race.v1"
);
assert_eq!(result.seed_lineage.seed_lineage_id, "seed.cancel.race.v1");
crate::test_complete!("scenario_runner_preserves_configured_dual_run_surface_metadata");
}
#[test]
fn scenario_runner_rejects_duplicate_ids() {
crate::test_utils::init_test_logging();
crate::test_phase!("scenario_runner_rejects_duplicate_ids");
let mut runner = SporkScenarioRunner::new();
runner
.register(SporkScenarioSpec::new("dup.id", |_| AppSpec::new("first")))
.unwrap();
let duplicate = runner.register(SporkScenarioSpec::new("dup.id", |_| AppSpec::new("dup")));
assert!(matches!(
duplicate,
Err(ScenarioRunnerError::DuplicateScenarioId(ref id)) if id == "dup.id"
));
crate::test_complete!("scenario_runner_rejects_duplicate_ids");
}
#[test]
fn scenario_runner_normalizes_whitespace_ids() {
crate::test_utils::init_test_logging();
crate::test_phase!("scenario_runner_normalizes_whitespace_ids");
let mut runner = SporkScenarioRunner::new();
runner
.register(SporkScenarioSpec::new(" normalized.id ", |_| {
AppSpec::new("normalized_app")
}))
.unwrap();
assert_eq!(runner.scenario_ids(), vec!["normalized.id"]);
let result = runner.run("normalized.id").unwrap();
assert_eq!(result.scenario_id, "normalized.id");
assert_eq!(result.report.app, "normalized_app");
crate::test_complete!("scenario_runner_normalizes_whitespace_ids");
}
fn conformance_child(name: &str) -> crate::supervision::ChildSpec {
crate::supervision::ChildSpec {
name: name.into(),
start: Box::new(
|scope: &crate::cx::Scope<'static, crate::types::policy::FailFast>,
state: &mut crate::runtime::state::RuntimeState,
_cx: &crate::cx::Cx| {
state
.create_task(scope.region_id(), scope.budget(), async { 0_u8 })
.map(|(_, stored)| stored.task_id())
},
),
restart: crate::supervision::SupervisionStrategy::Stop,
shutdown_budget: crate::types::Budget::INFINITE,
depends_on: vec![],
registration: crate::supervision::NameRegistrationPolicy::None,
start_immediately: true,
required: true,
}
}
fn conformance_child_depends(
name: &str,
deps: Vec<crate::supervision::ChildName>,
) -> crate::supervision::ChildSpec {
let mut child = conformance_child(name);
child.depends_on = deps;
child
}
fn schedule_children(harness: &SporkAppHarness) {
if let Some(app) = harness.app_handle() {
let task_ids: Vec<_> = app.supervisor().started.iter().map(|c| c.task_id).collect();
let mut sched = harness.runtime().scheduler.lock();
for tid in task_ids {
sched.schedule(tid, 0);
}
}
}
#[test]
fn conformance_single_child_all_oracles_pass() {
crate::test_utils::init_test_logging();
crate::test_phase!("conformance_single_child_all_oracles_pass");
let app = AppSpec::new("single_child").child(conformance_child("worker"));
let harness = SporkAppHarness::with_seed(42, app).unwrap();
schedule_children(&harness);
let report = harness.run_to_report().unwrap();
assert!(
report.run.oracle_report.all_passed(),
"oracle report must show all passed, failures: {:?}",
report.oracle_failures()
);
assert!(
report.run.invariant_violations.is_empty(),
"must have no invariant violations, got: {:?}",
report.run.invariant_violations
);
crate::test_complete!("conformance_single_child_all_oracles_pass");
}
#[test]
fn conformance_multi_child_all_oracles_pass() {
crate::test_utils::init_test_logging();
crate::test_phase!("conformance_multi_child_all_oracles_pass");
let app = AppSpec::new("multi_child")
.child(conformance_child("alpha"))
.child(conformance_child("bravo"))
.child(conformance_child("charlie"));
let harness = SporkAppHarness::with_seed(42, app).unwrap();
schedule_children(&harness);
let report = harness.run_to_report().unwrap();
assert!(
report.passed(),
"multi-child app must pass all oracles: {:?}",
report.oracle_failures()
);
crate::test_complete!("conformance_multi_child_all_oracles_pass");
}
#[test]
fn conformance_dependency_chain_all_oracles_pass() {
crate::test_utils::init_test_logging();
crate::test_phase!("conformance_dependency_chain_all_oracles_pass");
let app = AppSpec::new("dep_chain")
.child(conformance_child("alpha"))
.child(conformance_child_depends("bravo", vec!["alpha".into()]))
.child(conformance_child_depends("charlie", vec!["bravo".into()]));
let harness = SporkAppHarness::with_seed(42, app).unwrap();
schedule_children(&harness);
let report = harness.run_to_report().unwrap();
assert!(
report.passed(),
"dependency-chain app must pass all oracles: {:?}",
report.oracle_failures()
);
crate::test_complete!("conformance_dependency_chain_all_oracles_pass");
}
#[test]
fn conformance_deterministic_multi_child() {
crate::test_utils::init_test_logging();
crate::test_phase!("conformance_deterministic_multi_child");
let run_scenario = |seed| {
let app = AppSpec::new("det_multi")
.child(conformance_child("alpha"))
.child(conformance_child("bravo"))
.child(conformance_child_depends("charlie", vec!["alpha".into()]));
let harness = SporkAppHarness::with_seed(seed, app).unwrap();
schedule_children(&harness);
harness.run_to_report().unwrap()
};
let report_a = run_scenario(99);
let report_b = run_scenario(99);
assert_eq!(
report_a.run.trace_fingerprint, report_b.run.trace_fingerprint,
"same seed + topology must produce identical trace fingerprints"
);
assert_eq!(
report_a.to_json(),
report_b.to_json(),
"JSON reports must be identical for deterministic replay"
);
crate::test_complete!("conformance_deterministic_multi_child");
}
#[test]
fn conformance_different_seeds_differ() {
crate::test_utils::init_test_logging();
crate::test_phase!("conformance_different_seeds_differ");
let run_scenario = |seed| {
let app = AppSpec::new("seed_diff")
.child(conformance_child("alpha"))
.child(conformance_child("bravo"));
let harness = SporkAppHarness::with_seed(seed, app).unwrap();
schedule_children(&harness);
harness.run_to_report().unwrap()
};
let report_a = run_scenario(1);
let report_b = run_scenario(2);
assert!(
report_a.passed(),
"seed 1 must pass: violations={:?}, failures={:?}",
report_a.run.invariant_violations,
report_a.oracle_failures()
);
assert!(
report_b.passed(),
"seed 2 must pass: violations={:?}, failures={:?}",
report_b.run.invariant_violations,
report_b.oracle_failures()
);
crate::test_complete!("conformance_different_seeds_differ");
}
#[test]
fn conformance_quiescence_on_stop() {
crate::test_utils::init_test_logging();
crate::test_phase!("conformance_quiescence_on_stop");
let app = AppSpec::new("quiescence_app")
.child(conformance_child("svc_a"))
.child(conformance_child("svc_b"))
.child(conformance_child("svc_c"));
let harness = SporkAppHarness::with_seed(42, app).unwrap();
schedule_children(&harness);
let report = harness.run_to_report().unwrap();
assert!(
report.run.quiescent,
"runtime must reach quiescence after stop"
);
assert!(
report.passed(),
"all oracles must pass after quiescent stop: {:?}",
report.oracle_failures()
);
crate::test_complete!("conformance_quiescence_on_stop");
}
#[test]
fn conformance_oracles_pass_at_snapshot() {
crate::test_utils::init_test_logging();
crate::test_phase!("conformance_oracles_pass_at_snapshot");
let app = AppSpec::new("snapshot_oracle")
.child(conformance_child("worker_a"))
.child(conformance_child("worker_b"));
let mut harness = SporkAppHarness::with_seed(42, app).unwrap();
schedule_children(&harness);
harness.run_until_idle();
assert!(
harness.oracles_pass(),
"oracles must pass at intermediate snapshot"
);
let report = harness.run_to_report().unwrap();
assert!(
report.passed(),
"final report must pass: {:?}",
report.oracle_failures()
);
crate::test_complete!("conformance_oracles_pass_at_snapshot");
}
#[test]
fn conformance_report_schema_version() {
crate::test_utils::init_test_logging();
crate::test_phase!("conformance_report_schema_version");
let app = AppSpec::new("schema_check").child(conformance_child("w"));
let harness = SporkAppHarness::with_seed(42, app).unwrap();
schedule_children(&harness);
let report = harness.run_to_report().unwrap();
assert_eq!(report.schema_version, SporkHarnessReport::SCHEMA_VERSION);
let json = report.to_json();
assert_eq!(
json["schema_version"],
serde_json::json!(SporkHarnessReport::SCHEMA_VERSION)
);
crate::test_complete!("conformance_report_schema_version");
}
#[test]
fn conformance_oracle_entry_count() {
crate::test_utils::init_test_logging();
crate::test_phase!("conformance_oracle_entry_count");
let app = AppSpec::new("oracle_count_check").child(conformance_child("w"));
let harness = SporkAppHarness::with_seed(42, app).unwrap();
schedule_children(&harness);
let report = harness.run_to_report().unwrap();
assert!(
report.run.oracle_report.total > 0,
"oracle report must contain at least one oracle"
);
assert_eq!(
report.run.oracle_report.total,
report.run.oracle_report.entries.len(),
"total must match entries.len()"
);
assert_eq!(
report.run.oracle_report.passed + report.run.oracle_report.failed,
report.run.oracle_report.total,
"passed + failed must equal total"
);
crate::test_complete!("conformance_oracle_entry_count");
}
#[test]
fn conformance_scenario_lifecycle_with_invariants() {
crate::test_utils::init_test_logging();
crate::test_phase!("conformance_scenario_lifecycle_with_invariants");
let mut runner = SporkScenarioRunner::new();
runner
.register(
SporkScenarioSpec::new("conformance.lifecycle", |_config| {
AppSpec::new("scenario_lifecycle")
})
.with_description("Empty app lifecycle conformance")
.with_expected_invariants([
"no_task_leaks",
"no_obligation_leaks",
"quiescence_on_close",
]),
)
.unwrap();
let result = runner.run("conformance.lifecycle").unwrap();
assert!(
result.passed(),
"conformance scenario must pass: violations={:?}, failures={:?}",
result.report.run.invariant_violations,
result.report.oracle_failures()
);
assert_eq!(result.scenario_id, "conformance.lifecycle");
assert!(
result
.expected_invariants
.contains(&"no_task_leaks".to_string())
);
crate::test_complete!("conformance_scenario_lifecycle_with_invariants");
}
#[test]
fn conformance_scenario_run_all_deterministic() {
crate::test_utils::init_test_logging();
crate::test_phase!("conformance_scenario_run_all_deterministic");
let mut runner = SporkScenarioRunner::new();
runner
.register(SporkScenarioSpec::new("conformance.alpha", |_| {
AppSpec::new("alpha")
}))
.unwrap();
runner
.register(SporkScenarioSpec::new("conformance.bravo", |_| {
AppSpec::new("bravo")
}))
.unwrap();
runner
.register(SporkScenarioSpec::new("conformance.charlie", |_| {
AppSpec::new("charlie")
}))
.unwrap();
let results = runner.run_all().unwrap();
assert_eq!(results.len(), 3);
assert_eq!(results[0].scenario_id, "conformance.alpha");
assert_eq!(results[1].scenario_id, "conformance.bravo");
assert_eq!(results[2].scenario_id, "conformance.charlie");
for result in &results {
assert!(
result.passed(),
"scenario {} must pass: {:?}",
result.scenario_id,
result.report.oracle_failures()
);
}
crate::test_complete!("conformance_scenario_run_all_deterministic");
}
#[test]
fn conformance_budgeted_app_oracles_pass() {
crate::test_utils::init_test_logging();
crate::test_phase!("conformance_budgeted_app_oracles_pass");
let app = AppSpec::new("budgeted_conformance")
.with_budget(Budget::new().with_poll_quota(50_000))
.child(conformance_child("svc"));
let harness = SporkAppHarness::with_seed(42, app).unwrap();
schedule_children(&harness);
let report = harness.run_to_report().unwrap();
assert!(
report.passed(),
"budgeted app must pass: violations={:?}, failures={:?}",
report.run.invariant_violations,
report.oracle_failures()
);
crate::test_complete!("conformance_budgeted_app_oracles_pass");
}
#[test]
fn conformance_no_invariant_violations() {
crate::test_utils::init_test_logging();
crate::test_phase!("conformance_no_invariant_violations");
let app = AppSpec::new("clean_app")
.child(conformance_child("a"))
.child(conformance_child("b"));
let harness = SporkAppHarness::with_seed(42, app).unwrap();
schedule_children(&harness);
let report = harness.run_to_report().unwrap();
assert!(
report.run.invariant_violations.is_empty(),
"clean app must have no invariant violations, got: {:?}",
report.run.invariant_violations
);
crate::test_complete!("conformance_no_invariant_violations");
}
}