mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 21:27:02 +01:00
Fourth flavour of defect (a) found this session, after multi-line quotes,
`} |` and the backslash-continued assignment. bash reports a multi-line array
assignment at its OPENING line and never emits the element lines or the
closing paren, so kcov instrumenting them inflates the denominator:
+L3 items=('one' 'two') # opened line 3, closed line 6
# lines 4, 5, 6: never emitted
The rule is therefore symmetric with the existing multi-line-quote case: the
opening line is the statement, everything up to the closing paren is data.
_closes_array() finds that paren by recomputing quote state per candidate, so
an element like "a(b)" does not end the literal early.
Measured effect through fixes/lib/tests/run_all.sh:
arch_perf_fixes.sh 93.48% -> 100.00% (43/43)
bt_audio.sh 100.00% -> 100.00% (127/127)
arch_perf_report.sh 100.00% -> 100.00% (59/59)
arch_perf_probes.sh 100.00% -> 100.00% (93/93)
ubuntu_perf_more.sh 100.00% -> 100.00% (32/32)
bt_report.sh 100.00% -> 100.00% (37/37)
bt_pairing.sh 100.00% -> 100.00% (65/65)
One gained, six held, none went DOWN. Acceptance test unchanged:
dot_resolver_install.sh 39/39 and rpi_nc_ca.sh 73/73. arch_perf_fixes.sh
therefore comes off the allowlist in the same commit, confirmed by the real
gate with no exemption.
The fail-open check, since over-detection INFLATES coverage: the old and new
predicates were diffed over every allowlisted lib plus the six cleared ones,
printing each newly excluded line WITH its text. All 91 across 10 libs are
array elements or closing parens -- common.sh's tool tables, packages.sh's
associative arrays, arch_sysctl.sh's sysctl maps, thorium_repairs.sh's four
path lists, ubuntu_perf_fixes.sh's sssd units, and so on. No statement caught.
Tests cover both directions, including three negatives: a single-line
`x=(a b)`, an element containing a parenthesis, and `x=$(echo hi)` which is a
command substitution rather than an array. Module stays at 100% branch
coverage.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
243 lines
8.8 KiB
Python
243 lines
8.8 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_multiline_array_marks_elements_and_paren(
|
|
mod: ModuleType, tmp_path: Path
|
|
) -> None:
|
|
"""bash reports the whole assignment at the opening line; elements are data."""
|
|
src = _write(
|
|
tmp_path,
|
|
"a.sh",
|
|
'local -a c=(\n\t"one"\n\t"two"\n)\nfor i in "${c[@]}"; do :; done\n',
|
|
)
|
|
assert mod.continuation_lines(src) == {2, 3, 4}
|
|
|
|
|
|
def test_single_line_array_is_a_statement(mod: ModuleType, tmp_path: Path) -> None:
|
|
"""`x=(a b)` closes on its own line and stays a statement."""
|
|
src = _write(tmp_path, "a.sh", "x=(a b)\necho after\n")
|
|
assert mod.continuation_lines(src) == set()
|
|
|
|
|
|
def test_array_element_containing_a_paren_does_not_close_it(
|
|
mod: ModuleType, tmp_path: Path
|
|
) -> None:
|
|
"""A `)` inside quotes must not end the literal early."""
|
|
src = _write(tmp_path, "a.sh", 'x=(\n\t"a(b)"\n\t"c"\n)\necho after\n')
|
|
assert mod.continuation_lines(src) == {2, 3, 4}
|
|
|
|
|
|
def test_command_substitution_is_not_an_array(mod: ModuleType, tmp_path: Path) -> None:
|
|
"""`x=$(cmd` is a substitution, not an array literal, and must not match."""
|
|
src = _write(tmp_path, "a.sh", "x=$(echo hi)\necho after\n")
|
|
assert mod.continuation_lines(src) == set()
|
|
|
|
|
|
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")]
|