diff --git a/local/scripts/pre-push-checks.sh b/local/scripts/pre-push-checks.sh index 851dc87c68..a5a091e10f 100755 --- a/local/scripts/pre-push-checks.sh +++ b/local/scripts/pre-push-checks.sh @@ -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 ===" diff --git a/local/scripts/verify-collision-detection.py b/local/scripts/verify-collision-detection.py index 04bdfe063b..d38ce0ae16 100755 --- a/local/scripts/verify-collision-detection.py +++ b/local/scripts/verify-collision-detection.py @@ -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())