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),
|
LogFile(PathBuf),
|
||||||
/// A resumable build session root (chroot + journal).
|
/// A resumable build session root (chroot + journal).
|
||||||
Session(PathBuf),
|
Session(PathBuf),
|
||||||
|
/// A `pkh test` run directory (report artifacts).
|
||||||
|
TestRun(PathBuf),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Artifact {
|
impl Artifact {
|
||||||
@@ -109,7 +111,8 @@ impl Artifact {
|
|||||||
| Artifact::LockFile(p)
|
| Artifact::LockFile(p)
|
||||||
| Artifact::Tarball(p)
|
| Artifact::Tarball(p)
|
||||||
| Artifact::LogFile(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::Tarball(_) => "cached chroot tarball",
|
||||||
Artifact::LogFile(_) => "build log",
|
Artifact::LogFile(_) => "build log",
|
||||||
Artifact::Session(_) => "resumable build session",
|
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()
|
.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
|
/// Whether a session root is prunable under the given policy: everything
|
||||||
/// with `--all`, otherwise stale (untouched beyond the retention window)
|
/// with `--all`, otherwise stale (untouched beyond the retention window)
|
||||||
/// or corrupt (no readable manifest) sessions. Live sessions record a
|
/// or corrupt (no readable manifest) sessions. Live sessions record a
|
||||||
@@ -481,24 +510,28 @@ pub fn prune_in(
|
|||||||
cache_dir: Option<&Path>,
|
cache_dir: Option<&Path>,
|
||||||
options: PruneOptions,
|
options: PruneOptions,
|
||||||
) -> Result<PruneReport, Box<dyn std::error::Error>> {
|
) -> 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.
|
/// Execute the prune operation against explicit roots.
|
||||||
///
|
///
|
||||||
/// `sessions_dir` opts in to build-session pruning (the production
|
/// `sessions_dir` opts in to build-session pruning and `tests_dir` to
|
||||||
/// [`prune()`] passes the real sessions root; tests pass a controlled
|
/// test-run pruning (the production [`prune()`] passes the real roots;
|
||||||
/// directory or `None`).
|
/// tests pass controlled directories or `None`).
|
||||||
pub fn prune_in_roots(
|
pub fn prune_in_roots(
|
||||||
temp_dir: &Path,
|
temp_dir: &Path,
|
||||||
cache_dir: Option<&Path>,
|
cache_dir: Option<&Path>,
|
||||||
sessions_dir: Option<&Path>,
|
sessions_dir: Option<&Path>,
|
||||||
|
tests_dir: Option<&Path>,
|
||||||
options: PruneOptions,
|
options: PruneOptions,
|
||||||
) -> Result<PruneReport, Box<dyn std::error::Error>> {
|
) -> Result<PruneReport, Box<dyn std::error::Error>> {
|
||||||
let mut artifacts = discover_artifacts(temp_dir, cache_dir);
|
let mut artifacts = discover_artifacts(temp_dir, cache_dir);
|
||||||
if let Some(sessions_dir) = sessions_dir {
|
if let Some(sessions_dir) = sessions_dir {
|
||||||
artifacts.extend(discover_sessions(sessions_dir, options.all));
|
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 {
|
let mut report = PruneReport {
|
||||||
dry_run: options.dry_run,
|
dry_run: options.dry_run,
|
||||||
@@ -565,6 +598,7 @@ pub fn prune(options: PruneOptions) -> Result<PruneReport, Box<dyn std::error::E
|
|||||||
&temp_dir,
|
&temp_dir,
|
||||||
cache.as_deref(),
|
cache.as_deref(),
|
||||||
Some(&crate::deb::session::sessions_root()),
|
Some(&crate::deb::session::sessions_root()),
|
||||||
|
Some(&crate::test::runs_root()),
|
||||||
options,
|
options,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -765,6 +799,7 @@ none /tmp/other proc rw 0 0
|
|||||||
Artifact::Tarball(_) => tarballs += 1,
|
Artifact::Tarball(_) => tarballs += 1,
|
||||||
Artifact::LogFile(_) => logs += 1,
|
Artifact::LogFile(_) => logs += 1,
|
||||||
Artifact::Session(_) => {}
|
Artifact::Session(_) => {}
|
||||||
|
Artifact::TestRun(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert_eq!(temp_dirs, 2);
|
assert_eq!(temp_dirs, 2);
|
||||||
@@ -1192,8 +1227,14 @@ none /tmp/other proc rw 0 0
|
|||||||
fs::create_dir_all(&corrupt).unwrap();
|
fs::create_dir_all(&corrupt).unwrap();
|
||||||
fs::write(corrupt.join("session.json"), "not json").unwrap();
|
fs::write(corrupt.join("session.json"), "not json").unwrap();
|
||||||
|
|
||||||
let report =
|
let report = prune_in_roots(
|
||||||
prune_in_roots(temp.path(), None, Some(&sessions), PruneOptions::default()).unwrap();
|
temp.path(),
|
||||||
|
None,
|
||||||
|
Some(&sessions),
|
||||||
|
None,
|
||||||
|
PruneOptions::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert!(report.removed.contains(&stale));
|
assert!(report.removed.contains(&stale));
|
||||||
assert!(report.removed.contains(&corrupt));
|
assert!(report.removed.contains(&corrupt));
|
||||||
assert!(fresh.exists(), "a live session is not pruned by default");
|
assert!(fresh.exists(), "a live session is not pruned by default");
|
||||||
@@ -1205,6 +1246,54 @@ none /tmp/other proc rw 0 0
|
|||||||
temp.path(),
|
temp.path(),
|
||||||
None,
|
None,
|
||||||
Some(&sessions),
|
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 {
|
PruneOptions {
|
||||||
all: true,
|
all: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
+12
-2
@@ -530,12 +530,22 @@ fn drop_shell(
|
|||||||
/// `/var/tmp/pkh/tests/<source>-<series>-<arch>-<timestamp>/`.
|
/// `/var/tmp/pkh/tests/<source>-<series>-<arch>-<timestamp>/`.
|
||||||
fn create_run_dir(package: &str, series: &str, arch: &str) -> std::io::Result<PathBuf> {
|
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 timestamp = chrono::Utc::now().format("%Y%m%dT%H%M%S");
|
||||||
let dir =
|
let dir = runs_root().join(format!("{package}-{series}-{arch}-{timestamp}"));
|
||||||
PathBuf::from("/var/tmp/pkh/tests").join(format!("{package}-{series}-{arch}-{timestamp}"));
|
|
||||||
fs::create_dir_all(&dir)?;
|
fs::create_dir_all(&dir)?;
|
||||||
Ok(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
|
/// Render the tests declared in the tree's `debian/tests/control`, one
|
||||||
/// block per test (`pkh test --list-tests`).
|
/// block per test (`pkh test --list-tests`).
|
||||||
pub fn list_tests(tree: &Path) -> Result<String, Box<dyn std::error::Error>> {
|
pub fn list_tests(tree: &Path) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
|||||||
Reference in New Issue
Block a user