tests: fast host suites
The timing rules are in two tests (upgrade_and_discovery_deadlines, stop_deadlines). All other tests use a grace period of 0 or core.stop, poll with deadlines instead of sleeping, and use the persistent cache in CARGO_TARGET_TMPDIR. The 128-cycle singleton test is an ignored stress test that nix flake check runs; the normal run has a 4-cycle sample. Warm, the activation suite takes 12 s (was 47 s), and cargo test --workspace takes 65 s.
This commit is contained in:
+53
-26
@@ -12,12 +12,10 @@ fn fixture(version: usize) -> PathBuf {
|
||||
static FILES: OnceLock<Vec<PathBuf>> = OnceLock::new();
|
||||
FILES.get_or_init(|| {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
|
||||
let target = root.join(format!(
|
||||
"target/compat/activation-guests/{}",
|
||||
std::process::id()
|
||||
));
|
||||
let target = PathBuf::from(concat!(env!("CARGO_TARGET_TMPDIR"), "/activation-guests"));
|
||||
(1..=3)
|
||||
.map(|version| {
|
||||
let target = target.join(version.to_string());
|
||||
let status = std::process::Command::new("cargo")
|
||||
.env("FIXTURE_VERSION", version.to_string())
|
||||
.args([
|
||||
@@ -67,14 +65,11 @@ impl TestWorld {
|
||||
std::fs::write(self.0.join(format!("state/{name}/1.json")), old.to_string()).unwrap();
|
||||
std::fs::write(self.0.join(format!("state/{name}/2.json")), new.to_string()).unwrap();
|
||||
}
|
||||
/// Starts a host with an independent compile cache shared by these tests.
|
||||
/// Starts a host with a persistent compile cache shared by the test suites.
|
||||
async fn start(&self) -> Host {
|
||||
Host::start_with_cache(
|
||||
&self.0,
|
||||
&Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("target/compat/runtime-cache"),
|
||||
Path::new(concat!(env!("CARGO_TARGET_TMPDIR"), "/cache")),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -85,9 +80,9 @@ impl Drop for TestWorld {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
/// Builds a method-only document with a short upgrade grace period.
|
||||
/// Builds a method-only document without an upgrade delay.
|
||||
fn document(name: &str) -> Value {
|
||||
json!({"methods":[{"name":name,"params":[],"result":{"schema":{"type":"string"}}}],"x-sausage-grace":0.05})
|
||||
json!({"methods":[{"name":name,"params":[],"result":{"schema":{"type":"string"}}}],"x-sausage-grace":0})
|
||||
}
|
||||
/// Adds an import of a provider's method list.
|
||||
fn consumer(provider: &str, expected: Value) -> Value {
|
||||
@@ -187,8 +182,7 @@ async fn review_discovery_answer_stops() {
|
||||
marker(world.0.join("state/notes/discovery-ended-2"), "ended").await;
|
||||
host.stop().await;
|
||||
}
|
||||
/// M4: a rejected candidate's discovery process never becomes a retired installed version.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
/// Checks that a rejected discovery never receives a retirement hook.
|
||||
async fn review_candidate_is_not_old_version() {
|
||||
use sha2::{Digest, Sha256};
|
||||
let world = TestWorld::new();
|
||||
@@ -246,9 +240,7 @@ async fn review_shutdown_suppresses_hooks() {
|
||||
marker(world.0.join("state/notes/service"), "1").await;
|
||||
activate(&host, "notes").await.unwrap();
|
||||
host.stop().await;
|
||||
for _ in 0..100 {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
assert!(host.processes().is_empty());
|
||||
assert!(!world.0.join("state/notes/retired").exists());
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(world.0.join("state/notes/service")).unwrap(),
|
||||
@@ -270,8 +262,7 @@ async fn review_shutdown_suppresses_hooks() {
|
||||
1
|
||||
);
|
||||
}
|
||||
/// L15: retirement waits for new admissions of a reverted version, not just its first snapshot.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
/// Checks retirement against live admissions after a revert.
|
||||
async fn review_retirement_checks_live_version() {
|
||||
let world = TestWorld::new();
|
||||
let mut old = document("hold");
|
||||
@@ -340,7 +331,11 @@ async fn review_watcher_is_shallow() {
|
||||
let source = world.0.join("plugins/notes/source");
|
||||
std::fs::create_dir_all(&source).unwrap();
|
||||
std::fs::set_permissions(&source, std::fs::Permissions::from_mode(0o0)).unwrap();
|
||||
let result = Host::start_with_cache(&world.0, &world.0.join("compile-cache")).await;
|
||||
let result = Host::start_with_cache(
|
||||
&world.0,
|
||||
Path::new(concat!(env!("CARGO_TARGET_TMPDIR"), "/cache")),
|
||||
)
|
||||
.await;
|
||||
std::fs::set_permissions(&source, std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||
let host = result.unwrap();
|
||||
activate(&host, "notes").await.unwrap();
|
||||
@@ -355,7 +350,11 @@ async fn review_watcher_failure_is_nonfatal() {
|
||||
let inaccessible = world.0.join("plugins/inaccessible");
|
||||
std::fs::create_dir_all(&inaccessible).unwrap();
|
||||
std::fs::set_permissions(&inaccessible, std::fs::Permissions::from_mode(0o0)).unwrap();
|
||||
let result = Host::start_with_cache(&world.0, &world.0.join("compile-cache")).await;
|
||||
let result = Host::start_with_cache(
|
||||
&world.0,
|
||||
Path::new(concat!(env!("CARGO_TARGET_TMPDIR"), "/cache")),
|
||||
)
|
||||
.await;
|
||||
std::fs::set_permissions(&inaccessible, std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||
let host = result.unwrap();
|
||||
assert!(
|
||||
@@ -368,9 +367,21 @@ async fn review_watcher_failure_is_nonfatal() {
|
||||
activate(&host, "notes").await.unwrap();
|
||||
host.stop().await;
|
||||
}
|
||||
/// L14: rapid service upgrades never overlap singleton instances.
|
||||
/// PROTOCOL 8 (L14): rapid service upgrades never overlap singleton instances.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn review_singleton_restart() {
|
||||
singleton_restart_cycles(4).await;
|
||||
}
|
||||
|
||||
/// PROTOCOL 8 (L14): 128 upgrade/revert cycles stress the singleton restart race.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[ignore = "stress: run by nix flake check"]
|
||||
async fn singleton_restart_stress() {
|
||||
singleton_restart_cycles(128).await;
|
||||
}
|
||||
|
||||
/// Exercises service restarts without duplicating the normal and stress setup.
|
||||
async fn singleton_restart_cycles(cycles: usize) {
|
||||
let world = TestWorld::new();
|
||||
world.plugin("notes", document("serve"), document("serve"));
|
||||
std::fs::write(
|
||||
@@ -379,10 +390,12 @@ async fn review_singleton_restart() {
|
||||
)
|
||||
.unwrap();
|
||||
let host = world.start().await;
|
||||
for _ in 0..128 {
|
||||
for _ in 0..cycles {
|
||||
activate(&host, "notes").await.unwrap();
|
||||
call(&host, "core.revert", json!({"plugin":"notes"})).await;
|
||||
}
|
||||
activate(&host, "notes").await.unwrap();
|
||||
marker(world.0.join("state/notes/service"), "2").await;
|
||||
host.stop().await;
|
||||
assert!(!world.0.join("state/notes/duplicate-service").exists());
|
||||
}
|
||||
@@ -422,14 +435,28 @@ async fn review_dropped_activation_completes() {
|
||||
.unwrap();
|
||||
host.stop().await;
|
||||
}
|
||||
/// M4: discovery times out and its process stops after either an answer or a timeout.
|
||||
/// PROTOCOL 10.1: discovery fails after its fixed ten-second deadline and receives EOF.
|
||||
/// PROTOCOL 3.7: old processes retain their declared grace while new calls use new code.
|
||||
/// PROTOCOL 3.7: retirement waits for all live admissions, including reverted versions.
|
||||
/// PROTOCOL 3.7: rejected discovery candidates never receive retirement hooks.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn upgrade_and_discovery_deadlines() {
|
||||
tokio::join!(
|
||||
review_discovery_deadline_and_stop(),
|
||||
old_process_grace_and_new_calls(),
|
||||
review_retirement_checks_live_version(),
|
||||
review_candidate_is_not_old_version(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Checks the fixed discovery deadline and subsequent process stop.
|
||||
async fn review_discovery_deadline_and_stop() {
|
||||
let world = TestWorld::new();
|
||||
let mut next = document("add");
|
||||
next["x-test-wait"] = json!(true);
|
||||
next["x-test-no-answer"] = json!(true);
|
||||
world.plugin("notes", document("add"), next);
|
||||
let host = world.start().await;
|
||||
let before = std::time::Instant::now();
|
||||
let result = tokio::time::timeout(Duration::from_secs(15), activate(&host, "notes")).await;
|
||||
assert!(
|
||||
result.unwrap().unwrap_err()["message"]
|
||||
@@ -437,6 +464,7 @@ async fn review_discovery_deadline_and_stop() {
|
||||
.unwrap()
|
||||
.contains("discovery")
|
||||
);
|
||||
assert!(before.elapsed() >= Duration::from_millis(9900));
|
||||
assert!(
|
||||
!host
|
||||
.processes()
|
||||
@@ -720,8 +748,7 @@ async fn graph_missing_import() {
|
||||
assert!(dot.as_str().unwrap().contains("absent"));
|
||||
host.stop().await;
|
||||
}
|
||||
/// PROTOCOL 3.7: new calls use new code while old calls retain their grace period.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
/// Checks the declared upgrade grace and dispatch to the new version.
|
||||
async fn old_process_grace_and_new_calls() {
|
||||
let world = TestWorld::new();
|
||||
let mut old = document("add");
|
||||
|
||||
Reference in New Issue
Block a user