Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion vmm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,7 @@ serde = "1.0.228"
anyhow = "1.0.97"
bindgen = "0.72.0"

# [dev-dependencies]
[dev-dependencies]
fix-guest = { package = "fix", path = "../fix", artifact = "bin:fix", target = "x86_64-unknown-none" }

# kernel = { path = "../kernel", artifact = "bin", target = "x86_64-unknown-none" }
110 changes: 110 additions & 0 deletions vmm/tests/test_fix_eval.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use std::thread;
use std::time::{Duration, Instant};

// Keep the regression input with the test instead of relying on a fixture
const FIX_PROGRAM: &str = r#"x = create_blob(Int(2));
y = create_blob(Int(3));
add = create_blob(Path("./target/x86_64-unknown-none/addblob"));
add_x_y = create_application_thunk(create_tree(add, x, y));
z = create_blob(Int(1));
sum_x_y = create_strict_encode(add_x_y)
add_xy_z = create_application_thunk(create_tree(add, sum_x_y, z));
eval(create_strict_encode(add_xy_z));
"#;
const EXPECTED_OUTPUT: &str = "as a u64: 6";
// Time limited so this test doesn't stall the test suite (usually runs in <10s)
const DEADLOCK_LIMIT: Duration = Duration::from_secs(30);
// Poll lightly instead of busy-waiting for the child
const POLL_INTERVAL: Duration = Duration::from_millis(10);

struct ProgramFile {
path: PathBuf,
}

impl ProgramFile {
fn new() -> Self {
// The production CLI requires a path, so materialize the inline program
let path = std::env::temp_dir().join(format!("arca-fix-eval-{}.fix", std::process::id()));
std::fs::write(&path, FIX_PROGRAM).expect("write temporary Fix program");
Self { path }
}

fn path(&self) -> &Path {
&self.path
}
}

impl Drop for ProgramFile {
fn drop(&mut self) {
// Clean up the temp file
let _ = std::fs::remove_file(&self.path);
}
}

// Fail instead of hanging if the file-close handshake deadlocks
fn wait_for_output(mut child: Child) -> Output {
let start = Instant::now();

loop {
match child.try_wait().expect("poll Fix-on-Arca process") {
Some(_) => {
// Kill the child and collect both captured streams
return child
.wait_with_output()
.expect("collect Fix-on-Arca output");
}
None if start.elapsed() < DEADLOCK_LIMIT => thread::sleep(POLL_INTERVAL),
None => {
// Stop the hung VM before reporting diagnostics
let _ = child.kill();
let output = child
.wait_with_output()
.expect("collect timed-out Fix-on-Arca output");
panic!(
"fix eval did not complete its file-close handshake\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
}
}
}

#[test]
fn fix_eval_completes_and_prints_result() {
let program = ProgramFile::new();
// Resolve the program's addblob helper from the workspace target directory
let workspace = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("vmm crate is inside the workspace");

// Cargo supplies both binaries; VMM expects the guest ELF before guest argv
let child = Command::new(env!("CARGO_BIN_EXE_vmm"))
.arg(env!("CARGO_BIN_FILE_FIX_GUEST_fix"))
.arg("eval")
.arg(program.path())
.current_dir(workspace)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("launch Fix-on-Arca under the VMM");
Comment on lines +79 to +92

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you have Rust embed the guest binary into this one instead or reading it? This currently seems brittle.

@GregoryLi360 GregoryLi360 Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Cargo.toml I added

[dev-dependencies]
fix-guest = { package = "fix", path = "../fix", artifact = "bin:fix", target = "x86_64-unknown-none" }

so calling cargo test should build the Fix guest artifact first. I tried embedding it but couldn't really see a clean way of doing so. Also, I'm not quite sure of the benefit of that approach, is there something that could fail with how it is right now?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the main thing that concerns me is the path possibly being incorrect depending on the user's config; like their editor might invoke cargo test in the current file's directory rather than the project root. Right now the path is a compile-time literal but it's being evaluated at runtime which is odd. Also philosophically I'm hesitant to have a hard dependency that isn't visible to the compiler/runtime/OS...


// Decode captured bytes for assertions and failure diagnostics
let output = wait_for_output(child);
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);

// A successful status proves the guest reached kernel shutdown
assert!(
output.status.success(),
"fix eval failed with {}\nstdout:\n{stdout}\nstderr:\n{stderr}",
output.status,
);
// VMM forwards guest debug-console output to stderr
assert!(
stderr.contains(EXPECTED_OUTPUT),
"fix eval did not print the expected result\nstdout:\n{stdout}\nstderr:\n{stderr}",
);
}
Loading