mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 00:47:01 +01:00
The instrument, not the tests, was the bottleneck. Coverage is now
(PS4-traced lines | kcov hits) & (kcov line set - continuation lines).
The trace deliberately does NOT supply the denominator. A trace reports only
lines that RAN, so using it for both halves would make every subject 100.00%
and arm a repo-wide gate that checks nothing.
Three measured findings, each of which yields a silently wrong trace:
1. kcov's ptrace and bash's xtrace cannot share a process. Under
SHELLOPTS=xtrace every line kcov recorded as hits="1" comes back hits="0"
-- reproduced A/B on one subject. Hence two passes, two namespaces.
2. SHELLOPTS is readonly inside bash, so `export SHELLOPTS=xtrace` fails and
tracing never turns on; and `set -x` does not reach child processes.
3. Decisive: bash under `unshare --map-root-user` runs PRIVILEGED and
DISCARDS an inherited PS4, falling back to "+ " while still honouring
xtrace. The trace loses its file:line prefix and every subject reads 0%.
Fixed by delivering PS4 and `set -x` through a BASH_ENV file, which also
solves child-process propagation.
A fourth trap, in the report: kcov records filename="dwm_config.sh", a bare
basename. The source path comes from the trace's ${BASH_SOURCE} or a unique
repo search; an ambiguous basename yields no exclusion rather than a guess.
Measured through run_all.sh, nothing down:
dot_resolver_install.sh 39/39 = 100.00% (acceptance, unchanged)
rpi_nc_ca.sh 73/73 = 100.00% (was 72/73 = 98.63%)
dwm_config.sh 65/66 = 98.48% (was 35/73 = 47.95%)
rpi_nc_install.sh 85/88 = 96.59% (was 10/88 = 11.36%)
rpi_nc_ca reaching 100% is the load-bearing check: its missing line 141 was
cross-process contagion, and the trace pass has no kcov in it to contaminate.
dwm_config's remaining line 39 is a `done < <(...)`. Left in the denominator
on purpose -- removing lines inflates coverage, the fail-open direction, so a
second exclusion rule needs per-line justification, not a pattern from one lib.
Both new Python modules hold 100% branch coverage (29 tests).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
173 lines
6.3 KiB
Python
173 lines
6.3 KiB
Python
"""Tests for meta/scripts/lib/shell_coverage_lines.py.
|
|
|
|
This module decides what counts as a statement and what counts as executed,
|
|
so both of its failure modes are silent and one-directional:
|
|
|
|
* over-detecting continuation lines shrinks the denominator and INFLATES
|
|
coverage, which is the fail-open direction -- it could promote an untested
|
|
lib off the allowlist;
|
|
* losing trace lines shrinks the numerator, which merely keeps a lib on the
|
|
allowlist that deserves to come off.
|
|
|
|
The first is the one worth pinning down hardest, so the quote-state cases
|
|
below assert exact line sets rather than counts.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pytest
|
|
|
|
if TYPE_CHECKING:
|
|
from types import ModuleType
|
|
|
|
_ROOT = Path(__file__).resolve().parents[2]
|
|
_SCRIPT = _ROOT / "meta" / "scripts" / "lib" / "shell_coverage_lines.py"
|
|
|
|
|
|
def _load() -> ModuleType:
|
|
"""Import the module by path; it lives outside any importable package."""
|
|
spec = importlib.util.spec_from_file_location("shell_coverage_lines", _SCRIPT)
|
|
assert spec is not None
|
|
assert spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
@pytest.fixture(name="mod")
|
|
def _mod() -> ModuleType:
|
|
return _load()
|
|
|
|
|
|
def _write(tmp_path: Path, name: str, text: str) -> Path:
|
|
path = tmp_path / name
|
|
path.write_text(text, encoding="utf-8")
|
|
return path
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# continuation_lines
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def test_single_line_quotes_are_not_continuations(
|
|
mod: ModuleType, tmp_path: Path
|
|
) -> None:
|
|
"""A quote opened and closed on one line leaves no continuation."""
|
|
src = _write(tmp_path, "a.sh", "echo 'hello'\necho \"world\"\n")
|
|
assert mod.continuation_lines(src) == set()
|
|
|
|
|
|
def test_multiline_single_quote_marks_inner_lines(
|
|
mod: ModuleType, tmp_path: Path
|
|
) -> None:
|
|
"""This is the real dwm_config.sh perl-block shape: only the inner lines."""
|
|
src = _write(tmp_path, "a.sh", "perl -pe '\ns/a/b/;\ns/c/d/;\n' file\necho after\n")
|
|
# Line 1 opens the quote and IS a statement; 2-4 are inside it; 5 is after.
|
|
assert mod.continuation_lines(src) == {2, 3, 4}
|
|
|
|
|
|
def test_multiline_double_quote_marks_inner_lines(
|
|
mod: ModuleType, tmp_path: Path
|
|
) -> None:
|
|
"""Double quotes span lines the same way single quotes do."""
|
|
src = _write(tmp_path, "a.sh", 'x="\nline two\n"\necho after\n')
|
|
assert mod.continuation_lines(src) == {2, 3}
|
|
|
|
|
|
def test_comment_outside_quotes_ends_the_line(mod: ModuleType, tmp_path: Path) -> None:
|
|
"""An apostrophe inside a comment must not open a quote state."""
|
|
src = _write(tmp_path, "a.sh", "# don't be fooled\necho after\n")
|
|
assert mod.continuation_lines(src) == set()
|
|
|
|
|
|
def test_backslash_escape_outside_quotes_is_skipped(
|
|
mod: ModuleType, tmp_path: Path
|
|
) -> None:
|
|
"""An escaped quote outside quotes does not open a quote state."""
|
|
src = _write(tmp_path, "a.sh", "echo \\'\necho after\n")
|
|
assert mod.continuation_lines(src) == set()
|
|
|
|
|
|
def test_backslash_escape_inside_double_quotes(mod: ModuleType, tmp_path: Path) -> None:
|
|
"""An escaped double quote inside double quotes does not close them."""
|
|
src = _write(tmp_path, "a.sh", 'x="a\\"b\nstill inside\n"\n')
|
|
assert mod.continuation_lines(src) == {2, 3}
|
|
|
|
|
|
def test_backslash_inside_single_quotes_is_literal(
|
|
mod: ModuleType, tmp_path: Path
|
|
) -> None:
|
|
"""Single quotes honour no escapes, so a trailing backslash closes nothing."""
|
|
src = _write(tmp_path, "a.sh", "x='a\\'\necho after\n")
|
|
assert mod.continuation_lines(src) == set()
|
|
|
|
|
|
def test_hash_inside_quotes_is_not_a_comment(mod: ModuleType, tmp_path: Path) -> None:
|
|
"""A # inside an open quote must not be treated as a comment."""
|
|
src = _write(tmp_path, "a.sh", "x='# not a comment\nstill inside\n'\n")
|
|
assert mod.continuation_lines(src) == {2, 3}
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# traced_lines / traced_paths
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
def test_traced_lines_missing_dir_is_empty(mod: ModuleType, tmp_path: Path) -> None:
|
|
"""A missing trace dir yields nothing rather than raising."""
|
|
assert mod.traced_lines(tmp_path / "absent", "x.sh") == set()
|
|
assert mod.traced_paths(tmp_path / "absent", "x.sh") == []
|
|
|
|
|
|
def test_traced_lines_filters_by_basename(mod: ModuleType, tmp_path: Path) -> None:
|
|
"""Only the subject's own lines are collected."""
|
|
trace = tmp_path / "trace"
|
|
trace.mkdir()
|
|
(trace / "case.1").write_text(
|
|
"+PS4:/a/b/subject.sh:12 cmd\n"
|
|
"++PS4:/a/b/subject.sh:15 cmd\n"
|
|
"+PS4:/a/b/other.sh:99 cmd\n",
|
|
encoding="utf-8",
|
|
)
|
|
assert mod.traced_lines(trace, "subject.sh") == {12, 15}
|
|
|
|
|
|
def test_traced_lines_skips_subdirectories(mod: ModuleType, tmp_path: Path) -> None:
|
|
"""A directory inside the trace dir is not read as a trace file."""
|
|
trace = tmp_path / "trace"
|
|
(trace / "nested").mkdir(parents=True)
|
|
(trace / "case.1").write_text("+PS4:/a/subject.sh:3 cmd\n", encoding="utf-8")
|
|
assert mod.traced_lines(trace, "subject.sh") == {3}
|
|
|
|
|
|
def test_traced_lines_survives_invalid_utf8(mod: ModuleType, tmp_path: Path) -> None:
|
|
"""A split multi-byte sequence must not discard the whole trace."""
|
|
trace = tmp_path / "trace"
|
|
trace.mkdir()
|
|
(trace / "case.1").write_bytes(b"+PS4:/a/subject.sh:7 cmd\n\xff\xfe\n")
|
|
assert mod.traced_lines(trace, "subject.sh") == {7}
|
|
|
|
|
|
def test_traced_paths_skips_subdirectories(mod: ModuleType, tmp_path: Path) -> None:
|
|
"""A directory inside the trace dir is not read when resolving paths."""
|
|
trace = tmp_path / "trace"
|
|
(trace / "nested").mkdir(parents=True)
|
|
(trace / "case.1").write_text("+PS4:/a/subject.sh:3 cmd\n", encoding="utf-8")
|
|
assert mod.traced_paths(trace, "subject.sh") == [Path("/a/subject.sh")]
|
|
|
|
|
|
def test_traced_paths_deduplicates(mod: ModuleType, tmp_path: Path) -> None:
|
|
"""The same source path reported many times yields one entry."""
|
|
trace = tmp_path / "trace"
|
|
trace.mkdir()
|
|
(trace / "case.1").write_text(
|
|
"+PS4:/a/subject.sh:1 c\n+PS4:/a/subject.sh:2 c\n+PS4:/b/other.sh:2 c\n",
|
|
encoding="utf-8",
|
|
)
|
|
assert mod.traced_paths(trace, "subject.sh") == [Path("/a/subject.sh")]
|