- check_budget.py escalated a hard index-row violation to 'warn' on both branches, so INDEX_ROW_HARD could never fail anything. It fails now, and tests/unit/test_budget_measurement.py pins both thresholds. - The corpus note counted scripts/__pycache__/*.pyc, so the same commit reported 27 scripts after the suite had run and 16 on a clean checkout. - TOKENS_PER_CHAR is re-derived from a measurement (26,795 chars -> 6,065 tokens = 4.418); the comment's old figure and its arithmetic were both wrong for this file. - check_repo.py's yaml sentinel carries the type ignore, so mypy is clean with PyYAML installed as well as without it. - The CPUID test now skips with the probe's own note on a host without CPUID instead of failing, and pytest.ini points at tests/README.md, which exists, instead of an EXTENSION file that never did. Tooling: .flake8 (a measured 120-column limit), .gitattributes, .gitignore and .github/workflows/ci.yml (the five gates, the suite on 3.11-3.14 and a lint job), which is what CONTRIBUTING.md already claimed CI runs. The pull-request template it refers to now exists. Content: pitfalls P26-P34 -- the analysis-level failures the catalogue was missing (sampling read as observation, a self-built parser trusted, a verification that could not have failed, a shell-mangled command line) -- registered in the symptom index and its routing mirror. FINDINGS F11-F15 record the defects above. Tests: the coverage guard derives its subject from scripts/ rather than a hand-written list, pe_triage.py and pe_diff.py gain their first tests, and plan.py / preflight.py are declared uncovered with reasons.
138 lines
11 KiB
Markdown
138 lines
11 KiB
Markdown
# tests/ — executable regression suite for the exe-reverse skill
|
|
|
|
The suite that proves the shipped scripts still do what their docstrings say, offline, with only
|
|
the standard library and pytest.
|
|
|
|
## When to load this
|
|
|
|
Read it before changing anything under `skills/exe-reverse/scripts/`, and read it before quoting
|
|
a coverage claim about this skill. It is the boundary between "the script was written to do X"
|
|
and "the script was measured doing X on this host".
|
|
|
|
## What it runs
|
|
|
|
| File | What it asserts |
|
|
| --- | --- |
|
|
| `conftest.py` | Shared fixtures: builds the synthetic PE images at test time, loads a script by path, runs a script as a subprocess with a timeout, and the two synthetic scan corpora. |
|
|
| `unit/test_peutil_reader.py` | Header fields against the fixture's declared metadata, RVA to file-offset round trips including the virtual-only tail, the PE32 versus PE32+ optional-header difference, the checksum against a second implementation, the resource tree, named and ordinal imports, the TLS callback, the RSDS/PDB record, relocation blocks, entropy and flag decoding, and the parser's rejections. |
|
|
| `unit/test_pepatch_patcher.py` | Pattern parsing and matching, the ambiguity refusal, the expect-mismatch refusal, the length-unproven and length-change cases, the resolver's refusals, the re-read verification, and the CLI-level facts that a dry run writes nothing and a real run leaves its input byte-identical. |
|
|
| `unit/test_aob_scan.py` | The verdict boundaries at both sides of each threshold, hit reporting with offsets, RVAs and sections, section filtering, context windows, and every accepted shape of a pattern file. |
|
|
| `unit/test_scan_leaks.py` | Every rule firing on a shaped sample, every exemption class staying silent, the three measured deviations from the exemption list, scan position reporting, and the file walk's skips. |
|
|
| `unit/test_capabilities_registry.py` | The capability table: every atom's kind, basis and probe name, the transitive closure against an independent walk, the dependency graph's acyclicity, `resolve` on missing atoms and on `file:` atoms that are and are not on disk, the declaration parser including the documentation template it must reject, and the `declared` / `mapped` / `unknown` classification. |
|
|
| `unit/test_doctor.py` | The static dependency pass (stdlib, siblings and a third-party name separated without executing the file), `discover_scripts` scanning rather than listing, the per-script table with an unclassified script still counted, and the environment facts with their keys and severity labels. |
|
|
| `unit/test_budget_measurement.py` | The budget gate's two measurement paths agree (real tokenizer versus the calibrated fallback), the fallback constant still matches a fresh measurement of `SKILL.md`, and a row past `INDEX_ROW_HARD` fails rather than warns. |
|
|
| `unit/test_dart_snapshot_strings.py` | The ELF reader against a generated container, both literal encodings recovered at the offsets the fixture placed them at, the length and limit gates, and the pool counts -- zero on an image carrying neither idiom and non-zero on hand-built bytes carrying each one. |
|
|
| `cli/test_cli_surface.py` | Help and usage exit codes for every script, the documented exit codes for each script's error paths, and the negative case that inspect-only runs write nothing into the working directory. |
|
|
| `cli/test_cli_coverage.py` | Coverage derived from the scripts directory rather than a hand-written list: every shipped `.py` is either exercised or declared uncovered with a reason, and every one answers `--help` with its usage block. A new script fails here until it is registered. |
|
|
| `cli/test_cli_json.py` | Each script's machine-readable output: keys, values, and the conventions that are not what a reader would assume -- including `pe_triage.py`'s identity block on a generated image (and that it writes nothing) and `pe_diff.py`'s self-comparison, which is the control for every other diff. |
|
|
| `cli/test_cli_registry_surface.py` | The registry and doctor subprocess contracts: `--list` / `--json` / single-capability exit codes and `RESULT=` tokens, the doctor's three blocks and its 0-versus-3 verdict, and the view flags that print one block only. |
|
|
| `cli/test_cli_dart_snapshot.py` | The snapshot reader's CLI: the refusals and their exit codes, `--find` case-insensitivity and the exit code a miss produces, `--at` by file offset and by virtual address, and the `--json` document. |
|
|
| `integration/test_pipeline_end_to_end.py` | Build, locate, patch, re-read: the patched bytes, the untouched control site, the recomputed checksum, and a control run in which nothing changes. |
|
|
| `integration/test_across_builds.py` | Pattern hit counts compared between two builds, including the case where a patch removes the very byte the pattern was located with. |
|
|
| `run_tests.py` | The one-command entry point; prints the install line and exits 3 when pytest is missing. |
|
|
| `benchmark.md` | The regression matrix: dimension, target, scripts, measured result, strength label. |
|
|
|
|
## How to run it
|
|
|
|
```
|
|
python -m pytest -q # from the repository root: the whole suite
|
|
python -m pytest -q -m unit # one layer: unit, cli or integration
|
|
python tests/run_tests.py # same thing, with a clear message if pytest is missing
|
|
python tests/run_tests.py -q -m unit # arguments are forwarded to pytest
|
|
```
|
|
|
|
If pytest is not installed: `python -m pip install pytest`.
|
|
|
|
## What it deliberately does not do
|
|
|
|
It does not attach a debugger, load a driver, open a socket, read a device, or execute a target.
|
|
Those routes are conditional on host hardware and policy, and a suite that asserts them is either
|
|
fragile or dishonest. The recorded experiments for them live in `benchmark.md` with the strength
|
|
label that says whether they were reproduced here.
|
|
|
|
It does not commit or download a sample. Every PE input is generated into a temporary directory
|
|
by `make_fixture_pe.py` on the way in and removed with the session, so the suite carries no
|
|
binary, no licence question and no dependency on a target that changes underneath it.
|
|
|
|
It does not assert on timing. The only time in the suite is a generous per-process timeout, which
|
|
exists to turn a hang into a failure rather than to measure throughput. The timings in
|
|
`benchmark.md` were taken by hand and are recorded as measurements, not enforced as limits.
|
|
|
|
It does not test the reference documents' prose. Cross-reference, budget and routing checks belong
|
|
to the repository's own gates.
|
|
|
|
## Fixtures
|
|
|
|
`conftest.py` builds four images per session, addressing `make_fixture_pe.py`'s own recipes rather
|
|
than duplicating their shapes:
|
|
|
|
| Name | Recipe | Why it is in the suite |
|
|
| --- | --- | --- |
|
|
| `pe64` | `default` | PE32+ with every structure the builder can emit, five sections and the planted pair. |
|
|
| `pe32` | `pe32` | The 32-bit optional-header layout, whose field offsets differ. |
|
|
| `bare` | `bare` | No exports, resources, TLS or debug directory, so the absent cases are representable. |
|
|
| `fatalign` | `fat-align` | A 4 KiB file alignment, large enough that `.rdata` has no virtual-only tail. |
|
|
|
|
The builder's `no-virtual-tail` recipe is deliberately not used: with a zero virtual pad its own
|
|
guard rejects the layout it names, so the recipe exits 1 and writes nothing. `fatalign` covers the
|
|
same property. `cli/test_cli_surface.py` pins that failure so the recipe cannot quietly stay
|
|
broken.
|
|
|
|
`conftest.build_synthetic_elf` builds a second kind of input, because `dart_snapshot_strings.py`
|
|
reads an ELF shared object rather than a PE and no sample of any kind may be committed: a valid
|
|
ELF64 header, a `.text` of sixteen bytes carrying neither idiom the pool observer counts, a
|
|
`.rodata` holding one one-byte (ASCII) literal and one two-byte (UTF-16LE) literal at known
|
|
offsets, and a section-name string table. It is a container to hold the reader against, not a
|
|
Flutter snapshot: the counts the script's docstring records came from a real 26.8 MB `app.so` and
|
|
cannot be reproduced from a generated file, which is why the counting logic is asserted in both
|
|
directions -- zero on the fixture, non-zero on hand-built bytes -- and the real measurement stays a
|
|
recorded observation.
|
|
|
|
## Conventions
|
|
|
|
Marker is the layer: `unit` for pure functions and parsers, `cli` for subprocess contracts,
|
|
`integration` for the no-debugger flows. All three are declared in `pytest.ini`, and
|
|
`--strict-markers` is on, so an undeclared marker is an error rather than a warning. There is no
|
|
slow or deselected tier and therefore no `--runslow` switch: nothing in the suite needs one, and
|
|
declaring a marker that no test carries would be decoration.
|
|
|
|
A missing prerequisite skips with a printed reason: an interpreter older than the 3.9 the scripts
|
|
declare, or a local tool a check needs. A wrong result always fails. Nothing is skipped silently,
|
|
and no test was weakened to pass.
|
|
|
|
Untrusted sample text is assembled from fragments in `conftest.py` rather than quoted whole. The
|
|
suite has to contain strings shaped like pasted credentials and profile paths in order to prove
|
|
the scanner catches them; written whole they would also be caught by the repository's own
|
|
pre-publish scan. The runtime values are identical, and the source text matches nothing.
|
|
|
|
## Deviations the suite pins
|
|
|
|
Each of these is a measured behaviour that differs from what the corresponding docstring implies.
|
|
They are asserted as observations so a fix shows up as a test change instead of as a quietly
|
|
gained or lost result, and each is also recorded in `benchmark.md`.
|
|
|
|
| Where | Measured behaviour |
|
|
| --- | --- |
|
|
| `make_fixture_pe.py` | The `no-virtual-tail` recipe contradicts its own guard and cannot be built. |
|
|
| `peutil.py`, `pepatch.py` | A file truncated mid-header raises `struct.error` and prints a traceback instead of using the documented exit code; a fully non-PE input does use it. |
|
|
| `pepatch.py` | A missing `--spec` file reaches an unguarded `open()` and prints a traceback. |
|
|
| `aob_scan.py` | A bare JSON array of patterns raises `AttributeError`; only the object form is accepted. |
|
|
| `scan_leaks.py` | Exit code 3 is documented for an empty scan surface and is unreachable: an empty root reports `RESULT=clean` and exits 0. |
|
|
| `scan_leaks.py` | The bare-GUID exemption matches inside the machine-guid rule's own match text, so that rule cannot produce a finding. |
|
|
| `scan_leaks.py` | The `.NET` entry matches any `.net` TLD, so a hit inside such a host is suppressed as a platform term. |
|
|
| `scan_leaks.py` | The `<[a-z_]+>` placeholder alternative is wrapped in `\b`, so it cannot match bracketed prose. |
|
|
| `hwprobe.py` | Two route rows answer with a sentence instead of one of the three documented states. |
|
|
| `pepatch.py` | `--allow-resize` writes in place, so the file does not grow and the bytes after the site are overwritten rather than shifted. |
|
|
|
|
## Limits
|
|
|
|
Coverage is bounded by the fixture, not by the reader. A synthetic image exercises every structure
|
|
the builder emits and nothing it does not; a route that needs a device, a debugger, a driver or a
|
|
network is out of scope here and is listed as not run in `benchmark.md`. The suite also cannot say
|
|
whether a pattern will survive a build it has never seen, which is why the comparisons it does make
|
|
are between two images it built itself.
|
|
|
|
The scripts under `skills/exe-reverse/scripts/` were being edited while this suite was written, and
|
|
several assertions moved with them. Re-run the suite after any change to those files rather than
|
|
trusting a count recorded earlier.
|