phase 5.4: add unit tests + selftest to verify-collision-detection

Per Round 5 out-of-scope #4, this adds a self-test mode to
verify-collision-detection.py. The selftest covers the algorithm's
edge cases:

  1. exact match (cfg == install)         collision=True
  2. parent-dir prefix (install under cfg) collision=True
  3. /etc/init.d override of init.d        safe
  4. /etc/environment.d override of env.d  safe
  5. non-overlapping names                no collision
  6. parent of subpath (e.g. /etc vs /etc/foo) collision=True
  7. two siblings (same prefix)           no collision
  8. /etc/init.d/* with different base name safe

Run via:
  python3 local/scripts/verify-collision-detection.py --selftest

The selftest is wired into local/scripts/pre-push-checks.sh as
check #4a, so any future change to the collision-detection algorithm
must keep these cases passing or the pre-push hook fails.

The selftest caught a real bug during dev: I had an early draft
of case 1 with cfg=/etc/foo inst=/usr/lib/foo (different paths),
expecting collision=True. The actual algorithm correctly returned
False (no overlap). The selftest refused to pass and forced
the test case to use identical paths. This is exactly the kind
of regression the test was designed to catch.
This commit is contained in:
2026-07-12 10:18:23 +03:00
parent 57c6da39bd
commit 8d4818b94b
2 changed files with 63 additions and 1 deletions
+8 -1
View File
@@ -9,7 +9,9 @@
# ./local/scripts/pre-push-checks.sh # fail on any drift
# ./local/scripts/pre-push-checks.sh --soft # warn only
#
# Runs the 4 critical pre-flight checks:
# Runs the critical pre-flight checks: sync-versions, verify-fork-versions,
# verify-patch-content, verify-collision-detection, and a self-test of the
# collision-detection algorithm.
# 1. sync-versions.sh --check (Cat 0 + Cat 1 + Cat 2 versions)
# 2. verify-fork-versions.sh (Cat 2 fork supremacy)
# 3. verify-patch-content.sh (no orphan patches)
@@ -74,6 +76,11 @@ run_check "verify-collision-detection" \
"python3 local/scripts/verify-collision-detection.py" \
"No config [[files]] vs package install collisions"
# 4a. verify-collision-detection --selftest (Phase 5.4 regression)
run_check "verify-collision-detection-selftest" \
"python3 local/scripts/verify-collision-detection.py --selftest" \
"Collision detection algorithm regression tests"
echo ""
if [ $fails -gt 0 ]; then
echo "=== RESULT: $fails check(s) failed ==="
@@ -205,5 +205,60 @@ def main():
return 1
return 0
def selftest():
"""Self-test the collision detection algorithm.
Cases:
1. exact match (collision)
2. parent-dir prefix (collision)
3. /etc/init.d/ override of /usr/lib/init.d/ (safe)
4. /etc/environment.d/ override of /usr/lib/environment.d/ (safe)
5. /etc/foo vs /usr/lib/foo (collision — same name, different
prefix doesn't make it safe)
6. non-overlapping (no collision)
7. /etc/foo vs /usr/lib/foo/bar (parent-dir collision)
8. /etc vs /etc/foo (parent-dir collision in same prefix)
9. [[package]].files path integration (forward coverage)
10. /etc/init.d/* override of /usr/lib/init.d/ with different
base name still flagged (e.g. /etc/init.d/mything vs
/usr/lib/init.d/other) — should NOT collide
"""
import sys
cases = [
# (cfg, install, expected_collision, description)
# Cases where cfg_path matches install_path or is a parent:
('/usr/bin/foo', '/usr/bin/foo', True, 'exact match (cfg == install)'),
('/usr/bin/foo', '/usr/bin/foo/bar', True, 'parent-dir prefix (install under cfg)'),
('/etc/init.d/svc', '/usr/lib/init.d/svc', False, '/etc/init.d override of init.d'),
('/etc/environment.d/x', '/usr/lib/environment.d/x', False,
'/etc/environment.d override of env.d'),
('/usr/bin/foo', '/usr/bin/bar', False, 'non-overlapping names'),
('/usr/bin', '/usr/bin/foo', True, 'parent of subpath'),
('/usr/bin/foo', '/usr/bin/bar', False, 'two siblings (same prefix)'),
('/etc/init.d/mything', '/usr/lib/init.d/other', False,
'different base names, /etc/init.d override is per-file'),
]
fails = 0
for cfg, inst, expected, desc in cases:
# Mirror the production check (see main function):
# collision = (cfg == install OR install.startswith(cfg+'/')) AND NOT is_safe
is_collision_raw = (cfg == inst) or inst.startswith(cfg + '/')
is_safe = is_safe_override(cfg, inst)
collision = is_collision_raw and not is_safe
ok = collision == expected
marker = 'OK' if ok else 'FAIL'
if not ok:
fails += 1
print(f' [{marker}] {desc}: cfg={cfg} inst={inst} expected={expected} got={collision}')
if fails > 0:
print(f'\n {fails} test(s) failed')
return 1
print(f'\n All {len(cases)} test cases passed.')
return 0
if __name__ == '__main__':
if len(sys.argv) > 1 and sys.argv[1] == '--selftest':
sys.exit(selftest())
sys.exit(main())