Skip to content

Commit 044b810

Browse files
CEL Dev Teamcopybara-github
authored andcommitted
Enable coverage collection via the runner library for a single test.
PiperOrigin-RevId: 818456916
1 parent 38f6e94 commit 044b810

File tree

7 files changed

+260
-116
lines changed

7 files changed

+260
-116
lines changed

testing/testrunner/BUILD

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ cc_library(
3636
deps = [
3737
":cel_expression_source",
3838
":cel_test_context",
39+
":coverage_index",
40+
":coverage_reporting",
3941
"//checker:validation_result",
4042
"//common:ast",
4143
"//common:ast_proto",
@@ -49,10 +51,9 @@ cc_library(
4951
"//internal:testing_no_main",
5052
"//runtime",
5153
"//runtime:activation",
52-
"//tools:cel_unparser",
53-
"//tools:navigable_ast",
5454
"@com_google_absl//absl/functional:overload",
5555
"@com_google_absl//absl/status",
56+
"@com_google_absl//absl/status:status_matchers",
5657
"@com_google_absl//absl/status:statusor",
5758
"@com_google_absl//absl/strings",
5859
"@com_google_absl//absl/strings:string_view",
@@ -120,6 +121,19 @@ cc_test(
120121
],
121122
)
122123

124+
cc_library(
125+
name = "coverage_reporting",
126+
srcs = ["coverage_reporting.cc"],
127+
hdrs = ["coverage_reporting.h"],
128+
deps = [
129+
":coverage_index",
130+
"//internal:testing_no_main",
131+
"@com_google_absl//absl/log:absl_log",
132+
"@com_google_absl//absl/strings",
133+
"@com_google_absl//absl/strings:str_format",
134+
],
135+
)
136+
123137
cc_library(
124138
name = "runner",
125139
srcs = ["runner_bin.cc"],
@@ -128,6 +142,7 @@ cc_library(
128142
":cel_test_context",
129143
":cel_test_factories",
130144
":coverage_index",
145+
":coverage_reporting",
131146
":runner_lib",
132147
"//eval/public:cel_expression",
133148
"//internal:status_macros",
@@ -139,7 +154,6 @@ cc_library(
139154
"@com_google_absl//absl/status",
140155
"@com_google_absl//absl/status:statusor",
141156
"@com_google_absl//absl/strings",
142-
"@com_google_absl//absl/strings:str_format",
143157
"@com_google_cel_spec//proto/cel/expr:checked_cc_proto",
144158
"@com_google_cel_spec//proto/cel/expr/conformance/test:suite_cc_proto",
145159
"@com_google_protobuf//:protobuf",

testing/testrunner/cel_test_context.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ class CelTestContext {
102102
return custom_bindings_;
103103
}
104104

105+
bool enable_coverage() const { return enable_coverage_; }
106+
105107
// Allows the runner to inject the expression source
106108
// parsed from command-line flags.
107109
void SetExpressionSource(CelExpressionSource source) {
@@ -128,6 +130,9 @@ class CelTestContext {
128130
activation_factory_ = std::move(activation_factory);
129131
}
130132

133+
// Allows the runner to enable coverage collection.
134+
void SetEnableCoverage(bool enable) { enable_coverage_ = enable; }
135+
131136
const CelActivationFactoryFn& activation_factory() const {
132137
return activation_factory_;
133138
}
@@ -185,6 +190,9 @@ class CelTestContext {
185190

186191
CelActivationFactoryFn activation_factory_;
187192
AssertFn assert_fn_;
193+
194+
// Whether to enable coverage collection.
195+
bool enable_coverage_ = false;
188196
};
189197

190198
} // namespace cel::test
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "testing/testrunner/coverage_reporting.h"
16+
17+
#include <cerrno>
18+
#include <cstdlib>
19+
#include <cstring>
20+
#include <fstream>
21+
#include <string>
22+
23+
#include "absl/log/absl_log.h"
24+
#include "absl/strings/str_cat.h"
25+
#include "absl/strings/str_format.h"
26+
#include "absl/strings/str_join.h"
27+
#include "absl/strings/str_replace.h"
28+
#include "absl/strings/string_view.h"
29+
#include "internal/testing.h"
30+
#include "testing/testrunner/coverage_index.h"
31+
32+
namespace cel::test {
33+
void CoverageReportingEnvironment::TearDown() {
34+
CoverageIndex::CoverageReport coverage_report =
35+
coverage_index_.GetCoverageReport();
36+
testing::Test::RecordProperty("CEL Expression",
37+
coverage_report.cel_expression);
38+
std::cout << "CEL Expression: " << coverage_report.cel_expression;
39+
if (coverage_report.nodes == 0) {
40+
testing::Test::RecordProperty("CEL Coverage", "No coverage stats found");
41+
std::cout << "CEL Coverage: " << "No coverage stats found";
42+
return;
43+
}
44+
45+
// Log Node Coverage results
46+
double node_coverage = static_cast<double>(coverage_report.covered_nodes) /
47+
static_cast<double>(coverage_report.nodes) * 100.0;
48+
std::string node_coverage_string =
49+
absl::StrFormat("%.2f%% (%d out of %d nodes covered)", node_coverage,
50+
coverage_report.covered_nodes, coverage_report.nodes);
51+
testing::Test::RecordProperty("AST Node Coverage", node_coverage_string);
52+
std::cout << "AST Node Coverage: " << node_coverage_string;
53+
if (!coverage_report.unencountered_nodes.empty()) {
54+
testing::Test::RecordProperty(
55+
"Interesting Unencountered Nodes",
56+
absl::StrJoin(coverage_report.unencountered_nodes, "\n"));
57+
std::cout << "Interesting Unencountered Nodes: "
58+
<< absl::StrJoin(coverage_report.unencountered_nodes, "\n");
59+
}
60+
61+
// Log Branch Coverage results
62+
double branch_coverage = 0.0;
63+
if (coverage_report.branches > 0) {
64+
branch_coverage =
65+
static_cast<double>(coverage_report.covered_boolean_outcomes) /
66+
static_cast<double>(coverage_report.branches) * 100.0;
67+
}
68+
std::string branch_coverage_string = absl::StrFormat(
69+
"%.2f%% (%d out of %d branch outcomes covered)", branch_coverage,
70+
coverage_report.covered_boolean_outcomes, coverage_report.branches);
71+
testing::Test::RecordProperty("AST Branch Coverage", branch_coverage_string);
72+
std::cout << "AST Branch Coverage: " << branch_coverage_string;
73+
if (!coverage_report.unencountered_branches.empty()) {
74+
testing::Test::RecordProperty(
75+
"Interesting Unencountered Branch Paths",
76+
absl::StrJoin(coverage_report.unencountered_branches, "\n"));
77+
std::cout << "Interesting Unencountered Branch Paths: "
78+
<< absl::StrJoin(coverage_report.unencountered_branches,
79+
"\n");
80+
}
81+
if (!coverage_report.dot_graph.empty()) {
82+
WriteDotGraphToArtifact(coverage_report.dot_graph);
83+
}
84+
}
85+
86+
void CoverageReportingEnvironment::WriteDotGraphToArtifact(
87+
absl::string_view dot_graph) {
88+
// Save DOT graph to file in TEST_UNDECLARED_OUTPUTS_DIR or default dir
89+
const char* outputs_dir_env = std::getenv("TEST_UNDECLARED_OUTPUTS_DIR");
90+
// For non-Bazel/Blaze users, we write to a subdirectory under the current
91+
// working directory.
92+
// NOMUTANTS --cel_artifacts is for non-Bazel/Blaze users only so not
93+
// needed to test in our case.
94+
std::string outputs_dir =
95+
(outputs_dir_env == nullptr) ? "cel_artifacts" : outputs_dir_env;
96+
std::string coverage_dir = absl::StrCat(outputs_dir, "/cel_test_coverage");
97+
// Creates the directory to store CEL test coverage artifacts.
98+
// The second argument, `0755`, sets the directory's permissions in octal
99+
// format, which is a standard for file system operations. It grants:
100+
// - Owner: read, write, and execute permissions (7 = 4+2+1).
101+
// - Group: read and execute permissions (5 = 4+1).
102+
// - Others: read and execute permissions (5 = 4+1).
103+
// This gives the owner full control while allowing other users to access
104+
// the generated artifacts.
105+
int mkdir_result = mkdir(coverage_dir.c_str(), 0755);
106+
// If mkdir fails, it sets the global 'errno' variable to an error code
107+
// indicating the reason. We check this code to specifically ignore the
108+
// EEXIST error, which just means the directory already exists (this is not
109+
// a real failure we need to warn about).
110+
if (mkdir_result == 0 || errno == EEXIST) {
111+
std::string graph_path = absl::StrCat(coverage_dir, "/coverage_graph.txt");
112+
std::ofstream out(graph_path);
113+
if (out.is_open()) {
114+
out << dot_graph;
115+
out.close();
116+
} else {
117+
ABSL_LOG(WARNING) << "Failed to open file for writing: " << graph_path;
118+
}
119+
} else {
120+
ABSL_LOG(WARNING) << "Failed to create directory: " << coverage_dir
121+
<< " (reason: " << strerror(errno) << ")";
122+
}
123+
}
124+
} // namespace cel::test
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#ifndef THIRD_PARTY_CEL_CPP_TESTING_TESTRUNNER_COVERAGE_REPORTING_H_
16+
#define THIRD_PARTY_CEL_CPP_TESTING_TESTRUNNER_COVERAGE_REPORTING_H_
17+
18+
#include "absl/strings/string_view.h"
19+
#include "internal/testing.h"
20+
#include "testing/testrunner/coverage_index.h"
21+
22+
namespace cel::test {
23+
// A Google Test Environment that reports CEL coverage results in its TearDown
24+
// phase.
25+
//
26+
// This class encapsulates the logic for calculating coverage statistics and
27+
// logging them as test properties.
28+
class CoverageReportingEnvironment : public testing::Environment {
29+
public:
30+
explicit CoverageReportingEnvironment(CoverageIndex& coverage_index)
31+
: coverage_index_(coverage_index) {};
32+
33+
// Called by the Google Test framework after all tests have run.
34+
void TearDown() override;
35+
36+
private:
37+
// Helper function to write the DOT graph to a test artifact file.
38+
void WriteDotGraphToArtifact(absl::string_view dot_graph);
39+
40+
CoverageIndex& coverage_index_;
41+
};
42+
} // namespace cel::test
43+
#endif // THIRD_PARTY_CEL_CPP_TESTING_TESTRUNNER_COVERAGE_REPORTING_H_

0 commit comments

Comments
 (0)