mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 23:47:02 +01:00
continuation_lines() excluded the continuation lines of multi-line QUOTED
arguments but nothing else, so a line holding only a group's closing brace
and an operator -- `} |`, `} >>"$REPORT_FILE"` -- stayed in the denominator
while bash attributed no statement to it. That capped four fixes/lib libs
below 100% with no test able to move them.
Why the anchor makes this safe: bash attributes every command INSIDE a group
to its own line, and a pipe's right-hand side to its own line, leaving the
brace-plus-operator line with nothing. Anchoring the pattern on a LEADING
`}` therefore excludes only lines that can hold no statement. A one-liner
`{ echo a; } >file` begins with `{` and is untouched; a bare `}` closing a
function has no operator and is untouched.
Measured effect, all through fixes/lib/tests/run_all.sh:
bt_audio.sh 96.97% -> 100.00% (127/127)
arch_perf_report.sh 98.33% -> 100.00% (59/59)
bt_report.sh 100.00% -> 100.00% (37/37)
bt_pairing.sh 100.00% -> 100.00% (65/65)
bt_adapter.sh 98.55% -> 98.55% (68/69, capped by a different flavour)
Two gained, three held, none went DOWN -- the handoff's regression signal.
That is not luck: _measure computes covered_set = (executed|traced) &
denominator, so for a lib already at 100% the covered set IS the denominator
and removing a subset cannot lower it. The five gated pacman_hook_stall libs
are safe by the same arithmetic.
Acceptance test from the handoff passes unchanged: dot_resolver_install.sh
39/39 and rpi_nc_ca.sh 73/73, both 100.00%.
The fail-open check that actually matters, since over-detection INFLATES
coverage and could promote an untested lib: the new predicate was run over
every lib in the allowlist plus the cleared ones, printing each newly
excluded line WITH its text. All 17 across 10 libs are genuine
brace-plus-operator lines; no ordinary statement was caught.
Tests cover both directions, including the two negative cases (a bare `}`
and a single-line `{ ...; } >file`), keeping the module at 100% branch
coverage. The quote scanner moved into _quote_after() to stay under the
C901 complexity limit rather than suppressing it.
Deliberately NOT fixed here: the backslash-continued ASSIGNMENT flavour that
caps bt_adapter.sh line 97. bash attributes those to the LAST line of the run
but a backslash-continued plain COMMAND to its FIRST, so that predicate needs
a command-vs-assignment distinction. One line in one lib; not worth the
landmine today.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
211 lines
7.6 KiB
Python
211 lines
7.6 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_closing_brace_with_pipe_is_a_continuation(
|
|
mod: ModuleType, tmp_path: Path
|
|
) -> None:
|
|
"""`} |` carries no statement: bash attributes each part to its own line."""
|
|
src = _write(
|
|
tmp_path,
|
|
"a.sh",
|
|
"{\n\techo a\n\techo b\n} |\n\tconsumer\n",
|
|
)
|
|
assert mod.continuation_lines(src) == {4}
|
|
|
|
|
|
def test_closing_brace_with_redirect_is_a_continuation(
|
|
mod: ModuleType, tmp_path: Path
|
|
) -> None:
|
|
"""The arch_perf_report.sh shape: a group appended to a report file."""
|
|
src = _write(
|
|
tmp_path,
|
|
"a.sh",
|
|
'{\n\techo a\n} >>"$REPORT_FILE"\necho after\n',
|
|
)
|
|
assert mod.continuation_lines(src) == {3}
|
|
|
|
|
|
def test_bare_closing_brace_is_a_statement(mod: ModuleType, tmp_path: Path) -> None:
|
|
"""A `}` closing a function has no operator and must stay in the count."""
|
|
src = _write(tmp_path, "a.sh", "f() {\n\techo a\n}\nf\n")
|
|
assert mod.continuation_lines(src) == set()
|
|
|
|
|
|
def test_single_line_group_with_redirect_is_a_statement(
|
|
mod: ModuleType, tmp_path: Path
|
|
) -> None:
|
|
"""`{ echo a; } >file` begins with `{`, so the whole line is a statement."""
|
|
src = _write(tmp_path, "a.sh", '{ echo a; } >"$out"\necho after\n')
|
|
assert mod.continuation_lines(src) == set()
|
|
|
|
|
|
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")]
|