[libcxx-commits] [libcxx] [libc++] Add code coverage utilities (PR #233888)
Nikolas Klauser via libcxx-commits
libcxx-commits at lists.llvm.org
Tue Oct 20 09:00:37 PDT 2026
https://github.com/philnik777 created https://github.com/llvm/llvm-project/pull/233888
None
>From 1398a3a871b0c8e0373488bea279fefac34a2ea9 Mon Sep 17 00:00:00 2001
From: Nikolas Klauser <nikolasklauser at berlin.de>
Date: Mon, 28 Sep 2026 10:18:48 +0200
Subject: [PATCH] [libc++] Add code coverage utilities
---
libcxx/test/tools/merge-coverage.cpp | 230 +++++++++++++++++++++++++++
libcxx/utils/generate-coverage | 3 +
libcxx/utils/libcxx/test/params.py | 9 ++
3 files changed, 242 insertions(+)
create mode 100644 libcxx/test/tools/merge-coverage.cpp
create mode 100755 libcxx/utils/generate-coverage
diff --git a/libcxx/test/tools/merge-coverage.cpp b/libcxx/test/tools/merge-coverage.cpp
new file mode 100644
index 0000000000000..3de539b15d631
--- /dev/null
+++ b/libcxx/test/tools/merge-coverage.cpp
@@ -0,0 +1,230 @@
+#include <algorithm>
+#include <flat_map>
+#include <flat_set>
+#include <fstream>
+#include <iostream>
+#include <map>
+#include <nlohmann/json.hpp>
+#include <string>
+
+struct SourceRange
+{
+ uint64_t file;
+ uint64_t expanded_file;
+ uint64_t region_kind;
+ uint64_t line_start;
+ uint64_t column_start;
+ uint64_t line_end;
+ uint64_t column_end;
+
+ friend auto operator<=>(const SourceRange&, const SourceRange&) = default;
+};
+
+struct ExecutionCounts
+{
+ uint64_t true_count;
+ uint64_t false_count;
+};
+
+struct FunctionInfo
+{
+ std::vector<std::string> filenames;
+ std::flat_map<SourceRange, uint64_t> regions;
+ std::flat_map<SourceRange, ExecutionCounts> branches;
+ uint64_t count;
+};
+
+struct [[nodiscard]] BalancedBraces
+{
+ enum Kind
+ {
+ parens,
+ square,
+ brace,
+ } kind;
+
+ BalancedBraces(Kind k) : kind(k)
+ {
+ switch (kind)
+ {
+ case parens:
+ std::cout << '(';
+ break;
+ case square:
+ std::cout << '[';
+ break;
+ case brace:
+ std::cout << '{';
+ break;
+ }
+ }
+ BalancedBraces(const BalancedBraces&) = delete;
+ BalancedBraces& operator=(const BalancedBraces&) = delete;
+ ~BalancedBraces()
+ {
+ switch (kind)
+ {
+ case parens:
+ std::cout << ')';
+ break;
+ case square:
+ std::cout << ']';
+ break;
+ case brace:
+ std::cout << '}';
+ break;
+ }
+ }
+};
+
+int main(int, char** argv)
+{
+ std::ios::sync_with_stdio(false);
+ std::map<std::string, FunctionInfo> functions;
+
+ using json = nlohmann::json;
+
+ std::ifstream files(argv[1]);
+
+ std::string fname;
+ while (std::getline(files, fname))
+ {
+ std::ifstream f(fname);
+ if (f.peek() == EOF) // skip empty files
+ continue;
+ // TODO: Verify the the files have the expected layout
+ json report = json::parse(f)["data"][0]["functions"];
+ for (auto& file : report)
+ {
+ std::string function_name(file["name"]);
+
+ if (function_name == "main")
+ continue;
+
+ FunctionInfo& func_info = functions[std::move(function_name)];
+ auto& filenames = file["filenames"];
+ if (std::ranges::none_of(filenames, [](json j) {
+ auto name = std::string(j);
+ return name.contains("test-suite-install") || name.contains("libcxx/src")
+ || name.contains("libcxxabi/src") || name.contains("libunwind/src");
+ }))
+ continue;
+ if (func_info.filenames.empty())
+ {
+ for (auto& filename : filenames) func_info.filenames.push_back(std::string(filename));
+ }
+ else
+ {
+ auto [lhs_res, rhs_res] = std::ranges::mismatch(filenames, func_info.filenames, {},
+ [](json& j) { return std::string(j); });
+ if (lhs_res != filenames.end() && rhs_res != func_info.filenames.end())
+ {
+ std::clog << "Rejected information for function '" << file["name"]
+ << "' due to different file names\n";
+ for (const auto& n : func_info.filenames) std::clog << n << '\n';
+ std::clog << "vs.\n";
+ for (const auto& n : filenames) std::clog << std::string(n) << '\n';
+ continue;
+ }
+ std::transform(lhs_res, filenames.end(), std::back_inserter(func_info.filenames),
+ [](json j) { return std::string(j); });
+ }
+ for (auto& region : file["regions"])
+ {
+ if (uint64_t(region.at(5)) >= func_info.filenames.size()
+ || uint64_t(region.at(6)) >= func_info.filenames.size())
+ {
+ std::clog << "Incoherent indices\n";
+ continue;
+ }
+ func_info.regions[SourceRange {uint64_t(region.at(5)), uint64_t(region.at(6)),
+ uint64_t(region.at(7)), uint64_t(region.at(0)),
+ uint64_t(region.at(1)), uint64_t(region.at(2)),
+ uint64_t(region.at(3))}] += uint64_t(region.at(4));
+ }
+ for (auto& branch : file["branches"])
+ {
+ auto& [true_count, false_count] = func_info.branches[SourceRange {
+ uint64_t(branch.at(6)), uint64_t(branch.at(7)), uint64_t(branch.at(8)),
+ uint64_t(branch.at(0)), uint64_t(branch.at(1)), uint64_t(branch.at(2)),
+ uint64_t(branch.at(3))}];
+ true_count += uint64_t(branch.at(4));
+ false_count += uint64_t(branch.at(5));
+ }
+ func_info.count += uint64_t(file["count"]);
+ }
+ }
+
+ std::cout << R"json({"data":[{"functions":[)json";
+ bool is_first = true;
+
+ for (const auto& [name, info] : functions)
+ {
+ if (!is_first)
+ std::cout << ',';
+ is_first = false;
+
+ BalancedBraces _(BalancedBraces::brace);
+
+ std::cout << R"json("name":")json" << name << "\",";
+ std::cout << R"json("count":)json" << info.count << ',';
+
+ {
+ std::cout << R"json("filenames":)json";
+ BalancedBraces _(BalancedBraces::square);
+
+ bool is_first_filename = true;
+ for (const auto& filename : info.filenames)
+ {
+ if (!is_first_filename)
+ std::cout << ',';
+ is_first_filename = false;
+ std::cout << '"' << filename << '"';
+ }
+ }
+ std::cout << ',';
+
+ {
+ std::cout << R"json("branches":)json";
+ BalancedBraces _(BalancedBraces::square);
+
+ bool is_first_branch = true;
+ for (const auto& [branch, branch_val] : info.branches) {
+ if (!is_first_branch)
+ std::cout << ',';
+ is_first_branch = false;
+ BalancedBraces _(BalancedBraces::square);
+ std::cout << branch.line_start << ',' << branch.column_start << ',' << branch.line_end
+ << ',' << branch.column_end << ',' << branch_val.true_count << ','
+ << branch_val.false_count << ',' << branch.file << ',' << branch.expanded_file
+ << ',' << branch.region_kind;
+ }
+ }
+ std::cout << ',';
+
+ // TODO: Merge MCDC records
+ std::cout << R"json("mcdc_records":[],)json";
+
+ {
+ std::cout << R"json("regions":)json";
+ BalancedBraces _(BalancedBraces::square);
+
+ bool is_first_region = true;
+ for (const auto& [region, region_val] : info.regions)
+ {
+ if (!is_first_region)
+ std::cout << ',';
+ is_first_region = false;
+ BalancedBraces _(BalancedBraces::square);
+ std::cout << region.line_start << ',' << region.column_start << ',' << region.line_end
+ << ',' << region.column_end << ',' << region_val << ',' << region.file << ','
+ << region.expanded_file << ',' << region.region_kind;
+ }
+ }
+ }
+ std::cout << "]}],";
+
+ std::cout << R"json("type":"llvm.coverage.json.export","version":"3.1.0")json";
+
+ std::cout << "}";
+}
diff --git a/libcxx/utils/generate-coverage b/libcxx/utils/generate-coverage
new file mode 100755
index 0000000000000..63a06f892d504
--- /dev/null
+++ b/libcxx/utils/generate-coverage
@@ -0,0 +1,3 @@
+find -name default.profraw -execdir llvm-profdata merge -sparse default.profraw -o default.profdata \; -execdir sh -c "llvm-cov export ./t.tmp.exe -object $SHARED_LIB -instr-profile=default.profdata --include-filename-regex=this-does-not-match-anything > coverage.json" \;
+find -name coverage.json > coverage_jsons.txt
+$MERGE_COVERAGE coverage_jsons.txt > coverage.json
diff --git a/libcxx/utils/libcxx/test/params.py b/libcxx/utils/libcxx/test/params.py
index 951b0a4dd9806..3fec250a5f24e 100644
--- a/libcxx/utils/libcxx/test/params.py
+++ b/libcxx/utils/libcxx/test/params.py
@@ -529,5 +529,14 @@ def getSuitableClangTidy(cfg):
AddSubstitution("%{cmake_generator}", generator),
],
),
+ Parameter(
+ name="generate_coverage",
+ type=bool,
+ default=False,
+ help="Whether to generate coverage information when running the tests.",
+ actions=lambda enabled: [] if not enabled else [
+ AddCompileFlag("-fprofile-instr-generate -fcoverage-mapping -mllvm -runtime-counter-relocation -fcoverage-mcdc")
+ ],
+ )
]
# fmt: on
More information about the libcxx-commits
mailing list