coverage: Provide baseline support for Linux and QNX#698
Conversation
e70b0fe to
0c953aa
Compare
| bazel build --config=qnx //quality/coverage:coverage_scope --output_groups=allowlist | ||
| ALLOWLIST_FILE="$(bazel info bazel-bin)/quality/coverage/coverage_scope_allowlist.txt" |
There was a problem hiding this comment.
This is not very portable. Better would be if this target is provided as input artifact via bazel.
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
This section would benefit from a comment that explains what its goal is.
| if filename.startswith("/proc/self/cwd/"): | ||
| return filename[len("/proc/self/cwd/"):] |
There was a problem hiding this comment.
Better not duplicate this string.
| if not allowlist: | ||
| return list(file_entries) |
There was a problem hiding this comment.
This does not fulfill the contract of the function. Either extend the description of the function or change this to return an empty list.
| line_number = incoming_line["line_number"] | ||
| incoming_copy = dict(incoming_line) | ||
| incoming_copy["branches"] = [dict(branch) for branch in incoming_line.get("branches", [])] |
There was a problem hiding this comment.
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:
- If the modification of
incoming_lineis non-destructive, just drop the shallow copy - If the modification of
incoming_lineis destructive usecopy.deepcopy().
| 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"]) |
There was a problem hiding this comment.
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.""" |
There was a problem hiding this comment.
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.
0c953aa to
a3ec8d6
Compare
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
a3ec8d6 to
26aaf8b
Compare
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>
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.