coverage-flashlight 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.
- coverage_flashlight-1.0.0/.github/workflows/release.yml +119 -0
- coverage_flashlight-1.0.0/.github/workflows/test.yml +23 -0
- coverage_flashlight-1.0.0/.gitignore +11 -0
- coverage_flashlight-1.0.0/LICENSE +23 -0
- coverage_flashlight-1.0.0/PKG-INFO +150 -0
- coverage_flashlight-1.0.0/README.md +138 -0
- coverage_flashlight-1.0.0/pyproject.toml +32 -0
- coverage_flashlight-1.0.0/src/coverage_flashlight/__init__.py +1 -0
- coverage_flashlight-1.0.0/src/coverage_flashlight/__main__.py +3 -0
- coverage_flashlight-1.0.0/src/coverage_flashlight/bootstrap/sitecustomize.py +20 -0
- coverage_flashlight-1.0.0/src/coverage_flashlight/cli.py +107 -0
- coverage_flashlight-1.0.0/src/coverage_flashlight/config.py +98 -0
- coverage_flashlight-1.0.0/src/coverage_flashlight/coverage.py +82 -0
- coverage_flashlight-1.0.0/src/coverage_flashlight/coverage_flashlight.html +158 -0
- coverage_flashlight-1.0.0/src/coverage_flashlight/execution_flashlight.html +256 -0
- coverage_flashlight-1.0.0/src/coverage_flashlight/flashlight.css +52 -0
- coverage_flashlight-1.0.0/src/coverage_flashlight/pytest_plugin.py +28 -0
- coverage_flashlight-1.0.0/src/coverage_flashlight/runner.py +226 -0
- coverage_flashlight-1.0.0/src/coverage_flashlight/trace.py +353 -0
- coverage_flashlight-1.0.0/tests/test_cli.py +203 -0
- coverage_flashlight-1.0.0/tests/test_coverage_flashlight.py +50 -0
- coverage_flashlight-1.0.0/tests/test_flashlight_trace.py +277 -0
- coverage_flashlight-1.0.0/uv.lock +235 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
run-name: Release ${{ inputs.release_type }} from ${{ github.ref_name }}
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
release_type:
|
|
7
|
+
description: Select release type
|
|
8
|
+
required: true
|
|
9
|
+
type: choice
|
|
10
|
+
options: [release-current, dev, release-patch, release-minor]
|
|
11
|
+
default: dev
|
|
12
|
+
concurrency:
|
|
13
|
+
group: release-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: false
|
|
15
|
+
env:
|
|
16
|
+
UV_PYTHON: "3.13"
|
|
17
|
+
UV_VERSION: "0.10.6"
|
|
18
|
+
jobs:
|
|
19
|
+
publish:
|
|
20
|
+
name: Publish PyPI package and GitHub release
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
environment: pypi
|
|
23
|
+
permissions:
|
|
24
|
+
contents: write
|
|
25
|
+
id-token: write
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
28
|
+
with:
|
|
29
|
+
fetch-depth: 0
|
|
30
|
+
persist-credentials: false
|
|
31
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
32
|
+
with:
|
|
33
|
+
version: ${{ env.UV_VERSION }}
|
|
34
|
+
enable-cache: true
|
|
35
|
+
cache-dependency-glob: uv.lock
|
|
36
|
+
- run: uv python install "$UV_PYTHON"
|
|
37
|
+
- name: Select version
|
|
38
|
+
id: version
|
|
39
|
+
env:
|
|
40
|
+
RELEASE_TYPE: ${{ inputs.release_type }}
|
|
41
|
+
run: |
|
|
42
|
+
CURRENT=$(uv version --short)
|
|
43
|
+
case "$RELEASE_TYPE" in
|
|
44
|
+
release-current) ;;
|
|
45
|
+
dev)
|
|
46
|
+
if [[ "$CURRENT" == *".dev"* ]]; then
|
|
47
|
+
uv version --bump dev
|
|
48
|
+
else
|
|
49
|
+
uv version --bump patch --bump dev
|
|
50
|
+
fi ;;
|
|
51
|
+
release-patch)
|
|
52
|
+
if [[ "$CURRENT" == *".dev"* ]]; then
|
|
53
|
+
uv version --bump stable
|
|
54
|
+
else
|
|
55
|
+
uv version --bump patch
|
|
56
|
+
fi ;;
|
|
57
|
+
release-minor) uv version --bump minor ;;
|
|
58
|
+
*) exit 1 ;;
|
|
59
|
+
esac
|
|
60
|
+
VERSION=$(uv version --short)
|
|
61
|
+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
62
|
+
echo "tag=v$VERSION" >> "$GITHUB_OUTPUT"
|
|
63
|
+
if [[ "$VERSION" == *".dev"* ]]; then
|
|
64
|
+
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
|
65
|
+
else
|
|
66
|
+
echo "prerelease=false" >> "$GITHUB_OUTPUT"
|
|
67
|
+
fi
|
|
68
|
+
- run: uv sync --locked --all-extras
|
|
69
|
+
- run: uv run pytest
|
|
70
|
+
- run: uv run ruff check .
|
|
71
|
+
- run: uv run ruff format --check .
|
|
72
|
+
- run: uv build
|
|
73
|
+
- name: Verify installed wheel and packaged report assets
|
|
74
|
+
run: |
|
|
75
|
+
wheel=(dist/*.whl)
|
|
76
|
+
uv venv /tmp/flashlight-wheel
|
|
77
|
+
uv pip install --python /tmp/flashlight-wheel/bin/python "${wheel[0]}"
|
|
78
|
+
/tmp/flashlight-wheel/bin/flashlight --help
|
|
79
|
+
mkdir /tmp/flashlight-smoke
|
|
80
|
+
cd /tmp/flashlight-smoke
|
|
81
|
+
printf 'answer = 42\n' > sample.py
|
|
82
|
+
/tmp/flashlight-wheel/bin/flashlight run -- sample.py
|
|
83
|
+
/tmp/flashlight-wheel/bin/flashlight run --trace -- sample.py
|
|
84
|
+
test -s htmlcov/flashlight.html
|
|
85
|
+
test -s htmlcov/execution.html
|
|
86
|
+
- name: Commit and tag version
|
|
87
|
+
env:
|
|
88
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
89
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
90
|
+
TAG: ${{ steps.version.outputs.tag }}
|
|
91
|
+
RELEASE_TYPE: ${{ inputs.release_type }}
|
|
92
|
+
run: |
|
|
93
|
+
git config user.name "github-actions[bot]"
|
|
94
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
95
|
+
git add pyproject.toml uv.lock
|
|
96
|
+
git diff --cached --check
|
|
97
|
+
if [[ "$RELEASE_TYPE" == "release-current" ]]; then
|
|
98
|
+
git diff --cached --exit-code
|
|
99
|
+
else
|
|
100
|
+
git commit -m "$VERSION"
|
|
101
|
+
fi
|
|
102
|
+
git tag "$TAG"
|
|
103
|
+
gh auth setup-git
|
|
104
|
+
git push --atomic origin "HEAD:$GITHUB_REF_NAME" "refs/tags/$TAG"
|
|
105
|
+
- name: Publish to PyPI
|
|
106
|
+
run: uv publish --trusted-publishing always
|
|
107
|
+
- name: Create GitHub release
|
|
108
|
+
env:
|
|
109
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
110
|
+
TAG: ${{ steps.version.outputs.tag }}
|
|
111
|
+
PRERELEASE: ${{ steps.version.outputs.prerelease }}
|
|
112
|
+
run: |
|
|
113
|
+
args=("$TAG" dist/* --verify-tag --generate-notes --title "$TAG")
|
|
114
|
+
if [[ "$PRERELEASE" == "true" ]]; then
|
|
115
|
+
args+=(--prerelease)
|
|
116
|
+
else
|
|
117
|
+
args+=(--latest)
|
|
118
|
+
fi
|
|
119
|
+
gh release create "${args[@]}"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
permissions:
|
|
4
|
+
contents: read
|
|
5
|
+
jobs:
|
|
6
|
+
test:
|
|
7
|
+
strategy:
|
|
8
|
+
fail-fast: false
|
|
9
|
+
matrix:
|
|
10
|
+
os: [ubuntu-latest, windows-latest]
|
|
11
|
+
python: ["3.11", "3.14"]
|
|
12
|
+
runs-on: ${{ matrix.os }}
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
15
|
+
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
|
16
|
+
with:
|
|
17
|
+
version: "0.10.6"
|
|
18
|
+
python-version: ${{ matrix.python }}
|
|
19
|
+
- run: uv sync --locked --all-extras
|
|
20
|
+
- run: uv run pytest
|
|
21
|
+
- run: uv run ruff check .
|
|
22
|
+
- run: uv run ruff format --check .
|
|
23
|
+
- run: uv build
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Boost Software License - Version 1.0 - August 17th, 2003
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person or organization
|
|
4
|
+
obtaining a copy of the software and accompanying documentation covered by
|
|
5
|
+
this license (the "Software") to use, reproduce, display, distribute,
|
|
6
|
+
execute, and transmit the Software, and to prepare derivative works of the
|
|
7
|
+
Software, and to permit third-parties to whom the Software is furnished to
|
|
8
|
+
do so, all subject to the following:
|
|
9
|
+
|
|
10
|
+
The copyright notices in the Software and this entire statement, including
|
|
11
|
+
the above license grant, this restriction and the following disclaimer,
|
|
12
|
+
must be included in all copies of the Software, in whole or in part, and
|
|
13
|
+
all derivative works of the Software, unless such copies or derivative
|
|
14
|
+
works are solely in the form of machine-executable object code generated by
|
|
15
|
+
a source language processor.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
|
20
|
+
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
|
21
|
+
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
|
22
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
23
|
+
DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: coverage-flashlight
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Explore Python coverage and replay execution with a code flashlight.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: coverage>=7.10.6
|
|
9
|
+
Provides-Extra: pytest
|
|
10
|
+
Requires-Dist: pytest>=8; extra == 'pytest'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# Coverage flashlight
|
|
14
|
+
|
|
15
|
+
See which Python code ran, then replay its execution as a flashlight revealing
|
|
16
|
+
source through fog. Reports are standalone HTML with source snapshots, shared
|
|
17
|
+
light/dark themes, and no server or external assets.
|
|
18
|
+
|
|
19
|
+
## Install and run
|
|
20
|
+
|
|
21
|
+
Install in the environment containing the program and its dependencies:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
uv add --dev 'coverage-flashlight[pytest]'
|
|
25
|
+
# Or: python -m pip install 'coverage-flashlight[pytest]'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
# Static per-test line/branch coverage; pytest arguments follow --.
|
|
32
|
+
flashlight pytest -- tests/ -k database
|
|
33
|
+
|
|
34
|
+
# Ordered playback, including threads and newly launched Python processes.
|
|
35
|
+
flashlight pytest --trace -- tests/ -k database
|
|
36
|
+
|
|
37
|
+
# A script, with its arguments, executed once.
|
|
38
|
+
flashlight run -- scripts/example.py --input sample.bin
|
|
39
|
+
flashlight run --trace -- scripts/example.py --input sample.bin
|
|
40
|
+
|
|
41
|
+
# Python modules work too, including other test runners.
|
|
42
|
+
flashlight run -m my_package --help
|
|
43
|
+
flashlight run --trace -m unittest discover
|
|
44
|
+
|
|
45
|
+
# Render existing coverage.py branch data without another run.
|
|
46
|
+
flashlight report
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Open **htmlcov/flashlight.html** for static coverage or
|
|
50
|
+
**htmlcov/execution.html** for playback. Use `--output path/to/report.html`
|
|
51
|
+
before `--` to choose another destination. `python -m coverage_flashlight`
|
|
52
|
+
is equivalent to `flashlight`.
|
|
53
|
+
|
|
54
|
+
Coverage and tracing are separate modes: a command never silently runs a script
|
|
55
|
+
twice. `--trace` selects execution recording instead of coverage.py. To create
|
|
56
|
+
both reports, run the two commands explicitly. The program's exit status is
|
|
57
|
+
preserved and failed runs still produce a report. Pytest mode continues recording
|
|
58
|
+
other cases after a failure and returns a nonzero status.
|
|
59
|
+
|
|
60
|
+
Pytest is an optional adapter. Static measurement uses coverage.py; ordered
|
|
61
|
+
playback uses Python trace events. Script/module execution requires no pytest
|
|
62
|
+
installation. Each pytest case runs in its own interpreter with its own context,
|
|
63
|
+
which also follows newly launched Python children. This includes fixture setup
|
|
64
|
+
and teardown. Isolation is slower than a normal suite run and can expose fixture
|
|
65
|
+
or order dependencies; continue running ordinary tests for correctness.
|
|
66
|
+
|
|
67
|
+
## Source selection
|
|
68
|
+
|
|
69
|
+
Run from the target project's root. By default, the tool includes Python files
|
|
70
|
+
inside that root, excluding hidden directories, virtual environments, build
|
|
71
|
+
outputs, and node_modules. Narrow this with `--source src --source tests` or:
|
|
72
|
+
|
|
73
|
+
```toml
|
|
74
|
+
[tool.coverage-flashlight]
|
|
75
|
+
source = ["src/my_package"]
|
|
76
|
+
trace-source = ["src/my_package", "tests"]
|
|
77
|
+
omit = ["*/generated/*"]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Paths are project-relative files or directories, not import names. `trace-source`
|
|
81
|
+
defaults to `source`; absent both, the project root is used. `--source` overrides
|
|
82
|
+
the configured selection for the current command. Keep source files inside the
|
|
83
|
+
project root. Selected but unexecuted files remain visible in the reports.
|
|
84
|
+
|
|
85
|
+
Coverage mode writes fresh data to `.coverage` after collection, replacing its
|
|
86
|
+
previous contents. Use `--data-file path` to keep a separate database. It enables
|
|
87
|
+
branch measurement and subprocess/`os._exit()` coverage itself; no coverage.py
|
|
88
|
+
configuration is required. Existing report exclusion rules are respected when
|
|
89
|
+
rendering. `flashlight report` can also render a combined database collected by
|
|
90
|
+
coverage.py or pytest-cov, preserving its context names.
|
|
91
|
+
|
|
92
|
+
Raw recordings and manifests are kept beside the report under `runs/<run-id>/`.
|
|
93
|
+
The default trace cap is 100,000 events per process; increase it with
|
|
94
|
+
`--max-events 500000`. Capture caps, interrupted hooks, source I/O failures, and
|
|
95
|
+
missing process completion markers are explicitly reported.
|
|
96
|
+
|
|
97
|
+
## Reading the reports
|
|
98
|
+
|
|
99
|
+
Static coverage distinguishes executed lines, branch gaps, execution in another
|
|
100
|
+
scope, and unexecuted statements. Select a line to inspect its contexts and missing
|
|
101
|
+
branch destinations. Coverage is evidence of execution, not proof of assertions.
|
|
102
|
+
|
|
103
|
+
Playback offers Play/Pause, stepping, timeline scrubbing, Next new line, a pytest
|
|
104
|
+
Test body shortcut, Whole run, and process/thread selection. Unrevealed ranges
|
|
105
|
+
collapse to grey `…` rows; Reveal source context expands them without marking
|
|
106
|
+
them visited. Selecting a module pauses playback; Follow execution restores
|
|
107
|
+
automatic navigation. The module map and source scroll inside the window.
|
|
108
|
+
|
|
109
|
+
The timeline records observed event order, not variable history or cross-thread
|
|
110
|
+
causality. Speed is events per second, not original elapsed time. Generator
|
|
111
|
+
suspension produces Python return/call events. Native code, generated code with
|
|
112
|
+
synthetic filenames, pre-existing processes, and threads created outside Python's
|
|
113
|
+
threading module are outside capture. A forcibly killed process can lose its
|
|
114
|
+
buffered tail. A skipped test can have no events. The source snapshot must remain
|
|
115
|
+
consistent across cases; changed source is rejected rather than misattributed.
|
|
116
|
+
|
|
117
|
+
Tracing changes timing and cannot share a trace hook with coverage.py or a
|
|
118
|
+
debugger. Pytest-xdist parallel execution is rejected: per-case subprocess
|
|
119
|
+
isolation already supplies attribution. Python children using `-S` or `-I`
|
|
120
|
+
can bypass the opt-in bootstrap. No startup hook is installed globally.
|
|
121
|
+
|
|
122
|
+
## Development and releases
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
uv sync --all-extras
|
|
126
|
+
uv run pytest
|
|
127
|
+
uv run ruff check .
|
|
128
|
+
uv run ruff format --check .
|
|
129
|
+
uv build
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Tests use disposable Python projects, real threads/subprocesses, failing and
|
|
133
|
+
skipped cases, arbitrary pytest layouts, and actual script/module entry points.
|
|
134
|
+
They require no IDA installation. HTML, CSS and the opt-in bootstrap ship in the
|
|
135
|
+
wheel and source distribution.
|
|
136
|
+
|
|
137
|
+
The manual `Release` GitHub Actions workflow supports `release-current`, `dev`,
|
|
138
|
+
`release-patch`, and `release-minor`. It tests, builds and checks the wheel, commits
|
|
139
|
+
an optional version bump, creates a tag, publishes with `uv publish`, and creates
|
|
140
|
+
a GitHub release. Versioning uses `uv version`; there is only one package version.
|
|
141
|
+
|
|
142
|
+
To enable publication, create the GitHub repository and configure a PyPI trusted
|
|
143
|
+
publisher for project **coverage-flashlight**, your repository owner/name,
|
|
144
|
+
workflow **release.yml**, and environment **pypi**. Create that GitHub environment
|
|
145
|
+
and allow the workflow to push release commits/tags. No PyPI API token is needed.
|
|
146
|
+
These setup steps do not publish anything until the workflow is dispatched.
|
|
147
|
+
|
|
148
|
+
References: [uv publishing](https://docs.astral.sh/uv/guides/package/),
|
|
149
|
+
[coverage.py subprocess measurement](https://coverage.readthedocs.io/en/latest/subprocess.html),
|
|
150
|
+
[Python tracing](https://docs.python.org/3/library/sys.html#sys.settrace).
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Coverage flashlight
|
|
2
|
+
|
|
3
|
+
See which Python code ran, then replay its execution as a flashlight revealing
|
|
4
|
+
source through fog. Reports are standalone HTML with source snapshots, shared
|
|
5
|
+
light/dark themes, and no server or external assets.
|
|
6
|
+
|
|
7
|
+
## Install and run
|
|
8
|
+
|
|
9
|
+
Install in the environment containing the program and its dependencies:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
uv add --dev 'coverage-flashlight[pytest]'
|
|
13
|
+
# Or: python -m pip install 'coverage-flashlight[pytest]'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
# Static per-test line/branch coverage; pytest arguments follow --.
|
|
20
|
+
flashlight pytest -- tests/ -k database
|
|
21
|
+
|
|
22
|
+
# Ordered playback, including threads and newly launched Python processes.
|
|
23
|
+
flashlight pytest --trace -- tests/ -k database
|
|
24
|
+
|
|
25
|
+
# A script, with its arguments, executed once.
|
|
26
|
+
flashlight run -- scripts/example.py --input sample.bin
|
|
27
|
+
flashlight run --trace -- scripts/example.py --input sample.bin
|
|
28
|
+
|
|
29
|
+
# Python modules work too, including other test runners.
|
|
30
|
+
flashlight run -m my_package --help
|
|
31
|
+
flashlight run --trace -m unittest discover
|
|
32
|
+
|
|
33
|
+
# Render existing coverage.py branch data without another run.
|
|
34
|
+
flashlight report
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Open **htmlcov/flashlight.html** for static coverage or
|
|
38
|
+
**htmlcov/execution.html** for playback. Use `--output path/to/report.html`
|
|
39
|
+
before `--` to choose another destination. `python -m coverage_flashlight`
|
|
40
|
+
is equivalent to `flashlight`.
|
|
41
|
+
|
|
42
|
+
Coverage and tracing are separate modes: a command never silently runs a script
|
|
43
|
+
twice. `--trace` selects execution recording instead of coverage.py. To create
|
|
44
|
+
both reports, run the two commands explicitly. The program's exit status is
|
|
45
|
+
preserved and failed runs still produce a report. Pytest mode continues recording
|
|
46
|
+
other cases after a failure and returns a nonzero status.
|
|
47
|
+
|
|
48
|
+
Pytest is an optional adapter. Static measurement uses coverage.py; ordered
|
|
49
|
+
playback uses Python trace events. Script/module execution requires no pytest
|
|
50
|
+
installation. Each pytest case runs in its own interpreter with its own context,
|
|
51
|
+
which also follows newly launched Python children. This includes fixture setup
|
|
52
|
+
and teardown. Isolation is slower than a normal suite run and can expose fixture
|
|
53
|
+
or order dependencies; continue running ordinary tests for correctness.
|
|
54
|
+
|
|
55
|
+
## Source selection
|
|
56
|
+
|
|
57
|
+
Run from the target project's root. By default, the tool includes Python files
|
|
58
|
+
inside that root, excluding hidden directories, virtual environments, build
|
|
59
|
+
outputs, and node_modules. Narrow this with `--source src --source tests` or:
|
|
60
|
+
|
|
61
|
+
```toml
|
|
62
|
+
[tool.coverage-flashlight]
|
|
63
|
+
source = ["src/my_package"]
|
|
64
|
+
trace-source = ["src/my_package", "tests"]
|
|
65
|
+
omit = ["*/generated/*"]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Paths are project-relative files or directories, not import names. `trace-source`
|
|
69
|
+
defaults to `source`; absent both, the project root is used. `--source` overrides
|
|
70
|
+
the configured selection for the current command. Keep source files inside the
|
|
71
|
+
project root. Selected but unexecuted files remain visible in the reports.
|
|
72
|
+
|
|
73
|
+
Coverage mode writes fresh data to `.coverage` after collection, replacing its
|
|
74
|
+
previous contents. Use `--data-file path` to keep a separate database. It enables
|
|
75
|
+
branch measurement and subprocess/`os._exit()` coverage itself; no coverage.py
|
|
76
|
+
configuration is required. Existing report exclusion rules are respected when
|
|
77
|
+
rendering. `flashlight report` can also render a combined database collected by
|
|
78
|
+
coverage.py or pytest-cov, preserving its context names.
|
|
79
|
+
|
|
80
|
+
Raw recordings and manifests are kept beside the report under `runs/<run-id>/`.
|
|
81
|
+
The default trace cap is 100,000 events per process; increase it with
|
|
82
|
+
`--max-events 500000`. Capture caps, interrupted hooks, source I/O failures, and
|
|
83
|
+
missing process completion markers are explicitly reported.
|
|
84
|
+
|
|
85
|
+
## Reading the reports
|
|
86
|
+
|
|
87
|
+
Static coverage distinguishes executed lines, branch gaps, execution in another
|
|
88
|
+
scope, and unexecuted statements. Select a line to inspect its contexts and missing
|
|
89
|
+
branch destinations. Coverage is evidence of execution, not proof of assertions.
|
|
90
|
+
|
|
91
|
+
Playback offers Play/Pause, stepping, timeline scrubbing, Next new line, a pytest
|
|
92
|
+
Test body shortcut, Whole run, and process/thread selection. Unrevealed ranges
|
|
93
|
+
collapse to grey `…` rows; Reveal source context expands them without marking
|
|
94
|
+
them visited. Selecting a module pauses playback; Follow execution restores
|
|
95
|
+
automatic navigation. The module map and source scroll inside the window.
|
|
96
|
+
|
|
97
|
+
The timeline records observed event order, not variable history or cross-thread
|
|
98
|
+
causality. Speed is events per second, not original elapsed time. Generator
|
|
99
|
+
suspension produces Python return/call events. Native code, generated code with
|
|
100
|
+
synthetic filenames, pre-existing processes, and threads created outside Python's
|
|
101
|
+
threading module are outside capture. A forcibly killed process can lose its
|
|
102
|
+
buffered tail. A skipped test can have no events. The source snapshot must remain
|
|
103
|
+
consistent across cases; changed source is rejected rather than misattributed.
|
|
104
|
+
|
|
105
|
+
Tracing changes timing and cannot share a trace hook with coverage.py or a
|
|
106
|
+
debugger. Pytest-xdist parallel execution is rejected: per-case subprocess
|
|
107
|
+
isolation already supplies attribution. Python children using `-S` or `-I`
|
|
108
|
+
can bypass the opt-in bootstrap. No startup hook is installed globally.
|
|
109
|
+
|
|
110
|
+
## Development and releases
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
uv sync --all-extras
|
|
114
|
+
uv run pytest
|
|
115
|
+
uv run ruff check .
|
|
116
|
+
uv run ruff format --check .
|
|
117
|
+
uv build
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Tests use disposable Python projects, real threads/subprocesses, failing and
|
|
121
|
+
skipped cases, arbitrary pytest layouts, and actual script/module entry points.
|
|
122
|
+
They require no IDA installation. HTML, CSS and the opt-in bootstrap ship in the
|
|
123
|
+
wheel and source distribution.
|
|
124
|
+
|
|
125
|
+
The manual `Release` GitHub Actions workflow supports `release-current`, `dev`,
|
|
126
|
+
`release-patch`, and `release-minor`. It tests, builds and checks the wheel, commits
|
|
127
|
+
an optional version bump, creates a tag, publishes with `uv publish`, and creates
|
|
128
|
+
a GitHub release. Versioning uses `uv version`; there is only one package version.
|
|
129
|
+
|
|
130
|
+
To enable publication, create the GitHub repository and configure a PyPI trusted
|
|
131
|
+
publisher for project **coverage-flashlight**, your repository owner/name,
|
|
132
|
+
workflow **release.yml**, and environment **pypi**. Create that GitHub environment
|
|
133
|
+
and allow the workflow to push release commits/tags. No PyPI API token is needed.
|
|
134
|
+
These setup steps do not publish anything until the workflow is dispatched.
|
|
135
|
+
|
|
136
|
+
References: [uv publishing](https://docs.astral.sh/uv/guides/package/),
|
|
137
|
+
[coverage.py subprocess measurement](https://coverage.readthedocs.io/en/latest/subprocess.html),
|
|
138
|
+
[Python tracing](https://docs.python.org/3/library/sys.html#sys.settrace).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "coverage-flashlight"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Explore Python coverage and replay execution with a code flashlight."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
dependencies = ["coverage>=7.10.6"]
|
|
10
|
+
|
|
11
|
+
[project.optional-dependencies]
|
|
12
|
+
pytest = ["pytest>=8"]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
flashlight = "coverage_flashlight.cli:main"
|
|
16
|
+
|
|
17
|
+
[dependency-groups]
|
|
18
|
+
dev = ["pytest>=8", "ruff>=0.12.0"]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["hatchling"]
|
|
22
|
+
build-backend = "hatchling.build"
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["src/coverage_flashlight"]
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
testpaths = ["tests"]
|
|
29
|
+
|
|
30
|
+
[tool.coverage-flashlight]
|
|
31
|
+
source = ["src/coverage_flashlight"]
|
|
32
|
+
trace-source = ["src/coverage_flashlight", "tests"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Coverage reports and opt-in ordered Python execution recording."""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Opt-in child-process bootstrap; used only by the execution flashlight runner."""
|
|
2
|
+
|
|
3
|
+
import importlib.machinery
|
|
4
|
+
import importlib.util
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
# Preserve the environment's own startup customization.
|
|
10
|
+
_here = Path(__file__).parent.resolve()
|
|
11
|
+
_existing = importlib.machinery.PathFinder.find_spec(
|
|
12
|
+
"sitecustomize", [path for path in sys.path if Path(path).resolve() != _here]
|
|
13
|
+
)
|
|
14
|
+
if _existing is not None and _existing.loader is not None:
|
|
15
|
+
_existing.loader.exec_module(importlib.util.module_from_spec(_existing))
|
|
16
|
+
|
|
17
|
+
if os.environ.get("COVERAGE_FLASHLIGHT_DIR"):
|
|
18
|
+
from coverage_flashlight.trace import install
|
|
19
|
+
|
|
20
|
+
install()
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""The flashlight command; target arguments are passed through after --."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import subprocess
|
|
5
|
+
from importlib.metadata import version
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from coverage.exceptions import CoverageException
|
|
9
|
+
|
|
10
|
+
from coverage_flashlight.config import project_sources
|
|
11
|
+
from coverage_flashlight.runner import measure, render_coverage
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main(argv=None) -> int:
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
prog="flashlight",
|
|
17
|
+
description="Explore Python coverage or replay executed lines.",
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--version", action="version", version=version("coverage-flashlight")
|
|
21
|
+
)
|
|
22
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
23
|
+
for name, help_text in [
|
|
24
|
+
("pytest", "Measure each selected pytest case"),
|
|
25
|
+
("run", "Execute a Python script or module once"),
|
|
26
|
+
]:
|
|
27
|
+
command = commands.add_parser(name, help=help_text)
|
|
28
|
+
command.add_argument(
|
|
29
|
+
"--trace",
|
|
30
|
+
action="store_true",
|
|
31
|
+
help="record ordered execution instead of line/branch coverage",
|
|
32
|
+
)
|
|
33
|
+
command.add_argument(
|
|
34
|
+
"--source",
|
|
35
|
+
action="append",
|
|
36
|
+
help="project-relative source file or directory; repeat for multiple paths",
|
|
37
|
+
)
|
|
38
|
+
command.add_argument(
|
|
39
|
+
"--output",
|
|
40
|
+
type=Path,
|
|
41
|
+
help="HTML report path (default: htmlcov/flashlight.html or execution.html)",
|
|
42
|
+
)
|
|
43
|
+
command.add_argument(
|
|
44
|
+
"--data-file",
|
|
45
|
+
type=Path,
|
|
46
|
+
default=Path(".coverage"),
|
|
47
|
+
help="coverage database to replace (coverage mode only)",
|
|
48
|
+
)
|
|
49
|
+
command.add_argument(
|
|
50
|
+
"--max-events",
|
|
51
|
+
type=int,
|
|
52
|
+
default=100_000,
|
|
53
|
+
help="maximum trace events per process",
|
|
54
|
+
)
|
|
55
|
+
if name == "run":
|
|
56
|
+
command.add_argument(
|
|
57
|
+
"-m",
|
|
58
|
+
"--module",
|
|
59
|
+
action="store_true",
|
|
60
|
+
help="run the target as a Python module",
|
|
61
|
+
)
|
|
62
|
+
command.add_argument(
|
|
63
|
+
"arguments", nargs=argparse.REMAINDER, help="target arguments, after --"
|
|
64
|
+
)
|
|
65
|
+
report = commands.add_parser(
|
|
66
|
+
"report", help="Render existing branch coverage without executing code"
|
|
67
|
+
)
|
|
68
|
+
report.add_argument("--data-file", type=Path, default=Path(".coverage"))
|
|
69
|
+
report.add_argument("--output", type=Path, default=Path("htmlcov/flashlight.html"))
|
|
70
|
+
args = parser.parse_args(argv)
|
|
71
|
+
try:
|
|
72
|
+
if args.command == "report":
|
|
73
|
+
if not args.data_file.is_file():
|
|
74
|
+
parser.error(f"Coverage database does not exist: {args.data_file}")
|
|
75
|
+
render_coverage(args.data_file, args.output, Path.cwd())
|
|
76
|
+
return 0
|
|
77
|
+
arguments = (
|
|
78
|
+
args.arguments[1:] if args.arguments[:1] == ["--"] else args.arguments
|
|
79
|
+
)
|
|
80
|
+
if args.command == "run" and not arguments:
|
|
81
|
+
parser.error("run requires a script path or module name")
|
|
82
|
+
if args.max_events <= 0:
|
|
83
|
+
parser.error("--max-events must be positive")
|
|
84
|
+
sources = project_sources(Path.cwd(), trace=args.trace, override=args.source)
|
|
85
|
+
return measure(
|
|
86
|
+
arguments,
|
|
87
|
+
args.output
|
|
88
|
+
or Path(
|
|
89
|
+
"htmlcov/execution.html" if args.trace else "htmlcov/flashlight.html"
|
|
90
|
+
),
|
|
91
|
+
sources,
|
|
92
|
+
trace=args.trace,
|
|
93
|
+
pytest=args.command == "pytest",
|
|
94
|
+
module=getattr(args, "module", False),
|
|
95
|
+
max_events=args.max_events,
|
|
96
|
+
data_file=args.data_file,
|
|
97
|
+
)
|
|
98
|
+
except subprocess.CalledProcessError as error:
|
|
99
|
+
return error.returncode
|
|
100
|
+
except (ValueError, OSError, CoverageException) as error:
|
|
101
|
+
parser.error(str(error))
|
|
102
|
+
except KeyboardInterrupt:
|
|
103
|
+
return 130
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
raise SystemExit(main())
|