Skip to content

Commit 3160baa

Browse files
ChinmayMadeshicopybara-github
authored andcommitted
Setup of the coverage index.
PiperOrigin-RevId: 810740259
1 parent 31f0895 commit 3160baa

File tree

11 files changed

+575
-39
lines changed

11 files changed

+575
-39
lines changed

eval/compiler/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ DEFAULT_VISIBILITY = [
55
"//eval:__subpackages__",
66
"//runtime:__subpackages__",
77
"//extensions:__subpackages__",
8+
"//testing:__subpackages__",
89
]
910

1011
# This package contains code

testing/testrunner/BUILD

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ cc_library(
4444
"//internal:testing_no_main",
4545
"//runtime",
4646
"//runtime:activation",
47+
"//tools:cel_unparser",
48+
"//tools:navigable_ast",
4749
"@com_google_absl//absl/functional:overload",
4850
"@com_google_absl//absl/status",
4951
"@com_google_absl//absl/status:statusor",
@@ -80,6 +82,7 @@ cc_test(
8082
deps = [
8183
":cel_expression_source",
8284
":cel_test_context",
85+
":coverage_index",
8386
":runner_lib",
8487
"//checker:type_checker_builder",
8588
"//checker:validation_result",
@@ -115,14 +118,18 @@ cc_library(
115118
deps = [
116119
":cel_test_context",
117120
":cel_test_factories",
121+
":coverage_index",
118122
":runner_lib",
123+
"//eval/public:cel_expression",
119124
"//internal:testing_no_main",
125+
"//runtime",
120126
"@com_google_absl//absl/flags:flag",
121127
"@com_google_absl//absl/log:absl_check",
122128
"@com_google_absl//absl/log:absl_log",
123129
"@com_google_absl//absl/status",
124130
"@com_google_absl//absl/status:statusor",
125131
"@com_google_absl//absl/strings",
132+
"@com_google_absl//absl/strings:str_format",
126133
"@com_google_cel_spec//proto/cel/expr/conformance/test:suite_cc_proto",
127134
"@com_google_protobuf//:protobuf",
128135
"@com_google_protobuf//src/google/protobuf/io",
@@ -135,3 +142,27 @@ cc_library(
135142
hdrs = ["cel_expression_source.h"],
136143
deps = ["@com_google_cel_spec//proto/cel/expr:checked_cc_proto"],
137144
)
145+
146+
cc_library(
147+
name = "coverage_index",
148+
srcs = ["coverage_index.cc"],
149+
hdrs = ["coverage_index.h"],
150+
deps = [
151+
"//common:ast",
152+
"//common:value",
153+
"//eval/compiler:cel_expression_builder_flat_impl",
154+
"//eval/compiler:instrumentation",
155+
"//eval/public:cel_expression",
156+
"//internal:casts",
157+
"//runtime",
158+
"//runtime/internal:runtime_impl",
159+
"//tools:cel_unparser",
160+
"//tools:navigable_ast",
161+
"@com_google_absl//absl/base:nullability",
162+
"@com_google_absl//absl/container:flat_hash_map",
163+
"@com_google_absl//absl/status",
164+
"@com_google_absl//absl/status:statusor",
165+
"@com_google_absl//absl/strings",
166+
"@com_google_cel_spec//proto/cel/expr:syntax_cc_proto",
167+
],
168+
)

testing/testrunner/cel_cc_test.bzl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def cel_cc_test(
2121
test_suite = "",
2222
filegroup = "",
2323
deps = [],
24+
enable_coverage = False,
2425
test_data_path = "",
2526
data = []):
2627
"""trigger the cc impl of the CEL test runner.
@@ -36,6 +37,7 @@ def cel_cc_test(
3637
expression.
3738
deps: list of dependencies for the cc_test rule.
3839
data: list of data dependencies for the cc_test rule.
40+
enable_coverage: bool whether to enable coverage collection.
3941
test_data_path: absolute path of the directory containing the test files. This is needed only
4042
if the test files are not located in the same directory as the BUILD file.
4143
"""
@@ -48,6 +50,8 @@ def cel_cc_test(
4850
test_suite = test_data_path + "/" + test_suite
4951
args.append("--test_suite_path=" + test_suite)
5052

53+
args.append("--collect_coverage=" + str(enable_coverage))
54+
5155
cc_test(
5256
name = name,
5357
data = data,
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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_index.h"
16+
17+
#include <cstdint>
18+
#include <string>
19+
#include <vector>
20+
21+
#include "cel/expr/syntax.pb.h"
22+
#include "absl/base/nullability.h"
23+
#include "absl/container/flat_hash_map.h"
24+
#include "absl/status/status.h"
25+
#include "absl/status/statusor.h"
26+
#include "absl/strings/str_cat.h"
27+
#include "common/ast.h"
28+
#include "common/value.h"
29+
#include "eval/compiler/cel_expression_builder_flat_impl.h"
30+
#include "eval/compiler/instrumentation.h"
31+
#include "eval/public/cel_expression.h"
32+
#include "internal/casts.h"
33+
#include "runtime/internal/runtime_impl.h"
34+
#include "runtime/runtime.h"
35+
#include "tools/cel_unparser.h"
36+
#include "tools/navigable_ast.h"
37+
38+
namespace cel::test {
39+
namespace {
40+
41+
using ::cel::expr::CheckedExpr;
42+
using ::cel::expr::Type;
43+
using ::google::api::expr::runtime::CelExpressionBuilder;
44+
using ::google::api::expr::runtime::Instrumentation;
45+
using ::google::api::expr::runtime::InstrumentationFactory;
46+
47+
const Type* absl_nullable FindCheckerType(const CheckedExpr& expr,
48+
int64_t expr_id) {
49+
if (auto it = expr.type_map().find(expr_id); it != expr.type_map().end()) {
50+
return &it->second;
51+
}
52+
return nullptr;
53+
}
54+
55+
bool InferredBooleanNode(const CheckedExpr& checked_expr,
56+
const NavigableProtoAstNode& node) {
57+
int64_t node_id = node.expr()->id();
58+
const auto* checker_type = FindCheckerType(checked_expr, node_id);
59+
if (checker_type != nullptr) {
60+
return checker_type->has_primitive() &&
61+
checker_type->primitive() == Type::BOOL;
62+
}
63+
64+
return false;
65+
}
66+
67+
void TraverseAndCalculateCoverage(
68+
const CheckedExpr& checked_expr, const NavigableProtoAstNode& node,
69+
const absl::flat_hash_map<int64_t, CoverageIndex::NodeCoverageStats>&
70+
stats_map,
71+
bool log_unencountered, std::string preceeding_tabs,
72+
CoverageIndex::CoverageReport& report) {
73+
int64_t node_id = node.expr()->id();
74+
75+
const CoverageIndex::NodeCoverageStats& stats = stats_map.at(node_id);
76+
report.nodes++;
77+
78+
absl::StatusOr<std::string> unparsed =
79+
google::api::expr::Unparse(*node.expr());
80+
std::string expr_text = unparsed.ok() ? *unparsed : "unparse_failed";
81+
82+
bool is_interesting_bool_node =
83+
stats.is_boolean_node && !node.expr()->has_const_expr() &&
84+
(!node.expr()->has_call_expr() ||
85+
node.expr()->call_expr().function() != "cel.@block");
86+
87+
bool node_covered = stats.covered;
88+
if (node_covered) {
89+
report.covered_nodes++;
90+
} else if (log_unencountered) {
91+
if (is_interesting_bool_node) {
92+
report.unencountered_nodes.push_back(
93+
absl::StrCat("Expression ID ", node_id, " ('", expr_text, "')"));
94+
}
95+
log_unencountered = false;
96+
}
97+
98+
if (is_interesting_bool_node) {
99+
report.branches += 2;
100+
if (stats.has_true_branch) {
101+
report.covered_boolean_outcomes++;
102+
} else if (log_unencountered) {
103+
report.unencountered_branches.push_back(
104+
absl::StrCat("\n", preceeding_tabs, "Expression ID ", node_id, " ('",
105+
expr_text, "'): Never evaluated to 'true'"));
106+
preceeding_tabs += "\t\t";
107+
}
108+
if (stats.has_false_branch) {
109+
report.covered_boolean_outcomes++;
110+
} else if (log_unencountered) {
111+
report.unencountered_branches.push_back(
112+
absl::StrCat("\n", preceeding_tabs, "Expression ID ", node_id, " ('",
113+
expr_text, "'): Never evaluated to 'false'"));
114+
preceeding_tabs += "\t\t";
115+
}
116+
}
117+
118+
for (const auto* child : node.children()) {
119+
TraverseAndCalculateCoverage(checked_expr, *child, stats_map,
120+
log_unencountered, preceeding_tabs, report);
121+
}
122+
}
123+
124+
} // namespace
125+
126+
void CoverageIndex::RecordCoverage(int64_t node_id, const cel::Value& value) {
127+
NodeCoverageStats& stats = node_coverage_stats_[node_id];
128+
stats.covered = true;
129+
if (node_coverage_stats_[node_id].is_boolean_node) {
130+
if (value.AsBool()->NativeValue()) {
131+
stats.has_true_branch = true;
132+
} else {
133+
stats.has_false_branch = true;
134+
}
135+
}
136+
}
137+
138+
void CoverageIndex::Init(const cel::expr::CheckedExpr& checked_expr) {
139+
checked_expr_ = checked_expr;
140+
navigable_ast_ = NavigableProtoAst::Build(checked_expr_.expr());
141+
for (const auto& node : navigable_ast_.Root().DescendantsPreorder()) {
142+
NodeCoverageStats stats;
143+
stats.is_boolean_node = InferredBooleanNode(checked_expr_, node);
144+
node_coverage_stats_[node.expr()->id()] = stats;
145+
}
146+
}
147+
148+
CoverageIndex::CoverageReport CoverageIndex::GetCoverageReport() const {
149+
CoverageReport report;
150+
if (node_coverage_stats_.empty()) {
151+
return report;
152+
}
153+
TraverseAndCalculateCoverage(checked_expr_, navigable_ast_.Root(),
154+
node_coverage_stats_, true, "", report);
155+
report.cel_expression =
156+
google::api::expr::Unparse(checked_expr_).value_or("");
157+
return report;
158+
}
159+
160+
InstrumentationFactory InstrumentationFactoryForCoverage(
161+
CoverageIndex& coverage_index) {
162+
return [&](const cel::Ast& ast) -> Instrumentation {
163+
return [&](int64_t node_id, const cel::Value& value) -> absl::Status {
164+
coverage_index.RecordCoverage(node_id, value);
165+
return absl::OkStatus();
166+
};
167+
};
168+
}
169+
170+
absl::Status EnableCoverageInRuntime(cel::Runtime& runtime,
171+
CoverageIndex& coverage_index) {
172+
auto& runtime_impl =
173+
cel::internal::down_cast<runtime_internal::RuntimeImpl&>(runtime);
174+
runtime_impl.expr_builder().AddProgramOptimizer(
175+
google::api::expr::runtime::CreateInstrumentationExtension(
176+
InstrumentationFactoryForCoverage(coverage_index)));
177+
return absl::OkStatus();
178+
}
179+
180+
absl::Status EnableCoverageInCelExpressionBuilder(
181+
CelExpressionBuilder& cel_expression_builder,
182+
CoverageIndex& coverage_index) {
183+
auto& cel_expression_builder_impl = cel::internal::down_cast<
184+
google::api::expr::runtime::CelExpressionBuilderFlatImpl&>(
185+
cel_expression_builder);
186+
cel_expression_builder_impl.flat_expr_builder().AddProgramOptimizer(
187+
google::api::expr::runtime::CreateInstrumentationExtension(
188+
InstrumentationFactoryForCoverage(coverage_index)));
189+
return absl::OkStatus();
190+
}
191+
192+
} // namespace cel::test

0 commit comments

Comments
 (0)