[bazel] skia_android_unit_test rule: Support select() in extra_args attribute.

Also:
- Skip some test cases that make an existing skia_android_unit_test fail.
- Add a CI task for said test.

Suggested review order:

- //tests/android.bzl
- //tests/skia_binary_with_cmdline_flags_test.bzl
- //tests/adb_test.bzl
- Everything else.

Bug: skia:14227
Change-Id: I1a953519ed295617bf9e005876b9e2845d9d59b8
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/705156
Reviewed-by: Kevin Lubick <kjlubick@google.com>
Commit-Queue: Leandro Lovisolo <lovisolo@google.com>
diff --git a/infra/bots/jobs.json b/infra/bots/jobs.json
index c425d92..8dee2ea 100644
--- a/infra/bots/jobs.json
+++ b/infra/bots/jobs.json
@@ -26,6 +26,7 @@
   {"name": "BazelBuild-tests-gl_ganesh-linux_x64"},
   {"name": "BazelBuild-tests-vulkan_ganesh-linux_x64"},
   {"name": "BazelBuild-android_codec_test-pixel_5-linux_x64"},
+  {"name": "BazelBuild-android_cpu_only_test-pixel_5-linux_x64"},
   {"name": "BazelTest-canvaskit_gold-modules_canvaskit_js_tests-ck_full_cpu_release_chrome-linux_x64",
     "cq_config": {"location_regexes": ["modules/canvaskit/.*"]}
   },
@@ -40,6 +41,7 @@
   },
   {"name": "BazelTest-toolchain_layering_check-experimental_bazel_test_client-release-linux_x64"},
   {"name": "BazelTest-precompiled-android_codec_test-pixel_5-linux_arm64"},
+  {"name": "BazelTest-precompiled-android_cpu_only_test-pixel_5-linux_arm64"},
   {"name": "Build-Debian10-GCC-x86-Debug-Docker"},
   {"name": "Build-Debian10-GCC-x86-Release-Docker"},
   {"name": "Build-Debian10-GCC-x86_64-Debug-Docker"},
diff --git a/infra/bots/tasks.json b/infra/bots/tasks.json
index 2f9319a..5f0a861 100644
--- a/infra/bots/tasks.json
+++ b/infra/bots/tasks.json
@@ -5,6 +5,11 @@
         "BazelBuild-android_codec_test-pixel_5-linux_x64"
       ]
     },
+    "BazelBuild-android_cpu_only_test-pixel_5-linux_x64": {
+      "tasks": [
+        "BazelBuild-android_cpu_only_test-pixel_5-linux_x64"
+      ]
+    },
     "BazelBuild-base-enforce_iwyu-linux_x64": {
       "tasks": [
         "BazelBuild-base-enforce_iwyu-linux_x64"
@@ -90,6 +95,11 @@
         "BazelTest-precompiled-android_codec_test-pixel_5-linux_arm64"
       ]
     },
+    "BazelTest-precompiled-android_cpu_only_test-pixel_5-linux_arm64": {
+      "tasks": [
+        "BazelTest-precompiled-android_cpu_only_test-pixel_5-linux_arm64"
+      ]
+    },
     "BazelTest-toolchain_layering_check-experimental_bazel_test_client-release-linux_x64": {
       "tasks": [
         "BazelTest-toolchain_layering_check-experimental_bazel_test_client-release-linux_x64"
@@ -3689,6 +3699,52 @@
       ],
       "service_account": "skia-external-compile-tasks@skia-swarming-bots.iam.gserviceaccount.com"
     },
+    "BazelBuild-android_cpu_only_test-pixel_5-linux_x64": {
+      "casSpec": "bazel",
+      "cipd_packages": [
+        {
+          "name": "skia/bots/bazelisk_linux_amd64",
+          "path": "bazelisk_linux_amd64",
+          "version": "version:0"
+        }
+      ],
+      "command": [
+        "./bazel_build",
+        "--project_id=skia-swarming-bots",
+        "--task_id=<(TASK_ID)",
+        "--task_name=BazelBuild-android_cpu_only_test-pixel_5-linux_x64",
+        "--label=//tests:android_cpu_only_test",
+        "--config=pixel_5",
+        "--workdir=.",
+        "--out_path=bazel_output",
+        "--saved_output_dir=tests",
+        "--bazel_arg=--config=linux_rbe",
+        "--bazel_arg=--jobs=100",
+        "--bazel_arg=--remote_download_toplevel",
+        "--bazel_arg=--adb_platform=linux_arm64"
+      ],
+      "dependencies": [
+        "Housekeeper-PerCommit-BuildTaskDrivers_linux_amd64"
+      ],
+      "dimensions": [
+        "cpu:x86-64-Haswell_GCE",
+        "gpu:none",
+        "machine_type:n1-standard-16",
+        "os:Debian-10.3",
+        "pool:Skia"
+      ],
+      "env_prefixes": {
+        "PATH": [
+          "bazelisk_linux_amd64"
+        ]
+      },
+      "idempotent": true,
+      "max_attempts": 1,
+      "outputs": [
+        "bazel_output"
+      ],
+      "service_account": "skia-external-compile-tasks@skia-swarming-bots.iam.gserviceaccount.com"
+    },
     "BazelBuild-base-enforce_iwyu-linux_x64": {
       "casSpec": "bazel",
       "cipd_packages": [
@@ -4372,6 +4428,31 @@
       "max_attempts": 1,
       "service_account": "skia-external-compile-tasks@skia-swarming-bots.iam.gserviceaccount.com"
     },
+    "BazelTest-precompiled-android_cpu_only_test-pixel_5-linux_arm64": {
+      "casSpec": "bazel",
+      "command": [
+        "./bazel_test_precompiled",
+        "--project_id=skia-swarming-bots",
+        "--task_id=<(TASK_ID)",
+        "--task_name=BazelTest-precompiled-android_cpu_only_test-pixel_5-linux_arm64",
+        "--workdir=.",
+        "--command=bazel_output/tests/android_cpu_only_test",
+        "--command_workdir=bazel_output/tests/android_cpu_only_test.runfiles/skia"
+      ],
+      "dependencies": [
+        "BazelBuild-android_cpu_only_test-pixel_5-linux_x64",
+        "Housekeeper-PerCommit-BuildTaskDrivers_linux_arm64"
+      ],
+      "dimensions": [
+        "os:Android",
+        "device_type:redfin",
+        "device_os:SP2A.220305.012",
+        "pool:Skia"
+      ],
+      "idempotent": true,
+      "max_attempts": 1,
+      "service_account": "skia-external-compile-tasks@skia-swarming-bots.iam.gserviceaccount.com"
+    },
     "BazelTest-toolchain_layering_check-experimental_bazel_test_client-release-linux_x64": {
       "casSpec": "bazel",
       "cipd_packages": [
diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel
index 4cd8658..c6f6fcf 100644
--- a/tests/BUILD.bazel
+++ b/tests/BUILD.bazel
@@ -356,10 +356,22 @@
     ],
 )
 
-# Some test cases fail with --config=pixel_7.
 skia_android_unit_test(
     name = "android_cpu_only_test",
     srcs = CPU_ONLY_TESTS,
+    # TODO(lovisolo): We might want to extract these --skip flags into a separate file.
+    extra_args = select({
+        "//bazel/devices:pixel_5": [
+            "--skip",
+            "DrawText_dashout",
+            "FontHostStream",
+            "LegacyMakeTypeface",
+            "TextBlob_getIntercepts",
+            "TextBlob_iter",
+            "glyphs_to_unichars",
+        ],
+        "//conditions:default": [],
+    }),
     flags = {
         "fontmgr_factory": ["custom_directory_fontmgr_factory"],
         "enable_sksl": ["True"],
diff --git a/tests/BazelTestRunner.cpp b/tests/BazelTestRunner.cpp
index 909cd02..e3b016b 100644
--- a/tests/BazelTestRunner.cpp
+++ b/tests/BazelTestRunner.cpp
@@ -152,6 +152,17 @@
     gSkDebugToStdOut = true;
 #endif
 
+    if (argc < 2) {
+        SkDebugf("Test runner invoked with no arguments.\n");
+    } else {
+        std::ostringstream oss;
+        oss << "Test runner invoked with arguments:";
+        for (int i = 1; i < argc; i++) {
+            oss << " " << argv[i];
+        }
+        SkDebugf("%s\n", oss.str().c_str());
+    }
+
     CommandLineFlags::Parse(argc, argv);
 
     BazelReporter reporter;
diff --git a/tests/adb_test.bzl b/tests/adb_test.bzl
index f48eee1..350c479 100644
--- a/tests/adb_test.bzl
+++ b/tests/adb_test.bzl
@@ -131,7 +131,7 @@
                 "Test runner script that calls the compiled C++ binary with any necessary " +
                 "command-line arguments. This script will be executed on the Android device."
             ),
-            allow_single_file = [".sh"],
+            allow_single_file = True,
             mandatory = True,
         ),
         "archive": attr.label(
diff --git a/tests/android.bzl b/tests/android.bzl
index b5c5ae2..bdbe3ad 100644
--- a/tests/android.bzl
+++ b/tests/android.bzl
@@ -1,8 +1,8 @@
 """This module defines the skia_android_unit_test macro."""
 
 load("//bazel:cc_binary_with_flags.bzl", "cc_binary_with_flags")
-load("//bazel:remove_indentation.bzl", "remove_indentation")
 load(":adb_test.bzl", "adb_test")
+load(":skia_binary_with_cmdline_flags_test.bzl", "skia_binary_with_cmdline_flags_test")
 
 def skia_test(
         name,
@@ -16,16 +16,15 @@
         size = None):
     """Defines a generic Skia C++ unit test.
 
-    This macro produces a <name>_binary C++ binary and a <name>.sh wrapper script that runs the
+    This macro produces a <name>_binary C++ binary and a <name> wrapper script that runs the
     binary with the desired command-line arguments (see the extra_args and requires_resources_dir
-    arguments). The <name>.sh wrapper script is exposed as a Bazel test target via the sh_target
-    rule.
+    arguments).
 
     The reason why we place command-line arguments in a wrapper script is that it makes it easier
     to run a Bazel-built skia_test outside of Bazel. This is useful e.g. for CI jobs where we want
     to perform test compilation and execution as different steps on different hardware (e.g.
     compile on a GCE machine, run tests on a Skolo device). In this scenario, the test could be
-    executed outside of Bazel by simply running the <name>.sh script without any arguments. See the
+    executed outside of Bazel by simply running the <name> script without any arguments. See the
     skia_android_unit_test macro for an example.
 
     Note: The srcs attribute must explicitly include a test runner (e.g.
@@ -66,42 +65,12 @@
         testonly = True,  # Needed to gain access to test-only files.
     )
 
-    test_runner = "%s.sh" % name
-
-    test_args = ([
-        "--resourcePath",
-        "$$(realpath $$(dirname $(rootpath //resources:README)))",
-    ] if requires_resources_dir else []) + extra_args
-
-    # This test runner might run on Android devices, which might not have a /bin/bash binary.
-    test_runner_template = remove_indentation("""
-        #!/bin/sh
-        $(rootpath {test_binary}) {test_args}
-    """)
-
-    # TODO(lovisolo): This should be an actual rule. This will allow us to select() the arguments
-    #                 based on the device (e.g. for device-specific --skip flags to skip tests).
-    native.genrule(
-        name = "%s_runner" % name,
-        srcs = [test_binary] + (
-            # The script template computes the path to //resources under the runfiles tree via
-            # $$(dirname $(rootpath //resources:README)), so we need to list //resources:README
-            # here explicitly. This file was chosen arbitrarily; there is nothing special about it.
-            ["//resources", "//resources:README"] if requires_resources_dir else []
-        ),
-        outs = [test_runner],
-        cmd = "echo '%s' > $@" % test_runner_template.format(
-            test_binary = test_binary,
-            test_args = "\\\n    ".join(test_args),
-        ),
-        testonly = True,
-    )
-
-    native.sh_test(
+    skia_binary_with_cmdline_flags_test(
         name = name,
+        test_binary = test_binary,
+        requires_resources_dir = requires_resources_dir,
+        extra_args = extra_args,
         size = size,
-        srcs = [test_runner],
-        data = [test_binary] + (["//resources"] if requires_resources_dir else []),
         tags = tags,
     )
 
@@ -130,16 +99,16 @@
     - It produces a <name>.tar.gz archive containing the Android binary, a minimal launcher script
       that invokes the binary with the necessary command-line arguments, and any static resources
       needed by the test, such as fonts and images under //resources.
-    - It produces a <name>.sh test runner script that extracts the tarball into the device via
-      `adb`, sets up the device, runs the test, cleans up and pipes through the test's exit code.
+    - It produces a <name> test runner script that extracts the tarball into the device via `adb`,
+      sets up the device, runs the test, cleans up and pipes through the test's exit code.
 
     For CI jobs, rather than invoking "bazel test" on a Raspberry Pi attached to the Android device
     under test, we compile and run the test in two separate tasks:
 
     - A build task running on a GCE machine compiles the test on RBE with Bazel and stores the
-      <name>.tar.gz and <name>.sh output files to CAS.
-    - A test task running on a Skolo Raspberry Pi downloads <name>.tar.gz and <name>.sh from CAS
-      and executes <name>.sh *outside of Bazel*.
+      <name>.tar.gz and <name> output files to CAS.
+    - A test task running on a Skolo Raspberry Pi downloads <name>.tar.gz and <name> from CAS and
+      executes <name> *outside of Bazel*.
 
     The reason why we don't want to run Bazel on a Raspberry Pi is due to its constrained
     resources.
@@ -172,8 +141,11 @@
             to the path to said directory.
     """
 
+    test_runner = "%s_cpp_test" % name
+    test_binary = "%s_cpp_test_binary" % name  # Produced by the skia_test macro.
+
     skia_test(
-        name = "%s_cpp_test" % name,
+        name = test_runner,
         srcs = select({
             requires_condition: srcs + ["//tests:BazelTestRunner.cpp"],
             "//conditions:default": ["//tests:BazelNoopRunner.cpp"],
@@ -194,9 +166,6 @@
         size = "large",  # Can take several minutes.
     )
 
-    test_binary = "%s_cpp_test_binary" % name
-    test_runner = "%s_cpp_test.sh" % name
-
     archive = "%s_archive" % name
     archive_srcs = [test_runner, test_binary] + (
         ["//resources"] if requires_resources_dir else []
diff --git a/tests/skia_binary_with_cmdline_flags_test.bzl b/tests/skia_binary_with_cmdline_flags_test.bzl
new file mode 100644
index 0000000..ca724e0
--- /dev/null
+++ b/tests/skia_binary_with_cmdline_flags_test.bzl
@@ -0,0 +1,75 @@
+"""This module defines the skia_binary_with_cmdline_flags_test rule."""
+
+load("//bazel:remove_indentation.bzl", "remove_indentation")
+
+# https://bazel.build/rules/lib/builtins/ctx
+def _skia_binary_with_cmdline_flags_test_impl(ctx):
+    test_args = ([
+        "--resourcePath",
+        "$(dirname $(realpath $(rootpath %s)))" % ctx.attr._arbitrary_file_in_resources_dir.label,
+    ] if ctx.attr.requires_resources_dir else []) + ctx.attr.extra_args
+
+    template = remove_indentation("""
+        #!/bin/sh
+        $(rootpath {test_binary}) {test_args}
+    """)
+
+    template = ctx.expand_location(template.format(
+        test_binary = ctx.attr.test_binary.label,
+        test_args = " ".join(test_args),
+    ), targets = [
+        ctx.attr.test_binary,
+        ctx.attr._arbitrary_file_in_resources_dir,
+    ])
+
+    # https://bazel.build/rules/lib/builtins/actions.html#declare_file
+    output_file = ctx.actions.declare_file(ctx.attr.name)
+    ctx.actions.write(output_file, template, is_executable = True)
+
+    runfiles = ctx.runfiles(
+        files = ctx.files._resources_dir if ctx.attr.requires_resources_dir else [],
+    )
+    runfiles = runfiles.merge(ctx.attr.test_binary[DefaultInfo].default_runfiles)
+
+    return [DefaultInfo(
+        executable = output_file,
+        runfiles = runfiles,
+    )]
+
+skia_binary_with_cmdline_flags_test = rule(
+    doc = """Runs a C++ test binary with any necessary command-line flags.
+
+    This test rule produces a wrapper script that invokes a C++ test with a predetermined set of
+    command-line flags.
+
+    The reason why we use a custom rule rather than a genrule is that we wish to select() the
+    extra_args attribute based e.g. on the device under test and various build settings.
+    """,
+    implementation = _skia_binary_with_cmdline_flags_test_impl,
+    attrs = {
+        "test_binary": attr.label(
+            mandatory = True,
+            executable = True,
+            allow_single_file = True,
+            cfg = "target",
+        ),
+        "requires_resources_dir": attr.bool(
+            doc = (
+                "If true, the test will be passed flag --resourcePath <path to //resources dir>."
+            ),
+        ),
+        "extra_args": attr.string_list(
+            doc = "Any additional command-line arguments to pass to the C++ test binary.",
+        ),
+        "_resources_dir": attr.label(
+            doc = "This directory will optionally be added to the test's runfiles.",
+            default = Label("//resources"),
+        ),
+        "_arbitrary_file_in_resources_dir": attr.label(
+            doc = "Used to compute the --resourcePath flag.",
+            default = Label("//resources:README"),
+            allow_single_file = True,
+        ),
+    },
+    test = True,
+)