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:
@@ -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")`.
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user