use std::path::{Path, PathBuf};
use std::process::Command;
#[test]
fn build_dirty_is_exactly_true_or_false() {
let dirty = env!("DEVFLOW_BUILD_DIRTY");
assert!(
dirty == "true" || dirty == "false",
"DEVFLOW_BUILD_DIRTY must be exactly \"true\" or \"false\", got {dirty:?}"
);
}
#[test]
fn build_commit_is_empty_or_a_full_hex_sha() {
let commit = env!("DEVFLOW_BUILD_COMMIT");
assert!(
commit.is_empty() || (commit.len() == 40 && commit.chars().all(|c| c.is_ascii_hexdigit())),
"DEVFLOW_BUILD_COMMIT must be empty (no-git build, D-20) or a \
40-char hex SHA, got {commit:?}"
);
}
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("crates/devflow-cli has a workspace root two levels up")
.to_path_buf()
}
fn run(dir: &Path, program: &str, args: &[&str]) {
let output = Command::new(program)
.args(args)
.current_dir(dir)
.output()
.unwrap_or_else(|e| panic!("failed to run {program} {args:?}: {e}"));
assert!(
output.status.success(),
"{program} {args:?} failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
fn copy_tracked_worktree_into(dest: &Path) {
let root = workspace_root();
let output = Command::new("git")
.args(["ls-files", "-z"])
.current_dir(&root)
.output()
.expect("git ls-files");
assert!(output.status.success(), "git ls-files failed");
for rel in output.stdout.split(|&b| b == 0).filter(|s| !s.is_empty()) {
let rel = std::str::from_utf8(rel).expect("tracked path is utf8");
let src = root.join(rel);
let dst = dest.join(rel);
if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent).unwrap_or_else(|e| panic!("mkdir for {rel}: {e}"));
}
std::fs::copy(&src, &dst).unwrap_or_else(|e| panic!("copy {rel}: {e}"));
}
}
fn read_cached_dirty_flag(target_dir: &Path) -> String {
let build_dir = target_dir.join("debug/build");
let entries = std::fs::read_dir(&build_dir)
.unwrap_or_else(|e| panic!("reading {}: {e}", build_dir.display()));
let output_path = entries
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|n| n.starts_with("devflow-") && !n.starts_with("devflow-core-"))
})
.map(|dir| dir.join("output"))
.find(|candidate| candidate.exists())
.unwrap_or_else(|| {
panic!(
"no devflow-*/output build-script cache found under {}",
build_dir.display()
)
});
let contents = std::fs::read_to_string(&output_path)
.unwrap_or_else(|e| panic!("reading {}: {e}", output_path.display()));
contents
.lines()
.find_map(|line| line.strip_prefix("cargo:rustc-env=DEVFLOW_BUILD_DIRTY="))
.unwrap_or_else(|| panic!("no DEVFLOW_BUILD_DIRTY line in {}", output_path.display()))
.to_string()
}
#[test]
fn build_dirty_flips_false_to_true_across_a_working_tree_edit_after_rebuild() {
let clone_dir = tempfile::tempdir().expect("tempdir for synthetic checkout");
let target_dir = tempfile::tempdir().expect("tempdir for target-dir");
copy_tracked_worktree_into(clone_dir.path());
run(clone_dir.path(), "git", &["init", "-q"]);
run(
clone_dir.path(),
"git",
&["config", "user.email", "cr02-repro@example.com"],
);
run(
clone_dir.path(),
"git",
&["config", "user.name", "cr02-repro"],
);
run(
clone_dir.path(),
"git",
&["config", "commit.gpgsign", "false"],
);
run(clone_dir.path(), "git", &["add", "-A"]);
run(clone_dir.path(), "git", &["commit", "-q", "-m", "snapshot"]);
run(clone_dir.path(), "git", &["pack-refs", "--all"]);
let target_dir_str = target_dir.path().to_str().expect("target dir is utf8");
run(
clone_dir.path(),
"cargo",
&["build", "-p", "devflow", "--target-dir", target_dir_str],
);
let before = read_cached_dirty_flag(target_dir.path());
assert_eq!(
before, "false",
"a freshly snapshotted, committed tree's first build must be clean"
);
let edited = clone_dir.path().join("crates/devflow-cli/src/main.rs");
let mut contents = std::fs::read_to_string(&edited).expect("read main.rs");
contents.push_str("\n// CR-02 regression test edit\n");
std::fs::write(&edited, contents).expect("write main.rs");
run(
clone_dir.path(),
"cargo",
&["build", "-p", "devflow", "--target-dir", target_dir_str],
);
let after = read_cached_dirty_flag(target_dir.path());
assert_eq!(
after, "true",
"build.rs must re-run on every `cargo build` so DEVFLOW_BUILD_DIRTY \
reflects the working tree's CURRENT state — if this reads \
\"false\", build.rs's rerun trigger went stale again (CR-02)"
);
}