diff --git a/checker/src/main/java/dev/cel/checker/ProtoTypeMask.java b/checker/src/main/java/dev/cel/checker/ProtoTypeMask.java
index c019604d5..c9ff89989 100644
--- a/checker/src/main/java/dev/cel/checker/ProtoTypeMask.java
+++ b/checker/src/main/java/dev/cel/checker/ProtoTypeMask.java
@@ -80,7 +80,7 @@ public ProtoTypeMask withFieldsAsVariableDeclarations() {
*
*
* - All descendent fields after the last element in the field mask path are visible.
- *
- The asterisk '*' can be used as an explicit indicator that all descedent fields are
+ *
- The asterisk '*' can be used as an explicit indicator that all descendent fields are
* visible to CEL.
*
- Repeated fields are not supported.
*
diff --git a/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel b/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel
index 6924f753f..f8df0b1f4 100644
--- a/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel
+++ b/testing/src/main/java/dev/cel/testing/testrunner/BUILD.bazel
@@ -104,6 +104,7 @@ java_library(
"//runtime",
"//testing:expr_value_utils",
"//testing/testrunner:proto_descriptor_utils",
+ "//third_party/bazel/src/java_tools/junitrunner/java/com/google/testing/junit/runner/util",
"@cel_spec//proto/cel/expr:expr_java_proto",
"@maven//:com_google_guava_guava",
"@maven//:com_google_protobuf_protobuf_java",
diff --git a/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java b/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java
index a5e912ccb..9a41a38b3 100644
--- a/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java
+++ b/testing/src/main/java/dev/cel/testing/testrunner/TestRunnerLibrary.java
@@ -31,6 +31,7 @@
import com.google.protobuf.ExtensionRegistry;
import com.google.protobuf.Message;
import com.google.protobuf.TextFormat;
+import com.google.testing.junit.runner.util.TestPropertyExporter;
import dev.cel.bundle.Cel;
import dev.cel.bundle.CelEnvironment;
import dev.cel.bundle.CelEnvironment.ExtensionConfig;
@@ -66,6 +67,16 @@ public final class TestRunnerLibrary {
private static final Logger logger = Logger.getLogger(TestRunnerLibrary.class.getName());
+ private static final String ATTR_CEL_EXPR = "Cel Expr";
+ private static final String ATTR_CEL_COVERAGE = "Cel Coverage";
+ private static final String ATTR_AST_NODE_COVERAGE = "Ast Node Coverage";
+ private static final String ATTR_INTERESTING_UNENCOUNTERED_NODES =
+ "Interesting Unencountered Nodes";
+ private static final String ATTR_AST_BRANCH_COVERAGE = "Ast Branch Coverage";
+ private static final String ATTR_INTERESTING_UNENCOUNTERED_BRANCH_PATHS =
+ "Interesting Unencountered Branch Paths";
+ private static final String ATTR_CEL_TEST_COVERAGE_GRAPH_URL = "Cel Test Coverage Graph URL";
+
/**
* Run the assertions for a given raw/checked expression test case.
*
@@ -144,6 +155,15 @@ static void evaluateTestCase(
celCoverageIndex.init(ast);
}
evaluate(ast, testCase, celTestContext, celCoverageIndex);
+
+ // For programmatic tests, if coverage is not enabled via the build macro, update the Sponge
+ // properties with the coverage report.
+ // This flag does not exist when the test is run via direct invocation of {@link
+ // TestRunnerLibrary#runTest}
+ String isCoverageEnabled = System.getProperty("is_coverage_enabled");
+ if (isCoverageEnabled == null && celCoverageIndex != null) {
+ updateSpongeProperties(celCoverageIndex.generateCoverageReport());
+ }
}
private static CelAbstractSyntaxTree readAstFromCheckedExpression(
@@ -403,4 +423,47 @@ private static Object getValueFromBinding(Object value, CelTestContext celTestCo
}
return value;
}
+
+ /**
+ * Updates bazel/blaze test properties with the provided coverage report.
+ *
+ * This method is called when {@link TestRunnerLibrary#runTest} is invoked directly to export
+ * coverage data.
+ */
+ private static void updateSpongeProperties(CelCoverageIndex.CoverageReport report) {
+ TestPropertyExporter exporter = TestPropertyExporter.INSTANCE;
+ if (report.nodes() == 0) {
+ exporter.exportProperty(ATTR_CEL_COVERAGE, "No coverage stats found");
+ } else {
+ // CEL expression
+ exporter.exportProperty(ATTR_CEL_EXPR, report.celExpression());
+ // Node coverage
+ double nodeCoverage = (double) report.coveredNodes() / (double) report.nodes() * 100.0;
+ String nodeCoverageString =
+ String.format(
+ "%.2f%% (%d out of %d nodes covered)",
+ nodeCoverage, report.coveredNodes(), report.nodes());
+ exporter.exportProperty(ATTR_AST_NODE_COVERAGE, nodeCoverageString);
+ if (!report.unencounteredNodes().isEmpty()) {
+ exporter.exportProperty(
+ ATTR_INTERESTING_UNENCOUNTERED_NODES, String.join("\n", report.unencounteredNodes()));
+ }
+ // Branch coverage
+ double branchCoverage = 0.0;
+ if (report.branches() > 0) {
+ branchCoverage =
+ (double) report.coveredBooleanOutcomes() / (double) report.branches() * 100.0;
+ }
+ String branchCoverageString =
+ String.format(
+ "%.2f%% (%d out of %d branch outcomes covered)",
+ branchCoverage, report.coveredBooleanOutcomes(), report.branches());
+ exporter.exportProperty(ATTR_AST_BRANCH_COVERAGE, branchCoverageString);
+ if (!report.unencounteredBranches().isEmpty()) {
+ exporter.exportProperty(
+ ATTR_INTERESTING_UNENCOUNTERED_BRANCH_PATHS,
+ String.join("\n", report.unencounteredBranches()));
+ }
+ }
+ }
}