asmdiff 0.1.2__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- asmdiff-0.1.4/FIELD-TEST-REPORT.md +123 -0
- {asmdiff-0.1.2 → asmdiff-0.1.4}/PKG-INFO +52 -10
- {asmdiff-0.1.2 → asmdiff-0.1.4}/README.md +51 -9
- {asmdiff-0.1.2 → asmdiff-0.1.4}/asmdiff.example.toml +3 -2
- {asmdiff-0.1.2 → asmdiff-0.1.4}/asmdiff.py +262 -46
- {asmdiff-0.1.2 → asmdiff-0.1.4}/pyproject.toml +1 -1
- {asmdiff-0.1.2 → asmdiff-0.1.4}/test_asmdiff.py +304 -4
- {asmdiff-0.1.2 → asmdiff-0.1.4}/.claude/settings.local.json +0 -0
- {asmdiff-0.1.2 → asmdiff-0.1.4}/.gitignore +0 -0
- {asmdiff-0.1.2 → asmdiff-0.1.4}/CLAUDE.md +0 -0
- {asmdiff-0.1.2 → asmdiff-0.1.4}/LICENSE +0 -0
- {asmdiff-0.1.2 → asmdiff-0.1.4}/asmdiff_example.c +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# asmdiff field-test report
|
|
2
|
+
|
|
3
|
+
**Version tested:** working tree at 0.1.3 (post response-file/object-label/walk-up/verbose fixes), run via `uvx --from <repo>` so the packaged entry point, not the checkout, is what executed.
|
|
4
|
+
**Environment:** S3-Amysynth - a real ESP32-S3 / ESP-IDF v6 firmware (FreeRTOS, TinyUSB, vendored AMY synth engine), `build/compile_commands.json` with 1024 entries, xtensa-esp-elf-gcc 15.2.0 (picolibc via specs in a `@response` file), plus host gcc/clang and g++ for contrast.
|
|
5
|
+
|
|
6
|
+
## Scenario matrix
|
|
7
|
+
|
|
8
|
+
| # | Scenario | Result |
|
|
9
|
+
|---|----------|--------|
|
|
10
|
+
| A1 | Small harness, `old_*`/`new_*` auto-pairing, default gcc+clang fallback | works - both pairs found, fold-vs-libcall visible immediately |
|
|
11
|
+
| A2 | Same harness through a config target: glob `cc`, `compile_commands = true`, harness not in db | works - glob resolution announced, soft-miss note printed, xtensa FPU asm side-by-side |
|
|
12
|
+
| A3 | Two-target matrix (`--target s3 --target s3-size`) | works - O2 vs Os counts differ as expected |
|
|
13
|
+
| B1 | Whole-file summary of a real DSP file (`oscillators.c`, 58 functions) via db flags | works - and immediately useful: softfloat `__muldf3`/`__extendsfdf2` calls stand out in oscillator init paths |
|
|
14
|
+
| B2 | `--pair render_lut:render_lut_fb` inside a real file | works |
|
|
15
|
+
| C1 | `--across render_lut` between two config profiles | works - zero-overhead loop spans (`.L*_LEND`) detected on both sides |
|
|
16
|
+
| C2 | `--across` real file vs modified copy outside the tree | fails cleanly: soft-miss note + trimmed "amy.h: No such file" error, `--verbose` hint |
|
|
17
|
+
| C2b | `--across` real file vs copy **next to** the original | **compiles but silently misleads** - see Trap 1 |
|
|
18
|
+
| C3 | Two real files (both in db), per-file summary blocks | works - each file gets its own recorded flags |
|
|
19
|
+
| E1-E3 | Error paths: bogus target, string `flags`, explicit db + absent source | all produce specific, actionable messages (bogus target lists available targets) |
|
|
20
|
+
| E4 | C++ file via `g++` | compiles, functions extracted - but see Trap 3 |
|
|
21
|
+
| F1 | Giant function (`play_delta`, ~1000 insns) across O2/Os | works; see note on span semantics |
|
|
22
|
+
| F2 | Bare `--` extra flags (README's own example) | works |
|
|
23
|
+
| F3 | Source with no functions | `(no functions found)`, no crash |
|
|
24
|
+
| F4 | `--pair` with two files | correct argparse guard pointing at `--across` |
|
|
25
|
+
|
|
26
|
+
## Traps and gaps (ranked)
|
|
27
|
+
|
|
28
|
+
### 1. Same-directory copy: identical source, different assembly, one stderr note
|
|
29
|
+
The natural before/after move - copy `oscillators.c` to `osc_tweak.c` in the same
|
|
30
|
+
directory and `--across` them - *succeeds*, because quote-includes resolve next to
|
|
31
|
+
the source. But the copy has no db entry, so it compiles without the recorded
|
|
32
|
+
`-D`/`-I` set: in this project that means no `ESP_PLATFORM`, a different
|
|
33
|
+
`AMY_SAMPLE_RATE`, the portable code paths. The two columns show different
|
|
34
|
+
register allocation and frame sizes **for byte-identical code**. The only
|
|
35
|
+
warning is the one-line soft-miss note, whose implication ("the flag sets now
|
|
36
|
+
differ") is easy to miss. A user would read real codegen differences into a
|
|
37
|
+
header-config artifact.
|
|
38
|
+
|
|
39
|
+
**Recommendation:** when the two sides of an `--across`/two-file run received
|
|
40
|
+
*different* borrowed-flag sets (one hit, one soft miss), say so loudly - e.g.
|
|
41
|
+
tag the column header `[no db entry]` and print a warning that the comparison
|
|
42
|
+
crosses header configurations.
|
|
43
|
+
|
|
44
|
+
### 2. Before/after on a project source has no supported path
|
|
45
|
+
The tool's pitch ("what does the compiler emit before and after my rewrite")
|
|
46
|
+
cannot currently be executed for a source that needs db flags, except by
|
|
47
|
+
editing in place and diffing two separate runs by eye, or by maintaining two
|
|
48
|
+
built checkouts with two databases. C2/C2b are both dead ends by design.
|
|
49
|
+
|
|
50
|
+
**Recommendation (feature):** an opt-in fallback such as "borrow flags from
|
|
51
|
+
the same-basename entry" (with a warning), or an explicit
|
|
52
|
+
`--compile-commands-entry PATH` to name which db entry a given source should
|
|
53
|
+
borrow from. Either would make the copy workflow first-class and would also
|
|
54
|
+
neutralize Trap 1.
|
|
55
|
+
|
|
56
|
+
### 3. C++ defeats auto-pairing silently
|
|
57
|
+
`_Z9old_scalef` does not start with `old_`, so a C++ harness falls back to
|
|
58
|
+
whole-file summary with no hint about why. Workable via `extern "C"` or
|
|
59
|
+
explicit `--pair` with mangled names, but nothing tells the user that.
|
|
60
|
+
Acceptable for a C-focused tool; a doc note (or a "mangled names detected,
|
|
61
|
+
auto-pairing skipped" hint) would close it.
|
|
62
|
+
|
|
63
|
+
### 4. Loop spans are coarse on large control flow
|
|
64
|
+
`play_delta` (1001 insns) reports spans like `.L513:676` - two-thirds of the
|
|
65
|
+
function - because any backward branch defines a span, and big switch/dispatch
|
|
66
|
+
functions branch backwards for control flow that is not a loop. The README's
|
|
67
|
+
"the listing and your source know, not the tool" caveat covers this honestly,
|
|
68
|
+
but on 1000-instruction functions the span column stops being informative.
|
|
69
|
+
|
|
70
|
+
### Minor observations
|
|
71
|
+
- The glob-resolution notice (`cc pattern matched 2 toolchains ...`) prints on
|
|
72
|
+
every run; correct behavior (never silent), slightly noisy in scripted use.
|
|
73
|
+
- Two-file summary blocks are rendered as independent tables, so their columns
|
|
74
|
+
do not align with each other - cosmetic.
|
|
75
|
+
- `-ffast-math` after `--` reached the compiler as documented (no behavior
|
|
76
|
+
change for the test case, which was already folded).
|
|
77
|
+
|
|
78
|
+
## What clearly works now (regression-verified this session)
|
|
79
|
+
- ESP-IDF v6 out of the box: `@response-file` expansion delivers
|
|
80
|
+
`-specs=picolibc.specs`; `main.c` and every AMY source compiled with only
|
|
81
|
+
`-O2 -mlongcalls` supplied by the user.
|
|
82
|
+
- Function extraction is clean: 72/72 real functions on `amy.c`, zero data
|
|
83
|
+
labels, counts match `.type @function` ground truth.
|
|
84
|
+
- Walk-up discovery: project root, `main/`, and `components/amy/src/` all find
|
|
85
|
+
`build/compile_commands.json`; the `.git` boundary prevents escaping the repo.
|
|
86
|
+
- Failure UX: 4-line trimmed error with a `--verbose` escape hatch instead of a
|
|
87
|
+
13 KB flag dump.
|
|
88
|
+
- Config system end-to-end: CWD discovery, `default`, multi-target matrices,
|
|
89
|
+
glob `cc`, env expansion, precise config error messages.
|
|
90
|
+
|
|
91
|
+
## Postscript: guards added after this report
|
|
92
|
+
|
|
93
|
+
Traps 1-3 were addressed in the working tree the same day, and re-verified
|
|
94
|
+
against the scenarios above:
|
|
95
|
+
|
|
96
|
+
- **Trap 1:** a two-file comparison with borrowed flags on only one side now
|
|
97
|
+
tags the column `[no db entry]` and warns that differences may be header
|
|
98
|
+
configuration, not codegen. The C2b rerun makes the case vividly: identical
|
|
99
|
+
source showed 51 vs 14 instructions (partial outlining under the portable
|
|
100
|
+
header config) — now labeled instead of silent.
|
|
101
|
+
- **Trap 2:** new `--flags-like PATH` lets a db-less source borrow a named
|
|
102
|
+
entry's flags. Rerunning C2 and C2b with it produced identical 51/51
|
|
103
|
+
columns for identical source, in-tree and out-of-tree. The absent-source
|
|
104
|
+
error and the soft-miss note both suggest the flag when a same-basename
|
|
105
|
+
entry exists.
|
|
106
|
+
- **Trap 3:** falling back to summary when only mangled `_Z*old_*` symbols
|
|
107
|
+
exist now prints a note naming `extern "C"` and `--pair` as the outs.
|
|
108
|
+
|
|
109
|
+
Trap 4 (span coarseness on giant control flow) is documented behavior and
|
|
110
|
+
was left as-is.
|
|
111
|
+
|
|
112
|
+
## Evaluation
|
|
113
|
+
For its stated job - harness-based codegen comparison and whole-file/function
|
|
114
|
+
inspection of real embedded sources - the tool now works credibly in a real
|
|
115
|
+
ESP-IDF v6 project, with fast (<2 s) turnarounds and output that surfaced a
|
|
116
|
+
genuine finding within minutes (software-double promotion in AMY filter/
|
|
117
|
+
oscillator setup paths via `__extendsfdf2`/`__muldf3` in the calls column).
|
|
118
|
+
Error paths are uniformly specific and short. The remaining weaknesses cluster
|
|
119
|
+
around one theme: comparing *modified copies* of db-backed sources, where the
|
|
120
|
+
current behavior ranges from a clean failure (C2) to a quietly misleading
|
|
121
|
+
success (C2b). That workflow is close enough to the tool's core promise that
|
|
122
|
+
Trap 1's warning and Trap 2's fallback are the highest-value next changes;
|
|
123
|
+
everything else found is polish.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asmdiff
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Compare per-function assembly between paired C implementations
|
|
5
5
|
Project-URL: Homepage, https://github.com/rt-rtos/asmdiff
|
|
6
6
|
Project-URL: Repository, https://github.com/rt-rtos/asmdiff
|
|
@@ -94,7 +94,8 @@ $ asmdiff.py asmdiff_example.c
|
|
|
94
94
|
```
|
|
95
95
|
asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]... [--across FUNC]...
|
|
96
96
|
[--cc 'CC FLAGS']... [--target NAME]... [--config PATH]
|
|
97
|
-
[--compile-commands [PATH]] [--
|
|
97
|
+
[--compile-commands [PATH]] [--flags-like PATH] [-v]
|
|
98
|
+
[-- EXTRA_FLAGS...]
|
|
98
99
|
```
|
|
99
100
|
|
|
100
101
|
| Option | Meaning |
|
|
@@ -105,7 +106,9 @@ asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]... [--across FUNC]...
|
|
|
105
106
|
| `--cc 'CC FLAGS'` | One compiler invocation, command and flags in a single quoted string. Repeatable to build a matrix. |
|
|
106
107
|
| `--target NAME` | A named target from the config file, resolved to a `--cc` entry. Repeatable; appended to the matrix after `--cc` entries. |
|
|
107
108
|
| `--config PATH` | Config file to use. Default search: `asmdiff.toml` next to `SOURCE.c`, then in the current directory, then `~/.config/`. First hit wins. |
|
|
108
|
-
| `--compile-commands [PATH]` | Borrow each source's include/define flags from a `compile_commands.json`; with no `PATH`,
|
|
109
|
+
| `--compile-commands [PATH]` | Borrow each source's include/define flags from a `compile_commands.json`; with no `PATH`, walk up from the CWD checking each directory and its `build/` until the repository root. See [below](#borrowing-includes-from-compile_commandsjson). |
|
|
110
|
+
| `--flags-like PATH` | A source with no `compile_commands` entry borrows the flags recorded for `PATH` — the way to compare a modified copy of a project source under its original's header environment. |
|
|
111
|
+
| `-v`, `--verbose` | On compile failure, print the full compiler command and complete error output. Default shows only the compiler, the source, and the first error lines. |
|
|
109
112
|
| `-- FLAGS...` | Everything after a bare `--` is appended to *every* compiler invocation. |
|
|
110
113
|
|
|
111
114
|
With no `--cc` and no `--target`, the config file's top-level
|
|
@@ -207,7 +210,8 @@ compile_commands = "$HOME/myproject/build/compile_commands.json"
|
|
|
207
210
|
Now `asmdiff.py $HOME/myproject/components/dsp/biquad.c --target esp32s3-idf`
|
|
208
211
|
finds that file's entry in the database and adds the include/define flags
|
|
209
212
|
it recorded — `-I`, `-isystem`, `-iquote`, `-idirafter`, `-include`,
|
|
210
|
-
`-imacros`, `-D`, `-U
|
|
213
|
+
`-imacros`, `-D`, `-U`, plus the header-environment driver flags `-specs`
|
|
214
|
+
and `--sysroot` — to this target's command. Everything else the
|
|
211
215
|
database records (its own compiler, `-O`/`-std`/`-W` flags, `-c`, `-o`, the
|
|
212
216
|
source) is ignored: **you** own the compiler and optimisation flags via
|
|
213
217
|
`cc`/`flags`; only the header environment is borrowed. That split is the
|
|
@@ -219,6 +223,12 @@ Details that make it robust:
|
|
|
219
223
|
- **Paths are made absolute** against each entry's `directory`, so a
|
|
220
224
|
database full of build-relative `-I../include` flags still resolves when
|
|
221
225
|
asmdiff runs from anywhere.
|
|
226
|
+
- **`@file` response files are expanded.** Build systems park flags in
|
|
227
|
+
them — ESP-IDF v6 hides `-specs=picolibc.specs` in
|
|
228
|
+
`build/toolchain/cflags`, and without it every libc header breaks —
|
|
229
|
+
so the flag scan reads them (nested ones too) instead of skipping the
|
|
230
|
+
token. A bare specs name is left for the compiler's own search
|
|
231
|
+
directories; a specs path resolves like any other recorded path.
|
|
222
232
|
- **Per source file.** The lookup keys on the source you pass (matched by
|
|
223
233
|
resolved absolute path), so two files in an `--across`/summary run each
|
|
224
234
|
get their own recorded flags.
|
|
@@ -246,12 +256,15 @@ asmdiff.py biquad.c --compile-commands # same, for any matrix
|
|
|
246
256
|
asmdiff.py biquad.c --compile-commands path/to/compile_commands.json
|
|
247
257
|
```
|
|
248
258
|
|
|
249
|
-
Both
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
259
|
+
Both walk up from the current directory, checking each level for
|
|
260
|
+
`compile_commands.json` and then `build/compile_commands.json` (where
|
|
261
|
+
CMake and idf.py leave it) — first hit wins. The walk stops at the
|
|
262
|
+
repository root (the first directory with a `.git`), so running from any
|
|
263
|
+
depth of component directory finds the project database, but an unrelated
|
|
264
|
+
one further up the filesystem is never picked up. Finding nothing is an
|
|
265
|
+
error: discovery is opt-in, so if you asked for it, silence would be a
|
|
266
|
+
lie. It is never on by default — a target without `compile_commands` and
|
|
267
|
+
no `--compile-commands` flag borrows nothing.
|
|
255
268
|
|
|
256
269
|
The precedence is what you'd hope: a target that names its own
|
|
257
270
|
`compile_commands` path always keeps it; `--compile-commands` (with or
|
|
@@ -264,6 +277,29 @@ instead of being an error. That keeps standalone harness files working
|
|
|
264
277
|
when a `build/` directory happens to sit nearby; an explicitly named
|
|
265
278
|
database still treats an absent source as the error it is.
|
|
266
279
|
|
|
280
|
+
If a two-file comparison ends up with borrowed flags on only *one* side,
|
|
281
|
+
the mismatched column is tagged `[no db entry]` and a warning is printed:
|
|
282
|
+
the two sides then differ in header configuration — defines, include
|
|
283
|
+
paths — not just source, and byte-identical code can compile to visibly
|
|
284
|
+
different assembly. Don't read codegen meaning into such a diff.
|
|
285
|
+
|
|
286
|
+
### Comparing a modified copy of a project source
|
|
287
|
+
|
|
288
|
+
The before/after workflow — copy `oscillators.c` to `osc_tweak.c`, change
|
|
289
|
+
one thing, `--across` them — is exactly the situation above: the copy has
|
|
290
|
+
no database entry. `--flags-like` names the entry the copy should borrow:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
asmdiff.py src/oscillators.c src/osc_tweak.c --across render_lut \
|
|
294
|
+
--target s3 --flags-like src/oscillators.c
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Both sides now compile under the same recorded header environment, so the
|
|
298
|
+
diff is your edit and nothing else. This also covers a git-worktree
|
|
299
|
+
baseline (`../baseline/src/oscillators.c`), which is the same file under a
|
|
300
|
+
path the database has never heard of. The absent-source error and the
|
|
301
|
+
soft-miss note both point at this flag when a same-name entry exists.
|
|
302
|
+
|
|
267
303
|
## Whole-file summary
|
|
268
304
|
|
|
269
305
|
With no `--pair`, no `--across`, and no `old_*`/`new_*` functions to
|
|
@@ -292,6 +328,12 @@ weight, did a call appear that shouldn't have? It sums parsed function
|
|
|
292
328
|
bodies only (no literal pools, data, or alignment), so it is not a size
|
|
293
329
|
measurement, and per-function rows are where the real information is.
|
|
294
330
|
|
|
331
|
+
Only labels the assembler types as functions are listed — global data
|
|
332
|
+
(string constants, state structs, lookup tables) gets column-0 labels too
|
|
333
|
+
but is not code. A `calls` list longer than 8 symbols is truncated to
|
|
334
|
+
`..., +N more`; real firmware dispatch functions call dozens of distinct
|
|
335
|
+
symbols and would otherwise make rows thousands of characters wide.
|
|
336
|
+
|
|
295
337
|
## Comparing the same function across two builds (`--across`)
|
|
296
338
|
|
|
297
339
|
`--pair` needs both variants to coexist in one compilation. Real changes
|
|
@@ -76,7 +76,8 @@ $ asmdiff.py asmdiff_example.c
|
|
|
76
76
|
```
|
|
77
77
|
asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]... [--across FUNC]...
|
|
78
78
|
[--cc 'CC FLAGS']... [--target NAME]... [--config PATH]
|
|
79
|
-
[--compile-commands [PATH]] [--
|
|
79
|
+
[--compile-commands [PATH]] [--flags-like PATH] [-v]
|
|
80
|
+
[-- EXTRA_FLAGS...]
|
|
80
81
|
```
|
|
81
82
|
|
|
82
83
|
| Option | Meaning |
|
|
@@ -87,7 +88,9 @@ asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]... [--across FUNC]...
|
|
|
87
88
|
| `--cc 'CC FLAGS'` | One compiler invocation, command and flags in a single quoted string. Repeatable to build a matrix. |
|
|
88
89
|
| `--target NAME` | A named target from the config file, resolved to a `--cc` entry. Repeatable; appended to the matrix after `--cc` entries. |
|
|
89
90
|
| `--config PATH` | Config file to use. Default search: `asmdiff.toml` next to `SOURCE.c`, then in the current directory, then `~/.config/`. First hit wins. |
|
|
90
|
-
| `--compile-commands [PATH]` | Borrow each source's include/define flags from a `compile_commands.json`; with no `PATH`,
|
|
91
|
+
| `--compile-commands [PATH]` | Borrow each source's include/define flags from a `compile_commands.json`; with no `PATH`, walk up from the CWD checking each directory and its `build/` until the repository root. See [below](#borrowing-includes-from-compile_commandsjson). |
|
|
92
|
+
| `--flags-like PATH` | A source with no `compile_commands` entry borrows the flags recorded for `PATH` — the way to compare a modified copy of a project source under its original's header environment. |
|
|
93
|
+
| `-v`, `--verbose` | On compile failure, print the full compiler command and complete error output. Default shows only the compiler, the source, and the first error lines. |
|
|
91
94
|
| `-- FLAGS...` | Everything after a bare `--` is appended to *every* compiler invocation. |
|
|
92
95
|
|
|
93
96
|
With no `--cc` and no `--target`, the config file's top-level
|
|
@@ -189,7 +192,8 @@ compile_commands = "$HOME/myproject/build/compile_commands.json"
|
|
|
189
192
|
Now `asmdiff.py $HOME/myproject/components/dsp/biquad.c --target esp32s3-idf`
|
|
190
193
|
finds that file's entry in the database and adds the include/define flags
|
|
191
194
|
it recorded — `-I`, `-isystem`, `-iquote`, `-idirafter`, `-include`,
|
|
192
|
-
`-imacros`, `-D`, `-U
|
|
195
|
+
`-imacros`, `-D`, `-U`, plus the header-environment driver flags `-specs`
|
|
196
|
+
and `--sysroot` — to this target's command. Everything else the
|
|
193
197
|
database records (its own compiler, `-O`/`-std`/`-W` flags, `-c`, `-o`, the
|
|
194
198
|
source) is ignored: **you** own the compiler and optimisation flags via
|
|
195
199
|
`cc`/`flags`; only the header environment is borrowed. That split is the
|
|
@@ -201,6 +205,12 @@ Details that make it robust:
|
|
|
201
205
|
- **Paths are made absolute** against each entry's `directory`, so a
|
|
202
206
|
database full of build-relative `-I../include` flags still resolves when
|
|
203
207
|
asmdiff runs from anywhere.
|
|
208
|
+
- **`@file` response files are expanded.** Build systems park flags in
|
|
209
|
+
them — ESP-IDF v6 hides `-specs=picolibc.specs` in
|
|
210
|
+
`build/toolchain/cflags`, and without it every libc header breaks —
|
|
211
|
+
so the flag scan reads them (nested ones too) instead of skipping the
|
|
212
|
+
token. A bare specs name is left for the compiler's own search
|
|
213
|
+
directories; a specs path resolves like any other recorded path.
|
|
204
214
|
- **Per source file.** The lookup keys on the source you pass (matched by
|
|
205
215
|
resolved absolute path), so two files in an `--across`/summary run each
|
|
206
216
|
get their own recorded flags.
|
|
@@ -228,12 +238,15 @@ asmdiff.py biquad.c --compile-commands # same, for any matrix
|
|
|
228
238
|
asmdiff.py biquad.c --compile-commands path/to/compile_commands.json
|
|
229
239
|
```
|
|
230
240
|
|
|
231
|
-
Both
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
241
|
+
Both walk up from the current directory, checking each level for
|
|
242
|
+
`compile_commands.json` and then `build/compile_commands.json` (where
|
|
243
|
+
CMake and idf.py leave it) — first hit wins. The walk stops at the
|
|
244
|
+
repository root (the first directory with a `.git`), so running from any
|
|
245
|
+
depth of component directory finds the project database, but an unrelated
|
|
246
|
+
one further up the filesystem is never picked up. Finding nothing is an
|
|
247
|
+
error: discovery is opt-in, so if you asked for it, silence would be a
|
|
248
|
+
lie. It is never on by default — a target without `compile_commands` and
|
|
249
|
+
no `--compile-commands` flag borrows nothing.
|
|
237
250
|
|
|
238
251
|
The precedence is what you'd hope: a target that names its own
|
|
239
252
|
`compile_commands` path always keeps it; `--compile-commands` (with or
|
|
@@ -246,6 +259,29 @@ instead of being an error. That keeps standalone harness files working
|
|
|
246
259
|
when a `build/` directory happens to sit nearby; an explicitly named
|
|
247
260
|
database still treats an absent source as the error it is.
|
|
248
261
|
|
|
262
|
+
If a two-file comparison ends up with borrowed flags on only *one* side,
|
|
263
|
+
the mismatched column is tagged `[no db entry]` and a warning is printed:
|
|
264
|
+
the two sides then differ in header configuration — defines, include
|
|
265
|
+
paths — not just source, and byte-identical code can compile to visibly
|
|
266
|
+
different assembly. Don't read codegen meaning into such a diff.
|
|
267
|
+
|
|
268
|
+
### Comparing a modified copy of a project source
|
|
269
|
+
|
|
270
|
+
The before/after workflow — copy `oscillators.c` to `osc_tweak.c`, change
|
|
271
|
+
one thing, `--across` them — is exactly the situation above: the copy has
|
|
272
|
+
no database entry. `--flags-like` names the entry the copy should borrow:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
asmdiff.py src/oscillators.c src/osc_tweak.c --across render_lut \
|
|
276
|
+
--target s3 --flags-like src/oscillators.c
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Both sides now compile under the same recorded header environment, so the
|
|
280
|
+
diff is your edit and nothing else. This also covers a git-worktree
|
|
281
|
+
baseline (`../baseline/src/oscillators.c`), which is the same file under a
|
|
282
|
+
path the database has never heard of. The absent-source error and the
|
|
283
|
+
soft-miss note both point at this flag when a same-name entry exists.
|
|
284
|
+
|
|
249
285
|
## Whole-file summary
|
|
250
286
|
|
|
251
287
|
With no `--pair`, no `--across`, and no `old_*`/`new_*` functions to
|
|
@@ -274,6 +310,12 @@ weight, did a call appear that shouldn't have? It sums parsed function
|
|
|
274
310
|
bodies only (no literal pools, data, or alignment), so it is not a size
|
|
275
311
|
measurement, and per-function rows are where the real information is.
|
|
276
312
|
|
|
313
|
+
Only labels the assembler types as functions are listed — global data
|
|
314
|
+
(string constants, state structs, lookup tables) gets column-0 labels too
|
|
315
|
+
but is not code. A `calls` list longer than 8 symbols is truncated to
|
|
316
|
+
`..., +N more`; real firmware dispatch functions call dozens of distinct
|
|
317
|
+
symbols and would otherwise make rows thousands of characters wide.
|
|
318
|
+
|
|
277
319
|
## Comparing the same function across two builds (`--across`)
|
|
278
320
|
|
|
279
321
|
`--pair` needs both variants to coexist in one compilation. Real changes
|
|
@@ -42,8 +42,9 @@ flags = ["-O2", "-mlongcalls", "-DNDEBUG"]
|
|
|
42
42
|
# never transcribe include paths by hand. The compiler and -O/-m flags
|
|
43
43
|
# still come from cc/flags; only the header environment is borrowed.
|
|
44
44
|
# A path expands ~ and $VARS; `compile_commands = true` instead searches
|
|
45
|
-
#
|
|
46
|
-
#
|
|
45
|
+
# at run time: each directory from the CWD up to the repository root,
|
|
46
|
+
# and its build/ subdirectory (a source missing from a database found
|
|
47
|
+
# that way is compiled without borrowed flags, with a note).
|
|
47
48
|
[esp32s3-idf]
|
|
48
49
|
cc = "$HOME/.espressif/tools/xtensa-esp-elf/esp-*/xtensa-esp-elf/bin/xtensa-esp32s3-elf-gcc"
|
|
49
50
|
flags = ["-O2", "-mlongcalls"]
|
|
@@ -13,6 +13,7 @@ Usage:
|
|
|
13
13
|
[--across FUNC]... [--cc 'CC FLAGS']...
|
|
14
14
|
[--target NAME]... [--config PATH]
|
|
15
15
|
[--compile-commands [PATH]]
|
|
16
|
+
[--flags-like PATH] [-v]
|
|
16
17
|
[-- EXTRA_FLAGS...]
|
|
17
18
|
|
|
18
19
|
Three modes:
|
|
@@ -35,9 +36,12 @@ the include/define flags recorded there for the source being compiled are
|
|
|
35
36
|
added to that target's command, so a real project source resolves its
|
|
36
37
|
headers the way its own build system does (e.g. an ESP-IDF component).
|
|
37
38
|
compile_commands = true in a target — or a bare --compile-commands on the
|
|
38
|
-
command line — instead
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
command line — instead finds the database by checking each directory from
|
|
40
|
+
the CWD up to the repository root, and its build/ subdirectory; a source
|
|
41
|
+
absent from a database found that way is compiled without borrowed flags
|
|
42
|
+
(with a note) rather than being an error. --flags-like PATH lets such a
|
|
43
|
+
source borrow the flags recorded for PATH, so a modified copy compares
|
|
44
|
+
against its original under one header environment.
|
|
41
45
|
"""
|
|
42
46
|
import argparse
|
|
43
47
|
import glob
|
|
@@ -73,8 +77,19 @@ CONFIG_NAME = "asmdiff.toml"
|
|
|
73
77
|
_CC_DB_PATH_FLAGS = ("-I", "-iquote", "-isystem", "-idirafter",
|
|
74
78
|
"-include", "-imacros", "-isysroot")
|
|
75
79
|
_CC_DB_PLAIN_FLAGS = ("-D", "-U")
|
|
80
|
+
# Driver-level flags that also shape the header environment: a specs file
|
|
81
|
+
# can swap the entire libc header set (ESP-IDF v6 selects picolibc via
|
|
82
|
+
# -specs=picolibc.specs), and --sysroot moves every system include. Both
|
|
83
|
+
# accept =-glued and split spellings; both are re-emitted =-glued.
|
|
84
|
+
_CC_DB_EQ_FLAGS = ("-specs", "--specs", "--sysroot")
|
|
76
85
|
_DB_CACHE = {}
|
|
77
86
|
_MISS_NOTED = set()
|
|
87
|
+
_BORROW_NOTED = set()
|
|
88
|
+
# --flags-like PATH: a source with no database entry borrows the flags
|
|
89
|
+
# recorded for PATH. This is how a modified copy of a project source gets
|
|
90
|
+
# the same header environment as its original, so an --across between them
|
|
91
|
+
# compares codegen instead of header configurations.
|
|
92
|
+
FLAGS_LIKE = None
|
|
78
93
|
|
|
79
94
|
|
|
80
95
|
class Target(str):
|
|
@@ -105,6 +120,43 @@ def _abs_against(directory, value):
|
|
|
105
120
|
return str(p)
|
|
106
121
|
|
|
107
122
|
|
|
123
|
+
def _expand_response_files(tokens, directory, depth=0):
|
|
124
|
+
"""Inline GCC @file response files: each @FILE token is replaced by the
|
|
125
|
+
shlex-split contents of FILE, resolved against the entry's ``directory``
|
|
126
|
+
(the CWD the driver ran from). Build systems park header-environment
|
|
127
|
+
flags there — ESP-IDF v6 hides -specs=picolibc.specs in one — so
|
|
128
|
+
skipping them silently loses flags this feature exists to borrow.
|
|
129
|
+
An unreadable file is warned about and dropped; nesting is bounded as
|
|
130
|
+
a cycle guard.
|
|
131
|
+
"""
|
|
132
|
+
out = []
|
|
133
|
+
for tok in tokens:
|
|
134
|
+
if not tok.startswith("@") or len(tok) == 1:
|
|
135
|
+
out.append(tok)
|
|
136
|
+
continue
|
|
137
|
+
path = _abs_against(directory, tok[1:])
|
|
138
|
+
try:
|
|
139
|
+
content = Path(path).read_text()
|
|
140
|
+
except OSError:
|
|
141
|
+
print(f"warning: response file {path} not readable; "
|
|
142
|
+
"flags inside it are not borrowed", file=sys.stderr)
|
|
143
|
+
continue
|
|
144
|
+
inner = shlex.split(content)
|
|
145
|
+
if depth < 8:
|
|
146
|
+
inner = _expand_response_files(inner, directory, depth + 1)
|
|
147
|
+
out += inner
|
|
148
|
+
return out
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _specs_value(directory, value):
|
|
152
|
+
"""A specs argument with a path separator is a file path (resolved like
|
|
153
|
+
any other); a bare name (picolibc.specs) is looked up in the compiler's
|
|
154
|
+
own search directories and must pass through untouched."""
|
|
155
|
+
if "/" in value or os.sep in value:
|
|
156
|
+
return _abs_against(directory, value)
|
|
157
|
+
return value
|
|
158
|
+
|
|
159
|
+
|
|
108
160
|
def include_flags(tokens, directory):
|
|
109
161
|
"""Pick header-search, forced-include, and define flags out of one
|
|
110
162
|
recorded compile command; make relative paths absolute against
|
|
@@ -124,6 +176,19 @@ def include_flags(tokens, directory):
|
|
|
124
176
|
flags += [f, _abs_against(directory, tok[len(f):])]
|
|
125
177
|
matched = True
|
|
126
178
|
break
|
|
179
|
+
if not matched:
|
|
180
|
+
for f in _CC_DB_EQ_FLAGS:
|
|
181
|
+
value = None
|
|
182
|
+
if tok == f and i + 1 < n: # -specs file
|
|
183
|
+
value, extra = tokens[i + 1], 1
|
|
184
|
+
elif tok.startswith(f + "="): # -specs=file
|
|
185
|
+
value = tok[len(f) + 1:]
|
|
186
|
+
if value is not None:
|
|
187
|
+
resolve = (_specs_value if f.endswith("specs")
|
|
188
|
+
else _abs_against)
|
|
189
|
+
flags.append(f + "=" + resolve(directory, value))
|
|
190
|
+
matched = True
|
|
191
|
+
break
|
|
127
192
|
if not matched:
|
|
128
193
|
for f in _CC_DB_PLAIN_FLAGS:
|
|
129
194
|
if tok == f and i + 1 < n: # -D FOO
|
|
@@ -138,18 +203,26 @@ def include_flags(tokens, directory):
|
|
|
138
203
|
|
|
139
204
|
|
|
140
205
|
def find_compile_commands():
|
|
141
|
-
"""Locate a compile_commands.json
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
206
|
+
"""Locate a compile_commands.json by walking up from the current
|
|
207
|
+
directory.
|
|
208
|
+
|
|
209
|
+
Each directory from the CWD upward is checked for the database itself,
|
|
210
|
+
then for build/compile_commands.json (where CMake and idf.py leave it),
|
|
211
|
+
so the search works from a project root or from any depth of component
|
|
212
|
+
directory. The walk stops at the first directory containing .git (the
|
|
213
|
+
repository root — checked after that directory's own candidates) or
|
|
214
|
+
after a bounded number of levels, so an unrelated database further up
|
|
215
|
+
the filesystem is never picked up. Returns None when nothing is found.
|
|
147
216
|
"""
|
|
148
|
-
|
|
149
|
-
for
|
|
150
|
-
candidate
|
|
151
|
-
|
|
152
|
-
|
|
217
|
+
d = Path.cwd()
|
|
218
|
+
for _ in range(10):
|
|
219
|
+
for candidate in (d / "compile_commands.json",
|
|
220
|
+
d / "build" / "compile_commands.json"):
|
|
221
|
+
if candidate.is_file():
|
|
222
|
+
return str(candidate)
|
|
223
|
+
if (d / ".git").exists() or d.parent == d:
|
|
224
|
+
return None
|
|
225
|
+
d = d.parent
|
|
153
226
|
return None
|
|
154
227
|
|
|
155
228
|
|
|
@@ -159,8 +232,9 @@ def _discovered_db(who):
|
|
|
159
232
|
not a silent no-op."""
|
|
160
233
|
found = find_compile_commands()
|
|
161
234
|
if found is None:
|
|
162
|
-
sys.exit(f"error: {who}: no compile_commands.json found in "
|
|
163
|
-
"
|
|
235
|
+
sys.exit(f"error: {who}: no compile_commands.json found in the "
|
|
236
|
+
"current directory, its build/, or any parent up to the "
|
|
237
|
+
"repository root")
|
|
164
238
|
return found
|
|
165
239
|
|
|
166
240
|
|
|
@@ -192,9 +266,47 @@ def compile_commands_flags(db_path, source, missing_ok=False):
|
|
|
192
266
|
and the message flags a same-name entry recorded under a different path.
|
|
193
267
|
With ``missing_ok`` (auto-discovered databases) an absent source instead
|
|
194
268
|
compiles without borrowed flags, after a one-time note per (db, source).
|
|
269
|
+
In either mode, --flags-like PATH satisfies an absent source with the
|
|
270
|
+
flags recorded for PATH — the modified-copy workflow.
|
|
195
271
|
"""
|
|
196
272
|
entries = load_compile_commands(db_path)
|
|
197
273
|
want = Path(source).resolve()
|
|
274
|
+
flags, name_seen = _lookup_entry(entries, want)
|
|
275
|
+
if flags is not None:
|
|
276
|
+
return flags
|
|
277
|
+
if FLAGS_LIKE:
|
|
278
|
+
like = Path(FLAGS_LIKE).resolve()
|
|
279
|
+
like_flags, _ = _lookup_entry(entries, like)
|
|
280
|
+
if like_flags is None:
|
|
281
|
+
sys.exit(f"error: --flags-like {FLAGS_LIKE} "
|
|
282
|
+
f"not found in {db_path}")
|
|
283
|
+
key = (db_path, str(want))
|
|
284
|
+
if key not in _BORROW_NOTED:
|
|
285
|
+
_BORROW_NOTED.add(key)
|
|
286
|
+
print(f"note: {source} not in {db_path}; borrowing the flags "
|
|
287
|
+
f"recorded for {FLAGS_LIKE}", file=sys.stderr)
|
|
288
|
+
return like_flags
|
|
289
|
+
suggest = ("; --flags-like PATH can borrow another entry's flags"
|
|
290
|
+
if name_seen else "")
|
|
291
|
+
if missing_ok:
|
|
292
|
+
key = (db_path, str(want))
|
|
293
|
+
if key not in _MISS_NOTED:
|
|
294
|
+
_MISS_NOTED.add(key)
|
|
295
|
+
print(f"note: {source} not in {db_path}; compiling without "
|
|
296
|
+
f"its include/define flags{suggest}", file=sys.stderr)
|
|
297
|
+
return []
|
|
298
|
+
hint = (f"; an entry named {want.name} exists under a different path — "
|
|
299
|
+
"pass the source as it appears in the database, or borrow its "
|
|
300
|
+
"flags with --flags-like") if name_seen else ""
|
|
301
|
+
sys.exit(f"error: {source} not found in {db_path}{hint}")
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _lookup_entry(entries, want):
|
|
305
|
+
"""Flags for the entry whose file resolves to ``want``, or None.
|
|
306
|
+
|
|
307
|
+
The second result reports whether some entry shares ``want``'s
|
|
308
|
+
basename — the raw material for the miss hints above.
|
|
309
|
+
"""
|
|
198
310
|
name_seen = False
|
|
199
311
|
for entry in entries:
|
|
200
312
|
f = entry.get("file")
|
|
@@ -212,27 +324,26 @@ def compile_commands_flags(db_path, source, missing_ok=False):
|
|
|
212
324
|
args = entry.get("arguments")
|
|
213
325
|
tokens = (list(args) if isinstance(args, list)
|
|
214
326
|
else shlex.split(entry.get("command", "")))
|
|
215
|
-
|
|
327
|
+
tokens = _expand_response_files(tokens, directory)
|
|
328
|
+
return include_flags(tokens, directory), name_seen
|
|
216
329
|
if Path(f).name == want.name:
|
|
217
330
|
name_seen = True
|
|
218
|
-
|
|
219
|
-
key = (db_path, str(want))
|
|
220
|
-
if key not in _MISS_NOTED:
|
|
221
|
-
_MISS_NOTED.add(key)
|
|
222
|
-
print(f"note: {source} not in {db_path}; "
|
|
223
|
-
"compiling without its include/define flags",
|
|
224
|
-
file=sys.stderr)
|
|
225
|
-
return []
|
|
226
|
-
hint = (f"; an entry named {want.name} exists under a different path — "
|
|
227
|
-
"pass the source as it appears in the database") if name_seen else ""
|
|
228
|
-
sys.exit(f"error: {source} not found in {db_path}{hint}")
|
|
331
|
+
return None, name_seen
|
|
229
332
|
|
|
230
333
|
# A label at column 0 that is not a local (.L*) label starts a function.
|
|
231
334
|
FUNC_LABEL = re.compile(r"^([A-Za-z_][\w$.]*):")
|
|
232
|
-
#
|
|
335
|
+
# .type NAME, @object|@function — gcc/clang emit this before the label on
|
|
336
|
+
# ELF. Objects (string constants like __func__, global state, LUTs) get
|
|
337
|
+
# column-0 labels too and must not be reported as functions. The type
|
|
338
|
+
# marker is @ on x86/Xtensa/RISC-V, % on ARM, # on some targets.
|
|
339
|
+
TYPE_DIRECTIVE = re.compile(r'^\s*\.type\s+([^,\s]+)\s*,\s*[@%#]?(\w+)')
|
|
340
|
+
# Assembler directives that carry no information worth reading. .local/
|
|
341
|
+
# .comm/.lcomm declare zero-initialised data, .literal/.literal_position
|
|
342
|
+
# are Xtensa literal-pool bookkeeping — none of them is an instruction.
|
|
233
343
|
NOISE = re.compile(
|
|
234
344
|
r"^\s*\.(cfi_|p2align|align\b|loc\b|file\b|text\b|globl\b|global\b|"
|
|
235
|
-
r"type\b|section\b|ident\b|weak\b|hidden\b|addrsig|build_version
|
|
345
|
+
r"type\b|section\b|ident\b|weak\b|hidden\b|addrsig|build_version|"
|
|
346
|
+
r"local\b|comm\b|lcomm\b|literal_position|literal\b)"
|
|
236
347
|
)
|
|
237
348
|
# Compiler-generated bracketing labels that add nothing (.LFB0:, .Lfunc_end0:).
|
|
238
349
|
NOISE_LABEL = re.compile(r"^\.(LFB|LFE|Lfunc_begin|Lfunc_end)\d*:")
|
|
@@ -252,18 +363,28 @@ def extract_functions(asm_text):
|
|
|
252
363
|
|
|
253
364
|
A function body runs from its column-0 label to the matching .size
|
|
254
365
|
directive (gcc and clang both emit one on ELF) or the next function
|
|
255
|
-
label.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
366
|
+
label. Labels typed ``@object`` (string constants, global state,
|
|
367
|
+
lookup tables) are data, not functions, and are skipped entirely;
|
|
368
|
+
an untyped label still counts as a function so hand-written asm
|
|
369
|
+
keeps working. Comment lines, CFI/section/alignment directives,
|
|
370
|
+
compiler bracketing labels, and inline data (switch jump tables,
|
|
371
|
+
constants) are dropped; instructions and meaningful local labels
|
|
372
|
+
(loop targets) are kept, whitespace-stripped.
|
|
259
373
|
"""
|
|
260
374
|
funcs = {}
|
|
261
375
|
current = None
|
|
376
|
+
data_labels = set()
|
|
262
377
|
for raw in asm_text.splitlines():
|
|
378
|
+
t = TYPE_DIRECTIVE.match(raw)
|
|
379
|
+
if t and t.group(2) != "function":
|
|
380
|
+
data_labels.add(t.group(1))
|
|
263
381
|
m = FUNC_LABEL.match(raw)
|
|
264
382
|
if m:
|
|
265
|
-
|
|
266
|
-
|
|
383
|
+
if m.group(1) in data_labels:
|
|
384
|
+
current = None
|
|
385
|
+
else:
|
|
386
|
+
current = m.group(1)
|
|
387
|
+
funcs[current] = []
|
|
267
388
|
continue
|
|
268
389
|
if current is None:
|
|
269
390
|
continue
|
|
@@ -453,7 +574,7 @@ def target_command(config, name, config_path):
|
|
|
453
574
|
if not isinstance(db, str):
|
|
454
575
|
sys.exit(f"error: target [{name}]: compile_commands must be a "
|
|
455
576
|
"path to a compile_commands.json, or true to search "
|
|
456
|
-
"
|
|
577
|
+
"upward from the current directory")
|
|
457
578
|
db = os.path.expandvars(os.path.expanduser(db))
|
|
458
579
|
return Target(shlex.join([resolve_cc(cc, name), *flags]), db, discovered)
|
|
459
580
|
|
|
@@ -491,6 +612,31 @@ def build_matrix(cc_args, target_args, config, config_path, db_arg=None):
|
|
|
491
612
|
return entries
|
|
492
613
|
|
|
493
614
|
|
|
615
|
+
# --verbose: print full compiler command lines and untrimmed stderr.
|
|
616
|
+
VERBOSE = False
|
|
617
|
+
# Without --verbose, a failed compile shows this many stderr lines — enough
|
|
618
|
+
# for the include chain plus the first error, which is the actionable part.
|
|
619
|
+
MAX_STDERR_LINES = 20
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _compile_failure(cmd, stderr):
|
|
623
|
+
"""Exit for a failed compile.
|
|
624
|
+
|
|
625
|
+
A borrowed-flags command runs to hundreds of tokens and a broken
|
|
626
|
+
header environment produces pages of stderr; dumping both buries the
|
|
627
|
+
actual error. Default: compiler + source + the first stderr lines.
|
|
628
|
+
--verbose restores the complete command and output.
|
|
629
|
+
"""
|
|
630
|
+
if VERBOSE:
|
|
631
|
+
sys.exit(f"error: compile failed: {shlex.join(cmd)}\n{stderr}")
|
|
632
|
+
lines = stderr.splitlines()
|
|
633
|
+
shown = "\n".join(lines[:MAX_STDERR_LINES])
|
|
634
|
+
dropped = len(lines) - MAX_STDERR_LINES
|
|
635
|
+
more = f"\n... {dropped} more stderr lines" if dropped > 0 else ""
|
|
636
|
+
sys.exit(f"error: {cmd[0]} failed on {cmd[-1]}\n{shown}{more}\n"
|
|
637
|
+
"(re-run with --verbose for the full command and output)")
|
|
638
|
+
|
|
639
|
+
|
|
494
640
|
def asm_output_name(cc_cmd, harness):
|
|
495
641
|
"""Filesystem-safe .s name for one (compiler, source) compilation.
|
|
496
642
|
|
|
@@ -527,10 +673,45 @@ def compile_to_asm(cc_cmd, extra_flags, harness, out_dir):
|
|
|
527
673
|
+ ["-S", "-o", str(out_s), str(harness)])
|
|
528
674
|
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
529
675
|
if proc.returncode != 0:
|
|
530
|
-
|
|
676
|
+
_compile_failure(cmd, proc.stderr)
|
|
531
677
|
return out_s.read_text()
|
|
532
678
|
|
|
533
679
|
|
|
680
|
+
def mark_db_misses(cc_cmd, sources, tags):
|
|
681
|
+
"""Column tags for a two-file comparison, marking a side that compiled
|
|
682
|
+
without borrowed flags while the other side had them.
|
|
683
|
+
|
|
684
|
+
That situation — one source in the database, its copy not — means the
|
|
685
|
+
columns differ in header configuration (defines, include paths), not
|
|
686
|
+
just in source, so byte-identical code can show different assembly.
|
|
687
|
+
The marked tag and a warning keep that from reading as a codegen
|
|
688
|
+
finding. Both sides missing is a consistent environment: no warning.
|
|
689
|
+
"""
|
|
690
|
+
db = getattr(cc_cmd, "compile_commands", None)
|
|
691
|
+
if not db:
|
|
692
|
+
return list(tags)
|
|
693
|
+
missed = [(db, str(Path(s).resolve())) in _MISS_NOTED for s in sources]
|
|
694
|
+
if not any(missed) or all(missed):
|
|
695
|
+
return list(tags)
|
|
696
|
+
print("warning: only one side got flags from the database — assembly "
|
|
697
|
+
"differences may reflect header configuration, not source "
|
|
698
|
+
"changes; --flags-like PATH gives the copy its original's flags",
|
|
699
|
+
file=sys.stderr)
|
|
700
|
+
return [t + " [no db entry]" if m else t for t, m in zip(tags, missed)]
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
# C++ mangles old_scale(float) to _Z9old_scalef: the old_/new_ prefixes
|
|
704
|
+
# survive inside the mangled name but no longer lead it, so auto-pairing
|
|
705
|
+
# finds nothing and would fall back to the summary without explanation.
|
|
706
|
+
MANGLED_PAIR = re.compile(r"_Z\d+(?:old|new)_")
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def mangled_pair_hint(names):
|
|
710
|
+
"""True when the extracted names look like C++-mangled old_*/new_*
|
|
711
|
+
functions — the harness probably just needs extern \"C\"."""
|
|
712
|
+
return any(MANGLED_PAIR.match(n) for n in names)
|
|
713
|
+
|
|
714
|
+
|
|
534
715
|
def side_by_side(left, right, ltitle, rtitle, width=44):
|
|
535
716
|
"""Two-column view of a pair's asm lines."""
|
|
536
717
|
rows = [f"{ltitle:<{width}} | {rtitle}",
|
|
@@ -554,14 +735,28 @@ def format_spans(spans):
|
|
|
554
735
|
return " ".join(f"{label}:{n}" for label, n in spans) or "-"
|
|
555
736
|
|
|
556
737
|
|
|
738
|
+
# Real firmware dispatch functions call dozens of distinct symbols; an
|
|
739
|
+
# uncapped list makes summary rows thousands of characters wide.
|
|
740
|
+
MAX_CALLS_SHOWN = 8
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def format_calls(calls):
|
|
744
|
+
"""Comma-joined callee list, capped at MAX_CALLS_SHOWN for readability."""
|
|
745
|
+
if not calls:
|
|
746
|
+
return "-"
|
|
747
|
+
if len(calls) > MAX_CALLS_SHOWN:
|
|
748
|
+
return (", ".join(calls[:MAX_CALLS_SHOWN])
|
|
749
|
+
+ f", +{len(calls) - MAX_CALLS_SHOWN} more")
|
|
750
|
+
return ", ".join(calls)
|
|
751
|
+
|
|
752
|
+
|
|
557
753
|
def summary_table(pairs, funcs):
|
|
558
754
|
"""Instruction counts, outbound calls, and loop spans per pair member."""
|
|
559
755
|
rows = [("function", "role", "insns", "calls", "loop spans")]
|
|
560
756
|
for old, new in pairs:
|
|
561
757
|
for name, role in ((old, "baseline"), (new, "candidate")):
|
|
562
758
|
insns, calls = analyze(funcs[name])
|
|
563
|
-
rows.append((name, role, str(insns),
|
|
564
|
-
", ".join(calls) or "-",
|
|
759
|
+
rows.append((name, role, str(insns), format_calls(calls),
|
|
565
760
|
format_spans(loop_spans(funcs[name]))))
|
|
566
761
|
return render_table(rows)
|
|
567
762
|
|
|
@@ -582,10 +777,10 @@ def file_summary_table(funcs):
|
|
|
582
777
|
for sym in calls:
|
|
583
778
|
if sym not in all_calls:
|
|
584
779
|
all_calls.append(sym)
|
|
585
|
-
rows.append((name, str(insns),
|
|
780
|
+
rows.append((name, str(insns), format_calls(calls),
|
|
586
781
|
format_spans(loop_spans(lines))))
|
|
587
782
|
rows.append((f"TOTAL ({len(funcs)} functions)", str(total_insns),
|
|
588
|
-
|
|
783
|
+
format_calls(all_calls), "-"))
|
|
589
784
|
return render_table(rows)
|
|
590
785
|
|
|
591
786
|
|
|
@@ -639,9 +834,10 @@ def run_across(sources, matrix, fn_names, extra_flags, tmp):
|
|
|
639
834
|
if len(sides) < 2:
|
|
640
835
|
continue
|
|
641
836
|
ran_any = True
|
|
837
|
+
tags = mark_db_misses(cc_cmd, sources,
|
|
838
|
+
file_tags(sources[0], sources[1]))
|
|
642
839
|
print(f"\n== {cc_cmd} ==\n")
|
|
643
|
-
report_across(fn_names, sides[0], sides[1],
|
|
644
|
-
*file_tags(sources[0], sources[1]))
|
|
840
|
+
report_across(fn_names, sides[0], sides[1], *tags)
|
|
645
841
|
if not ran_any:
|
|
646
842
|
sys.exit("error: no usable compiler in the matrix")
|
|
647
843
|
return 0
|
|
@@ -679,8 +875,10 @@ def run_summary(sources, matrix, extra_flags, tmp):
|
|
|
679
875
|
if len(sections) < len(sources):
|
|
680
876
|
continue
|
|
681
877
|
ran_any = True
|
|
878
|
+
shown = (mark_db_misses(cc_cmd, sources, tags)
|
|
879
|
+
if len(sources) == 2 else tags)
|
|
682
880
|
print(f"\n== {cc_cmd} ==")
|
|
683
|
-
for tag, funcs in zip(
|
|
881
|
+
for tag, funcs in zip(shown, sections):
|
|
684
882
|
if len(sections) > 1:
|
|
685
883
|
print(f"\n-- {tag} --")
|
|
686
884
|
print()
|
|
@@ -707,6 +905,10 @@ def run_pairs(source, matrix, pair_specs, extra_flags, tmp):
|
|
|
707
905
|
pairs = ([tuple(p.split(":", 1)) for p in pair_specs]
|
|
708
906
|
or auto_pairs(funcs))
|
|
709
907
|
if not pairs:
|
|
908
|
+
if mangled_pair_hint(funcs):
|
|
909
|
+
print('note: C++ mangling defeats old_*/new_* auto-pairing; '
|
|
910
|
+
'declare the pairs extern "C" or use --pair with the '
|
|
911
|
+
'mangled names', file=sys.stderr)
|
|
710
912
|
print(f"\n== {cc_cmd} ==\n")
|
|
711
913
|
print(file_summary_table(funcs) if funcs
|
|
712
914
|
else "(no functions found)")
|
|
@@ -763,13 +965,27 @@ def main(argv=None):
|
|
|
763
965
|
default=None, metavar="PATH",
|
|
764
966
|
help="borrow each source's include/define flags from "
|
|
765
967
|
"a compile_commands.json; with no PATH, search "
|
|
766
|
-
"
|
|
968
|
+
"each directory (and its build/) from the CWD "
|
|
969
|
+
"up to the repository root. A target whose "
|
|
767
970
|
"config names its own compile_commands keeps it")
|
|
768
971
|
parser.add_argument("--config", metavar="PATH",
|
|
769
972
|
help=f"config file; default search: {CONFIG_NAME} "
|
|
770
973
|
"next to SOURCE.c, in the current directory, "
|
|
771
974
|
"then in ~/.config/")
|
|
975
|
+
parser.add_argument("--flags-like", metavar="PATH",
|
|
976
|
+
help="when a source has no compile_commands entry, "
|
|
977
|
+
"borrow the include/define flags recorded for "
|
|
978
|
+
"PATH — lets a modified copy of a project "
|
|
979
|
+
"source compile (and compare) under its "
|
|
980
|
+
"original's header environment")
|
|
981
|
+
parser.add_argument("-v", "--verbose", action="store_true",
|
|
982
|
+
help="on compile failure, print the full compiler "
|
|
983
|
+
"command and complete error output instead of "
|
|
984
|
+
"the first lines")
|
|
772
985
|
args = parser.parse_args(argv)
|
|
986
|
+
global VERBOSE, FLAGS_LIKE
|
|
987
|
+
VERBOSE = args.verbose
|
|
988
|
+
FLAGS_LIKE = args.flags_like
|
|
773
989
|
|
|
774
990
|
if len(args.sources) > 2:
|
|
775
991
|
parser.error("at most two source files may be given")
|
|
@@ -388,6 +388,77 @@ class TestIncludeFlags(unittest.TestCase):
|
|
|
388
388
|
["-isystem", "/s"])
|
|
389
389
|
|
|
390
390
|
|
|
391
|
+
class TestSpecsAndSysroot(unittest.TestCase):
|
|
392
|
+
"""Driver flags that change the header environment (-specs, --sysroot)."""
|
|
393
|
+
|
|
394
|
+
def test_specs_glued_bare_name_not_resolved(self):
|
|
395
|
+
# A bare specs name (no path separator) is found in the compiler's
|
|
396
|
+
# own search dirs; gluing a directory onto it would break it.
|
|
397
|
+
self.assertEqual(
|
|
398
|
+
asmdiff.include_flags(["-specs=picolibc.specs"], "/build"),
|
|
399
|
+
["-specs=picolibc.specs"])
|
|
400
|
+
|
|
401
|
+
def test_specs_with_path_resolved_against_directory(self):
|
|
402
|
+
self.assertEqual(
|
|
403
|
+
asmdiff.include_flags(["-specs=./custom/my.specs"], "/build"),
|
|
404
|
+
["-specs=/build/custom/my.specs"])
|
|
405
|
+
|
|
406
|
+
def test_specs_split_and_double_dash(self):
|
|
407
|
+
self.assertEqual(
|
|
408
|
+
asmdiff.include_flags(["-specs", "nano.specs",
|
|
409
|
+
"--specs=nosys.specs"], "/b"),
|
|
410
|
+
["-specs=nano.specs", "--specs=nosys.specs"])
|
|
411
|
+
|
|
412
|
+
def test_sysroot_glued_and_split(self):
|
|
413
|
+
self.assertEqual(
|
|
414
|
+
asmdiff.include_flags(["--sysroot=sr", "--sysroot", "/abs"],
|
|
415
|
+
"/b"),
|
|
416
|
+
["--sysroot=/b/sr", "--sysroot=/abs"])
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class TestResponseFiles(unittest.TestCase):
|
|
420
|
+
"""GCC @file response files inside compile_commands entries."""
|
|
421
|
+
|
|
422
|
+
def test_flags_inside_response_file_are_borrowed(self):
|
|
423
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
424
|
+
rsp = Path(tmp) / "toolchain" / "cflags"
|
|
425
|
+
rsp.parent.mkdir()
|
|
426
|
+
rsp.write_text("-mlongcalls\n-specs=picolibc.specs\n-Irspinc\n")
|
|
427
|
+
src = Path(tmp) / "a.c"
|
|
428
|
+
src.touch()
|
|
429
|
+
db = Path(tmp) / "compile_commands.json"
|
|
430
|
+
db.write_text(json.dumps([{
|
|
431
|
+
"directory": tmp, "file": str(src),
|
|
432
|
+
"command": f"cc -Iinc @{rsp} -c a.c"}]))
|
|
433
|
+
asmdiff._DB_CACHE.clear()
|
|
434
|
+
self.assertEqual(
|
|
435
|
+
asmdiff.compile_commands_flags(str(db), str(src)),
|
|
436
|
+
["-I", f"{tmp}/inc", "-specs=picolibc.specs",
|
|
437
|
+
"-I", f"{tmp}/rspinc"])
|
|
438
|
+
|
|
439
|
+
def test_relative_response_file_resolves_against_directory(self):
|
|
440
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
441
|
+
(Path(tmp) / "cflags").write_text("-DFROMRSP")
|
|
442
|
+
self.assertEqual(
|
|
443
|
+
asmdiff._expand_response_files(["@cflags"], tmp),
|
|
444
|
+
["-DFROMRSP"])
|
|
445
|
+
|
|
446
|
+
def test_nested_response_files(self):
|
|
447
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
448
|
+
(Path(tmp) / "outer").write_text("@inner -DOUTER")
|
|
449
|
+
(Path(tmp) / "inner").write_text("-DINNER")
|
|
450
|
+
self.assertEqual(
|
|
451
|
+
asmdiff._expand_response_files(["@outer"], tmp),
|
|
452
|
+
["-DINNER", "-DOUTER"])
|
|
453
|
+
|
|
454
|
+
def test_missing_response_file_warns_and_continues(self):
|
|
455
|
+
err = io.StringIO()
|
|
456
|
+
with contextlib.redirect_stderr(err):
|
|
457
|
+
out = asmdiff._expand_response_files(["-DKEEP", "@/nope/x"], "/b")
|
|
458
|
+
self.assertEqual(out, ["-DKEEP"])
|
|
459
|
+
self.assertIn("/nope/x", err.getvalue())
|
|
460
|
+
|
|
461
|
+
|
|
391
462
|
class TestCompileCommandsFlags(unittest.TestCase):
|
|
392
463
|
def _db(self, tmp, entries):
|
|
393
464
|
path = Path(tmp) / "compile_commands.json"
|
|
@@ -477,15 +548,22 @@ def _inside(directory):
|
|
|
477
548
|
|
|
478
549
|
|
|
479
550
|
class TestFindCompileCommands(unittest.TestCase):
|
|
480
|
-
"""Auto-discovery of compile_commands.json
|
|
551
|
+
"""Auto-discovery of compile_commands.json by walking up from the CWD."""
|
|
481
552
|
|
|
482
553
|
def _touch_db(self, directory):
|
|
483
554
|
directory.mkdir(parents=True, exist_ok=True)
|
|
484
555
|
(directory / "compile_commands.json").write_text("[]")
|
|
485
556
|
|
|
486
|
-
def
|
|
557
|
+
def _repo(self, tmp):
|
|
558
|
+
"""A fake repo root: .git bounds the walk so tests never escape
|
|
559
|
+
the tempdir and pick up a stray database further up."""
|
|
560
|
+
root = Path(tmp)
|
|
561
|
+
(root / ".git").mkdir()
|
|
562
|
+
return root
|
|
563
|
+
|
|
564
|
+
def test_nearer_hits_win(self):
|
|
487
565
|
with tempfile.TemporaryDirectory() as tmp:
|
|
488
|
-
root =
|
|
566
|
+
root = self._repo(tmp)
|
|
489
567
|
cwd = root / "sub"
|
|
490
568
|
cwd.mkdir()
|
|
491
569
|
for where, expect in [(root / "build", root / "build"),
|
|
@@ -497,14 +575,236 @@ class TestFindCompileCommands(unittest.TestCase):
|
|
|
497
575
|
self.assertEqual(asmdiff.find_compile_commands(),
|
|
498
576
|
str(expect / "compile_commands.json"))
|
|
499
577
|
|
|
578
|
+
def test_walks_up_from_nested_component_dir(self):
|
|
579
|
+
# components/amy/src is three levels below the project root where
|
|
580
|
+
# idf.py leaves build/compile_commands.json.
|
|
581
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
582
|
+
root = self._repo(tmp)
|
|
583
|
+
self._touch_db(root / "build")
|
|
584
|
+
cwd = root / "components" / "amy" / "src"
|
|
585
|
+
cwd.mkdir(parents=True)
|
|
586
|
+
with _inside(cwd):
|
|
587
|
+
self.assertEqual(asmdiff.find_compile_commands(),
|
|
588
|
+
str(root / "build" / "compile_commands.json"))
|
|
589
|
+
|
|
590
|
+
def test_stops_at_repository_root(self):
|
|
591
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
592
|
+
self._touch_db(Path(tmp)) # db ABOVE the repo root
|
|
593
|
+
repo = Path(tmp) / "repo"
|
|
594
|
+
repo.mkdir()
|
|
595
|
+
(repo / ".git").write_text("gitdir: elsewhere") # worktree form
|
|
596
|
+
cwd = repo / "src"
|
|
597
|
+
cwd.mkdir()
|
|
598
|
+
with _inside(cwd):
|
|
599
|
+
self.assertIsNone(asmdiff.find_compile_commands())
|
|
600
|
+
|
|
500
601
|
def test_nothing_found_returns_none(self):
|
|
501
602
|
with tempfile.TemporaryDirectory() as tmp:
|
|
502
|
-
|
|
603
|
+
root = self._repo(tmp)
|
|
604
|
+
cwd = root / "a" / "b"
|
|
503
605
|
cwd.mkdir(parents=True)
|
|
504
606
|
with _inside(cwd):
|
|
505
607
|
self.assertIsNone(asmdiff.find_compile_commands())
|
|
506
608
|
|
|
507
609
|
|
|
610
|
+
class TestObjectLabels(unittest.TestCase):
|
|
611
|
+
"""Data labels must not be reported as functions."""
|
|
612
|
+
|
|
613
|
+
ASM = """\
|
|
614
|
+
\t.type\tscale, @function
|
|
615
|
+
scale:
|
|
616
|
+
\tentry\tsp, 32
|
|
617
|
+
\tretw.n
|
|
618
|
+
\t.size\tscale, .-scale
|
|
619
|
+
\t.type\t__func__$1, @object
|
|
620
|
+
\t.size\t__func__$1, 9
|
|
621
|
+
__func__$1:
|
|
622
|
+
\t.string\t"app_main"
|
|
623
|
+
\t.local\ts_queue
|
|
624
|
+
\t.comm\ts_queue,4,4
|
|
625
|
+
\t.lcomm\ts_tmp,8
|
|
626
|
+
"""
|
|
627
|
+
|
|
628
|
+
def test_object_labels_skipped(self):
|
|
629
|
+
funcs = asmdiff.extract_functions(self.ASM)
|
|
630
|
+
self.assertEqual(list(funcs), ["scale"])
|
|
631
|
+
|
|
632
|
+
def test_arm_percent_function_type_still_reported(self):
|
|
633
|
+
asm = "\t.type\tf, %function\nf:\n\tbx\tlr\n\t.size\tf, .-f\n"
|
|
634
|
+
self.assertEqual(list(asmdiff.extract_functions(asm)), ["f"])
|
|
635
|
+
|
|
636
|
+
def test_untyped_label_still_treated_as_function(self):
|
|
637
|
+
# Hand-written asm often has no .type at all.
|
|
638
|
+
asm = "myfunc:\n\tret\n"
|
|
639
|
+
self.assertEqual(list(asmdiff.extract_functions(asm)), ["myfunc"])
|
|
640
|
+
|
|
641
|
+
def test_local_comm_literal_lines_not_counted(self):
|
|
642
|
+
asm = ("f:\n\tmov.n\ta2, a3\n\t.literal_position\n"
|
|
643
|
+
"\t.literal .LC1, 4096\n\t.local\tx\n\t.comm\tx,4,4\n"
|
|
644
|
+
"\tretw.n\n")
|
|
645
|
+
funcs = asmdiff.extract_functions(asm)
|
|
646
|
+
insns, _ = asmdiff.analyze(funcs["f"])
|
|
647
|
+
self.assertEqual(insns, 2)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
class TestFlagsLike(unittest.TestCase):
|
|
651
|
+
"""--flags-like: a source absent from the db borrows a named entry."""
|
|
652
|
+
|
|
653
|
+
def _db(self, tmp):
|
|
654
|
+
path = Path(tmp) / "compile_commands.json"
|
|
655
|
+
path.write_text(json.dumps([{
|
|
656
|
+
"directory": tmp, "file": f"{tmp}/real.c",
|
|
657
|
+
"command": "cc -Iinc -DESP_PLATFORM -c real.c"}]))
|
|
658
|
+
asmdiff._DB_CACHE.clear()
|
|
659
|
+
asmdiff._MISS_NOTED.clear()
|
|
660
|
+
return str(path)
|
|
661
|
+
|
|
662
|
+
def test_missing_source_borrows_named_entry(self):
|
|
663
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
664
|
+
db = self._db(tmp)
|
|
665
|
+
asmdiff.FLAGS_LIKE = f"{tmp}/real.c"
|
|
666
|
+
try:
|
|
667
|
+
err = io.StringIO()
|
|
668
|
+
with contextlib.redirect_stderr(err):
|
|
669
|
+
flags = asmdiff.compile_commands_flags(
|
|
670
|
+
db, f"{tmp}/copy.c", missing_ok=True)
|
|
671
|
+
finally:
|
|
672
|
+
asmdiff.FLAGS_LIKE = None
|
|
673
|
+
self.assertEqual(flags, ["-I", f"{tmp}/inc", "-DESP_PLATFORM"])
|
|
674
|
+
self.assertIn("real.c", err.getvalue()) # borrow is announced
|
|
675
|
+
# A satisfied borrow is not a miss: no apples-to-oranges state.
|
|
676
|
+
self.assertEqual(asmdiff._MISS_NOTED, set())
|
|
677
|
+
|
|
678
|
+
def test_applies_to_explicit_db_too(self):
|
|
679
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
680
|
+
db = self._db(tmp)
|
|
681
|
+
asmdiff.FLAGS_LIKE = f"{tmp}/real.c"
|
|
682
|
+
try:
|
|
683
|
+
with contextlib.redirect_stderr(io.StringIO()):
|
|
684
|
+
flags = asmdiff.compile_commands_flags(
|
|
685
|
+
db, f"{tmp}/copy.c", missing_ok=False)
|
|
686
|
+
finally:
|
|
687
|
+
asmdiff.FLAGS_LIKE = None
|
|
688
|
+
self.assertEqual(flags[-1], "-DESP_PLATFORM")
|
|
689
|
+
|
|
690
|
+
def test_bad_flags_like_target_is_an_error(self):
|
|
691
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
692
|
+
db = self._db(tmp)
|
|
693
|
+
asmdiff.FLAGS_LIKE = f"{tmp}/nowhere.c"
|
|
694
|
+
try:
|
|
695
|
+
with self.assertRaises(SystemExit):
|
|
696
|
+
asmdiff.compile_commands_flags(db, f"{tmp}/copy.c",
|
|
697
|
+
missing_ok=True)
|
|
698
|
+
finally:
|
|
699
|
+
asmdiff.FLAGS_LIKE = None
|
|
700
|
+
|
|
701
|
+
def test_miss_messages_suggest_flags_like(self):
|
|
702
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
703
|
+
db = self._db(tmp)
|
|
704
|
+
with self.assertRaises(SystemExit) as ctx: # hard error path
|
|
705
|
+
asmdiff.compile_commands_flags(db, f"{tmp}/other/real.c")
|
|
706
|
+
self.assertIn("--flags-like", str(ctx.exception))
|
|
707
|
+
err = io.StringIO() # soft-miss path
|
|
708
|
+
with contextlib.redirect_stderr(err):
|
|
709
|
+
asmdiff.compile_commands_flags(db, f"{tmp}/sub/real.c",
|
|
710
|
+
missing_ok=True)
|
|
711
|
+
self.assertIn("--flags-like", err.getvalue())
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
class TestMarkDbMisses(unittest.TestCase):
|
|
715
|
+
"""Two-file comparisons where only one side got borrowed flags."""
|
|
716
|
+
|
|
717
|
+
def _entry(self, db):
|
|
718
|
+
return asmdiff.Target("gcc -O2", db, db_discovered=True)
|
|
719
|
+
|
|
720
|
+
def test_one_sided_miss_tags_and_warns(self):
|
|
721
|
+
asmdiff._MISS_NOTED.clear()
|
|
722
|
+
asmdiff._MISS_NOTED.add(("/db.json", str(Path("b.c").resolve())))
|
|
723
|
+
err = io.StringIO()
|
|
724
|
+
with contextlib.redirect_stderr(err):
|
|
725
|
+
tags = asmdiff.mark_db_misses(self._entry("/db.json"),
|
|
726
|
+
["a.c", "b.c"], ("a.c", "b.c"))
|
|
727
|
+
self.assertEqual(tags, ["a.c", "b.c [no db entry]"])
|
|
728
|
+
self.assertIn("header", err.getvalue())
|
|
729
|
+
|
|
730
|
+
def test_both_missed_means_same_env_no_warning(self):
|
|
731
|
+
asmdiff._MISS_NOTED.clear()
|
|
732
|
+
for s in ("a.c", "b.c"):
|
|
733
|
+
asmdiff._MISS_NOTED.add(("/db.json", str(Path(s).resolve())))
|
|
734
|
+
err = io.StringIO()
|
|
735
|
+
with contextlib.redirect_stderr(err):
|
|
736
|
+
tags = asmdiff.mark_db_misses(self._entry("/db.json"),
|
|
737
|
+
["a.c", "b.c"], ("a.c", "b.c"))
|
|
738
|
+
self.assertEqual(tags, ["a.c", "b.c"])
|
|
739
|
+
self.assertEqual(err.getvalue(), "")
|
|
740
|
+
|
|
741
|
+
def test_no_db_entry_untouched(self):
|
|
742
|
+
tags = asmdiff.mark_db_misses("gcc -O2", ["a.c", "b.c"],
|
|
743
|
+
("a.c", "b.c"))
|
|
744
|
+
self.assertEqual(tags, ["a.c", "b.c"])
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
class TestMangledPairHint(unittest.TestCase):
|
|
748
|
+
def test_mangled_old_new_detected(self):
|
|
749
|
+
self.assertTrue(asmdiff.mangled_pair_hint(
|
|
750
|
+
["_Z9old_scalef", "_Z9new_scalef"]))
|
|
751
|
+
|
|
752
|
+
def test_plain_c_names_no_hint(self):
|
|
753
|
+
self.assertFalse(asmdiff.mangled_pair_hint(["scale", "helper"]))
|
|
754
|
+
|
|
755
|
+
def test_mangled_but_not_old_new_no_hint(self):
|
|
756
|
+
self.assertFalse(asmdiff.mangled_pair_hint(["_Z6renderv"]))
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
class TestCompileFailureOutput(unittest.TestCase):
|
|
760
|
+
CMD = ["xtensa-gcc", "-O2"] + [f"-I/inc{i}" for i in range(50)] + ["a.c"]
|
|
761
|
+
STDERR = "\n".join(f"err line {i}" for i in range(60))
|
|
762
|
+
|
|
763
|
+
def test_default_trims_flags_and_stderr(self):
|
|
764
|
+
asmdiff.VERBOSE = False
|
|
765
|
+
with self.assertRaises(SystemExit) as ctx:
|
|
766
|
+
asmdiff._compile_failure(self.CMD, self.STDERR)
|
|
767
|
+
msg = str(ctx.exception)
|
|
768
|
+
self.assertIn("xtensa-gcc failed on a.c", msg)
|
|
769
|
+
self.assertNotIn("-I/inc0", msg) # no flag dump
|
|
770
|
+
self.assertIn("err line 0", msg)
|
|
771
|
+
self.assertNotIn("err line 30", msg) # stderr trimmed
|
|
772
|
+
self.assertIn("40 more stderr lines", msg)
|
|
773
|
+
self.assertIn("--verbose", msg)
|
|
774
|
+
|
|
775
|
+
def test_verbose_shows_everything(self):
|
|
776
|
+
asmdiff.VERBOSE = True
|
|
777
|
+
try:
|
|
778
|
+
with self.assertRaises(SystemExit) as ctx:
|
|
779
|
+
asmdiff._compile_failure(self.CMD, self.STDERR)
|
|
780
|
+
finally:
|
|
781
|
+
asmdiff.VERBOSE = False
|
|
782
|
+
msg = str(ctx.exception)
|
|
783
|
+
self.assertIn("-I/inc0", msg)
|
|
784
|
+
self.assertIn("err line 59", msg)
|
|
785
|
+
|
|
786
|
+
def test_short_stderr_not_annotated_with_more(self):
|
|
787
|
+
asmdiff.VERBOSE = False
|
|
788
|
+
with self.assertRaises(SystemExit) as ctx:
|
|
789
|
+
asmdiff._compile_failure(["gcc", "x.c"], "one error\n")
|
|
790
|
+
self.assertNotIn("more stderr", str(ctx.exception))
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
class TestFormatCalls(unittest.TestCase):
|
|
794
|
+
def test_short_list_unchanged(self):
|
|
795
|
+
self.assertEqual(asmdiff.format_calls(["a", "b"]), "a, b")
|
|
796
|
+
|
|
797
|
+
def test_empty_is_dash(self):
|
|
798
|
+
self.assertEqual(asmdiff.format_calls([]), "-")
|
|
799
|
+
|
|
800
|
+
def test_long_list_capped(self):
|
|
801
|
+
calls = [f"fn{i}" for i in range(12)]
|
|
802
|
+
out = asmdiff.format_calls(calls)
|
|
803
|
+
self.assertTrue(out.endswith("+4 more"))
|
|
804
|
+
self.assertIn("fn7", out)
|
|
805
|
+
self.assertNotIn("fn8,", out)
|
|
806
|
+
|
|
807
|
+
|
|
508
808
|
class TestDiscoveredCompileCommands(unittest.TestCase):
|
|
509
809
|
"""compile_commands = true in a target / bare --compile-commands."""
|
|
510
810
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|