naiveproxy/tools/binary_size/libsupersize/integration_test.py
2018-12-09 21:59:24 -05:00

524 lines
20 KiB
Python
Executable File

#!/usr/bin/env python
# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import cStringIO
import contextlib
import copy
import difflib
import glob
import itertools
import logging
import os
import unittest
import re
import shutil
import subprocess
import sys
import tempfile
import zipfile
import archive
import describe
import diff
import file_format
import models
_SCRIPT_DIR = os.path.dirname(__file__)
_TEST_DATA_DIR = os.path.join(_SCRIPT_DIR, 'testdata')
_TEST_SOURCE_DIR = os.path.join(_TEST_DATA_DIR, 'mock_source_directory')
_TEST_OUTPUT_DIR = os.path.join(_TEST_SOURCE_DIR, 'out', 'Release')
_TEST_TOOL_PREFIX = os.path.join(
os.path.abspath(_TEST_DATA_DIR), 'mock_toolchain', '')
_TEST_APK_ROOT_DIR = os.path.join(_TEST_DATA_DIR, 'mock_apk')
_TEST_MAP_PATH = os.path.join(_TEST_DATA_DIR, 'test.map')
_TEST_PAK_INFO_PATH = os.path.join(
_TEST_OUTPUT_DIR, 'size-info/test.apk.pak.info')
_TEST_ELF_FILE_BEGIN = os.path.join(_TEST_OUTPUT_DIR, 'elf.begin')
_TEST_APK_PAK_PATH = os.path.join(_TEST_APK_ROOT_DIR, 'assets/en-US.pak')
# The following files are dynamically created.
_TEST_ELF_PATH = os.path.join(_TEST_OUTPUT_DIR, 'elf')
_TEST_APK_PATH = os.path.join(_TEST_OUTPUT_DIR, 'test.apk')
# Generated file paths relative to apk
_TEST_APK_SO_PATH = 'test.so'
_TEST_APK_SMALL_SO_PATH = 'smalltest.so'
_TEST_APK_DEX_PATH = 'test.dex'
_TEST_APK_OTHER_FILE_PATH = 'assets/icudtl.dat'
_TEST_APK_RES_FILE_PATH = 'res/drawable-v13/test.xml'
update_goldens = False
def _AssertGolden(expected_lines, actual_lines, golden_path):
expected = list(expected_lines)
actual = list(l + '\n' for l in actual_lines)
assert actual == expected, (('Did not match %s.\n' % golden_path) +
''.join(difflib.unified_diff(expected, actual, 'expected', 'actual')))
def _CompareWithGolden(name=None):
def real_decorator(func):
basename = name
if not basename:
basename = func.__name__.replace('test_', '')
golden_path = os.path.join(_TEST_DATA_DIR, basename + '.golden')
def inner(self):
actual_lines = func(self)
actual_lines = (re.sub(r'(elf_mtime=).*', r'\1{redacted}', l)
for l in actual_lines)
actual_lines = (re.sub(r'(Loaded from ).*', r'\1{redacted}', l)
for l in actual_lines)
if update_goldens:
with open(golden_path, 'w') as file_obj:
describe.WriteLines(actual_lines, file_obj.write)
logging.info('Wrote %s', golden_path)
else:
with open(golden_path) as file_obj:
_AssertGolden(file_obj, actual_lines, golden_path)
return inner
return real_decorator
@contextlib.contextmanager
def _AddMocksToPath():
prev_path = os.environ['PATH']
os.environ['PATH'] = _TEST_TOOL_PREFIX[:-1] + os.path.pathsep + prev_path
yield
os.environ['PATH'] = prev_path
def _RunApp(name, args, debug_measures=False):
argv = [os.path.join(_SCRIPT_DIR, 'main.py'), name, '--no-pypy']
argv.extend(args)
with _AddMocksToPath():
env = None
if debug_measures:
env = os.environ.copy()
env['SUPERSIZE_DISABLE_ASYNC'] = '1'
env['SUPERSIZE_MEASURE_GZIP'] = '1'
return subprocess.check_output(argv, env=env).splitlines()
def _DiffCounts(sym):
counts = sym.CountsByDiffStatus()
return (counts[models.DIFF_STATUS_CHANGED],
counts[models.DIFF_STATUS_ADDED],
counts[models.DIFF_STATUS_REMOVED])
class IntegrationTest(unittest.TestCase):
maxDiff = None # Don't trucate diffs in errors.
cached_size_info = {}
@staticmethod
def _CreateBlankData(power_of_two):
data = '\0'
for _ in range(power_of_two):
data = data + data
return data
@staticmethod
def _SafeRemoveFiles(file_names):
for file_name in file_names:
if os.path.exists(file_name):
os.remove(file_name)
@classmethod
def setUpClass(cls):
shutil.copy(_TEST_ELF_FILE_BEGIN, _TEST_ELF_PATH)
# Exactly 128MB of data (2^27), extra bytes will be accounted in overhead.
with open(_TEST_ELF_PATH, 'a') as elf_file:
elf_file.write(IntegrationTest._CreateBlankData(27))
with zipfile.ZipFile(_TEST_APK_PATH, 'w') as apk_file:
apk_file.write(_TEST_ELF_PATH, _TEST_APK_SO_PATH)
# Exactly 4MB of data (2^22).
apk_file.writestr(
_TEST_APK_SMALL_SO_PATH, IntegrationTest._CreateBlankData(22))
# Exactly 1MB of data (2^20).
apk_file.writestr(
_TEST_APK_OTHER_FILE_PATH, IntegrationTest._CreateBlankData(20))
# Exactly 1KB of data (2^10).
apk_file.writestr(
_TEST_APK_RES_FILE_PATH, IntegrationTest._CreateBlankData(10))
pak_rel_path = os.path.relpath(_TEST_APK_PAK_PATH, _TEST_APK_ROOT_DIR)
apk_file.write(_TEST_APK_PAK_PATH, pak_rel_path)
# Exactly 8MB of data (2^23).
apk_file.writestr(
_TEST_APK_DEX_PATH, IntegrationTest._CreateBlankData(23))
@classmethod
def tearDownClass(cls):
IntegrationTest._SafeRemoveFiles([
_TEST_ELF_PATH,
_TEST_APK_PATH,
])
def _CloneSizeInfo(self, use_output_directory=True, use_elf=True,
use_apk=False, use_pak=False):
assert not use_elf or use_output_directory
assert not (use_apk and use_pak)
cache_key = (use_output_directory, use_elf, use_apk, use_pak)
if cache_key not in IntegrationTest.cached_size_info:
elf_path = _TEST_ELF_PATH if use_elf else None
output_directory = _TEST_OUTPUT_DIR if use_output_directory else None
knobs = archive.SectionSizeKnobs()
# Override for testing. Lower the bar for compacting symbols, to allow
# smaller test cases to be created.
knobs.max_same_name_alias_count = 3
knobs.src_root = _TEST_SOURCE_DIR
apk_path = None
apk_so_path = None
if use_apk:
apk_path = _TEST_APK_PATH
apk_so_path = _TEST_APK_SO_PATH
pak_files = None
pak_info_file = None
if use_pak:
pak_files = [_TEST_APK_PAK_PATH]
pak_info_file = _TEST_PAK_INFO_PATH
metadata = None
linker_name = 'gold'
if use_elf:
with _AddMocksToPath():
metadata = archive.CreateMetadata(
_TEST_MAP_PATH, elf_path, apk_path, _TEST_TOOL_PREFIX,
output_directory, linker_name)
section_sizes, raw_symbols = archive.CreateSectionSizesAndSymbols(
map_path=_TEST_MAP_PATH, tool_prefix=_TEST_TOOL_PREFIX,
elf_path=elf_path, output_directory=output_directory,
apk_path=apk_path, apk_so_path=apk_so_path, metadata=metadata,
pak_files=pak_files, pak_info_file=pak_info_file,
linker_name=linker_name, knobs=knobs)
IntegrationTest.cached_size_info[cache_key] = archive.CreateSizeInfo(
section_sizes, raw_symbols, metadata=metadata)
return copy.deepcopy(IntegrationTest.cached_size_info[cache_key])
def _DoArchive(self, archive_path, use_output_directory=True, use_elf=True,
use_apk=False, use_pak=False, debug_measures=False):
args = [
archive_path,
'--map-file', _TEST_MAP_PATH,
'--source-directory', _TEST_SOURCE_DIR,
]
if use_output_directory:
# Let autodetection find output_directory when --elf-file is used.
if not use_elf:
args += ['--output-directory', _TEST_OUTPUT_DIR]
else:
args += ['--no-source-paths']
if use_elf:
args += ['--elf-file', _TEST_ELF_PATH]
if use_apk:
args += ['--apk-file', _TEST_APK_PATH]
if use_pak:
args += ['--pak-file', _TEST_APK_PAK_PATH,
'--pak-info-file', _TEST_PAK_INFO_PATH]
_RunApp('archive', args, debug_measures=debug_measures)
def _DoArchiveTest(self, use_output_directory=True, use_elf=True,
use_apk=False, use_pak=False, debug_measures=False):
with tempfile.NamedTemporaryFile(suffix='.size') as temp_file:
self._DoArchive(
temp_file.name, use_output_directory=use_output_directory,
use_elf=use_elf, use_apk=use_apk, use_pak=use_pak,
debug_measures=debug_measures)
size_info = archive.LoadAndPostProcessSizeInfo(temp_file.name)
# Check that saving & loading is the same as directly parsing.
expected_size_info = self._CloneSizeInfo(
use_output_directory=use_output_directory, use_elf=use_elf,
use_apk=use_apk, use_pak=use_pak)
self.assertEquals(expected_size_info.metadata, size_info.metadata)
# Don't cluster.
expected_size_info.symbols = expected_size_info.raw_symbols
size_info.symbols = size_info.raw_symbols
expected = list(describe.GenerateLines(expected_size_info, verbose=True))
actual = list(describe.GenerateLines(size_info, verbose=True))
self.assertEquals(expected, actual)
sym_strs = (repr(sym) for sym in size_info.symbols)
stats = describe.DescribeSizeInfoCoverage(size_info)
if size_info.metadata:
metadata = describe.DescribeMetadata(size_info.metadata)
else:
metadata = []
return itertools.chain(metadata, stats, sym_strs)
@_CompareWithGolden()
def test_Archive(self):
return self._DoArchiveTest(use_output_directory=False, use_elf=False)
@_CompareWithGolden()
def test_Archive_OutputDirectory(self):
return self._DoArchiveTest(use_elf=False)
@_CompareWithGolden()
def test_Archive_Elf(self):
return self._DoArchiveTest()
@_CompareWithGolden()
def test_Archive_Apk(self):
return self._DoArchiveTest(use_apk=True)
@_CompareWithGolden()
def test_Archive_Pak_Files(self):
return self._DoArchiveTest(use_pak=True)
@_CompareWithGolden(name='Archive_Elf')
def test_Archive_Elf_DebugMeasures(self):
return self._DoArchiveTest(debug_measures=True)
@_CompareWithGolden()
def test_Console(self):
with tempfile.NamedTemporaryFile(suffix='.size') as size_file, \
tempfile.NamedTemporaryFile(suffix='.txt') as output_file:
file_format.SaveSizeInfo(self._CloneSizeInfo(), size_file.name)
query = [
'ShowExamples()',
'ExpandRegex("_foo_")',
'canned_queries.CategorizeGenerated()',
'canned_queries.CategorizeByChromeComponent()',
'canned_queries.LargeFiles()',
'canned_queries.TemplatesByName()',
'canned_queries.StaticInitializers()',
'canned_queries.PakByPath()',
'Print(ReadStringLiterals(elf_path={}))'.format(repr(_TEST_ELF_PATH)),
'Print(size_info, to_file=%r)' % output_file.name,
]
ret = _RunApp('console', [size_file.name, '--query', '; '.join(query)])
with open(output_file.name) as f:
ret.extend(l.rstrip() for l in f)
return ret
@_CompareWithGolden()
def test_Csv(self):
with tempfile.NamedTemporaryFile(suffix='.size') as size_file, \
tempfile.NamedTemporaryFile(suffix='.txt') as output_file:
file_format.SaveSizeInfo(self._CloneSizeInfo(), size_file.name)
query = [
'Csv(size_info, to_file=%r)' % output_file.name,
]
ret = _RunApp('console', [size_file.name, '--query', '; '.join(query)])
with open(output_file.name) as f:
ret.extend(l.rstrip() for l in f)
return ret
@_CompareWithGolden()
def test_Diff_NullDiff(self):
with tempfile.NamedTemporaryFile(suffix='.size') as temp_file:
file_format.SaveSizeInfo(self._CloneSizeInfo(), temp_file.name)
return _RunApp('diff', [temp_file.name, temp_file.name])
# Runs archive 3 times, and asserts the contents are the same each time.
def test_Idempotent(self):
prev_contents = None
for _ in xrange(3):
with tempfile.NamedTemporaryFile(suffix='.size') as temp_file:
self._DoArchive(temp_file.name)
contents = temp_file.read()
self.assertTrue(prev_contents is None or contents == prev_contents)
prev_contents = contents
@_CompareWithGolden()
def test_Diff_Basic(self):
size_info1 = self._CloneSizeInfo(use_elf=False, use_pak=True)
size_info2 = self._CloneSizeInfo(use_elf=False, use_pak=True)
size_info1.metadata = {"foo": 1, "bar": [1,2,3], "baz": "yes"}
size_info2.metadata = {"foo": 1, "bar": [1,3], "baz": "yes"}
size_info1.raw_symbols -= size_info1.raw_symbols[:2]
size_info2.raw_symbols -= size_info2.raw_symbols[-3:]
changed_sym = size_info1.raw_symbols.WhereNameMatches('Patcher::Name_')[0]
changed_sym.size -= 10
padding_sym = size_info2.raw_symbols.WhereNameMatches('symbol gap 0')[0]
padding_sym.padding += 20
padding_sym.size += 20
pak_sym = size_info2.raw_symbols.WhereInSection(
models.SECTION_PAK_TRANSLATIONS)[0]
pak_sym.full_name = 'foo: ' + pak_sym.full_name.split()[-1]
# Serialize & de-serialize so that name normalization runs again for the pak
# symbol.
stringio = cStringIO.StringIO()
file_format.SaveSizeInfo(size_info2, 'path', file_obj=stringio)
stringio.seek(0)
size_info2 = archive.LoadAndPostProcessSizeInfo('path', file_obj=stringio)
d = diff.Diff(size_info1, size_info2)
d.raw_symbols = d.raw_symbols.Sorted()
self.assertEquals(d.raw_symbols.CountsByDiffStatus()[1:], [2, 2, 3])
changed_sym = d.raw_symbols.WhereNameMatches('Patcher::Name_')[0]
padding_sym = d.raw_symbols.WhereNameMatches('symbol gap 0')[0]
bss_sym = d.raw_symbols.WhereInSection(models.SECTION_BSS)[0]
# Padding-only deltas should sort after all non-padding changes.
padding_idx = d.raw_symbols.index(padding_sym)
changed_idx = d.raw_symbols.index(changed_sym)
bss_idx = d.raw_symbols.index(bss_sym)
self.assertLess(changed_idx, padding_idx)
# And before bss.
self.assertLess(padding_idx, bss_idx)
return describe.GenerateLines(d, verbose=True)
def test_Diff_Aliases1(self):
size_info1 = self._CloneSizeInfo()
size_info2 = self._CloneSizeInfo()
# Find a list of exact 4 symbols with the same aliases in |size_info2|:
# text@2a0010: BarAlias()
# text@2a0010: FooAlias()
# text@2a0010: blink::ContiguousContainerBase::shrinkToFit() @ path1
# text@2a0010: blink::ContiguousContainerBase::shrinkToFit() @ path2
# The blink::...::shrinkToFit() group has another member:
# text@2a0000: blink::ContiguousContainerBase::shrinkToFit() @ path3
a1, _, _, _ = (
size_info2.raw_symbols.Filter(lambda s: s.num_aliases == 4)[0].aliases)
# Remove FooAlias().
size_info2.raw_symbols -= [a1]
a1.aliases.remove(a1)
# From |size_info1| -> |size_info2|: 1 symbol is deleted.
d = diff.Diff(size_info1, size_info2)
# Total size should not change.
self.assertEquals(d.raw_symbols.pss, 0)
# 1 symbol is erased, and PSS distributed among 3 remaining aliases, and
# considered as change.
self.assertEquals((3, 0, 1), _DiffCounts(d.raw_symbols))
# Grouping combines 2 x blink::ContiguousContainerBase::shrinkToFit(), so
# now ther are 2 changed aliases.
self.assertEquals((2, 0, 1), _DiffCounts(d.symbols.GroupedByFullName()))
# From |size_info2| -> |size_info1|: 1 symbol is added.
d = diff.Diff(size_info2, size_info1)
self.assertEquals(d.raw_symbols.pss, 0)
self.assertEquals((3, 1, 0), _DiffCounts(d.raw_symbols))
self.assertEquals((2, 1, 0), _DiffCounts(d.symbols.GroupedByFullName()))
def test_Diff_Aliases2(self):
size_info1 = self._CloneSizeInfo()
size_info2 = self._CloneSizeInfo()
# Same list of 4 symbols as before.
a1, _, a2, _ = (
size_info2.raw_symbols.Filter(lambda s: s.num_aliases == 4)[0].aliases)
# Remove BarAlias() and blink::...::shrinkToFit().
size_info2.raw_symbols -= [a1, a2]
a1.aliases.remove(a1)
a1.aliases.remove(a2)
# From |size_info1| -> |size_info2|: 2 symbols are deleted.
d = diff.Diff(size_info1, size_info2)
self.assertEquals(d.raw_symbols.pss, 0)
self.assertEquals((2, 0, 2), _DiffCounts(d.raw_symbols))
self.assertEquals((2, 0, 1), _DiffCounts(d.symbols.GroupedByFullName()))
# From |size_info2| -> |size_info1|: 2 symbols are added.
d = diff.Diff(size_info2, size_info1)
self.assertEquals(d.raw_symbols.pss, 0)
self.assertEquals((2, 2, 0), _DiffCounts(d.raw_symbols))
self.assertEquals((2, 1, 0), _DiffCounts(d.symbols.GroupedByFullName()))
def test_Diff_Aliases4(self):
size_info1 = self._CloneSizeInfo()
size_info2 = self._CloneSizeInfo()
# Same list of 4 symbols as before.
a1, a2, a3, a4 = (
size_info2.raw_symbols.Filter(lambda s: s.num_aliases == 4)[0].aliases)
# Remove all 4 aliases.
size_info2.raw_symbols -= [a1, a2, a3, a4]
# From |size_info1| -> |size_info2|: 4 symbols are deleted.
d = diff.Diff(size_info1, size_info2)
self.assertEquals(d.raw_symbols.pss, -a1.size)
self.assertEquals((0, 0, 4), _DiffCounts(d.raw_symbols))
# When grouped, BarAlias() and FooAlias() are deleted, but the
# blink::...::shrinkToFit() has 1 remaining symbol, so is changed.
self.assertEquals((1, 0, 2), _DiffCounts(d.symbols.GroupedByFullName()))
# From |size_info2| -> |size_info1|: 4 symbols are added.
d = diff.Diff(size_info2, size_info1)
self.assertEquals(d.raw_symbols.pss, a1.size)
self.assertEquals((0, 4, 0), _DiffCounts(d.raw_symbols))
self.assertEquals((1, 2, 0), _DiffCounts(d.symbols.GroupedByFullName()))
def test_Diff_Clustering(self):
size_info1 = self._CloneSizeInfo()
size_info2 = self._CloneSizeInfo()
S = models.SECTION_TEXT
size_info1.symbols += [
models.Symbol(S, 11, name='.L__unnamed_1193', object_path='a'), # 1
models.Symbol(S, 22, name='.L__unnamed_1194', object_path='a'), # 2
models.Symbol(S, 33, name='.L__unnamed_1195', object_path='b'), # 3
models.Symbol(S, 44, name='.L__bar_195', object_path='b'), # 4
models.Symbol(S, 55, name='.L__bar_1195', object_path='b'), # 5
]
size_info2.symbols += [
models.Symbol(S, 33, name='.L__unnamed_2195', object_path='b'), # 3
models.Symbol(S, 11, name='.L__unnamed_2194', object_path='a'), # 1
models.Symbol(S, 22, name='.L__unnamed_2193', object_path='a'), # 2
models.Symbol(S, 44, name='.L__bar_2195', object_path='b'), # 4
models.Symbol(S, 55, name='.L__bar_295', object_path='b'), # 5
]
d = diff.Diff(size_info1, size_info2)
d.symbols = d.symbols.Sorted()
self.assertEquals(d.symbols.CountsByDiffStatus()[models.DIFF_STATUS_ADDED],
0)
self.assertEquals(d.symbols.size, 0)
@_CompareWithGolden()
def test_FullDescription(self):
size_info = self._CloneSizeInfo()
# Show both clustered and non-clustered so that they can be compared.
size_info.symbols = size_info.raw_symbols
return itertools.chain(
describe.GenerateLines(size_info, verbose=True),
describe.GenerateLines(size_info.symbols._Clustered(), recursive=True,
verbose=True),
)
@_CompareWithGolden()
def test_SymbolGroupMethods(self):
all_syms = self._CloneSizeInfo().symbols
global_syms = all_syms.WhereNameMatches('GLOBAL')
# Tests Filter(), Inverted(), and __sub__().
non_global_syms = global_syms.Inverted()
self.assertEqual(non_global_syms, (all_syms - global_syms))
# Tests Sorted() and __add__().
self.assertEqual(all_syms.Sorted(),
(global_syms + non_global_syms).Sorted())
# Tests GroupedByName() and __len__().
return itertools.chain(
['GroupedByName()'],
describe.GenerateLines(all_syms.GroupedByName()),
['GroupedByName(depth=1)'],
describe.GenerateLines(all_syms.GroupedByName(depth=1)),
['GroupedByName(depth=-1)'],
describe.GenerateLines(all_syms.GroupedByName(depth=-1)),
['GroupedByName(depth=1, min_count=2)'],
describe.GenerateLines(all_syms.GroupedByName(depth=1, min_count=2)),
)
def main():
argv = sys.argv
if len(argv) > 1 and argv[1] == '--update':
argv.pop(0)
global update_goldens
update_goldens = True
for f in glob.glob(os.path.join(_TEST_DATA_DIR, '*.golden')):
os.unlink(f)
unittest.main(argv=argv, verbosity=2)
if __name__ == '__main__':
main()