mpy-coverage 1.0.0__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.
@@ -0,0 +1,48 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v5
15
+ - run: uv sync --only-group dev
16
+ - run: uv run ruff check src/ tests/
17
+ - run: uv run ruff format --check src/ tests/
18
+
19
+ test:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: astral-sh/setup-uv@v5
24
+ - run: uv sync
25
+ - run: uv run pytest tests/test_mpy_analysis.py -v
26
+
27
+ build:
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ - uses: astral-sh/setup-uv@v5
32
+ - run: uv build
33
+ - run: uv run mpy-coverage --help
34
+ - run: uv run python -m mpy_coverage --help
35
+
36
+ integration:
37
+ runs-on: ubuntu-latest
38
+ needs: test
39
+ container:
40
+ image: micropython/unix:coverage_v1.27.0
41
+ steps:
42
+ - uses: actions/checkout@v4
43
+ - run: apt-get update && apt-get install -y python3 python3-pip python3-venv curl
44
+ - uses: astral-sh/setup-uv@v5
45
+ - run: uv sync
46
+ - run: uv run pytest tests/ -v
47
+ env:
48
+ MPY_BINARY: /usr/local/bin/micropython
@@ -0,0 +1,30 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: astral-sh/setup-uv@v5
13
+ - run: uv build
14
+ - uses: actions/upload-artifact@v4
15
+ with:
16
+ name: dist
17
+ path: dist/
18
+
19
+ publish:
20
+ runs-on: ubuntu-latest
21
+ needs: build
22
+ environment: pypi
23
+ permissions:
24
+ id-token: write
25
+ steps:
26
+ - uses: actions/download-artifact@v4
27
+ with:
28
+ name: dist
29
+ path: dist/
30
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .mpy_coverage/
8
+ *.mpy
9
+ src/mpy_coverage/_version.py
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrew Leech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: mpy-coverage
3
+ Version: 1.0.0
4
+ Summary: Code coverage for MicroPython
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: coverage
9
+ Requires-Dist: mpy-cross
@@ -0,0 +1,293 @@
1
+ # mpy-coverage
2
+
3
+ Code coverage for MicroPython. Lightweight on-device tracer using `sys.settrace`, with host-side reporting via coverage.py.
4
+
5
+ The tracer runs on the MicroPython target (unix port or real hardware with settrace enabled), collects executed line data, and exports it as JSON. The host-side tooling then merges multiple runs and generates reports using coverage.py's reporting engine.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install mpy-coverage
11
+ ```
12
+
13
+ This pulls in `coverage` and `mpy-cross` automatically.
14
+
15
+ Dev setup:
16
+ ```bash
17
+ git clone git@github.com:andrewleech/mpy-coverage.git
18
+ cd mpy-coverage
19
+ uv sync
20
+ ```
21
+
22
+ ## Getting started
23
+
24
+ You need a micropython binary with settrace support. The quickest way is the unix coverage variant:
25
+ ```bash
26
+ cd ports/unix && make submodules && make VARIANT=coverage
27
+ ```
28
+
29
+ Say you have a module `myapp.py` and a test script `test_myapp.py` that exercises it:
30
+
31
+ ```python
32
+ # test_myapp.py
33
+ import myapp
34
+ myapp.run()
35
+ ```
36
+
37
+ Collect coverage and generate a report:
38
+ ```bash
39
+ mpy-coverage run test_myapp.py --include myapp
40
+ mpy-coverage report --show-missing
41
+ ```
42
+
43
+ That's it. The run command executes `test_myapp.py` under micropython with the tracer active, saves a JSON data file to `.mpy_coverage/`, and the report command reads it and prints a coverage summary.
44
+
45
+ For an HTML report instead:
46
+ ```bash
47
+ mpy-coverage report --format html --output-dir htmlcov
48
+ ```
49
+
50
+ If you have multiple test files, run each one separately then generate a single merged report:
51
+ ```bash
52
+ mpy-coverage run tests/test_network.py --include myapp
53
+ mpy-coverage run tests/test_storage.py --include myapp
54
+ mpy-coverage report --show-missing
55
+ ```
56
+
57
+ ## Prerequisites
58
+
59
+ For unix-port testing you need a micropython coverage build:
60
+ ```bash
61
+ cd ports/unix && make submodules && make VARIANT=coverage
62
+ ```
63
+
64
+ Hardware targets need firmware built with `MICROPY_PY_SYS_SETTRACE=1`.
65
+
66
+ ## Usage
67
+
68
+ Each test run stores a timestamped JSON file, the report command merges all collected data.
69
+
70
+ ```bash
71
+ mpy-coverage run test_foo.py --include myapp
72
+ mpy-coverage run test_bar.py --include myapp
73
+ mpy-coverage report --method auto --show-missing
74
+ ```
75
+
76
+ micropython binary is auto-detected from PATH or `ports/unix/build-coverage/micropython` relative to CWD. Override with `--micropython`. Also works as `python -m mpy_coverage`.
77
+
78
+ ### Hardware
79
+
80
+ ```bash
81
+ # deploys tracer to device automatically, runs test, collects data
82
+ mpy-coverage run test_foo.py --device /dev/serial/by-id/usb-... --include myapp
83
+
84
+ # skip deploy if mpy_coverage.py is already on device
85
+ mpy-coverage run test_foo.py --device /dev/serial/by-id/usb-... --no-deploy --include myapp
86
+ ```
87
+
88
+ ### Multi-pass
89
+
90
+ Run tests separately, accumulate data, generate one merged report:
91
+ ```bash
92
+ mpy-coverage run tests/test_network.py --include myapp --branch
93
+ mpy-coverage run tests/test_storage.py --include myapp --branch
94
+ mpy-coverage run tests/test_ui.py --include myapp --branch
95
+ mpy-coverage report --show-missing --format html --output-dir htmlcov
96
+ ```
97
+
98
+ Data stored in `.mpy_coverage/` by default, override with `--data-dir`.
99
+
100
+ Other subcommands: `mpy-coverage list` and `mpy-coverage clean`.
101
+
102
+ ## Branch coverage
103
+
104
+ Branch coverage follows the same pattern as [coverage.py's branch measurement](https://coverage.readthedocs.io/en/latest/branch.html): `--branch` is a collection-time flag on `run`. Report commands auto-detect branch data from the JSON files and include branch columns when arc data is present — no `--branch` flag on `report`.
105
+
106
+ ```bash
107
+ # Collect with arcs
108
+ mpy-coverage run test_myapp.py --include myapp --branch
109
+
110
+ # Report auto-detects arc data and shows branch columns
111
+ mpy-coverage report --show-missing
112
+
113
+ # Force line-only report even when arc data exists
114
+ mpy-coverage report --show-missing --no-branch
115
+ ```
116
+
117
+ This matches [coverage.py's CLI](https://coverage.readthedocs.io/en/latest/commands/cmd_run.html) where `coverage run --branch` enables collection and `coverage report` auto-detects.
118
+
119
+ ## Test map
120
+
121
+ Show which tests cover which application files:
122
+
123
+ ```bash
124
+ mpy-coverage run tests/test_a.py --include myapp
125
+ mpy-coverage run tests/test_b.py --include myapp
126
+ mpy-coverage test-map
127
+ ```
128
+
129
+ Output:
130
+ ```
131
+ app_file , test
132
+ myapp.py , test_a
133
+ myapp.py , test_b
134
+ helpers.py , test_a
135
+ ```
136
+
137
+ For per-line detail:
138
+ ```bash
139
+ mpy-coverage test-map --line-detail
140
+ ```
141
+
142
+ Output:
143
+ ```
144
+ app_file , line, test
145
+ myapp.py , 1 , test_a
146
+ myapp.py , 1 , test_b
147
+ myapp.py , 5 , test_a
148
+ ```
149
+
150
+ Test names are extracted from `_metadata.test_script` in the JSON (set automatically by the CLI), with a filename-based fallback for older data files.
151
+
152
+ ## Report formats
153
+
154
+ Reports are generated by coverage.py's reporting infrastructure, so all standard formats are supported with identical output to `coverage report`, `coverage html`, etc. See coverage.py's documentation for format details:
155
+
156
+ | Format | Flag | Description | coverage.py docs |
157
+ |--------|------|-------------|-----------------|
158
+ | `text` | `--format text` (default) | Terminal summary table | [cmd_report](https://coverage.readthedocs.io/en/latest/commands/cmd_report.html) |
159
+ | `html` | `--format html` | Annotated source HTML | [cmd_html](https://coverage.readthedocs.io/en/latest/commands/cmd_html.html) |
160
+ | `json` | `--format json` | Machine-readable JSON | [cmd_json](https://coverage.readthedocs.io/en/latest/commands/cmd_json.html) |
161
+ | `xml` | `--format xml` | Cobertura XML for CI | [cmd_xml](https://coverage.readthedocs.io/en/latest/commands/cmd_xml.html) |
162
+ | `lcov` | `--format lcov` | LCOV tracefile | [cmd_lcov](https://coverage.readthedocs.io/en/latest/commands/cmd_lcov.html) |
163
+
164
+ Multiple formats in one invocation: `--format text --format html --format xml`
165
+
166
+ ## Tracer API
167
+
168
+ For direct use without the CLI wrapper. This runs on the MicroPython device, not the host.
169
+
170
+ ```python
171
+ import mpy_coverage
172
+
173
+ # Functional API
174
+ mpy_coverage.start(
175
+ include=['mymod'], # filename substring filters (list or None)
176
+ exclude=['test_'], # exclusion filters (list or None)
177
+ collect_executable=False, # collect co_lines data for pathway A
178
+ collect_arcs=False, # collect line-to-line arcs for branch coverage
179
+ )
180
+ # ... run code under test ...
181
+ mpy_coverage.stop()
182
+ data = mpy_coverage.get_data() # returns dict
183
+ mpy_coverage.export_json('out.json') # to file
184
+ mpy_coverage.export_json() # to stdout with serial delimiters
185
+
186
+ # Context manager
187
+ with mpy_coverage.coverage(include=['mymod'], collect_arcs=True):
188
+ import mymod
189
+ mymod.run()
190
+ ```
191
+
192
+ Filtering uses substring matching on filenames. The tracer always excludes itself.
193
+
194
+ Then on the host:
195
+ ```bash
196
+ python -m mpy_coverage.report coverage.json --method ast --show-missing
197
+ ```
198
+
199
+ ## Executable line detection
200
+
201
+ Three methods for determining which lines are executable:
202
+
203
+ | Method | Where | Pros | Cons |
204
+ |--------|-------|------|------|
205
+ | `co_lines` | On-device | No host tools needed, exact MicroPython view | Only sees called functions; uncalled code is invisible |
206
+ | `ast` | Host CPython | Sees all code, same parser as coverage.py | May differ from MicroPython's view on edge cases |
207
+ | `mpy` | Host via mpy-cross | Exact MicroPython bytecode view, sees all code | Requires mpy-cross binary |
208
+
209
+ `--method auto` (default) tries `mpy` first, falling back through the others. For cross-validation against coverage.py, `ast` is preferred since both sides use the same `PythonParser` — any differences in executed/missing lines reveal genuine tracing divergences rather than parser disagreements.
210
+
211
+ ## JSON format
212
+
213
+ ```json
214
+ {
215
+ "_metadata": {"test_script": "test_myapp"},
216
+ "executed": {"filename.py": [1, 3, 5, 7]},
217
+ "executable": {"filename.py": [1, 2, 3, 5, 6, 7, 10]},
218
+ "arcs": {"filename.py": [[-1, 1], [1, 3], [3, 5], [5, -1]]}
219
+ }
220
+ ```
221
+
222
+ - `executed`: always present — lines traced during execution
223
+ - `executable`: only present when `collect_executable=True` (co_lines pathway)
224
+ - `arcs`: only present when `collect_arcs=True` or `--branch` was used — each arc is `[from_line, to_line]` where negative values encode function entry/exit boundaries
225
+ - `_metadata`: added by the CLI wrapper, includes `test_script` name for test-map
226
+
227
+ ## Data directory structure
228
+
229
+ ```
230
+ .mpy_coverage/
231
+ 20260213_143022_test_foo.json
232
+ 20260213_143025_test_bar.json
233
+ 20260213_143030_test_baz.json
234
+ ```
235
+
236
+ Files are named `<YYYYMMDD_HHMMSS>_<script_basename>.json`. The `report` command merges all JSON files in the directory.
237
+
238
+ ## Divergences from CPython / coverage.py
239
+
240
+ ### `--branch` flag placement
241
+
242
+ In [coverage.py](https://coverage.readthedocs.io/en/latest/branch.html), `--branch` is a collection-time flag on `coverage run`. Report commands auto-detect branch data. This toolchain follows the same pattern: `--branch` on `run`, auto-detect on `report`.
243
+
244
+ ### except-header line tracing
245
+
246
+ MicroPython's compiler emits `set_source_line` for `except XxxError:` handler lines even when the exception path is not taken. CPython only traces these lines when the handler actually executes. This causes MicroPython to report one extra "executed" line per unvisited except clause.
247
+
248
+ In practice this means slightly higher coverage percentages for code with exception handlers that aren't exercised. The cross-validation tests document this as a known divergence.
249
+
250
+ ### Arc encoding conventions
251
+
252
+ MicroPython's settrace produces raw line-to-line transition arcs recorded as `(previous_line, current_line)` pairs. coverage.py's `PythonParser` models arcs using AST-derived conventions where negative line numbers encode function entry (`-co_firstlineno -> first_body_line`) and exit (`last_line -> -co_firstlineno`).
253
+
254
+ These representations don't map 1:1:
255
+ - Function entry arcs: MicroPython records `(def_line, first_body_line)` as a positive transition; coverage.py uses `(-def_line, first_body_line)`
256
+ - While/for loop arcs: MicroPython's bytecode produces different line-to-line transitions than CPython's AST-derived arc model
257
+ - Class body: Sequential class body lines appear as interior arcs in MicroPython but are modeled as entry arcs by coverage.py
258
+
259
+ Line-level coverage metrics (statements, executed, missing, percentage) are unaffected by these arc convention differences and match exactly between the two systems when using the `ast` method.
260
+
261
+ ### Constructs not cross-validated
262
+
263
+ The following constructs are excluded from cross-validation tests due to known irreconcilable differences:
264
+
265
+ - **List comprehensions**: CPython 3.12+ creates a hidden function scope; MicroPython doesn't. This produces different arc structures.
266
+ - **Generators/yield**: arc encoding for yield vs return differs between VMs.
267
+ - **Boolean short-circuit**: arc differences within a single line aren't detectable at line-level coverage.
268
+
269
+ ## Cross-validation tests
270
+
271
+ `test_coverage.py` includes 4 cross-validation trials that run the same code under both CPython+coverage.py and MicroPython+mpy_coverage, then compare metrics:
272
+
273
+ | Trial | Description |
274
+ |-------|-------------|
275
+ | `trial_xval_lines` | Line coverage with partial path execution (~66%) |
276
+ | `trial_xval_lines_full` | Line coverage with full path execution (100%) |
277
+ | `trial_xval_branches` | Branch data collection with partial paths |
278
+ | `trial_xval_branches_full` | Branch data collection with full paths |
279
+
280
+ The test target (`test_target_xval.py`) covers: if/elif/else, for+break+else, while, try/except/finally, nested closures, classes with methods, ternary expressions, multiple return points, and with-statements.
281
+
282
+ Run all trials:
283
+ ```bash
284
+ MPY_BINARY=path/to/micropython python tests/test_coverage.py
285
+ ```
286
+
287
+ ## Limitations
288
+
289
+ - settrace adds significant runtime overhead, not suitable for timing-sensitive code
290
+ - `co_lines` method only reports executable lines for functions that were entered, uncalled functions are invisible rather than showing 0%
291
+ - native/viper functions are not traced by settrace
292
+ - large `_executed` dicts may hit memory limits on constrained devices
293
+ - report integration overrides `Coverage._get_file_reporter()` which is a private API and may change across coverage.py versions
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["hatchling", "hatch-vcs"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mpy-coverage"
7
+ dynamic = ["version"]
8
+ description = "Code coverage for MicroPython"
9
+ requires-python = ">=3.10"
10
+ license = "MIT"
11
+ dependencies = ["coverage", "mpy-cross"]
12
+
13
+ [project.scripts]
14
+ mpy-coverage = "mpy_coverage.cli:main"
15
+
16
+ [dependency-groups]
17
+ dev = ["pytest", "ruff"]
18
+
19
+ [tool.hatch.version]
20
+ source = "vcs"
21
+
22
+ [tool.hatch.build.hooks.vcs]
23
+ version-file = "src/mpy_coverage/_version.py"
24
+
25
+ [tool.ruff]
26
+ line-length = 99
27
+ extend-exclude = ["src/mpy_coverage/_vendor"]
@@ -0,0 +1,14 @@
1
+ """MicroPython code coverage toolchain."""
2
+
3
+ from mpy_coverage._version import __version__ # noqa: F401
4
+
5
+
6
+ def __getattr__(name):
7
+ if name in ("merge_coverage_data", "run_report"):
8
+ from mpy_coverage.report import merge_coverage_data, run_report
9
+
10
+ return {"merge_coverage_data": merge_coverage_data, "run_report": run_report}[name]
11
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
12
+
13
+
14
+ __all__ = ["merge_coverage_data", "run_report", "__version__"]
@@ -0,0 +1,3 @@
1
+ from mpy_coverage.cli import main
2
+
3
+ main()