Skip to content

coverage: Provide baseline support for Linux and QNX#698

Open
castler wants to merge 3 commits into
mainfrom
js_linux_baseline_coverage
Open

coverage: Provide baseline support for Linux and QNX#698
castler wants to merge 3 commits into
mainfrom
js_linux_baseline_coverage

Conversation

@castler

@castler castler commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

With this commit we provide baseline support for Linux and QNX coverage.
We can remove manual filtering based on patterns and ensure that only the files (but also all files) from the respective public libraries are covered.

Comment thread quality/coverage/llvm_cov/reporter.py Fixed
@castler castler force-pushed the js_linux_baseline_coverage branch 2 times, most recently from e70b0fe to 0c953aa Compare July 14, 2026 09:09
@castler castler marked this pull request as ready for review July 14, 2026 09:46
Comment thread quality/coverage/coverage_scope.bzl
Comment on lines +123 to +124
bazel build --config=qnx //quality/coverage:coverage_scope --output_groups=allowlist
ALLOWLIST_FILE="$(bazel info bazel-bin)/quality/coverage/coverage_scope_allowlist.txt"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not very portable. Better would be if this target is provided as input artifact via bazel.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is actually as portable as possible - only better approach is to not do it in this preprocessing step. I have local changes for this refactoring, but this is right now not performant enough.

Comment on lines +130 to +154
BASELINE_LCOV="${TMPDIR_EXTRACT}/baseline_lcov.dat"
BASELINE_COUNT=0
LCOV_INPUT_LIST="${BUILD_WORKSPACE_DIRECTORY}/bazel-out/_coverage/lcov_files.tmp"
if [[ -f "${LCOV_INPUT_LIST}" ]]; then
while IFS= read -r lcov_input; do
if [[ "${lcov_input}" == *"/baseline_coverage.dat" ]]; then
if [[ -f "${lcov_input}" ]]; then
printf "\n" >> "${BASELINE_LCOV}"
cat "${lcov_input}" >> "${BASELINE_LCOV}"
printf "\n" >> "${BASELINE_LCOV}"
BASELINE_COUNT=$((BASELINE_COUNT + 1))
fi
continue
fi
if [[ "${lcov_input}" == *"/coverage.dat" ]]; then
baseline_input="${lcov_input%coverage.dat}baseline_coverage.dat"
if [[ -f "${baseline_input}" ]]; then
printf "\n" >> "${BASELINE_LCOV}"
cat "${baseline_input}" >> "${BASELINE_LCOV}"
printf "\n" >> "${BASELINE_LCOV}"
BASELINE_COUNT=$((BASELINE_COUNT + 1))
fi
fi
done < <(sort -u "${LCOV_INPUT_LIST}")
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section would benefit from a comment that explains what its goal is.

Comment thread quality/coverage/lcov_to_html.py Outdated
Comment on lines +145 to +146
if filename.startswith("/proc/self/cwd/"):
return filename[len("/proc/self/cwd/"):]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better not duplicate this string.

Comment on lines +171 to +172
if not allowlist:
return list(file_entries)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not fulfill the contract of the function. Either extend the description of the function or change this to return an empty list.

Comment thread quality/coverage/lcov_to_html.py Outdated
Comment on lines +214 to +216
line_number = incoming_line["line_number"]
incoming_copy = dict(incoming_line)
incoming_copy["branches"] = [dict(branch) for branch in incoming_line.get("branches", [])]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code smell: Based on access-pattern in line 214 incoming_line is a dict. Based on this, the assignment in line 215 results in a shallow copy. This means, in line 216 you do not only modify incoming_copy but also incoming_line.

Possible options:

  1. If the modification of incoming_line is non-destructive, just drop the shallow copy
  2. If the modification of incoming_line is destructive use copy.deepcopy().

Comment thread quality/coverage/lcov_to_html.py Outdated
Comment on lines +210 to +249
def _merge_single_file_entry(target: Dict[str, Any], incoming: Dict[str, Any]) -> None:
"""Merge one file entry into another in-place."""
target_lines = {line["line_number"]: dict(line) for line in target["lines"]}
for incoming_line in incoming.get("lines", []):
line_number = incoming_line["line_number"]
incoming_copy = dict(incoming_line)
incoming_copy["branches"] = [dict(branch) for branch in incoming_line.get("branches", [])]

if line_number not in target_lines:
target_lines[line_number] = incoming_copy
continue

existing = target_lines[line_number]
existing["count"] = max(existing.get("count", 0), incoming_copy.get("count", 0))
existing["branches"] = _merge_branch_entries(
existing.get("branches", []),
incoming_copy.get("branches", []),
)

target["lines"] = sorted(target_lines.values(), key=lambda line: line["line_number"])

target_functions = {func["name"]: dict(func) for func in target["functions"]}
for incoming_func in incoming.get("functions", []):
name = incoming_func["name"]
incoming_lineno = incoming_func.get("lineno", 0)
incoming_execution_count = incoming_func.get("execution_count", 0)

if name not in target_functions:
target_functions[name] = dict(incoming_func)
continue

existing_func = target_functions[name]
if existing_func.get("lineno", 0) == 0 and incoming_lineno != 0:
existing_func["lineno"] = incoming_lineno
existing_func["execution_count"] = max(
existing_func.get("execution_count", 0),
incoming_execution_count,
)

target["functions"] = sorted(target_functions.values(), key=lambda func: func["name"])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function would benefit from splitting into dedicated subfunctions for line and function coverage as well.



def _merge_single_file_entry(target: Dict[str, Any], incoming: Dict[str, Any]) -> None:
"""Merge one file entry into another in-place."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would help to describe the merge strategy here, since it is not straightforward.
Instead of actually merging the counts, you perform a selection of the maximum value between the two files.
This results in completely different counts than a user would expect.

Should this be a concious choice, I would prefer an explaination here.
If this was not concious, I would prefer to actually add the counts (considering overflows) so that the numbers are as a user would expect them.

Comment thread MODULE.bazel
@castler castler force-pushed the js_linux_baseline_coverage branch from 0c953aa to a3ec8d6 Compare July 15, 2026 06:25
Replace static filename-regex filtering with a Bazel-derived coverage scope:
- add `coverage_scope` rule/aspect to collect dependable-element source files
- generate allowlist + baseline object manifest for the reporter
- add `reporter_wrapper` rule to wire scope artifacts into reporter invocation
- update llvm-cov reporter to consume allowlist/baseline objects and support baseline-only coverage via `--empty-profile`
- remove obsolete `filter_regexes.txt` and update coverage docs/config comments
@castler castler force-pushed the js_linux_baseline_coverage branch from a3ec8d6 to 26aaf8b Compare July 15, 2026 07:57
castler and others added 2 commits July 15, 2026 10:53
Switch the QNX gcovr report path away from regex-based file filtering
to using the Bazel-collected coverage allowlist from coverage_scope.

Changes:
- generate_coverage_html.sh: Build //quality/coverage:coverage_scope
  in default config, resolve its allowlist.txt and pass it to
  lcov_to_html.py via the new --allowlist flag. Remove the dependency
  on quality/coverage/llvm_cov/filter_regexes.txt for QNX.
- lcov_to_html.py: Add --allowlist argument; load the allowlist,
  normalize LCOV SF: paths to workspace-relative form, and filter
  entries against the allowlist before handing off to gcovr.
  Fail fast if an explicit allowlist resolves to an empty set.
- lcov_to_html_test.py: Add regression tests for allowlist loading,
  path normalization/filtering, and the empty-allowlist guard.
- BUILD: Add py_test for lcov_to_html_test.
- README.md: Document that QNX scope is now driven by the collected
  allowlist, not regex filters.

The coverage_scope target is built without --config=qnx because the
dependable element indices it depends on are Linux-only; the resulting
workspace-relative allowlist is equally valid for QNX report scoping.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants