//! Durable event transactions, crash recovery, and world extension reactors. use sausage::{harness::run_module, world::World}; use std::{ path::Path, process::Command, time::{Duration, Instant}, }; const BUNDLE: &str = include_str!("../../js/events-test-bundle.js"); struct Author(std::sync::Arc); #[async_trait::async_trait] impl sausage::exec::Exec for Author { async fn exec( &self, command: sausage::exec::Command, mut options: sausage::exec::Options, output: &mut (dyn for<'a> FnMut(sausage::exec::Stream, &'a [u8]) + Send), ) -> anyhow::Result { for (key, value) in [ ("GIT_AUTHOR_NAME", "Test"), ("GIT_AUTHOR_EMAIL", "test@example.com"), ("GIT_COMMITTER_NAME", "Test"), ("GIT_COMMITTER_EMAIL", "test@example.com"), ] { options.env.push((key.into(), value.into())); } self.0.exec(command, options, output).await } } async fn run(dir: &Path, mode: &str) -> anyhow::Result<()> { let world = World::open(dir)?; run_module( world, sausage::process::exec(World::open(dir)?)?, &format!("globalThis.eventsMode={mode:?};\n{BUNDLE}"), ) .await } #[tokio::test(flavor = "current_thread")] async fn transactions_and_feed() -> anyhow::Result<()> { let dir = tempfile::tempdir()?; run(dir.path(), "normal").await } #[tokio::test(flavor = "current_thread")] async fn restart_child() -> anyhow::Result<()> { if let Ok(dir) = std::env::var("EVENTS_RESTART_DIR") { run(Path::new(&dir), &std::env::var("EVENTS_MODE")?).await?; } Ok(()) } #[test] fn sigkill_between_emit_and_reaction() -> anyhow::Result<()> { let dir = tempfile::tempdir()?; let mut child = Command::new(std::env::current_exe()?) .args(["--exact", "events::restart_child", "--nocapture"]) .env("EVENTS_RESTART_DIR", dir.path()) .env("EVENTS_MODE", "emit") .spawn()?; let deadline = Instant::now() + Duration::from_secs(30); while !dir.path().join("world/emitted").is_file() { if Instant::now() > deadline { child.kill()?; child.wait()?; anyhow::bail!("Event did not commit"); } std::thread::yield_now(); } child.kill()?; child.wait()?; for _ in 0..2 { let mut child = Command::new(std::env::current_exe()?) .args(["--exact", "events::restart_child", "--nocapture"]) .env("EVENTS_RESTART_DIR", dir.path()) .env("EVENTS_MODE", "consume") .spawn()?; let deadline = Instant::now() + Duration::from_secs(30); loop { if let Some(status) = child.try_wait()? { assert!(status.success()); break; } if Instant::now() > deadline { child.kill()?; child.wait()?; anyhow::bail!("Reaction did not finish"); } std::thread::yield_now(); } } Ok(()) } #[tokio::test(flavor = "current_thread")] async fn world_extensions() -> anyhow::Result<()> { let target = crate::common::build_git(); let dir = tempfile::tempdir()?; let world = World::open(dir.path())?; let disk = dir.path().join("world"); std::fs::create_dir_all(disk.join("bin"))?; let git = target.join("wasm32-wasip2/release/git.wasm"); std::fs::copy(git, disk.join("bin/git.wasm"))?; let repo = disk.join("src/sausage"); std::fs::create_dir_all(&repo)?; assert!( Command::new("git") .args(["init", "-b", "main"]) .arg(&repo) .status()? .success() ); assert!( Command::new("git") .args(["commit", "--no-gpg-sign", "--allow-empty", "-m", "empty"]) .env("GIT_AUTHOR_NAME", "Test") .env("GIT_AUTHOR_EMAIL", "test@example.com") .env("GIT_COMMITTER_NAME", "Test") .env("GIT_COMMITTER_EMAIL", "test@example.com") .current_dir(&repo) .status()? .success() ); let exec = std::sync::Arc::new(Author(sausage::process::exec(World { root: world.root.clone(), host_dir: world.host_dir.clone(), processes: world.processes.clone(), })?)); run_module( world, exec, &format!("globalThis.eventsMode='world';\n{BUNDLE}"), ) .await }