use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
use testing_conventions::co_change::stale_sources;
use testing_conventions::colocated_test::Language;
use testing_conventions::patch_coverage::changed_lines;
struct TempRepo(PathBuf);
impl TempRepo {
fn new(slug: &str) -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let root = std::env::temp_dir().join(format!(
"tc-diff-scope-{}-{}-{}",
slug,
std::process::id(),
COUNTER.fetch_add(1, Ordering::Relaxed),
));
std::fs::create_dir_all(&root).unwrap();
git(&root, &["init", "-q"]);
git(&root, &["config", "user.email", "test@example.com"]);
git(&root, &["config", "user.name", "Test"]);
TempRepo(root)
}
fn write(&self, rel: &str, contents: &str) {
let path = self.0.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, contents).unwrap();
}
fn commit(&self, message: &str) {
git(&self.0, &["add", "-A"]);
git(
&self.0,
&["-c", "commit.gpgsign=false", "commit", "-q", "-m", message],
);
}
fn head(&self) -> String {
let out = Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&self.0)
.output()
.expect("git rev-parse should run");
assert!(out.status.success(), "git rev-parse failed");
String::from_utf8(out.stdout).unwrap().trim().to_string()
}
}
impl Drop for TempRepo {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn git(dir: &Path, args: &[&str]) {
let status = Command::new("git")
.args(args)
.current_dir(dir)
.status()
.expect("git should run");
assert!(status.success(), "git {args:?} failed");
}
#[test]
fn a_plus_plus_body_line_keeps_the_files_later_changed_lines_in_scope() {
let repo = TempRepo::new("plusplus");
repo.write("calc.py", "def calc(n):\n return n\n");
repo.commit("base");
let base = repo.head();
repo.write(
"calc.py",
"def calc(n):\n return n\n\n\n++ 1\n\n\ndef never_run():\n return 999\n",
);
repo.commit("append a ++ line and an untested helper");
let changed = changed_lines(&repo.0, &base).expect("diffing a readable repo succeeds");
let lines = changed
.get("calc.py")
.unwrap_or_else(|| panic!("calc.py must be in scope; got keys {:?}", changed.keys()));
assert!(
lines.contains(&8) && lines.contains(&9),
"the lines after the ++ line must stay in scope; got {lines:?}"
);
assert!(
!changed.contains_key("1"),
"the ++ body line must not create a phantom file key; got keys {:?}",
changed.keys()
);
}
#[test]
fn a_non_ascii_path_is_scoped_under_default_git_config() {
let repo = TempRepo::new("nonascii");
repo.write("src/föö.py", "def foo(n):\n return n\n");
repo.commit("base");
let base = repo.head();
repo.write("src/föö.py", "def foo(n):\n return n + 1\n");
repo.commit("edit the non-ASCII source");
let changed = changed_lines(&repo.0, &base).expect("diffing a readable repo succeeds");
assert!(
changed.contains_key("src/föö.py"),
"the non-ASCII path must decode to its real UTF-8 key; got keys {:?}",
changed.keys()
);
}
#[test]
fn co_change_scopes_a_non_ascii_modified_source() {
let repo = TempRepo::new("cochange-nonascii");
repo.write("src/föö.py", "def foo(n):\n return n\n");
repo.write(
"src/föö_test.py",
"from föö import foo\n\n\ndef test_foo():\n assert foo(1) == 1\n",
);
repo.commit("base");
let base = repo.head();
repo.write("src/föö.py", "def foo(n):\n return n + 1\n");
repo.commit("edit the non-ASCII source only");
let stale: Vec<String> = stale_sources(&repo.0, &base, Language::Python, &Default::default())
.expect("diffing a readable repo succeeds")
.iter()
.map(|p| p.to_string_lossy().replace('\\', "/"))
.collect();
assert_eq!(
stale,
vec!["src/föö.py".to_string()],
"the non-ASCII modified source with a stale test must be flagged"
);
}