about summary refs log tree commit diff
diff options
context:
space:
mode:
authorJakub Beránek <berykubik@gmail.com>2025-03-19 10:36:48 +0100
committerJakub Beránek <berykubik@gmail.com>2025-04-20 09:35:42 +0200
commitf414afbf9f6928038fbf00c8a08ded93269f12d3 (patch)
treebd04ee2c7faf68db1f496f7a012f393f3347a509
parent432c4a80e9b03505f73e85828bd152a5efc2f98b (diff)
downloadrust-f414afbf9f6928038fbf00c8a08ded93269f12d3.tar.gz
rust-f414afbf9f6928038fbf00c8a08ded93269f12d3.zip
Move freshness test to bootstrap
-rw-r--r--src/bootstrap/Cargo.lock83
-rw-r--r--src/bootstrap/Cargo.toml1
-rw-r--r--src/bootstrap/src/core/config/tests.rs170
-rw-r--r--src/bootstrap/src/utils/mod.rs2
-rw-r--r--src/bootstrap/src/utils/tests/git.rs143
-rw-r--r--src/bootstrap/src/utils/tests/mod.rs1
-rw-r--r--src/build_helper/Cargo.toml3
-rw-r--r--src/build_helper/src/git.rs4
-rw-r--r--src/build_helper/src/git/tests.rs305
9 files changed, 396 insertions, 316 deletions
diff --git a/src/bootstrap/Cargo.lock b/src/bootstrap/Cargo.lock
index beb9ea93989..d415668f54a 100644
--- a/src/bootstrap/Cargo.lock
+++ b/src/bootstrap/Cargo.lock
@@ -56,6 +56,7 @@ dependencies = [
  "sha2",
  "sysinfo",
  "tar",
+ "tempfile",
  "termcolor",
  "toml",
  "tracing",
@@ -234,13 +235,19 @@ dependencies = [
 ]
 
 [[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
 name = "fd-lock"
 version = "4.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947"
 dependencies = [
  "cfg-if",
- "rustix",
+ "rustix 0.38.40",
  "windows-sys 0.52.0",
 ]
 
@@ -267,6 +274,18 @@ dependencies = [
 ]
 
 [[package]]
+name = "getrandom"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi",
+]
+
+[[package]]
 name = "globset"
 version = "0.4.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -356,6 +375,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
 
 [[package]]
+name = "linux-raw-sys"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413"
+
+[[package]]
 name = "log"
 version = "0.4.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -487,6 +512,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "r-efi"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
+
+[[package]]
 name = "redox_syscall"
 version = "0.5.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -548,11 +579,24 @@ dependencies = [
  "bitflags",
  "errno",
  "libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.4.14",
  "windows-sys 0.52.0",
 ]
 
 [[package]]
+name = "rustix"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.9.3",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
 name = "ryu"
 version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -679,6 +723,19 @@ dependencies = [
 ]
 
 [[package]]
+name = "tempfile"
+version = "3.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600"
+dependencies = [
+ "fastrand",
+ "getrandom",
+ "once_cell",
+ "rustix 1.0.2",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
 name = "termcolor"
 version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -825,6 +882,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
 name = "winapi"
 version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -991,14 +1057,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
 name = "xattr"
 version = "1.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f"
 dependencies = [
  "libc",
- "linux-raw-sys",
- "rustix",
+ "linux-raw-sys 0.4.14",
+ "rustix 0.38.40",
 ]
 
 [[package]]
diff --git a/src/bootstrap/Cargo.toml b/src/bootstrap/Cargo.toml
index 23aa87a7407..712a9b04c1a 100644
--- a/src/bootstrap/Cargo.toml
+++ b/src/bootstrap/Cargo.toml
@@ -83,6 +83,7 @@ features = [
 
 [dev-dependencies]
 pretty_assertions = "1.4"
+tempfile = "3.15.0"
 
 # We care a lot about bootstrap's compile times, so don't include debuginfo for
 # dependencies, only bootstrap itself.
diff --git a/src/bootstrap/src/core/config/tests.rs b/src/bootstrap/src/core/config/tests.rs
index affabd49720..04e328c8b1b 100644
--- a/src/bootstrap/src/core/config/tests.rs
+++ b/src/bootstrap/src/core/config/tests.rs
@@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
 use std::{env, fs};
 
 use build_helper::ci::CiEnv;
+use build_helper::git::PathFreshness;
 use clap::CommandFactory;
 use serde::Deserialize;
 
@@ -15,6 +16,7 @@ use crate::core::build_steps::clippy::{LintConfig, get_clippy_rules_in_order};
 use crate::core::build_steps::llvm;
 use crate::core::build_steps::llvm::LLVM_INVALIDATION_PATHS;
 use crate::core::config::{LldMode, Target, TargetSelection, TomlConfig};
+use crate::utils::tests::git::git_test;
 
 pub(crate) fn parse(config: &str) -> Config {
     Config::parse_inner(
@@ -744,3 +746,171 @@ fn test_include_precedence_over_profile() {
     // override profile settings, so we expect this to be "dev" here.
     assert_eq!(config.channel, "dev");
 }
+
+#[test]
+fn test_pr_ci_unchanged_anywhere() {
+    git_test(|ctx| {
+        let sha = ctx.create_upstream_merge(&["a"]);
+        ctx.create_nonupstream_merge(&["b"]);
+        let src = ctx.check_modifications(&["c"], CiEnv::GitHubActions);
+        assert_eq!(src, PathFreshness::LastModifiedUpstream { upstream: sha });
+    });
+}
+
+#[test]
+fn test_pr_ci_changed_in_pr() {
+    git_test(|ctx| {
+        let sha = ctx.create_upstream_merge(&["a"]);
+        ctx.create_nonupstream_merge(&["b"]);
+        let src = ctx.check_modifications(&["b"], CiEnv::GitHubActions);
+        assert_eq!(src, PathFreshness::HasLocalModifications { upstream: sha });
+    });
+}
+
+#[test]
+fn test_auto_ci_unchanged_anywhere_select_parent() {
+    git_test(|ctx| {
+        let sha = ctx.create_upstream_merge(&["a"]);
+        ctx.create_upstream_merge(&["b"]);
+        let src = ctx.check_modifications(&["c"], CiEnv::GitHubActions);
+        assert_eq!(src, PathFreshness::LastModifiedUpstream { upstream: sha });
+    });
+}
+
+#[test]
+fn test_auto_ci_changed_in_pr() {
+    git_test(|ctx| {
+        let sha = ctx.create_upstream_merge(&["a"]);
+        ctx.create_upstream_merge(&["b", "c"]);
+        let src = ctx.check_modifications(&["c", "d"], CiEnv::GitHubActions);
+        assert_eq!(src, PathFreshness::HasLocalModifications { upstream: sha });
+    });
+}
+
+#[test]
+fn test_local_uncommitted_modifications() {
+    git_test(|ctx| {
+        let sha = ctx.create_upstream_merge(&["a"]);
+        ctx.create_branch("feature");
+        ctx.modify("a");
+
+        assert_eq!(
+            ctx.check_modifications(&["a", "d"], CiEnv::None),
+            PathFreshness::HasLocalModifications { upstream: sha }
+        );
+    });
+}
+
+#[test]
+fn test_local_committed_modifications() {
+    git_test(|ctx| {
+        let sha = ctx.create_upstream_merge(&["a"]);
+        ctx.create_upstream_merge(&["b", "c"]);
+        ctx.create_branch("feature");
+        ctx.modify("x");
+        ctx.commit();
+        ctx.modify("a");
+        ctx.commit();
+
+        assert_eq!(
+            ctx.check_modifications(&["a", "d"], CiEnv::None),
+            PathFreshness::HasLocalModifications { upstream: sha }
+        );
+    });
+}
+
+#[test]
+fn test_local_committed_modifications_subdirectory() {
+    git_test(|ctx| {
+        let sha = ctx.create_upstream_merge(&["a/b/c"]);
+        ctx.create_upstream_merge(&["b", "c"]);
+        ctx.create_branch("feature");
+        ctx.modify("a/b/d");
+        ctx.commit();
+
+        assert_eq!(
+            ctx.check_modifications(&["a/b"], CiEnv::None),
+            PathFreshness::HasLocalModifications { upstream: sha }
+        );
+    });
+}
+
+#[test]
+fn test_local_changes_in_head_upstream() {
+    git_test(|ctx| {
+        // We want to resolve to the upstream commit that made modifications to a,
+        // even if it is currently HEAD
+        let sha = ctx.create_upstream_merge(&["a"]);
+        assert_eq!(
+            ctx.check_modifications(&["a", "d"], CiEnv::None),
+            PathFreshness::LastModifiedUpstream { upstream: sha }
+        );
+    });
+}
+
+#[test]
+fn test_local_changes_in_previous_upstream() {
+    git_test(|ctx| {
+        // We want to resolve to this commit, which modified a
+        let sha = ctx.create_upstream_merge(&["a", "e"]);
+        // Not to this commit, which is the latest upstream commit
+        ctx.create_upstream_merge(&["b", "c"]);
+        ctx.create_branch("feature");
+        ctx.modify("d");
+        ctx.commit();
+        assert_eq!(
+            ctx.check_modifications(&["a"], CiEnv::None),
+            PathFreshness::LastModifiedUpstream { upstream: sha }
+        );
+    });
+}
+
+#[test]
+fn test_local_no_upstream_commit_with_changes() {
+    git_test(|ctx| {
+        ctx.create_upstream_merge(&["a", "e"]);
+        ctx.create_upstream_merge(&["a", "e"]);
+        // We want to fall back to this commit, because there are no commits
+        // that modified `x`.
+        let sha = ctx.create_upstream_merge(&["a", "e"]);
+        ctx.create_branch("feature");
+        ctx.modify("d");
+        ctx.commit();
+        assert_eq!(
+            ctx.check_modifications(&["x"], CiEnv::None),
+            PathFreshness::LastModifiedUpstream { upstream: sha }
+        );
+    });
+}
+
+#[test]
+fn test_local_no_upstream_commit() {
+    git_test(|ctx| {
+        let src = ctx.check_modifications(&["c", "d"], CiEnv::None);
+        assert_eq!(src, PathFreshness::MissingUpstream);
+    });
+}
+
+#[test]
+fn test_local_changes_negative_path() {
+    git_test(|ctx| {
+        let upstream = ctx.create_upstream_merge(&["a"]);
+        ctx.create_branch("feature");
+        ctx.modify("b");
+        ctx.modify("d");
+        ctx.commit();
+
+        assert_eq!(
+            ctx.check_modifications(&[":!b", ":!d"], CiEnv::None),
+            PathFreshness::LastModifiedUpstream { upstream: upstream.clone() }
+        );
+        assert_eq!(
+            ctx.check_modifications(&[":!c"], CiEnv::None),
+            PathFreshness::HasLocalModifications { upstream: upstream.clone() }
+        );
+        assert_eq!(
+            ctx.check_modifications(&[":!d", ":!x"], CiEnv::None),
+            PathFreshness::HasLocalModifications { upstream }
+        );
+    });
+}
diff --git a/src/bootstrap/src/utils/mod.rs b/src/bootstrap/src/utils/mod.rs
index caef8ce3088..169fcec303e 100644
--- a/src/bootstrap/src/utils/mod.rs
+++ b/src/bootstrap/src/utils/mod.rs
@@ -20,4 +20,4 @@ pub(crate) mod tracing;
 pub(crate) mod metrics;
 
 #[cfg(test)]
-mod tests;
+pub(crate) mod tests;
diff --git a/src/bootstrap/src/utils/tests/git.rs b/src/bootstrap/src/utils/tests/git.rs
new file mode 100644
index 00000000000..59581dd9c37
--- /dev/null
+++ b/src/bootstrap/src/utils/tests/git.rs
@@ -0,0 +1,143 @@
+use std::ffi::OsStr;
+use std::fs::OpenOptions;
+use std::path::Path;
+use std::process::Command;
+
+use build_helper::ci::CiEnv;
+use build_helper::git::{GitConfig, PathFreshness, check_path_modifications};
+
+pub struct GitCtx {
+    dir: tempfile::TempDir,
+    pub git_repo: String,
+    pub nightly_branch: String,
+    pub merge_bot_email: String,
+}
+
+impl GitCtx {
+    fn new() -> Self {
+        let dir = tempfile::TempDir::new().unwrap();
+        let ctx = Self {
+            dir,
+            git_repo: "rust-lang/rust".to_string(),
+            nightly_branch: "nightly".to_string(),
+            merge_bot_email: "Merge bot <merge-bot@rust-lang.org>".to_string(),
+        };
+        ctx.run_git(&["init"]);
+        ctx.run_git(&["config", "user.name", "Tester"]);
+        ctx.run_git(&["config", "user.email", "tester@rust-lang.org"]);
+        ctx.modify("README.md");
+        ctx.commit();
+        ctx.run_git(&["branch", "-m", "main"]);
+        ctx
+    }
+
+    pub fn get_path(&self) -> &Path {
+        self.dir.path()
+    }
+
+    pub fn check_modifications(&self, target_paths: &[&str], ci_env: CiEnv) -> PathFreshness {
+        check_path_modifications(Some(self.dir.path()), &self.git_config(), target_paths, ci_env)
+            .unwrap()
+    }
+
+    pub fn create_upstream_merge(&self, modified_files: &[&str]) -> String {
+        self.create_branch_and_merge("previous-pr", modified_files, &self.merge_bot_email)
+    }
+
+    pub fn create_nonupstream_merge(&self, modified_files: &[&str]) -> String {
+        self.create_branch_and_merge("pr", modified_files, "Tester <tester@rust-lang.org>")
+    }
+
+    pub fn create_branch_and_merge(
+        &self,
+        branch: &str,
+        modified_files: &[&str],
+        author: &str,
+    ) -> String {
+        self.create_branch(branch);
+        for file in modified_files {
+            self.modify(file);
+        }
+        self.commit();
+        self.switch_to_branch("main");
+        self.merge(branch, author);
+        self.run_git(&["branch", "-d", branch]);
+        self.get_current_commit()
+    }
+
+    pub fn get_current_commit(&self) -> String {
+        self.run_git(&["rev-parse", "HEAD"])
+    }
+
+    pub fn merge(&self, branch: &str, author: &str) {
+        self.run_git(&["merge", "--no-commit", "--no-ff", branch]);
+        self.run_git(&[
+            "commit".to_string(),
+            "-m".to_string(),
+            "Merge of {branch}".to_string(),
+            "--author".to_string(),
+            author.to_string(),
+        ]);
+    }
+
+    pub fn modify(&self, path: &str) {
+        use std::io::Write;
+
+        let path = self.dir.path().join(path);
+        std::fs::create_dir_all(&path.parent().unwrap()).unwrap();
+
+        let mut file = OpenOptions::new().create(true).append(true).open(path).unwrap();
+        writeln!(file, "line").unwrap();
+    }
+
+    pub fn commit(&self) -> String {
+        self.run_git(&["add", "."]);
+        self.run_git(&["commit", "-m", "commit message"]);
+        self.get_current_commit()
+    }
+
+    pub fn switch_to_branch(&self, name: &str) {
+        self.run_git(&["switch", name]);
+    }
+
+    /// Creates a branch and switches to it.
+    pub fn create_branch(&self, name: &str) {
+        self.run_git(&["checkout", "-b", name]);
+    }
+
+    pub fn run_git<S: AsRef<OsStr>>(&self, args: &[S]) -> String {
+        let mut cmd = self.git_cmd();
+        cmd.args(args);
+        eprintln!("Running {cmd:?}");
+        let output = cmd.output().unwrap();
+        let stdout = String::from_utf8(output.stdout).unwrap().trim().to_string();
+        let stderr = String::from_utf8(output.stderr).unwrap().trim().to_string();
+        if !output.status.success() {
+            panic!("Git command `{cmd:?}` failed\nStdout\n{stdout}\nStderr\n{stderr}");
+        }
+        stdout
+    }
+
+    fn git_cmd(&self) -> Command {
+        let mut cmd = Command::new("git");
+        cmd.current_dir(&self.dir);
+        cmd
+    }
+
+    fn git_config(&self) -> GitConfig<'_> {
+        GitConfig {
+            git_repository: &self.git_repo,
+            nightly_branch: &self.nightly_branch,
+            git_merge_commit_email: &self.merge_bot_email,
+        }
+    }
+}
+
+/// Run an end-to-end test that allows testing git logic.
+pub fn git_test<F>(test_fn: F)
+where
+    F: FnOnce(&mut GitCtx),
+{
+    let mut ctx = GitCtx::new();
+    test_fn(&mut ctx);
+}
diff --git a/src/bootstrap/src/utils/tests/mod.rs b/src/bootstrap/src/utils/tests/mod.rs
index 0791f7a6e20..73d55db994c 100644
--- a/src/bootstrap/src/utils/tests/mod.rs
+++ b/src/bootstrap/src/utils/tests/mod.rs
@@ -1 +1,2 @@
+pub mod git;
 mod shared_helpers_tests;
diff --git a/src/build_helper/Cargo.toml b/src/build_helper/Cargo.toml
index bdead0a36de..66894e1abc4 100644
--- a/src/build_helper/Cargo.toml
+++ b/src/build_helper/Cargo.toml
@@ -8,6 +8,3 @@ edition = "2021"
 [dependencies]
 serde = "1"
 serde_derive = "1"
-
-[dev-dependencies]
-tempfile = "3.19"
diff --git a/src/build_helper/src/git.rs b/src/build_helper/src/git.rs
index e40e5db3028..8b019c5929a 100644
--- a/src/build_helper/src/git.rs
+++ b/src/build_helper/src/git.rs
@@ -1,11 +1,9 @@
-#[cfg(test)]
-mod tests;
-
 use std::path::Path;
 use std::process::{Command, Stdio};
 
 use crate::ci::CiEnv;
 
+#[derive(Debug)]
 pub struct GitConfig<'a> {
     pub git_repository: &'a str,
     pub nightly_branch: &'a str,
diff --git a/src/build_helper/src/git/tests.rs b/src/build_helper/src/git/tests.rs
deleted file mode 100644
index 43bffb8befa..00000000000
--- a/src/build_helper/src/git/tests.rs
+++ /dev/null
@@ -1,305 +0,0 @@
-use std::ffi::OsStr;
-use std::fs::OpenOptions;
-use std::process::Command;
-
-use crate::ci::CiEnv;
-use crate::git::{GitConfig, PathFreshness, check_path_modifications};
-
-#[test]
-fn test_pr_ci_unchanged_anywhere() {
-    git_test(|ctx| {
-        let sha = ctx.create_upstream_merge(&["a"]);
-        ctx.create_nonupstream_merge(&["b"]);
-        let src = ctx.check_modifications(&["c"], CiEnv::GitHubActions);
-        assert_eq!(src, PathFreshness::LastModifiedUpstream { upstream: sha });
-    });
-}
-
-#[test]
-fn test_pr_ci_changed_in_pr() {
-    git_test(|ctx| {
-        let sha = ctx.create_upstream_merge(&["a"]);
-        ctx.create_nonupstream_merge(&["b"]);
-        let src = ctx.check_modifications(&["b"], CiEnv::GitHubActions);
-        assert_eq!(src, PathFreshness::HasLocalModifications { upstream: sha });
-    });
-}
-
-#[test]
-fn test_auto_ci_unchanged_anywhere_select_parent() {
-    git_test(|ctx| {
-        let sha = ctx.create_upstream_merge(&["a"]);
-        ctx.create_upstream_merge(&["b"]);
-        let src = ctx.check_modifications(&["c"], CiEnv::GitHubActions);
-        assert_eq!(src, PathFreshness::LastModifiedUpstream { upstream: sha });
-    });
-}
-
-#[test]
-fn test_auto_ci_changed_in_pr() {
-    git_test(|ctx| {
-        let sha = ctx.create_upstream_merge(&["a"]);
-        ctx.create_upstream_merge(&["b", "c"]);
-        let src = ctx.check_modifications(&["c", "d"], CiEnv::GitHubActions);
-        assert_eq!(src, PathFreshness::HasLocalModifications { upstream: sha });
-    });
-}
-
-#[test]
-fn test_local_uncommitted_modifications() {
-    git_test(|ctx| {
-        let sha = ctx.create_upstream_merge(&["a"]);
-        ctx.create_branch("feature");
-        ctx.modify("a");
-
-        assert_eq!(
-            ctx.check_modifications(&["a", "d"], CiEnv::None),
-            PathFreshness::HasLocalModifications { upstream: sha }
-        );
-    });
-}
-
-#[test]
-fn test_local_committed_modifications() {
-    git_test(|ctx| {
-        let sha = ctx.create_upstream_merge(&["a"]);
-        ctx.create_upstream_merge(&["b", "c"]);
-        ctx.create_branch("feature");
-        ctx.modify("x");
-        ctx.commit();
-        ctx.modify("a");
-        ctx.commit();
-
-        assert_eq!(
-            ctx.check_modifications(&["a", "d"], CiEnv::None),
-            PathFreshness::HasLocalModifications { upstream: sha }
-        );
-    });
-}
-
-#[test]
-fn test_local_committed_modifications_subdirectory() {
-    git_test(|ctx| {
-        let sha = ctx.create_upstream_merge(&["a/b/c"]);
-        ctx.create_upstream_merge(&["b", "c"]);
-        ctx.create_branch("feature");
-        ctx.modify("a/b/d");
-        ctx.commit();
-
-        assert_eq!(
-            ctx.check_modifications(&["a/b"], CiEnv::None),
-            PathFreshness::HasLocalModifications { upstream: sha }
-        );
-    });
-}
-
-#[test]
-fn test_local_changes_in_head_upstream() {
-    git_test(|ctx| {
-        // We want to resolve to the upstream commit that made modifications to a,
-        // even if it is currently HEAD
-        let sha = ctx.create_upstream_merge(&["a"]);
-        assert_eq!(
-            ctx.check_modifications(&["a", "d"], CiEnv::None),
-            PathFreshness::LastModifiedUpstream { upstream: sha }
-        );
-    });
-}
-
-#[test]
-fn test_local_changes_in_previous_upstream() {
-    git_test(|ctx| {
-        // We want to resolve to this commit, which modified a
-        let sha = ctx.create_upstream_merge(&["a", "e"]);
-        // Not to this commit, which is the latest upstream commit
-        ctx.create_upstream_merge(&["b", "c"]);
-        ctx.create_branch("feature");
-        ctx.modify("d");
-        ctx.commit();
-        assert_eq!(
-            ctx.check_modifications(&["a"], CiEnv::None),
-            PathFreshness::LastModifiedUpstream { upstream: sha }
-        );
-    });
-}
-
-#[test]
-fn test_local_no_upstream_commit_with_changes() {
-    git_test(|ctx| {
-        ctx.create_upstream_merge(&["a", "e"]);
-        ctx.create_upstream_merge(&["a", "e"]);
-        // We want to fall back to this commit, because there are no commits
-        // that modified `x`.
-        let sha = ctx.create_upstream_merge(&["a", "e"]);
-        ctx.create_branch("feature");
-        ctx.modify("d");
-        ctx.commit();
-        assert_eq!(
-            ctx.check_modifications(&["x"], CiEnv::None),
-            PathFreshness::LastModifiedUpstream { upstream: sha }
-        );
-    });
-}
-
-#[test]
-fn test_local_no_upstream_commit() {
-    git_test(|ctx| {
-        let src = ctx.check_modifications(&["c", "d"], CiEnv::None);
-        assert_eq!(src, PathFreshness::MissingUpstream);
-    });
-}
-
-#[test]
-fn test_local_changes_negative_path() {
-    git_test(|ctx| {
-        let upstream = ctx.create_upstream_merge(&["a"]);
-        ctx.create_branch("feature");
-        ctx.modify("b");
-        ctx.modify("d");
-        ctx.commit();
-
-        assert_eq!(
-            ctx.check_modifications(&[":!b", ":!d"], CiEnv::None),
-            PathFreshness::LastModifiedUpstream { upstream: upstream.clone() }
-        );
-        assert_eq!(
-            ctx.check_modifications(&[":!c"], CiEnv::None),
-            PathFreshness::HasLocalModifications { upstream: upstream.clone() }
-        );
-        assert_eq!(
-            ctx.check_modifications(&[":!d", ":!x"], CiEnv::None),
-            PathFreshness::HasLocalModifications { upstream }
-        );
-    });
-}
-
-struct GitCtx {
-    dir: tempfile::TempDir,
-    git_repo: String,
-    nightly_branch: String,
-    merge_bot_email: String,
-}
-
-impl GitCtx {
-    fn new() -> Self {
-        let dir = tempfile::TempDir::new().unwrap();
-        let ctx = Self {
-            dir,
-            git_repo: "rust-lang/rust".to_string(),
-            nightly_branch: "nightly".to_string(),
-            merge_bot_email: "Merge bot <merge-bot@rust-lang.org>".to_string(),
-        };
-        ctx.run_git(&["init"]);
-        ctx.run_git(&["config", "user.name", "Tester"]);
-        ctx.run_git(&["config", "user.email", "tester@rust-lang.org"]);
-        ctx.modify("README.md");
-        ctx.commit();
-        ctx.run_git(&["branch", "-m", "main"]);
-        ctx
-    }
-
-    fn check_modifications(&self, target_paths: &[&str], ci_env: CiEnv) -> PathFreshness {
-        check_path_modifications(Some(self.dir.path()), &self.git_config(), target_paths, ci_env)
-            .unwrap()
-    }
-
-    fn create_upstream_merge(&self, modified_files: &[&str]) -> String {
-        self.create_branch_and_merge("previous-pr", modified_files, &self.merge_bot_email)
-    }
-
-    fn create_nonupstream_merge(&self, modified_files: &[&str]) -> String {
-        self.create_branch_and_merge("pr", modified_files, "Tester <tester@rust-lang.org>")
-    }
-
-    fn create_branch_and_merge(
-        &self,
-        branch: &str,
-        modified_files: &[&str],
-        author: &str,
-    ) -> String {
-        self.create_branch(branch);
-        for file in modified_files {
-            self.modify(file);
-        }
-        self.commit();
-        self.switch_to_branch("main");
-        self.merge(branch, author);
-        self.run_git(&["branch", "-d", branch]);
-        self.get_current_commit()
-    }
-
-    fn get_current_commit(&self) -> String {
-        self.run_git(&["rev-parse", "HEAD"])
-    }
-
-    fn merge(&self, branch: &str, author: &str) {
-        self.run_git(&["merge", "--no-commit", "--no-ff", branch]);
-        self.run_git(&[
-            "commit".to_string(),
-            "-m".to_string(),
-            "Merge of {branch}".to_string(),
-            "--author".to_string(),
-            author.to_string(),
-        ]);
-    }
-
-    fn modify(&self, path: &str) {
-        use std::io::Write;
-
-        let path = self.dir.path().join(path);
-        std::fs::create_dir_all(&path.parent().unwrap()).unwrap();
-
-        let mut file = OpenOptions::new().create(true).append(true).open(path).unwrap();
-        writeln!(file, "line").unwrap();
-    }
-
-    fn commit(&self) -> String {
-        self.run_git(&["add", "."]);
-        self.run_git(&["commit", "-m", "commit message"]);
-        self.get_current_commit()
-    }
-
-    fn switch_to_branch(&self, name: &str) {
-        self.run_git(&["switch", name]);
-    }
-
-    /// Creates a branch and switches to it.
-    fn create_branch(&self, name: &str) {
-        self.run_git(&["checkout", "-b", name]);
-    }
-
-    fn run_git<S: AsRef<OsStr>>(&self, args: &[S]) -> String {
-        let mut cmd = self.git_cmd();
-        cmd.args(args);
-        eprintln!("Running {cmd:?}");
-        let output = cmd.output().unwrap();
-        let stdout = String::from_utf8(output.stdout).unwrap().trim().to_string();
-        let stderr = String::from_utf8(output.stderr).unwrap().trim().to_string();
-        if !output.status.success() {
-            panic!("Git command `{cmd:?}` failed\nStdout\n{stdout}\nStderr\n{stderr}");
-        }
-        stdout
-    }
-
-    fn git_cmd(&self) -> Command {
-        let mut cmd = Command::new("git");
-        cmd.current_dir(&self.dir);
-        cmd
-    }
-
-    fn git_config(&self) -> GitConfig<'_> {
-        GitConfig {
-            git_repository: &self.git_repo,
-            nightly_branch: &self.nightly_branch,
-            git_merge_commit_email: &self.merge_bot_email,
-        }
-    }
-}
-
-fn git_test<F>(test_fn: F)
-where
-    F: FnOnce(&GitCtx),
-{
-    let ctx = GitCtx::new();
-    test_fn(&ctx);
-}