[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