You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
324 lines
12 KiB
324 lines
12 KiB
#!/usr/bin/env python
|
|
#
|
|
# Copyright (C) 2020 The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
"""Unit tests for apex_compression_tool."""
|
|
import hashlib
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
|
|
|
import apex_manifest_pb2
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
TEST_APEX = 'com.android.example.apex'
|
|
|
|
# In order to debug test failures, set DEBUG_TEST to True and run the test from
|
|
# local workstation bypassing atest, e.g.:
|
|
# $ m apex_compression_tool_test && \
|
|
# out/host/linux-x86/nativetest64/apex_compression_tool_test/\
|
|
# apex_compression_tool_test
|
|
#
|
|
# the test will print out the command used, and the temporary files used by the
|
|
# test.
|
|
DEBUG_TEST = False
|
|
|
|
|
|
def run(args, verbose=None, **kwargs):
|
|
"""Creates and returns a subprocess.Popen object.
|
|
|
|
Args:
|
|
args: The command represented as a list of strings.
|
|
verbose: Whether the commands should be shown. Default to the global
|
|
verbosity if unspecified.
|
|
kwargs: Any additional args to be passed to subprocess.Popen(), such as env,
|
|
stdin, etc. stdout and stderr will default to subprocess.PIPE and
|
|
subprocess.STDOUT respectively unless caller specifies any of them.
|
|
universal_newlines will default to True, as most of the users in
|
|
releasetools expect string output.
|
|
|
|
Returns:
|
|
A subprocess.Popen object.
|
|
"""
|
|
if 'stdout' not in kwargs and 'stderr' not in kwargs:
|
|
kwargs['stdout'] = subprocess.PIPE
|
|
kwargs['stderr'] = subprocess.STDOUT
|
|
if 'universal_newlines' not in kwargs:
|
|
kwargs['universal_newlines'] = True
|
|
if DEBUG_TEST:
|
|
print('\nRunning: \n%s\n' % ' '.join(args))
|
|
# Don't log any if caller explicitly says so.
|
|
if verbose:
|
|
logger.info(' Running: \'%s\'', ' '.join(args))
|
|
return subprocess.Popen(args, **kwargs)
|
|
|
|
|
|
def run_host_command(args, verbose=None, **kwargs):
|
|
host_build_top = os.environ.get('ANDROID_BUILD_TOP')
|
|
if host_build_top:
|
|
host_command_dir = os.path.join(host_build_top,
|
|
'out/soong/host/linux-x86/bin')
|
|
args[0] = os.path.join(host_command_dir, args[0])
|
|
return run_and_check_output(args, verbose, **kwargs)
|
|
|
|
|
|
def run_and_check_output(args, verbose=None, **kwargs):
|
|
"""Runs the given command and returns the output.
|
|
|
|
Args:
|
|
args: The command represented as a list of strings.
|
|
verbose: Whether the commands should be shown. Default to the global
|
|
verbosity if unspecified.
|
|
kwargs: Any additional args to be passed to subprocess.Popen(), such as env,
|
|
stdin, etc. stdout and stderr will default to subprocess.PIPE and
|
|
subprocess.STDOUT respectively unless caller specifies any of them.
|
|
|
|
Returns:
|
|
The output string.
|
|
|
|
Raises:
|
|
ExternalError: On non-zero exit from the command.
|
|
"""
|
|
|
|
proc = run(args, verbose=verbose, **kwargs)
|
|
output, _ = proc.communicate()
|
|
if output is None:
|
|
output = ''
|
|
# Don't log any if caller explicitly says so.
|
|
if verbose:
|
|
logger.info('%s', output.rstrip())
|
|
if proc.returncode != 0:
|
|
raise RuntimeError(
|
|
"Failed to run command '{}' (exit code {}):\n{}".format(
|
|
args, proc.returncode, output))
|
|
return output
|
|
|
|
|
|
def get_current_dir():
|
|
"""Returns the current dir, relative to the script dir."""
|
|
# The script dir is the one we want, which could be different from pwd.
|
|
current_dir = os.path.dirname(os.path.realpath(__file__))
|
|
return current_dir
|
|
|
|
|
|
def get_sha1sum(file_path):
|
|
h = hashlib.sha256()
|
|
|
|
with open(file_path, 'rb') as file:
|
|
while True:
|
|
# Reading is buffered, so we can read smaller chunks.
|
|
chunk = file.read(h.block_size)
|
|
if not chunk:
|
|
break
|
|
h.update(chunk)
|
|
|
|
return h.hexdigest()
|
|
|
|
|
|
class ApexCompressionTest(unittest.TestCase):
|
|
def setUp(self):
|
|
self._to_cleanup = []
|
|
|
|
def tearDown(self):
|
|
if not DEBUG_TEST:
|
|
for i in self._to_cleanup:
|
|
if os.path.isdir(i):
|
|
shutil.rmtree(i, ignore_errors=True)
|
|
else:
|
|
os.remove(i)
|
|
del self._to_cleanup[:]
|
|
else:
|
|
print('Cleanup: ' + str(self._to_cleanup))
|
|
|
|
def _run_apex_compression_tool(self, args):
|
|
cmd = ['apex_compression_tool']
|
|
host_build_top = os.environ.get('ANDROID_BUILD_TOP')
|
|
if host_build_top:
|
|
os.environ['APEX_COMPRESSION_TOOL_PATH'] = (
|
|
os.path.join(host_build_top, 'out/soong/host/linux-x86/bin')
|
|
+ ':' + os.path.join(host_build_top, 'prebuilts/sdk/tools/linux/bin'))
|
|
else:
|
|
os.environ['APEX_COMPRESSION_TOOL_PATH'] = os.path.dirname(
|
|
shutil.which('apex_compression_tool'))
|
|
cmd.extend(args)
|
|
run_host_command(cmd, True)
|
|
|
|
def _get_container_files(self, apex_file_path):
|
|
dir_name = tempfile.mkdtemp(
|
|
prefix=self._testMethodName + '_container_files_')
|
|
self._to_cleanup.append(dir_name)
|
|
with ZipFile(apex_file_path, 'r') as zip_obj:
|
|
zip_obj.extractall(path=dir_name)
|
|
files = {}
|
|
for i in ['apex_manifest.json', 'apex_manifest.pb', 'apex_pubkey',
|
|
'apex_build_info.pb', 'apex_payload.img', 'apex_payload.zip',
|
|
'AndroidManifest.xml', 'original_apex']:
|
|
file_path = os.path.join(dir_name, i)
|
|
if os.path.exists(file_path):
|
|
files[i] = file_path
|
|
|
|
image_file = files.get('apex_payload.img', None)
|
|
if image_file is None:
|
|
image_file = files.get('apex_payload.zip', None)
|
|
else:
|
|
files['apex_payload'] = image_file
|
|
# Also retrieve the root digest of the image
|
|
avbtool_cmd = ['avbtool',
|
|
'print_partition_digests', '--image', files['apex_payload']]
|
|
# avbtool_cmd output has format "<name>: <value>"
|
|
files['digest'] = run_host_command(
|
|
avbtool_cmd, True).split(': ')[1].strip()
|
|
|
|
return files
|
|
|
|
def _get_manifest_string(self, manifest_path):
|
|
cmd = ['conv_apex_manifest']
|
|
cmd.extend([
|
|
'print',
|
|
manifest_path
|
|
])
|
|
return run_host_command(cmd, 'True')
|
|
|
|
# Mutates the manifest located at |manifest_path|
|
|
def _unset_original_apex_digest(self, manifest_path):
|
|
# Open the protobuf
|
|
with open(manifest_path, 'rb') as f:
|
|
pb = apex_manifest_pb2.ApexManifest()
|
|
pb.ParseFromString(f.read())
|
|
pb.ClearField('capexMetadata')
|
|
with open(manifest_path, 'wb') as f:
|
|
f.write(pb.SerializeToString())
|
|
|
|
def _compress_apex(self, uncompressed_apex_fp):
|
|
"""Returns file path to compressed APEX"""
|
|
fd, compressed_apex_fp = tempfile.mkstemp(
|
|
prefix=self._testMethodName + '_compressed_',
|
|
suffix='.capex')
|
|
os.close(fd)
|
|
self._to_cleanup.append(compressed_apex_fp)
|
|
self._run_apex_compression_tool([
|
|
'compress',
|
|
'--input', uncompressed_apex_fp,
|
|
'--output', compressed_apex_fp
|
|
])
|
|
return compressed_apex_fp
|
|
|
|
def _decompress_apex(self, compressed_apex_fp):
|
|
"""Returns file path to decompressed APEX"""
|
|
decompressed_apex_fp = tempfile. \
|
|
NamedTemporaryFile(prefix=self._testMethodName + '_decompressed_',
|
|
suffix='.apex').name
|
|
# Use deapexer to decompress
|
|
cmd = ['deapexer']
|
|
cmd.extend([
|
|
'decompress',
|
|
'--input', compressed_apex_fp,
|
|
'--output', decompressed_apex_fp
|
|
])
|
|
run_host_command(cmd, True)
|
|
|
|
self.assertTrue(os.path.exists(decompressed_apex_fp),
|
|
'Decompressed APEX does not exist')
|
|
self._to_cleanup.append(decompressed_apex_fp)
|
|
return decompressed_apex_fp
|
|
|
|
def _get_type(self, apex_file_path):
|
|
cmd = ['deapexer', 'info', '--print-type', apex_file_path]
|
|
return run_host_command(cmd, True).strip()
|
|
|
|
def test_compression(self):
|
|
uncompressed_apex_fp = os.path.join(get_current_dir(), TEST_APEX + '.apex')
|
|
# TODO(samiul): try compressing a compressed APEX
|
|
compressed_apex_fp = self._compress_apex(uncompressed_apex_fp)
|
|
|
|
# Verify output file has been created and is smaller than input file
|
|
uncompressed_file_size = os.path.getsize(uncompressed_apex_fp)
|
|
compressed_file_size = os.path.getsize(compressed_apex_fp)
|
|
self.assertGreater(compressed_file_size, 0, 'Compressed APEX is empty')
|
|
self.assertLess(compressed_file_size, uncompressed_file_size,
|
|
'Compressed APEX is not smaller than uncompressed APEX')
|
|
|
|
# Verify type of the apex is 'COMPRESSED'
|
|
self.assertEqual(self._get_type(compressed_apex_fp), 'COMPRESSED')
|
|
|
|
# Verify the contents of the compressed apex files
|
|
content_in_compressed_apex = self._get_container_files(compressed_apex_fp)
|
|
self.assertIsNotNone(content_in_compressed_apex['original_apex'])
|
|
content_in_uncompressed_apex = self._get_container_files(
|
|
uncompressed_apex_fp)
|
|
self.assertIsNotNone(content_in_uncompressed_apex['apex_payload'])
|
|
self.assertIsNotNone(content_in_uncompressed_apex['digest'])
|
|
|
|
# Verify that CAPEX manifest contains digest of original_apex
|
|
manifest_string = self._get_manifest_string(
|
|
content_in_compressed_apex['apex_manifest.pb'])
|
|
self.assertIn('originalApexDigest: "'
|
|
+ content_in_uncompressed_apex['digest'] + '"', manifest_string)
|
|
|
|
for i in ['apex_manifest.json', 'apex_manifest.pb', 'apex_pubkey',
|
|
'apex_build_info.pb', 'AndroidManifest.xml']:
|
|
if i in content_in_uncompressed_apex:
|
|
if i == 'apex_manifest.pb':
|
|
# Get rid of originalApexDigest field, which should be the
|
|
# only difference
|
|
self._unset_original_apex_digest(content_in_compressed_apex[i])
|
|
self.assertEqual(get_sha1sum(content_in_compressed_apex[i]),
|
|
get_sha1sum(content_in_uncompressed_apex[i]))
|
|
|
|
def test_decompression(self):
|
|
# setup: create compressed APEX
|
|
uncompressed_apex_fp = os.path.join(get_current_dir(), TEST_APEX + '.apex')
|
|
compressed_apex_fp = self._compress_apex(uncompressed_apex_fp)
|
|
|
|
# Decompress it
|
|
decompressed_apex_fp = self._decompress_apex(compressed_apex_fp)
|
|
|
|
# Verify type of the apex is 'UNCOMPRESSED'
|
|
self.assertEqual(self._get_type(decompressed_apex_fp), 'UNCOMPRESSED')
|
|
|
|
# Verify decompressed APEX is same as uncompressed APEX
|
|
self.assertEqual(get_sha1sum(uncompressed_apex_fp),
|
|
get_sha1sum(decompressed_apex_fp),
|
|
'Decompressed APEX is not same as uncompressed APEX')
|
|
|
|
# Try decompressing uncompressed APEX. It should not work.
|
|
with self.assertRaises(RuntimeError) as error:
|
|
self._decompress_apex(uncompressed_apex_fp)
|
|
|
|
self.assertIn(uncompressed_apex_fp
|
|
+ ' is not a compressed APEX', str(error.exception))
|
|
|
|
def test_only_original_apex_is_compressed(self):
|
|
uncompressed_apex_fp = os.path.join(get_current_dir(), TEST_APEX + '.apex')
|
|
compressed_apex_fp = self._compress_apex(uncompressed_apex_fp)
|
|
|
|
with ZipFile(compressed_apex_fp, 'r') as zip_obj:
|
|
self.assertEqual(zip_obj.getinfo('original_apex').compress_type,
|
|
ZIP_DEFLATED)
|
|
content_in_uncompressed_apex = self._get_container_files(
|
|
uncompressed_apex_fp)
|
|
for i in ['apex_manifest.json', 'apex_manifest.pb', 'apex_pubkey',
|
|
'apex_build_info.pb', 'AndroidManifest.xml']:
|
|
if i in content_in_uncompressed_apex:
|
|
self.assertEqual(zip_obj.getinfo(i).compress_type, ZIP_STORED)
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main(verbosity=2)
|