Files
bikini-patchwork/tests/test_audit.py
T
2026-06-17 20:45:45 -05:00

162 lines
6.5 KiB
Python

from __future__ import annotations
import subprocess
import sys
from pathlib import Path
import tempfile
import unittest
import json
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from patchwork.audit import analyze_source, build_manifest, verify_manifest
class AuditTests(unittest.TestCase):
def run_cli(self, *args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, "-m", "patchwork", *args],
cwd=ROOT,
capture_output=True,
text=True,
)
def test_analyze_source_reports_review_indicators(self) -> None:
audit = analyze_source("import subprocess\nsubprocess.run(['echo', 'x'])\n")
self.assertTrue(audit["review"]["requires_human_review"])
names = {item["name"] for item in audit["review"]["indicators"]}
self.assertIn("subprocess", names)
self.assertIn("subprocess.run", names)
def test_analyze_source_collects_basic_structure(self) -> None:
audit = analyze_source("class App:\n pass\n\ndef main():\n return 1\n")
self.assertEqual(["App"], audit["classes"])
self.assertEqual(["main"], audit["functions"])
self.assertFalse(audit["review"]["requires_human_review"])
def test_build_manifest_contains_hashes_and_options(self) -> None:
audit = analyze_source("print('hello')\n")
manifest = build_manifest(
input_path="hello.py",
output_path="hello_obf.py",
input_source="print('hello')\n",
output_source="print('hello')\n",
seed=123,
options={"layers": 3},
audit=audit,
)
self.assertEqual("patchwork.manifest.v1", manifest["schema"])
self.assertEqual(123, manifest["build"]["seed"])
self.assertEqual(manifest["input"]["sha256"], manifest["output"]["sha256"])
self.assertIn("stats", manifest)
def test_cli_strict_audit_refuses_sensitive_input(self) -> None:
with tempfile.TemporaryDirectory() as raw:
path = Path(raw) / "sample.py"
path.write_text("import subprocess\nsubprocess.run(['echo', 'x'])\n", encoding="utf-8")
result = self.run_cli(str(path), "--strict-audit")
self.assertEqual(3, result.returncode)
self.assertIn("strict audit refused", result.stderr)
def test_cli_version_flag_does_not_require_input(self) -> None:
result = self.run_cli("--version")
self.assertEqual(0, result.returncode)
self.assertIn("patchwork 0.8.0", result.stdout)
def test_config_dump_and_keep_file_merge_options(self) -> None:
with tempfile.TemporaryDirectory() as raw:
directory = Path(raw)
source = directory / "sample.py"
config = directory / "config.json"
keep_file = directory / "keep.txt"
dumped = directory / "effective.json"
source.write_text("def public():\n return 1\n", encoding="utf-8")
config.write_text(json.dumps({"layers": 2, "rename": False, "keep": ["public"]}), encoding="utf-8")
keep_file.write_text("external_name\n# ignored\n", encoding="utf-8")
result = self.run_cli(str(source), "--config", str(config), "--keep-file", str(keep_file), "--dump-config", str(dumped), "--dry-run", "--quiet")
self.assertEqual(0, result.returncode)
effective = json.loads(dumped.read_text(encoding="utf-8"))
self.assertEqual(2, effective["layers"])
self.assertFalse(effective["rename"])
self.assertEqual(["external_name", "public"], effective["keep"])
def test_dry_run_does_not_write_obfuscated_output(self) -> None:
with tempfile.TemporaryDirectory() as raw:
source = Path(raw) / "sample.py"
output = Path(raw) / "sample_obf.py"
source.write_text("print('dry run')\n", encoding="utf-8")
result = self.run_cli(str(source), "-o", str(output), "--dry-run", "--quiet")
self.assertEqual(0, result.returncode)
self.assertFalse(output.exists())
def test_cli_writes_stats_report_manifest_and_verifies_manifest(self) -> None:
with tempfile.TemporaryDirectory() as raw:
directory = Path(raw)
source = directory / "sample.py"
output = directory / "sample_obf.py"
stats = directory / "stats.json"
report = directory / "report.html"
manifest = directory / "manifest.json"
source.write_text("print('manifest')\n", encoding="utf-8")
result = self.run_cli(
str(source),
"-o",
str(output),
"--seed",
"123",
"--stats-json",
str(stats),
"--report",
str(report),
"--manifest",
str(manifest),
"--quiet",
)
self.assertEqual(0, result.returncode)
self.assertTrue(output.exists())
self.assertTrue(report.read_text(encoding="utf-8").startswith("<!doctype html>"))
stats_doc = json.loads(stats.read_text(encoding="utf-8"))
self.assertGreater(stats_doc["output_bytes"], stats_doc["input_bytes"])
self.assertTrue(verify_manifest(manifest)["valid"])
verify_result = self.run_cli("--verify-manifest", str(manifest), "--quiet")
self.assertEqual(0, verify_result.returncode)
def test_budget_gate_refuses_to_write_large_output(self) -> None:
with tempfile.TemporaryDirectory() as raw:
source = Path(raw) / "sample.py"
output = Path(raw) / "sample_obf.py"
source.write_text("print('too large')\n", encoding="utf-8")
result = self.run_cli(str(source), "-o", str(output), "--max-output-bytes", "1", "--quiet")
self.assertEqual(4, result.returncode)
self.assertFalse(output.exists())
self.assertIn("exceed max_output_bytes", result.stderr)
def test_review_indicator_gate_refuses_sensitive_input(self) -> None:
with tempfile.TemporaryDirectory() as raw:
source = Path(raw) / "sample.py"
source.write_text("import subprocess\n", encoding="utf-8")
result = self.run_cli(str(source), "--max-review-indicators", "0", "--quiet")
self.assertEqual(4, result.returncode)
self.assertIn("review indicators", result.stderr)
if __name__ == "__main__":
unittest.main()