From 88a12981534d0b6c281ef8b0e26841ebad96a46d Mon Sep 17 00:00:00 2001 From: CEL Dev Team Date: Mon, 15 Sep 2025 00:08:34 -0700 Subject: [PATCH] Support checked_expression, raw cel_expressions and cel files via bzl macro PiperOrigin-RevId: 807110591 --- testing/testrunner/BUILD | 3 + testing/testrunner/cel_cc_test.bzl | 47 +++++++- testing/testrunner/cel_test_factories.h | 3 +- testing/testrunner/resources/BUILD | 5 +- .../subtraction_checked_expr.textproto | 42 +++++++ testing/testrunner/runner_bin.cc | 95 ++++++++++++---- testing/testrunner/user_tests/BUILD | 56 ++++++++++ .../testrunner/user_tests/flexible_test.cc | 104 ++++++++++++++++++ .../user_tests/raw_expression_test.cc | 3 +- testing/testrunner/user_tests/simple.cc | 3 +- 10 files changed, 336 insertions(+), 25 deletions(-) create mode 100644 testing/testrunner/resources/subtraction_checked_expr.textproto create mode 100644 testing/testrunner/user_tests/flexible_test.cc diff --git a/testing/testrunner/BUILD b/testing/testrunner/BUILD index 975b5884d..047736b8b 100644 --- a/testing/testrunner/BUILD +++ b/testing/testrunner/BUILD @@ -113,9 +113,11 @@ cc_library( name = "runner", srcs = ["runner_bin.cc"], deps = [ + ":cel_expression_source", ":cel_test_context", ":cel_test_factories", ":runner_lib", + "//internal:status_macros", "//internal:testing_no_main", "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/log:absl_check", @@ -123,6 +125,7 @@ cc_library( "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", + "@com_google_cel_spec//proto/cel/expr:checked_cc_proto", "@com_google_cel_spec//proto/cel/expr/conformance/test:suite_cc_proto", "@com_google_protobuf//:protobuf", "@com_google_protobuf//src/google/protobuf/io", diff --git a/testing/testrunner/cel_cc_test.bzl b/testing/testrunner/cel_cc_test.bzl index 07581ce3c..33c6bbcf4 100644 --- a/testing/testrunner/cel_cc_test.bzl +++ b/testing/testrunner/cel_cc_test.bzl @@ -19,6 +19,8 @@ load("@rules_cc//cc:cc_test.bzl", "cc_test") def cel_cc_test( name, test_suite = "", + cel_expr = "", + is_raw_expr = False, filegroup = "", deps = [], test_data_path = "", @@ -32,6 +34,10 @@ def cel_cc_test( name: str name for the generated artifact test_suite: str label of a file containing a test suite. The file should have a .textproto extension. + cel_expr: The CEL expression source. The meaning of this argument depends on `is_raw_expr`. + is_raw_expr: bool whether the cel_expr is a raw expression string. If False, + cel_expr is treated as a file path. The file type (.cel or .textproto) + is inferred from the extension. filegroup: str label of a filegroup containing the test suite, the config and the checked expression. deps: list of dependencies for the cc_test rule. @@ -39,7 +45,14 @@ def cel_cc_test( test_data_path: absolute path of the directory containing the test files. This is needed only if the test files are not located in the same directory as the BUILD file. """ - data, test_data_path = _update_data_with_test_files(data, filegroup, test_data_path, test_suite) + data, test_data_path = _update_data_with_test_files( + data, + filegroup, + test_data_path, + test_suite, + cel_expr, + is_raw_expr, + ) args = [] test_data_path = test_data_path.lstrip("/") @@ -48,6 +61,24 @@ def cel_cc_test( test_suite = test_data_path + "/" + test_suite args.append("--test_suite_path=" + test_suite) + if cel_expr != "": + expression_kind = "" + cel_expr_value = "" + if is_raw_expr: + expression_kind = "raw" + cel_expr_value = "\"" + cel_expr + "\"" + else: + _, ext = _split_extension(cel_expr) + resolved_path = test_data_path + "/" + cel_expr + if ext == ".cel": + expression_kind = "file" + else: + expression_kind = "checked" + cel_expr_value = resolved_path + + args.append("--expression_kind=" + expression_kind) + args.append("--cel_expr_value=" + cel_expr_value) + cc_test( name = name, data = data, @@ -55,7 +86,15 @@ def cel_cc_test( deps = ["//testing/testrunner:runner"] + deps, ) -def _update_data_with_test_files(data, filegroup, test_data_path, test_suite): +def _split_extension(path): + """Extracts the file extension from a path string.""" + + parts = path.rsplit(".", 1) + if len(parts) == 1: + return path, "" + return parts[0], "." + parts[1] + +def _update_data_with_test_files(data, filegroup, test_data_path, test_suite, cel_expr, is_raw_expr): """Updates the data with the test files.""" if filegroup != "": @@ -63,8 +102,12 @@ def _update_data_with_test_files(data, filegroup, test_data_path, test_suite): elif test_data_path != "" and test_data_path != native.package_name(): if test_suite != "": data = data + [test_data_path + ":" + test_suite] + if cel_expr != "" and not is_raw_expr: + data = data + [test_data_path + ":" + cel_expr] else: test_data_path = native.package_name() if test_suite != "": data = data + [test_suite] + if cel_expr != "" and not is_raw_expr: + data = data + [cel_expr] return data, test_data_path diff --git a/testing/testrunner/cel_test_factories.h b/testing/testrunner/cel_test_factories.h index 61058be13..bde4f7e77 100644 --- a/testing/testrunner/cel_test_factories.h +++ b/testing/testrunner/cel_test_factories.h @@ -28,7 +28,8 @@ namespace cel::test { namespace internal { using CelTestContextFactoryFn = - std::function>()>; + std::function>( + CelTestContextOptions options)>; using CelTestSuiteFactoryFn = std::function; diff --git a/testing/testrunner/resources/BUILD b/testing/testrunner/resources/BUILD index 663f81780..6572b67f8 100644 --- a/testing/testrunner/resources/BUILD +++ b/testing/testrunner/resources/BUILD @@ -1,7 +1,10 @@ package(default_visibility = ["//visibility:public"]) exports_files( - ["test.cel"], + [ + "test.cel", + "subtraction_checked_expr.textproto", + ], ) filegroup( diff --git a/testing/testrunner/resources/subtraction_checked_expr.textproto b/testing/testrunner/resources/subtraction_checked_expr.textproto new file mode 100644 index 000000000..660aedf0e --- /dev/null +++ b/testing/testrunner/resources/subtraction_checked_expr.textproto @@ -0,0 +1,42 @@ +# proto-file: google3/google/api/expr/checked.proto +# proto-message: google.api.expr.CheckedExpr + +# CEL expression: x - y (where x is Int64, y is Int64) + +expr { + id: 1 + call_expr { + function: "_-_" + args { + id: 2 + ident_expr { + name: "x" + } + } + args { + id: 3 + ident_expr { + name: "y" + } + } + } +} +# Type information confirming the final result is an Int64 +type_map { + key: 1 + value { + primitive: INT64 + } +} +type_map { + key: 2 + value { + primitive: INT64 + } +} +type_map { + key: 3 + value { + primitive: INT64 + } +} diff --git a/testing/testrunner/runner_bin.cc b/testing/testrunner/runner_bin.cc index a307d45c0..6f3604c77 100644 --- a/testing/testrunner/runner_bin.cc +++ b/testing/testrunner/runner_bin.cc @@ -16,34 +16,44 @@ // written in the CEL test suite format. #include #include -#include #include +#include #include -#include #include +#include "cel/expr/checked.pb.h" #include "absl/flags/flag.h" #include "absl/log/absl_check.h" #include "absl/log/absl_log.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "internal/status_macros.h" #include "internal/testing.h" +#include "testing/testrunner/cel_expression_source.h" #include "testing/testrunner/cel_test_context.h" #include "testing/testrunner/cel_test_factories.h" #include "testing/testrunner/runner_lib.h" #include "cel/expr/conformance/test/suite.pb.h" -#include "google/protobuf/io/zero_copy_stream_impl.h" #include "google/protobuf/text_format.h" ABSL_FLAG(std::string, test_suite_path, "", "The path to the file containing the test suite to run."); +ABSL_FLAG(std::string, expression_kind, "", + "The kind of expression source: 'raw', 'file', or 'checked'."); +ABSL_FLAG(std::string, cel_expr_value, "", + "The value of the CEL expression source. For 'raw', it's the " + "expression string. For 'file' and 'checked', it's the file path."); namespace { using ::cel::expr::conformance::test::TestCase; using ::cel::expr::conformance::test::TestSuite; +using ::cel::test::CelExpressionSource; +using ::cel::test::CelTestContextOptions; using ::cel::test::TestRunner; +using ::cel::expr::CheckedExpr; class CelTest : public testing::Test { public: @@ -73,21 +83,37 @@ absl::Status RegisterTests(const TestSuite& test_suite, return absl::OkStatus(); } -TestSuite ReadTestSuiteFromPath(std::string_view test_suite_path) { - TestSuite test_suite; - { - std::ifstream in; - in.open(std::string(test_suite_path), - std::ios_base::in | std::ios_base::binary); - if (!in.is_open()) { - ABSL_LOG(FATAL) << "failed to open file: " << test_suite_path; - } - google::protobuf::io::IstreamInputStream stream(&in); - if (!google::protobuf::TextFormat::Parse(&stream, &test_suite)) { - ABSL_LOG(FATAL) << "failed to parse file: " << test_suite_path; - } +absl::StatusOr ReadFileToString(absl::string_view file_path) { + std::ifstream file_stream{std::string(file_path)}; + if (!file_stream.is_open()) { + return absl::NotFoundError( + absl::StrCat("Unable to open file: ", file_path)); + } + std::stringstream buffer; + buffer << file_stream.rdbuf(); + return buffer.str(); +} + +template +absl::StatusOr ReadTextProtoFromFile(absl::string_view file_path) { + CEL_ASSIGN_OR_RETURN(std::string contents, ReadFileToString(file_path)); + T message; + if (!google::protobuf::TextFormat::ParseFromString(contents, &message)) { + return absl::InternalError(absl::StrCat( + "Failed to parse text-format proto from file: ", file_path)); + } + return message; +} + +TestSuite ReadTestSuiteFromPath(absl::string_view test_suite_path) { + absl::StatusOr test_suite_or = + ReadTextProtoFromFile(test_suite_path); + + if (!test_suite_or.ok()) { + ABSL_LOG(FATAL) << "Failed to load test suite from " << test_suite_path + << ": " << test_suite_or.status(); } - return test_suite; + return *std::move(test_suite_or); } TestSuite GetTestSuite() { @@ -108,15 +134,46 @@ TestSuite GetTestSuite() { } return test_suite_factory(); } + +absl::StatusOr GetExpressionSourceOptions() { + CelTestContextOptions options; + if (absl::GetFlag(FLAGS_expression_kind).empty()) { + return options; + } + + std::string kind = absl::GetFlag(FLAGS_expression_kind); + std::string value = absl::GetFlag(FLAGS_cel_expr_value); + + if (kind == "raw") { + options.expression_source = CelExpressionSource::FromRawExpression(value); + } else if (kind == "file") { + options.expression_source = CelExpressionSource::FromCelFile(value); + } else if (kind == "checked") { + CEL_ASSIGN_OR_RETURN(CheckedExpr checked_expr, + ReadTextProtoFromFile(value)); + options.expression_source = + CelExpressionSource::FromCheckedExpr(std::move(checked_expr)); + } else { + ABSL_LOG(FATAL) << "Unknown expression kind: " << kind; + } + return options; +} + } // namespace int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); - + absl::StatusOr test_context_options = + GetExpressionSourceOptions(); + if (!test_context_options.ok()) { + ABSL_LOG(FATAL) << "Failed to get expression source options: " + << test_context_options.status(); + } // Create a test context using the factory function returned by the global // factory function provider which was initialized by the user. absl::StatusOr> cel_test_context = - cel::test::internal::GetCelTestContextFactory()(); + cel::test::internal::GetCelTestContextFactory()( + std::move(*test_context_options)); if (!cel_test_context.ok()) { ABSL_LOG(FATAL) << "Failed to create CEL test context: " << cel_test_context.status(); diff --git a/testing/testrunner/user_tests/BUILD b/testing/testrunner/user_tests/BUILD index 436176f1c..aa445a3dc 100644 --- a/testing/testrunner/user_tests/BUILD +++ b/testing/testrunner/user_tests/BUILD @@ -61,6 +61,35 @@ cc_library( ], ) +cc_library( + name = "flexible_user_test", + testonly = True, + srcs = ["flexible_test.cc"], + deps = [ + "//checker:type_checker_builder", + "//common:decl", + "//common:type", + "//compiler", + "//compiler:compiler_factory", + "//compiler:standard_library", + "//internal:status_macros", + "//internal:testing_descriptor_pool", + "//runtime", + "//runtime:runtime_builder", + "//runtime:standard_runtime_builder_factory", + "//testing/testrunner:cel_test_context", + "//testing/testrunner:cel_test_factories", + "@com_google_absl//absl/log:absl_check", + "@com_google_absl//absl/log:absl_log", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings:string_view", + "@com_google_cel_spec//proto/cel/expr:checked_cc_proto", + "@com_google_cel_spec//proto/cel/expr/conformance/test:suite_cc_proto", + "@com_google_protobuf//:protobuf", + ], + alwayslink = True, +) + cel_cc_test( name = "simple_test", filegroup = "//testing/testrunner/resources", @@ -86,3 +115,30 @@ cel_cc_test( ":raw_expression_user_test", ], ) + +cel_cc_test( + name = "subtraction_checked_expr_test", + cel_expr = "subtraction_checked_expr.textproto", + test_data_path = "//testing/testrunner/resources", + deps = [ + ":flexible_user_test", + ], +) + +cel_cc_test( + name = "subtraction_raw_expr_test", + cel_expr = "x - y", + is_raw_expr = True, + deps = [ + ":flexible_user_test", + ], +) + +cel_cc_test( + name = "subtraction_cel_file_test", + cel_expr = "test.cel", + test_data_path = "//testing/testrunner/resources", + deps = [ + ":flexible_user_test", + ], +) diff --git a/testing/testrunner/user_tests/flexible_test.cc b/testing/testrunner/user_tests/flexible_test.cc new file mode 100644 index 000000000..48230e8cd --- /dev/null +++ b/testing/testrunner/user_tests/flexible_test.cc @@ -0,0 +1,104 @@ +// Copyright 2025 Google LLC +// +// 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 +// +// https://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. + +#include +#include + +#include "absl/log/absl_check.h" +#include "absl/log/absl_log.h" +#include "absl/status/statusor.h" +#include "absl/strings/string_view.h" +#include "checker/type_checker_builder.h" +#include "common/decl.h" +#include "common/type.h" +#include "compiler/compiler.h" +#include "compiler/compiler_factory.h" +#include "compiler/standard_library.h" +#include "internal/status_macros.h" +#include "internal/testing_descriptor_pool.h" +#include "runtime/runtime.h" +#include "runtime/runtime_builder.h" +#include "runtime/standard_runtime_builder_factory.h" +#include "testing/testrunner/cel_test_context.h" +#include "testing/testrunner/cel_test_factories.h" +#include "cel/expr/conformance/test/suite.pb.h" +#include "google/protobuf/text_format.h" + +namespace cel::testing { + +using ::cel::test::CelTestContext; +using ::cel::test::CelTestContextOptions; + +template +T ParseTextProtoOrDie(absl::string_view text_proto) { + T result; + ABSL_CHECK(google::protobuf::TextFormat::ParseFromString(text_proto, &result)); + return result; +} + +CEL_REGISTER_TEST_SUITE_FACTORY([]() { + return ParseTextProtoOrDie(R"pb( + name: "cli_expression_tests" + description: "Tests designed for expressions passed via CLI flags." + sections: { + name: "subtraction_test" + description: "Tests subtraction of two variables." + tests: { + name: "variable_subtraction" + description: "Test that subtraction of two variables works." + input: { + key: "x" + value { value { int64_value: 10 } } + } + input { + key: "y" + value { value { int64_value: 5 } } + } + output { result_value { int64_value: 5 } } + } + } + )pb"); +}); + +CEL_REGISTER_TEST_CONTEXT_FACTORY( + [](CelTestContextOptions options) + -> absl::StatusOr> { + ABSL_LOG(INFO) << "Creating flexible test context"; + + // Create a compiler. + CEL_ASSIGN_OR_RETURN( + std::unique_ptr builder, + cel::NewCompilerBuilder(cel::internal::GetTestingDescriptorPool())); + CEL_RETURN_IF_ERROR(builder->AddLibrary(cel::StandardCompilerLibrary())); + cel::TypeCheckerBuilder& checker_builder = builder->GetCheckerBuilder(); + CEL_RETURN_IF_ERROR(checker_builder.AddVariable( + cel::MakeVariableDecl("x", cel::IntType()))); + CEL_RETURN_IF_ERROR(checker_builder.AddVariable( + cel::MakeVariableDecl("y", cel::IntType()))); + + CEL_ASSIGN_OR_RETURN(std::unique_ptr compiler, + std::move(builder)->Build()); + options.compiler = std::move(compiler); + + // Create a runtime. + CEL_ASSIGN_OR_RETURN(cel::RuntimeBuilder runtime_builder, + cel::CreateStandardRuntimeBuilder( + cel::internal::GetTestingDescriptorPool(), {})); + CEL_ASSIGN_OR_RETURN(std::unique_ptr runtime, + std::move(runtime_builder).Build()); + + return CelTestContext::CreateFromRuntime(std::move(runtime), + std::move(options)); + }); +} // namespace cel::testing diff --git a/testing/testrunner/user_tests/raw_expression_test.cc b/testing/testrunner/user_tests/raw_expression_test.cc index 333ba66a1..4b1749626 100644 --- a/testing/testrunner/user_tests/raw_expression_test.cc +++ b/testing/testrunner/user_tests/raw_expression_test.cc @@ -72,7 +72,8 @@ CEL_REGISTER_TEST_SUITE_FACTORY([]() { }); CEL_REGISTER_TEST_CONTEXT_FACTORY( - []() -> absl::StatusOr> { + [](cel::test::CelTestContextOptions options) + -> absl::StatusOr> { // Create a compiler. CEL_ASSIGN_OR_RETURN( std::unique_ptr builder, diff --git a/testing/testrunner/user_tests/simple.cc b/testing/testrunner/user_tests/simple.cc index e199f6d17..11b3d55ca 100644 --- a/testing/testrunner/user_tests/simple.cc +++ b/testing/testrunner/user_tests/simple.cc @@ -88,7 +88,8 @@ CEL_REGISTER_TEST_SUITE_FACTORY([]() { }); CEL_REGISTER_TEST_CONTEXT_FACTORY( - []() -> absl::StatusOr> { + [](cel::test::CelTestContextOptions options) + -> absl::StatusOr> { ABSL_LOG(INFO) << "Creating test context"; // Create a compiler.