Support multiple test sources, `test_classes`, and `non_test_srcs` in `java_test` PiperOrigin-RevId: 985918418 Change-Id: I9600eab4e0221cb6f6ba66be80d7f4d68ccdf160
diff --git a/java/bazel/rules/bazel_java_binary.bzl b/java/bazel/rules/bazel_java_binary.bzl index 0611e06..998ac89 100644 --- a/java/bazel/rules/bazel_java_binary.bzl +++ b/java/bazel/rules/bazel_java_binary.bzl
@@ -79,17 +79,15 @@ _java_runtime_version = semantics.find_java_runtime_toolchain(ctx).version if _java_runtime_version >= 17 and _java_runtime_version < 24: jvm_flags.append("-Djava.security.manager=allow") - test_class = ctx.attr.test_class if hasattr(ctx.attr, "test_class") else "" - if test_class == "": - test_class = helper.primary_class(ctx) - if test_class == None: - fail("cannot determine test class. You might want to rename the " + - "rule or add a 'test_class' attribute.") + test_classes = helper.test_classes(ctx) + if not test_classes: + fail("cannot determine test class. You might want to add a " + + "'test_class' or 'test_classes' attribute.") jvm_flags.extend([ "-ea", "-D{prop_name}={prop_value}".format( prop_name = semantics.TEST_SUITE_PROPERTY_NAME, - prop_value = helper.shell_escape(test_class), + prop_value = helper.shell_escape(",".join(test_classes)), ), ])
diff --git a/java/common/rules/impl/java_binary_impl.bzl b/java/common/rules/impl/java_binary_impl.bzl index 13718eb..379c213 100644 --- a/java/common/rules/impl/java_binary_impl.bzl +++ b/java/common/rules/impl/java_binary_impl.bzl
@@ -87,7 +87,14 @@ if not ctx.attr.use_launcher and (ctx.attr.launcher and ctx.attr.launcher.label != semantics.LAUNCHER_FLAG_LABEL): fail("launcher specified but use_launcher is false") - if not ctx.attr.srcs and ctx.attr.deps: + if not is_test_rule_class: + if hasattr(ctx.attr, "test_classes") and ctx.attr.test_classes: + fail("in %s: 'test_classes' is not allowed on java_binary" % ctx.label) + if hasattr(ctx.attr, "non_test_srcs") and ctx.attr.non_test_srcs: + fail("in %s: 'non_test_srcs' is not allowed on java_binary" % ctx.label) + + srcs = ctx.files.srcs + [src for src in getattr(ctx.files, "non_test_srcs", []) if src not in ctx.files.srcs] + if not srcs and ctx.attr.deps: fail("deps not allowed without srcs; move to runtime_deps?") module_flags = [dep[JavaInfo].module_flags_info for dep in runtime_deps if JavaInfo in dep] @@ -103,7 +110,7 @@ timezone_data = [toolchain._timezone_data] if toolchain._timezone_data else [] target, common_info = basic_java_library( ctx, - srcs = ctx.files.srcs, + srcs = srcs, deps = deps, runtime_deps = runtime_deps, plugins = ctx.attr.plugins,
diff --git a/java/common/rules/impl/java_helper.bzl b/java/common/rules/impl/java_helper.bzl index 638878b..2ee5427 100644 --- a/java/common/rules/impl/java_helper.bzl +++ b/java/common/rules/impl/java_helper.bzl
@@ -88,13 +88,23 @@ main_class = _primary_class(ctx) return main_class +def _canonical_class_from_target_name(ctx): + return _full_classname(helper.get_relative(ctx.label.package, ctx.label.name)) + def _primary_class(ctx): if ctx.attr.srcs: main = ctx.label.name + ".java" - for src in ctx.files.srcs: + for src in _test_srcs(ctx): if src.basename == main: return _full_classname(_strip_extension(src)) - return _full_classname(helper.get_relative(ctx.label.package, ctx.label.name)) + return _canonical_class_from_target_name(ctx) + +def _test_srcs(ctx): + """Returns ctx.files.srcs excluding any files listed in non_test_srcs.""" + if not hasattr(ctx.files, "non_test_srcs") or not ctx.files.non_test_srcs: + return ctx.files.srcs + non_test_srcs = {src: None for src in ctx.files.non_test_srcs} + return [src for src in ctx.files.srcs if src not in non_test_srcs] def _strip_extension(file): return file.dirname + "/" + ( @@ -106,6 +116,28 @@ java_segments = _loading_phase_helper.java_segments(path) return ".".join(java_segments) if java_segments != None else None +def _test_classes(ctx): + has_test_class = hasattr(ctx.attr, "test_class") and bool(ctx.attr.test_class) + has_test_classes = hasattr(ctx.attr, "test_classes") and bool(ctx.attr.test_classes) + if has_test_class and has_test_classes: + fail("in %s: cannot specify both 'test_class' and 'test_classes'" % ctx.label) + if has_test_class: + return [ctx.attr.test_class] + if has_test_classes: + return ctx.attr.test_classes + if ctx.attr.srcs: + classes = [] + for src in _test_srcs(ctx): + if src.extension == "java": + cls = _full_classname(_strip_extension(src)) + if cls == None: + return [] + classes.append(cls) + if classes: + return classes + canonical = _canonical_class_from_target_name(ctx) + return [canonical] if canonical != None else [] + def _concat(*lists): result = [] for list in lists: @@ -256,6 +288,8 @@ launcher_artifact_for_target = _launcher_artifact_for_target, check_and_get_main_class = _check_and_get_main_class, primary_class = _primary_class, + test_classes = _test_classes, + test_srcs = _test_srcs, strip_extension = _strip_extension, concat = _concat, get_shared_native_deps_path = _get_shared_native_deps_path,
diff --git a/java/common/rules/java_binary.bzl b/java/common/rules/java_binary.bzl index 609ca97..12250ab 100644 --- a/java/common/rules/java_binary.bzl +++ b/java/common/rules/java_binary.bzl
@@ -360,6 +360,24 @@ </p> """, ), + "test_classes": attr.string_list( + doc = """ +The Java classes to be loaded by the test runner. +<p> + By default, test classes are automatically inferred from source file names in + <code>srcs</code>. +</p> +<p> + Specify <code>test_classes</code> explicitly when: + <ul> + <li>The classes to run cannot be inferred from the source file names or do not + match the package path convention.</li> + <li>Only a specific subset of test classes compiled into the binary should be executed.</li> + <li>Test suites or base test classes are defined elsewhere.</li> + </ul> +</p> + """, + ), "env_inherit": attr.string_list(), "_apple_constraints": attr.label_list( default = [
diff --git a/test/java/bazel/rules/java_test_tests.bzl b/test/java/bazel/rules/java_test_tests.bzl index 5c76b0e..f01d6dc 100644 --- a/test/java/bazel/rules/java_test_tests.bzl +++ b/test/java/bazel/rules/java_test_tests.bzl
@@ -1,11 +1,17 @@ """Tests for the Bazel java_test rule""" +load("@bazel_features//private:util.bzl", _bazel_version_ge = "ge") load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite") load("@rules_testing//lib:truth.bzl", "matching", "subjects") load("@rules_testing//lib:util.bzl", "util") load("//java:java_test.bzl", "java_test") +load("//test/java/testutil:helper.bzl", "always_passes") def _test_deduced_test_class(name): + if not _bazel_version_ge("8.0.0"): + always_passes(name) + return + util.helper_target( java_test, name = name + "/foo", @@ -16,6 +22,7 @@ name = name, impl = _test_deduced_test_class_impl, target = name + "/foo", + attr_values = {"tags": ["min_bazel_8"]}, ) def _test_deduced_test_class_impl(env, target): @@ -32,7 +39,7 @@ # Windows assert_jvm_flags = assert_action.argv() assert_jvm_flags.contains_predicate( - matching.str_matches("-Dbazel.test_suite=bazel.rules.test_deduced_test_class.foo"), + matching.str_matches("-Dbazel.test_suite=bazel.rules.test_deduced_test_class.Foo"), ) # regression test for https://github.com/bazelbuild/bazel/issues/20378 @@ -49,11 +56,73 @@ matching.str_matches("cannot determine test class."), ) +def _test_test_classes(name): + if not _bazel_version_ge("8.0.0"): + always_passes(name) + return + + util.helper_target( + java_test, + name = name + "/foo", + test_classes = ["com.example.FooTest", "com.example.BarTest"], + ) + + analysis_test( + name = name, + impl = _test_test_classes_impl, + target = name + "/foo", + attr_values = {"tags": ["min_bazel_8"]}, + ) + +def _test_test_classes_impl(env, target): + executable = target[DefaultInfo].files_to_run.executable.short_path + assert_action = env.expect.that_target(target).action_generating(executable) + + if assert_action.actual.substitutions: + # TemplateExpansion action on linux/mac + assert_jvm_flags = assert_action.substitutions().get( + "%jvm_flags%", + factory = lambda v, meta: subjects.collection([v], meta), + ) + else: + # Windows + assert_jvm_flags = assert_action.argv() + assert_jvm_flags.contains_predicate( + matching.str_matches("-Dbazel.test_suite=com.example.FooTest,com.example.BarTest"), + ) + +def _test_both_test_class_and_test_classes_fails(name): + if not _bazel_version_ge("8.0.0"): + always_passes(name) + return + + util.helper_target( + java_test, + name = name + "/foo", + test_class = "FooTest", + test_classes = ["FooTest"], + ) + + analysis_test( + name = name, + impl = _test_both_test_class_and_test_classes_fails_impl, + target = name + "/foo", + expect_failure = True, + attr_values = {"tags": ["min_bazel_8"]}, + ) + +def _test_both_test_class_and_test_classes_fails_impl(env, target): + env.expect.that_target(target).failures().contains_predicate( + matching.str_matches("cannot specify both 'test_class' and 'test_classes'"), + ) + def java_test_tests(name): test_suite( name = name, tests = [ _test_deduced_test_class, _test_invalid_test_class_at_repo_root, + _test_test_classes, + _test_both_test_class_and_test_classes_fails, ], )