DecBench run: stop on a lost evaluation, refuse parallel workers for a big binary

The 2026-10-17 run lost every bash evaluation to the OOM killer, evaluate_project
returned {} and the scoreboard dropped the rows without a word. run_gate.py
checks, after the driver, that each binary a decompiler rendered in this run has
a score key of that decompiler in evaluated/<binary>.toml; the 16-byte
'binary = "bash"' file, a missing file and a file that scores only other
decompilers exit 71. On the 2026-10-17 tree it reports 55 of 192 cells, all bash.

More than one worker is refused (exit 72) for a binary whose decompiled C is over
8 MB: before the run for C already in the tree, by evaluate_checkpointed.py, and
inside the driver before evaluate_project, as SystemExit because the driver
catches Exception there and goes on with {}. run_decbench.sh also expands its
empty arrays in a way bash 3.2 accepts.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01456qmqCGpCWJkrg6JUZNNM
This commit is contained in:
Priyanshu KumarandClaude Sonnet 5.5 committed 2026-10-23 17:23:30 +05:30
1 parent f71bfcd366
commit 499180ed21
6 files changed
+462 -12

No files matched your search

+20 -7
View File
@@ -107,14 +107,27 @@ Evaluating r2s's `bash` (a 13 MB C file for 1,540 functions) takes 5.2 GB in one
`evaluate_project` worker. Four workers on the 8 GB benchmark host are killed by
the OOM killer, `evaluate_project` then returns `{}` for the binary, and the
scoreboard silently drops it: the 2026-10-17 run scored no bash row for any
decompiler (angr kept 692 rows of one build). Run `bash` with `--workers 1`, check
`evaluated/<binary>.toml` is not 16 bytes, and read the scoreboard only over rows
every compared decompiler has (`compare_runs.py --exclude-project bash`): the
scoreboard's denominator is every row *anyone* scored, so a row only one decompiler
has counts as a miss for the others. A re-run of one decompiler is
decompiler (angr kept 692 rows of one build). Two gates in `run_gate.py` now stop
that, and `run_decbench.sh` runs both:
* `--workers` above 1 is refused for a binary whose decompiled C exceeds 8 MB
(exit 72): before the run, for C already in `--out`; inside the driver, before
`evaluate_project` (`decbench_cli.py` installs the guard, and it raises
`SystemExit` because the driver catches `Exception` there and carries on with
`{}`); and by `evaluate_checkpointed.py`. Run `bash` with `--workers 1`.
* After the driver, every binary it decompiled in this run needs an
`evaluated/<binary>.toml` that carries a score key of each decompiler that
rendered functions of it; a missing file, the 16-byte `binary = "bash"` and a
file that scores only other decompilers stop the run (exit 71) and name the
cells. Checked on the 2026-10-17 tree it reports 55 of 192 cells, all bash.
Read the scoreboard only over rows every compared decompiler has
(`compare_runs.py --exclude-project bash` for a tree that has none): its
denominator is every row *anyone* scored, so a row only one decompiler has counts
as a miss for the others. A re-run of one decompiler is
`DECBENCH_REDO_DECOMPILERS=r2sleigh_native` on a copy of the tree; DecBench's
`finalize_results.py --allow-drops` is needed when functions that used to be declined
(rows named by their address) are now rendered.
`finalize_results.py --allow-drops` is needed when functions that used to be
declined (rows named by their address) are now rendered.
Analysis tools
--------------
+2
View File
@@ -29,6 +29,7 @@ from decbench.metrics.base import Metric
from decbench.metrics.registry import MetricRegistry, register_metric
from decbench.models.metrics import AggregationType, MetricValue
from phase_timing import install_phase_timing
from run_gate import install_workers_guard
def register_vj_ged_if_needed() -> None:
@@ -99,6 +100,7 @@ def run_benchmark(arguments: list[str]) -> None:
raise SystemExit(f"no {script}: DecBench must be installed from a checkout (pip -e)")
sys.argv = [str(script), *arguments]
sys.path.insert(0, str(script.parent))
install_workers_guard() # before the driver imports `evaluate_project`
runpy.run_path(str(script), run_name="__main__")
+6
View File
@@ -10,6 +10,7 @@ re-scored without decompiling it again. This takes the stored `DecompilationResu
`project_source_functions`, `extract_source_cfgs` and DecBench's `evaluate_project` exactly as `main` does, writes the
scores into the checkpoint, and leaves `finalize_results.py` to rebuild `scoreboard.toml`. Nothing is decompiled, and
the per-binary `evaluated/*.toml` files are not touched (the output directory of `evaluate_project` is a scratch one).
`--workers` above 1 is refused when a binary's stored C is over 8 MB (`run_gate.py`).
"""
from __future__ import annotations
@@ -21,6 +22,8 @@ import sys
import tempfile
from pathlib import Path
from run_gate import refuse_unsafe_workers
def main() -> int:
p = argparse.ArgumentParser()
@@ -45,6 +48,9 @@ def main() -> int:
decs = args.decompilers.split(",")
ckpt = args.tree / "checkpoints" / f"{args.project}.pkl"
ck = pickle.loads(ckpt.read_bytes())
for opt_decompiled in ck["decompile"].values():
refuse_unsafe_workers({stem: {d: per[d] for d in decs if d in per}
for stem, per in opt_decompiled.items()}, args.workers)
project = Project.from_toml(next(t for t in gather_project_tomls() if t.stem == args.project))
rb.discover(project, args.tree)
scratch = Path(tempfile.mkdtemp(prefix="evaluate_checkpointed_"))
+20 -5
View File
@@ -16,8 +16,14 @@
# 4. optionally compile the projects (DecBench's compile_all.py);
# 5. run scripts/run_benchmark.py through decbench_cli.py, which registers
# the vj_ged metric when DecBench does not;
# 6. check every r2sleigh_native result names that sha256, then report
# against baseline.json (report_decbench.py).
# 6. check every binary the run decompiled has its scores in evaluated/
# (run_gate.py: a run that loses rows stops here with status 71), check
# every r2sleigh_native result names that sha256, then report against
# baseline.json (report_decbench.py).
#
# More than one worker is refused for a binary whose decompiled C exceeds 8 MB
# (r2s's bash: 13 MB, 5.2 GB per evaluation worker; run_gate.py): up front for
# C already in --out, and inside the driver before it evaluates.
#
# The driver needs cgroup v2 and a user systemd manager (`systemd-run --user`)
# for its per-binary limits; it says so and stops when they are missing.
@@ -51,7 +57,7 @@ usage: tests/decbench/run_decbench.sh [options]
--opt-level OPT O0, O2 or O2_noinline; repeatable (default: O0 O2)
--decompilers LIST (default: r2sleigh_native,angr)
--metrics LIST (default: byte_match,ged,vj_ged,type_match)
--workers N DecBench workers (default: 4)
--workers N DecBench workers (default: 4); 1 for bash
--r2s PATH measure this r2s instead of building target/release/r2s
--compile compile the projects first (DecBench's compile_all.py)
--accept-baseline start or extend baseline.json with this run
@@ -78,6 +84,8 @@ while [[ $# -gt 0 ]]; do
esac
done
(( ${#opts[@]} )) || opts=(O0 O2)
[[ $workers =~ ^[1-9][0-9]*$ ]] || { echo "--workers wants a positive integer, not '$workers'" >&2; exit 64; }
started=$(date +%s)
out=${out:-$root/tests/decbench/artifacts/run-$(date -u +%Y%m%dT%H%M%SZ)}
opt_list=$(IFS=,; echo "${opts[*]}")
@@ -88,6 +96,11 @@ run() {
(( plan )) || "$@"
}
if (( workers > 1 )); then
python3 "$root/tests/decbench/run_gate.py" workers --tree "$out" --workers "$workers" \
--decompilers "$decompilers" || exit $?
fi
if [[ -z $r2s ]]; then
run cargo build --release -p r2s --features sleigh --manifest-path "$root/Cargo.toml"
r2s=${CARGO_TARGET_DIR:-$root/target}/release/r2s
@@ -113,7 +126,7 @@ fi
export R2SLEIGH_R2S_BIN=$r2s
export R2SLEIGH_REFUSAL_CENSUS_DIR=$out/refusals
if (( compile )); then
run env -C "$decbench" "$python" scripts/compile_all.py "$out" "$workers" "${projects[@]}"
run env -C "$decbench" "$python" scripts/compile_all.py "$out" "$workers" ${projects[@]+"${projects[@]}"}
fi
declare -a only=()
(( ${#projects[@]} )) && only=(-- "${projects[@]}")
@@ -123,8 +136,10 @@ run env -C "$decbench" \
DECBENCH_METRICS="$metrics" \
DECBENCH_WORKERS="$workers" \
R2SLEIGH_DECBENCH_PHASE_LOG="$out/phases.tsv" \
"$python" "$root/tests/decbench/decbench_cli.py" run-benchmark "$out" "${only[@]}"
"$python" "$root/tests/decbench/decbench_cli.py" run-benchmark "$out" ${only[@]+"${only[@]}"}
run python3 "$root/tests/decbench/run_gate.py" evaluated --tree "$out" \
--decompilers "$decompilers" --since "$started"
(( plan )) && exit 0
results=$out/function_results.json
python3 - "$results" "$witness" <<'PY'
+190
View File
@@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""Fail a DecBench run that lost rows, and refuse an evaluation that the host will kill.
run_gate.py evaluated --tree <results tree> --decompilers a,b [--since EPOCH]
run_gate.py workers --tree <results tree> --workers N --decompilers a,b
On 2026-10-17 the host's OOM killer took the evaluation worker of every bash binary, `evaluate_project` returned `{}`,
and the driver went on: kuna, ghidra and r2s had no bash row, and the scoreboard dropped them without a word.
* `evaluated`: every binary a decompiler rendered functions for in this run (its `decompiled/<dec>_<binary>.toml` is
newer than `--since` and says `function_count > 0`) needs an `evaluated/<binary>.toml` that carries a score key of that
decompiler. A missing file, a file of just `binary = "bash"` (16 bytes) and a file that scores only other
decompilers all stop the run with exit status 71.
* `workers`: one evaluation worker needs 5.2 GB for the 13 MB of C that r2s writes for bash, so more than one worker on
a binary whose decompiled C exceeds 8 MB is refused (exit status 72). The same check runs inside the driver through
`install_workers_guard`, before `evaluate_project`, where the C is known; it raises `SystemExit` because the driver
catches `Exception` around the evaluation and carries on with `{}`.
"""
from __future__ import annotations
import argparse
import functools
import inspect
import os
import re
import sys
from pathlib import Path
LOST = 71
REFUSED = 72
MAX_PARALLEL_C_BYTES = 8 * 1024 * 1024
# Read without a TOML parser, so the shell can run this under any python3: DecBench writes
# `function_count = N` and quotes the dotted score keys, `"<decompiler>.<metric>.total" = 0.5`.
FUNCTION_COUNT = re.compile(r"^function_count\s*=\s*(\d+)", re.MULTILINE)
SCORE_KEY = re.compile(r'^"?([^".=\s]+)\.[^"=]*"?\s*=', re.MULTILINE)
def split_name(stem: str, decompilers: list[str]) -> tuple[str, str] | None:
"""`r2sleigh_native_bash` -> (`r2sleigh_native`, `bash`), the longest decompiler name that fits."""
for decompiler in sorted(decompilers, key=len, reverse=True):
if stem.startswith(decompiler + "_") and len(stem) > len(decompiler) + 1:
return decompiler, stem[len(decompiler) + 1:]
return None
def rendered_binaries(tree: Path, decompilers: list[str], since: float | None = None) -> list[dict]:
"""The (opt, project, binary, decompiler) cells the driver decompiled into this tree, with function counts."""
cells = []
for toml in sorted(tree.glob("*/*/decompiled/*.toml")):
named = split_name(toml.stem, decompilers)
if named is None or (since is not None and int(toml.stat().st_mtime) < since):
continue
try:
found = FUNCTION_COUNT.search(toml.read_text())
except (OSError, ValueError):
found = None
count = int(found.group(1)) if found else 0
if count > 0:
opt, project = toml.parts[-4], toml.parts[-3]
cells.append({"opt": opt, "project": project, "binary": named[1], "decompiler": named[0],
"functions": count, "dir": toml.parent.parent, "toml": toml})
return cells
def lost_evaluations(tree: Path, decompilers: list[str], since: float | None = None) -> tuple[int, list[str]]:
"""How many cells were checked, and one message per cell whose scores are not in the evaluated file."""
cells = rendered_binaries(tree, decompilers, since)
lost = []
for cell in cells:
where = f"{cell['opt']}/{cell['project']}/evaluated/{cell['binary']}.toml"
evaluated = cell["dir"] / "evaluated" / f"{cell['binary']}.toml"
who = f"{cell['decompiler']} rendered {cell['functions']} functions of {cell['binary']}"
if not evaluated.is_file():
reason = f"{where} is missing"
else:
try:
scored = sorted(set(SCORE_KEY.findall(evaluated.read_text())))
except (OSError, ValueError):
scored = None
size = evaluated.stat().st_size
if scored is None:
reason = f"{where} cannot be read ({size} bytes)"
elif cell["decompiler"] not in scored:
reason = (f"{where} is {size} bytes and scores "
f"{', '.join(scored) if scored else 'no decompiler'}, not {cell['decompiler']}")
else:
continue
c_file = cell["toml"].with_suffix(".c")
if c_file.is_file() and c_file.stat().st_size > MAX_PARALLEL_C_BYTES:
reason += (f"; its decompiled C is {c_file.stat().st_size / 2**20:.1f} MB, so the evaluation worker was "
"most likely OOM-killed (evaluate with --workers 1)")
lost.append(f"{who}, but {reason}")
return len(cells), lost
def c_bytes_of(decompilations: dict) -> dict[tuple[str, str], int]:
"""`{binary: {decompiler: DecompilationResult}}` -> the bytes of C each pair holds."""
sizes = {}
for binary, per_decompiler in decompilations.items():
for decompiler, result in per_decompiler.items():
functions = getattr(result, "functions", None) or {}
sizes[(str(binary), str(decompiler))] = sum(
len((getattr(function, "decompiled_code", "") or "").encode()) for function in functions.values())
return sizes
def unsafe_workers(sizes: dict[tuple[str, str], int], workers: int | None) -> list[str]:
"""One line per binary whose decompiled C is over the limit when more than one worker would evaluate it."""
if (workers if workers is not None else os.cpu_count() or 1) <= 1:
return []
return [f"{binary} ({decompiler}): {size / 2**20:.1f} MB of C"
for (binary, decompiler), size in sorted(sizes.items()) if size > MAX_PARALLEL_C_BYTES]
def refuse_unsafe_workers(decompilations: dict, workers: int | None) -> None:
over = unsafe_workers(c_bytes_of(decompilations), workers)
if over:
raise SystemExit(
f"refusing to evaluate with {workers if workers is not None else 'all'} workers: "
+ "; ".join(over)
+ f" (limit {MAX_PARALLEL_C_BYTES // 2**20} MB). One evaluation worker for r2s's bash holds 5.2 GB, "
"so parallel workers are OOM-killed and the binary loses every score. Run it with --workers 1.")
def install_workers_guard() -> None:
"""Put `refuse_unsafe_workers` in front of DecBench's `evaluate_project`, before the driver imports the name."""
from decbench.pipeline import evaluate
original = evaluate.evaluate_project
if getattr(original, "_r2sleigh_guard", False):
return
signature = inspect.signature(original)
@functools.wraps(original)
def guarded(*args, **kwargs):
given = signature.bind_partial(*args, **kwargs).arguments
refuse_unsafe_workers(given.get("decompilations") or {}, given.get("workers"))
return original(*args, **kwargs)
guarded._r2sleigh_guard = True
evaluate.evaluate_project = guarded
def tree_c_bytes(tree: Path, decompilers: list[str]) -> dict[tuple[str, str], int]:
"""The decompiled C files already in a tree: `(opt/project/binary, decompiler) -> bytes`."""
sizes = {}
for c_file in sorted(tree.glob("*/*/decompiled/*.c")):
named = split_name(c_file.stem, decompilers)
if named is not None:
sizes[(f"{c_file.parts[-4]}/{c_file.parts[-3]}/{named[1]}", named[0])] = c_file.stat().st_size
return sizes
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser.add_argument("check", choices=("evaluated", "workers"))
parser.add_argument("--tree", type=Path, required=True)
parser.add_argument("--decompilers", default="")
parser.add_argument("--since", type=float)
parser.add_argument("--workers", type=int, default=1)
args = parser.parse_args(argv)
decompilers = [d for d in args.decompilers.split(",") if d]
if not decompilers:
parser.error("--decompilers is required")
if args.check == "workers":
over = unsafe_workers(tree_c_bytes(args.tree, decompilers), args.workers)
if over:
print(f"refusing --workers {args.workers}: " + "; ".join(over) + f" in {args.tree} "
f"(limit {MAX_PARALLEL_C_BYTES // 2**20} MB); one evaluation worker for r2s's bash holds 5.2 GB. "
"Use --workers 1.", file=sys.stderr)
return REFUSED
return 0
checked, lost = lost_evaluations(args.tree, decompilers, args.since)
if lost:
print(f"ERROR: {len(lost)} of {checked} rendered binaries have no evaluation; their rows would be dropped "
"from the scoreboard:", file=sys.stderr)
for line in lost[:30]:
print(f" {line}", file=sys.stderr)
if len(lost) > 30:
print(f" ... and {len(lost) - 30} more", file=sys.stderr)
return LOST
print(f"evaluations present for all {checked} binaries the run decompiled")
return 0
if __name__ == "__main__":
sys.exit(main())
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""A run that loses rows stops, and an evaluation the host would kill is refused."""
from __future__ import annotations
import contextlib
import io
import os
import sys
import tempfile
import types
import unittest
from pathlib import Path
from unittest import mock
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
import run_gate # noqa: E402
SCORED = '''binary = "{binary}"
"{dec}.byte_match.total" = 0.8461538461538461
"{dec}.byte_match.functions.main" = 0.0
"{dec}.ged.total" = 4.0
'''
class Tree:
"""A results tree: `<opt>/<project>/{decompiled,evaluated}/`."""
def __init__(self, root: str):
self.root = Path(root)
def decompiled(self, decompiler: str, binary: str, functions: int = 3, c_bytes: int = 0,
opt: str = "O2", project: str = "bash") -> Path:
directory = self.root / opt / project / "decompiled"
directory.mkdir(parents=True, exist_ok=True)
toml = directory / f"{decompiler}_{binary}.toml"
toml.write_text(f'binary = "{binary}"\ndecompiler = "{decompiler}"\nfunction_count = {functions}\n')
if c_bytes:
(directory / f"{decompiler}_{binary}.c").write_bytes(b"a" * c_bytes)
return toml
def evaluated(self, binary: str, text: str, opt: str = "O2", project: str = "bash") -> Path:
directory = self.root / opt / project / "evaluated"
directory.mkdir(parents=True, exist_ok=True)
path = directory / f"{binary}.toml"
path.write_text(text)
return path
def gate(*argv: str) -> tuple[int, str]:
err = io.StringIO()
with contextlib.redirect_stderr(err), contextlib.redirect_stdout(io.StringIO()):
status = run_gate.main(list(argv))
return status, err.getvalue()
class EvaluatedFiles(unittest.TestCase):
def setUp(self):
self.dir = tempfile.TemporaryDirectory()
self.tree = Tree(self.dir.name)
self.args = ("evaluated", "--tree", self.dir.name, "--decompilers", "r2sleigh_native,angr,kuna")
def tearDown(self):
self.dir.cleanup()
def test_a_16_byte_evaluated_file_stops_the_run(self):
# The 2026-10-17 bash: the evaluation worker was killed and the file kept its header only.
self.tree.decompiled("r2sleigh_native", "bash", functions=1540)
path = self.tree.evaluated("bash", 'binary = "bash"\n')
self.assertEqual(path.stat().st_size, 16)
status, err = gate(*self.args)
self.assertEqual(status, run_gate.LOST)
self.assertIn("r2sleigh_native rendered 1540 functions of bash", err)
self.assertIn("O2/bash/evaluated/bash.toml is 16 bytes and scores no decompiler", err)
def test_the_command_exits_nonzero_for_the_shell(self):
import subprocess
self.tree.decompiled("kuna", "bash")
self.tree.evaluated("bash", 'binary = "bash"\n')
done = subprocess.run([sys.executable, str(HERE / "run_gate.py"), *self.args],
capture_output=True, text=True)
self.assertEqual(done.returncode, 71)
self.assertIn("1 of 1 rendered binaries have no evaluation", done.stderr)
def test_a_missing_file_stops_the_run(self):
self.tree.decompiled("angr", "bash")
status, err = gate(*self.args)
self.assertEqual(status, run_gate.LOST)
self.assertIn("O2/bash/evaluated/bash.toml is missing", err)
def test_a_large_file_that_scores_only_other_decompilers_stops_the_run(self):
# O2-noinline bash of 2026-10-17: 108 KB, angr's 692 rows, nothing of kuna, ghidra or r2s.
self.tree.decompiled("angr", "bash")
self.tree.decompiled("kuna", "bash")
self.tree.evaluated("bash", SCORED.format(binary="bash", dec="angr") * 2000)
status, err = gate(*self.args)
self.assertEqual(status, run_gate.LOST)
self.assertIn("kuna rendered 3 functions of bash", err)
self.assertIn("scores angr, not kuna", err)
self.assertNotIn("angr rendered", err)
def test_an_oversized_c_file_names_the_likely_cause(self):
self.tree.decompiled("r2sleigh_native", "bash", c_bytes=9 * 2**20)
self.tree.evaluated("bash", 'binary = "bash"\n')
status, err = gate(*self.args)
self.assertEqual(status, run_gate.LOST)
self.assertIn("9.0 MB", err)
self.assertIn("--workers 1", err)
def test_scored_binaries_pass_including_dotted_names_and_small_files(self):
self.tree.decompiled("r2sleigh_native", "libz.so.1.2", project="zlib")
self.tree.evaluated("libz.so.1.2", SCORED.format(binary="libz.so.1.2", dec="r2sleigh_native"),
project="zlib")
self.tree.decompiled("kuna", "psize")
self.tree.evaluated("psize", SCORED.format(binary="psize", dec="kuna"))
status, err = gate(*self.args)
self.assertEqual((status, err), (0, ""))
def test_only_what_this_run_decompiled_is_checked(self):
# A resumed run: angr is older than the run's start and not evaluated again.
old = self.tree.decompiled("angr", "bash")
os.utime(old, (1_000, 1_000))
self.tree.decompiled("r2sleigh_native", "bash")
self.tree.evaluated("bash", SCORED.format(binary="bash", dec="r2sleigh_native"))
self.assertEqual(gate(*self.args, "--since", "2000")[0], 0)
self.assertEqual(gate(*self.args)[0], run_gate.LOST)
def test_a_decompiler_that_rendered_nothing_owes_no_scores(self):
self.tree.decompiled("angr", "bash", functions=0)
self.assertEqual(gate(*self.args)[0], 0)
class OneWorkerForBigC(unittest.TestCase):
def setUp(self):
self.dir = tempfile.TemporaryDirectory()
self.tree = Tree(self.dir.name)
def tearDown(self):
self.dir.cleanup()
def workers(self, n: int) -> tuple[int, str]:
return gate("workers", "--tree", self.dir.name, "--workers", str(n),
"--decompilers", "r2sleigh_native,angr")
def test_more_than_one_worker_is_refused_for_a_binary_over_8_mb(self):
self.tree.decompiled("r2sleigh_native", "bash", c_bytes=13 * 2**20)
self.tree.decompiled("angr", "bash", c_bytes=2**20)
status, err = self.workers(4)
self.assertEqual(status, run_gate.REFUSED)
self.assertIn("O2/bash/bash (r2sleigh_native): 13.0 MB", err)
self.assertNotIn("angr", err)
self.assertEqual(self.workers(1), (0, ""))
def test_a_binary_under_the_limit_may_use_every_worker(self):
self.tree.decompiled("r2sleigh_native", "bzip2", c_bytes=7 * 2**20, project="bzip2")
self.assertEqual(self.workers(8), (0, ""))
class FakeResult:
def __init__(self, megabytes: float):
code = "a" * int(megabytes * 2**20)
self.functions = {"main": types.SimpleNamespace(decompiled_code=code)}
class DriverGuard(unittest.TestCase):
"""`install_workers_guard` in front of a stand-in `evaluate_project` with DecBench's signature."""
def setUp(self):
self.calls = []
def evaluate_project(project, decompilations, output_dir, optimization=None, metrics=None,
parallel=True, workers=None, precomputed_source_cfgs=None,
source_function_owners=None):
self.calls.append(workers)
return {"bash": {"r2sleigh_native": {"ged": 1}}}
self.module = types.ModuleType("decbench.pipeline.evaluate")
self.module.evaluate_project = evaluate_project
pipeline = types.ModuleType("decbench.pipeline")
pipeline.evaluate = self.module
patcher = mock.patch.dict(sys.modules, {
"decbench": types.ModuleType("decbench"),
"decbench.pipeline": pipeline,
"decbench.pipeline.evaluate": self.module,
})
patcher.start()
self.addCleanup(patcher.stop)
run_gate.install_workers_guard()
run_gate.install_workers_guard() # idempotent
def test_parallel_workers_over_big_c_end_the_run_instead_of_returning_nothing(self):
big = {"bash": {"r2sleigh_native": FakeResult(9), "angr": FakeResult(1)}}
# The driver catches `Exception` around `evaluate_project` and carries on with `{}`.
with self.assertRaises(SystemExit) as stop:
self.module.evaluate_project("project", big, Path("out"), None, None, True, 2)
self.assertNotIsInstance(stop.exception, Exception)
self.assertIn("bash (r2sleigh_native): 9.0 MB", str(stop.exception))
self.assertIn("--workers 1", str(stop.exception))
with self.assertRaises(SystemExit):
self.module.evaluate_project("project", big, Path("out"), workers=2)
self.assertEqual(self.calls, [])
def test_one_worker_or_small_c_passes_through(self):
big = {"bash": {"r2sleigh_native": FakeResult(9)}}
small = {"bzip2": {"r2sleigh_native": FakeResult(1)}}
self.assertEqual(self.module.evaluate_project("p", big, Path("o"), workers=1),
{"bash": {"r2sleigh_native": {"ged": 1}}})
self.module.evaluate_project("p", small, Path("o"), workers=8)
self.assertEqual(self.calls, [1, 8])
def test_no_worker_count_means_every_cpu(self):
big = {"bash": {"r2sleigh_native": FakeResult(9)}}
with mock.patch.object(run_gate.os, "cpu_count", return_value=4):
with self.assertRaises(SystemExit):
self.module.evaluate_project("p", big, Path("o"))
with mock.patch.object(run_gate.os, "cpu_count", return_value=1):
self.module.evaluate_project("p", big, Path("o"))
if __name__ == "__main__":
unittest.main()