prune: garbage-collect pkh test run directories
Test artifacts land under /var/tmp/pkh/tests (<source>-<series>-<arch>- <timestamp>/), one directory per run: they are disposable results, not state worth keeping. Prune removes them on the same 7-day retention as build sessions, and with --all; the root is overridable through PKH_TESTS_DIR like the sessions root.
This commit is contained in:
+96
-7
@@ -99,6 +99,8 @@ enum Artifact {
|
||||
LogFile(PathBuf),
|
||||
/// A resumable build session root (chroot + journal).
|
||||
Session(PathBuf),
|
||||
/// A `pkh test` run directory (report artifacts).
|
||||
TestRun(PathBuf),
|
||||
}
|
||||
|
||||
impl Artifact {
|
||||
@@ -109,7 +111,8 @@ impl Artifact {
|
||||
| Artifact::LockFile(p)
|
||||
| Artifact::Tarball(p)
|
||||
| Artifact::LogFile(p)
|
||||
| Artifact::Session(p) => p,
|
||||
| Artifact::Session(p)
|
||||
| Artifact::TestRun(p) => p,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +124,7 @@ impl Artifact {
|
||||
Artifact::Tarball(_) => "cached chroot tarball",
|
||||
Artifact::LogFile(_) => "build log",
|
||||
Artifact::Session(_) => "resumable build session",
|
||||
Artifact::TestRun(_) => "pkh test run",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,6 +146,31 @@ fn discover_sessions(sessions_dir: &Path, all: bool) -> Vec<Artifact> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Discover the `pkh test` run directories prune may remove under
|
||||
/// `tests_dir`: with `--all`, every run; otherwise only those untouched
|
||||
/// for longer than the sessions retention window.
|
||||
fn discover_test_runs(tests_dir: &Path, all: bool) -> Vec<Artifact> {
|
||||
let Ok(entries) = fs::read_dir(tests_dir) else {
|
||||
return Vec::new();
|
||||
};
|
||||
entries
|
||||
.flatten()
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.is_dir())
|
||||
.filter(|path| {
|
||||
all || fs::metadata(path)
|
||||
.and_then(|meta| meta.modified())
|
||||
.is_ok_and(|mtime| {
|
||||
chrono::Utc::now()
|
||||
.signed_duration_since(chrono::DateTime::<chrono::Utc>::from(mtime))
|
||||
.to_std()
|
||||
.is_ok_and(|age| age >= crate::deb::session::SESSION_RETENTION)
|
||||
})
|
||||
})
|
||||
.map(Artifact::TestRun)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Whether a session root is prunable under the given policy: everything
|
||||
/// with `--all`, otherwise stale (untouched beyond the retention window)
|
||||
/// or corrupt (no readable manifest) sessions. Live sessions record a
|
||||
@@ -481,24 +510,28 @@ pub fn prune_in(
|
||||
cache_dir: Option<&Path>,
|
||||
options: PruneOptions,
|
||||
) -> Result<PruneReport, Box<dyn std::error::Error>> {
|
||||
prune_in_roots(temp_dir, cache_dir, None, options)
|
||||
prune_in_roots(temp_dir, cache_dir, None, None, options)
|
||||
}
|
||||
|
||||
/// Execute the prune operation against explicit roots.
|
||||
///
|
||||
/// `sessions_dir` opts in to build-session pruning (the production
|
||||
/// [`prune()`] passes the real sessions root; tests pass a controlled
|
||||
/// directory or `None`).
|
||||
/// `sessions_dir` opts in to build-session pruning and `tests_dir` to
|
||||
/// test-run pruning (the production [`prune()`] passes the real roots;
|
||||
/// tests pass controlled directories or `None`).
|
||||
pub fn prune_in_roots(
|
||||
temp_dir: &Path,
|
||||
cache_dir: Option<&Path>,
|
||||
sessions_dir: Option<&Path>,
|
||||
tests_dir: Option<&Path>,
|
||||
options: PruneOptions,
|
||||
) -> Result<PruneReport, Box<dyn std::error::Error>> {
|
||||
let mut artifacts = discover_artifacts(temp_dir, cache_dir);
|
||||
if let Some(sessions_dir) = sessions_dir {
|
||||
artifacts.extend(discover_sessions(sessions_dir, options.all));
|
||||
}
|
||||
if let Some(tests_dir) = tests_dir {
|
||||
artifacts.extend(discover_test_runs(tests_dir, options.all));
|
||||
}
|
||||
|
||||
let mut report = PruneReport {
|
||||
dry_run: options.dry_run,
|
||||
@@ -565,6 +598,7 @@ pub fn prune(options: PruneOptions) -> Result<PruneReport, Box<dyn std::error::E
|
||||
&temp_dir,
|
||||
cache.as_deref(),
|
||||
Some(&crate::deb::session::sessions_root()),
|
||||
Some(&crate::test::runs_root()),
|
||||
options,
|
||||
)
|
||||
}
|
||||
@@ -765,6 +799,7 @@ none /tmp/other proc rw 0 0
|
||||
Artifact::Tarball(_) => tarballs += 1,
|
||||
Artifact::LogFile(_) => logs += 1,
|
||||
Artifact::Session(_) => {}
|
||||
Artifact::TestRun(_) => {}
|
||||
}
|
||||
}
|
||||
assert_eq!(temp_dirs, 2);
|
||||
@@ -1192,8 +1227,14 @@ none /tmp/other proc rw 0 0
|
||||
fs::create_dir_all(&corrupt).unwrap();
|
||||
fs::write(corrupt.join("session.json"), "not json").unwrap();
|
||||
|
||||
let report =
|
||||
prune_in_roots(temp.path(), None, Some(&sessions), PruneOptions::default()).unwrap();
|
||||
let report = prune_in_roots(
|
||||
temp.path(),
|
||||
None,
|
||||
Some(&sessions),
|
||||
None,
|
||||
PruneOptions::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(report.removed.contains(&stale));
|
||||
assert!(report.removed.contains(&corrupt));
|
||||
assert!(fresh.exists(), "a live session is not pruned by default");
|
||||
@@ -1205,6 +1246,54 @@ none /tmp/other proc rw 0 0
|
||||
temp.path(),
|
||||
None,
|
||||
Some(&sessions),
|
||||
None,
|
||||
PruneOptions {
|
||||
all: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(report.removed.contains(&fresh));
|
||||
assert!(!fresh.exists());
|
||||
}
|
||||
|
||||
/// `pkh test` run directories follow the retention window: stale
|
||||
/// runs go, fresh runs stay, `--all` takes everything.
|
||||
#[test]
|
||||
fn prunes_test_runs_by_retention() {
|
||||
let temp = tempdir().unwrap();
|
||||
let tests = temp.path().join("tests");
|
||||
let stale = tests.join("pkg-noble-amd64-20260101T000000");
|
||||
let fresh = tests.join("pkg-noble-amd64-20991231T235959");
|
||||
fs::create_dir_all(&stale).unwrap();
|
||||
fs::create_dir_all(&fresh).unwrap();
|
||||
// Directories open read-only (write-open is a file-only mode):
|
||||
// futimens still applies through the read handle.
|
||||
let dir = fs::File::open(&stale).unwrap();
|
||||
dir.set_times(
|
||||
std::fs::FileTimes::new()
|
||||
.set_modified(SystemTime::now() - std::time::Duration::from_secs(8 * 24 * 60 * 60)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let report = prune_in_roots(
|
||||
temp.path(),
|
||||
None,
|
||||
None,
|
||||
Some(&tests),
|
||||
PruneOptions::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(report.removed.contains(&stale));
|
||||
assert!(!stale.exists());
|
||||
assert!(fresh.exists(), "a fresh test run is not pruned");
|
||||
|
||||
// --all removes every run.
|
||||
let report = prune_in_roots(
|
||||
temp.path(),
|
||||
None,
|
||||
None,
|
||||
Some(&tests),
|
||||
PruneOptions {
|
||||
all: true,
|
||||
..Default::default()
|
||||
|
||||
+12
-2
@@ -530,12 +530,22 @@ fn drop_shell(
|
||||
/// `/var/tmp/pkh/tests/<source>-<series>-<arch>-<timestamp>/`.
|
||||
fn create_run_dir(package: &str, series: &str, arch: &str) -> std::io::Result<PathBuf> {
|
||||
let timestamp = chrono::Utc::now().format("%Y%m%dT%H%M%S");
|
||||
let dir =
|
||||
PathBuf::from("/var/tmp/pkh/tests").join(format!("{package}-{series}-{arch}-{timestamp}"));
|
||||
let dir = runs_root().join(format!("{package}-{series}-{arch}-{timestamp}"));
|
||||
fs::create_dir_all(&dir)?;
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
/// The test-run artifacts root: `$PKH_TESTS_DIR`, defaulting to
|
||||
/// `/var/tmp/pkh/tests`.
|
||||
pub fn runs_root() -> PathBuf {
|
||||
if let Ok(dir) = std::env::var("PKH_TESTS_DIR")
|
||||
&& !dir.is_empty()
|
||||
{
|
||||
return PathBuf::from(dir);
|
||||
}
|
||||
PathBuf::from("/var/tmp/pkh/tests")
|
||||
}
|
||||
|
||||
/// Render the tests declared in the tree's `debian/tests/control`, one
|
||||
/// block per test (`pkh test --list-tests`).
|
||||
pub fn list_tests(tree: &Path) -> Result<String, Box<dyn std::error::Error>> {
|
||||
|
||||
Reference in New Issue
Block a user