sundgren 0.1.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.
- sundgren-0.1.0/.github/workflows/ci.yml +53 -0
- sundgren-0.1.0/.github/workflows/release.yml +58 -0
- sundgren-0.1.0/.gitignore +8 -0
- sundgren-0.1.0/DECISIONS.md +218 -0
- sundgren-0.1.0/LICENSE +201 -0
- sundgren-0.1.0/PKG-INFO +125 -0
- sundgren-0.1.0/README.md +100 -0
- sundgren-0.1.0/pyproject.toml +66 -0
- sundgren-0.1.0/src/sundgren/__init__.py +3 -0
- sundgren-0.1.0/src/sundgren/__main__.py +3 -0
- sundgren-0.1.0/src/sundgren/cli.py +459 -0
- sundgren-0.1.0/src/sundgren/core/__init__.py +1 -0
- sundgren-0.1.0/src/sundgren/core/diff.py +536 -0
- sundgren-0.1.0/src/sundgren/core/hashing.py +21 -0
- sundgren-0.1.0/src/sundgren/core/ingest/__init__.py +1 -0
- sundgren-0.1.0/src/sundgren/core/ingest/base.py +11 -0
- sundgren-0.1.0/src/sundgren/core/ingest/lcov.py +211 -0
- sundgren-0.1.0/src/sundgren/core/junit.py +81 -0
- sundgren-0.1.0/src/sundgren/core/linemap.py +103 -0
- sundgren-0.1.0/src/sundgren/core/model.py +120 -0
- sundgren-0.1.0/src/sundgren/core/normalize.py +101 -0
- sundgren-0.1.0/src/sundgren/core/patch.py +318 -0
- sundgren-0.1.0/src/sundgren/core/report/__init__.py +1 -0
- sundgren-0.1.0/src/sundgren/core/report/delta.schema.json +420 -0
- sundgren-0.1.0/src/sundgren/core/report/json_report.py +156 -0
- sundgren-0.1.0/src/sundgren/core/report/text_report.py +98 -0
- sundgren-0.1.0/src/sundgren/core/rollup.py +188 -0
- sundgren-0.1.0/src/sundgren/core/symbols.py +90 -0
- sundgren-0.1.0/src/sundgren/lang/__init__.py +1 -0
- sundgren-0.1.0/src/sundgren/lang/python/__init__.py +13 -0
- sundgren-0.1.0/src/sundgren/lang/python/presets.toml +15 -0
- sundgren-0.1.0/src/sundgren/lang/python/selector.py +46 -0
- sundgren-0.1.0/src/sundgren/lang/python/symbolizer.py +74 -0
- sundgren-0.1.0/src/sundgren/lang/rust/__init__.py +7 -0
- sundgren-0.1.0/src/sundgren/lang/rust/presets.toml +14 -0
- sundgren-0.1.0/src/sundgren/run/__init__.py +1 -0
- sundgren-0.1.0/src/sundgren/run/guards.py +26 -0
- sundgren-0.1.0/src/sundgren/run/presets.py +144 -0
- sundgren-0.1.0/src/sundgren/run/runner.py +164 -0
- sundgren-0.1.0/src/sundgren/run/selectors.py +61 -0
- sundgren-0.1.0/src/sundgren/run/template.py +39 -0
- sundgren-0.1.0/src/sundgren/run/workspace.py +180 -0
- sundgren-0.1.0/tests/e2e/conftest.py +11 -0
- sundgren-0.1.0/tests/e2e/test_shed_py.py +440 -0
- sundgren-0.1.0/tests/e2e/test_shed_rs.py +97 -0
- sundgren-0.1.0/tests/fixtures/junit/nextest.xml +44 -0
- sundgren-0.1.0/tests/fixtures/junit/pytest.xml +43 -0
- sundgren-0.1.0/tests/fixtures/lcov/coveragepy.lcov +54 -0
- sundgren-0.1.0/tests/fixtures/lcov/duplicate_sf.lcov +83 -0
- sundgren-0.1.0/tests/fixtures/lcov/lcov2_fn.lcov +42 -0
- sundgren-0.1.0/tests/fixtures/lcov/llvm_cov.lcov +59 -0
- sundgren-0.1.0/tests/fixtures/lcov/tarpaulin.lcov +20 -0
- sundgren-0.1.0/tests/fixtures/patches/bad.patch +12 -0
- sundgren-0.1.0/tests/fixtures/patches/drop_zero_case.patch +12 -0
- sundgren-0.1.0/tests/fixtures/patches/ignore_divide_rs.patch +12 -0
- sundgren-0.1.0/tests/fixtures/patches/no_op.patch +12 -0
- sundgren-0.1.0/tests/fixtures/patches/skip_divide.patch +12 -0
- sundgren-0.1.0/tests/fixtures/patches/skip_divide_mnemonic.patch +12 -0
- sundgren-0.1.0/tests/fixtures/patches/touch_mid.patch +20 -0
- sundgren-0.1.0/tests/fixtures/patches/touch_mid_offset.patch +20 -0
- sundgren-0.1.0/tests/fixtures/patches/touch_source.patch +17 -0
- sundgren-0.1.0/tests/fixtures/patches/unit/binary.patch +13 -0
- sundgren-0.1.0/tests/fixtures/patches/unit/deleted_file.patch +11 -0
- sundgren-0.1.0/tests/fixtures/patches/unit/format_patch.patch +27 -0
- sundgren-0.1.0/tests/fixtures/patches/unit/git_binary.patch +29 -0
- sundgren-0.1.0/tests/fixtures/patches/unit/multi_file.patch +36 -0
- sundgren-0.1.0/tests/fixtures/patches/unit/multi_hunk.patch +29 -0
- sundgren-0.1.0/tests/fixtures/patches/unit/new_file.patch +9 -0
- sundgren-0.1.0/tests/fixtures/patches/unit/no_newline_eof.patch +20 -0
- sundgren-0.1.0/tests/fixtures/patches/unit/plain_diff.patch +11 -0
- sundgren-0.1.0/tests/fixtures/patches/unit/pure_add.patch +13 -0
- sundgren-0.1.0/tests/fixtures/patches/unit/pure_delete.patch +13 -0
- sundgren-0.1.0/tests/fixtures/patches/unit/pure_rename.patch +4 -0
- sundgren-0.1.0/tests/fixtures/patches/unit/quoted_paths.patch +19 -0
- sundgren-0.1.0/tests/fixtures/patches/unit/rename_with_edits.patch +16 -0
- sundgren-0.1.0/tests/fixtures/patches/unit/single_hunk.patch +16 -0
- sundgren-0.1.0/tests/fixtures/shed_py/.coveragerc +3 -0
- sundgren-0.1.0/tests/fixtures/shed_py/pyproject.toml +2 -0
- sundgren-0.1.0/tests/fixtures/shed_py/shed/__init__.py +0 -0
- sundgren-0.1.0/tests/fixtures/shed_py/shed/calc.py +15 -0
- sundgren-0.1.0/tests/fixtures/shed_py/tests/test_calc.py +15 -0
- sundgren-0.1.0/tests/fixtures/shed_py_runs/drop_zero_case/base.lcov +34 -0
- sundgren-0.1.0/tests/fixtures/shed_py_runs/drop_zero_case/base.xml +1 -0
- sundgren-0.1.0/tests/fixtures/shed_py_runs/drop_zero_case/patched.lcov +34 -0
- sundgren-0.1.0/tests/fixtures/shed_py_runs/drop_zero_case/patched.xml +1 -0
- sundgren-0.1.0/tests/fixtures/shed_py_runs/skip_divide/base.lcov +34 -0
- sundgren-0.1.0/tests/fixtures/shed_py_runs/skip_divide/base.xml +1 -0
- sundgren-0.1.0/tests/fixtures/shed_py_runs/skip_divide/patched.lcov +34 -0
- sundgren-0.1.0/tests/fixtures/shed_py_runs/skip_divide/patched.xml +1 -0
- sundgren-0.1.0/tests/fixtures/shed_rs/.gitignore +1 -0
- sundgren-0.1.0/tests/fixtures/shed_rs/Cargo.toml +7 -0
- sundgren-0.1.0/tests/fixtures/shed_rs/src/lib.rs +34 -0
- sundgren-0.1.0/tests/support/__init__.py +5 -0
- sundgren-0.1.0/tests/support/shed.py +152 -0
- sundgren-0.1.0/tests/unit/test_cli_patch.py +123 -0
- sundgren-0.1.0/tests/unit/test_cli_version.py +13 -0
- sundgren-0.1.0/tests/unit/test_diff.py +567 -0
- sundgren-0.1.0/tests/unit/test_guards.py +45 -0
- sundgren-0.1.0/tests/unit/test_json_report.py +352 -0
- sundgren-0.1.0/tests/unit/test_junit.py +146 -0
- sundgren-0.1.0/tests/unit/test_layering.py +59 -0
- sundgren-0.1.0/tests/unit/test_lcov.py +315 -0
- sundgren-0.1.0/tests/unit/test_linemap.py +178 -0
- sundgren-0.1.0/tests/unit/test_normalize.py +133 -0
- sundgren-0.1.0/tests/unit/test_patch.py +296 -0
- sundgren-0.1.0/tests/unit/test_presets.py +146 -0
- sundgren-0.1.0/tests/unit/test_python_symbolizer.py +288 -0
- sundgren-0.1.0/tests/unit/test_rollup.py +214 -0
- sundgren-0.1.0/tests/unit/test_runner.py +206 -0
- sundgren-0.1.0/tests/unit/test_selectors.py +103 -0
- sundgren-0.1.0/tests/unit/test_symbols.py +206 -0
- sundgren-0.1.0/tests/unit/test_template.py +71 -0
- sundgren-0.1.0/tests/unit/test_text_report.py +118 -0
- sundgren-0.1.0/tests/unit/test_workspace.py +291 -0
- sundgren-0.1.0/uv.lock +683 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
env:
|
|
13
|
+
UV_LOCKED: "1"
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
lint:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v7
|
|
20
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
21
|
+
- run: uv sync
|
|
22
|
+
- run: uv run ruff check .
|
|
23
|
+
- run: uv run ruff format --check .
|
|
24
|
+
- run: uv run mypy
|
|
25
|
+
|
|
26
|
+
test:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
strategy:
|
|
29
|
+
fail-fast: false
|
|
30
|
+
matrix:
|
|
31
|
+
python: ["3.11", "3.12", "3.13", "3.14"]
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v7
|
|
34
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
35
|
+
with:
|
|
36
|
+
python-version: ${{ matrix.python }}
|
|
37
|
+
- run: uv sync
|
|
38
|
+
- run: uv run pytest tests/unit
|
|
39
|
+
- run: uv run pytest tests/e2e/test_shed_py.py -rs
|
|
40
|
+
|
|
41
|
+
e2e-rust:
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v7
|
|
45
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
46
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
47
|
+
with:
|
|
48
|
+
components: llvm-tools-preview
|
|
49
|
+
- uses: taiki-e/install-action@v2
|
|
50
|
+
with:
|
|
51
|
+
tool: cargo-llvm-cov,cargo-tarpaulin
|
|
52
|
+
- run: uv sync
|
|
53
|
+
- run: uv run pytest tests/e2e/test_shed_rs.py -rs
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
env:
|
|
11
|
+
UV_LOCKED: "1"
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
19
|
+
- run: uv sync
|
|
20
|
+
- name: Tag matches package version
|
|
21
|
+
run: |
|
|
22
|
+
version=$(uv run python -c "import sundgren; print(sundgren.__version__)")
|
|
23
|
+
test "v$version" = "$GITHUB_REF_NAME" || { echo "tag $GITHUB_REF_NAME != v$version"; exit 1; }
|
|
24
|
+
- run: uv run pytest tests/unit
|
|
25
|
+
- run: uv build
|
|
26
|
+
- uses: actions/upload-artifact@v7
|
|
27
|
+
with:
|
|
28
|
+
name: dist
|
|
29
|
+
path: dist/
|
|
30
|
+
|
|
31
|
+
publish:
|
|
32
|
+
needs: build
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
environment:
|
|
35
|
+
name: pypi
|
|
36
|
+
url: https://pypi.org/p/sundgren
|
|
37
|
+
permissions:
|
|
38
|
+
id-token: write # PyPI Trusted Publishing
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/download-artifact@v8
|
|
41
|
+
with:
|
|
42
|
+
name: dist
|
|
43
|
+
path: dist/
|
|
44
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
45
|
+
|
|
46
|
+
github-release:
|
|
47
|
+
needs: publish
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
permissions:
|
|
50
|
+
contents: write
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/download-artifact@v8
|
|
53
|
+
with:
|
|
54
|
+
name: dist
|
|
55
|
+
path: dist/
|
|
56
|
+
- run: gh release create "$GITHUB_REF_NAME" dist/* --repo "$GITHUB_REPOSITORY" --generate-notes
|
|
57
|
+
env:
|
|
58
|
+
GH_TOKEN: ${{ github.token }}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Decisions
|
|
2
|
+
|
|
3
|
+
Choices made where the scaffold spec was silent or where the implementation deviates from it.
|
|
4
|
+
Spec deviations are marked **(deviation)**.
|
|
5
|
+
|
|
6
|
+
## Build and layout
|
|
7
|
+
|
|
8
|
+
- hatchling build. `coverage` and `pytest` are dev dependencies only, for the e2e target project;
|
|
9
|
+
sundgren never imports them.
|
|
10
|
+
- Language packs register their own `presets.toml` (`register_builtin_presets`), so `run` never
|
|
11
|
+
globs `lang/`. Only `cli.py` imports `lang`.
|
|
12
|
+
|
|
13
|
+
## CLI and run mode
|
|
14
|
+
|
|
15
|
+
- `--format` defaults to json. `{python}` is `python` on PATH, else `python3`; `--python` overrides.
|
|
16
|
+
- Run mode builds line maps from the diff git actually applied, not from the user's patch file:
|
|
17
|
+
`git apply --index`, then `git diff --cached` in the patched worktree. That diff's shape is pinned
|
|
18
|
+
against user git config (prefixes, colour, external diff, textconv, rename detection, context).
|
|
19
|
+
An offset or a non-`a/`/`b/` prefix in the user's patch therefore can't skew the line maps.
|
|
20
|
+
- A hunk applied at an offset adds the warning `patch_offset`. `i/`/`w/` prefixes don't warn.
|
|
21
|
+
- `meta.patch.files` comes from the applied diff; `meta.patch.sha256` is the user's file.
|
|
22
|
+
- Patches are read as bytes and decoded as UTF-8 with `surrogateescape`, without newline
|
|
23
|
+
translation, so CRs and non-UTF-8 path bytes survive.
|
|
24
|
+
- Exit codes: a bad `--base` ref is 2, a patch that doesn't apply is 3, any other git failure is 1.
|
|
25
|
+
- `meta.base_ref` is the full sha. `meta.commands` expands only `{python}` and `{tests}`, because temp
|
|
26
|
+
paths would break determinism.
|
|
27
|
+
- `sundgren diff --patch` uses the user's patch as it is and assumes it applies exactly to the base
|
|
28
|
+
tree.
|
|
29
|
+
|
|
30
|
+
## Presets, templates, runner
|
|
31
|
+
|
|
32
|
+
- Only `commands` (non-empty) and `lcov` are required in a preset. Unknown keys are rejected.
|
|
33
|
+
Defaults: description "", env {}, junit/selector None, tests_style "args", shell false, hints ().
|
|
34
|
+
- If `sundgren.toml` and `[tool.sundgren.presets]` define the same name, `sundgren.toml` wins. Two
|
|
35
|
+
language packs defining the same name differently raise `PresetError`.
|
|
36
|
+
- Templates expand in one regex pass, so an expanded value containing `{out}` is not expanded again.
|
|
37
|
+
In shell mode only `{tests}` is quoted.
|
|
38
|
+
- The runner runs every command; a side fails only when its lcov is missing. `returncode` is the
|
|
39
|
+
first non-zero code. Commands get stdin from /dev/null. A command that can't start is logged as
|
|
40
|
+
127 (missing) or 126 (other OSError).
|
|
41
|
+
- Relative lcov/junit templates resolve against the worktree. Leftover output files are deleted
|
|
42
|
+
before the commands run. Repeats go to `run-1`, `run-2`, ….
|
|
43
|
+
- Each side runs with its own `CARGO_TARGET_DIR`: slower, but the trees never share build artifacts.
|
|
44
|
+
- Both worktrees are checked out at the resolved base sha. If setup fails, everything is cleaned up,
|
|
45
|
+
even with `--keep-worktrees`. With `--workdir`, only `base/` and `patched/` are removed.
|
|
46
|
+
|
|
47
|
+
## Test selection (Python)
|
|
48
|
+
|
|
49
|
+
- **(deviation)** A touched test file that was added, deleted or renamed gives `run_all`. The same
|
|
50
|
+
selection runs on both sides, so a new test file would otherwise fail on the base side.
|
|
51
|
+
- With `tests_style = "args"`, test paths missing on a side are dropped for that side. If none
|
|
52
|
+
remain, that side counts as empty coverage with a `nothing_to_run` warning.
|
|
53
|
+
|
|
54
|
+
## Patch parsing and line maps
|
|
55
|
+
|
|
56
|
+
- Paths drop their first component as `git apply -p1` does (`a/`, `i/`, `old/`, anything); a bare
|
|
57
|
+
name is kept. `rename from/to` names are never stripped. `/dev/null` is `None`.
|
|
58
|
+
- `new file mode` / `deleted file mode` override the `---`/`+++` paths; `rename from/to` and
|
|
59
|
+
`---`/`+++` override the `diff --git` header.
|
|
60
|
+
- Plain (non-git) diffs never rename: both sides take the `+++` name, or the `---` name when it is a
|
|
61
|
+
prefix of it (`diff -u x.orig x` is a change to `x`).
|
|
62
|
+
- C-quoted paths are decoded (escapes, octal bytes, UTF-8 with surrogateescape).
|
|
63
|
+
- Text before the first diff is skipped (format-patch email, commit message, diffstat, signature).
|
|
64
|
+
Unknown lines inside a git header are skipped.
|
|
65
|
+
- Binary diffs give `binary=True`, no hunks, and a warning; their line map has no changed lines.
|
|
66
|
+
Copies are treated as renames with a warning. Mode-only changes have no hunks and no warning.
|
|
67
|
+
- The hunk header's counts decide which lines belong to a hunk. Malformed input raises `ValueError`
|
|
68
|
+
with a 1-based line number. This includes extra hunk lines after an undercounting header, which is
|
|
69
|
+
stricter than `git apply`.
|
|
70
|
+
- A hunk whose `new_start` disagrees with `old_start` plus the drift so far is placed from
|
|
71
|
+
`old_start` and warns.
|
|
72
|
+
- Line maps are keyed by both old and new path. On a key collision, the map whose `new_path` is the
|
|
73
|
+
key wins.
|
|
74
|
+
- `LineMap.to_new`/`to_old` use a counting rule from `deleted_old`/`added_new`; `old_to_new` holds
|
|
75
|
+
in-hunk pairs only. A randomized check against `difflib` (1600 diffs, context 0–3) found no
|
|
76
|
+
disagreement.
|
|
77
|
+
|
|
78
|
+
## lcov ingest
|
|
79
|
+
|
|
80
|
+
- BRDA: line and block split from the left, taken from the right, so branch ids may contain commas.
|
|
81
|
+
Block stays a string.
|
|
82
|
+
- FN: `start,<digits>,name` is lcov 2.x; anything else is 1.x, so names may contain commas. FNL/FNA
|
|
83
|
+
(lcov 2.2) are supported.
|
|
84
|
+
- A FNDA with no FN of that name is dropped (debug log); a FN with no FNDA has `hits=None`.
|
|
85
|
+
- Repeats within one SF section are summed. Duplicate SF sections merge with `merge_file_coverage`
|
|
86
|
+
(`-` + n = n, `-` + `-` = `-`).
|
|
87
|
+
- Malformed known records raise `ValueError` (exit 1); unknown records are skipped with a debug log.
|
|
88
|
+
- A leading BOM is stripped. Lines split on `\n` only, not on `\f`, `\x85` and the like.
|
|
89
|
+
- `dump` is deterministic and round-trips: `parse(dump(s)) == s`.
|
|
90
|
+
- Rust function names stay mangled.
|
|
91
|
+
|
|
92
|
+
## Path normalization
|
|
93
|
+
|
|
94
|
+
- Lexical only: `\` becomes `/`, `.` and empty components are dropped, and `..` pops. The only file
|
|
95
|
+
system access is `realpath`.
|
|
96
|
+
- The longest matching root wins, on component boundaries only. Every absolute root also matches
|
|
97
|
+
its `realpath`, and an unmatched absolute path is retried via `realpath` (`/tmp` vs `/private/tmp`).
|
|
98
|
+
- An absolute path matching no root, or a relative path escaping via `..`, is external.
|
|
99
|
+
- `C:/…` is absolute, with a case-insensitive drive letter; the rest of a Windows path stays
|
|
100
|
+
case-sensitive.
|
|
101
|
+
|
|
102
|
+
## JUnit
|
|
103
|
+
|
|
104
|
+
- pytest 9 writes xfail as `<skipped type="pytest.xfail">`, so `type` decides: `pytest.xfail` gives
|
|
105
|
+
xfailed and `pytest.skip` gives skipped, whatever the message says. An untyped `<skipped>` follows
|
|
106
|
+
the spec's message-prefix rule.
|
|
107
|
+
- **(deviation)** A strict XPASS (`<failure message="[XPASS(strict)]…">`) is `xpassed`, not
|
|
108
|
+
`failed`. A non-strict xpass can't be detected and reads as passed.
|
|
109
|
+
- Severity order: passed < skipped < xfailed < xpassed < failed < error. Duplicate ids (reruns,
|
|
110
|
+
failure plus teardown error) and multiple result children resolve to the most severe.
|
|
111
|
+
- Test id is `classname::name`, or `name` when classname is empty. Testcases are found at any
|
|
112
|
+
nesting depth, and suite names are ignored.
|
|
113
|
+
- A testcase without a name is dropped with a debug log. Malformed XML or a wrong root raises
|
|
114
|
+
`ValueError`, and the CLI then drops the file with a `junit_unreadable` warning.
|
|
115
|
+
|
|
116
|
+
## Function spans
|
|
117
|
+
|
|
118
|
+
- Function status counts only `Span.body_lines`, so signature and decorator lines, which run at
|
|
119
|
+
import in Python, don't count.
|
|
120
|
+
- Python symbolizer (`ast`): names match `__qualname__`; `start` is the first decorator line. A
|
|
121
|
+
leading docstring is skipped when another statement follows. A one-liner has `body_start = None`.
|
|
122
|
+
- Every parse failure becomes `SyntaxError`, including `RecursionError` and `MemoryError` on deeply
|
|
123
|
+
nested source, so `resolve_spans` falls back to lcov FN. Warnings from parsing the target's
|
|
124
|
+
source are silenced. A leading BOM is stripped.
|
|
125
|
+
- `LcovFnSymbolizer` sets `body_start = start + 1` for multi-line spans: tarpaulin and coverage.py
|
|
126
|
+
report the FN line as hit even when the body never runs. A missing end runs to the next distinct
|
|
127
|
+
FN start − 1, and the last function runs to the last executable line.
|
|
128
|
+
|
|
129
|
+
## Diff
|
|
130
|
+
|
|
131
|
+
- File pairing: patched paths first, via `linemap_for(P).old_path`; then unclaimed base paths. A
|
|
132
|
+
base file colliding with an already-paired path is dropped with an `unpaired_file` warning.
|
|
133
|
+
- A side is *measured*, *inferred* (missing from coverage but present under a known root),
|
|
134
|
+
*nonexistent* (per the line map or the root), or *unmeasured* (root unknown, line map silent).
|
|
135
|
+
- An inferred side takes the other side's executable lines and branches, mapped through the line
|
|
136
|
+
map, with count 0, and warns `inferred_file`. When the measured side has nothing executable,
|
|
137
|
+
nothing is inferred and there is no warning.
|
|
138
|
+
- An unchanged line executable on one side only is `unmeasured`, with one warning per file (none
|
|
139
|
+
when the count is 0).
|
|
140
|
+
- A changed line counts as deleted plus added, so an edited `if` line gives branch_deleted plus
|
|
141
|
+
branch_new.
|
|
142
|
+
- Branches match on (mapped line, block, mapped id), and every `line N` in a base branch id is
|
|
143
|
+
mapped through the line map. coverage.py ids name the target line (`jump to line 7`), so a shift
|
|
144
|
+
would otherwise produce false lost/gained. A target that doesn't map leaves the branch unmatched.
|
|
145
|
+
- Branch categories: `branch_kept_hit`, `branch_kept_miss`, `branch_lost`, `branch_gained`,
|
|
146
|
+
`branch_new`, `branch_deleted`, `branch_flaky`, `branch_unmeasured`. Entries carry `base_line`
|
|
147
|
+
(an addition to spec §7).
|
|
148
|
+
- Flaky (hit in some repeats but not all) lines are removed from `lines` entirely, including the
|
|
149
|
+
new/deleted counts, and listed in `files[].flaky`. Stats come from run 0.
|
|
150
|
+
- `source_changed`: sha256 differs when both shas are known; otherwise true if the file is absent
|
|
151
|
+
on one side or its line map has changes. A pure rename is not a source change.
|
|
152
|
+
- Tests: when both outcome maps are None the list is `[]`. When only one exists it's `[]` plus a
|
|
153
|
+
`tests_one_side` warning. Otherwise the result is the union of ids, with `not_run` for a missing
|
|
154
|
+
side.
|
|
155
|
+
- Warnings are `{code, message}`, deduplicated, sorted, and use repo-relative paths only.
|
|
156
|
+
- Sort order: lines by line then base_line, with deleted lines (`line` null) last. Branches follow,
|
|
157
|
+
then by block and a natural-sorted branch id.
|
|
158
|
+
|
|
159
|
+
## Functions and tree
|
|
160
|
+
|
|
161
|
+
- Functions match by qualified name (first span per name), then by largest line overlap after
|
|
162
|
+
mapping. That overlap step pairs coverage.py FN names (`outer.inner`) with symbolizer names
|
|
163
|
+
(`outer.<locals>.inner`).
|
|
164
|
+
- `went_dark`/`lit_up` take precedence. Otherwise `mixed`, `partial_loss` or `partial_gain` follow
|
|
165
|
+
from the body's lost/gained counts, else `unchanged`. Flaky lines are ignored.
|
|
166
|
+
- A side with unknown spans borrows the other side's spans through the line map.
|
|
167
|
+
- `tree` covers every non-external file on either side, not just changed ones. Dir stats are sums.
|
|
168
|
+
A file and a dir may share a name (file `a` deleted, `a/b` added).
|
|
169
|
+
|
|
170
|
+
## Reports
|
|
171
|
+
|
|
172
|
+
- Default output lists only files with a non-kept change, drops `kept_*` lines, kept branches and
|
|
173
|
+
`unchanged` functions, and lists only changed test outcomes. `--full` keeps everything; `summary`
|
|
174
|
+
and `tree` are never filtered.
|
|
175
|
+
- `--counts` fills `count_changed` and also lists files whose only change is hit counts.
|
|
176
|
+
- `summary.functions` has 7 statuses (no `unchanged`). Coverage % is from the root tree stats,
|
|
177
|
+
rounded to 1 decimal, and null when nothing is executable.
|
|
178
|
+
- `dumps` is `json.dumps(sort_keys=True, indent=2)` plus a trailing newline. The schema is Draft
|
|
179
|
+
2020-12 with `additionalProperties: false`, and meta fields are conditional on mode.
|
|
180
|
+
- The text report renders from the JSON dict, not the Delta. Hints are printed only when there are
|
|
181
|
+
warnings or a listed file has `inferred_side`.
|
|
182
|
+
|
|
183
|
+
## Tests and fixtures
|
|
184
|
+
|
|
185
|
+
- `shed_py` is the spec's §12.1 code verbatim, including its header comment, so `divide` is at
|
|
186
|
+
lines 5–8.
|
|
187
|
+
- `shed_rs` returns `Result` from `divide`. No `Cargo.lock` is committed.
|
|
188
|
+
- Rust E2E covers both `cargo-llvm-cov` and `cargo-tarpaulin`; each is skipped if not installed.
|
|
189
|
+
CI installs both. Tarpaulin skips when it can't trace (no ptrace). The stderr pattern is a guess,
|
|
190
|
+
since no machine without ptrace was available.
|
|
191
|
+
- E2E runs sundgren in a clean env (no `GIT_*`, `PYTEST_*`, `COVERAGE_*`, `PYTHONPATH`, user git
|
|
192
|
+
config) with a private TMPDIR. After every run it asserts that no worktree or temp dir is left.
|
|
193
|
+
One scenario uses a deliberately hostile git config.
|
|
194
|
+
- Saved JUnit fixtures have neutral hostname, timestamps and paths.
|
|
195
|
+
|
|
196
|
+
## Known limitations (v1)
|
|
197
|
+
|
|
198
|
+
- Functions with the same name in one lcov file collapse into one record, as lcov keys by name.
|
|
199
|
+
- Rust closures have FN records with no end, so the enclosing function is cut at the closure's
|
|
200
|
+
start. Fixing this needs a Rust symbolizer.
|
|
201
|
+
- A one-line function can't be `went_dark` from line data, because its def line runs at import.
|
|
202
|
+
- A deleted file plus a rename onto the same path (or a file turning into a symlink) can't both be
|
|
203
|
+
represented in the line maps.
|
|
204
|
+
- A 1.x FN name starting with `<digits>,` is read as 2.x.
|
|
205
|
+
- The Python symbolizer parses with the running interpreter's grammar, so newer syntax falls back
|
|
206
|
+
to lcov FN.
|
|
207
|
+
- lcov 2.2 FNL/FNA support was written from the documented format, without real output to check
|
|
208
|
+
against. The llvm-cov unit fixture and nextest JUnit are hand-written; real llvm-cov output is
|
|
209
|
+
only exercised by the CI e2e run, which asserts loosely (no exact function names).
|
|
210
|
+
|
|
211
|
+
## Spec examples vs. real output
|
|
212
|
+
|
|
213
|
+
- skip_divide loses 3 lines, not the 6 in the §7 example, because the def line runs at import. The
|
|
214
|
+
lost branches have taken `[1, null]` (coverage.py writes `-`), not `[1, 0]`.
|
|
215
|
+
- **(deviation)** touch_source: the new function's def line is `new_hit`, because it runs at import;
|
|
216
|
+
only its body is `new_miss`.
|
|
217
|
+
- With tarpaulin, `divide`'s signature line stays hit when its test is ignored, which is why
|
|
218
|
+
`LcovFnSymbolizer` skips the FN line.
|
sundgren-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
sundgren-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sundgren
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI tool for measuring how code patches affect line coverage.
|
|
5
|
+
Project-URL: Homepage, https://github.com/axean/sundgren
|
|
6
|
+
Project-URL: Source, https://github.com/axean/sundgren
|
|
7
|
+
Project-URL: Issues, https://github.com/axean/sundgren/issues
|
|
8
|
+
Author: Axel E. Andersson
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: coverage,diff,lcov,patch,pytest,rust,testing
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: POSIX
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
+
Classifier: Topic :: Software Development :: Testing
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# sundgren
|
|
27
|
+
|
|
28
|
+
*Sundgren* is Swedish for "sound branch". Given a repo, a base ref and a patch (usually one that
|
|
29
|
+
changes tests), it answers: **what happened to source coverage because of this patch?**
|
|
30
|
+
|
|
31
|
+
It runs a coverage command on the base tree and on the patched tree (git worktrees), diffs the two
|
|
32
|
+
lcov files with line numbers mapped through the patch, and writes a JSON report
|
|
33
|
+
(`sundgren.delta/1`) or a text summary.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
uv tool install sundgren # or: pipx install sundgren
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Sundgren never imports coverage or pytest itself. They run as commands in your project's
|
|
42
|
+
environment: `{python}` is the `python` on your `PATH` (or `python3`), override with `--python`.
|
|
43
|
+
|
|
44
|
+
## Python
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
sundgren run --patch skip_divide.patch # preset "pytest" is picked from pyproject.toml
|
|
48
|
+
sundgren run --patch p.patch --base main --format text
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The `pytest` preset runs `coverage run --branch -m pytest <selected tests>` then `coverage lcov`.
|
|
52
|
+
It only runs the test files the patch touches; if the patch touches `conftest.py`, any other `.py`
|
|
53
|
+
file or pytest config, it runs everything. Override with `--tests tests/test_x.py ...`.
|
|
54
|
+
|
|
55
|
+
- Set `[run] source = <your package>` in `.coveragerc`, so never-imported files are reported.
|
|
56
|
+
- Editable installs measure your main checkout, not the worktree. Sundgren detects this and exits 1;
|
|
57
|
+
fix with `--env PYTHONPATH={worktree}/src`.
|
|
58
|
+
|
|
59
|
+
## Rust
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
sundgren run --patch ignore_divide.patch # cargo-llvm-cov
|
|
63
|
+
sundgren run --patch ignore_divide.patch --preset cargo-tarpaulin
|
|
64
|
+
sundgren run --patch p.patch --tests divide # name filters, passed after --
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Each run uses its own `CARGO_TARGET_DIR`, so both sides build from scratch. Function names are
|
|
68
|
+
stored mangled.
|
|
69
|
+
|
|
70
|
+
## Any language: `--cmd`
|
|
71
|
+
|
|
72
|
+
Anything that writes lcov works:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
sundgren run --patch p.patch \
|
|
76
|
+
--cmd "npx jest --coverage --coverageReporters=lcov --coverageDirectory={out}" \
|
|
77
|
+
--lcov "{out}/lcov.info"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Placeholders: `{python}`, `{out}` (per-run output dir), `{worktree}`, `{lcov}`, `{junit}`,
|
|
81
|
+
`{tests}`. A token that is exactly `{tests}` expands to one argv entry per selected test (none if
|
|
82
|
+
nothing is selected). `--shell` runs commands through `/bin/sh -c`. `--junit TEMPLATE` adds test
|
|
83
|
+
outcomes to the report. `--env K=V` sets (templated) environment variables.
|
|
84
|
+
|
|
85
|
+
Custom presets go in `sundgren.toml` (`[presets.<name>]`) or `[tool.sundgren.presets.<name>]` in
|
|
86
|
+
`pyproject.toml`, with the same keys as `src/sundgren/lang/python/presets.toml`. `sundgren presets`
|
|
87
|
+
lists them.
|
|
88
|
+
|
|
89
|
+
## Other commands
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
sundgren diff base.lcov patched.lcov --patch p.patch --base-root A --patched-root B
|
|
93
|
+
sundgren ingest coverage.lcov --root . # normalized snapshot as JSON
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Options shared by `run` and `diff`: `--format json|text`, `-o OUT`, `--full` (include unchanged
|
|
97
|
+
lines, functions and tests), `--counts` (hit-count changes on lines that stayed hit), `--repeat N`
|
|
98
|
+
(`run` only; lines whose status varies across runs are reported as `flaky`).
|
|
99
|
+
|
|
100
|
+
Exit codes: 0 ok, 1 tool or run failure, 2 usage error, 3 patch doesn't apply.
|
|
101
|
+
|
|
102
|
+
## Report
|
|
103
|
+
|
|
104
|
+
`files[].lines[].category` is one of `lost`, `gained`, `new_hit`, `new_miss`, `deleted_hit`,
|
|
105
|
+
`deleted_miss`, `kept_hit`, `kept_miss`, `unmeasured` (genhtml: LBC, GBC, GNC, UNC, DCB, DUB, CBC,
|
|
106
|
+
UBC). Functions are `went_dark`, `lit_up`, `partial_loss`, `partial_gain`, `mixed`, `unchanged`,
|
|
107
|
+
`added` or `removed`. `tree` rolls the line stats up per directory. Schema:
|
|
108
|
+
`src/sundgren/core/report/delta.schema.json`.
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
uv sync # dev env in .venv
|
|
114
|
+
uv run pytest
|
|
115
|
+
uv run ruff check . && uv run ruff format --check .
|
|
116
|
+
uv run mypy
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Layers: `core` (no languages, no subprocesses) ← `run` (worktrees, command templates) ← `lang`
|
|
120
|
+
(presets, selectors, symbolizers). Only `cli.py` imports `lang`. `tests/unit/test_layering.py`
|
|
121
|
+
enforces this. See `DECISIONS.md` for choices the spec left open.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
Apache-2.0. See `LICENSE`.
|