#![cfg(target_os = "linux")]
use std::fs;
use serde_json::Value;
use super::{heddle, setup_repo};
#[test]
#[ignore = "requires Linux + FUSE + heddle built with --features mount"]
fn virtualized_thread_round_trip() {
let main = setup_repo("greet.txt", "hello from heddle");
let raw = heddle(
&[
"--json",
"start",
"feature/mount-demo",
"--workspace",
"light",
"--no-daemon",
],
Some(main.path()),
)
.expect("virtualized start succeeded; if this errors with `requires Linux + heddle built with --features mount` the test was run without --features mount");
let out: Value = serde_json::from_str(&raw).unwrap();
let mount_path = out
.get("thread")
.and_then(|t| t.get("path"))
.and_then(Value::as_str)
.expect("virtualized thread output should include the mount path under thread.path")
.to_string();
assert!(
mount_path.contains("-heddle-mounts/"),
"mount path should sit under the conventional .<repo>-heddle-mounts/ sibling \
(got {mount_path:?})"
);
let observed = fs::read_to_string(format!("{mount_path}/greet.txt"))
.expect("read through FUSE mount succeeded");
assert_eq!(observed, "hello from heddle");
heddle(&["thread", "drop", "feature/mount-demo"], Some(main.path()))
.expect("thread drop after virtualized mount");
assert!(
fs::read_to_string(format!("{mount_path}/greet.txt")).is_err(),
"after drop, the file should no longer be readable through the mount"
);
}
fn capture_short(cwd: &std::path::Path, msg: &str) -> String {
let out = heddle(&["--json", "capture", "-m", msg], Some(cwd)).expect("snapshot succeeded");
let v: Value = serde_json::from_str(&out).expect("snapshot --json is valid JSON");
v.get("change_id")
.and_then(Value::as_str)
.expect("snapshot output exposes change_id")
.to_string()
}
fn mount_path_from_start(raw: &str) -> String {
let out: Value = serde_json::from_str(raw).expect("start --json output");
out.get("thread")
.and_then(|t| t.get("path"))
.and_then(Value::as_str)
.expect("virtualized thread output should include thread.path")
.to_string()
}
#[test]
fn virtualized_from_other_thread_head_serves_that_threads_tip() {
let main = setup_repo("greet.txt", "S1");
fs::write(main.path().join("greet.txt"), "S2").unwrap();
let _s2 = capture_short(main.path(), "S2 in main");
let alpha_dir = tempfile::TempDir::new().unwrap();
heddle(
&[
"start",
"alpha",
"--workspace",
"visible",
"--from",
"HEAD~1",
"--path",
alpha_dir.path().to_str().unwrap(),
],
Some(main.path()),
)
.expect("start alpha from HEAD~1");
let alpha_initial = fs::read_to_string(alpha_dir.path().join("greet.txt")).unwrap();
assert_eq!(alpha_initial, "S1", "alpha should hydrate from S1");
fs::write(alpha_dir.path().join("greet.txt"), "S3").unwrap();
let _s3 = capture_short(alpha_dir.path(), "S3 on alpha");
let raw = heddle(
&[
"--json",
"start",
"beta",
"--workspace",
"light",
"--from",
"alpha",
"--no-daemon",
],
Some(main.path()),
)
.expect("start beta --from alpha succeeded");
let mount_path = mount_path_from_start(&raw);
let observed = fs::read_to_string(format!("{mount_path}/greet.txt"))
.expect("read through beta's FUSE mount");
assert_eq!(
observed, "S3",
"beta --from alpha must serve alpha's tip (S3), not S1 or S2"
);
heddle(&["thread", "drop", "beta"], Some(main.path())).expect("drop beta to release the mount");
}
#[test]
fn virtualized_from_short_change_id_serves_that_state() {
let main = setup_repo("greet.txt", "S1");
let log: Value = serde_json::from_str(
&heddle(&["--json", "log", "main", "-n", "1"], Some(main.path())).expect("log main"),
)
.unwrap();
let s1_short = log["states"][0]["change_id"]
.as_str()
.expect("log exposes change_id")
.to_string();
fs::write(main.path().join("greet.txt"), "S2").unwrap();
let _s2 = capture_short(main.path(), "S2 in main");
let raw = heddle(
&[
"--json",
"start",
"gamma",
"--workspace",
"light",
"--from",
&s1_short,
"--no-daemon",
],
Some(main.path()),
)
.expect("start gamma --from <S1 short> succeeded");
let mount_path = mount_path_from_start(&raw);
let observed = fs::read_to_string(format!("{mount_path}/greet.txt"))
.expect("read through gamma's FUSE mount");
assert_eq!(
observed, "S1",
"gamma --from <S1 short> must serve S1, not S2"
);
heddle(&["thread", "drop", "gamma"], Some(main.path()))
.expect("drop gamma to release the mount");
}
#[test]
fn virtualized_from_head_tilde_serves_parent_state() {
let main = setup_repo("greet.txt", "S1");
fs::write(main.path().join("greet.txt"), "S2").unwrap();
let _s2 = capture_short(main.path(), "S2 in main");
let raw = heddle(
&[
"--json",
"start",
"delta",
"--workspace",
"light",
"--from",
"HEAD~1",
"--no-daemon",
],
Some(main.path()),
)
.expect("start delta --from HEAD~1 succeeded");
let mount_path = mount_path_from_start(&raw);
let observed = fs::read_to_string(format!("{mount_path}/greet.txt"))
.expect("read through delta's FUSE mount");
assert_eq!(
observed, "S1",
"delta --from HEAD~1 must serve S1 (the parent of HEAD), not S2"
);
heddle(&["thread", "drop", "delta"], Some(main.path()))
.expect("drop delta to release the mount");
}
fn heddled_endpoint_path(repo_root: &std::path::Path) -> std::path::PathBuf {
repo_root
.join(".heddle")
.join("state")
.join("heddled.endpoint.json")
}
#[test]
#[ignore = "requires Linux + FUSE + heddle built with --features mount"]
fn default_uses_daemon_when_no_flag() {
let main = setup_repo("greet.txt", "default-daemon");
let endpoint = heddled_endpoint_path(main.path());
assert!(
!endpoint.exists(),
"fresh repo should not have a daemon endpoint before any virtualized start"
);
heddle(
&[
"--json",
"start",
"feature/default-daemon",
"--workspace",
"light",
],
Some(main.path()),
)
.expect("default virtualized start should spawn the daemon");
assert!(
endpoint.exists(),
"default `--workspace light` (no flag) must hand the mount \
to `heddled`, leaving the endpoint file at {} as evidence",
endpoint.display(),
);
heddle(
&["thread", "drop", "feature/default-daemon"],
Some(main.path()),
)
.expect("drop daemon-owned thread");
}
#[test]
#[ignore = "requires Linux + FUSE + heddle built with --features mount"]
fn no_daemon_flag_uses_in_process() {
let main = setup_repo("greet.txt", "in-process-only");
let endpoint = heddled_endpoint_path(main.path());
assert!(
!endpoint.exists(),
"fresh repo should not have a daemon endpoint before any virtualized start"
);
heddle(
&[
"--json",
"start",
"feature/no-daemon",
"--workspace",
"light",
"--no-daemon",
],
Some(main.path()),
)
.expect("--no-daemon virtualized start should succeed in-process");
assert!(
!endpoint.exists(),
"explicit `--no-daemon` must NOT spawn the daemon; \
endpoint file at {} should not exist",
endpoint.display(),
);
heddle(&["thread", "drop", "feature/no-daemon"], Some(main.path()))
.expect("drop in-process virtualized thread");
}