cargo.build: a persistent build cache shared by all worlds

- The cargo target folder is <cache>/cargo-target, next to Wasmtime's
  cache, which moves to <cache>/wasmtime: Wasmtime's cleanup worker
  deletes unknown entries in its folder, also Cargo's artifacts.
- Builds use the SDK at <cache>/sdk/<content hash>/, so cargo reuses
  the compiled SDK in every world. /plugins/cargo/sdk/ stays for agents.
- Tests run in the release profile and share compiled crates with the
  release build.
- The cargo and cli tests use a persistent cache in CARGO_TARGET_TMPDIR:
  warm, the cargo suite takes 11 s (was 235 s) and the cli suite 6 s
  (was 149 s).
- nix flake check runs ignored stress tests. AGENTS.md: rules for fast
  tests.
This commit is contained in:
2026-10-23 08:05:15 +01:00
parent a090e1fd57
commit 93f092e02b
11 changed files with 155 additions and 48 deletions
+8 -2
View File
@@ -35,11 +35,17 @@ Keep it that way.
## Checks ## Checks
```sh ```sh
nix flake check -L # all tests below; CI runs this on every push nix flake check -L # everything below, plus the stress tests; CI runs this on every push
nix develop -c cargo test --workspace nix develop -c cargo test --workspace # fast with a warm cache: run it during development
nix develop -c cargo build --release -p agent --target wasm32-wasip2 nix develop -c cargo build --release -p agent --target wasm32-wasip2
nix develop -c cargo clippy --workspace nix develop -c cargo clippy --workspace
``` ```
`nix run -- init` and `nix run -- run` must keep working without other `nix run -- init` and `nix run -- run` must keep working without other
setup. setup.
Tests must stay fast. Only a few tests may wait for a timing (grace periods,
deadlines). Poll for a condition with a deadline, do not sleep. `#[ignore]` is
only for stress tests: `nix flake check` runs them with `--include-ignored`.
Tests that build plugins use the persistent cache in
`env!("CARGO_TARGET_TMPDIR")`.
+6
View File
@@ -69,6 +69,12 @@ a plugin, or a change that the agent makes to itself.
core. core.
- **The SDK is not published.** `cargo` points `sausage-plugin = "0.1"` at - **The SDK is not published.** `cargo` points `sausage-plugin = "0.1"` at
its own copy, which it also shows in `/plugins/cargo/sdk/`. its own copy, which it also shows in `/plugins/cargo/sdk/`.
- **Compiled code is a cache, not state.** It lives outside the world, in
one cache folder that all worlds share: `wasmtime/` (owned by Wasmtime,
whose cleanup deletes unknown entries), `cargo-target/`, and
`sdk/<hash>/`. A path that depends only on the content of the SDK lets
cargo reuse the compiled SDK in every world. Tests run in the release
profile, so that they share compiled crates with the release build.
- **Discovery has a deadline of 10 seconds.** It is an internal call of the - **Discovery has a deadline of 10 seconds.** It is an internal call of the
core, not a limit on plugins. Without it, one plugin that does not answer core, not a limit on plugins. Without it, one plugin that does not answer
would block every activation. would block every activation.
+7 -4
View File
@@ -411,16 +411,19 @@ The core has one native plugin: `cargo`. `cargo.build {"plugin": "notes"}`
builds `/plugins/notes/source/`: builds `/plugins/notes/source/`:
1. It copies the source once. The tests and the build both use this copy. 1. It copies the source once. The tests and the build both use this copy.
2. It runs the tests under WASI. 2. It runs the tests under WASI, in the release profile.
3. It builds the release `.wasm` for `wasm32-wasip2`. 3. It builds the release `.wasm` for `wasm32-wasip2`. The tests and the build
share the compiled dependencies.
4. It returns `{"wasm": "/state/cargo/builds/<n>/notes.wasm", "tests": 4. It returns `{"wasm": "/state/cargo/builds/<n>/notes.wasm", "tests":
{"passed", "failed", "output"}, "warnings"}`. If a step fails, it returns {"passed", "failed", "output"}, "warnings"}`. If a step fails, it returns
an error with the compiler or test output. an error with the compiler or test output.
- A call to `cargo.build` gets a process ID, a folder in `/proc`, and journal - A call to `cargo.build` gets a process ID, a folder in `/proc`, and journal
entries, like other calls. entries, like other calls.
- The dependency `sausage-plugin = "0.1"` points to the SDK copy at - `/plugins/cargo/sdk/` shows the source of the SDK. The dependency
`/plugins/cargo/sdk/`. `sausage-plugin = "0.1"` points to the same files in the host's cache.
- Compiled crates stay in the host's cache, outside the world, and all worlds
share them. So a second build compiles only what changed.
- Build scripts can compile C: `CC_wasm32_wasip2` and `AR_wasm32_wasip2` - Build scripts can compile C: `CC_wasm32_wasip2` and `AR_wasm32_wasip2`
point to a WASI clang. point to a WASI clang.
- The build does not activate the result. Call `core.activate` next. - The build does not activate the result. Call `core.activate` next.
+7 -2
View File
@@ -17,8 +17,9 @@ an LLM (OpenAI Responses or Anthropic Messages API). No clone needed:
nix run git+https://gitea.surma.technology/surma/sausage nix run git+https://gitea.surma.technology/surma/sausage
``` ```
This creates a world in `./world` the first time (it builds the agent, which This creates a world in `./world` the first time (it builds the agent: the
takes a few minutes), then starts it with a console. Later runs start the first build on a machine takes a few minutes, later worlds reuse the compiled
crates), then starts it with a console. Later runs start the
existing world. The two steps on their own: existing world. The two steps on their own:
```sh ```sh
@@ -32,6 +33,10 @@ the base URL, the model, and the token. It reads `SAUSAGE_API`,
values go to `world/config/agent.json`. Both commands take another folder as values go to `world/config/agent.json`. Both commands take another folder as
an argument, for example `... -- run my-world`. an argument, for example `... -- run my-world`.
Compiled code is cached outside the world, in `$XDG_CACHE_HOME/sausage/`
(`~/.cache/sausage/` without the variable). All worlds share it.
`--cache-dir <folder>` changes it for `init` and `run`.
To keep a world on the newest version, run it in a loop: To keep a world on the newest version, run it in a loop:
```sh ```sh
+56 -18
View File
@@ -3,6 +3,7 @@ use crate::{Host, world::World};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use include_dir::{Dir, include_dir}; use include_dir::{Dir, include_dir};
use serde_json::{Value, json}; use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use std::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
@@ -58,10 +59,12 @@ pub struct Cargo {
world: World, world: World,
builds: Arc<tokio::sync::Mutex<()>>, builds: Arc<tokio::sync::Mutex<()>>,
runner: PathBuf, runner: PathBuf,
target: PathBuf,
sdk: PathBuf,
} }
impl Cargo { impl Cargo {
/// Installs changed SDK files and selects the current sausage executable as runner. /// Installs changed SDK files and selects the current sausage executable as runner.
pub fn new(world: World) -> Result<Self> { pub fn new(world: World, cache: &Path) -> Result<Self> {
let mut runner = std::env::current_exe()?; let mut runner = std::env::current_exe()?;
if runner if runner
.parent() .parent()
@@ -69,28 +72,29 @@ impl Cargo {
{ {
runner = runner.parent().unwrap().parent().unwrap().join("sausage"); runner = runner.parent().unwrap().parent().unwrap().join("sausage");
} }
Self::with_runner(world, runner) Self::with_runner(world, runner, cache)
} }
/// Installs the SDK and accepts an explicit sausage binary, including for init integrations. /// Installs the SDK and accepts an explicit sausage binary, including for init integrations.
pub fn with_runner(world: World, runner: PathBuf) -> Result<Self> { pub fn with_runner(world: World, runner: PathBuf, cache: &Path) -> Result<Self> {
let sdk = world.plugin_dir(ID).join("sdk"); let sdk = world.plugin_dir(ID).join("sdk");
write_dir(&SDK, &sdk.join("src"))?; let mut files = std::collections::BTreeMap::new();
write_dir(&MACROS, &sdk.join("macros/src"))?; collect_files(&SDK, Path::new("src"), &mut files);
write_dir(&INTERFACE, &sdk.join("interface/src"))?; collect_files(&MACROS, Path::new("macros/src"), &mut files);
collect_files(&INTERFACE, Path::new("interface/src"), &mut files);
for (manifest, destination, interface_path) in [ for (manifest, destination, interface_path) in [
( (
include_str!("../../sdk/Cargo.toml"), include_str!("../../sdk/Cargo.toml"),
sdk.join("Cargo.toml"), PathBuf::from("Cargo.toml"),
Some("interface"), Some("interface"),
), ),
( (
include_str!("../../sdk/macros/Cargo.toml"), include_str!("../../sdk/macros/Cargo.toml"),
sdk.join("macros/Cargo.toml"), PathBuf::from("macros/Cargo.toml"),
Some("../interface"), Some("../interface"),
), ),
( (
include_str!("../../interface/Cargo.toml"), include_str!("../../interface/Cargo.toml"),
sdk.join("interface/Cargo.toml"), PathBuf::from("interface/Cargo.toml"),
None, None,
), ),
] { ] {
@@ -108,9 +112,40 @@ impl Cargo {
} }
} }
} }
write_changed(&destination, document.to_string().as_bytes())?; files.insert(destination, document.to_string().into_bytes());
}
let mut hash = Sha256::new();
for (path, bytes) in &files {
let name = path.to_str().unwrap().as_bytes();
hash.update((name.len() as u64).to_le_bytes());
hash.update(name);
hash.update((bytes.len() as u64).to_le_bytes());
hash.update(bytes);
write_changed(&sdk.join(path), bytes)?;
}
let cached = std::path::absolute(cache)?
.join("sdk")
.join(format!("{:x}", hash.finalize()));
if !cached.is_dir() {
let parent = cached.parent().unwrap();
std::fs::create_dir_all(parent)
.with_context(|| format!("create {}", parent.display()))?;
let staging = tempfile::Builder::new()
.prefix("install-")
.tempdir_in(parent)?;
for (path, bytes) in &files {
write_changed(&staging.path().join(path), bytes)?;
}
// Another builder can publish the same complete, content-addressed SDK first.
if let Err(error) = std::fs::rename(staging.path(), &cached)
&& !cached.is_dir()
{
return Err(error).with_context(|| format!("install {}", cached.display()));
}
} }
Ok(Self { Ok(Self {
sdk: cached,
target: std::path::absolute(cache)?.join("cargo-target"),
world, world,
runner, runner,
builds: Arc::new(tokio::sync::Mutex::new(())), builds: Arc::new(tokio::sync::Mutex::new(())),
@@ -136,12 +171,12 @@ impl Cargo {
pub async fn build(&self, source: &Path) -> std::result::Result<BuildOutput, BuildFailure> { pub async fn build(&self, source: &Path) -> std::result::Result<BuildOutput, BuildFailure> {
let build_guard = self.builds.clone().lock_owned().await; let build_guard = self.builds.clone().lock_owned().await;
let work = self.world.state_dir(ID).join("work"); let work = self.world.state_dir(ID).join("work");
let target = self.world.state_dir(ID).join("target"); let target = self.target.clone();
let temporary = self.world.state_dir(ID).join("tmp"); let temporary = self.world.state_dir(ID).join("tmp");
let cleanup = temporary.clone(); let cleanup = temporary.clone();
let source = source.to_path_buf(); let source = source.to_path_buf();
let copy = work.clone(); let copy = work.clone();
let sdk = self.world.plugin_dir(ID).join("sdk"); let sdk = self.sdk.clone();
let build_guard = tokio::task::spawn_blocking(move || -> Result<_> { let build_guard = tokio::task::spawn_blocking(move || -> Result<_> {
if cleanup.exists() { if cleanup.exists() {
std::fs::remove_dir_all(&cleanup) std::fs::remove_dir_all(&cleanup)
@@ -168,7 +203,7 @@ impl Cargo {
.await .await
.map_err(anyhow::Error::from)??; .map_err(anyhow::Error::from)??;
let mut test = Command::new("cargo"); let mut test = Command::new("cargo");
test.args(["test", "--target", TARGET, "--target-dir"]) test.args(["test", "--release", "--target", TARGET, "--target-dir"])
.arg(&target) .arg(&target)
.current_dir(&work) .current_dir(&work)
.env("TMPDIR", &temporary) .env("TMPDIR", &temporary)
@@ -372,15 +407,18 @@ fn write_changed(path: &Path, bytes: &[u8]) -> Result<()> {
} }
Ok(()) Ok(())
} }
/// Installs one embedded source tree without touching unchanged files. /// Collects embedded source files for deterministic hashing and installation.
fn write_dir(directory: &Dir<'_>, destination: &Path) -> Result<()> { fn collect_files(
directory: &Dir<'_>,
destination: &Path,
files: &mut std::collections::BTreeMap<PathBuf, Vec<u8>>,
) {
for file in directory.files() { for file in directory.files() {
write_changed(&destination.join(file.path()), file.contents())?; files.insert(destination.join(file.path()), file.contents().to_vec());
} }
for directory in directory.dirs() { for directory in directory.dirs() {
write_dir(directory, destination)?; collect_files(directory, destination, files);
} }
Ok(())
} }
/// Counts only final libtest summaries, never individual status lines. /// Counts only final libtest summaries, never individual status lines.
fn test_count(output: &str, field: &str) -> u64 { fn test_count(output: &str, field: &str) -> u64 {
+5 -2
View File
@@ -58,6 +58,7 @@ impl Host {
/// Start a world with an explicit compile cache folder. /// Start a world with an explicit compile cache folder.
pub async fn start_with_cache(path: &Path, cache: &Path) -> anyhow::Result<Self> { pub async fn start_with_cache(path: &Path, cache: &Path) -> anyhow::Result<Self> {
let path = path.to_path_buf(); let path = path.to_path_buf();
let cargo_cache = cache.to_path_buf();
let cache = cache.to_path_buf(); let cache = cache.to_path_buf();
let (world, runtime) = tokio::task::spawn_blocking(move || -> anyhow::Result<_> { let (world, runtime) = tokio::task::spawn_blocking(move || -> anyhow::Result<_> {
Ok((World::open(&path)?, Runtime::new(&cache)?)) Ok((World::open(&path)?, Runtime::new(&cache)?))
@@ -67,8 +68,10 @@ impl Host {
journal::Journal::open(world.state_dir("core").join("journal.sqlite")).await?; journal::Journal::open(world.state_dir("core").join("journal.sqlite")).await?;
let host = Self::with_parts(world, runtime, journal); let host = Self::with_parts(world, runtime, journal);
let cargo_world = host.0.world.clone(); let cargo_world = host.0.world.clone();
let cargo = let cargo = tokio::task::spawn_blocking(move || {
tokio::task::spawn_blocking(move || crate::cargo::Cargo::new(cargo_world)).await??; crate::cargo::Cargo::new(cargo_world, &cargo_cache)
})
.await??;
cargo.register(&host); cargo.register(&host);
host.boot().await?; host.boot().await?;
Ok(host) Ok(host)
+12 -4
View File
@@ -79,7 +79,7 @@ fn setting(var: &str, question: &str, default: Option<&str>) -> Result<String> {
} }
} }
/// Creates a configured world and builds its embedded agent through the standalone builder. /// Creates a configured world and builds its embedded agent through the standalone builder.
async fn init(dir: &Path) -> Result<()> { async fn init(dir: &Path, cache: Option<&Path>) -> Result<()> {
let api = setting("SAUSAGE_API", "API shape", Some("openai-responses"))?; let api = setting("SAUSAGE_API", "API shape", Some("openai-responses"))?;
if api != "openai-responses" && api != "anthropic-messages" { if api != "openai-responses" && api != "anthropic-messages" {
bail!("unknown API shape {api:?}"); bail!("unknown API shape {api:?}");
@@ -105,7 +105,15 @@ async fn init(dir: &Path) -> Result<()> {
&json!({"api":api,"base_url":base_url.trim_end_matches('/'),"model":model,"token":token}), &json!({"api":api,"base_url":base_url.trim_end_matches('/'),"model":model,"token":token}),
)?, )?,
)?; )?;
let builder = sausage::cargo::Cargo::new(world.clone())?; let cache = cache.map(Path::to_path_buf).unwrap_or_else(|| {
let base = std::env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| {
PathBuf::from(std::env::var_os("HOME").unwrap_or_default()).join(".cache")
});
base.join("sausage")
});
let builder = sausage::cargo::Cargo::new(world.clone(), &cache)?;
let source = world.plugin_dir("agent").join("source"); let source = world.plugin_dir("agent").join("source");
std::fs::create_dir_all(source.join("src"))?; std::fs::create_dir_all(source.join("src"))?;
AGENT.extract(source.join("src"))?; AGENT.extract(source.join("src"))?;
@@ -133,7 +141,7 @@ async fn main() -> Result<std::process::ExitCode> {
None => { None => {
let dir = PathBuf::from("world"); let dir = PathBuf::from("world");
if !dir.join("config/boot.json").is_file() { if !dir.join("config/boot.json").is_file() {
init(&dir).await?; init(&dir, cli.cache_dir.as_deref()).await?;
} }
Cmd::Run { dir } Cmd::Run { dir }
} }
@@ -144,7 +152,7 @@ async fn main() -> Result<std::process::ExitCode> {
return Ok(std::process::ExitCode::FAILURE); return Ok(std::process::ExitCode::FAILURE);
} }
} }
Cmd::Init { dir } => init(&dir).await?, Cmd::Init { dir } => init(&dir, cli.cache_dir.as_deref()).await?,
Cmd::Run { dir } => { Cmd::Run { dir } => {
let mut term = let mut term =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?; tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
+2 -1
View File
@@ -222,7 +222,8 @@ impl Runtime {
let mut config = Config::new(); let mut config = Config::new();
config.epoch_interruption(true); config.epoch_interruption(true);
let disk_cache = (|| -> Result<_> { let disk_cache = (|| -> Result<_> {
std::fs::create_dir_all(cache)?; let cache = cache.join("wasmtime");
std::fs::create_dir_all(&cache)?;
let mut cache_config = wasmtime::CacheConfig::new(); let mut cache_config = wasmtime::CacheConfig::new();
cache_config.with_directory(cache.canonicalize()?); cache_config.with_directory(cache.canonicalize()?);
Ok(wasmtime::Cache::new(cache_config)?) Ok(wasmtime::Cache::new(cache_config)?)
+34 -8
View File
@@ -3,6 +3,7 @@ use sausage::{Host, world::World};
use serde_json::{Value, json}; use serde_json::{Value, json};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
const CACHE: &str = concat!(env!("CARGO_TARGET_TMPDIR"), "/cache");
struct TestWorld(PathBuf); struct TestWorld(PathBuf);
impl TestWorld { impl TestWorld {
/// Creates a fresh native build world. /// Creates a fresh native build world.
@@ -45,10 +46,12 @@ fn fixture(name: &str) -> PathBuf {
} }
/// Builds through the real registered native plugin. /// Builds through the real registered native plugin.
async fn build(world: &TestWorld, name: &str) -> Result<Value, Value> { async fn build(world: &TestWorld, name: &str) -> Result<Value, Value> {
let host = Host::start(&world.0).await.unwrap(); let host = Host::start_with_cache(&world.0, Path::new(CACHE))
.await
.unwrap();
let runner = world.0.join("state/runner with spaces"); let runner = world.0.join("state/runner with spaces");
std::os::unix::fs::symlink(env!("CARGO_BIN_EXE_sausage"), &runner).unwrap(); std::os::unix::fs::symlink(env!("CARGO_BIN_EXE_sausage"), &runner).unwrap();
sausage::cargo::Cargo::with_runner(World::open(&world.0).unwrap(), runner) sausage::cargo::Cargo::with_runner(World::open(&world.0).unwrap(), runner, Path::new(CACHE))
.unwrap() .unwrap()
.register(&host); .register(&host);
let result = host.call("cargo.build", json!({"plugin":name})).await; let result = host.call("cargo.build", json!({"plugin":name})).await;
@@ -93,6 +96,7 @@ async fn valid() -> &'static Valid {
.args([ .args([
"test", "test",
"--no-run", "--no-run",
"--release",
"--message-format=json", "--message-format=json",
"--bin", "--bin",
"cargo-valid", "cargo-valid",
@@ -100,7 +104,7 @@ async fn valid() -> &'static Valid {
"wasm32-wasip2", "wasm32-wasip2",
"--target-dir", "--target-dir",
]) ])
.arg(world.0.join("state/cargo/target")) .arg(Path::new(CACHE).join("cargo-target"))
.current_dir(world.0.join("state/cargo/work")) .current_dir(world.0.join("state/cargo/work"))
.stdin(std::process::Stdio::null()) .stdin(std::process::Stdio::null())
.output() .output()
@@ -221,7 +225,16 @@ async fn manifest_and_nested_sources() {
let manifest = std::fs::read_to_string(root.join("Cargo.toml")).unwrap(); let manifest = std::fs::read_to_string(root.join("Cargo.toml")).unwrap();
assert!(manifest.contains("workspace.package")); assert!(manifest.contains("workspace.package"));
assert!(manifest.contains("optional = true")); assert!(manifest.contains("optional = true"));
assert!(manifest.contains("/plugins/cargo/sdk")); let document: toml_edit::DocumentMut = manifest.parse().unwrap();
let sdk = Path::new(document["target"]["cfg(target_arch = \"wasm32\")"]["dependencies"]["sausage_plugin"]["path"].as_str().unwrap());
assert_eq!(sdk.parent().unwrap(), Path::new(CACHE).join("sdk"));
let hash = sdk.file_name().unwrap().to_str().unwrap();
assert_eq!(hash.len(), 64);
assert!(hash.bytes().all(|byte| byte.is_ascii_hexdigit()));
assert_eq!(
std::fs::read(sdk.join("src/lib.rs")).unwrap(),
std::fs::read(valid.world.0.join("plugins/cargo/sdk/src/lib.rs")).unwrap()
);
assert_eq!( assert_eq!(
std::fs::read_to_string(root.join("src/target/data")).unwrap(), std::fs::read_to_string(root.join("src/target/data")).unwrap(),
"nested target" "nested target"
@@ -235,7 +248,9 @@ async fn manifest_and_nested_sources() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn native_discovery_and_error_paths() { async fn native_discovery_and_error_paths() {
let world = TestWorld::new(); let world = TestWorld::new();
let host = Host::start(&world.0).await.unwrap(); let host = Host::start_with_cache(&world.0, Path::new(CACHE))
.await
.unwrap();
assert_eq!( assert_eq!(
host.call("cargo.rpc.discover", json!({})).await.unwrap()["methods"][0]["name"], host.call("cargo.rpc.discover", json!({})).await.unwrap()["methods"][0]["name"],
"build" "build"
@@ -264,7 +279,7 @@ async fn release_only_failure() {
"[package]\nname='release-only'\nversion='0.1.0'\nedition='2024'\n", "[package]\nname='release-only'\nversion='0.1.0'\nedition='2024'\n",
) )
.unwrap(); .unwrap();
std::fs::write(root.join("main.rs"), "#[cfg(not(debug_assertions))] compile_error!(\"release-only error\"); fn main() {} #[test] fn passes() {}\n").unwrap(); std::fs::write(root.join("main.rs"), "#[cfg(not(test))] compile_error!(\"release-only error\"); fn main() {} #[test] fn passes() {}\n").unwrap();
let error = build(&world, "release").await.unwrap_err(); let error = build(&world, "release").await.unwrap_err();
assert!( assert!(
error["data"]["output"] error["data"]["output"]
@@ -296,6 +311,7 @@ async fn cancelled_preparation_is_serialized() {
sausage::cargo::Cargo::with_runner( sausage::cargo::Cargo::with_runner(
World::open(&world.0).unwrap(), World::open(&world.0).unwrap(),
PathBuf::from(env!("CARGO_BIN_EXE_sausage")), PathBuf::from(env!("CARGO_BIN_EXE_sausage")),
Path::new(CACHE),
) )
.unwrap(), .unwrap(),
); );
@@ -358,6 +374,12 @@ async fn valid_build() {
assert_eq!(valid.result["tests"]["passed"], 12); assert_eq!(valid.result["tests"]["passed"], 12);
assert_eq!(valid.result["tests"]["failed"], 0); assert_eq!(valid.result["tests"]["failed"], 0);
assert!(!valid.world.0.join("plugins/valid/plugin.wasm").exists()); assert!(!valid.world.0.join("plugins/valid/plugin.wasm").exists());
assert!(!valid.world.0.join("state/cargo/target").exists());
assert!(
Path::new(CACHE)
.join("cargo-target/wasm32-wasip2/release")
.is_dir()
);
} }
/// PROTOCOL 11: tests and compilation share one source copy without target metadata. /// PROTOCOL 11: tests and compilation share one source copy without target metadata.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -373,7 +395,7 @@ async fn source_copy() {
assert!( assert!(
std::fs::read_to_string(valid.world.0.join("state/cargo/work/Cargo.toml")) std::fs::read_to_string(valid.world.0.join("state/cargo/work/Cargo.toml"))
.unwrap() .unwrap()
.contains("/plugins/cargo/sdk") .contains(Path::new(CACHE).join("sdk").to_str().unwrap())
); );
} }
/// PROTOCOL 11: compiler failures return compiler diagnostics. /// PROTOCOL 11: compiler failures return compiler diagnostics.
@@ -514,6 +536,7 @@ async fn unchanged_sdk() {
sausage::cargo::Cargo::with_runner( sausage::cargo::Cargo::with_runner(
World::open(&world.0).unwrap(), World::open(&world.0).unwrap(),
PathBuf::from(env!("CARGO_BIN_EXE_sausage")), PathBuf::from(env!("CARGO_BIN_EXE_sausage")),
Path::new(CACHE),
) )
.unwrap() .unwrap()
}; };
@@ -534,6 +557,7 @@ async fn standalone_builder() {
let builder = sausage::cargo::Cargo::with_runner( let builder = sausage::cargo::Cargo::with_runner(
World::open(&world.0).unwrap(), World::open(&world.0).unwrap(),
PathBuf::from(env!("CARGO_BIN_EXE_sausage")), PathBuf::from(env!("CARGO_BIN_EXE_sausage")),
Path::new(CACHE),
) )
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
@@ -555,7 +579,9 @@ async fn kill_process_group() {
); );
let world = TestWorld::new(); let world = TestWorld::new();
world.source("hang", &fixture("cargo-hang")); world.source("hang", &fixture("cargo-hang"));
let host = Host::start(&world.0).await.unwrap(); let host = Host::start_with_cache(&world.0, Path::new(CACHE))
.await
.unwrap();
let building = host.clone(); let building = host.clone();
let task = let task =
tokio::spawn(async move { building.call("cargo.build", json!({"plugin":"hang"})).await }); tokio::spawn(async move { building.call("cargo.build", json!({"plugin":"hang"})).await });
+14 -5
View File
@@ -12,9 +12,15 @@ use tokio::{
process::{Child, Command}, process::{Child, Command},
}; };
const CACHE: &str = concat!(env!("CARGO_TARGET_TMPDIR"), "/cache");
fn binary() -> Command { fn binary() -> Command {
let mut command = Command::new(env!("CARGO_BIN_EXE_sausage")); let mut command = Command::new(env!("CARGO_BIN_EXE_sausage"));
command.env_remove("SAUSAGE_HEADLESS").kill_on_drop(true); command
.arg("--cache-dir")
.arg(CACHE)
.env_remove("SAUSAGE_HEADLESS")
.kill_on_drop(true);
command command
} }
fn world() -> (tempfile::TempDir, PathBuf) { fn world() -> (tempfile::TempDir, PathBuf) {
@@ -133,10 +139,13 @@ async fn init_builds_agent() {
std::fs::read(world.join("plugins/agent/source/Cargo.lock")).unwrap(), std::fs::read(world.join("plugins/agent/source/Cargo.lock")).unwrap(),
include_bytes!("../../Cargo.lock") include_bytes!("../../Cargo.lock")
); );
let host = Host::start_with_cache( assert!(!world.join("state/cargo/target").exists());
&world, assert!(
&Path::new(env!("CARGO_MANIFEST_DIR")).join("../target/core/cli-cache"), Path::new(CACHE)
) .join("cargo-target/wasm32-wasip2/release")
.is_dir()
);
let host = Host::start_with_cache(&world, Path::new(CACHE))
.await .await
.unwrap(); .unwrap();
let doc = host.call("agent.rpc.discover", json!({})).await.unwrap(); let doc = host.call("agent.rpc.discover", json!({})).await.unwrap();
+2
View File
@@ -98,6 +98,8 @@
checks.tests = craneLib.cargoTest (common // wasiCcEnv // { checks.tests = craneLib.cargoTest (common // wasiCcEnv // {
inherit cargoArtifacts; inherit cargoArtifacts;
cargoExtraArgs = "--locked ${testPackages}"; cargoExtraArgs = "--locked ${testPackages}";
# Stress tests are ignored in a normal `cargo test`. The check runs them.
cargoTestExtraArgs = "-- --include-ignored";
# Nothing builds on the test run's target folder: don't store it. # Nothing builds on the test run's target folder: don't store it.
doInstallCargoArtifacts = false; doInstallCargoArtifacts = false;
# The SDK tests run sample plugins with the wasmtime CLI. # The SDK tests run sample plugins with the wasmtime CLI.