Merge v3-t15: shared compile cache, precompile, /bin/*.wasm and /src/sausage copies (T15)
This commit is contained in:
+114
-8
@@ -1,13 +1,13 @@
|
||||
//! P1 integration: a read-only bundle and real shell calls from the durable harness.
|
||||
//! P1 integration: independent bundle copies and real shell calls from the durable harness.
|
||||
use sausage::{harness::run_module, world::World};
|
||||
use sausage_vfs::Open;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn bundle_mount_and_real_shell() -> anyhow::Result<()> {
|
||||
async fn bundle_copy_and_real_shell() -> anyhow::Result<()> {
|
||||
let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..");
|
||||
let bundle = repo.join("target/userland/bin");
|
||||
if !bundle.join("sh").exists() {
|
||||
let bundle = repo.join("target/userland/bundle");
|
||||
if !bundle.join("bin/sh.wasm").exists() {
|
||||
assert!(
|
||||
std::process::Command::new("nu")
|
||||
.env_remove("CARGO_TARGET_DIR")
|
||||
@@ -18,9 +18,16 @@ async fn bundle_mount_and_real_shell() -> anyhow::Result<()> {
|
||||
}
|
||||
let dir = tempfile::tempdir()?;
|
||||
let world = World::open_with_userland(dir.path(), Some(&bundle))?;
|
||||
assert!(world.root.stat("/bin/sh", true).await?.size > 0);
|
||||
assert!(world.root.stat("/bin/sh.wasm", true).await?.size > 0);
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
for entry in std::fs::read_dir(dir.path().join("world/bin"))? {
|
||||
let entry = entry?;
|
||||
assert_eq!(entry.path().extension().unwrap(), "wasm");
|
||||
assert_eq!(entry.metadata()?.nlink(), 1);
|
||||
assert!(!entry.file_type()?.is_symlink());
|
||||
}
|
||||
assert!(world.root.stat("/usr/local/bin", true).await?.directory);
|
||||
let denied = world
|
||||
let writable = world
|
||||
.root
|
||||
.open(
|
||||
"/bin/forbidden",
|
||||
@@ -31,17 +38,72 @@ async fn bundle_mount_and_real_shell() -> anyhow::Result<()> {
|
||||
},
|
||||
)
|
||||
.await;
|
||||
assert!(matches!(denied, Err(e) if e.kind() == std::io::ErrorKind::ReadOnlyFilesystem));
|
||||
assert!(writable.is_ok());
|
||||
let exec = sausage::process::exec(World {
|
||||
root: world.root.clone(),
|
||||
host_dir: world.host_dir.clone(),
|
||||
processes: world.processes.clone(),
|
||||
})?;
|
||||
use sausage::exec::{Command, Options, Stream};
|
||||
for (script, expected) in [
|
||||
("command -v ls", "/bin/ls.wasm\n"),
|
||||
("ls", "bin"),
|
||||
("git --version", "git version"),
|
||||
("printf 'a\\n' | xargs echo", "a\n"),
|
||||
("env FOO=1 printenv FOO", "1\n"),
|
||||
] {
|
||||
let mut out = Vec::new();
|
||||
let code = exec
|
||||
.exec(
|
||||
Command::Shell(script.into()),
|
||||
Options::default(),
|
||||
&mut |stream, bytes| {
|
||||
if matches!(stream, Stream::Stdout) {
|
||||
out.extend_from_slice(bytes);
|
||||
}
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(code, 0, "{script}");
|
||||
assert!(String::from_utf8(out)?.contains(expected), "{script}");
|
||||
}
|
||||
let mut history = Vec::new();
|
||||
assert_eq!(
|
||||
exec.exec(
|
||||
Command::Shell("cd /src/sausage && git log --oneline".into()),
|
||||
Options::default(),
|
||||
&mut |stream, bytes| {
|
||||
if matches!(stream, Stream::Stdout) {
|
||||
history.extend_from_slice(bytes);
|
||||
}
|
||||
},
|
||||
)
|
||||
.await?,
|
||||
0,
|
||||
);
|
||||
let history = String::from_utf8(history)?;
|
||||
assert_eq!(history.lines().count(), 1);
|
||||
assert!(history.contains("Bundle source "));
|
||||
let mut status = Vec::new();
|
||||
assert_eq!(
|
||||
exec.exec(
|
||||
Command::Shell("cd /src/sausage && git status --short".into()),
|
||||
Options::default(),
|
||||
&mut |stream, bytes| {
|
||||
if matches!(stream, Stream::Stdout) {
|
||||
status.extend_from_slice(bytes);
|
||||
}
|
||||
},
|
||||
)
|
||||
.await?,
|
||||
0,
|
||||
);
|
||||
assert!(status.is_empty(), "{}", String::from_utf8_lossy(&status));
|
||||
let source = format!(
|
||||
"globalThis.testP1 = true;\n{}",
|
||||
include_str!("../js/test-bundle.js")
|
||||
);
|
||||
run_module(world, exec, &source).await?;
|
||||
run_module(world, exec.clone(), &source).await?;
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(dir.path().join("world/test.txt"))?,
|
||||
"after\n"
|
||||
@@ -59,6 +121,50 @@ async fn bundle_mount_and_real_shell() -> anyhow::Result<()> {
|
||||
let actual = std::fs::read_to_string(spills[0].path())?;
|
||||
let expected: String = (1..=10000).map(|n| format!("{n}\n")).collect();
|
||||
assert_eq!(actual, expected);
|
||||
let source_cat = std::fs::read(bundle.join("bin/cat.wasm"))?;
|
||||
let head = std::fs::read(dir.path().join("world/bin/head.wasm"))?;
|
||||
let mut out = Vec::new();
|
||||
assert_eq!(
|
||||
exec.exec(
|
||||
Command::Shell(
|
||||
"cp /bin/true.wasm /bin/cat.wasm; cat /test.txt; head -n 1 /test.txt".into()
|
||||
),
|
||||
Options::default(),
|
||||
&mut |stream, bytes| {
|
||||
if matches!(stream, Stream::Stdout) {
|
||||
out.extend_from_slice(bytes);
|
||||
}
|
||||
}
|
||||
)
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
assert_eq!(out, b"after\n");
|
||||
assert_eq!(std::fs::read(bundle.join("bin/cat.wasm"))?, source_cat);
|
||||
assert_eq!(std::fs::read(dir.path().join("world/bin/head.wasm"))?, head);
|
||||
assert_eq!(
|
||||
exec.exec(
|
||||
Command::Shell("cp /bin/true.wasm /usr/local/bin/head.wasm; head /test.txt".into()),
|
||||
Options::default(),
|
||||
&mut |_, bytes| assert!(bytes.is_empty())
|
||||
)
|
||||
.await?,
|
||||
0
|
||||
);
|
||||
let changed = std::fs::read(dir.path().join("world/bin/cat.wasm"))?;
|
||||
let reopened = World::open_with_userland(
|
||||
dir.path(),
|
||||
Some(std::path::Path::new("/nonexistent-bundle")),
|
||||
)?;
|
||||
assert_eq!(
|
||||
reopened
|
||||
.root
|
||||
.open("/bin/cat.wasm", Open::default())
|
||||
.await?
|
||||
.read(0, changed.len())
|
||||
.await?,
|
||||
changed
|
||||
);
|
||||
// An unbundled world remains usable by process tests and file-only callers.
|
||||
let unbundled = tempfile::tempdir()?;
|
||||
let world = World::open(unbundled.path())?;
|
||||
|
||||
+56
-11
@@ -9,6 +9,39 @@ use std::{
|
||||
sync::{Arc, Once},
|
||||
time::Duration,
|
||||
};
|
||||
#[test]
|
||||
fn world_copies_programs_and_nested_source_only_once() -> anyhow::Result<()> {
|
||||
let temp = tempfile::tempdir()?;
|
||||
let bundle = temp.path().join("bundle");
|
||||
std::fs::create_dir_all(bundle.join("bin"))?;
|
||||
std::fs::create_dir_all(bundle.join("src/sausage/.git"))?;
|
||||
std::fs::write(bundle.join("bin/probe.wasm"), b"program")?;
|
||||
std::fs::write(
|
||||
bundle.join("src/sausage/.git/HEAD"),
|
||||
b"ref: refs/heads/main\n",
|
||||
)?;
|
||||
std::fs::write(bundle.join("src/sausage/README.md"), b"source")?;
|
||||
let path = temp.path().join("world");
|
||||
let _world = World::open_with_userland(&path, Some(&bundle))?;
|
||||
assert_eq!(
|
||||
std::fs::read(path.join("world/bin/probe.wasm"))?,
|
||||
b"program"
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read(path.join("world/src/sausage/README.md"))?,
|
||||
b"source"
|
||||
);
|
||||
assert!(path.join("world/src/sausage/.git/HEAD").is_file());
|
||||
std::fs::write(path.join("world/src/sausage/README.md"), b"agent patch")?;
|
||||
let _world = World::open_with_userland(&path, Some(Path::new("/missing-bundle")))?;
|
||||
assert_eq!(
|
||||
std::fs::read(path.join("world/src/sausage/README.md"))?,
|
||||
b"agent patch"
|
||||
);
|
||||
assert!(!path.join("host/compile-cache").exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod features {
|
||||
//! Acceptance coverage for preview1, identity, inherited IO, services, HTTP, and bundle aliases.
|
||||
use super::*;
|
||||
@@ -260,28 +293,32 @@ mod features {
|
||||
empty(&processes).await;
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn relative_aliases_share_compilation() {
|
||||
async fn independent_copies_share_compilation() {
|
||||
let _serial = SERIAL.lock().await;
|
||||
let (temp, runtime, _) = world();
|
||||
let bin = temp.path().join("world/bin");
|
||||
for name in ["true", "echo"] {
|
||||
std::os::unix::fs::symlink("cat", bin.join(name)).unwrap();
|
||||
for name in ["read-a.wasm", "read-b.wasm"] {
|
||||
std::fs::copy(bin.join("cat.wasm"), bin.join(name)).unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
run(&runtime, Command::Argv(vec!["/bin/true".into()]), "/")
|
||||
.await
|
||||
.0,
|
||||
run(
|
||||
&runtime,
|
||||
Command::Argv(vec!["/bin/read-a".into(), "/f".into()]),
|
||||
"/"
|
||||
)
|
||||
.await
|
||||
.0,
|
||||
0
|
||||
);
|
||||
assert_eq!(runtime.compiled_programs(), 1);
|
||||
let (code, out, _) = run(
|
||||
&runtime,
|
||||
Command::Argv(vec!["/bin/echo".into(), "shared".into()]),
|
||||
Command::Argv(vec!["/bin/read-b".into(), "/f".into()]),
|
||||
"/",
|
||||
)
|
||||
.await;
|
||||
assert_eq!(code, 0);
|
||||
assert_eq!(out, b"shared\n");
|
||||
assert_eq!(out, b"root\n");
|
||||
assert_eq!(runtime.compiled_programs(), 1);
|
||||
}
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
@@ -464,13 +501,17 @@ fn world() -> (tempfile::TempDir, Arc<Runtime>, sausage::process::Processes) {
|
||||
let world = World::open(temp.path()).unwrap();
|
||||
let cache = Path::new(env!("CARGO_TARGET_TMPDIR")).join("process-host/compile-cache");
|
||||
std::fs::create_dir_all(&cache).unwrap();
|
||||
std::os::unix::fs::symlink(cache, world.host_dir.join("compile-cache")).unwrap();
|
||||
|
||||
let processes = world.processes.clone();
|
||||
fixture(&temp.path().join("world"));
|
||||
let artifacts = repo().join("userland/target/wasm32-wasip2/release");
|
||||
let bin = temp.path().join("world/bin");
|
||||
for tool in ["cat", "tr", "ls", "wc", "yes", "head", "sleep"] {
|
||||
std::fs::copy(artifacts.join("sausage-coreutils.wasm"), bin.join(tool)).unwrap();
|
||||
std::fs::copy(
|
||||
artifacts.join(format!("{tool}.wasm")),
|
||||
bin.join(format!("{tool}.wasm")),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
for tool in ["ps", "kill"] {
|
||||
std::fs::copy(artifacts.join("sausage-procps.wasm"), bin.join(tool)).unwrap();
|
||||
@@ -491,7 +532,11 @@ fn world() -> (tempfile::TempDir, Arc<Runtime>, sausage::process::Processes) {
|
||||
bin.join("git"),
|
||||
)
|
||||
.unwrap();
|
||||
(temp, Runtime::new(world).unwrap(), processes)
|
||||
(
|
||||
temp,
|
||||
Runtime::new_with_cache(world, &cache).unwrap(),
|
||||
processes,
|
||||
)
|
||||
}
|
||||
async fn run(runtime: &Arc<Runtime>, command: Command, cwd: &str) -> (i32, Vec<u8>, Vec<u8>) {
|
||||
let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
|
||||
|
||||
Reference in New Issue
Block a user