|
| 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