xref: /aosp_15_r20/build/make/tools/releasetools/test_utils.py (revision 9e94795a3d4ef5c1d47486f9a02bb378756cea8a)
1*9e94795aSAndroid Build Coastguard Worker#!/usr/bin/env python
2*9e94795aSAndroid Build Coastguard Worker#
3*9e94795aSAndroid Build Coastguard Worker# Copyright (C) 2018 The Android Open Source Project
4*9e94795aSAndroid Build Coastguard Worker#
5*9e94795aSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
6*9e94795aSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
7*9e94795aSAndroid Build Coastguard Worker# You may obtain a copy of the License at
8*9e94795aSAndroid Build Coastguard Worker#
9*9e94795aSAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
10*9e94795aSAndroid Build Coastguard Worker#
11*9e94795aSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
12*9e94795aSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
13*9e94795aSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14*9e94795aSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
15*9e94795aSAndroid Build Coastguard Worker# limitations under the License.
16*9e94795aSAndroid Build Coastguard Worker#
17*9e94795aSAndroid Build Coastguard Worker
18*9e94795aSAndroid Build Coastguard Worker"""
19*9e94795aSAndroid Build Coastguard WorkerUtils for running unittests.
20*9e94795aSAndroid Build Coastguard Worker"""
21*9e94795aSAndroid Build Coastguard Worker
22*9e94795aSAndroid Build Coastguard Workerimport avbtool
23*9e94795aSAndroid Build Coastguard Workerimport logging
24*9e94795aSAndroid Build Coastguard Workerimport os
25*9e94795aSAndroid Build Coastguard Workerimport os.path
26*9e94795aSAndroid Build Coastguard Workerimport re
27*9e94795aSAndroid Build Coastguard Workerimport struct
28*9e94795aSAndroid Build Coastguard Workerimport sys
29*9e94795aSAndroid Build Coastguard Workerimport unittest
30*9e94795aSAndroid Build Coastguard Workerimport zipfile
31*9e94795aSAndroid Build Coastguard Worker
32*9e94795aSAndroid Build Coastguard Workerimport common
33*9e94795aSAndroid Build Coastguard Worker
34*9e94795aSAndroid Build Coastguard Worker# Some test runner doesn't like outputs from stderr.
35*9e94795aSAndroid Build Coastguard Workerlogging.basicConfig(stream=sys.stdout)
36*9e94795aSAndroid Build Coastguard Worker
37*9e94795aSAndroid Build Coastguard WorkerALLOWED_TEST_SUBDIRS = ('merge',)
38*9e94795aSAndroid Build Coastguard Worker
39*9e94795aSAndroid Build Coastguard Worker# Use ANDROID_BUILD_TOP as an indicator to tell if the needed tools (e.g.
40*9e94795aSAndroid Build Coastguard Worker# avbtool, mke2fs) are available while running the tests, unless
41*9e94795aSAndroid Build Coastguard Worker# FORCE_RUN_RELEASETOOLS is set to '1'. Not having the required vars means we
42*9e94795aSAndroid Build Coastguard Worker# can't run the tests that require external tools.
43*9e94795aSAndroid Build Coastguard WorkerEXTERNAL_TOOLS_UNAVAILABLE = (
44*9e94795aSAndroid Build Coastguard Worker    not os.environ.get('ANDROID_BUILD_TOP') and
45*9e94795aSAndroid Build Coastguard Worker    os.environ.get('FORCE_RUN_RELEASETOOLS') != '1')
46*9e94795aSAndroid Build Coastguard Worker
47*9e94795aSAndroid Build Coastguard Worker
48*9e94795aSAndroid Build Coastguard Workerdef SkipIfExternalToolsUnavailable():
49*9e94795aSAndroid Build Coastguard Worker  """Decorator function that allows skipping tests per tools availability."""
50*9e94795aSAndroid Build Coastguard Worker  if EXTERNAL_TOOLS_UNAVAILABLE:
51*9e94795aSAndroid Build Coastguard Worker    return unittest.skip('External tools unavailable')
52*9e94795aSAndroid Build Coastguard Worker  return lambda func: func
53*9e94795aSAndroid Build Coastguard Worker
54*9e94795aSAndroid Build Coastguard Worker
55*9e94795aSAndroid Build Coastguard Workerdef get_testdata_dir():
56*9e94795aSAndroid Build Coastguard Worker  """Returns the testdata dir, in relative to the script dir."""
57*9e94795aSAndroid Build Coastguard Worker  # The script dir is the one we want, which could be different from pwd.
58*9e94795aSAndroid Build Coastguard Worker  current_dir = os.path.dirname(os.path.realpath(__file__))
59*9e94795aSAndroid Build Coastguard Worker  return os.path.join(current_dir, 'testdata')
60*9e94795aSAndroid Build Coastguard Worker
61*9e94795aSAndroid Build Coastguard Worker
62*9e94795aSAndroid Build Coastguard Workerdef get_current_dir():
63*9e94795aSAndroid Build Coastguard Worker  """Returns the current dir, relative to the script dir."""
64*9e94795aSAndroid Build Coastguard Worker  # The script dir is the one we want, which could be different from pwd.
65*9e94795aSAndroid Build Coastguard Worker  current_dir = os.path.dirname(os.path.realpath(__file__))
66*9e94795aSAndroid Build Coastguard Worker  return current_dir
67*9e94795aSAndroid Build Coastguard Worker
68*9e94795aSAndroid Build Coastguard Worker
69*9e94795aSAndroid Build Coastguard Workerdef get_search_path():
70*9e94795aSAndroid Build Coastguard Worker  """Returns the search path that has 'framework/signapk.jar' under."""
71*9e94795aSAndroid Build Coastguard Worker
72*9e94795aSAndroid Build Coastguard Worker  def signapk_exists(path):
73*9e94795aSAndroid Build Coastguard Worker    signapk_path = os.path.realpath(
74*9e94795aSAndroid Build Coastguard Worker        os.path.join(path, 'framework', 'signapk.jar'))
75*9e94795aSAndroid Build Coastguard Worker    return os.path.exists(signapk_path)
76*9e94795aSAndroid Build Coastguard Worker
77*9e94795aSAndroid Build Coastguard Worker  # Try with ANDROID_BUILD_TOP first.
78*9e94795aSAndroid Build Coastguard Worker  full_path = os.path.realpath(os.path.join(
79*9e94795aSAndroid Build Coastguard Worker      os.environ.get('ANDROID_BUILD_TOP', ''), 'out', 'host', 'linux-x86'))
80*9e94795aSAndroid Build Coastguard Worker  if signapk_exists(full_path):
81*9e94795aSAndroid Build Coastguard Worker    return full_path
82*9e94795aSAndroid Build Coastguard Worker
83*9e94795aSAndroid Build Coastguard Worker  # Otherwise try going with relative pathes.
84*9e94795aSAndroid Build Coastguard Worker  current_dir = os.path.dirname(os.path.realpath(__file__))
85*9e94795aSAndroid Build Coastguard Worker  for path in (
86*9e94795aSAndroid Build Coastguard Worker      # In relative to 'build/make/tools/releasetools' in the Android source.
87*9e94795aSAndroid Build Coastguard Worker      ['..'] * 4 + ['out', 'host', 'linux-x86'],
88*9e94795aSAndroid Build Coastguard Worker      # Or running the script unpacked from otatools.zip.
89*9e94795aSAndroid Build Coastguard Worker          ['..']):
90*9e94795aSAndroid Build Coastguard Worker    full_path = os.path.realpath(os.path.join(current_dir, *path))
91*9e94795aSAndroid Build Coastguard Worker    if signapk_exists(full_path):
92*9e94795aSAndroid Build Coastguard Worker      return full_path
93*9e94795aSAndroid Build Coastguard Worker  return None
94*9e94795aSAndroid Build Coastguard Worker
95*9e94795aSAndroid Build Coastguard Worker
96*9e94795aSAndroid Build Coastguard Workerdef append_avb_footer(file_path: str, partition_name: str = ""):
97*9e94795aSAndroid Build Coastguard Worker  avb = avbtool.AvbTool()
98*9e94795aSAndroid Build Coastguard Worker  try:
99*9e94795aSAndroid Build Coastguard Worker    args = ["avbtool", "add_hashtree_footer", "--image", file_path,
100*9e94795aSAndroid Build Coastguard Worker            "--partition_name", partition_name, "--do_not_generate_fec"]
101*9e94795aSAndroid Build Coastguard Worker    avb.run(args)
102*9e94795aSAndroid Build Coastguard Worker  except SystemExit:
103*9e94795aSAndroid Build Coastguard Worker    raise ValueError(f"Failed to append hashtree footer {args}")
104*9e94795aSAndroid Build Coastguard Worker
105*9e94795aSAndroid Build Coastguard Worker
106*9e94795aSAndroid Build Coastguard Workerdef erase_avb_footer(file_path: str):
107*9e94795aSAndroid Build Coastguard Worker  avb = avbtool.AvbTool()
108*9e94795aSAndroid Build Coastguard Worker  try:
109*9e94795aSAndroid Build Coastguard Worker    args = ["avbtool", "erase_footer", "--image", file_path]
110*9e94795aSAndroid Build Coastguard Worker    avb.run(args)
111*9e94795aSAndroid Build Coastguard Worker  except SystemExit:
112*9e94795aSAndroid Build Coastguard Worker    raise ValueError(f"Failed to erase hashtree footer {args}")
113*9e94795aSAndroid Build Coastguard Worker
114*9e94795aSAndroid Build Coastguard Worker
115*9e94795aSAndroid Build Coastguard Workerdef construct_sparse_image(chunks, partition_name: str = ""):
116*9e94795aSAndroid Build Coastguard Worker  """Returns a sparse image file constructed from the given chunks.
117*9e94795aSAndroid Build Coastguard Worker
118*9e94795aSAndroid Build Coastguard Worker  From system/core/libsparse/sparse_format.h.
119*9e94795aSAndroid Build Coastguard Worker  typedef struct sparse_header {
120*9e94795aSAndroid Build Coastguard Worker    __le32 magic;  // 0xed26ff3a
121*9e94795aSAndroid Build Coastguard Worker    __le16 major_version;  // (0x1) - reject images with higher major versions
122*9e94795aSAndroid Build Coastguard Worker    __le16 minor_version;  // (0x0) - allow images with higer minor versions
123*9e94795aSAndroid Build Coastguard Worker    __le16 file_hdr_sz;  // 28 bytes for first revision of the file format
124*9e94795aSAndroid Build Coastguard Worker    __le16 chunk_hdr_sz;  // 12 bytes for first revision of the file format
125*9e94795aSAndroid Build Coastguard Worker    __le32 blk_sz;  // block size in bytes, must be a multiple of 4 (4096)
126*9e94795aSAndroid Build Coastguard Worker    __le32 total_blks;  // total blocks in the non-sparse output image
127*9e94795aSAndroid Build Coastguard Worker    __le32 total_chunks;  // total chunks in the sparse input image
128*9e94795aSAndroid Build Coastguard Worker    __le32 image_checksum;  // CRC32 checksum of the original data, counting
129*9e94795aSAndroid Build Coastguard Worker                            // "don't care" as 0. Standard 802.3 polynomial,
130*9e94795aSAndroid Build Coastguard Worker                            // use a Public Domain table implementation
131*9e94795aSAndroid Build Coastguard Worker  } sparse_header_t;
132*9e94795aSAndroid Build Coastguard Worker
133*9e94795aSAndroid Build Coastguard Worker  typedef struct chunk_header {
134*9e94795aSAndroid Build Coastguard Worker    __le16 chunk_type;  // 0xCAC1 -> raw; 0xCAC2 -> fill;
135*9e94795aSAndroid Build Coastguard Worker                        // 0xCAC3 -> don't care
136*9e94795aSAndroid Build Coastguard Worker    __le16 reserved1;
137*9e94795aSAndroid Build Coastguard Worker    __le32 chunk_sz;  // in blocks in output image
138*9e94795aSAndroid Build Coastguard Worker    __le32 total_sz;  // in bytes of chunk input file including chunk header
139*9e94795aSAndroid Build Coastguard Worker                      // and data
140*9e94795aSAndroid Build Coastguard Worker  } chunk_header_t;
141*9e94795aSAndroid Build Coastguard Worker
142*9e94795aSAndroid Build Coastguard Worker  Args:
143*9e94795aSAndroid Build Coastguard Worker    chunks: A list of chunks to be written. Each entry should be a tuple of
144*9e94795aSAndroid Build Coastguard Worker        (chunk_type, block_number).
145*9e94795aSAndroid Build Coastguard Worker
146*9e94795aSAndroid Build Coastguard Worker  Returns:
147*9e94795aSAndroid Build Coastguard Worker    Filename of the created sparse image.
148*9e94795aSAndroid Build Coastguard Worker  """
149*9e94795aSAndroid Build Coastguard Worker  SPARSE_HEADER_MAGIC = 0xED26FF3A
150*9e94795aSAndroid Build Coastguard Worker  SPARSE_HEADER_FORMAT = "<I4H4I"
151*9e94795aSAndroid Build Coastguard Worker  CHUNK_HEADER_FORMAT = "<2H2I"
152*9e94795aSAndroid Build Coastguard Worker
153*9e94795aSAndroid Build Coastguard Worker  sparse_image = common.MakeTempFile(prefix='sparse-', suffix='.img')
154*9e94795aSAndroid Build Coastguard Worker  with open(sparse_image, 'wb') as fp:
155*9e94795aSAndroid Build Coastguard Worker    fp.write(struct.pack(
156*9e94795aSAndroid Build Coastguard Worker        SPARSE_HEADER_FORMAT, SPARSE_HEADER_MAGIC, 1, 0, 28, 12, 4096,
157*9e94795aSAndroid Build Coastguard Worker        sum(chunk[1] for chunk in chunks),
158*9e94795aSAndroid Build Coastguard Worker        len(chunks), 0))
159*9e94795aSAndroid Build Coastguard Worker
160*9e94795aSAndroid Build Coastguard Worker    for chunk in chunks:
161*9e94795aSAndroid Build Coastguard Worker      data_size = 0
162*9e94795aSAndroid Build Coastguard Worker      if chunk[0] == 0xCAC1:
163*9e94795aSAndroid Build Coastguard Worker        data_size = 4096 * chunk[1]
164*9e94795aSAndroid Build Coastguard Worker      elif chunk[0] == 0xCAC2:
165*9e94795aSAndroid Build Coastguard Worker        data_size = 4
166*9e94795aSAndroid Build Coastguard Worker      elif chunk[0] == 0xCAC3:
167*9e94795aSAndroid Build Coastguard Worker        pass
168*9e94795aSAndroid Build Coastguard Worker      else:
169*9e94795aSAndroid Build Coastguard Worker        assert False, "Unsupported chunk type: {}".format(chunk[0])
170*9e94795aSAndroid Build Coastguard Worker
171*9e94795aSAndroid Build Coastguard Worker      fp.write(struct.pack(
172*9e94795aSAndroid Build Coastguard Worker          CHUNK_HEADER_FORMAT, chunk[0], 0, chunk[1], data_size + 12))
173*9e94795aSAndroid Build Coastguard Worker      if data_size != 0:
174*9e94795aSAndroid Build Coastguard Worker        fp.write(os.urandom(data_size))
175*9e94795aSAndroid Build Coastguard Worker
176*9e94795aSAndroid Build Coastguard Worker  append_avb_footer(sparse_image, partition_name)
177*9e94795aSAndroid Build Coastguard Worker  return sparse_image
178*9e94795aSAndroid Build Coastguard Worker
179*9e94795aSAndroid Build Coastguard Worker
180*9e94795aSAndroid Build Coastguard Workerclass MockScriptWriter(object):
181*9e94795aSAndroid Build Coastguard Worker  """A class that mocks edify_generator.EdifyGenerator.
182*9e94795aSAndroid Build Coastguard Worker
183*9e94795aSAndroid Build Coastguard Worker  It simply pushes the incoming arguments onto script stack, which is to assert
184*9e94795aSAndroid Build Coastguard Worker  the calls to EdifyGenerator functions.
185*9e94795aSAndroid Build Coastguard Worker  """
186*9e94795aSAndroid Build Coastguard Worker
187*9e94795aSAndroid Build Coastguard Worker  def __init__(self, enable_comments=False):
188*9e94795aSAndroid Build Coastguard Worker    self.lines = []
189*9e94795aSAndroid Build Coastguard Worker    self.enable_comments = enable_comments
190*9e94795aSAndroid Build Coastguard Worker
191*9e94795aSAndroid Build Coastguard Worker  def Mount(self, *args):
192*9e94795aSAndroid Build Coastguard Worker    self.lines.append(('Mount',) + args)
193*9e94795aSAndroid Build Coastguard Worker
194*9e94795aSAndroid Build Coastguard Worker  def AssertDevice(self, *args):
195*9e94795aSAndroid Build Coastguard Worker    self.lines.append(('AssertDevice',) + args)
196*9e94795aSAndroid Build Coastguard Worker
197*9e94795aSAndroid Build Coastguard Worker  def AssertOemProperty(self, *args):
198*9e94795aSAndroid Build Coastguard Worker    self.lines.append(('AssertOemProperty',) + args)
199*9e94795aSAndroid Build Coastguard Worker
200*9e94795aSAndroid Build Coastguard Worker  def AssertFingerprintOrThumbprint(self, *args):
201*9e94795aSAndroid Build Coastguard Worker    self.lines.append(('AssertFingerprintOrThumbprint',) + args)
202*9e94795aSAndroid Build Coastguard Worker
203*9e94795aSAndroid Build Coastguard Worker  def AssertSomeFingerprint(self, *args):
204*9e94795aSAndroid Build Coastguard Worker    self.lines.append(('AssertSomeFingerprint',) + args)
205*9e94795aSAndroid Build Coastguard Worker
206*9e94795aSAndroid Build Coastguard Worker  def AssertSomeThumbprint(self, *args):
207*9e94795aSAndroid Build Coastguard Worker    self.lines.append(('AssertSomeThumbprint',) + args)
208*9e94795aSAndroid Build Coastguard Worker
209*9e94795aSAndroid Build Coastguard Worker  def Comment(self, comment):
210*9e94795aSAndroid Build Coastguard Worker    if not self.enable_comments:
211*9e94795aSAndroid Build Coastguard Worker      return
212*9e94795aSAndroid Build Coastguard Worker    self.lines.append('# {}'.format(comment))
213*9e94795aSAndroid Build Coastguard Worker
214*9e94795aSAndroid Build Coastguard Worker  def AppendExtra(self, extra):
215*9e94795aSAndroid Build Coastguard Worker    self.lines.append(extra)
216*9e94795aSAndroid Build Coastguard Worker
217*9e94795aSAndroid Build Coastguard Worker  def __str__(self):
218*9e94795aSAndroid Build Coastguard Worker    return '\n'.join(self.lines)
219*9e94795aSAndroid Build Coastguard Worker
220*9e94795aSAndroid Build Coastguard Worker
221*9e94795aSAndroid Build Coastguard Workerclass ReleaseToolsTestCase(unittest.TestCase):
222*9e94795aSAndroid Build Coastguard Worker  """A common base class for all the releasetools unittests."""
223*9e94795aSAndroid Build Coastguard Worker
224*9e94795aSAndroid Build Coastguard Worker  def tearDown(self):
225*9e94795aSAndroid Build Coastguard Worker    common.Cleanup()
226*9e94795aSAndroid Build Coastguard Worker
227*9e94795aSAndroid Build Coastguard Worker
228*9e94795aSAndroid Build Coastguard Workerclass PropertyFilesTestCase(ReleaseToolsTestCase):
229*9e94795aSAndroid Build Coastguard Worker
230*9e94795aSAndroid Build Coastguard Worker  @staticmethod
231*9e94795aSAndroid Build Coastguard Worker  def construct_zip_package(entries):
232*9e94795aSAndroid Build Coastguard Worker    zip_file = common.MakeTempFile(suffix='.zip')
233*9e94795aSAndroid Build Coastguard Worker    with zipfile.ZipFile(zip_file, 'w', allowZip64=True) as zip_fp:
234*9e94795aSAndroid Build Coastguard Worker      for entry in entries:
235*9e94795aSAndroid Build Coastguard Worker        zip_fp.writestr(
236*9e94795aSAndroid Build Coastguard Worker            entry,
237*9e94795aSAndroid Build Coastguard Worker            entry.replace('.', '-').upper(),
238*9e94795aSAndroid Build Coastguard Worker            zipfile.ZIP_STORED)
239*9e94795aSAndroid Build Coastguard Worker    return zip_file
240*9e94795aSAndroid Build Coastguard Worker
241*9e94795aSAndroid Build Coastguard Worker  @staticmethod
242*9e94795aSAndroid Build Coastguard Worker  def _parse_property_files_string(data):
243*9e94795aSAndroid Build Coastguard Worker    result = {}
244*9e94795aSAndroid Build Coastguard Worker    for token in data.split(','):
245*9e94795aSAndroid Build Coastguard Worker      name, info = token.split(':', 1)
246*9e94795aSAndroid Build Coastguard Worker      result[name] = info
247*9e94795aSAndroid Build Coastguard Worker    return result
248*9e94795aSAndroid Build Coastguard Worker
249*9e94795aSAndroid Build Coastguard Worker  def setUp(self):
250*9e94795aSAndroid Build Coastguard Worker    common.OPTIONS.no_signing = False
251*9e94795aSAndroid Build Coastguard Worker
252*9e94795aSAndroid Build Coastguard Worker  def _verify_entries(self, input_file, tokens, entries):
253*9e94795aSAndroid Build Coastguard Worker    for entry in entries:
254*9e94795aSAndroid Build Coastguard Worker      offset, size = map(int, tokens[entry].split(':'))
255*9e94795aSAndroid Build Coastguard Worker      with open(input_file, 'rb') as input_fp:
256*9e94795aSAndroid Build Coastguard Worker        input_fp.seek(offset)
257*9e94795aSAndroid Build Coastguard Worker        if entry == 'metadata':
258*9e94795aSAndroid Build Coastguard Worker          expected = b'META-INF/COM/ANDROID/METADATA'
259*9e94795aSAndroid Build Coastguard Worker        elif entry == 'metadata.pb':
260*9e94795aSAndroid Build Coastguard Worker          expected = b'META-INF/COM/ANDROID/METADATA-PB'
261*9e94795aSAndroid Build Coastguard Worker        else:
262*9e94795aSAndroid Build Coastguard Worker          expected = entry.replace('.', '-').upper().encode()
263*9e94795aSAndroid Build Coastguard Worker        self.assertEqual(expected, input_fp.read(size))
264*9e94795aSAndroid Build Coastguard Worker
265*9e94795aSAndroid Build Coastguard Worker
266*9e94795aSAndroid Build Coastguard Workerif __name__ == '__main__':
267*9e94795aSAndroid Build Coastguard Worker  # We only want to run tests from the top level directory. Unfortunately the
268*9e94795aSAndroid Build Coastguard Worker  # pattern option of unittest.discover, internally using fnmatch, doesn't
269*9e94795aSAndroid Build Coastguard Worker  # provide a good API to filter the test files based on directory. So we do an
270*9e94795aSAndroid Build Coastguard Worker  # os walk and load them manually.
271*9e94795aSAndroid Build Coastguard Worker  test_modules = []
272*9e94795aSAndroid Build Coastguard Worker  base_path = os.path.dirname(os.path.realpath(__file__))
273*9e94795aSAndroid Build Coastguard Worker  test_dirs = [base_path] + [
274*9e94795aSAndroid Build Coastguard Worker      os.path.join(base_path, subdir) for subdir in ALLOWED_TEST_SUBDIRS
275*9e94795aSAndroid Build Coastguard Worker  ]
276*9e94795aSAndroid Build Coastguard Worker  for dirpath, _, files in os.walk(base_path):
277*9e94795aSAndroid Build Coastguard Worker    for fn in files:
278*9e94795aSAndroid Build Coastguard Worker      if dirpath in test_dirs and re.match('test_.*\\.py$', fn):
279*9e94795aSAndroid Build Coastguard Worker        test_modules.append(fn[:-3])
280*9e94795aSAndroid Build Coastguard Worker
281*9e94795aSAndroid Build Coastguard Worker  test_suite = unittest.TestLoader().loadTestsFromNames(test_modules)
282*9e94795aSAndroid Build Coastguard Worker
283*9e94795aSAndroid Build Coastguard Worker  # atest needs a verbosity level of >= 2 to correctly parse the result.
284*9e94795aSAndroid Build Coastguard Worker  unittest.TextTestRunner(verbosity=2).run(test_suite)
285