Files
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

1615 lines
62 KiB
Python
Executable File

#!/usr/bin/env python3
"""Hermetic tests for utils/hotswap/hotswap_reduce.py."""
from __future__ import annotations
import argparse
import importlib.util
import json
import os
import stat
import sys
import tempfile
import textwrap
import time
import unittest
from pathlib import Path
from typing import Optional, Union
from unittest import mock
SCRIPT = Path(__file__).resolve().parents[2] / "utils" / "hotswap" / "hotswap_reduce.py"
SPEC = importlib.util.spec_from_file_location("hotswap_reduce", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
hotswap_reduce = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = hotswap_reduce
SPEC.loader.exec_module(hotswap_reduce)
def write_executable(path: Path, body: str) -> Path:
path.write_text(
"#!/usr/bin/env python3\n" + textwrap.dedent(body),
encoding="utf-8",
)
path.chmod(path.stat().st_mode | stat.S_IXUSR)
return path
def write_bundle(
root: Path,
object_contents: list[bytes],
metadata: object,
names: Optional[list[str]] = None,
) -> Path:
objects = []
names = names or [f"object-{index}.co" for index in range(len(object_contents))]
for index, (name, content) in enumerate(zip(names, object_contents)):
path = root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(content)
objects.append({"id": f"id-{index}", "path": name})
bundle = {
"format": hotswap_reduce.BUNDLE_FORMAT,
"version": hotswap_reduce.BUNDLE_VERSION,
"code_objects": objects,
"metadata": metadata,
}
path = root / "bundle.json"
path.write_text(json.dumps(bundle), encoding="utf-8")
return path
def make_args(
bundle: Path,
output: Path,
predicate: Union[Path, str],
predicate_args: list[str],
**overrides: object,
) -> argparse.Namespace:
values: dict[str, object] = {
"bundle": bundle,
"code_object": None,
"worklist": None,
"metadata": None,
"output": output,
"predicate": str(predicate),
"predicate_arg": predicate_args,
"interesting_exit_code": 0,
"predicate_runs": 1,
"timeout": 5.0,
"cache_file": None,
"cache_dependency": [],
"cache_tag": [],
"allow_empty_bundle": False,
"allow_remove_section": [],
"protect_section": [],
"readobj": "llvm-readobj",
"objcopy": "llvm-objcopy",
"tool_timeout": 5.0,
}
values.update(overrides)
return argparse.Namespace(**values)
class DdminTests(unittest.TestCase):
def test_finds_one_minimal_required_pair(self) -> None:
attempts = []
def interesting(kept: list[str], removed: list[str]) -> bool:
attempts.append((tuple(kept), tuple(removed)))
return "b" in kept and "e" in kept
result = hotswap_reduce.ddmin(list("abcdef"), interesting)
self.assertEqual(result, ["b", "e"])
self.assertTrue(attempts)
for index in range(len(result)):
candidate = result[:index] + result[index + 1 :]
self.assertFalse("b" in candidate and "e" in candidate)
def test_respects_minimum_size(self) -> None:
result = hotswap_reduce.ddmin(
[1, 2, 3],
lambda kept, removed: True,
minimum_size=1,
)
self.assertEqual(len(result), 1)
def test_duplicate_items_are_reduced_by_position(self) -> None:
result = hotswap_reduce.ddmin(
["x", "x", "y"],
lambda kept, removed: kept.count("x") >= 1,
)
self.assertEqual(result, ["x"])
def test_exhaustive_monotone_predicates_are_one_minimal(self) -> None:
for size in range(7):
items = list(range(size))
for required_bits in range(1 << size):
required = {item for item in items if required_bits & (1 << item)}
result = hotswap_reduce.ddmin(
items,
lambda kept, removed, required=required: required.issubset(kept),
)
self.assertEqual(set(result), required)
for index in range(len(result)):
self.assertFalse(
required.issubset(result[:index] + result[index + 1 :])
)
class BundleValidationTests(unittest.TestCase):
def test_rejects_malformed_metadata_list(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
bundle = write_bundle(root, [b"object"], {"cases": {}})
with self.assertRaisesRegex(
hotswap_reduce.ReducerError,
r"metadata\.cases must be a JSON list",
):
hotswap_reduce.load_bundle(bundle)
def test_rejects_duplicate_ids(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
(root / "a").write_bytes(b"a")
(root / "b").write_bytes(b"b")
bundle = {
"format": hotswap_reduce.BUNDLE_FORMAT,
"version": 1,
"code_objects": [
{"id": "same", "path": "a"},
{"id": "same", "path": "b"},
],
}
path = root / "bundle.json"
path.write_text(json.dumps(bundle), encoding="utf-8")
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "duplicate code object id"
):
hotswap_reduce.load_bundle(path)
def test_bundle_paths_cannot_escape_bundle_directory(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
bundle_root = root / "bundle"
bundle_root.mkdir()
outside = root / "outside.co"
outside.write_bytes(b"outside")
bundle = {
"format": hotswap_reduce.BUNDLE_FORMAT,
"version": 1,
"code_objects": [{"id": "outside", "path": "../outside.co"}],
}
bundle_path = bundle_root / "bundle.json"
bundle_path.write_text(json.dumps(bundle), encoding="utf-8")
with self.assertRaisesRegex(hotswap_reduce.ReducerError, "escapes"):
hotswap_reduce.load_bundle(bundle_path)
bundle["code_objects"][0]["path"] = str(outside)
bundle_path.write_text(json.dumps(bundle), encoding="utf-8")
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "must be relative"
):
hotswap_reduce.load_bundle(bundle_path)
def test_bundle_symlinks_cannot_escape_bundle_directory(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
bundle_root = root / "bundle"
bundle_root.mkdir()
outside = root / "outside.co"
outside.write_bytes(b"outside")
link = bundle_root / "linked.co"
try:
link.symlink_to(outside)
except OSError as error:
self.skipTest(f"symlinks unavailable: {error}")
bundle = {
"format": hotswap_reduce.BUNDLE_FORMAT,
"version": 1,
"code_objects": [{"id": "outside", "path": link.name}],
}
bundle_path = bundle_root / "bundle.json"
bundle_path.write_text(json.dumps(bundle), encoding="utf-8")
with self.assertRaisesRegex(hotswap_reduce.ReducerError, "escapes"):
hotswap_reduce.load_bundle(bundle_path)
def test_rejects_non_finite_json(self) -> None:
with tempfile.TemporaryDirectory() as name:
path = Path(name) / "bundle.json"
path.write_text('{"value": NaN}', encoding="utf-8")
with self.assertRaisesRegex(hotswap_reduce.ReducerError, "non-finite"):
hotswap_reduce.load_json(path)
def test_accepts_spaces_and_special_characters_in_paths(self) -> None:
with tempfile.TemporaryDirectory(prefix="reduce spaces [") as name:
root = Path(name)
bundle = write_bundle(
root,
[b"one"],
{},
names=["objects/a file [1] $x.co"],
)
candidate = hotswap_reduce.load_bundle(bundle)
self.assertEqual(candidate.code_objects[0].path.read_bytes(), b"one")
with tempfile.TemporaryDirectory() as output_name:
output = Path(output_name) / "candidate"
hotswap_reduce.materialize_candidate(candidate, output)
output_bundle = json.loads(
(output / "bundle.json").read_text(encoding="utf-8")
)
relative = output_bundle["code_objects"][0]["path"]
self.assertEqual((output / relative).read_bytes(), b"one")
def test_materialization_rejects_changed_source_bytes(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
bundle = write_bundle(root, [b"original"], {})
candidate = hotswap_reduce.load_bundle(bundle)
candidate.code_objects[0].path.write_bytes(b"changed")
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "changed while materializing"
):
hotswap_reduce.materialize_candidate(candidate, root / "candidate")
def test_loads_inventory_nul_worklist_and_selector_metadata(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
first = root / "object with spaces.co"
# Windows rejects control characters in file names. Preserve the
# newline-path coverage where the host permits it while still
# exercising NUL-delimited parsing on every platform.
second_name = (
"object;with-semicolon.co"
if os.name == "nt"
else "object\nwith-newline.co"
)
second = root / second_name
first.write_bytes(b"first")
second.write_bytes(b"second")
worklist = root / "selected.list"
worklist.write_bytes(
os.fsencode(first.resolve())
+ b"\0"
+ os.fsencode(second.resolve())
+ b"\0"
)
selection = root / "selection.json"
selection.write_text(
json.dumps(
{
"kind": "producer-selection",
"schema_version": 1,
"selected_tests": [
{"id": "test-a"},
{"id": "test-b"},
],
}
),
encoding="utf-8",
)
candidate = hotswap_reduce.load_inputs(None, [], worklist, selection)
self.assertEqual(
[item.path for item in candidate.code_objects],
[first.resolve(), second.resolve()],
)
self.assertEqual(
candidate.metadata["selected_tests"],
[{"id": "test-a"}, {"id": "test-b"}],
)
def test_rejects_malformed_and_duplicate_nul_worklists(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
object_path = root / "object.co"
object_path.write_bytes(b"object")
worklist = root / "worklist"
worklist.write_bytes(os.fsencode(object_path.resolve()))
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "must end with NUL"
):
hotswap_reduce.load_nul_worklist(worklist)
encoded_path = os.fsencode(object_path.resolve())
worklist.write_bytes(encoded_path + b"\0" + encoded_path + b"\0")
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "duplicate worklist path"
):
hotswap_reduce.load_nul_worklist(worklist)
class PredicateTests(unittest.TestCase):
def make_candidate(self, root: Path, value: bytes = b"interesting"):
path = root / "input.co"
path.write_bytes(value)
return hotswap_reduce.Candidate(
(
hotswap_reduce.CodeObject(
"id",
path.name,
path,
hotswap_reduce.sha256_file(path),
),
),
{},
)
def make_runner(
self,
root: Path,
script: Path,
arguments: list[str],
**overrides: object,
):
values = {
"argv_template": [sys.executable, str(script)] + arguments,
"interesting_exit_code": 0,
"runs": 1,
"timeout": 5.0,
"work_root": root,
"cache": hotswap_reduce.PredicateCache(None),
}
values.update(overrides)
return hotswap_reduce.PredicateRunner(**values)
def test_cache_uses_candidate_content(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
counter = root / "count"
script = root / "predicate.py"
script.write_text(
textwrap.dedent(
f"""
import pathlib
counter = pathlib.Path({str(counter)!r})
count = int(counter.read_text()) if counter.exists() else 0
counter.write_text(str(count + 1))
raise SystemExit(0)
"""
),
encoding="utf-8",
)
runner = self.make_runner(
root,
script,
["{input}"],
)
candidate = self.make_candidate(root)
first = runner.evaluate(candidate)
second = runner.evaluate(candidate)
self.assertTrue(first.interesting)
self.assertFalse(first.cached)
self.assertTrue(second.cached)
self.assertEqual(counter.read_text(encoding="utf-8"), "1")
changed = self.make_candidate(root, b"changed")
runner.evaluate(changed)
self.assertEqual(counter.read_text(encoding="utf-8"), "2")
def test_persistent_cache_survives_runner_recreation(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
counter = root / "count"
cache_path = root / "predicate-cache.json"
script = root / "predicate.py"
script.write_text(
textwrap.dedent(
f"""
import pathlib
counter = pathlib.Path({str(counter)!r})
count = int(counter.read_text()) if counter.exists() else 0
counter.write_text(str(count + 1))
raise SystemExit(0)
"""
),
encoding="utf-8",
)
candidate = self.make_candidate(root)
first = self.make_runner(
root,
script,
["{input}"],
cache=hotswap_reduce.PredicateCache(cache_path),
)
self.assertFalse(first.evaluate(candidate).cached)
second = self.make_runner(
root,
script,
["{input}"],
cache=hotswap_reduce.PredicateCache(cache_path),
)
self.assertTrue(second.evaluate(candidate).cached)
self.assertEqual(counter.read_text(encoding="utf-8"), "1")
cache = json.loads(cache_path.read_text(encoding="utf-8"))
self.assertEqual(cache["format"], hotswap_reduce.CACHE_FORMAT)
self.assertEqual(cache["version"], 1)
def test_cached_and_uncached_results_are_equivalent(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
cache_path = root / "predicate-cache.json"
script = root / "predicate.py"
script.write_text(
"print('stable output')\n"
"print('stable error', file=__import__('sys').stderr)\n"
"raise SystemExit(7)\n",
encoding="utf-8",
)
candidate = self.make_candidate(root)
runner = self.make_runner(
root,
script,
["{input}"],
interesting_exit_code=7,
runs=2,
cache=hotswap_reduce.PredicateCache(cache_path),
)
uncached = runner.evaluate(candidate)
cached = runner.evaluate(candidate)
self.assertFalse(uncached.cached)
self.assertTrue(cached.cached)
self.assertEqual(uncached.status, cached.status)
self.assertEqual(uncached.exit_codes, cached.exit_codes)
self.assertEqual(uncached.stdout, cached.stdout)
self.assertEqual(uncached.stderr, cached.stderr)
self.assertEqual(uncached.for_log(), cached.for_log())
self.assertNotIn("cached", cached.for_log())
def test_corrupt_matching_cache_entry_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
cache_path = root / "predicate-cache.json"
script = root / "predicate.py"
script.write_text("raise SystemExit(0)\n", encoding="utf-8")
candidate = self.make_candidate(root)
runner = self.make_runner(
root,
script,
["{input}"],
cache=hotswap_reduce.PredicateCache(cache_path),
)
runner.evaluate(candidate)
cache = json.loads(cache_path.read_text(encoding="utf-8"))
entry = next(iter(cache["entries"].values()))
entry["exit_codes"] = []
cache_path.write_text(json.dumps(cache), encoding="utf-8")
recreated = self.make_runner(
root,
script,
["{input}"],
cache=hotswap_reduce.PredicateCache(cache_path),
)
with self.assertRaisesRegex(hotswap_reduce.ReducerError, "corrupt entry"):
recreated.evaluate(candidate)
def test_persistent_cache_invalidates_changed_predicate_script(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
counter = root / "count"
cache_path = root / "predicate-cache.json"
script = root / "predicate.py"
def write_predicate(marker: str) -> None:
script.write_text(
textwrap.dedent(
f"""
# identity: {marker}
import pathlib
counter = pathlib.Path({str(counter)!r})
count = int(counter.read_text()) if counter.exists() else 0
counter.write_text(str(count + 1))
raise SystemExit(0)
"""
),
encoding="utf-8",
)
candidate = self.make_candidate(root)
write_predicate("first")
first = self.make_runner(
root,
script,
["{input}"],
cache=hotswap_reduce.PredicateCache(cache_path),
)
self.assertFalse(first.evaluate(candidate).cached)
write_predicate("second")
second = self.make_runner(
root,
script,
["{input}"],
cache=hotswap_reduce.PredicateCache(cache_path),
)
self.assertFalse(second.evaluate(candidate).cached)
self.assertEqual(counter.read_text(encoding="utf-8"), "2")
def test_persistent_cache_invalidates_dependency_and_tag(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
counter = root / "count"
cache_path = root / "predicate-cache.json"
dependency = root / "libamd_comgr.so"
dependency.write_bytes(b"build-one")
script = root / "predicate.py"
script.write_text(
textwrap.dedent(
f"""
import pathlib
counter = pathlib.Path({str(counter)!r})
count = int(counter.read_text()) if counter.exists() else 0
counter.write_text(str(count + 1))
raise SystemExit(0)
"""
),
encoding="utf-8",
)
candidate = self.make_candidate(root)
def make(cache_tag: str):
return self.make_runner(
root,
script,
["{input}"],
cache=hotswap_reduce.PredicateCache(cache_path),
cache_dependencies=[dependency],
cache_tags=[cache_tag],
)
self.assertFalse(make("environment-one").evaluate(candidate).cached)
self.assertTrue(make("environment-one").evaluate(candidate).cached)
dependency.write_bytes(b"build-two")
self.assertFalse(make("environment-one").evaluate(candidate).cached)
self.assertFalse(make("environment-two").evaluate(candidate).cached)
self.assertEqual(counter.read_text(encoding="utf-8"), "3")
def test_changed_identity_during_run_is_rejected(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
dependency = root / "dependency"
dependency.write_bytes(b"one")
script = root / "predicate.py"
script.write_text("raise SystemExit(0)\n", encoding="utf-8")
runner = self.make_runner(
root,
script,
["{input}"],
cache_dependencies=[dependency],
)
dependency.write_bytes(b"two")
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "identity changed"
):
runner.evaluate(self.make_candidate(root))
def test_changed_identity_mode_invalidates_cached_result(self) -> None:
if os.name == "nt":
self.skipTest("POSIX executable-mode semantics")
with tempfile.TemporaryDirectory() as name:
root = Path(name)
executable = write_executable(
root / "predicate",
"""
raise SystemExit(0)
""",
)
candidate = self.make_candidate(root)
runner = hotswap_reduce.PredicateRunner(
[str(executable), "{input}"],
0,
1,
5.0,
root,
hotswap_reduce.PredicateCache(None),
)
self.assertFalse(runner.evaluate(candidate).cached)
executable.chmod(executable.stat().st_mode & ~stat.S_IXUSR)
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "identity changed"
):
runner.evaluate(candidate)
def test_candidate_digest_does_not_reread_large_objects(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
candidate = self.make_candidate(root)
with mock.patch.object(
hotswap_reduce,
"sha256_file",
side_effect=AssertionError("unexpected object reread"),
):
digest = candidate.digest()
self.assertRegex(digest, r"^[0-9a-f]{64}$")
def test_nonzero_exit_can_mean_interesting(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
script = root / "predicate.py"
script.write_text("raise SystemExit(17)\n", encoding="utf-8")
runner = self.make_runner(
root,
script,
["{input}"],
interesting_exit_code=17,
)
result = runner.evaluate(self.make_candidate(root))
self.assertEqual(result.status, "interesting")
self.assertEqual(result.exit_codes, (17,))
def test_timeout_is_rejected_and_not_cached(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
script = root / "predicate.py"
script.write_text("import time\ntime.sleep(10)\n", encoding="utf-8")
runner = self.make_runner(
root,
script,
[],
timeout=0.02,
)
candidate = self.make_candidate(root)
first = runner.evaluate(candidate)
second = runner.evaluate(candidate)
self.assertEqual(first.status, "timeout")
self.assertEqual(second.status, "timeout")
self.assertFalse(second.cached)
def test_flaky_predicate_is_rejected_and_not_cached(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
counter = root / "count"
script = root / "predicate.py"
script.write_text(
textwrap.dedent(
"""
import pathlib
import sys
path = pathlib.Path(sys.argv[1])
count = int(path.read_text()) if path.exists() else 0
path.write_text(str(count + 1))
raise SystemExit(count % 2)
"""
),
encoding="utf-8",
)
runner = self.make_runner(
root,
script,
[str(counter)],
runs=2,
)
result = runner.evaluate(self.make_candidate(root))
self.assertEqual(result.status, "flaky")
self.assertFalse(result.cached)
def test_different_noninteresting_exit_codes_are_flaky(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
counter = root / "count"
script = root / "predicate.py"
script.write_text(
textwrap.dedent(
"""
import pathlib
import sys
path = pathlib.Path(sys.argv[1])
count = int(path.read_text()) if path.exists() else 0
path.write_text(str(count + 1))
raise SystemExit(1 + count % 2)
"""
),
encoding="utf-8",
)
runner = self.make_runner(root, script, [str(counter)], runs=2)
result = runner.evaluate(self.make_candidate(root))
self.assertEqual(result.status, "flaky")
self.assertEqual(result.exit_codes, (1, 2))
self.assertFalse(result.cached)
def test_timeout_terminates_predicate_descendants(self) -> None:
if os.name == "nt":
self.skipTest("POSIX process-group behavior")
with tempfile.TemporaryDirectory() as name:
root = Path(name)
marker = root / "descendant-ran"
child_code = (
"import pathlib,time;"
"time.sleep(0.3);"
f"pathlib.Path({str(marker)!r}).write_text('leaked')"
)
script = root / "predicate.py"
script.write_text(
"import subprocess,sys,time\n"
f"subprocess.Popen([sys.executable, '-c', {child_code!r}])\n"
"time.sleep(10)\n",
encoding="utf-8",
)
runner = self.make_runner(root, script, [], timeout=0.05)
result = runner.evaluate(self.make_candidate(root))
self.assertEqual(result.status, "timeout")
time.sleep(0.5)
self.assertFalse(marker.exists())
def test_completed_predicate_does_not_leave_descendants(self) -> None:
if os.name == "nt":
self.skipTest("POSIX process-group behavior")
with tempfile.TemporaryDirectory() as name:
root = Path(name)
marker = root / "descendant-ran"
child_code = (
"import pathlib,time;"
"time.sleep(0.3);"
f"pathlib.Path({str(marker)!r}).write_text('leaked')"
)
script = root / "predicate.py"
script.write_text(
"import subprocess,sys\n"
f"subprocess.Popen([sys.executable, '-c', {child_code!r}])\n"
"raise SystemExit(0)\n",
encoding="utf-8",
)
runner = self.make_runner(root, script, [])
result = runner.evaluate(self.make_candidate(root))
self.assertTrue(result.interesting)
time.sleep(0.5)
self.assertFalse(marker.exists())
def test_capture_is_bounded(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
script = root / "predicate.py"
script.write_text(
f"print('x' * {hotswap_reduce.CAPTURE_LIMIT * 100})\n"
"raise SystemExit(0)\n",
encoding="utf-8",
)
runner = self.make_runner(root, script, [])
result = runner.evaluate(self.make_candidate(root))
self.assertTrue(result.interesting)
self.assertLessEqual(
len(result.stdout),
hotswap_reduce.CAPTURE_LIMIT + len("\n<truncated>"),
)
self.assertTrue(result.stdout.endswith("\n<truncated>"))
def test_unknown_placeholder_fails_before_launch(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "unknown.*placeholder"
):
hotswap_reduce.PredicateRunner(
[sys.executable, "{unknown}"],
0,
1,
1.0,
root,
hotswap_reduce.PredicateCache(None),
)
def test_regular_file_argv_is_resolved_before_workspace_change(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
script = root / "predicate.py"
script.write_text("raise SystemExit(0)\n", encoding="utf-8")
original_directory = Path.cwd()
try:
os.chdir(root)
runner = hotswap_reduce.PredicateRunner(
[sys.executable, "predicate.py", "{input}"],
0,
1,
5.0,
root,
hotswap_reduce.PredicateCache(None),
)
finally:
os.chdir(original_directory)
self.assertEqual(runner.argv_template[1], str(script.resolve()))
self.assertTrue(runner.evaluate(self.make_candidate(root)).interesting)
def test_input_placeholder_rejects_multi_object_candidate(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
one = self.make_candidate(root, b"one")
second_path = root / "second.co"
second_path.write_bytes(b"two")
two = hotswap_reduce.Candidate(
one.code_objects
+ (
hotswap_reduce.CodeObject(
"second",
second_path.name,
second_path,
hotswap_reduce.sha256_file(second_path),
),
),
{},
)
script = root / "predicate.py"
script.write_text("raise SystemExit(0)\n", encoding="utf-8")
runner = self.make_runner(root, script, ["{input}"])
with self.assertRaisesRegex(hotswap_reduce.ReducerError, "exactly one"):
runner.evaluate(two)
def test_nonfinite_timeouts_are_rejected_by_argument_parser(self) -> None:
parser = hotswap_reduce.make_argument_parser()
base_arguments = [
"--code-object",
"input.co",
"--output",
"output",
"--predicate",
"predicate",
]
for option in ("--timeout", "--tool-timeout"):
for value in ("nan", "inf", "-inf", "0", "-1"):
with mock.patch("sys.stderr"):
with self.assertRaises(SystemExit):
parser.parse_args(base_arguments + [option, value])
class FakeElfToolTests(unittest.TestCase):
def write_readobj(
self,
root: Path,
sections: list[tuple[str, bool]],
architecture: str = "amdgcn",
) -> Path:
return write_executable(
root / "fake readobj",
f"""
import json
import pathlib
import sys
sections = {sections!r}
contents = pathlib.Path(sys.argv[-1]).read_bytes().decode(
"utf-8", errors="ignore"
)
removed = {{
word.split("=", 1)[1]
for word in contents.split()
if word.startswith("--remove-section=")
}}
value = [{{
"FileSummary": {{
"Format": "elf64-amdgpu",
"Arch": {architecture!r},
}},
"Sections": [
{{
"Section": {{
"Name": {{"Name": name}},
"Flags": {{
"Flags": ([{{"Name": "SHF_ALLOC"}}] if allocated else [])
}},
}}
}}
for name, allocated in sections
if name not in removed
],
}}]
print(json.dumps(value))
""",
)
def write_objcopy(
self,
root: Path,
fail: bool = False,
annotate: bool = False,
extra_removal: Optional[str] = None,
) -> Path:
return write_executable(
root / "fake objcopy",
f"""
import pathlib
import shutil
import sys
if {fail!r}:
print("intentional objcopy failure", file=sys.stderr)
raise SystemExit(4)
shutil.copyfile(pathlib.Path(sys.argv[-2]), pathlib.Path(sys.argv[-1]))
if {annotate!r}:
with pathlib.Path(sys.argv[-1]).open("ab") as stream:
stream.write((" " + " ".join(sys.argv[1:-2])).encode())
if {extra_removal!r} is not None:
with pathlib.Path(sys.argv[-1]).open("ab") as stream:
stream.write((" --remove-section=" + {extra_removal!r}).encode())
""",
)
def test_policy_only_allows_nonallocated_unprotected_sections(self) -> None:
if os.name == "nt":
self.skipTest("executable script fixtures require a POSIX host")
with tempfile.TemporaryDirectory() as name:
root = Path(name)
readobj = self.write_readobj(
root,
[
("", False),
(".text", True),
(".note", False),
(".AMDGPU.config", False),
(".rela.debug_info", False),
(".group", False),
(".debug_info", False),
(".comment", False),
(".allocated_debug", True),
],
)
objcopy = self.write_objcopy(root)
tools = hotswap_reduce.ElfSectionTools(
str(readobj),
str(objcopy),
5.0,
["*"],
[".comment"],
root / "artifacts",
)
tools.artifact_root.mkdir()
obj = root / "object.co"
obj.write_bytes(b"ELF")
self.assertEqual(tools.removable_sections(obj), [".debug_info"])
def test_duplicate_name_is_protected_if_any_instance_is_allocated(self) -> None:
if os.name == "nt":
self.skipTest("executable script fixtures require a POSIX host")
with tempfile.TemporaryDirectory() as name:
root = Path(name)
readobj = self.write_readobj(
root,
[
(".debug_duplicate", False),
(".debug_duplicate", True),
(".debug_safe", False),
],
)
objcopy = self.write_objcopy(root)
tools = hotswap_reduce.ElfSectionTools(
str(readobj),
str(objcopy),
5.0,
[".debug_*"],
[],
root / "artifacts",
)
tools.artifact_root.mkdir()
obj = root / "object.co"
obj.write_bytes(b"ELF")
self.assertEqual(tools.removable_sections(obj), [".debug_safe"])
def test_missing_tool_is_reported(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "could not find executable"
):
hotswap_reduce.ElfSectionTools(
str(root / "does-not-exist"),
str(root / "also-missing"),
1.0,
["*"],
[],
root,
)
def test_rejects_non_amdgpu_elf(self) -> None:
if os.name == "nt":
self.skipTest("executable script fixtures require a POSIX host")
with tempfile.TemporaryDirectory() as name:
root = Path(name)
readobj = self.write_readobj(root, [(".debug_info", False)], "x86_64")
objcopy = self.write_objcopy(root)
artifacts = root / "artifacts"
artifacts.mkdir()
tools = hotswap_reduce.ElfSectionTools(
str(readobj),
str(objcopy),
5.0,
["*"],
[],
artifacts,
)
obj = root / "object.co"
obj.write_bytes(b"ELF")
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "requires an AMDGPU ELF"
):
tools.removable_sections(obj)
def test_objcopy_failure_does_not_replace_input(self) -> None:
if os.name == "nt":
self.skipTest("executable script fixtures require a POSIX host")
with tempfile.TemporaryDirectory() as name:
root = Path(name)
readobj = self.write_readobj(root, [(".debug_info", False)])
objcopy = self.write_objcopy(root, fail=True)
artifacts = root / "artifacts"
artifacts.mkdir()
tools = hotswap_reduce.ElfSectionTools(
str(readobj),
str(objcopy),
5.0,
[".debug_*"],
[],
artifacts,
)
path = root / "object.co"
path.write_bytes(b"original")
code_object = hotswap_reduce.CodeObject(
"id", path.name, path, hotswap_reduce.sha256_file(path)
)
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "intentional objcopy failure"
):
tools.remove_sections(code_object, [".debug_info"])
self.assertEqual(path.read_bytes(), b"original")
self.assertEqual(list(artifacts.iterdir()), [])
def test_malformed_readobj_json_is_reported(self) -> None:
if os.name == "nt":
self.skipTest("executable script fixtures require a POSIX host")
with tempfile.TemporaryDirectory() as name:
root = Path(name)
readobj = write_executable(root / "fake readobj", "print('not json')\n")
objcopy = self.write_objcopy(root)
artifacts = root / "artifacts"
artifacts.mkdir()
tools = hotswap_reduce.ElfSectionTools(
str(readobj),
str(objcopy),
5.0,
["*"],
[],
artifacts,
)
path = root / "object.co"
path.write_bytes(b"ELF")
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "malformed section JSON"
):
tools.removable_sections(path)
def test_objcopy_output_must_remove_requested_sections(self) -> None:
if os.name == "nt":
self.skipTest("executable script fixtures require a POSIX host")
with tempfile.TemporaryDirectory() as name:
root = Path(name)
readobj = self.write_readobj(root, [(".debug_info", False)])
objcopy = self.write_objcopy(root)
artifacts = root / "artifacts"
artifacts.mkdir()
tools = hotswap_reduce.ElfSectionTools(
str(readobj),
str(objcopy),
5.0,
[".debug_*"],
[],
artifacts,
)
path = root / "object.co"
path.write_bytes(b"ELF")
code_object = hotswap_reduce.CodeObject(
"id", path.name, path, hotswap_reduce.sha256_file(path)
)
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "requested sections remain"
):
tools.remove_sections(code_object, [".debug_info"])
self.assertEqual(list(artifacts.iterdir()), [])
def test_objcopy_output_must_preserve_unrequested_sections(self) -> None:
if os.name == "nt":
self.skipTest("executable script fixtures require a POSIX host")
with tempfile.TemporaryDirectory() as name:
root = Path(name)
readobj = self.write_readobj(
root,
[
(".debug_info", False),
(".comment", False),
],
)
objcopy = self.write_objcopy(root, annotate=True, extra_removal=".comment")
artifacts = root / "artifacts"
artifacts.mkdir()
tools = hotswap_reduce.ElfSectionTools(
str(readobj),
str(objcopy),
5.0,
[".debug_info"],
[],
artifacts,
)
path = root / "object.co"
path.write_bytes(b"ELF")
code_object = hotswap_reduce.CodeObject(
"id", path.name, path, hotswap_reduce.sha256_file(path)
)
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "unrequested sections disappeared"
):
tools.remove_sections(code_object, [".debug_info"])
self.assertEqual(list(artifacts.iterdir()), [])
class EndToEndTests(unittest.TestCase):
def test_reduces_bundle_and_metadata_and_writes_reproducer(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
bundle = write_bundle(
root,
[b"drop", b"KEEP", b"also-drop"],
{
"kernels": ["drop", "kernel-keep"],
"cases": [
{"name": "drop", "arguments": ["drop"]},
{
"name": "case-keep",
"arguments": ["drop", "arg-keep"],
},
],
"arguments": ["drop", "global-keep"],
},
)
predicate = root / "predicate.py"
predicate.write_text(
textwrap.dedent(
"""
import json
import pathlib
import sys
bundle_path = pathlib.Path(sys.argv[1])
value = json.loads(bundle_path.read_text())
root = bundle_path.parent
object_data = b"".join(
(root / item["path"]).read_bytes()
for item in value["code_objects"]
)
metadata = json.loads((root / value["metadata"]).read_text())
text = json.dumps(metadata)
interesting = (
b"KEEP" in object_data
and "kernel-keep" in text
and "case-keep" in text
and "arg-keep" in text
and "global-keep" in text
)
raise SystemExit(0 if interesting else 1)
"""
),
encoding="utf-8",
)
output = root / "reduced output [x]"
args = make_args(
bundle,
output,
sys.executable,
[str(predicate), "{bundle}"],
)
final, log = hotswap_reduce.run_from_arguments(args)
self.assertEqual(
[item.path.read_bytes() for item in final.code_objects],
[b"KEEP"],
)
self.assertEqual(final.metadata["kernels"], ["kernel-keep"])
self.assertEqual(
[case["name"] for case in final.metadata["cases"]],
["case-keep"],
)
self.assertEqual(final.metadata["cases"][0]["arguments"], ["arg-keep"])
self.assertEqual(final.metadata["arguments"], ["global-keep"])
self.assertEqual(log["format"], hotswap_reduce.LOG_FORMAT)
self.assertEqual(log["version"], 1)
self.assertTrue(log["transformations"])
self.assertTrue(any(item["accepted"] for item in log["transformations"]))
published_log = json.loads(
(output / "reduction-log.json").read_text(encoding="utf-8")
)
self.assertEqual(published_log, log)
self.assertEqual(published_log["reproduction"]["argv"][-1], "bundle.json")
def test_deterministic_output_and_log(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
bundle = write_bundle(
root,
[b"drop", b"keep"],
{"cases": ["drop", "keep"]},
)
predicate = root / "predicate.py"
predicate.write_text(
textwrap.dedent(
"""
import pathlib
import sys
raise SystemExit(
0 if b"keep" in pathlib.Path(sys.argv[1]).read_bytes()
else 1
)
"""
),
encoding="utf-8",
)
logs = []
bundles = []
for index in range(2):
output = root / f"out-{index}"
args = make_args(
bundle,
output,
sys.executable,
[str(predicate), "{metadata}"],
)
_, log = hotswap_reduce.run_from_arguments(args)
logs.append(log)
bundles.append(
(output / "bundle.json").read_bytes()
+ (output / "metadata.json").read_bytes()
)
self.assertEqual(logs[0], logs[1])
self.assertEqual(bundles[0], bundles[1])
def test_offline_reference_manifest_differential(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
bundle = write_bundle(
root,
[b"same-a", b"candidate-differs", b"same-b"],
{"cases": ["a", "discrepant-case", "b"]},
)
reference = root / "pr3598-reference.json"
reference.write_text(
json.dumps({"forbidden_sha256": "unused"}),
encoding="utf-8",
)
predicate = root / "offline-diff.py"
predicate.write_text(
textwrap.dedent(
"""
import json
import pathlib
import sys
reference = json.loads(pathlib.Path(sys.argv[1]).read_text())
bundle_path = pathlib.Path(sys.argv[2])
bundle = json.loads(bundle_path.read_text())
root = bundle_path.parent
objects = [
(root / entry["path"]).read_bytes()
for entry in bundle["code_objects"]
]
metadata = json.loads((root / bundle["metadata"]).read_text())
difference = (
b"candidate-differs" in objects
and "discrepant-case" in metadata.get("cases", [])
and "forbidden_sha256" in reference
)
raise SystemExit(0 if difference else 1)
"""
),
encoding="utf-8",
)
output = root / "reduced-offline-difference"
args = make_args(
bundle,
output,
sys.executable,
[str(predicate), str(reference), "{bundle}"],
predicate_runs=2,
)
final, _ = hotswap_reduce.run_from_arguments(args)
self.assertEqual(
[item.path.read_bytes() for item in final.code_objects],
[b"candidate-differs"],
)
self.assertEqual(final.metadata["cases"], ["discrepant-case"])
def test_repeats_hierarchical_passes_to_a_fixpoint(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
bundle = write_bundle(
root,
[b"A", b"B"],
{"arguments": ["X", "Y"]},
)
predicate = root / "nonmonotone-predicate.py"
predicate.write_text(
textwrap.dedent(
"""
import json
import pathlib
import sys
bundle_path = pathlib.Path(sys.argv[1])
bundle = json.loads(bundle_path.read_text())
root = bundle_path.parent
objects = {
(root / entry["path"]).read_bytes().decode()
for entry in bundle["code_objects"]
}
metadata = json.loads((root / bundle["metadata"]).read_text())
arguments = metadata["arguments"]
interesting = (
(objects == {"A", "B"} and arguments in (["X", "Y"], ["X"]))
or (objects == {"A"} and arguments == ["X"])
)
raise SystemExit(0 if interesting else 1)
"""
),
encoding="utf-8",
)
output = root / "reduced"
args = make_args(
bundle,
output,
sys.executable,
[str(predicate), "{bundle}"],
)
final, _ = hotswap_reduce.run_from_arguments(args)
self.assertEqual(
[item.path.read_bytes() for item in final.code_objects],
[b"A"],
)
self.assertEqual(final.metadata["arguments"], ["X"])
def test_section_reduction_with_fake_llvm_tools(self) -> None:
if os.name == "nt":
self.skipTest("executable script fixtures require a POSIX host")
with tempfile.TemporaryDirectory() as name:
root = Path(name)
bundle = write_bundle(root, [b"ELF"], {})
fixture = FakeElfToolTests()
readobj = fixture.write_readobj(
root,
[
("", False),
(".text", True),
(".debug_info", False),
(".note", False),
],
)
objcopy = fixture.write_objcopy(root, annotate=True)
predicate = root / "predicate.py"
predicate.write_text("raise SystemExit(0)\n", encoding="utf-8")
output = root / "output"
args = make_args(
bundle,
output,
sys.executable,
[str(predicate), "{input}"],
allow_remove_section=["*"],
readobj=str(readobj),
objcopy=str(objcopy),
)
final, log = hotswap_reduce.run_from_arguments(args)
self.assertIn(
b"--remove-section=.debug_info",
final.code_objects[0].path.read_bytes(),
)
accepted_sections = [
item["transformation"]["remove_sections"]
for item in log["transformations"]
if item["pass"] == "elf-sections" and item["accepted"]
]
self.assertEqual(accepted_sections, [[".debug_info"]])
def test_unsupported_section_format_publishes_nothing(self) -> None:
if os.name == "nt":
self.skipTest("executable script fixtures require a POSIX host")
with tempfile.TemporaryDirectory() as name:
root = Path(name)
bundle = write_bundle(root, [b"ELF"], {})
fixture = FakeElfToolTests()
readobj = fixture.write_readobj(
root, [(".debug_info", False)], architecture="x86_64"
)
objcopy = fixture.write_objcopy(root)
predicate = root / "predicate.py"
predicate.write_text("raise SystemExit(0)\n", encoding="utf-8")
output = root / "output"
args = make_args(
bundle,
output,
sys.executable,
[str(predicate), "{input}"],
allow_remove_section=[".debug_*"],
readobj=str(readobj),
objcopy=str(objcopy),
)
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "requires an AMDGPU ELF"
):
hotswap_reduce.run_from_arguments(args)
self.assertFalse(output.exists())
def test_existing_output_is_never_overwritten(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
bundle = write_bundle(root, [b"original"], {})
output = root / "output"
output.mkdir()
marker = output / "marker"
marker.write_bytes(b"preserve")
args = make_args(
bundle,
output,
sys.executable,
["-c", "raise SystemExit(0)", "{bundle}"],
)
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "refusing to overwrite"
):
hotswap_reduce.run_from_arguments(args)
self.assertEqual(marker.read_bytes(), b"preserve")
def test_cache_cannot_overwrite_input_or_live_inside_output(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
bundle = write_bundle(root, [b"original"], {})
object_path = root / "object-0.co"
predicate = root / "predicate.py"
predicate.write_text("raise SystemExit(0)\n", encoding="utf-8")
output = root / "output"
for cache_path in (object_path, output / "cache.json", predicate):
args = make_args(
bundle,
output,
sys.executable,
[str(predicate), "{bundle}"],
cache_file=cache_path,
)
with self.assertRaisesRegex(
hotswap_reduce.ReducerError,
r"refusing to overwrite|must not be inside",
):
hotswap_reduce.run_from_arguments(args)
self.assertFalse(output.exists())
self.assertEqual(object_path.read_bytes(), b"original")
self.assertEqual(
predicate.read_text(encoding="utf-8"),
"raise SystemExit(0)\n",
)
def test_cache_cannot_overwrite_external_bundle_metadata(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
external_metadata = root / "metadata.json"
external_metadata.write_text(
json.dumps(
{
"format": hotswap_reduce.CACHE_FORMAT,
"version": hotswap_reduce.CACHE_VERSION,
"entries": {},
}
),
encoding="utf-8",
)
original_metadata = external_metadata.read_bytes()
bundle = write_bundle(root, [b"object"], external_metadata.name)
predicate = root / "predicate.py"
predicate.write_text("raise SystemExit(0)\n", encoding="utf-8")
args = make_args(
bundle,
root / "output",
sys.executable,
[str(predicate), "{bundle}"],
cache_file=external_metadata,
)
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "overwrite reducer input"
):
hotswap_reduce.run_from_arguments(args)
self.assertEqual(external_metadata.read_bytes(), original_metadata)
def test_identity_change_during_reduction_aborts_without_output(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
bundle = write_bundle(root, [b"one", b"two"], {})
dependency = root / "dependency"
dependency.write_bytes(b"original")
counter = root / "counter"
predicate = root / "predicate.py"
predicate.write_text(
textwrap.dedent(
f"""
import pathlib
counter = pathlib.Path({str(counter)!r})
dependency = pathlib.Path({str(dependency)!r})
count = int(counter.read_text()) if counter.exists() else 0
counter.write_text(str(count + 1))
if count == 1:
dependency.write_bytes(b"changed")
raise SystemExit(0)
"""
),
encoding="utf-8",
)
output = root / "output"
args = make_args(
bundle,
output,
sys.executable,
[str(predicate), "{bundle}"],
cache_dependency=[dependency],
)
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "identity changed"
):
hotswap_reduce.run_from_arguments(args)
self.assertFalse(output.exists())
def test_interruption_publishes_nothing_and_preserves_original(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
bundle = write_bundle(root, [b"one", b"two"], {})
output = root / "output"
args = make_args(
bundle,
output,
sys.executable,
["-c", "raise SystemExit(0)", "{bundle}"],
)
interesting = hotswap_reduce.PredicateResult("interesting", (0,), "", "")
with mock.patch.object(
hotswap_reduce.PredicateRunner,
"evaluate",
side_effect=[interesting, KeyboardInterrupt()],
):
with self.assertRaises(KeyboardInterrupt):
hotswap_reduce.run_from_arguments(args)
self.assertFalse(output.exists())
self.assertEqual((root / "object-0.co").read_bytes(), b"one")
self.assertEqual((root / "object-1.co").read_bytes(), b"two")
def test_publish_interruption_rolls_back_temporary_output(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
bundle = write_bundle(root, [b"original"], {})
candidate = hotswap_reduce.load_bundle(bundle)
output = root / "published"
real_replace = os.replace
def replace_or_interrupt(
source: str | bytes | os.PathLike[str] | os.PathLike[bytes],
destination: str | bytes | os.PathLike[str] | os.PathLike[bytes],
) -> None:
if Path(os.fsdecode(destination)) == output:
raise KeyboardInterrupt()
real_replace(source, destination)
with mock.patch.object(
hotswap_reduce.os,
"replace",
side_effect=replace_or_interrupt,
):
with self.assertRaises(KeyboardInterrupt):
hotswap_reduce.atomic_publish(
output,
candidate,
{"format": "test", "version": 1},
)
self.assertFalse(output.exists())
self.assertEqual((root / "object-0.co").read_bytes(), b"original")
temporary_outputs = list(root.glob(".published.*"))
self.assertEqual(temporary_outputs, [])
def test_uninteresting_original_publishes_nothing(self) -> None:
with tempfile.TemporaryDirectory() as name:
root = Path(name)
bundle = write_bundle(root, [b"object"], {})
predicate = root / "predicate.py"
predicate.write_text("raise SystemExit(1)\n", encoding="utf-8")
output = root / "output"
args = make_args(
bundle,
output,
sys.executable,
[str(predicate), "{bundle}"],
)
with self.assertRaisesRegex(
hotswap_reduce.ReducerError, "original input is not"
):
hotswap_reduce.run_from_arguments(args)
self.assertFalse(output.exists())
if __name__ == "__main__":
unittest.main()