bf43c0a34f
Pre-existing uncommitted work (materialized by the build's local-over-WIP preflight): CollisionTracker detects config [[files]] (Layer 1) being silently overwritten by package staging (Layer 2) — the D-Bus regression class. Committed as-is so the fork has a clean HEAD for pkgar fingerprints; no functional change intended by this commit beyond tracking it.
296 lines
10 KiB
Rust
296 lines
10 KiB
Rust
//! Integration tests for the runtime file-collision tracker.
|
|
//!
|
|
//! These exercise the public API only (`CollisionTracker` and friends,
|
|
//! re-exported at the crate root). They live in `tests/` rather than inline
|
|
//! in `src/collision.rs` to keep the production module under the 250 pure
|
|
//! LOC ceiling and to enforce the public-API-only test contract — the
|
|
//! tracker's private helpers are not touched here, so a future internal
|
|
//! rewrite stays test-compatible as long as the public surface is stable.
|
|
|
|
use redox_installer::{
|
|
CollisionTracker, FileProvenance, Layer, ProvenanceSource, ERROR_MARKER, STRICT_ENV_VAR,
|
|
};
|
|
use std::path::PathBuf;
|
|
|
|
#[test]
|
|
fn test_no_collision_when_paths_distinct() {
|
|
// Given: an empty tracker and two distinct paths.
|
|
let mut tracker = CollisionTracker::new_with_strict(false);
|
|
let path_a = PathBuf::from("/output/etc/foo.conf");
|
|
let path_b = PathBuf::from("/output/etc/bar.conf");
|
|
|
|
// When: both paths are recorded from different layers.
|
|
tracker
|
|
.record(
|
|
path_a.clone(),
|
|
FileProvenance {
|
|
source: ProvenanceSource::ConfigFile("/etc/foo.conf".to_string()),
|
|
layer: Layer::ConfigPreInstall,
|
|
},
|
|
Some("/etc/foo.conf"),
|
|
)
|
|
.unwrap();
|
|
tracker
|
|
.record(
|
|
path_b.clone(),
|
|
FileProvenance {
|
|
source: ProvenanceSource::ConfigFile("/etc/bar.conf".to_string()),
|
|
layer: Layer::ConfigPostInstall,
|
|
},
|
|
Some("/etc/bar.conf"),
|
|
)
|
|
.unwrap();
|
|
|
|
// Then: no collisions, both paths tracked.
|
|
assert_eq!(tracker.collision_count(), 0, "distinct paths never collide");
|
|
assert_eq!(tracker.len(), 2);
|
|
assert!(tracker.collisions().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_collision_warning_when_layer2_overwrites_layer1() {
|
|
// Given: a tracker with a Layer 1 (ConfigPreInstall) write to a path.
|
|
let mut tracker = CollisionTracker::new_with_strict(false);
|
|
let abs_path = PathBuf::from("/output/usr/lib/init.d/dbus");
|
|
tracker
|
|
.record(
|
|
abs_path.clone(),
|
|
FileProvenance {
|
|
source: ProvenanceSource::ConfigFile("/usr/lib/init.d/dbus".to_string()),
|
|
layer: Layer::ConfigPreInstall,
|
|
},
|
|
Some("/usr/lib/init.d/dbus"),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(tracker.collision_count(), 0);
|
|
|
|
// When: Layer 2 (PackageStaging) writes the same path.
|
|
let result = tracker.record(
|
|
abs_path.clone(),
|
|
FileProvenance {
|
|
source: ProvenanceSource::Package("base".to_string()),
|
|
layer: Layer::PackageStaging,
|
|
},
|
|
Some("/usr/lib/init.d/dbus"),
|
|
);
|
|
|
|
// Then: a collision is recorded, the call succeeds (warn-only mode),
|
|
// and the collision metadata points at the right layers.
|
|
assert!(result.is_ok(), "warn-only mode must not error");
|
|
assert_eq!(
|
|
tracker.collision_count(),
|
|
1,
|
|
"Layer 2 overwriting Layer 1 is a collision"
|
|
);
|
|
let collision = &tracker.collisions()[0];
|
|
assert_eq!(collision.path, abs_path);
|
|
assert_eq!(collision.previous.layer, Layer::ConfigPreInstall);
|
|
assert_eq!(collision.current.layer, Layer::PackageStaging);
|
|
}
|
|
|
|
#[test]
|
|
fn test_no_warning_for_postinstall_override() {
|
|
// Given: a tracker where Layer 2 (PackageStaging) has written a path.
|
|
let mut tracker = CollisionTracker::new_with_strict(false);
|
|
let abs_path = PathBuf::from("/output/etc/system.conf");
|
|
tracker
|
|
.record(
|
|
abs_path.clone(),
|
|
FileProvenance {
|
|
source: ProvenanceSource::Package("base".to_string()),
|
|
layer: Layer::PackageStaging,
|
|
},
|
|
Some("/etc/system.conf"),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(tracker.collision_count(), 0);
|
|
|
|
// When: Layer 3 (ConfigPostInstall) writes the same path — the
|
|
// documented intentional-override pattern.
|
|
let result = tracker.record(
|
|
abs_path.clone(),
|
|
FileProvenance {
|
|
source: ProvenanceSource::ConfigFile("/etc/system.conf".to_string()),
|
|
layer: Layer::ConfigPostInstall,
|
|
},
|
|
Some("/etc/system.conf"),
|
|
);
|
|
|
|
// Then: no collision is recorded. Layer 3 is the override layer.
|
|
assert!(result.is_ok());
|
|
assert_eq!(
|
|
tracker.collision_count(),
|
|
0,
|
|
"Layer 3 postinstall overrides are intentional and must not warn"
|
|
);
|
|
assert!(tracker.collisions().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_known_safe_override_paths_dont_warn() {
|
|
// Given: a tracker where Layer 2 (PackageStaging) has written the
|
|
// package-default init service at /usr/lib/init.d/dbus.
|
|
//
|
|
// In a real install the config override at /etc/init.d/dbus would
|
|
// resolve to a different abs_path and no collision would fire. The
|
|
// known-safe rule only matters when both writes land at the same
|
|
// abs_path — e.g. when /etc/init.d is a symlink to /usr/lib/init.d,
|
|
// or in unit tests that exercise the rule directly.
|
|
let mut tracker = CollisionTracker::new_with_strict(false);
|
|
let abs_path = PathBuf::from("/output/etc/init.d/dbus");
|
|
tracker
|
|
.record(
|
|
abs_path.clone(),
|
|
FileProvenance {
|
|
source: ProvenanceSource::Package("base".to_string()),
|
|
layer: Layer::PackageStaging,
|
|
},
|
|
Some("/usr/lib/init.d/dbus"),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(tracker.collision_count(), 0);
|
|
|
|
// When: a config write (Layer 1 — not Layer 3, so the layer-3
|
|
// suppression rule does not apply) targets /etc/init.d/dbus, which
|
|
// resolves to the same abs_path but matches the known-safe override
|
|
// pair (/etc/init.d over /usr/lib/init.d).
|
|
let result = tracker.record(
|
|
abs_path.clone(),
|
|
FileProvenance {
|
|
source: ProvenanceSource::ConfigFile("/etc/init.d/dbus".to_string()),
|
|
layer: Layer::ConfigPreInstall,
|
|
},
|
|
Some("/etc/init.d/dbus"),
|
|
);
|
|
|
|
// Then: no collision — the known-safe override pair suppresses the
|
|
// warning, even though the layers differ and neither is Layer 3.
|
|
assert!(result.is_ok());
|
|
assert_eq!(
|
|
tracker.collision_count(),
|
|
0,
|
|
"known-safe override pair (/etc/init.d over /usr/lib/init.d) must not warn"
|
|
);
|
|
|
|
// Sanity: the opposite direction (default after override) is NOT
|
|
// suppressed — that would be a package clobbering a config override,
|
|
// which is the D-Bus regression class.
|
|
let result2 = tracker.record(
|
|
abs_path,
|
|
FileProvenance {
|
|
source: ProvenanceSource::Package("base".to_string()),
|
|
layer: Layer::PackageStaging,
|
|
},
|
|
Some("/usr/lib/init.d/dbus-other-direction"),
|
|
);
|
|
assert!(result2.is_ok());
|
|
assert_eq!(
|
|
tracker.collision_count(),
|
|
1,
|
|
"default-after-override direction is NOT a known-safe override"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_strict_mode_promotes_collision_to_error() {
|
|
// Given: a strict-mode tracker with a Layer 1 write.
|
|
let mut tracker = CollisionTracker::new_with_strict(true);
|
|
let abs_path = PathBuf::from("/output/etc/dbus.conf");
|
|
tracker
|
|
.record(
|
|
abs_path.clone(),
|
|
FileProvenance {
|
|
source: ProvenanceSource::ConfigFile("/etc/dbus.conf".to_string()),
|
|
layer: Layer::ConfigPreInstall,
|
|
},
|
|
Some("/etc/dbus.conf"),
|
|
)
|
|
.unwrap();
|
|
|
|
// When: Layer 2 writes the same path.
|
|
let result = tracker.record(
|
|
abs_path.clone(),
|
|
FileProvenance {
|
|
source: ProvenanceSource::Package("base".to_string()),
|
|
layer: Layer::PackageStaging,
|
|
},
|
|
Some("/etc/dbus.conf"),
|
|
);
|
|
|
|
// Then: the call returns Err and the error message references the
|
|
// strict env var so the operator knows how to demote if needed.
|
|
assert!(
|
|
result.is_err(),
|
|
"strict mode must promote collisions to errors"
|
|
);
|
|
let err = result.unwrap_err().to_string();
|
|
assert!(
|
|
err.contains(ERROR_MARKER),
|
|
"strict-mode error must carry the {ERROR_MARKER} marker for log grepping"
|
|
);
|
|
assert!(
|
|
err.contains(STRICT_ENV_VAR),
|
|
"strict-mode error must name the env var so the operator can demote"
|
|
);
|
|
assert!(
|
|
err.contains("layer 1"),
|
|
"strict-mode error must name the overwritten layer"
|
|
);
|
|
assert!(
|
|
err.contains("layer 2"),
|
|
"strict-mode error must name the overwriting layer"
|
|
);
|
|
|
|
// And: the collision is still counted in the tracker's history.
|
|
assert_eq!(tracker.collision_count(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_scan_package_staging_detects_layer2_over_layer1_on_real_disk() {
|
|
// End-to-end: write a Layer 1 file to a tempdir, then ask
|
|
// scan_package_staging to walk it. The walk must detect the file as a
|
|
// Layer 2 write that collides with the already-recorded Layer 1 entry.
|
|
//
|
|
// This is the closest unit-level proof of the D-Bus regression class:
|
|
// a config wrote to a path, then a package stages the same path, and
|
|
// the walk catches it.
|
|
let dir = std::env::temp_dir().join(format!(
|
|
"redbear_collision_test_{}_{}",
|
|
std::process::id(),
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.map(|d| d.as_nanos())
|
|
.unwrap_or(0),
|
|
));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
|
|
// Layer 1: a config writes /usr/lib/init.d/dbus.
|
|
let rel = "usr/lib/init.d/dbus";
|
|
let abs_path = dir.join(rel);
|
|
std::fs::create_dir_all(abs_path.parent().unwrap()).unwrap();
|
|
std::fs::write(&abs_path, b"# config layer 1").unwrap();
|
|
|
|
let mut tracker = CollisionTracker::new_with_strict(false);
|
|
tracker
|
|
.record(
|
|
abs_path.clone(),
|
|
FileProvenance {
|
|
source: ProvenanceSource::ConfigFile("/usr/lib/init.d/dbus".to_string()),
|
|
layer: Layer::ConfigPreInstall,
|
|
},
|
|
Some("/usr/lib/init.d/dbus"),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(tracker.collision_count(), 0);
|
|
|
|
// Layer 2: the walk sees the same file on disk and flags the collision.
|
|
tracker.scan_package_staging(&dir).unwrap();
|
|
assert_eq!(
|
|
tracker.collision_count(),
|
|
1,
|
|
"scan_package_staging must catch a Layer 2 write to a Layer 1 path"
|
|
);
|
|
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|