mirror of
https://github.com/bikini/patchwork
synced 2026-06-27 08:08:41 +00:00
162 lines
6.5 KiB
Python
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()
|