test: Use testable_packages_optional to allow no tests to be found.

If testable_packages_optional pass in --no-testable-packages-ok to the
cros_run_unit_test script.

BUG=chromium:1124357
TEST=./run_pytest

Cq-Depend: chromium:2404087
Change-Id: I5b0b67b3ffd3ccb2d5ea7434b2d7e054704b3fa5
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/2404271
Reviewed-by: Sean Abraham <seanabraham@chromium.org>
Reviewed-by: Alex Klein <saklein@chromium.org>
Commit-Queue: Navil Perez <navil@google.com>
Tested-by: Navil Perez <navil@google.com>
diff --git a/api/controller/test.py b/api/controller/test.py
index dfe6045..4a8ddfd 100644
--- a/api/controller/test.py
+++ b/api/controller/test.py
@@ -106,6 +106,9 @@
   for package_info in blacklisted_package_info:
     blacklist.append(controller_util.PackageInfoToString(package_info))
 
+  # Allow call to succeed if no tests were found.
+  testable_packages_optional = input_proto.flags.testable_packages_optional
+
   build_target = controller_util.ParseBuildTarget(input_proto.build_target)
   chroot = controller_util.ParseChroot(input_proto.chroot)
 
@@ -117,7 +120,8 @@
       packages=packages,
       blacklist=blacklist,
       was_built=was_built,
-      code_coverage=code_coverage)
+      code_coverage=code_coverage,
+      testable_packages_optional=testable_packages_optional)
 
   if not result.success:
     # Failed to run tests or some tests failed.
diff --git a/api/gen/chromite/api/test_pb2.py b/api/gen/chromite/api/test_pb2.py
index 87dd17d..c1e05be 100644
--- a/api/gen/chromite/api/test_pb2.py
+++ b/api/gen/chromite/api/test_pb2.py
@@ -23,7 +23,7 @@
   package='chromite.api',
   syntax='proto3',
   serialized_options=_b('Z6go.chromium.org/chromiumos/infra/proto/go/chromite/api'),
-  serialized_pb=_b('\n\x17\x63hromite/api/test.proto\x12\x0c\x63hromite.api\x1a\x1c\x63hromite/api/build_api.proto\x1a\x1a\x63hromite/api/sysroot.proto\x1a\x17\x63hromiumos/common.proto\x1a\x18\x63hromiumos/metrics.proto\"\xf4\x02\n\x1a\x42uildTargetUnitTestRequest\x12-\n\x0c\x62uild_target\x18\x01 \x01(\x0b\x32\x17.chromiumos.BuildTarget\x12\x13\n\x0bresult_path\x18\x02 \x01(\t\x12\"\n\x06\x63hroot\x18\x03 \x01(\x0b\x32\x12.chromiumos.Chroot\x12=\n\x05\x66lags\x18\x04 \x01(\x0b\x32..chromite.api.BuildTargetUnitTestRequest.Flags\x12\x32\n\x11package_blacklist\x18\x05 \x03(\x0b\x32\x17.chromiumos.PackageInfo\x12)\n\x08packages\x18\x06 \x03(\x0b\x32\x17.chromiumos.PackageInfo\x1aP\n\x05\x46lags\x12\x15\n\rempty_sysroot\x18\x01 \x01(\x08\x12\x19\n\x11toolchain_changed\x18\x02 \x01(\x08\x12\x15\n\rcode_coverage\x18\x03 \x01(\x08\"\x8e\x01\n\x1b\x42uildTargetUnitTestResponse\x12\x14\n\x0ctarball_path\x18\x01 \x01(\t\x12\x30\n\x0f\x66\x61iled_packages\x18\x02 \x03(\x0b\x32\x17.chromiumos.PackageInfo\x12\'\n\x06\x65vents\x18\x03 \x03(\x0b\x32\x17.chromiumos.MetricEvent\"=\n\x17\x43hromiteUnitTestRequest\x12\"\n\x06\x63hroot\x18\x01 \x01(\x0b\x32\x12.chromiumos.Chroot\"\x1a\n\x18\x43hromiteUnitTestResponse\";\n\x15\x43hromitePytestRequest\x12\"\n\x06\x63hroot\x18\x01 \x01(\x0b\x32\x12.chromiumos.Chroot\"\x18\n\x16\x43hromitePytestResponse\"<\n\x16\x43rosSigningTestRequest\x12\"\n\x06\x63hroot\x18\x01 \x01(\x0b\x32\x12.chromiumos.Chroot\"\x19\n\x17\x43rosSigningTestResponse\"b\n\x14\x44\x65\x62ugInfoTestRequest\x12&\n\x07sysroot\x18\x01 \x01(\x0b\x32\x15.chromite.api.Sysroot\x12\"\n\x06\x63hroot\x18\x02 \x01(\x0b\x32\x12.chromiumos.Chroot\"\x17\n\x15\x44\x65\x62ugInfoTestResponse\"\xd2\x03\n\rVmTestRequest\x12-\n\x0c\x62uild_target\x18\x01 \x01(\x0b\x32\x17.chromiumos.BuildTarget\x12\"\n\x06\x63hroot\x18\x02 \x01(\x0b\x32\x12.chromiumos.Chroot\x12!\n\x07vm_path\x18\x03 \x01(\x0b\x32\x10.chromiumos.Path\x12;\n\x0bssh_options\x18\x04 \x01(\x0b\x32&.chromite.api.VmTestRequest.SshOptions\x12=\n\x0ctest_harness\x18\x05 \x01(\x0e\x32\'.chromite.api.VmTestRequest.TestHarness\x12\x34\n\x08vm_tests\x18\x06 \x03(\x0b\x32\".chromite.api.VmTestRequest.VmTest\x1a\x46\n\nSshOptions\x12*\n\x10private_key_path\x18\x01 \x01(\x0b\x32\x10.chromiumos.Path\x12\x0c\n\x04port\x18\x02 \x01(\x05\x1a\x19\n\x06VmTest\x12\x0f\n\x07pattern\x18\x01 \x01(\t\"6\n\x0bTestHarness\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x08\n\x04TAST\x10\x01\x12\x0c\n\x08\x41UTOTEST\x10\x02\"\x10\n\x0eVmTestResponse\"\xe9\x01\n\x13MoblabVmTestRequest\x12\"\n\x06\x63hroot\x18\x01 \x01(\x0b\x32\x12.chromiumos.Chroot\x12@\n\rimage_payload\x18\x02 \x01(\x0b\x32).chromite.api.MoblabVmTestRequest.Payload\x12\x41\n\x0e\x63\x61\x63he_payloads\x18\x03 \x03(\x0b\x32).chromite.api.MoblabVmTestRequest.Payload\x1a)\n\x07Payload\x12\x1e\n\x04path\x18\x01 \x01(\x0b\x32\x10.chromiumos.Path\"\x16\n\x14MoblabVmTestResponse\"\x8b\x01\n\x1fSimpleChromeWorkflowTestRequest\x12&\n\x07sysroot\x18\x01 \x01(\x0b\x32\x15.chromite.api.Sysroot\x12\x13\n\x0b\x63hrome_root\x18\x02 \x01(\t\x12+\n\x0bgoma_config\x18\x03 \x01(\x0b\x32\x16.chromiumos.GomaConfig\"\"\n SimpleChromeWorkflowTestResponse2\xb1\x06\n\x0bTestService\x12r\n\x13\x42uildTargetUnitTest\x12(.chromite.api.BuildTargetUnitTestRequest\x1a).chromite.api.BuildTargetUnitTestResponse\"\x06\xc2\xed\x1a\x02\x10\x02\x12\x61\n\x10\x43hromiteUnitTest\x12%.chromite.api.ChromiteUnitTestRequest\x1a&.chromite.api.ChromiteUnitTestResponse\x12[\n\x0e\x43hromitePytest\x12#.chromite.api.ChromitePytestRequest\x1a$.chromite.api.ChromitePytestResponse\x12^\n\x0f\x43rosSigningTest\x12$.chromite.api.CrosSigningTestRequest\x1a%.chromite.api.CrosSigningTestResponse\x12X\n\rDebugInfoTest\x12\".chromite.api.DebugInfoTestRequest\x1a#.chromite.api.DebugInfoTestResponse\x12\x43\n\x06VmTest\x12\x1b.chromite.api.VmTestRequest\x1a\x1c.chromite.api.VmTestResponse\x12]\n\x0cMoblabVmTest\x12!.chromite.api.MoblabVmTestRequest\x1a\".chromite.api.MoblabVmTestResponse\"\x06\xc2\xed\x1a\x02\x10\x02\x12\x81\x01\n\x18SimpleChromeWorkflowTest\x12-.chromite.api.SimpleChromeWorkflowTestRequest\x1a..chromite.api.SimpleChromeWorkflowTestResponse\"\x06\xc2\xed\x1a\x02\x10\x02\x1a\x0c\xc2\xed\x1a\x08\n\x04test\x10\x01\x42\x38Z6go.chromium.org/chromiumos/infra/proto/go/chromite/apib\x06proto3')
+  serialized_pb=_b('\n\x17\x63hromite/api/test.proto\x12\x0c\x63hromite.api\x1a\x1c\x63hromite/api/build_api.proto\x1a\x1a\x63hromite/api/sysroot.proto\x1a\x17\x63hromiumos/common.proto\x1a\x18\x63hromiumos/metrics.proto\"\x98\x03\n\x1a\x42uildTargetUnitTestRequest\x12-\n\x0c\x62uild_target\x18\x01 \x01(\x0b\x32\x17.chromiumos.BuildTarget\x12\x13\n\x0bresult_path\x18\x02 \x01(\t\x12\"\n\x06\x63hroot\x18\x03 \x01(\x0b\x32\x12.chromiumos.Chroot\x12=\n\x05\x66lags\x18\x04 \x01(\x0b\x32..chromite.api.BuildTargetUnitTestRequest.Flags\x12\x32\n\x11package_blacklist\x18\x05 \x03(\x0b\x32\x17.chromiumos.PackageInfo\x12)\n\x08packages\x18\x06 \x03(\x0b\x32\x17.chromiumos.PackageInfo\x1at\n\x05\x46lags\x12\x15\n\rempty_sysroot\x18\x01 \x01(\x08\x12\x19\n\x11toolchain_changed\x18\x02 \x01(\x08\x12\x15\n\rcode_coverage\x18\x03 \x01(\x08\x12\"\n\x1atestable_packages_optional\x18\x04 \x01(\x08\"\x8e\x01\n\x1b\x42uildTargetUnitTestResponse\x12\x14\n\x0ctarball_path\x18\x01 \x01(\t\x12\x30\n\x0f\x66\x61iled_packages\x18\x02 \x03(\x0b\x32\x17.chromiumos.PackageInfo\x12\'\n\x06\x65vents\x18\x03 \x03(\x0b\x32\x17.chromiumos.MetricEvent\"=\n\x17\x43hromiteUnitTestRequest\x12\"\n\x06\x63hroot\x18\x01 \x01(\x0b\x32\x12.chromiumos.Chroot\"\x1a\n\x18\x43hromiteUnitTestResponse\";\n\x15\x43hromitePytestRequest\x12\"\n\x06\x63hroot\x18\x01 \x01(\x0b\x32\x12.chromiumos.Chroot\"\x18\n\x16\x43hromitePytestResponse\"<\n\x16\x43rosSigningTestRequest\x12\"\n\x06\x63hroot\x18\x01 \x01(\x0b\x32\x12.chromiumos.Chroot\"\x19\n\x17\x43rosSigningTestResponse\"b\n\x14\x44\x65\x62ugInfoTestRequest\x12&\n\x07sysroot\x18\x01 \x01(\x0b\x32\x15.chromite.api.Sysroot\x12\"\n\x06\x63hroot\x18\x02 \x01(\x0b\x32\x12.chromiumos.Chroot\"\x17\n\x15\x44\x65\x62ugInfoTestResponse\"\xd2\x03\n\rVmTestRequest\x12-\n\x0c\x62uild_target\x18\x01 \x01(\x0b\x32\x17.chromiumos.BuildTarget\x12\"\n\x06\x63hroot\x18\x02 \x01(\x0b\x32\x12.chromiumos.Chroot\x12!\n\x07vm_path\x18\x03 \x01(\x0b\x32\x10.chromiumos.Path\x12;\n\x0bssh_options\x18\x04 \x01(\x0b\x32&.chromite.api.VmTestRequest.SshOptions\x12=\n\x0ctest_harness\x18\x05 \x01(\x0e\x32\'.chromite.api.VmTestRequest.TestHarness\x12\x34\n\x08vm_tests\x18\x06 \x03(\x0b\x32\".chromite.api.VmTestRequest.VmTest\x1a\x46\n\nSshOptions\x12*\n\x10private_key_path\x18\x01 \x01(\x0b\x32\x10.chromiumos.Path\x12\x0c\n\x04port\x18\x02 \x01(\x05\x1a\x19\n\x06VmTest\x12\x0f\n\x07pattern\x18\x01 \x01(\t\"6\n\x0bTestHarness\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x08\n\x04TAST\x10\x01\x12\x0c\n\x08\x41UTOTEST\x10\x02\"\x10\n\x0eVmTestResponse\"\xe9\x01\n\x13MoblabVmTestRequest\x12\"\n\x06\x63hroot\x18\x01 \x01(\x0b\x32\x12.chromiumos.Chroot\x12@\n\rimage_payload\x18\x02 \x01(\x0b\x32).chromite.api.MoblabVmTestRequest.Payload\x12\x41\n\x0e\x63\x61\x63he_payloads\x18\x03 \x03(\x0b\x32).chromite.api.MoblabVmTestRequest.Payload\x1a)\n\x07Payload\x12\x1e\n\x04path\x18\x01 \x01(\x0b\x32\x10.chromiumos.Path\"\x16\n\x14MoblabVmTestResponse\"\x8b\x01\n\x1fSimpleChromeWorkflowTestRequest\x12&\n\x07sysroot\x18\x01 \x01(\x0b\x32\x15.chromite.api.Sysroot\x12\x13\n\x0b\x63hrome_root\x18\x02 \x01(\t\x12+\n\x0bgoma_config\x18\x03 \x01(\x0b\x32\x16.chromiumos.GomaConfig\"\"\n SimpleChromeWorkflowTestResponse2\xb1\x06\n\x0bTestService\x12r\n\x13\x42uildTargetUnitTest\x12(.chromite.api.BuildTargetUnitTestRequest\x1a).chromite.api.BuildTargetUnitTestResponse\"\x06\xc2\xed\x1a\x02\x10\x02\x12\x61\n\x10\x43hromiteUnitTest\x12%.chromite.api.ChromiteUnitTestRequest\x1a&.chromite.api.ChromiteUnitTestResponse\x12[\n\x0e\x43hromitePytest\x12#.chromite.api.ChromitePytestRequest\x1a$.chromite.api.ChromitePytestResponse\x12^\n\x0f\x43rosSigningTest\x12$.chromite.api.CrosSigningTestRequest\x1a%.chromite.api.CrosSigningTestResponse\x12X\n\rDebugInfoTest\x12\".chromite.api.DebugInfoTestRequest\x1a#.chromite.api.DebugInfoTestResponse\x12\x43\n\x06VmTest\x12\x1b.chromite.api.VmTestRequest\x1a\x1c.chromite.api.VmTestResponse\x12]\n\x0cMoblabVmTest\x12!.chromite.api.MoblabVmTestRequest\x1a\".chromite.api.MoblabVmTestResponse\"\x06\xc2\xed\x1a\x02\x10\x02\x12\x81\x01\n\x18SimpleChromeWorkflowTest\x12-.chromite.api.SimpleChromeWorkflowTestRequest\x1a..chromite.api.SimpleChromeWorkflowTestResponse\"\x06\xc2\xed\x1a\x02\x10\x02\x1a\x0c\xc2\xed\x1a\x08\n\x04test\x10\x01\x42\x38Z6go.chromium.org/chromiumos/infra/proto/go/chromite/apib\x06proto3')
   ,
   dependencies=[chromite_dot_api_dot_build__api__pb2.DESCRIPTOR,chromite_dot_api_dot_sysroot__pb2.DESCRIPTOR,chromiumos_dot_common__pb2.DESCRIPTOR,chromiumos_dot_metrics__pb2.DESCRIPTOR,])
 
@@ -50,8 +50,8 @@
   ],
   containing_type=None,
   serialized_options=None,
-  serialized_start=1475,
-  serialized_end=1529,
+  serialized_start=1511,
+  serialized_end=1565,
 )
 _sym_db.RegisterEnumDescriptor(_VMTESTREQUEST_TESTHARNESS)
 
@@ -84,6 +84,13 @@
       message_type=None, enum_type=None, containing_type=None,
       is_extension=False, extension_scope=None,
       serialized_options=None, file=DESCRIPTOR),
+    _descriptor.FieldDescriptor(
+      name='testable_packages_optional', full_name='chromite.api.BuildTargetUnitTestRequest.Flags.testable_packages_optional', index=3,
+      number=4, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR),
   ],
   extensions=[
   ],
@@ -97,7 +104,7 @@
   oneofs=[
   ],
   serialized_start=443,
-  serialized_end=523,
+  serialized_end=559,
 )
 
 _BUILDTARGETUNITTESTREQUEST = _descriptor.Descriptor(
@@ -162,7 +169,7 @@
   oneofs=[
   ],
   serialized_start=151,
-  serialized_end=523,
+  serialized_end=559,
 )
 
 
@@ -206,8 +213,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=526,
-  serialized_end=668,
+  serialized_start=562,
+  serialized_end=704,
 )
 
 
@@ -237,8 +244,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=670,
-  serialized_end=731,
+  serialized_start=706,
+  serialized_end=767,
 )
 
 
@@ -261,8 +268,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=733,
-  serialized_end=759,
+  serialized_start=769,
+  serialized_end=795,
 )
 
 
@@ -292,8 +299,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=761,
-  serialized_end=820,
+  serialized_start=797,
+  serialized_end=856,
 )
 
 
@@ -316,8 +323,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=822,
-  serialized_end=846,
+  serialized_start=858,
+  serialized_end=882,
 )
 
 
@@ -347,8 +354,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=848,
-  serialized_end=908,
+  serialized_start=884,
+  serialized_end=944,
 )
 
 
@@ -371,8 +378,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=910,
-  serialized_end=935,
+  serialized_start=946,
+  serialized_end=971,
 )
 
 
@@ -409,8 +416,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=937,
-  serialized_end=1035,
+  serialized_start=973,
+  serialized_end=1071,
 )
 
 
@@ -433,8 +440,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1037,
-  serialized_end=1060,
+  serialized_start=1073,
+  serialized_end=1096,
 )
 
 
@@ -471,8 +478,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1376,
-  serialized_end=1446,
+  serialized_start=1412,
+  serialized_end=1482,
 )
 
 _VMTESTREQUEST_VMTEST = _descriptor.Descriptor(
@@ -501,8 +508,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1448,
-  serialized_end=1473,
+  serialized_start=1484,
+  serialized_end=1509,
 )
 
 _VMTESTREQUEST = _descriptor.Descriptor(
@@ -567,8 +574,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1063,
-  serialized_end=1529,
+  serialized_start=1099,
+  serialized_end=1565,
 )
 
 
@@ -591,8 +598,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1531,
-  serialized_end=1547,
+  serialized_start=1567,
+  serialized_end=1583,
 )
 
 
@@ -622,8 +629,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1742,
-  serialized_end=1783,
+  serialized_start=1778,
+  serialized_end=1819,
 )
 
 _MOBLABVMTESTREQUEST = _descriptor.Descriptor(
@@ -666,8 +673,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1550,
-  serialized_end=1783,
+  serialized_start=1586,
+  serialized_end=1819,
 )
 
 
@@ -690,8 +697,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1785,
-  serialized_end=1807,
+  serialized_start=1821,
+  serialized_end=1843,
 )
 
 
@@ -735,8 +742,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1810,
-  serialized_end=1949,
+  serialized_start=1846,
+  serialized_end=1985,
 )
 
 
@@ -759,8 +766,8 @@
   extension_ranges=[],
   oneofs=[
   ],
-  serialized_start=1951,
-  serialized_end=1985,
+  serialized_start=1987,
+  serialized_end=2021,
 )
 
 _BUILDTARGETUNITTESTREQUEST_FLAGS.containing_type = _BUILDTARGETUNITTESTREQUEST
@@ -964,8 +971,8 @@
   file=DESCRIPTOR,
   index=0,
   serialized_options=_b('\302\355\032\010\n\004test\020\001'),
-  serialized_start=1988,
-  serialized_end=2805,
+  serialized_start=2024,
+  serialized_end=2841,
   methods=[
   _descriptor.MethodDescriptor(
     name='BuildTargetUnitTest',
diff --git a/service/test.py b/service/test.py
index ae63f1e..e06daf6 100644
--- a/service/test.py
+++ b/service/test.py
@@ -53,7 +53,8 @@
                         packages=None,
                         blacklist=None,
                         was_built=True,
-                        code_coverage=False):
+                        code_coverage=False,
+                        testable_packages_optional=False):
   """Run the ebuild unit tests for the target.
 
   Args:
@@ -64,6 +65,8 @@
     blacklist (list[str]|None): Tests to skip.
     was_built (bool): Whether packages were built.
     code_coverage (bool): Whether to produce code coverage data.
+    testable_packages_optional (bool): Whether to allow no testable packages to
+    be found.
 
   Returns:
     BuildTargetUnitTestResult
@@ -78,6 +81,9 @@
   if blacklist:
     cmd.extend(['--blacklist_packages', ' '.join(blacklist)])
 
+  if testable_packages_optional:
+    cmd.append('--no-testable-packages-ok')
+
   if not was_built:
     cmd.append('--assume-empty-sysroot')
 
diff --git a/service/test_unittest.py b/service/test_unittest.py
index 3b3bd9e..6e98028 100644
--- a/service/test_unittest.py
+++ b/service/test_unittest.py
@@ -98,6 +98,12 @@
                              blacklist=blacklist)
     self.assertCommandContains(['--blacklist_packages', 'foo/bar cat/pkg'])
 
+  def testTestablePackagesOptional(self):
+    """Test the testable packages optional argument."""
+    test.BuildTargetUnitTest(
+        self.build_target, self.chroot, testable_packages_optional=True)
+    self.assertCommandContains(['--no-testable-packages-ok'])
+
   def testFailure(self):
     """Test non-zero return code and failed package handling."""
     packages = ['foo/bar', 'cat/pkg']