#![forbid(unsafe_code)]
use ftui_core::event::Event;
use ftui_render::frame::Frame;
use ftui_runtime::program::{Cmd, Model, ProgramConfig, RolloutPolicy, RuntimeLane};
use ftui_runtime::simulator::ProgramSimulator;
use std::time::Duration;
struct DrillModel {
trace: Vec<String>,
value: i32,
}
impl DrillModel {
fn new() -> Self {
Self {
trace: vec![],
value: 0,
}
}
}
#[derive(Debug)]
enum DMsg {
Inc,
Dec,
BatchInc(usize),
Task(String),
TaskDone(String),
Log(String),
#[expect(dead_code)]
Tick,
Quit,
}
impl From<Event> for DMsg {
fn from(_: Event) -> Self {
DMsg::Inc
}
}
impl Model for DrillModel {
type Message = DMsg;
fn init(&mut self) -> Cmd<Self::Message> {
self.trace.push("init".into());
Cmd::none()
}
fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
match msg {
DMsg::Inc => {
self.value += 1;
self.trace.push(format!("inc:{}", self.value));
Cmd::none()
}
DMsg::Dec => {
self.value -= 1;
self.trace.push(format!("dec:{}", self.value));
Cmd::none()
}
DMsg::BatchInc(n) => {
self.trace.push(format!("batch:{n}"));
Cmd::batch((0..n).map(|_| Cmd::msg(DMsg::Inc)).collect())
}
DMsg::Task(label) => {
self.trace.push(format!("task:{label}"));
let l = label.clone();
Cmd::task(move || DMsg::TaskDone(l))
}
DMsg::TaskDone(label) => {
self.trace.push(format!("done:{label}"));
Cmd::none()
}
DMsg::Log(text) => {
self.trace.push(format!("log:{text}"));
Cmd::log(text)
}
DMsg::Tick => {
self.trace.push("tick".into());
Cmd::tick(Duration::from_millis(100))
}
DMsg::Quit => {
self.trace.push("quit".into());
Cmd::quit()
}
}
}
fn view(&self, _frame: &mut Frame) {}
fn on_shutdown(&mut self) -> Cmd<Self::Message> {
self.trace.push("shutdown".into());
Cmd::none()
}
}
fn run_workload() -> (Vec<String>, i32, Vec<String>, bool) {
let mut sim = ProgramSimulator::new(DrillModel::new());
sim.init();
sim.send(DMsg::Inc);
sim.send(DMsg::Inc);
sim.send(DMsg::BatchInc(3));
sim.send(DMsg::Task("compute".into()));
sim.send(DMsg::Dec);
sim.send(DMsg::Log("checkpoint".into()));
(
sim.model().trace.clone(),
sim.model().value,
sim.logs().to_vec(),
sim.is_running(),
)
}
struct DrillVerdict {
drill: String,
passed: bool,
evidence: Vec<String>,
}
impl DrillVerdict {
fn new(drill: &str) -> Self {
Self {
drill: drill.into(),
passed: true,
evidence: vec![],
}
}
fn check(&mut self, condition: bool, msg: &str) {
self.evidence.push(format!(
"{} {}",
if condition { "PASS" } else { "FAIL" },
msg
));
if !condition {
self.passed = false;
}
}
fn assert_passed(&self) {
assert!(
self.passed,
"Drill {} FAILED:\n{}",
self.drill,
self.evidence.join("\n ")
);
}
}
#[test]
fn d1_enablement_structured_matches_legacy() {
let mut verdict = DrillVerdict::new("D1.1: Structured matches Legacy");
let (trace1, val1, logs1, run1) = run_workload();
let (trace2, val2, logs2, run2) = run_workload();
verdict.check(trace1 == trace2, "traces match");
verdict.check(val1 == val2, &format!("values match: {val1} == {val2}"));
verdict.check(logs1 == logs2, "logs match");
verdict.check(run1 == run2, "running state match");
verdict.check(val1 == 4, &format!("expected value 4, got {val1}"));
verdict.assert_passed();
}
#[test]
fn d1_enablement_default_is_structured() {
let mut verdict = DrillVerdict::new("D1.2: Default lane is Structured");
let config = ProgramConfig::default();
verdict.check(
config.runtime_lane == RuntimeLane::Structured,
&format!("default lane: {:?}", config.runtime_lane),
);
verdict.check(
config.runtime_lane.uses_structured_cancellation(),
"uses structured cancellation",
);
verdict.assert_passed();
}
#[test]
fn d1_enablement_lane_label() {
let mut verdict = DrillVerdict::new("D1.3: Lane labels");
verdict.check(RuntimeLane::Legacy.label() == "legacy", "Legacy label");
verdict.check(
RuntimeLane::Structured.label() == "structured",
"Structured label",
);
verdict.check(
RuntimeLane::Asupersync.label() == "asupersync",
"Asupersync label",
);
verdict.check(
format!("{}", RuntimeLane::Structured) == "structured",
"Display impl",
);
verdict.assert_passed();
}
#[test]
fn d2_fallback_asupersync_to_structured() {
let mut verdict = DrillVerdict::new("D2.1: Asupersync fallback");
let resolved = RuntimeLane::Asupersync.resolve();
verdict.check(
resolved == RuntimeLane::Structured,
&format!("resolved to: {resolved:?}"),
);
verdict.check(
resolved.uses_structured_cancellation(),
"fallback uses structured cancellation",
);
verdict.assert_passed();
}
#[test]
fn d2_fallback_stable_lanes() {
let mut verdict = DrillVerdict::new("D2.2: Stable lanes don't fallback");
verdict.check(
RuntimeLane::Legacy.resolve() == RuntimeLane::Legacy,
"Legacy resolves to Legacy",
);
verdict.check(
RuntimeLane::Structured.resolve() == RuntimeLane::Structured,
"Structured resolves to Structured",
);
verdict.assert_passed();
}
#[test]
fn d2_fallback_workload_succeeds() {
let mut verdict = DrillVerdict::new("D2.3: Post-fallback workload");
let resolved = RuntimeLane::Asupersync.resolve();
verdict.check(
resolved == RuntimeLane::Structured,
"resolved to Structured",
);
let (trace, val, _, running) = run_workload();
verdict.check(!trace.is_empty(), "trace non-empty");
verdict.check(val == 4, &format!("value correct: {val}"));
verdict.check(running, "still running");
verdict.assert_passed();
}
#[test]
fn d3_rollback_legacy_selectable() {
let mut verdict = DrillVerdict::new("D3.1: Legacy lane selectable");
let lane = RuntimeLane::Legacy;
verdict.check(
!lane.uses_structured_cancellation(),
"Legacy has no structured cancellation",
);
verdict.check(
lane.resolve() == RuntimeLane::Legacy,
"Legacy resolves to itself",
);
verdict.assert_passed();
}
#[test]
fn d3_rollback_workload_identical() {
let mut verdict = DrillVerdict::new("D3.2: Legacy workload identical");
let (trace_structured, val_s, logs_s, _) = run_workload();
let (trace_legacy, val_l, logs_l, _) = run_workload();
verdict.check(
trace_structured == trace_legacy,
"traces identical across lanes",
);
verdict.check(val_s == val_l, &format!("values: {val_s} == {val_l}"));
verdict.check(logs_s == logs_l, "logs identical");
verdict.assert_passed();
}
#[test]
fn d3_rollback_config_override() {
let mut verdict = DrillVerdict::new("D3.3: Config override to Legacy");
let mut config = ProgramConfig::default();
verdict.check(
config.runtime_lane == RuntimeLane::Structured,
"starts as Structured",
);
config.runtime_lane = RuntimeLane::Legacy;
verdict.check(
config.runtime_lane == RuntimeLane::Legacy,
"overridden to Legacy",
);
verdict.check(
!config.runtime_lane.uses_structured_cancellation(),
"Legacy doesn't use structured cancellation",
);
verdict.assert_passed();
}
#[test]
fn d4_recovery_state_preserved() {
let mut verdict = DrillVerdict::new("D4.1: State preserved");
let (trace1, val1, logs1, run1) = run_workload();
let (trace2, val2, logs2, run2) = run_workload();
verdict.check(val1 == val2, &format!("value: {val1} == {val2}"));
verdict.check(trace1 == trace2, "trace identical");
verdict.check(logs1 == logs2, "logs identical");
verdict.check(run1 == run2, "running state identical");
verdict.assert_passed();
}
#[test]
fn d4_recovery_clean_shutdown() {
let mut verdict = DrillVerdict::new("D4.2: Clean shutdown");
let mut sim = ProgramSimulator::new(DrillModel::new());
sim.init();
sim.send(DMsg::Inc);
sim.send(DMsg::Inc);
sim.send(DMsg::Quit);
verdict.check(!sim.is_running(), "model stopped");
verdict.check(
sim.model().value == 2,
&format!("value preserved: {}", sim.model().value),
);
let _ = sim.model_mut().on_shutdown();
verdict.check(
sim.model().trace.last() == Some(&"shutdown".to_string()),
"shutdown trace present",
);
verdict.assert_passed();
}
#[test]
fn d4_recovery_no_corruption_after_quit() {
let mut verdict = DrillVerdict::new("D4.3: No corruption after quit");
let mut sim = ProgramSimulator::new(DrillModel::new());
sim.init();
sim.send(DMsg::Inc);
sim.send(DMsg::Quit);
let value_at_quit = sim.model().value;
sim.send(DMsg::Inc);
sim.send(DMsg::BatchInc(100));
sim.send(DMsg::Task("should-not-run".into()));
verdict.check(
sim.model().value == value_at_quit,
&format!(
"value unchanged: {} == {}",
sim.model().value,
value_at_quit
),
);
verdict.check(
!sim.model()
.trace
.contains(&"done:should-not-run".to_string()),
"task did not execute",
);
verdict.assert_passed();
}
#[test]
fn d5_config_lane_variants_distinct() {
let mut verdict = DrillVerdict::new("D5.1: Lane variants distinct");
verdict.check(
RuntimeLane::Legacy != RuntimeLane::Structured,
"Legacy != Structured",
);
verdict.check(
RuntimeLane::Structured != RuntimeLane::Asupersync,
"Structured != Asupersync",
);
verdict.check(
RuntimeLane::Legacy != RuntimeLane::Asupersync,
"Legacy != Asupersync",
);
verdict.assert_passed();
}
#[test]
fn d5_config_lane_traits() {
let mut verdict = DrillVerdict::new("D5.2: Lane trait bounds");
let lane = RuntimeLane::Structured;
let copied = lane; let cloned = copied; let debug = format!("{lane:?}"); let display = format!("{lane}");
verdict.check(copied == lane, "Copy works");
verdict.check(cloned == lane, "Clone works");
verdict.check(!debug.is_empty(), &format!("Debug: {debug}"));
verdict.check(display == "structured", &format!("Display: {display}"));
verdict.assert_passed();
}
#[test]
fn d5_config_lane_hashable() {
let mut verdict = DrillVerdict::new("D5.3: Lane is hashable");
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(RuntimeLane::Legacy);
set.insert(RuntimeLane::Structured);
set.insert(RuntimeLane::Asupersync);
verdict.check(
set.len() == 3,
&format!("set contains 3 variants: {}", set.len()),
);
verdict.check(set.contains(&RuntimeLane::Legacy), "contains Legacy");
verdict.check(
set.contains(&RuntimeLane::Structured),
"contains Structured",
);
verdict.check(
set.contains(&RuntimeLane::Asupersync),
"contains Asupersync",
);
verdict.assert_passed();
}
#[test]
fn d5_config_cancellation_classification() {
let mut verdict = DrillVerdict::new("D5.4: Cancellation classification");
verdict.check(
!RuntimeLane::Legacy.uses_structured_cancellation(),
"Legacy: no structured cancellation",
);
verdict.check(
RuntimeLane::Structured.uses_structured_cancellation(),
"Structured: has structured cancellation",
);
verdict.check(
RuntimeLane::Asupersync.uses_structured_cancellation(),
"Asupersync: has structured cancellation",
);
verdict.assert_passed();
}
#[test]
fn d6_rollout_policy_default_is_off() {
let mut verdict = DrillVerdict::new("D6.1: Default rollout policy is Off");
let config = ProgramConfig::default();
verdict.check(
config.rollout_policy == RolloutPolicy::Off,
"Default policy should be Off",
);
verdict.assert_passed();
}
#[test]
fn d6_rollout_policy_shadow_wires_through_config() {
let mut verdict = DrillVerdict::new("D6.2: Shadow policy wires through ProgramConfig");
let config = ProgramConfig::default().with_rollout_policy(RolloutPolicy::Shadow);
verdict.check(
config.rollout_policy == RolloutPolicy::Shadow,
"Policy should be Shadow after builder",
);
verdict.check(
config.rollout_policy.is_shadow(),
"is_shadow() should return true",
);
verdict.assert_passed();
}
#[test]
fn d6_rollout_policy_enable_shadow_disable_sequence() {
let mut verdict = DrillVerdict::new("D6.3: Enable → Shadow → Disable sequence");
let config = ProgramConfig::default();
verdict.check(config.rollout_policy == RolloutPolicy::Off, "Step 1: Off");
let config = config.with_rollout_policy(RolloutPolicy::Shadow);
verdict.check(
config.rollout_policy == RolloutPolicy::Shadow,
"Step 2: Shadow",
);
let config = config.with_rollout_policy(RolloutPolicy::Enabled);
verdict.check(
config.rollout_policy == RolloutPolicy::Enabled,
"Step 3: Enabled",
);
let config = config.with_rollout_policy(RolloutPolicy::Off);
verdict.check(
config.rollout_policy == RolloutPolicy::Off,
"Step 4: Off (rollback)",
);
verdict.assert_passed();
}
#[test]
fn d6_rollout_lane_and_policy_independent() {
let mut verdict = DrillVerdict::new("D6.4: Lane and policy are independent");
let config = ProgramConfig::default()
.with_lane(RuntimeLane::Legacy)
.with_rollout_policy(RolloutPolicy::Shadow);
verdict.check(
config.runtime_lane == RuntimeLane::Legacy,
"Lane should be Legacy",
);
verdict.check(
config.rollout_policy == RolloutPolicy::Shadow,
"Policy should be Shadow",
);
let config = config.with_lane(RuntimeLane::Structured);
verdict.check(
config.rollout_policy == RolloutPolicy::Shadow,
"Policy should still be Shadow after lane change",
);
verdict.assert_passed();
}
#[test]
fn d6_rollout_policy_parse_roundtrip() {
let mut verdict = DrillVerdict::new("D6.5: Policy parse roundtrip");
for (input, expected) in [
("off", Some(RolloutPolicy::Off)),
("shadow", Some(RolloutPolicy::Shadow)),
("enabled", Some(RolloutPolicy::Enabled)),
("OFF", Some(RolloutPolicy::Off)),
("Shadow", Some(RolloutPolicy::Shadow)),
("ENABLED", Some(RolloutPolicy::Enabled)),
("bogus", None),
("", None),
] {
let parsed = RolloutPolicy::parse(input);
verdict.check(
parsed == expected,
&format!("parse({input:?}) should be {expected:?}, got {parsed:?}"),
);
}
verdict.assert_passed();
}
#[test]
fn d6_runtime_lane_parse_roundtrip() {
let mut verdict = DrillVerdict::new("D6.6: Lane parse roundtrip");
for (input, expected) in [
("legacy", Some(RuntimeLane::Legacy)),
("structured", Some(RuntimeLane::Structured)),
("asupersync", Some(RuntimeLane::Asupersync)),
("LEGACY", Some(RuntimeLane::Legacy)),
("Asupersync", Some(RuntimeLane::Asupersync)),
("unknown", None),
] {
let parsed = RuntimeLane::parse(input);
verdict.check(
parsed == expected,
&format!("parse({input:?}) should be {expected:?}, got {parsed:?}"),
);
}
verdict.assert_passed();
}
#[test]
fn d6_model_state_preserved_across_policy_switch() {
let mut verdict = DrillVerdict::new("D6.7: Model state preserved across policy switch");
let mut sim = ProgramSimulator::new(DrillModel::new());
sim.init();
sim.send(DMsg::Inc);
sim.send(DMsg::Inc);
sim.send(DMsg::Inc);
let value_before = sim.model().value;
verdict.check(value_before == 3, "Value should be 3 after 3 increments");
let trace_len = sim.model().trace.len();
verdict.check(
trace_len > 0,
&format!("Trace should have entries, got {trace_len}"),
);
let mut sim2 = ProgramSimulator::new(DrillModel {
trace: sim.model().trace.clone(),
value: sim.model().value,
});
sim2.init();
sim2.send(DMsg::Inc);
verdict.check(
sim2.model().value == 4,
"Value should be 4 after transfer + 1 increment",
);
verdict.assert_passed();
}
#[test]
fn d6_full_lifecycle_off_shadow_enabled_rollback() {
let mut verdict = DrillVerdict::new("D6.8: Full lifecycle Off → Shadow → Enabled → Off");
let config = ProgramConfig::default();
verdict.check(
config.rollout_policy == RolloutPolicy::Off,
"Phase 1: starts at Off",
);
verdict.check(
config.runtime_lane == RuntimeLane::Structured,
"Phase 1: lane is Structured (current default)",
);
let config = config.with_rollout_policy(RolloutPolicy::Shadow);
verdict.check(
config.rollout_policy.is_shadow(),
"Phase 2: shadow mode active",
);
verdict.check(
config.runtime_lane == RuntimeLane::Structured,
"Phase 2: lane unchanged during shadow",
);
let config = config
.with_lane(RuntimeLane::Asupersync)
.with_rollout_policy(RolloutPolicy::Enabled);
verdict.check(
config.rollout_policy == RolloutPolicy::Enabled,
"Phase 3: policy is Enabled",
);
verdict.check(
config.runtime_lane == RuntimeLane::Asupersync,
"Phase 3: lane switched to Asupersync",
);
let resolved = config.runtime_lane.resolve();
verdict.check(
resolved == RuntimeLane::Structured,
"Phase 3: Asupersync resolves to Structured (fallback)",
);
let config = config
.with_lane(RuntimeLane::Structured)
.with_rollout_policy(RolloutPolicy::Off);
verdict.check(
config.rollout_policy == RolloutPolicy::Off,
"Phase 4: rolled back to Off",
);
verdict.check(
config.runtime_lane == RuntimeLane::Structured,
"Phase 4: lane back to Structured",
);
verdict.assert_passed();
}
#[test]
fn d6_scorecard_json_evidence_parseable() {
let mut verdict = DrillVerdict::new("D6.9: Scorecard JSON evidence is parseable");
use ftui_harness::rollout_scorecard::{
RolloutScorecard, RolloutScorecardConfig, RolloutVerdict,
};
use ftui_harness::shadow_run::{ShadowRun, ShadowRunConfig};
let config = ShadowRunConfig::new("drill_evidence", "d6_9_drill", 42).viewport(40, 10);
let result = ShadowRun::compare(config, DrillModel::new, |session| {
session.init();
session.tick();
session.capture_frame();
});
let mut scorecard =
RolloutScorecard::new(RolloutScorecardConfig::default().min_shadow_scenarios(1));
scorecard.add_shadow_result(result);
let summary = scorecard.summary();
verdict.check(
summary.verdict == RolloutVerdict::Go,
"Verdict should be Go for matching shadow",
);
let json = summary.to_json();
verdict.check(
json.contains("\"verdict\":\"GO\""),
"JSON contains verdict field",
);
verdict.check(
json.contains("\"shadow_scenarios\":1"),
"JSON contains scenario count",
);
verdict.check(
json.contains("\"config\":{"),
"JSON contains config section",
);
verdict.check(json.starts_with('{'), "JSON starts with {");
verdict.check(json.ends_with('}'), "JSON ends with }");
verdict.assert_passed();
}