Files
MeshCore/test/test_build_output_safety.py
T

148 lines
8.0 KiB
Python

"""Reject dangerous cleanup paths without touching any existing build output."""
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
class Tests(unittest.TestCase):
def prepare(self, output, *, cwd=ROOT, resume=False, stub=False, overrides=''):
script = r'''
source "$1/build_legacy.sh"
OUTPUT_DIR=$2
RESUME_BUILD_OUTPUT=$3
'''
if stub:
# Protected-path tests never execute a real filesystem mutation,
# even if this regression is reintroduced in production.
script += r'''
rm() { printf 'UNSAFE_DELETE_CALL:%s\n' "$*"; }
mkdir() { printf 'UNSAFE_MKDIR_CALL:%s\n' "$*"; }
'''
script += overrides + '\nprepare_output_dir\n'
return subprocess.run(['bash', '-c', script, 'output-safety', str(ROOT),
str(output), '1' if resume else '0'], cwd=cwd,
capture_output=True, text=True, timeout=10)
def assert_rejected_without_mutation(self, output, **kwargs):
result = self.prepare(output, stub=True, **kwargs)
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn('Refusing unsafe output directory:', result.stderr)
self.assertNotIn('UNSAFE_DELETE_CALL:', result.stdout)
self.assertNotIn('UNSAFE_MKDIR_CALL:', result.stdout)
def test_roots_homes_checkout_ancestors_and_dot_aliases_fail_before_rm(self):
targets = ('', '/', '//', '.', './', '..', '../', str(ROOT),
str(ROOT/'out/..'), str(ROOT.parent), str(Path.home()),
*(str(parent) for parent in ROOT.parents))
for resume in (False, True):
for output in dict.fromkeys(targets):
with self.subTest(output=output, resume=resume):
self.assert_rejected_without_mutation(output, resume=resume)
def test_protected_symlink_targets_and_loops_are_not_deleted(self):
with tempfile.TemporaryDirectory(prefix='mesh-build-output-safety-') as temporary:
directory = Path(temporary)
for i, protected in enumerate((ROOT, ROOT.parent, Path.home(), Path('/'))):
link = directory/f'protected-{i}'
link.symlink_to(protected, target_is_directory=True)
for output in (link, str(link)+'/', str(link)+'/out/..'):
self.assert_rejected_without_mutation(output)
self.assertTrue(link.is_symlink())
loop = directory/'loop'
loop.symlink_to(loop)
self.assert_rejected_without_mutation(loop)
self.assertTrue(loop.is_symlink())
def test_tracked_subtrees_and_git_metadata_fail_before_rm_even_through_parent_links(self):
for output in (ROOT/'src', ROOT/'examples', ROOT/'scripts', ROOT/'test',
ROOT/'.git', ROOT/'.git/objects', ROOT/'.git/config'):
with self.subTest(output=output):
self.assert_rejected_without_mutation(output)
with tempfile.TemporaryDirectory(prefix='mesh-build-output-safety-') as temporary:
link = Path(temporary)/'checkout-parent-link'
link.symlink_to(ROOT, target_is_directory=True)
for suffix in ('src', 'examples', '.git/objects'):
self.assert_rejected_without_mutation(link/suffix)
self.assertTrue(link.is_symlink())
def test_safe_output_directory_is_cleaned_and_created_without_touching_siblings(self):
with tempfile.TemporaryDirectory(prefix='mesh-build-output-safety-') as temporary:
directory = Path(temporary)
sibling = directory/'keep.bin'; sibling.write_bytes(b'keep sibling')
for relative in ('output', 'path with spaces', 'nested/output',
'output; touch injected', '$(touch injected)'):
output = directory/relative; output.mkdir(parents=True)
(output/'old.bin').write_bytes(b'old diagnostic artifact')
result = self.prepare(relative, cwd=directory)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertTrue(output.is_dir())
self.assertEqual(list(output.iterdir()), [])
self.assertEqual(sibling.read_bytes(), b'keep sibling')
self.assertFalse((directory/'injected').exists())
result = self.prepare(directory/'not-created-yet'/'output')
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertTrue((directory/'not-created-yet'/'output').is_dir())
def test_resume_preserves_output_files_and_safe_symlink_but_clean_refuses_the_link(self):
with tempfile.TemporaryDirectory(prefix='mesh-build-output-safety-') as temporary:
directory = Path(temporary)
output = directory/'output'; output.mkdir()
artifact = output/'previous.bin'; artifact.write_bytes(b'previous diagnostic build')
initial = artifact.stat()
link = directory/'output-link'; link.symlink_to(output, target_is_directory=True)
for selected in (output, link):
result = self.prepare(selected, resume=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(artifact.read_bytes(), b'previous diagnostic build')
self.assertEqual((artifact.stat().st_ino, artifact.stat().st_mtime_ns),
(initial.st_ino, initial.st_mtime_ns))
self.assert_rejected_without_mutation(link)
self.assert_rejected_without_mutation(str(link)+'/')
self.assertTrue(link.is_symlink())
self.assertEqual(artifact.read_bytes(), b'previous diagnostic build')
def test_safe_parent_symlink_keeps_link_and_other_outputs(self):
with tempfile.TemporaryDirectory(prefix='mesh-build-output-safety-') as temporary:
directory = Path(temporary)
physical = directory/'physical'; physical.mkdir()
output = physical/'output'; output.mkdir()
(output/'old.bin').write_bytes(b'old isolated diagnostic artifact')
sibling = physical/'keep.bin'; sibling.write_bytes(b'keep sibling')
link = directory/'parent-link'; link.symlink_to(physical, target_is_directory=True)
result = self.prepare(link/'output')
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertTrue(link.is_symlink())
self.assertTrue(output.is_dir())
self.assertEqual(list(output.iterdir()), [])
self.assertEqual(sibling.read_bytes(), b'keep sibling')
def test_existing_regular_files_are_preserved_and_rejected(self):
with tempfile.TemporaryDirectory(prefix='mesh-build-output-safety-') as temporary:
path = Path(temporary)/'not-a-build-directory'
path.write_bytes(b'user data')
for resume in (False, True):
self.assert_rejected_without_mutation(path, resume=resume)
self.assertEqual(path.read_bytes(), b'user data')
def test_delete_and_mkdir_failures_abort_before_build_can_continue(self):
with tempfile.TemporaryDirectory(prefix='mesh-build-output-safety-') as temporary:
output = Path(temporary)/'output'; output.mkdir()
(output/'previous.bin').write_bytes(b'previous isolated diagnostic artifact')
result = self.prepare(output, overrides=r'''
rm() { printf 'FAILED_DELETE\n'; return 67; }
mkdir() { printf 'SHOULD_NOT_MKDIR\n'; }
''')
self.assertNotEqual(result.returncode, 0)
self.assertIn('FAILED_DELETE', result.stdout)
self.assertNotIn('SHOULD_NOT_MKDIR', result.stdout)
self.assertTrue((output/'previous.bin').exists())
result = self.prepare(output, resume=True, overrides='mkdir() { return 73; }')
self.assertNotEqual(result.returncode, 0)
self.assertTrue((output/'previous.bin').exists())
if __name__ == '__main__':
unittest.main()