mod support;
use acts::Engine;
use acts_package_shell::ShellPackage;
use std::time::Duration;
use support::{
config, endless_flood, endless_stderr_flood, flood, late_writer, quick, run_shell, run_shells,
run_vars, scratch, sleeper,
};
async fn engine(toml_text: &str) -> Engine {
Engine::builder()
.set_config(&config(toml_text))
.add_package::<ShellPackage>()
.start()
.await
.unwrap()
}
async fn engine_with_lanes(toml_text: &str, lanes: usize) -> Engine {
Engine::builder()
.set_config(&config(toml_text))
.scheduler_workers(lanes)
.add_package::<ShellPackage>()
.start()
.await
.unwrap()
}
#[tokio::test]
async fn a_script_that_never_exits_fails_at_the_deadline() {
let dir = scratch("deadline");
let target = dir.join("late.txt");
let (shell, script) = late_writer(&target, 2);
let engine = engine("[shell]\ntimeout-ms = 300\n").await;
let outcome = run_shell(&engine, "shell-deadline", shell, &script).await;
assert!(outcome.failed, "the run must fail");
let elapsed = outcome.elapsed;
assert!(
elapsed < Duration::from_secs(10),
"the act must not wait for the script: took {elapsed:?}"
);
assert!(
run_vars(&engine, &outcome.pid)
.await
.contains("timed out after 300 ms"),
"the failure must name the deadline"
);
tokio::time::sleep(Duration::from_millis(2_500)).await;
assert!(
!target.exists(),
"the killed script wrote {} after the act failed",
target.display()
);
std::fs::remove_dir_all(&dir).ok();
engine.close().await;
}
#[tokio::test]
async fn a_flooding_stream_is_stopped_at_the_capture_limit() {
let (shell, script) = flood(true);
let engine = engine("[shell]\nmax-output-bytes = 4096\n").await;
let outcome = run_shell(&engine, "shell-flood", shell, &script).await;
assert!(outcome.failed, "the run must fail");
assert!(
run_vars(&engine, &outcome.pid)
.await
.contains("max-output-bytes limit (4096)"),
"the failure must name the capture limit"
);
engine.close().await;
}
#[tokio::test]
async fn the_default_capture_limit_bounds_a_stream() {
let (shell, script) = flood(false);
let engine = engine("").await;
let outcome = run_shell(&engine, "shell-default-flood", shell, &script).await;
assert!(outcome.failed, "the run must fail");
assert!(
run_vars(&engine, &outcome.pid)
.await
.contains("max-output-bytes limit (1048576)"),
"the default limit must be the one the act reports"
);
engine.close().await;
}
#[tokio::test]
async fn the_default_deadline_does_not_refuse_a_quick_script() {
let (shell, script) = sleeper(0);
let engine = engine("").await;
let outcome = run_shell(&engine, "shell-default-quick", shell, &script).await;
assert!(
!outcome.failed,
"a script inside the deadline must complete"
);
engine.close().await;
}
#[tokio::test]
async fn a_flood_that_never_stops_releases_the_lane() {
let engine = engine_with_lanes("[shell]\nmax-output-bytes = 4096\n", 1).await;
let (shell, script) = endless_flood();
let outcome = tokio::time::timeout(
Duration::from_secs(30),
run_shell(&engine, "shell-endless-flood", shell, &script),
)
.await
.expect("the act must fail at the capture limit, not wait for a script that never stops");
assert!(outcome.failed, "the run must fail");
assert!(
run_vars(&engine, &outcome.pid)
.await
.contains("max-output-bytes limit (4096)"),
"the failure must name the capture limit"
);
let (shell, script) = quick();
let after = tokio::time::timeout(
Duration::from_secs(30),
run_shell(&engine, "shell-after-flood", shell, &script),
)
.await
.expect("the lane the flood held must be released");
assert!(!after.failed, "an act after the flood must complete");
assert!(
run_vars(&engine, &after.pid).await.contains("ok"),
"the act after the flood must have run and reported its output"
);
engine.close().await;
}
#[tokio::test]
async fn an_endless_flood_on_stderr_fails_at_the_limit() {
let engine = engine_with_lanes("[shell]\nmax-output-bytes = 4096\n", 1).await;
let (shell, script) = endless_stderr_flood();
let outcome = tokio::time::timeout(
Duration::from_secs(30),
run_shell(&engine, "shell-endless-stderr", shell, &script),
)
.await
.expect("a flood on stderr must fail the act, not block on the full pipe");
assert!(outcome.failed, "the run must fail");
assert!(
run_vars(&engine, &outcome.pid)
.await
.contains("max-output-bytes limit (4096)"),
"the failure must name the capture limit"
);
engine.close().await;
}
#[tokio::test]
async fn floods_on_every_lane_leave_the_worker_running() {
const LANES: usize = 4;
let engine = engine_with_lanes("[shell]\nmax-output-bytes = 4096\n", LANES).await;
let (shell, script) = endless_flood();
let outcomes = tokio::time::timeout(
Duration::from_secs(60),
run_shells(
&engine,
&[
"shell-flood-lane-0",
"shell-flood-lane-1",
"shell-flood-lane-2",
"shell-flood-lane-3",
],
shell,
&script,
),
)
.await
.expect("floods on every lane must fail at the limit, not wait for their scripts");
assert_eq!(outcomes.len(), LANES, "every flood must report an outcome");
for outcome in &outcomes {
assert!(outcome.failed, "every flood must fail");
assert!(
run_vars(&engine, &outcome.pid)
.await
.contains("max-output-bytes limit (4096)"),
"each failure must name the capture limit"
);
}
let (shell, script) = quick();
let after = tokio::time::timeout(
Duration::from_secs(30),
run_shell(&engine, "shell-after-lane-floods", shell, &script),
)
.await
.expect("the lanes the floods held must be released");
assert!(!after.failed, "an act after the floods must complete");
assert!(
run_vars(&engine, &after.pid).await.contains("ok"),
"the act after the floods must have run and reported its output"
);
engine.close().await;
}