mod common;
use std::collections::BTreeSet;
use common::{
Fixture, Recorder, TaskBuilder, completed_for, host_path, make_graph, make_project,
make_workspace_at, nested_project_root, push_n_default_specs, tid,
workspace_settings_with_fixed_cap,
};
use haz_domain::task::Task;
use haz_domain::task_id::TaskId;
use haz_exec::mock_impl::MockProcessSpawner;
use haz_exec::run_graph::{RuntimeInvariantViolation, run_graph};
use haz_exec::run_task::RunState;
use haz_vfs::WritableFilesystem;
const WORKSPACE_HOST: &str = "/ws";
const A_OUT: &str = "/lib/a_out";
const B_OUT: &str = "/app/b_out";
#[tokio::test]
async fn cross_project_runtime_cycle_yields_one_violation_spanning_both_projects() {
let produce: Task = TaskBuilder::new("produce")
.command(&["echo", "lib:produce"])
.input(B_OUT)
.output(A_OUT)
.build();
let consume: Task = TaskBuilder::new("consume")
.command(&["echo", "app:consume"])
.input(A_OUT)
.output(B_OUT)
.build();
let lib_project = make_project("lib", nested_project_root("/lib"), vec![produce]);
let app_project = make_project("app", nested_project_root("/app"), vec![consume]);
let workspace_host = std::path::PathBuf::from(WORKSPACE_HOST);
let workspace = make_workspace_at(
&workspace_host,
vec![lib_project, app_project],
workspace_settings_with_fixed_cap(2),
);
let graph = make_graph(
vec![tid("lib", "produce"), tid("app", "consume")],
Vec::new(),
);
let fixture = Fixture::new_mem(&workspace_host, workspace, graph);
fixture
.cache
.fs()
.write_file(&host_path(&workspace_host, A_OUT), b"a")
.unwrap();
fixture
.cache
.fs()
.write_file(&host_path(&workspace_host, B_OUT), b"b")
.unwrap();
let spawner = MockProcessSpawner::new();
push_n_default_specs(&spawner, 2);
let observer = Recorder::default();
let ctx = common::make_ctx(&fixture, &spawner, &observer);
let result = run_graph(&ctx, 1_700_000_000).await.unwrap();
for task in [tid("lib", "produce"), tid("app", "consume")] {
let rec = completed_for(&result.outcomes, &task);
assert_eq!(rec.state, RunState::Succeeded);
}
assert_eq!(spawner.spawns().len(), 2);
assert_eq!(result.invariant_violations.len(), 1);
let expected: BTreeSet<TaskId> = BTreeSet::from([tid("lib", "produce"), tid("app", "consume")]);
match &result.invariant_violations[0] {
RuntimeInvariantViolation::RuntimeCycle {
nodes,
offending_edge,
} => {
assert_eq!(nodes, &expected);
assert!(expected.contains(&offending_edge.0));
assert!(expected.contains(&offending_edge.1));
assert_ne!(offending_edge.0, offending_edge.1);
assert_ne!(
offending_edge.0.project, offending_edge.1.project,
"the cycle's offending edge must cross project boundaries",
);
}
other @ RuntimeInvariantViolation::OutputOverlap { .. } => {
panic!("expected RuntimeCycle, got {other:?}")
}
}
}