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