Skip to content

Commit 870adf2

Browse files
l46kokcopybara-github
authored andcommitted
Introduce CEL Policy Conformance Test Runner for Java
PiperOrigin-RevId: 907725999
1 parent 21c3318 commit 870adf2

5 files changed

Lines changed: 357 additions & 0 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
load("@rules_java//java:defs.bzl", "java_library")
2+
load(":cel_policy_conformance_test.bzl", "cel_policy_conformance_test_java")
3+
4+
package(
5+
default_applicable_licenses = ["//:license"],
6+
default_testonly = True,
7+
)
8+
9+
java_library(
10+
name = "run",
11+
srcs = glob(["*.java"]),
12+
deps = [
13+
"//:auto_value",
14+
"//bundle:cel",
15+
"//testing/testrunner:cel_expression_source",
16+
"//testing/testrunner:cel_test_context",
17+
"//testing/testrunner:cel_test_suite",
18+
"//testing/testrunner:cel_test_suite_text_proto_parser",
19+
"//testing/testrunner:cel_test_suite_yaml_parser",
20+
"//testing/testrunner:test_runner_library",
21+
"@maven//:com_google_guava_guava",
22+
"@maven//:com_google_protobuf_protobuf_java",
23+
"@maven//:junit_junit",
24+
],
25+
)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2026 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+
package dev.cel.conformance.policy;
16+
17+
import com.google.protobuf.ListValue;
18+
import com.google.protobuf.Struct;
19+
import com.google.protobuf.Value;
20+
import dev.cel.bundle.Cel;
21+
import dev.cel.bundle.CelFactory;
22+
import dev.cel.testing.testrunner.CelExpressionSource;
23+
import dev.cel.testing.testrunner.CelTestContext;
24+
import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase;
25+
import dev.cel.testing.testrunner.TestRunnerLibrary;
26+
import java.nio.file.Files;
27+
import java.nio.file.Path;
28+
import org.junit.runners.model.Statement;
29+
30+
/** Statement representing a single CEL policy conformance test case. */
31+
public final class PolicyConformanceTest extends Statement {
32+
33+
private static final Cel CEL = CelFactory.standardCelBuilder().build();
34+
35+
private final String name;
36+
private final CelTestCase testCase;
37+
private final String dirPath;
38+
39+
public PolicyConformanceTest(String name, CelTestCase testCase, String dirPath) {
40+
this.name = name;
41+
this.testCase = testCase;
42+
this.dirPath = dirPath;
43+
}
44+
45+
public String getName() {
46+
return name;
47+
}
48+
49+
@Override
50+
public void evaluate() throws Throwable {
51+
String policyFile = Path.of(dirPath, "policy.yaml").toString();
52+
53+
CelTestContext.Builder contextBuilder =
54+
CelTestContext.newBuilder()
55+
.setCelExpression(CelExpressionSource.fromSource(policyFile))
56+
.setCel(CEL)
57+
.addMessageTypes(
58+
Struct.getDescriptor(), Value.getDescriptor(), ListValue.getDescriptor());
59+
60+
Path yamlConfigPath = Path.of(dirPath, "config.yaml");
61+
Path textprotoConfigPath = Path.of(dirPath, "config.textproto");
62+
63+
if (Files.exists(yamlConfigPath)) {
64+
contextBuilder.setConfigFile(yamlConfigPath.toString());
65+
} else if (Files.exists(textprotoConfigPath)) {
66+
contextBuilder.setConfigFile(textprotoConfigPath.toString());
67+
}
68+
69+
TestRunnerLibrary.runTest(testCase, contextBuilder.build());
70+
}
71+
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// Copyright 2026 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+
package dev.cel.conformance.policy;
16+
17+
import static java.nio.charset.StandardCharsets.UTF_8;
18+
19+
import com.google.auto.value.AutoValue;
20+
import com.google.common.base.Splitter;
21+
import com.google.common.base.Strings;
22+
import com.google.common.collect.ImmutableList;
23+
import com.google.common.io.Files;
24+
import com.google.protobuf.ListValue;
25+
import com.google.protobuf.Struct;
26+
import com.google.protobuf.TypeRegistry;
27+
import com.google.protobuf.Value;
28+
import dev.cel.testing.testrunner.CelTestSuite;
29+
import dev.cel.testing.testrunner.CelTestSuite.CelTestSection;
30+
import dev.cel.testing.testrunner.CelTestSuite.CelTestSection.CelTestCase;
31+
import dev.cel.testing.testrunner.CelTestSuiteTextProtoParser;
32+
import dev.cel.testing.testrunner.CelTestSuiteYamlParser;
33+
import java.io.File;
34+
import java.util.Arrays;
35+
import java.util.List;
36+
import org.junit.runner.Description;
37+
import org.junit.runner.notification.RunNotifier;
38+
import org.junit.runners.ParentRunner;
39+
import org.junit.runners.model.InitializationError;
40+
41+
/** Custom JUnit runner for CEL policy conformance tests. */
42+
public final class PolicyConformanceTestRunner extends ParentRunner<PolicyConformanceTest> {
43+
44+
private static final Splitter SPLITTER = Splitter.on(",").omitEmptyStrings();
45+
private static final String TESTS_YAML_FILE_NAME = "tests.yaml";
46+
private static final String TESTS_TEXTPROTO_FILE_NAME = "tests.textproto";
47+
private static final TypeRegistry TYPE_REGISTRY =
48+
TypeRegistry.newBuilder()
49+
.add(Struct.getDescriptor())
50+
.add(Value.getDescriptor())
51+
.add(ListValue.getDescriptor())
52+
.build();
53+
54+
private static final String TEST_DIRS_PROP =
55+
System.getProperty("dev.cel.policy.conformance.tests");
56+
private static final String TESTDATA_DIR =
57+
System.getProperty("dev.cel.policy.conformance.testdata_dir", "testdata");
58+
private static final String SKIP_TESTS_PROP =
59+
System.getProperty("dev.cel.policy.conformance.skip_tests");
60+
61+
private static final ImmutableList<String> TESTS_TO_SKIP =
62+
Strings.isNullOrEmpty(SKIP_TESTS_PROP)
63+
? ImmutableList.of()
64+
: ImmutableList.copyOf(SPLITTER.splitToList(SKIP_TESTS_PROP));
65+
66+
private static final ImmutableList<String> TEST_DIRS =
67+
Strings.isNullOrEmpty(TEST_DIRS_PROP)
68+
? discoverTestDirs(TESTDATA_DIR)
69+
: ImmutableList.copyOf(SPLITTER.splitToList(TEST_DIRS_PROP));
70+
71+
private static ImmutableList<String> discoverTestDirs(String testdataDir) {
72+
File dir = new File(testdataDir);
73+
if (!dir.exists() || !dir.isDirectory()) {
74+
return ImmutableList.of();
75+
}
76+
String[] directories = dir.list((current, name) -> new File(current, name).isDirectory());
77+
if (directories == null) {
78+
return ImmutableList.of();
79+
}
80+
Arrays.sort(directories);
81+
return ImmutableList.copyOf(directories);
82+
}
83+
84+
private final ImmutableList<PolicyConformanceTest> tests;
85+
86+
private ImmutableList<PolicyConformanceTest> loadTests() {
87+
if (TEST_DIRS.isEmpty()) {
88+
return ImmutableList.of();
89+
}
90+
91+
ImmutableList.Builder<PolicyConformanceTest> testsBuilder = ImmutableList.builder();
92+
93+
for (String dir : TEST_DIRS) {
94+
String fullDirPath = TESTDATA_DIR + "/" + dir;
95+
try {
96+
ImmutableList<CelTestSuiteContext> suites = readTestSuites(fullDirPath);
97+
for (CelTestSuiteContext namedSuite : suites) {
98+
for (CelTestSection section : namedSuite.testSuite().sections()) {
99+
for (CelTestCase testCase : section.tests()) {
100+
String baseName = String.format("%s/%s/%s", dir, section.name(), testCase.name());
101+
String displayName = baseName + namedSuite.formatSuffix();
102+
if (!shouldSkipTest(baseName, TESTS_TO_SKIP)) {
103+
testsBuilder.add(new PolicyConformanceTest(displayName, testCase, fullDirPath));
104+
}
105+
}
106+
}
107+
}
108+
} catch (Exception e) {
109+
throw new RuntimeException("Failed to load test suite in " + fullDirPath, e);
110+
}
111+
}
112+
return testsBuilder.build();
113+
}
114+
115+
private static boolean shouldSkipTest(String name, List<String> testsToSkip) {
116+
for (String testToSkip : testsToSkip) {
117+
if (name.startsWith(testToSkip)) {
118+
String consumedName = name.substring(testToSkip.length());
119+
if (consumedName.isEmpty() || consumedName.startsWith("/")) {
120+
return true;
121+
}
122+
}
123+
}
124+
return false;
125+
}
126+
127+
private static ImmutableList<CelTestSuiteContext> readTestSuites(String dirPath)
128+
throws Exception {
129+
File dir = new File(dirPath);
130+
File yamlFile = new File(dir, TESTS_YAML_FILE_NAME);
131+
File textprotoFile = new File(dir, TESTS_TEXTPROTO_FILE_NAME);
132+
133+
boolean bothExist = yamlFile.exists() && textprotoFile.exists();
134+
ImmutableList.Builder<CelTestSuiteContext> suitesBuilder = ImmutableList.builder();
135+
136+
if (yamlFile.exists()) {
137+
suitesBuilder.add(
138+
CelTestSuiteContext.create(
139+
CelTestSuiteYamlParser.newInstance()
140+
.parse(Files.asCharSource(yamlFile, UTF_8).read()),
141+
bothExist ? " (yaml)" : ""));
142+
}
143+
if (textprotoFile.exists()) {
144+
suitesBuilder.add(
145+
CelTestSuiteContext.create(
146+
CelTestSuiteTextProtoParser.newInstance()
147+
.parse(Files.asCharSource(textprotoFile, UTF_8).read(), TYPE_REGISTRY),
148+
bothExist ? " (textproto)" : ""));
149+
}
150+
151+
ImmutableList<CelTestSuiteContext> suites = suitesBuilder.build();
152+
if (suites.isEmpty()) {
153+
throw new IllegalArgumentException(
154+
String.format(
155+
"No %s or %s found in %s", TESTS_YAML_FILE_NAME, TESTS_TEXTPROTO_FILE_NAME, dirPath));
156+
}
157+
return suites;
158+
}
159+
160+
@Override
161+
protected ImmutableList<PolicyConformanceTest> getChildren() {
162+
return tests;
163+
}
164+
165+
@Override
166+
protected Description describeChild(PolicyConformanceTest child) {
167+
return Description.createTestDescription(getTestClass().getJavaClass(), child.getName());
168+
}
169+
170+
@Override
171+
protected void runChild(PolicyConformanceTest child, RunNotifier notifier) {
172+
runLeaf(child, describeChild(child), notifier);
173+
}
174+
175+
public PolicyConformanceTestRunner(Class<?> clazz) throws InitializationError {
176+
super(clazz);
177+
this.tests = loadTests();
178+
}
179+
180+
@AutoValue
181+
abstract static class CelTestSuiteContext {
182+
abstract CelTestSuite testSuite();
183+
184+
abstract String formatSuffix();
185+
186+
static CelTestSuiteContext create(CelTestSuite testSuite, String formatSuffix) {
187+
return new AutoValue_PolicyConformanceTestRunner_CelTestSuiteContext(testSuite, formatSuffix);
188+
}
189+
}
190+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2026 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+
package dev.cel.conformance.policy;
16+
17+
import org.junit.runner.RunWith;
18+
19+
/** Main test class for CEL policy conformance tests. */
20+
@RunWith(PolicyConformanceTestRunner.class)
21+
public class PolicyConformanceTests {}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Copyright 2026 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+
"""Macro to run CEL policy conformance tests."""
16+
17+
load("@rules_java//java:defs.bzl", "java_test")
18+
19+
def cel_policy_conformance_test_java(
20+
name,
21+
testdata,
22+
test_cases = [],
23+
skip_tests = [],
24+
**kwargs):
25+
"""Macro to run CEL policy conformance tests for Java.
26+
27+
Args:
28+
name: The name of the test target.
29+
testdata: Testdata filegroup target.
30+
test_cases: (optional) List of test case names (directory names) to run.
31+
skip_tests: (optional) List of test case names (directory names) to skip.
32+
**kwargs: Other standard Bazel target attributes.
33+
"""
34+
35+
lbl = native.package_relative_label(testdata)
36+
testdata_dir = lbl.package + "/" + lbl.name
37+
38+
java_test(
39+
name = name,
40+
jvm_flags = [
41+
"-Ddev.cel.policy.conformance.tests=" + ",".join(test_cases),
42+
"-Ddev.cel.policy.conformance.testdata_dir=" + testdata_dir,
43+
"-Ddev.cel.policy.conformance.skip_tests=" + ",".join(skip_tests),
44+
],
45+
data = [testdata],
46+
size = "small",
47+
test_class = "dev.cel.conformance.policy.PolicyConformanceTests",
48+
runtime_deps = [Label(":run")],
49+
**kwargs
50+
)

0 commit comments

Comments
 (0)