diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ebe35d82..ac6982b1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -68,19 +68,6 @@ jobs: - name: Microbenchmarks run: ./gradlew jmh - - name: Cache Bazel stuff - if: ${{ matrix.java-version == '21' }} - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 - with: - path: | - ~/.cache/bazel - key: bazel-${{ hashFiles('**/.gitmodules') }} - restore-keys: bazel- - - - name: Conformance tests - if: ${{ matrix.java-version == '21' }} - run: conformance/run-conformance-tests.sh - - name: Capture test results uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: failure() diff --git a/README.md b/README.md index 68a511d0..fee5f965 100644 --- a/README.md +++ b/README.md @@ -433,17 +433,17 @@ The CEL-Go implementation does not pass these CEL-spec conformance tests: ```text --- FAIL: TestSimpleFile/conversions/int/double_truncate (0.01s) - simple_test.go:219: double_truncate: Eval got [int64_value:2], want [int64_value:1] + double_truncate: Eval got [int64_value:2], want [int64_value:1] --- FAIL: TestSimpleFile/conversions/int/double_truncate_neg (0.01s) - simple_test.go:219: double_truncate_neg: Eval got [int64_value:-8], want [int64_value:-7] + double_truncate_neg: Eval got [int64_value:-8], want [int64_value:-7] --- FAIL: TestSimpleFile/conversions/int/double_half_pos (0.01s) - simple_test.go:219: double_half_pos: Eval got [int64_value:12], want [int64_value:11] + double_half_pos: Eval got [int64_value:12], want [int64_value:11] --- FAIL: TestSimpleFile/conversions/int/double_half_neg (0.01s) - simple_test.go:219: double_half_neg: Eval got [int64_value:-4], want [int64_value:-3] + double_half_neg: Eval got [int64_value:-4], want [int64_value:-3] --- FAIL: TestSimpleFile/conversions/uint/double_truncate (0.01s) - simple_test.go:219: double_truncate: Eval got [uint64_value:2], want [uint64_value:1] + double_truncate: Eval got [uint64_value:2], want [uint64_value:1] --- FAIL: TestSimpleFile/conversions/uint/double_half (0.01s) - simple_test.go:219: double_half: Eval got [uint64_value:26], want [uint64_value:25] + double_half: Eval got [uint64_value:26], want [uint64_value:25] ``` ## Building and testing CEL-Java @@ -470,6 +470,11 @@ repository, in case you want to use CEL-Java snapshot artifacts from another pro The project uses Google Java style and the Spotless plugin. Run `./gradlew spotlessApply` to fix formatting issues. -To run the CEL-spec conformance tests, Go, Bazel, and their toolchains are required. From the -CEL-Java repo, run `conformance/run-conformance-tests.sh`. That script performs the necessary Gradle -and Bazel builds. +To run the CEL-Spec conformance tests, use the JUnit-based conformance suite: + +```shell +./gradlew :cel-conformance:test +``` + +The conformance suite reads upstream CEL-Spec textproto testdata from the `submodules/cel-spec` +submodule. It does not require Bazel, Go, or a separate conformance server. diff --git a/build-logic/src/main/kotlin/cel-conventions.gradle.kts b/build-logic/src/main/kotlin/cel-conventions.gradle.kts index 49cdeef8..8a66f9c5 100644 --- a/build-logic/src/main/kotlin/cel-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/cel-conventions.gradle.kts @@ -20,8 +20,7 @@ nessieConfigureJava() val nonPublishedProjects = setOf( - "conformance", - "jacoco", + "cel-conformance", "cel-quarkus-smoke-standalone", "cel-quarkus-smoke-core-pb3-jackson3", ) diff --git a/conformance/README.md b/conformance/README.md index 8854a558..eb881226 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -1,57 +1,40 @@ -# Running CEL-spec conformance tests against CEL-Java +# Running CEL-Spec conformance tests against CEL-Java -If your environment is already setup, just run the shell script -```shell -./run-conformance-tests.sh -``` - -## Requirements & Setup +The CEL-Java conformance suite is a JUnit test suite that reads upstream CEL-Spec +simple testdata from the `submodules/cel-spec` Git submodule. -The CEL-spec conformance test suite is written in Go and uses the bazel build tool. +Run it with: -Required tools: -* [Bazel build tool](https://bazel.build/) (see [bazelisk](https://github.com/bazelbuild/bazelisk?tab=readme-ov-file#installation)) - - See [Bazel web site](https://bazel.build/install) for installation instructions. - Do **not** use the `bazel-bootstrap` package on Ubuntu, because it comes with - pre-compiled classes that are built with Java 17 or newer, preventing it from - running bazel using older Java versions. -* gcc - - On Ubuntu run `apt-get install gcc` +```shell +./gradlew :cel-conformance:test +``` -Other required dependencies like "Go" will be managed by bazel. +The suite does not require Bazel, Go, a separate conformance server, or the old +upstream `simple_test` binary. -## FAQ +## Test selection -### Bazel build hangs +The curated conformance file list and skip list live in: -If the bazel build does not start, i.e. it gets stuck with messages like -``` -Starting local Bazel server and connecting to it... -... still trying to connect to local Bazel server after 10 seconds ... -... still trying to connect to local Bazel server after 20 seconds ... -... still trying to connect to local Bazel server after 30 seconds ... +```text +conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java ``` -then kill all bazel processes make sure that Java 11 is the current one. It seems, that the -above *may* happen when with Java 8. -### Bazel build fails from `./run-conformance-tests.sh` +Each skip uses the upstream conformance path: -If the bazel build fails with an error like this (note the `Failed to create temporary file`), -run the bazel build once from the console: - -```shell -cd submodules/cel-spec - -bazel build ... +```text +file/section/test ``` -If the build still fails, try the following options: +or a whole-section path: -```shell -bazel build ... --sandbox_writable_path="${HOME}/.ccache" --strategy=CppCompile=standalone +```text +file/section ``` -After the build succeeds once from that directory, you can use the `./run-conformance-tests.sh` -script. +Unmatched skips fail the test suite, so stale skips are visible when upstream +testdata changes. + +Optional CEL-Spec files such as extension libraries, optionals, and type +deduction are intentionally not enabled by default. Add those separately with +explicit skip reasoning. diff --git a/conformance/build.gradle.kts b/conformance/build.gradle.kts index c70a4a71..8f4e2769 100644 --- a/conformance/build.gradle.kts +++ b/conformance/build.gradle.kts @@ -22,7 +22,6 @@ import org.gradle.api.tasks.compile.JavaCompile plugins { `java-library` - id("com.gradleup.shadow") id("cel-conventions") } @@ -33,10 +32,6 @@ val mainProtoResourcesDir = layout.buildDirectory.dir("generated/proto-resources val syncMainProtoSources = tasks.register("syncMainProtoSources") { into(syncedMainProtoDir) - from(layout.settingsDirectory.dir("submodules/googleapis/google/rpc")) { into("google/rpc") } - from(layout.settingsDirectory.dir("submodules/googleapis/google/api/expr/conformance")) { - into("google/api/expr/conformance") - } from(layout.settingsDirectory.dir("submodules/cel-spec/proto/cel/expr")) { include("checked.proto", "eval.proto", "syntax.proto", "value.proto") into("cel/expr") @@ -50,13 +45,7 @@ val syncMainProtoSources = val emptyTestProtoDir = layout.buildDirectory.dir("pb-src/test/proto") sourceSets.main { - java.setSrcDirs( - listOf( - layout.projectDirectory.dir("src/main/java"), - layout.buildDirectory.dir("generated/sources/proto/main/java"), - layout.buildDirectory.dir("generated/sources/proto/main/grpc"), - ) - ) + java.setSrcDirs(listOf(layout.buildDirectory.dir("generated/sources/proto/main/java"))) resources.setSrcDirs(listOf(mainProtoResourcesDir)) extensions.configure("proto") { setSrcDirs(listOf(syncedMainProtoDir)) @@ -76,15 +65,11 @@ configurations.all { exclude(group = "org.projectnessie.cel", module = "cel-gene dependencies { implementation(project(":cel-core")) implementation(project(":cel-generated-pb3")) - implementation(testFixtures(project(":cel-core"))) implementation(testFixtures(project(":cel-generated-pb3"))) implementation(libs.protobuf.java) { version { strictly(libs.versions.protobuf3.get()) } } - implementation(libs.grpc.protobuf) - implementation(libs.grpc.stub) - runtimeOnly(libs.grpc.netty.shaded) - compileOnly(libs.tomcat.annotations.api) + testImplementation(testFixtures(project(":cel-core"))) testImplementation(platform(libs.junit.bom)) testImplementation(libs.bundles.junit.testing) @@ -92,10 +77,6 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } -tasks.named("shadowJar") { - manifest { attributes("Main-Class" to "org.projectnessie.cel.server.ConformanceServer") } -} - // *.proto files taken from https://github.com/google/cel-spec/ repo, available as a git submodule configure { // Configure the protoc executable @@ -103,10 +84,6 @@ configure { // Download from repositories artifact = "com.google.protobuf:protoc:${libs.versions.protobuf3.get()}" } - plugins { - this.create("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:${libs.versions.grpc.get()}" } - } - generateProtoTasks { all().configureEach { this.plugins.create("grpc") {} } } } tasks.named("generateProto") { dependsOn(syncMainProtoSources) } diff --git a/conformance/conformance-server.sh b/conformance/conformance-server.sh deleted file mode 100755 index 2425b553..00000000 --- a/conformance/conformance-server.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright (C) 2021 The Authors of CEL-Java -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -wd="$(dirname "$0")" - -pid_file="${wd}/conformance-server.pid" - -function kill_server() { - if [[ -f ${pid_file} ]] ; then - kill "$(cat "${pid_file}")" && rm "${pid_file}" - fi -} - -trap kill_server SIGINT SIGTERM - -java -Djava.net.preferIPv4Stack=true -jar "${wd}"/build/libs/cel-conformance-*-all.jar "${@}" & -java_pid=$! -echo "${java_pid}" > "${pid_file}" -wait diff --git a/conformance/run-conformance-tests.sh b/conformance/run-conformance-tests.sh deleted file mode 100755 index 325acca6..00000000 --- a/conformance/run-conformance-tests.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright (C) 2021 The Authors of CEL-Java -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -wd="$(dirname $0)" - -cd "${wd}/.." || exit 1 - -./gradlew :cel-conformance:shadowJar || exit 1 - -cel_spec_dir="$(realpath submodules/cel-spec)" -cd "${cel_spec_dir}" || exit 1 - -cel_java_skips=( - # Without the checker, it is quite difficult to verify whether an assignment is allowed (by - # the CEL spec), especially from a 'map' to a 'struct' as in these tests using the expression - # `TestAllTypes{single_struct: {1: 'uno'}}`. Note: the checker catches this case and the - # Java implementation currently converts the 'int(1)' to a 'string("1")', which is not strictly - # allowed, but OTOH overall not a serious issue. - "--skip_test=dynamic/struct/field_assign_proto2_bad" - "--skip_test=dynamic/struct/field_assign_proto3_bad" - # The test expects a -0.0d, but in Java `-0.0d==0.0d` is true, so -0.0d is evaluates as "not set", - # so it returns the field as empty. - "--skip_test=dynamic/float/field_assign_proto3_round_to_zero" - # "Malicious" protobuf message. The actual CEL-spec test produces a request with a too deeply - # nested protobuf-object-structure, which gets rejected during gRPC/protobuf request - # deserialization. Just skip those tests. - "--skip_test=parse/nest/message_literal" - # Proto equality specialties don't seem to be in effect for Java - "--skip_test=comparisons/eq_wrapper/eq_proto_nan_equal" - "--skip_test=comparisons/ne_literal/ne_proto_nan_not_equal" - - # TODO Actual known issue to fix, a protobuf Any returned via this test is wrapped twice (Any in Any). - "--skip_test=dynamic/any/var" - - # New CEL-Spec v0.25.2 expectations that need follow-up CEL-Java parser/runtime changes. - "--skip_test=conversions/bool/string_1,string_t,string_0,string_f,string_true_badcase,string_false_badcase" - "--skip_test=fields/quoted_map_fields/field_access_slash,field_access_dash,field_access_dot,has_field_slash,has_field_dash,has_field_dot" - "--skip_test=namespace/namespace_shadowing/comprehension_shadowing,comprehension_shadowing_disambiguation,comprehension_shadowing_parse_only,comprehension_shadowing_selector,comprehension_shadowing_selector_parse_only,comprehension_shadowing_namespaced_selector,comprehension_shadowing_namespaced_selector_parse_only,comprehension_shadowing_nesting" - "--skip_test=proto2/set_null/single_message,single_duration,single_timestamp,repeated_field_timestamp_null_pruned,repeated_field_duration_null_pruned,repeated_field_wrapper_null_pruned,map_timestamp_null_pruned,map_duration_null_pruned,map_wrapper_null_pruned,map_anytype_null_retained,single_scalar,repeated,map,list_value,single_struct" - "--skip_test=proto2/quoted_fields/set_field_with_quoted_name,get_field_with_quoted_name" - "--skip_test=proto2/extensions_has/package_scoped_int32,package_scoped_nested_ext,package_scoped_test_all_types_ext,package_scoped_test_all_types_nested_enum_ext,package_scoped_repeated_test_all_types,message_scoped_int64,message_scoped_nested_ext,message_scoped_nested_enum_ext,message_scoped_repeated_test_all_types" - "--skip_test=proto2/extensions_get/package_scoped_int32,package_scoped_nested_ext,package_scoped_test_all_types_ext,package_scoped_test_all_types_nested_enum_ext,package_scoped_repeated_test_all_types,message_scoped_int64,message_scoped_nested_ext,message_scoped_nested_enum_ext,message_scoped_repeated_test_all_types" - "--skip_test=proto3/set_null/single_message,single_duration,single_timestamp,repeated_field_timestamp_null_pruned,repeated_field_duration_null_pruned,repeated_field_wrapper_null_pruned,map_timestamp_null_pruned,map_duration_null_pruned,map_wrapper_null_pruned,map_anytype_null_retained,single_scalar,repeated,map,list_value,single_struct" - "--skip_test=proto3/quoted_fields/set_field,get_field" - "--skip_test=timestamps/timestamp_conversions/type_comparison" - "--skip_test=timestamps/duration_conversions/type_comparison" - "--skip_test=wrappers/bool/to_null" - "--skip_test=wrappers/int32/to_null" - "--skip_test=wrappers/int64/to_null" - "--skip_test=wrappers/uint32/to_null" - "--skip_test=wrappers/uint64/to_null" - "--skip_test=wrappers/float/to_null" - "--skip_test=wrappers/double/to_null" - "--skip_test=wrappers/bytes/to_null" - "--skip_test=wrappers/string/to_null" -) - -cel_go_skips=( - "--skip_test=dynamic/int32/field_assign_proto2_range,field_assign_proto3_range" - "--skip_test=dynamic/uint32/field_assign_proto2_range,field_assign_proto3_range" - "--skip_test=dynamic/float/field_assign_proto2_range,field_assign_proto3_range" - "--skip_test=enums/legacy_proto2/assign_standalone_int_too_big,assign_standalone_int_too_neg" - "--skip_test=enums/legacy_proto3/assign_standalone_int_too_big,assign_standalone_int_too_neg" - "--skip_test=enums/strong_proto2" - "--skip_test=enums/strong_proto3" - # This conformance test is invalid nowadays - "--skip_test=fields/qualified_identifier_resolution/map_key_float" - # Unclear why the 'to_json_string' is expected to return a string, unlike the preceding to_json_number test. - "--skip_test=wrappers/uint64/to_json_string" - # TODO implement proper "toJson" at some point - "--skip_test=wrappers/field_mask/to_json" - "--skip_test=wrappers/timestamp/to_json" - "--skip_test=wrappers/empty/to_json" -) - -test_files=( - "tests/simple/testdata/basic.textproto" - "tests/simple/testdata/comparisons.textproto" - "tests/simple/testdata/conversions.textproto" - "tests/simple/testdata/dynamic.textproto" - "tests/simple/testdata/enums.textproto" - "tests/simple/testdata/fields.textproto" - "tests/simple/testdata/fp_math.textproto" - "tests/simple/testdata/integer_math.textproto" - "tests/simple/testdata/lists.textproto" - "tests/simple/testdata/logic.textproto" - "tests/simple/testdata/macros.textproto" - "tests/simple/testdata/namespace.textproto" - "tests/simple/testdata/parse.textproto" - "tests/simple/testdata/plumbing.textproto" - "tests/simple/testdata/proto2.textproto" - "tests/simple/testdata/proto3.textproto" - "tests/simple/testdata/string.textproto" - # TODO add when implemnting the string-extensions "tests/simple/testdata/string_ext.textproto" - "tests/simple/testdata/timestamps.textproto" - "tests/simple/testdata/unknowns.textproto" - "tests/simple/testdata/wrappers.textproto" -) - -java -cp ../../conformance/build/libs/*-all.jar \ - org.projectnessie.cel.server.SimpleConformanceTestRunner \ - "${cel_java_skips[@]}" \ - "${cel_go_skips[@]}" \ - "${test_files[@]}" -code=$? - -exit $code diff --git a/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServer.java b/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServer.java deleted file mode 100644 index 952e2ee6..00000000 --- a/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServer.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2021 The Authors of CEL-Java - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.projectnessie.cel.server; - -import io.grpc.Server; -import io.grpc.ServerBuilder; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.util.List; - -public class ConformanceServer implements AutoCloseable { - - private final Server server; - - public ConformanceServer(Server server) { - this.server = server; - } - - public static String getListenHost(Server server) { - List addrs = server.getListenSockets(); - SocketAddress addr = addrs.get(0); - InetSocketAddress ia = (InetSocketAddress) addr; - InetAddress a = ia.getAddress(); - - String host; - if (a instanceof Inet6Address) { - if (a.isAnyLocalAddress()) { - host = "::1"; - } else { - host = a.getCanonicalHostName(); - } - } else { - if (a.isAnyLocalAddress()) { - host = "127.0.0.1"; - } else { - host = a.getCanonicalHostName(); - } - } - - return host; - } - - public void blockUntilShutdown() throws InterruptedException { - server.awaitTermination(); - } - - @Override - public void close() throws Exception { - server.shutdown().awaitTermination(); - } - - public static void main(String[] args) throws Exception { - ConformanceServiceImpl service = new ConformanceServiceImpl(); - - for (String arg : args) { - if ("--verbose".equals(arg) || "-v".equals(arg)) { - service.setVerboseEvalErrors(true); - } - } - - Server c = ServerBuilder.forPort(0).addService(service).build(); - - Thread hook = new Thread(c::shutdown); - - try (ConformanceServer cs = new ConformanceServer(c.start())) { - System.out.printf("Listening on %s:%d%n", getListenHost(cs.server), cs.server.getPort()); - - Runtime.getRuntime().addShutdownHook(hook); - cs.blockUntilShutdown(); - } finally { - try { - Runtime.getRuntime().removeShutdownHook(hook); - } catch (IllegalStateException e) { - // ignore (might happen, when a JVM shutdown is already in progress) - } - } - } -} diff --git a/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServiceImpl.java b/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServiceImpl.java deleted file mode 100644 index 308df848..00000000 --- a/conformance/src/main/java/org/projectnessie/cel/server/ConformanceServiceImpl.java +++ /dev/null @@ -1,424 +0,0 @@ -/* - * Copyright (C) 2021 The Authors of CEL-Java - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.projectnessie.cel.server; - -import static org.projectnessie.cel.CEL.astToCheckedExpr; -import static org.projectnessie.cel.CEL.astToParsedExpr; -import static org.projectnessie.cel.CEL.checkedExprToAst; -import static org.projectnessie.cel.CEL.parsedExprToAst; -import static org.projectnessie.cel.Env.newCustomEnv; -import static org.projectnessie.cel.Env.newEnv; -import static org.projectnessie.cel.EnvOption.clearMacros; -import static org.projectnessie.cel.EnvOption.container; -import static org.projectnessie.cel.EnvOption.declarations; -import static org.projectnessie.cel.EnvOption.types; -import static org.projectnessie.cel.Library.StdLib; -import static org.projectnessie.cel.common.types.BoolT.True; -import static org.projectnessie.cel.common.types.BytesT.bytesOf; -import static org.projectnessie.cel.common.types.DoubleT.doubleOf; -import static org.projectnessie.cel.common.types.Err.isError; -import static org.projectnessie.cel.common.types.Err.newErr; -import static org.projectnessie.cel.common.types.IntT.intOf; -import static org.projectnessie.cel.common.types.StringT.stringOf; -import static org.projectnessie.cel.common.types.TypeT.newObjectTypeValue; -import static org.projectnessie.cel.common.types.Types.boolOf; -import static org.projectnessie.cel.common.types.UintT.uintOf; -import static org.projectnessie.cel.common.types.UnknownT.isUnknown; -import static org.projectnessie.cel.common.types.UnknownT.unknownOf; - -import com.google.api.expr.conformance.v1alpha1.CheckRequest; -import com.google.api.expr.conformance.v1alpha1.CheckResponse; -import com.google.api.expr.conformance.v1alpha1.ConformanceServiceGrpc.ConformanceServiceImplBase; -import com.google.api.expr.conformance.v1alpha1.EvalRequest; -import com.google.api.expr.conformance.v1alpha1.EvalResponse; -import com.google.api.expr.conformance.v1alpha1.IssueDetails; -import com.google.api.expr.conformance.v1alpha1.ParseRequest; -import com.google.api.expr.conformance.v1alpha1.ParseResponse; -import com.google.api.expr.conformance.v1alpha1.SourcePosition; -import com.google.api.expr.v1alpha1.ErrorSet; -import com.google.api.expr.v1alpha1.ExprValue; -import com.google.api.expr.v1alpha1.ListValue; -import com.google.api.expr.v1alpha1.MapValue; -import com.google.api.expr.v1alpha1.MapValue.Entry; -import com.google.api.expr.v1alpha1.UnknownSet; -import com.google.api.expr.v1alpha1.Value; -import com.google.protobuf.Any; -import com.google.protobuf.ByteString; -import com.google.protobuf.Duration; -import com.google.protobuf.Message; -import com.google.protobuf.Timestamp; -import com.google.rpc.Code; -import com.google.rpc.Status; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import org.projectnessie.cel.Ast; -import org.projectnessie.cel.Env; -import org.projectnessie.cel.Env.AstIssuesTuple; -import org.projectnessie.cel.EnvOption; -import org.projectnessie.cel.Program; -import org.projectnessie.cel.Program.EvalResult; -import org.projectnessie.cel.common.CELError; -import org.projectnessie.cel.common.types.Err; -import org.projectnessie.cel.common.types.IteratorT; -import org.projectnessie.cel.common.types.NullT; -import org.projectnessie.cel.common.types.TypeT; -import org.projectnessie.cel.common.types.Types; -import org.projectnessie.cel.common.types.ref.Type; -import org.projectnessie.cel.common.types.ref.TypeAdapter; -import org.projectnessie.cel.common.types.ref.Val; -import org.projectnessie.cel.common.types.traits.Lister; -import org.projectnessie.cel.common.types.traits.Mapper; - -public class ConformanceServiceImpl extends ConformanceServiceImplBase { - - private boolean verboseEvalErrors; - - public void setVerboseEvalErrors(boolean verboseEvalErrors) { - this.verboseEvalErrors = verboseEvalErrors; - } - - @Override - public void parse( - ParseRequest request, io.grpc.stub.StreamObserver responseObserver) { - try { - String sourceText = request.getCelSource(); - if (sourceText.trim().isEmpty()) { - throw new IllegalArgumentException("No source code."); - } - - // NOTE: syntax_version isn't currently used - List parseOptions = new ArrayList<>(); - if (request.getDisableMacros()) { - parseOptions.add(clearMacros()); - } - - Env env = newEnv(parseOptions.toArray(new EnvOption[0])); - AstIssuesTuple astIss = env.parse(sourceText); - - ParseResponse.Builder response = ParseResponse.newBuilder(); - if (!astIss.hasIssues()) { - // Success - response.setParsedExpr(astToParsedExpr(astIss.getAst())); - } else { - // Failure - appendErrors(astIss.getIssues().getErrors(), response::addIssuesBuilder); - } - - responseObserver.onNext(response.build()); - responseObserver.onCompleted(); - } catch (Exception e) { - responseObserver.onError( - io.grpc.Status.fromCode(io.grpc.Status.Code.UNKNOWN) - .withDescription(stacktrace(e)) - .asException()); - } - } - - @Override - public void check( - CheckRequest request, io.grpc.stub.StreamObserver responseObserver) { - try { - // Build the environment. - List checkOptions = new ArrayList<>(); - if (!request.getNoStdEnv()) { - checkOptions.add(StdLib()); - } - - checkOptions.add(container(request.getContainer())); - checkOptions.add(declarations(request.getTypeEnvList())); - checkOptions.add( - types( - dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance(), - dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance())); - Env env = newCustomEnv(checkOptions.toArray(new EnvOption[0])); - - // Check the expression. - AstIssuesTuple astIss = env.check(parsedExprToAst(request.getParsedExpr())); - CheckResponse.Builder resp = CheckResponse.newBuilder(); - - if (!astIss.hasIssues()) { - // Success - resp.setCheckedExpr(astToCheckedExpr(astIss.getAst())); - } else { - // Failure - appendErrors(astIss.getIssues().getErrors(), resp::addIssuesBuilder); - } - - responseObserver.onNext(resp.build()); - responseObserver.onCompleted(); - } catch (Exception e) { - responseObserver.onError( - io.grpc.Status.fromCode(io.grpc.Status.Code.UNKNOWN) - .withDescription(stacktrace(e)) - .asException()); - } - } - - @Override - public void eval( - EvalRequest request, io.grpc.stub.StreamObserver responseObserver) { - try { - Env env = - newEnv( - container(request.getContainer()), - types( - dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance(), - dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance())); - - Program prg; - Ast ast; - - switch (request.getExprKindCase()) { - case PARSED_EXPR: - ast = parsedExprToAst(request.getParsedExpr()); - break; - case CHECKED_EXPR: - ast = checkedExprToAst(request.getCheckedExpr()); - break; - default: - throw new IllegalArgumentException("No expression."); - } - - prg = env.program(ast); - - Map args = new HashMap<>(); - request - .getBindingsMap() - .forEach( - (name, exprValue) -> { - Val refVal = exprValueToRefValue(env.getTypeAdapter(), exprValue); - args.put(name, refVal); - }); - - // NOTE: the EvalState is currently discarded - EvalResult res = prg.eval(args); - ExprValue resultExprVal; - if (!isError(res.getVal())) { - resultExprVal = refValueToExprValue(res.getVal()); - } else { - Err err = (Err) res.getVal(); - - if (verboseEvalErrors) { - System.err.printf( - "%n" + "Eval error (not necessarily a bug!!!):%n" + " error: %s%n" + "%s", - err, err.hasCause() ? (stacktrace(err.toRuntimeException()) + "\n") : ""); - } - - resultExprVal = - ExprValue.newBuilder() - .setError( - ErrorSet.newBuilder().addErrors(Status.newBuilder().setMessage(err.toString()))) - .build(); - } - - EvalResponse.Builder resp = EvalResponse.newBuilder().setResult(resultExprVal); - - responseObserver.onNext(resp.build()); - responseObserver.onCompleted(); - } catch (Exception e) { - responseObserver.onError( - io.grpc.Status.fromCode(io.grpc.Status.Code.UNKNOWN) - .withDescription(stacktrace(e)) - .asException()); - } - } - - static String stacktrace(Throwable t) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - t.printStackTrace(pw); - pw.flush(); - return sw.toString(); - } - - /** - * appendErrors converts the errors from errs to Status messages and appends them to the list of - * issues. - */ - static void appendErrors(List errs, Supplier builderSupplier) { - errs.forEach(e -> errToStatus(e, IssueDetails.Severity.ERROR, builderSupplier.get())); - } - - /** ErrToStatus converts an Error to a Status message with the given severity. */ - static void errToStatus(CELError e, IssueDetails.Severity severity, Status.Builder status) { - IssueDetails.Builder detail = - IssueDetails.newBuilder() - .setSeverity(severity) - .setPosition( - SourcePosition.newBuilder() - .setLine(e.getLocation().line()) - .setColumn(e.getLocation().column()) - .build()); - - status - .setCode(Code.INVALID_ARGUMENT_VALUE) - .setMessage(e.getMessage()) - .addDetails(Any.pack(detail.build())); - } - - /** RefValueToExprValue converts between ref.Val and exprpb.ExprValue. */ - static ExprValue refValueToExprValue(Val res) { - if (isUnknown(res)) { - return ExprValue.newBuilder() - .setUnknown(UnknownSet.newBuilder().addExprs(res.intValue())) - .build(); - } - Value v = refValueToValue(res); - return ExprValue.newBuilder().setValue(v).build(); - } - - // TODO(jimlarson): The following conversion code should be moved to - // common/types/provider.go and consolidated/refactored as appropriate. - // In particular, make judicious use of types.NativeToValue(). - - /** - * RefValueToValue converts between ref.Val and Value. The ref.Val must not be error or unknown. - */ - static Value refValueToValue(Val res) { - switch (res.type().typeEnum()) { - case Bool: - return Value.newBuilder().setBoolValue(res.booleanValue()).build(); - case Bytes: - return Value.newBuilder().setBytesValue(res.convertToNative(ByteString.class)).build(); - case Double: - return Value.newBuilder().setDoubleValue(res.convertToNative(Double.class)).build(); - case Int: - return Value.newBuilder().setInt64Value(res.intValue()).build(); - case Null: - return Value.newBuilder().setNullValueValue(0).build(); - case String: - return Value.newBuilder().setStringValue(res.value().toString()).build(); - case Type: - return Value.newBuilder().setTypeValue(((TypeT) res).typeName()).build(); - case Uint: - return Value.newBuilder().setUint64Value(res.intValue()).build(); - case Duration: - Duration d = res.convertToNative(Duration.class); - return Value.newBuilder().setObjectValue(Any.pack(d)).build(); - case Timestamp: - Timestamp t = res.convertToNative(Timestamp.class); - return Value.newBuilder().setObjectValue(Any.pack(t)).build(); - case List: - Lister l = (Lister) res; - ListValue.Builder elts = ListValue.newBuilder(); - for (IteratorT i = l.iterator(); i.hasNext() == True; ) { - Val v = i.next(); - elts.addValues(refValueToValue(v)); - } - return Value.newBuilder().setListValue(elts).build(); - case Map: - Mapper m = (Mapper) res; - MapValue.Builder elems = MapValue.newBuilder(); - for (IteratorT i = m.iterator(); i.hasNext() == True; ) { - Val k = i.next(); - Val v = m.get(k); - Value kv = refValueToValue(k); - Value vv = refValueToValue(v); - elems.addEntriesBuilder().setKey(kv).setValue(vv); - } - return Value.newBuilder().setMapValue(elems).build(); - case Object: - // Object type - Message pb = (Message) res.value(); - Value.Builder v = Value.newBuilder(); - // Somehow the conformance tests - if (pb instanceof ListValue) { - v.setListValue((ListValue) pb); - } else if (pb instanceof MapValue) { - v.setMapValue((MapValue) pb); - } else { - v.setObjectValue(Any.pack(pb)); - } - return v.build(); - default: - throw new IllegalStateException(String.format("Unknown %s", res.type().typeEnum())); - } - } - - /** ExprValueToRefValue converts between exprpb.ExprValue and ref.Val. */ - static Val exprValueToRefValue(TypeAdapter adapter, ExprValue ev) { - switch (ev.getKindCase()) { - case VALUE: - return valueToRefValue(adapter, ev.getValue()); - case ERROR: - // An error ExprValue is a repeated set of rpcpb.Status - // messages, with no convention for the status details. - // To convert this to a types.Err, we need to convert - // these Status messages to a single string, and be - // able to decompose that string on output so we can - // round-trip arbitrary ExprValue messages. - // TODO(jimlarson) make a convention for this. - return newErr("XXX add details later"); - case UNKNOWN: - return unknownOf(ev.getUnknown().getExprs(0)); - } - throw new IllegalArgumentException("unknown ExprValue kind " + ev.getKindCase()); - } - - /** ValueToRefValue converts between exprpb.Value and ref.Val. */ - static Val valueToRefValue(TypeAdapter adapter, Value v) { - switch (v.getKindCase()) { - case NULL_VALUE: - return NullT.NullValue; - case BOOL_VALUE: - return boolOf(v.getBoolValue()); - case INT64_VALUE: - return intOf(v.getInt64Value()); - case UINT64_VALUE: - return uintOf(v.getUint64Value()); - case DOUBLE_VALUE: - return doubleOf(v.getDoubleValue()); - case STRING_VALUE: - return stringOf(v.getStringValue()); - case BYTES_VALUE: - return bytesOf(v.getBytesValue().toByteArray()); - case OBJECT_VALUE: - Any any = v.getObjectValue(); - return adapter.nativeToValue(any); - case MAP_VALUE: - MapValue m = v.getMapValue(); - Map entries = new HashMap<>(); - for (Entry entry : m.getEntriesList()) { - Val key = valueToRefValue(adapter, entry.getKey()); - Val pb = valueToRefValue(adapter, entry.getValue()); - entries.put(key, pb); - } - return adapter.nativeToValue(entries); - case LIST_VALUE: - ListValue l = v.getListValue(); - List elts = - l.getValuesList().stream() - .map(el -> valueToRefValue(adapter, el)) - .collect(Collectors.toList()); - return adapter.nativeToValue(elts); - case TYPE_VALUE: - String typeName = v.getTypeValue(); - Type tv = Types.getTypeByName(typeName); - if (tv != null) { - return tv; - } - return newObjectTypeValue(typeName); - default: - throw new IllegalArgumentException("unknown value " + v.getKindCase()); - } - } -} diff --git a/conformance/src/main/java/org/projectnessie/cel/server/SimpleConformanceTestRunner.java b/conformance/src/main/java/org/projectnessie/cel/server/SimpleConformanceTestRunner.java deleted file mode 100644 index 41070a14..00000000 --- a/conformance/src/main/java/org/projectnessie/cel/server/SimpleConformanceTestRunner.java +++ /dev/null @@ -1,489 +0,0 @@ -/* - * Copyright (C) 2026 The Authors of CEL-Java - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.projectnessie.cel.server; - -import com.google.api.expr.conformance.v1alpha1.CheckRequest; -import com.google.api.expr.conformance.v1alpha1.CheckResponse; -import com.google.api.expr.conformance.v1alpha1.ConformanceServiceGrpc; -import com.google.api.expr.conformance.v1alpha1.EvalRequest; -import com.google.api.expr.conformance.v1alpha1.EvalResponse; -import com.google.api.expr.conformance.v1alpha1.ParseRequest; -import com.google.api.expr.conformance.v1alpha1.ParseResponse; -import com.google.api.expr.v1alpha1.CheckedExpr; -import com.google.api.expr.v1alpha1.Decl; -import com.google.api.expr.v1alpha1.ErrorSet; -import com.google.api.expr.v1alpha1.ExprValue; -import com.google.api.expr.v1alpha1.MapValue; -import com.google.api.expr.v1alpha1.ParsedExpr; -import com.google.api.expr.v1alpha1.Type; -import com.google.api.expr.v1alpha1.UnknownSet; -import com.google.api.expr.v1alpha1.Value; -import com.google.protobuf.ExtensionRegistry; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.Message; -import com.google.protobuf.TextFormat; -import com.google.protobuf.TypeRegistry; -import dev.cel.expr.conformance.test.SimpleTest; -import dev.cel.expr.conformance.test.SimpleTest.ResultMatcherCase; -import dev.cel.expr.conformance.test.SimpleTestFile; -import dev.cel.expr.conformance.test.SimpleTestSection; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.Server; -import io.grpc.ServerBuilder; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * Local replacement for the upstream CEL-Spec simple conformance runner. - * - *

CEL-Spec v0.25.x ships the simple conformance testdata, but no longer builds the old {@code - * tests/simple/simple_test} binary. The testdata schema moved to {@code dev.cel.expr.*} generated - * classes, while CEL-Java's conformance service still exposes the historical {@code - * com.google.api.expr.v1alpha1} API. The message wire layouts for the fields used by the current - * curated test set are compatible, so this runner converts between the two Java packages by parsing - * serialized protobuf bytes into the corresponding service message type. - */ -public final class SimpleConformanceTestRunner { - - private static final TextFormat.Printer TEXT_PRINTER = - TextFormat.printer().emittingSingleLine(true); - - private final ConformanceServiceGrpc.ConformanceServiceBlockingStub stub; - private final boolean checkedOnly; - private final boolean skipCheck; - private final Set skipTests; - private final List failures = new ArrayList<>(); - private final Set matchedSkips = new HashSet<>(); - private int total; - private int skipped; - private int passed; - - private SimpleConformanceTestRunner( - ConformanceServiceGrpc.ConformanceServiceBlockingStub stub, - boolean checkedOnly, - boolean skipCheck, - Set skipTests) { - this.stub = stub; - this.checkedOnly = checkedOnly; - this.skipCheck = skipCheck; - this.skipTests = skipTests; - } - - public static void main(String[] args) throws Exception { - Arguments arguments = Arguments.parse(args); - if (arguments.testFiles.isEmpty()) { - System.err.println("Usage: SimpleConformanceTestRunner [--skip_test=...] "); - System.exit(2); - } - - Server server = ServerBuilder.forPort(0).addService(new ConformanceServiceImpl()).build(); - ManagedChannel channel = null; - try { - server.start(); - channel = - ManagedChannelBuilder.forAddress( - ConformanceServer.getListenHost(server), server.getPort()) - .usePlaintext() - .build(); - - SimpleConformanceTestRunner runner = - new SimpleConformanceTestRunner( - ConformanceServiceGrpc.newBlockingStub(channel), - arguments.checkedOnly, - arguments.skipCheck, - arguments.skipTests); - int result = runner.run(arguments.testFiles); - System.exit(result); - } finally { - if (channel != null) { - channel.shutdown(); - channel.awaitTermination(30, TimeUnit.SECONDS); - } - server.shutdown(); - server.awaitTermination(30, TimeUnit.SECONDS); - } - } - - private int run(List testFiles) throws IOException { - for (Path testFile : testFiles) { - runFile(parseSimpleFile(testFile)); - } - - skipTests.stream() - .filter(skip -> !matchedSkips.contains(skip)) - .sorted() - .forEach(skip -> failures.add("Skip did not match any test or section: " + skip)); - - System.out.printf( - "Conformance tests: %d total, %d passed, %d skipped, %d failed%n", - total, passed, skipped, failures.size()); - failures.forEach(failure -> System.err.println("FAILED: " + failure)); - return failures.isEmpty() ? 0 : 1; - } - - private static SimpleTestFile parseSimpleFile(Path testFile) throws IOException { - TypeRegistry typeRegistry = - TypeRegistry.newBuilder() - .add(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor()) - .add(dev.cel.expr.conformance.proto2.NestedTestAllTypes.getDescriptor()) - .add(dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor()) - .add(dev.cel.expr.conformance.proto3.NestedTestAllTypes.getDescriptor()) - .build(); - - SimpleTestFile.Builder builder = SimpleTestFile.newBuilder(); - ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); - dev.cel.expr.conformance.proto2.TestAllTypesExtensions.registerAllExtensions(extensionRegistry); - TextFormat.Parser.newBuilder() - .setTypeRegistry(typeRegistry) - .build() - .merge(Files.readString(testFile, StandardCharsets.UTF_8), extensionRegistry, builder); - return builder.build(); - } - - private void runFile(SimpleTestFile file) { - for (SimpleTestSection section : file.getSectionList()) { - String sectionPath = file.getName() + "/" + section.getName(); - if (skipTests.contains(sectionPath)) { - matchedSkips.add(sectionPath); - skipped += section.getTestCount(); - total += section.getTestCount(); - continue; - } - - for (SimpleTest test : section.getTestList()) { - String testPath = sectionPath + "/" + test.getName(); - total++; - if (skipTests.contains(testPath) || (checkedOnly && test.getDisableCheck())) { - skipTests.stream() - .filter(skip -> skip.equals(testPath)) - .findFirst() - .ifPresent(matchedSkips::add); - skipped++; - continue; - } - - try { - runTest(testPath, test); - passed++; - } catch (Throwable e) { - failures.add(testPath + ": " + e.getMessage()); - } - } - } - } - - private void runTest(String testPath, SimpleTest test) throws InvalidProtocolBufferException { - if (test.getName().isEmpty()) { - throw new IllegalArgumentException("simple test has no name"); - } - if (test.getExpr().isEmpty()) { - throw new IllegalArgumentException("test has no expression"); - } - - ParseResponse parseResponse = - stub.parse( - ParseRequest.newBuilder() - .setCelSource(test.getExpr()) - .setSourceLocation(test.getName()) - .setDisableMacros(test.getDisableMacros()) - .build()); - ParsedExpr parsedExpr = parseResponse.getParsedExpr(); - if (!parsedExpr.hasExpr()) { - throw new AssertionError("fatal parse errors: " + parseResponse.getIssuesList()); - } - - CheckedExpr checkedExpr = null; - if (!test.getDisableCheck() && !skipCheck) { - CheckRequest.Builder checkRequest = - CheckRequest.newBuilder().setParsedExpr(parsedExpr).setContainer(test.getContainer()); - for (dev.cel.expr.Decl decl : test.getTypeEnvList()) { - checkRequest.addTypeEnv(convert(decl, Decl.class)); - } - - CheckResponse checkResponse = stub.check(checkRequest.build()); - checkedExpr = checkResponse.getCheckedExpr(); - if (!checkedExpr.hasExpr()) { - throw new AssertionError("fatal check errors: " + checkResponse.getIssuesList()); - } - - if (!checkedExpr.getTypeMapMap().containsKey(parsedExpr.getExpr().getId())) { - throw new AssertionError("no type for top-level expression"); - } - - if (test.getResultMatcherCase() == ResultMatcherCase.TYPED_RESULT) { - Type expectedType = convert(test.getTypedResult().getDeducedType(), Type.class); - Type actualType = checkedExpr.getTypeMapOrThrow(parsedExpr.getExpr().getId()); - if (!actualType.equals(expectedType)) { - throw new AssertionError( - "deduced type mismatch, got " + print(actualType) + ", want " + print(expectedType)); - } - } - } - - if (test.getCheckOnly()) { - return; - } - - runEval( - testPath, - test, - EvalRequest.newBuilder() - .setParsedExpr(parsedExpr) - .setContainer(test.getContainer()) - .putAllBindings(convertBindings(test.getBindingsMap())) - .build()); - - if (checkedExpr != null) { - runEval( - testPath, - test, - EvalRequest.newBuilder() - .setCheckedExpr(checkedExpr) - .setContainer(test.getContainer()) - .putAllBindings(convertBindings(test.getBindingsMap())) - .build()); - } - } - - private void runEval(String testPath, SimpleTest test, EvalRequest request) - throws InvalidProtocolBufferException { - EvalResponse evalResponse = stub.eval(request); - ExprValue actual = evalResponse.getResult(); - if (evalResponse.getIssuesCount() != 0) { - throw new AssertionError("eval issues: " + evalResponse.getIssuesList()); - } - if (actual.getKindCase() == ExprValue.KindCase.KIND_NOT_SET) { - throw new AssertionError("empty eval response"); - } - match(testPath, test, actual); - } - - private static void match(String testPath, SimpleTest test, ExprValue actual) - throws InvalidProtocolBufferException { - switch (test.getResultMatcherCase()) { - case VALUE: - matchValue(testPath, convert(test.getValue(), Value.class), actual); - return; - case TYPED_RESULT: - matchValue(testPath, convert(test.getTypedResult().getResult(), Value.class), actual); - return; - case EVAL_ERROR: - matchError(testPath, convert(test.getEvalError(), ErrorSet.class), actual); - return; - case ANY_EVAL_ERRORS: - if (test.getAnyEvalErrors().getErrorsList().isEmpty() - || actual.getKindCase() != ExprValue.KindCase.ERROR) { - throw new AssertionError("got " + print(actual) + ", want one of several eval errors"); - } - return; - case UNKNOWN: - matchUnknown(testPath, convert(test.getUnknown(), UnknownSet.class), actual); - return; - case ANY_UNKNOWNS: - if (test.getAnyUnknowns().getUnknownsList().isEmpty() - || actual.getKindCase() != ExprValue.KindCase.UNKNOWN) { - throw new AssertionError("got " + print(actual) + ", want one of several unknowns"); - } - return; - case RESULTMATCHER_NOT_SET: - matchValue(testPath, Value.newBuilder().setBoolValue(true).build(), actual); - return; - } - throw new AssertionError("unsupported result matcher " + test.getResultMatcherCase()); - } - - private static void matchValue(String testPath, Value expected, ExprValue actual) { - if (actual.getKindCase() != ExprValue.KindCase.VALUE) { - throw new AssertionError("got " + print(actual) + ", want value " + print(expected)); - } - if (!valuesEqual(expected, actual.getValue())) { - throw new AssertionError( - testPath - + ": eval got [" - + print(actual.getValue()) - + "], want [" - + print(expected) - + "]"); - } - } - - private static void matchError(String testPath, ErrorSet expected, ExprValue actual) { - if (actual.getKindCase() != ExprValue.KindCase.ERROR) { - throw new AssertionError( - testPath + ": got " + print(actual) + ", want error " + print(expected)); - } - } - - private static void matchUnknown(String testPath, UnknownSet expected, ExprValue actual) { - if (actual.getKindCase() != ExprValue.KindCase.UNKNOWN) { - throw new AssertionError( - testPath + ": got " + print(actual) + ", want unknown " + print(expected)); - } - } - - private static boolean valuesEqual(Value expected, Value actual) { - if (expected.getKindCase() != actual.getKindCase()) { - return false; - } - - switch (expected.getKindCase()) { - case DOUBLE_VALUE: - double expectedValue = expected.getDoubleValue(); - double actualValue = actual.getDoubleValue(); - return expectedValue == actualValue - || (Double.isNaN(expectedValue) && Double.isNaN(actualValue)); - case MAP_VALUE: - return mapsEqual(expected.getMapValue(), actual.getMapValue()); - case LIST_VALUE: - if (expected.getListValue().getValuesCount() != actual.getListValue().getValuesCount()) { - return false; - } - for (int i = 0; i < expected.getListValue().getValuesCount(); i++) { - if (!valuesEqual( - expected.getListValue().getValues(i), actual.getListValue().getValues(i))) { - return false; - } - } - return true; - default: - return expected.equals(actual); - } - } - - private static boolean mapsEqual(MapValue expected, MapValue actual) { - if (expected.getEntriesCount() != actual.getEntriesCount()) { - return false; - } - boolean[] matched = new boolean[actual.getEntriesCount()]; - for (MapValue.Entry expectedEntry : expected.getEntriesList()) { - boolean found = false; - for (int i = 0; i < actual.getEntriesCount(); i++) { - if (!matched[i] - && valuesEqual(expectedEntry.getKey(), actual.getEntries(i).getKey()) - && valuesEqual(expectedEntry.getValue(), actual.getEntries(i).getValue())) { - matched[i] = true; - found = true; - break; - } - } - if (!found) { - return false; - } - } - return true; - } - - private static Map convertBindings( - Map bindings) { - try { - java.util.LinkedHashMap converted = new java.util.LinkedHashMap<>(); - for (Map.Entry entry : bindings.entrySet()) { - converted.put(entry.getKey(), convert(entry.getValue(), ExprValue.class)); - } - return converted; - } catch (InvalidProtocolBufferException e) { - throw new IllegalArgumentException("invalid binding value", e); - } - } - - private static T convert(Message message, Class targetType) - throws InvalidProtocolBufferException { - try { - @SuppressWarnings("unchecked") - T converted = - (T) targetType.getMethod("parseFrom", byte[].class).invoke(null, message.toByteArray()); - return converted; - } catch (ReflectiveOperationException e) { - Throwable cause = e.getCause(); - if (cause instanceof InvalidProtocolBufferException) { - throw (InvalidProtocolBufferException) cause; - } - throw new IllegalStateException("cannot convert to " + targetType.getName(), e); - } - } - - private static String print(Message message) { - return TEXT_PRINTER.printToString(message); - } - - private static final class Arguments { - private final boolean checkedOnly; - private final boolean skipCheck; - private final Set skipTests; - private final List testFiles; - - private Arguments( - boolean checkedOnly, boolean skipCheck, Set skipTests, List testFiles) { - this.checkedOnly = checkedOnly; - this.skipCheck = skipCheck; - this.skipTests = skipTests; - this.testFiles = testFiles; - } - - private static Arguments parse(String[] args) { - boolean checkedOnly = false; - boolean skipCheck = false; - Set skipTests = new HashSet<>(); - List testFiles = new ArrayList<>(); - for (String arg : args) { - if ("--checked_only".equals(arg)) { - checkedOnly = true; - } else if ("--skip_check".equals(arg)) { - skipCheck = true; - } else if (arg.startsWith("--skip_test=")) { - parseSkipTest(arg.substring("--skip_test=".length()), skipTests); - } else if (arg.startsWith("--")) { - throw new IllegalArgumentException("unsupported argument: " + arg); - } else { - testFiles.add(Path.of(arg)); - } - } - return new Arguments(checkedOnly, skipCheck, skipTests, testFiles); - } - - private static void parseSkipTest(String value, Set skipTests) { - int fileSeparator = value.indexOf('/'); - if (fileSeparator < 1 || fileSeparator == value.length() - 1) { - throw new IllegalArgumentException( - "skip_test argument must contain at least /

: " + value); - } - - String fileName = value.substring(0, fileSeparator); - String sectionString = value.substring(fileSeparator + 1); - for (String sectionValue : sectionString.split(";")) { - int sectionSeparator = sectionValue.indexOf('/'); - if (sectionSeparator < 0) { - skipTests.add(fileName + "/" + sectionValue); - } else { - String sectionName = sectionValue.substring(0, sectionSeparator); - String testString = sectionValue.substring(sectionSeparator + 1); - for (String test : testString.split(",")) { - skipTests.add(fileName + "/" + sectionName + "/" + test); - } - } - } - } - } -} diff --git a/conformance/src/main/proto/google/api/expr/conformance b/conformance/src/main/proto/google/api/expr/conformance deleted file mode 120000 index 04adab4d..00000000 --- a/conformance/src/main/proto/google/api/expr/conformance +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../submodules/googleapis/google/api/expr/conformance \ No newline at end of file diff --git a/conformance/src/main/proto/google/rpc b/conformance/src/main/proto/google/rpc deleted file mode 120000 index 9b33cdfa..00000000 --- a/conformance/src/main/proto/google/rpc +++ /dev/null @@ -1 +0,0 @@ -../../../../../submodules/googleapis/google/rpc/ \ No newline at end of file diff --git a/conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java b/conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java new file mode 100644 index 00000000..b71ec3f1 --- /dev/null +++ b/conformance/src/test/java/org/projectnessie/cel/conformance/SimpleConformanceTest.java @@ -0,0 +1,705 @@ +/* + * Copyright (C) 2026 The Authors of CEL-Java + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.projectnessie.cel.conformance; + +import static java.util.stream.Collectors.toCollection; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.abort; +import static org.junit.jupiter.api.DynamicContainer.dynamicContainer; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.projectnessie.cel.CEL.astToCheckedExpr; +import static org.projectnessie.cel.CEL.astToParsedExpr; +import static org.projectnessie.cel.CEL.checkedExprToAst; +import static org.projectnessie.cel.CEL.parsedExprToAst; +import static org.projectnessie.cel.Env.newCustomEnv; +import static org.projectnessie.cel.Env.newEnv; +import static org.projectnessie.cel.EnvOption.clearMacros; +import static org.projectnessie.cel.EnvOption.container; +import static org.projectnessie.cel.EnvOption.declarations; +import static org.projectnessie.cel.EnvOption.types; +import static org.projectnessie.cel.Library.StdLib; +import static org.projectnessie.cel.common.types.BoolT.True; +import static org.projectnessie.cel.common.types.BytesT.bytesOf; +import static org.projectnessie.cel.common.types.DoubleT.doubleOf; +import static org.projectnessie.cel.common.types.Err.isError; +import static org.projectnessie.cel.common.types.Err.newErr; +import static org.projectnessie.cel.common.types.IntT.intOf; +import static org.projectnessie.cel.common.types.StringT.stringOf; +import static org.projectnessie.cel.common.types.TypeT.newObjectTypeValue; +import static org.projectnessie.cel.common.types.Types.boolOf; +import static org.projectnessie.cel.common.types.UintT.uintOf; +import static org.projectnessie.cel.common.types.UnknownT.isUnknown; +import static org.projectnessie.cel.common.types.UnknownT.unknownOf; + +import com.google.api.expr.v1alpha1.CheckedExpr; +import com.google.api.expr.v1alpha1.Decl; +import com.google.api.expr.v1alpha1.ErrorSet; +import com.google.api.expr.v1alpha1.ExprValue; +import com.google.api.expr.v1alpha1.ListValue; +import com.google.api.expr.v1alpha1.MapValue; +import com.google.api.expr.v1alpha1.MapValue.Entry; +import com.google.api.expr.v1alpha1.ParsedExpr; +import com.google.api.expr.v1alpha1.Type; +import com.google.api.expr.v1alpha1.UnknownSet; +import com.google.api.expr.v1alpha1.Value; +import com.google.protobuf.Any; +import com.google.protobuf.ByteString; +import com.google.protobuf.Duration; +import com.google.protobuf.ExtensionRegistry; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import com.google.protobuf.TextFormat; +import com.google.protobuf.Timestamp; +import com.google.protobuf.TypeRegistry; +import com.google.rpc.Status; +import dev.cel.expr.conformance.test.SimpleTest; +import dev.cel.expr.conformance.test.SimpleTest.ResultMatcherCase; +import dev.cel.expr.conformance.test.SimpleTestFile; +import dev.cel.expr.conformance.test.SimpleTestSection; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.TestFactory; +import org.projectnessie.cel.Ast; +import org.projectnessie.cel.Env; +import org.projectnessie.cel.Env.AstIssuesTuple; +import org.projectnessie.cel.EnvOption; +import org.projectnessie.cel.Program; +import org.projectnessie.cel.Program.EvalResult; +import org.projectnessie.cel.common.types.Err; +import org.projectnessie.cel.common.types.IteratorT; +import org.projectnessie.cel.common.types.NullT; +import org.projectnessie.cel.common.types.TypeT; +import org.projectnessie.cel.common.types.Types; +import org.projectnessie.cel.common.types.ref.TypeAdapter; +import org.projectnessie.cel.common.types.ref.Val; +import org.projectnessie.cel.common.types.traits.Lister; +import org.projectnessie.cel.common.types.traits.Mapper; + +class SimpleConformanceTest { + + private static final Path TESTDATA_DIR = testdataDir(); + private static final TextFormat.Printer TEXT_PRINTER = + TextFormat.printer().emittingSingleLine(true); + + private static final List TEST_FILES = + List.of( + "basic.textproto", + "comparisons.textproto", + "conversions.textproto", + "dynamic.textproto", + "enums.textproto", + "fields.textproto", + "fp_math.textproto", + "integer_math.textproto", + "lists.textproto", + "logic.textproto", + "macros.textproto", + "namespace.textproto", + "parse.textproto", + "plumbing.textproto", + "proto2.textproto", + "proto3.textproto", + "string.textproto", + "timestamps.textproto", + "unknowns.textproto", + "wrappers.textproto"); + + private static final Set SKIP_TESTS = + SkipList.parse( + // Without the checker, verifying whether an assignment is allowed by the CEL spec is + // difficult, especially from a map to a struct. The checker catches this case, while the + // evaluator currently converts int(1) to string("1"). + "dynamic/struct/field_assign_proto2_bad", + "dynamic/struct/field_assign_proto3_bad", + // The test expects -0.0d, but in Java -0.0d == 0.0d, so -0.0d is evaluated as not set. + "dynamic/float/field_assign_proto3_round_to_zero", + // Malicious too-deep protobuf structure. + "parse/nest/message_literal", + // Proto equality specialties do not seem to be in effect for Java. + "comparisons/eq_wrapper/eq_proto_nan_equal", + "comparisons/ne_literal/ne_proto_nan_not_equal", + // TODO Actual known issue: protobuf Any returned by this test is wrapped twice. + "dynamic/any/var", + // New CEL-Spec v0.25.2 expectations that need follow-up parser/runtime changes. + "conversions/bool/string_1,string_t,string_0,string_f,string_true_badcase,string_false_badcase", + "fields/quoted_map_fields/field_access_slash,field_access_dash,field_access_dot,has_field_slash,has_field_dash,has_field_dot", + "namespace/namespace_shadowing/comprehension_shadowing,comprehension_shadowing_disambiguation,comprehension_shadowing_parse_only,comprehension_shadowing_selector,comprehension_shadowing_selector_parse_only,comprehension_shadowing_namespaced_selector,comprehension_shadowing_namespaced_selector_parse_only,comprehension_shadowing_nesting", + "proto2/set_null/single_message,single_duration,single_timestamp,repeated_field_timestamp_null_pruned,repeated_field_duration_null_pruned,repeated_field_wrapper_null_pruned,map_timestamp_null_pruned,map_duration_null_pruned,map_wrapper_null_pruned,map_anytype_null_retained,single_scalar,repeated,map,list_value,single_struct", + "proto2/quoted_fields/set_field_with_quoted_name,get_field_with_quoted_name", + "proto2/extensions_has/package_scoped_int32,package_scoped_nested_ext,package_scoped_test_all_types_ext,package_scoped_test_all_types_nested_enum_ext,package_scoped_repeated_test_all_types,message_scoped_int64,message_scoped_nested_ext,message_scoped_nested_enum_ext,message_scoped_repeated_test_all_types", + "proto2/extensions_get/package_scoped_int32,package_scoped_nested_ext,package_scoped_test_all_types_ext,package_scoped_test_all_types_nested_enum_ext,package_scoped_repeated_test_all_types,message_scoped_int64,message_scoped_nested_ext,message_scoped_nested_enum_ext,message_scoped_repeated_test_all_types", + "proto3/set_null/single_message,single_duration,single_timestamp,repeated_field_timestamp_null_pruned,repeated_field_duration_null_pruned,repeated_field_wrapper_null_pruned,map_timestamp_null_pruned,map_duration_null_pruned,map_wrapper_null_pruned,map_anytype_null_retained,single_scalar,repeated,map,list_value,single_struct", + "proto3/quoted_fields/set_field,get_field", + "timestamps/timestamp_conversions/type_comparison", + "timestamps/duration_conversions/type_comparison", + "wrappers/bool/to_null", + "wrappers/int32/to_null", + "wrappers/int64/to_null", + "wrappers/uint32/to_null", + "wrappers/uint64/to_null", + "wrappers/float/to_null", + "wrappers/double/to_null", + "wrappers/bytes/to_null", + "wrappers/string/to_null", + "dynamic/int32/field_assign_proto2_range,field_assign_proto3_range", + "dynamic/uint32/field_assign_proto2_range,field_assign_proto3_range", + "dynamic/float/field_assign_proto2_range,field_assign_proto3_range", + "enums/legacy_proto2/assign_standalone_int_too_big,assign_standalone_int_too_neg", + "enums/legacy_proto3/assign_standalone_int_too_big,assign_standalone_int_too_neg", + "enums/strong_proto2", + "enums/strong_proto3", + "fields/qualified_identifier_resolution/map_key_float", + "wrappers/uint64/to_json_string", + "wrappers/field_mask/to_json", + "wrappers/timestamp/to_json", + "wrappers/empty/to_json"); + + private static final Set matchedSkips = new LinkedHashSet<>(); + private static final AtomicInteger total = new AtomicInteger(); + private static final AtomicInteger passed = new AtomicInteger(); + private static final AtomicInteger skipped = new AtomicInteger(); + + @TestFactory + Stream simpleConformance() { + List files = new ArrayList<>(); + TEST_FILES.forEach(fileName -> files.add(dynamicContainer(fileName, fileTests(fileName)))); + files.add(dynamicTest("skip list matches testdata", this::assertAllSkipsMatched)); + return files.stream(); + } + + @AfterAll + static void printSummary() { + System.out.printf( + "Conformance tests: %d total, %d passed, %d skipped%n", + total.get(), passed.get(), skipped.get()); + } + + private Stream fileTests(String fileName) { + SimpleTestFile file; + try { + file = parseSimpleFile(TESTDATA_DIR.resolve(fileName)); + } catch (IOException e) { + throw new IllegalStateException("Cannot parse conformance testdata " + fileName, e); + } + + return file.getSectionList().stream() + .map( + section -> + dynamicContainer( + section.getName(), + section.getTestList().stream() + .map(test -> dynamicTest(test.getName(), () -> run(file, section, test))))); + } + + private void run(SimpleTestFile file, SimpleTestSection section, SimpleTest test) + throws InvalidProtocolBufferException { + total.incrementAndGet(); + String sectionPath = file.getName() + "/" + section.getName(); + String testPath = sectionPath + "/" + test.getName(); + if (SKIP_TESTS.contains(sectionPath)) { + matchedSkips.add(sectionPath); + skipped.incrementAndGet(); + abort("Skipped conformance section " + sectionPath); + } + if (SKIP_TESTS.contains(testPath)) { + matchedSkips.add(testPath); + skipped.incrementAndGet(); + abort("Skipped conformance test " + testPath); + } + + ConformanceCaseRunner.run(testPath, test); + passed.incrementAndGet(); + } + + private void assertAllSkipsMatched() { + Set unmatched = + SKIP_TESTS.stream() + .filter(skip -> !matchedSkips.contains(skip)) + .collect(toCollection(LinkedHashSet::new)); + if (!unmatched.isEmpty()) { + fail("Skip did not match any test or section: " + unmatched); + } + } + + private static SimpleTestFile parseSimpleFile(Path testFile) throws IOException { + TypeRegistry typeRegistry = + TypeRegistry.newBuilder() + .add(dev.cel.expr.conformance.proto2.TestAllTypes.getDescriptor()) + .add(dev.cel.expr.conformance.proto2.NestedTestAllTypes.getDescriptor()) + .add(dev.cel.expr.conformance.proto3.TestAllTypes.getDescriptor()) + .add(dev.cel.expr.conformance.proto3.NestedTestAllTypes.getDescriptor()) + .build(); + + SimpleTestFile.Builder builder = SimpleTestFile.newBuilder(); + ExtensionRegistry extensionRegistry = ExtensionRegistry.newInstance(); + dev.cel.expr.conformance.proto2.TestAllTypesExtensions.registerAllExtensions(extensionRegistry); + TextFormat.Parser.newBuilder() + .setTypeRegistry(typeRegistry) + .build() + .merge(Files.readString(testFile, StandardCharsets.UTF_8), extensionRegistry, builder); + return builder.build(); + } + + private static Path testdataDir() { + Path rootRelative = Path.of("submodules/cel-spec/tests/simple/testdata"); + if (Files.isDirectory(rootRelative)) { + return rootRelative; + } + + Path moduleRelative = Path.of("../submodules/cel-spec/tests/simple/testdata"); + if (Files.isDirectory(moduleRelative)) { + return moduleRelative; + } + + throw new IllegalStateException("Cannot locate CEL-Spec simple conformance testdata"); + } + + private static final class ConformanceCaseRunner { + private ConformanceCaseRunner() {} + + private static void run(String testPath, SimpleTest test) + throws InvalidProtocolBufferException { + if (test.getName().isEmpty()) { + throw new IllegalArgumentException("simple test has no name"); + } + if (test.getExpr().isEmpty()) { + throw new IllegalArgumentException("test has no expression"); + } + + ParsedExpr parsedExpr = ConformanceEvaluator.parse(test); + CheckedExpr checkedExpr = null; + if (!test.getDisableCheck()) { + checkedExpr = ConformanceEvaluator.check(test, parsedExpr); + if (!checkedExpr.getTypeMapMap().containsKey(parsedExpr.getExpr().getId())) { + throw new AssertionError("no type for top-level expression"); + } + if (test.getResultMatcherCase() == ResultMatcherCase.TYPED_RESULT) { + Type expectedType = convert(test.getTypedResult().getDeducedType(), Type.class); + Type actualType = checkedExpr.getTypeMapOrThrow(parsedExpr.getExpr().getId()); + if (!actualType.equals(expectedType)) { + throw new AssertionError( + "deduced type mismatch, got " + + print(actualType) + + ", want " + + print(expectedType)); + } + } + } + + if (test.getCheckOnly()) { + return; + } + + match(testPath, test, ConformanceEvaluator.evalParsed(test, parsedExpr)); + if (checkedExpr != null) { + match(testPath, test, ConformanceEvaluator.evalChecked(test, checkedExpr)); + } + } + } + + private static final class ConformanceEvaluator { + private ConformanceEvaluator() {} + + private static ParsedExpr parse(SimpleTest test) { + String sourceText = test.getExpr(); + if (sourceText.trim().isEmpty()) { + throw new IllegalArgumentException("No source code."); + } + + List parseOptions = new ArrayList<>(); + if (test.getDisableMacros()) { + parseOptions.add(clearMacros()); + } + + Env env = newEnv(parseOptions.toArray(new EnvOption[0])); + AstIssuesTuple astIss = env.parse(sourceText); + if (astIss.hasIssues()) { + throw new AssertionError("fatal parse errors: " + astIss.getIssues().getErrors()); + } + return astToParsedExpr(astIss.getAst()); + } + + private static CheckedExpr check(SimpleTest test, ParsedExpr parsedExpr) + throws InvalidProtocolBufferException { + List typeEnv = new ArrayList<>(); + for (dev.cel.expr.Decl decl : test.getTypeEnvList()) { + typeEnv.add(convert(decl, Decl.class)); + } + + Env env = + newCustomEnv( + StdLib(), + container(test.getContainer()), + declarations(typeEnv), + types( + dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance(), + dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance())); + + AstIssuesTuple astIss = env.check(parsedExprToAst(parsedExpr)); + if (astIss.hasIssues()) { + throw new AssertionError("fatal check errors: " + astIss.getIssues().getErrors()); + } + return astToCheckedExpr(astIss.getAst()); + } + + private static ExprValue evalParsed(SimpleTest test, ParsedExpr parsedExpr) { + return eval(test, parsedExprToAst(parsedExpr)); + } + + private static ExprValue evalChecked(SimpleTest test, CheckedExpr checkedExpr) { + return eval(test, checkedExprToAst(checkedExpr)); + } + + private static ExprValue eval(SimpleTest test, Ast ast) { + Env env = + newEnv( + container(test.getContainer()), + types( + dev.cel.expr.conformance.proto2.TestAllTypes.getDefaultInstance(), + dev.cel.expr.conformance.proto3.TestAllTypes.getDefaultInstance())); + + Program program = env.program(ast); + Map args = new HashMap<>(); + test.getBindingsMap() + .forEach( + (name, exprValue) -> + args.put(name, exprValueToRefValue(env.getTypeAdapter(), exprValue))); + + EvalResult res = program.eval(args); + if (!isError(res.getVal())) { + return refValueToExprValue(res.getVal()); + } + + Err err = (Err) res.getVal(); + return ExprValue.newBuilder() + .setError(ErrorSet.newBuilder().addErrors(Status.newBuilder().setMessage(err.toString()))) + .build(); + } + } + + private static void match(String testPath, SimpleTest test, ExprValue actual) + throws InvalidProtocolBufferException { + switch (test.getResultMatcherCase()) { + case VALUE: + matchValue(testPath, convert(test.getValue(), Value.class), actual); + return; + case TYPED_RESULT: + matchValue(testPath, convert(test.getTypedResult().getResult(), Value.class), actual); + return; + case EVAL_ERROR: + matchError(testPath, convert(test.getEvalError(), ErrorSet.class), actual); + return; + case ANY_EVAL_ERRORS: + if (actual.getKindCase() == ExprValue.KindCase.ERROR) { + return; + } + throw new AssertionError("got " + print(actual) + ", want one of several eval errors"); + case UNKNOWN: + matchUnknown(testPath, convert(test.getUnknown(), UnknownSet.class), actual); + return; + case ANY_UNKNOWNS: + if (actual.getKindCase() == ExprValue.KindCase.UNKNOWN) { + return; + } + throw new AssertionError("got " + print(actual) + ", want one of several unknowns"); + case RESULTMATCHER_NOT_SET: + matchValue(testPath, Value.newBuilder().setBoolValue(true).build(), actual); + return; + } + throw new AssertionError("unsupported result matcher " + test.getResultMatcherCase()); + } + + private static void matchValue(String testPath, Value expected, ExprValue actual) { + if (actual.getKindCase() != ExprValue.KindCase.VALUE) { + throw new AssertionError("got " + print(actual) + ", want value " + print(expected)); + } + if (!valuesEqual(expected, actual.getValue())) { + throw new AssertionError( + testPath + + ": eval got [" + + print(actual.getValue()) + + "], want [" + + print(expected) + + "]"); + } + } + + private static void matchError(String testPath, ErrorSet expected, ExprValue actual) { + if (actual.getKindCase() != ExprValue.KindCase.ERROR) { + throw new AssertionError( + testPath + ": got " + print(actual) + ", want error " + print(expected)); + } + } + + private static void matchUnknown(String testPath, UnknownSet expected, ExprValue actual) { + if (actual.getKindCase() != ExprValue.KindCase.UNKNOWN) { + throw new AssertionError( + testPath + ": got " + print(actual) + ", want unknown " + print(expected)); + } + } + + private static boolean valuesEqual(Value expected, Value actual) { + if (expected.getKindCase() != actual.getKindCase()) { + return false; + } + + switch (expected.getKindCase()) { + case DOUBLE_VALUE: + double expectedValue = expected.getDoubleValue(); + double actualValue = actual.getDoubleValue(); + return expectedValue == actualValue + || (Double.isNaN(expectedValue) && Double.isNaN(actualValue)); + case MAP_VALUE: + return mapsEqual(expected.getMapValue(), actual.getMapValue()); + case LIST_VALUE: + if (expected.getListValue().getValuesCount() != actual.getListValue().getValuesCount()) { + return false; + } + for (int i = 0; i < expected.getListValue().getValuesCount(); i++) { + if (!valuesEqual( + expected.getListValue().getValues(i), actual.getListValue().getValues(i))) { + return false; + } + } + return true; + default: + return expected.equals(actual); + } + } + + private static boolean mapsEqual(MapValue expected, MapValue actual) { + if (expected.getEntriesCount() != actual.getEntriesCount()) { + return false; + } + boolean[] matched = new boolean[actual.getEntriesCount()]; + for (MapValue.Entry expectedEntry : expected.getEntriesList()) { + boolean found = false; + for (int i = 0; i < actual.getEntriesCount(); i++) { + if (!matched[i] + && valuesEqual(expectedEntry.getKey(), actual.getEntries(i).getKey()) + && valuesEqual(expectedEntry.getValue(), actual.getEntries(i).getValue())) { + matched[i] = true; + found = true; + break; + } + } + if (!found) { + return false; + } + } + return true; + } + + private static Val exprValueToRefValue(TypeAdapter adapter, dev.cel.expr.ExprValue ev) { + try { + return exprValueToRefValue(adapter, convert(ev, ExprValue.class)); + } catch (InvalidProtocolBufferException e) { + throw new IllegalArgumentException("invalid expression value", e); + } + } + + private static Val exprValueToRefValue(TypeAdapter adapter, ExprValue ev) { + switch (ev.getKindCase()) { + case VALUE: + return valueToRefValue(adapter, ev.getValue()); + case ERROR: + return newErr("XXX add details later"); + case UNKNOWN: + return unknownOf(ev.getUnknown().getExprs(0)); + default: + throw new IllegalArgumentException("unknown ExprValue kind " + ev.getKindCase()); + } + } + + private static Val valueToRefValue(TypeAdapter adapter, Value v) { + switch (v.getKindCase()) { + case NULL_VALUE: + return NullT.NullValue; + case BOOL_VALUE: + return boolOf(v.getBoolValue()); + case INT64_VALUE: + return intOf(v.getInt64Value()); + case UINT64_VALUE: + return uintOf(v.getUint64Value()); + case DOUBLE_VALUE: + return doubleOf(v.getDoubleValue()); + case STRING_VALUE: + return stringOf(v.getStringValue()); + case BYTES_VALUE: + return bytesOf(v.getBytesValue().toByteArray()); + case OBJECT_VALUE: + return adapter.nativeToValue(v.getObjectValue()); + case MAP_VALUE: + Map entries = new HashMap<>(); + for (Entry entry : v.getMapValue().getEntriesList()) { + entries.put( + valueToRefValue(adapter, entry.getKey()), valueToRefValue(adapter, entry.getValue())); + } + return adapter.nativeToValue(entries); + case LIST_VALUE: + List elements = new ArrayList<>(); + for (Value element : v.getListValue().getValuesList()) { + elements.add(valueToRefValue(adapter, element)); + } + return adapter.nativeToValue(elements); + case TYPE_VALUE: + String typeName = v.getTypeValue(); + org.projectnessie.cel.common.types.ref.Type type = Types.getTypeByName(typeName); + if (type != null) { + return type; + } + return newObjectTypeValue(typeName); + default: + throw new IllegalArgumentException("unknown value " + v.getKindCase()); + } + } + + private static ExprValue refValueToExprValue(Val res) { + if (isUnknown(res)) { + return ExprValue.newBuilder() + .setUnknown(UnknownSet.newBuilder().addExprs(res.intValue())) + .build(); + } + return ExprValue.newBuilder().setValue(refValueToValue(res)).build(); + } + + private static Value refValueToValue(Val res) { + switch (res.type().typeEnum()) { + case Bool: + return Value.newBuilder().setBoolValue(res.booleanValue()).build(); + case Bytes: + return Value.newBuilder().setBytesValue(res.convertToNative(ByteString.class)).build(); + case Double: + return Value.newBuilder().setDoubleValue(res.convertToNative(Double.class)).build(); + case Int: + return Value.newBuilder().setInt64Value(res.intValue()).build(); + case Null: + return Value.newBuilder().setNullValueValue(0).build(); + case String: + return Value.newBuilder().setStringValue(res.value().toString()).build(); + case Type: + return Value.newBuilder().setTypeValue(((TypeT) res).typeName()).build(); + case Uint: + return Value.newBuilder().setUint64Value(res.intValue()).build(); + case Duration: + return Value.newBuilder() + .setObjectValue(Any.pack(res.convertToNative(Duration.class))) + .build(); + case Timestamp: + return Value.newBuilder() + .setObjectValue(Any.pack(res.convertToNative(Timestamp.class))) + .build(); + case List: + Lister lister = (Lister) res; + ListValue.Builder elements = ListValue.newBuilder(); + for (IteratorT i = lister.iterator(); i.hasNext() == True; ) { + elements.addValues(refValueToValue(i.next())); + } + return Value.newBuilder().setListValue(elements).build(); + case Map: + Mapper mapper = (Mapper) res; + MapValue.Builder entries = MapValue.newBuilder(); + for (IteratorT i = mapper.iterator(); i.hasNext() == True; ) { + Val key = i.next(); + entries + .addEntriesBuilder() + .setKey(refValueToValue(key)) + .setValue(refValueToValue(mapper.get(key))); + } + return Value.newBuilder().setMapValue(entries).build(); + case Object: + Message pb = (Message) res.value(); + Value.Builder value = Value.newBuilder(); + if (pb instanceof ListValue) { + value.setListValue((ListValue) pb); + } else if (pb instanceof MapValue) { + value.setMapValue((MapValue) pb); + } else { + value.setObjectValue(Any.pack(pb)); + } + return value.build(); + default: + throw new IllegalStateException(String.format("Unknown %s", res.type().typeEnum())); + } + } + + private static T convert(Message message, Class targetType) + throws InvalidProtocolBufferException { + try { + @SuppressWarnings("unchecked") + T converted = + (T) targetType.getMethod("parseFrom", byte[].class).invoke(null, message.toByteArray()); + return converted; + } catch (ReflectiveOperationException e) { + Throwable cause = e.getCause(); + if (cause instanceof InvalidProtocolBufferException) { + throw (InvalidProtocolBufferException) cause; + } + throw new IllegalStateException("cannot convert to " + targetType.getName(), e); + } + } + + private static String print(Message message) { + return TEXT_PRINTER.printToString(message); + } + + private static final class SkipList { + private SkipList() {} + + private static Set parse(String... values) { + Set skipTests = new LinkedHashSet<>(); + for (String value : values) { + parse(value, skipTests); + } + return Set.copyOf(skipTests); + } + + private static void parse(String value, Set skipTests) { + int fileSeparator = value.indexOf('/'); + if (fileSeparator < 1 || fileSeparator == value.length() - 1) { + throw new IllegalArgumentException( + "skip argument must contain at least /
: " + value); + } + + String fileName = value.substring(0, fileSeparator); + String sectionString = value.substring(fileSeparator + 1); + for (String sectionValue : sectionString.split(";")) { + int sectionSeparator = sectionValue.indexOf('/'); + if (sectionSeparator < 0) { + skipTests.add(fileName + "/" + sectionValue); + } else { + String sectionName = sectionValue.substring(0, sectionSeparator); + String testString = sectionValue.substring(sectionSeparator + 1); + for (String test : testString.split(",")) { + skipTests.add(fileName + "/" + sectionName + "/" + test); + } + } + } + } + } +} diff --git a/conformance/src/test/java/org/projectnessie/cel/server/ConformanceServerTest.java b/conformance/src/test/java/org/projectnessie/cel/server/ConformanceServerTest.java deleted file mode 100644 index ce79883b..00000000 --- a/conformance/src/test/java/org/projectnessie/cel/server/ConformanceServerTest.java +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Copyright (C) 2021 The Authors of CEL-Java - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.projectnessie.cel.server; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.projectnessie.cel.TestExpr.ExprCall; -import static org.projectnessie.cel.TestExpr.ExprLiteral; -import static org.projectnessie.cel.Util.mapOf; - -import com.google.api.expr.conformance.v1alpha1.CheckRequest; -import com.google.api.expr.conformance.v1alpha1.CheckResponse; -import com.google.api.expr.conformance.v1alpha1.ConformanceServiceGrpc; -import com.google.api.expr.conformance.v1alpha1.ConformanceServiceGrpc.ConformanceServiceBlockingStub; -import com.google.api.expr.conformance.v1alpha1.EvalRequest; -import com.google.api.expr.conformance.v1alpha1.EvalResponse; -import com.google.api.expr.conformance.v1alpha1.ParseRequest; -import com.google.api.expr.conformance.v1alpha1.ParseResponse; -import com.google.api.expr.v1alpha1.CheckedExpr; -import com.google.api.expr.v1alpha1.Constant; -import com.google.api.expr.v1alpha1.Constant.ConstantKindCase; -import com.google.api.expr.v1alpha1.Expr; -import com.google.api.expr.v1alpha1.Expr.Call; -import com.google.api.expr.v1alpha1.Expr.ExprKindCase; -import com.google.api.expr.v1alpha1.ExprValue; -import com.google.api.expr.v1alpha1.ExprValue.KindCase; -import com.google.api.expr.v1alpha1.ParsedExpr; -import com.google.api.expr.v1alpha1.SourceInfo; -import com.google.api.expr.v1alpha1.Type; -import com.google.api.expr.v1alpha1.Type.PrimitiveType; -import com.google.api.expr.v1alpha1.Type.TypeKindCase; -import com.google.api.expr.v1alpha1.Value; -import io.grpc.ManagedChannel; -import io.grpc.ManagedChannelBuilder; -import io.grpc.Server; -import io.grpc.ServerBuilder; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.projectnessie.cel.checker.Decls; -import org.projectnessie.cel.common.operators.Operator; - -class ConformanceServerTest { - - private static Server server; - private static ConformanceServiceBlockingStub stub; - private static ManagedChannel channel; - - private static ParsedExpr parsed; - - @BeforeAll - static void startServer() throws Exception { - server = ServerBuilder.forPort(0).addService(new ConformanceServiceImpl()).build(); - server.start(); - - String host = ConformanceServer.getListenHost(server); - - channel = ManagedChannelBuilder.forAddress(host, server.getPort()).usePlaintext().build(); - stub = ConformanceServiceGrpc.newBlockingStub(channel); - - parsed = - ParsedExpr.newBuilder() - .setExpr(ExprCall(1, Operator.Add.id, ExprLiteral(2, 1L), ExprLiteral(3, 1L))) - .setSourceInfo( - SourceInfo.newBuilder() - .setLocation("the location") - .putAllPositions( - mapOf( - 1L, 0, - 2L, 0, - 3L, 4)) - .build()) - .build(); - } - - @AfterAll - static void stopServer() throws Exception { - Exception x = null; - try { - channel.shutdown(); - } catch (Exception e) { - x = e; - } - - try { - server.shutdown(); - } catch (Exception e) { - if (x == null) x = e; - else x.addSuppressed(e); - } - - try { - channel.awaitTermination(30, TimeUnit.SECONDS); - } catch (Exception e) { - if (x == null) x = e; - else x.addSuppressed(e); - } - - try { - server.awaitTermination(30, TimeUnit.SECONDS); - } catch (Exception e) { - if (x == null) x = e; - else x.addSuppressed(e); - } - - if (x != null) throw x; - } - - /** TestParse tests the Parse method. */ - @Test - void Parse() { - ParseRequest req = ParseRequest.newBuilder().setCelSource("1 + 1").build(); - ParseResponse res = stub.parse(req); - assertThat(res.isInitialized()).isTrue(); - assertThat(res.getParsedExpr().isInitialized()).isTrue(); - // Could check against 'parsed' above, - // but the expression ids are arbitrary, - // and explicit comparison logic is about as - // much work as normalization would be. - assertThat(res.getParsedExpr().getExpr().isInitialized()).isTrue(); - assertThat(res.getParsedExpr().getExpr().getExprKindCase()).isSameAs(ExprKindCase.CALL_EXPR); - - Call c = res.getParsedExpr().getExpr().getCallExpr(); - assertThat(c.getTarget().isInitialized()).isTrue(); - assertThat(c.getFunction()).isEqualTo("_+_"); - assertThat(c.getArgsCount()).isEqualTo(2); - for (Expr a : c.getArgsList()) { - assertThat(a.getExprKindCase()).isSameAs(ExprKindCase.CONST_EXPR); - Constant l = a.getConstExpr(); - assertThat(l.getConstantKindCase()).isSameAs(ConstantKindCase.INT64_VALUE); - assertThat(l.getInt64Value()).isEqualTo(1); - } - } - - /** TestCheck tests the Check method. */ - @Test - void Check() { - // If TestParse() passes, it validates a good chunk - // of the server mechanisms for data conversion, so we - // won't be as fussy here.. - CheckRequest req = CheckRequest.newBuilder().setParsedExpr(parsed).build(); - CheckResponse res = stub.check(req); - assertThat(res.isInitialized()).isTrue(); - assertThat(res.getCheckedExpr().isInitialized()).isTrue(); - Type tp = res.getCheckedExpr().getTypeMapMap().get(1L); - assertThat(tp).isNotNull(); - assertThat(tp.getTypeKindCase()).isSameAs(TypeKindCase.PRIMITIVE); - assertThat(tp.getPrimitive()).isSameAs(PrimitiveType.INT64); - } - - /** TestEval tests the Eval method. */ - @Test - void Eval() { - EvalRequest req = EvalRequest.newBuilder().setParsedExpr(parsed).build(); - EvalResponse res = stub.eval(req); - assertThat(res.isInitialized()).isTrue(); - assertThat(res.getResult().isInitialized()).isTrue(); - - assertThat(res.getResult().getKindCase()).isSameAs(KindCase.VALUE); - assertThat(res.getResult().getValue().getKindCase()).isSameAs(Value.KindCase.INT64_VALUE); - assertThat(res.getResult().getValue().getInt64Value()).isEqualTo(2L); - } - - /** TestFullUp tests Parse, Check, and Eval back-to-back. */ - @Test - void FullUp() { - ParseRequest preq = ParseRequest.newBuilder().setCelSource("x + y").build(); - ParseResponse pres = stub.parse(preq); - assertThat(pres.isInitialized()).isTrue(); - ParsedExpr parsedExpr = pres.getParsedExpr(); - assertThat(parsedExpr.isInitialized()).isTrue(); - - CheckRequest creq = - CheckRequest.newBuilder() - .setParsedExpr(parsedExpr) - .addTypeEnv(Decls.newVar("x", Decls.Int)) - .addTypeEnv(Decls.newVar("y", Decls.Int)) - .build(); - CheckResponse cres = stub.check(creq); - assertThat(cres.isInitialized()).isTrue(); - CheckedExpr checkedExpr = cres.getCheckedExpr(); - assertThat(checkedExpr.isInitialized()).isTrue(); - - Type tp = checkedExpr.getTypeMapMap().get(1L); - assertThat(tp).isNotNull(); - assertThat(tp.getTypeKindCase()).isSameAs(TypeKindCase.PRIMITIVE); - assertThat(tp.getPrimitive()).isSameAs(PrimitiveType.INT64); - - EvalRequest ereq = - EvalRequest.newBuilder() - .setCheckedExpr(checkedExpr) - .putBindings("x", exprValueInt64(1)) - .putBindings("y", exprValueInt64(2)) - .build(); - EvalResponse eres = stub.eval(ereq); - assertThat(eres.isInitialized()).isTrue(); - assertThat(eres.getResult().isInitialized()).isTrue(); - - assertThat(eres.getResult().getKindCase()).isSameAs(KindCase.VALUE); - assertThat(eres.getResult().getValue().getKindCase()).isSameAs(Value.KindCase.INT64_VALUE); - assertThat(eres.getResult().getValue().getInt64Value()).isEqualTo(3L); - } - - static ExprValue exprValueInt64(long x) { - return ExprValue.newBuilder().setValue(Value.newBuilder().setInt64Value(x)).build(); - } - - static class FullPipelineResult { - final ParseResponse parseResponse; - final CheckResponse checkResponse; - final EvalResponse evalResponse; - - FullPipelineResult( - ParseResponse parseResponse, CheckResponse checkResponse, EvalResponse evalResponse) { - this.parseResponse = parseResponse; - this.checkResponse = checkResponse; - this.evalResponse = evalResponse; - } - } - - /** - * fullPipeline parses, checks, and evaluates the CEL expression in source and returns the result - * from the Eval call. - */ - FullPipelineResult fullPipeline(String source) { - - // Parse - ParseRequest preq = ParseRequest.newBuilder().setCelSource(source).build(); - ParseResponse pres = stub.parse(preq); - assertThat(pres.isInitialized()).isTrue(); - ParsedExpr parsedExpr = pres.getParsedExpr(); - assertThat(parsedExpr.isInitialized()).isTrue(); - assertThat(parsedExpr.getExpr().isInitialized()).isTrue(); - - // Check - CheckRequest creq = CheckRequest.newBuilder().setParsedExpr(parsedExpr).build(); - CheckResponse cres = stub.check(creq); - assertThat(cres.isInitialized()).isTrue(); - CheckedExpr checkedExpr = cres.getCheckedExpr(); - assertThat(checkedExpr.isInitialized()).isTrue(); - - // Eval - EvalRequest ereq = EvalRequest.newBuilder().setCheckedExpr(checkedExpr).build(); - EvalResponse eres = stub.eval(ereq); - assertThat(eres.isInitialized()).isTrue(); - assertThat(eres.getResult().isInitialized()).isTrue(); - - return new FullPipelineResult(pres, cres, eres); - } - - /** - * expectEvalTrue parses, checks, and evaluates the CEL expression in source and checks that the - * result is the boolean value 'true'. - */ - void expectEvalTrue(String source) { - FullPipelineResult fp = fullPipeline(source); - - long rootID = fp.parseResponse.getParsedExpr().getExpr().getId(); - Type topType = fp.checkResponse.getCheckedExpr().getTypeMapMap().get(rootID); - assertThat(topType).extracting(Type::getTypeKindCase).isEqualTo(Type.TypeKindCase.PRIMITIVE); - assertThat(topType).extracting(Type::getPrimitive).isEqualTo(Type.PrimitiveType.BOOL); - - ExprValue er = fp.evalResponse.getResult(); - assertThat(er).extracting(ExprValue::getKindCase).isEqualTo(ExprValue.KindCase.VALUE); - Value ev = er.getValue(); - assertThat(ev).extracting(Value::getKindCase).isEqualTo(Value.KindCase.BOOL_VALUE); - assertThat(ev).extracting(Value::getBoolValue).isEqualTo(true); - } - - /** TestCondTrue tests true conditional behavior. */ - @Test - void CondTrue() { - expectEvalTrue("(true ? 'a' : 'b') == 'a'"); - } - - /** TestCondFalse tests false conditional behavior. */ - @Test - void CondFalse() { - expectEvalTrue("(false ? 'a' : 'b') == 'b'"); - } - - /** TestMapOrderInsignificant tests that maps with different order are equal. */ - @Test - void MapOrderInsignificant() { - expectEvalTrue("{1: 'a', 2: 'b'} == {2: 'b', 1: 'a'}"); - } - - /** FailsTestOneMetaType tests that types of different types are equal. */ - @Test - void FailsTestOneMetaType() { - expectEvalTrue("type(type(1)) == type(type('foo'))"); - } - - /** FailsTestTypeType tests that the meta-type is its own type. */ - @Test - void FailsTestTypeType() { - expectEvalTrue("type(type) == type"); - } - - /** FailsTestNullTypeName checks that the type of null is "null_type". */ - @Test - void FailsTestNullTypeName() { - expectEvalTrue("type(null) == null_type"); - } - - /** TestError ensures that errors are properly transmitted. */ - @Test - void Error() { - FullPipelineResult fp = fullPipeline("1 / 0"); - assertThat(fp.evalResponse.getResult().getKindCase()).isEqualTo(ExprValue.KindCase.ERROR); - } -}