use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
fn ev() -> Command {
Command::new(env!("CARGO_BIN_EXE_ev"))
}
fn repo() -> std::path::PathBuf {
static N: AtomicU64 = AtomicU64::new(0);
let p = std::env::temp_dir().join(format!(
"ev-migrate-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::Relaxed)
));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).unwrap();
assert!(ev()
.arg("init")
.current_dir(&p)
.output()
.unwrap()
.status
.success());
p
}
fn run(repo: &std::path::Path, args: &[&str]) -> std::process::Output {
ev().args(args).current_dir(repo).output().unwrap()
}
fn write_source(repo: &std::path::Path, kind: &str, name: &str, body: &str) -> String {
let path = repo.join(name);
std::fs::write(&path, body).unwrap();
format!("{kind}:{}", path.display())
}
fn tick_count(repo: &std::path::Path) -> usize {
std::fs::read_dir(repo.join(".evolving/ticks"))
.unwrap()
.filter(|e| e.as_ref().unwrap().path().is_file())
.count()
}
const TWO_ROUNDS: &str = "\
## R2289 restore-safety counter DB-backed
- rejected: Redis: would add a new infra dependency
## R2290 ship the cross-pod drain
";
#[test]
fn migrate_should_skip_every_record_when_run_twice() {
let r = repo();
let src = write_source(&r, "gitlog", "chat-room.md", TWO_ROUNDS);
let first = run(&r, &["migrate", "--source", &src, "--blame", "Wang Yu"]);
assert!(
first.status.success(),
"stderr: {}",
String::from_utf8_lossy(&first.stderr)
);
let after_first = tick_count(&r);
assert_eq!(after_first, 2, "first import writes both records");
let second = run(&r, &["migrate", "--source", &src, "--blame", "Wang Yu"]);
assert!(second.status.success());
assert_eq!(tick_count(&r), after_first, "a re-run writes no new ticks");
let out = String::from_utf8_lossy(&second.stdout);
assert!(
out.contains("imported 0") && out.contains("skipped 2"),
"summary was {out:?}"
);
}
#[test]
fn migrate_should_report_the_relinked_count_when_a_record_is_back_dated() {
let r = repo();
let later = write_source(
&r,
"gitlog",
"later.md",
"## R2290 ship the cross-pod drain\n",
);
assert!(
run(&r, &["migrate", "--source", &later, "--blame", "Wang Yu"])
.status
.success()
);
let both = write_source(
&r,
"gitlog",
"both.md",
"## R2289 restore-safety counter DB-backed\n## R2290 ship the cross-pod drain\n",
);
let out = run(&r, &["migrate", "--source", &both, "--blame", "Wang Yu"]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let s = String::from_utf8_lossy(&out.stdout);
assert!(
s.contains("imported 1") && s.contains("re-linked 1"),
"summary was {s:?}"
);
}
#[test]
fn migrate_reconcile_should_surface_a_source_only_ruling_as_a_gap() {
let r = repo();
let seed = write_source(
&r,
"gitlog",
"seed.md",
"## R2289 restore-safety counter DB-backed\n",
);
assert!(
run(&r, &["migrate", "--source", &seed, "--blame", "Wang Yu"])
.status
.success()
);
let against = write_source(
&r,
"gitlog",
"against.md",
"## R2289 restore-safety counter DB-backed\n## R9999 a ruling never captured\n",
);
let out = run(&r, &["migrate", "--reconcile", "--against", &against]);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let s = String::from_utf8_lossy(&out.stdout);
assert!(s.contains("in-both 1"), "summary was {s:?}");
assert!(s.contains("source-only 1"), "summary was {s:?}");
}
#[test]
fn migrate_should_require_a_blame_fallback_when_a_source_lacks_authors() {
let r = repo();
let src = write_source(&r, "gitlog", "no-authors.md", TWO_ROUNDS);
let out = run(&r, &["migrate", "--source", &src]);
assert!(out.status.success(), "a surfaced gap is not a hard failure");
assert_eq!(tick_count(&r), 0, "no tick is written without an author");
let s = String::from_utf8_lossy(&out.stdout);
assert!(
s.contains("source-only gap") || s.contains("source-only"),
"the gap must be surfaced; summary was {s:?}"
);
}
#[test]
fn migrate_dry_run_should_write_no_tick_when_asked_to_preview() {
let r = repo();
let src = write_source(&r, "gitlog", "chat-room.md", TWO_ROUNDS);
let out = run(
&r,
&[
"migrate",
"--source",
&src,
"--blame",
"Wang Yu",
"--dry-run",
],
);
assert!(out.status.success());
assert_eq!(tick_count(&r), 0, "--dry-run writes no ticks");
let s = String::from_utf8_lossy(&out.stdout);
assert!(
s.contains("imported 2"),
"preview should count both; was {s:?}"
);
}
#[test]
fn migrate_bind_check_should_print_a_harvested_check_when_a_selector_is_given() {
let r = repo();
let out = run(
&r,
&[
"migrate",
"--bind-check",
"pytest tests/test_invariant_no_redis.py",
"--on-platform",
"linux-ci",
"--triggered-by",
"pyproject.toml",
"--surface",
"pyproject-deps",
"--verified-at-sha",
"d308afac1b2c3d4e5f60718293a4b5c6d7e8f901",
],
);
assert!(
out.status.success(),
"stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let s = String::from_utf8_lossy(&out.stdout);
assert!(s.contains("harvested"), "output was {s:?}");
assert!(
s.contains("pytest tests/test_invariant_no_redis.py"),
"output was {s:?}"
);
}
#[test]
fn migrate_should_round_trip_clean_through_verify_when_records_are_imported() {
let r = repo();
let src = write_source(&r, "gitlog", "chat-room.md", TWO_ROUNDS);
assert!(
run(&r, &["migrate", "--source", &src, "--blame", "Wang Yu"])
.status
.success()
);
let v = run(&r, &["verify"]);
assert!(
v.status.success(),
"verify failed: {}",
String::from_utf8_lossy(&v.stderr)
);
}