chromium/tools/binary_size/libsupersize/diff_test.py

#!/usr/bin/env python3
# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import unittest

import diff
import models


def _MakeSym(section, size, path, name=None, container=None):
  if name is None:
    # Trailing letter is important since diffing trims numbers.
    name = '{}_{}A'.format(section[1:], size)
  ret = models.Symbol(section,
                      size,
                      full_name=name,
                      template_name=name,
                      name=name,
                      object_path=path)
  if container:
    ret.container = container
  return ret


def _SetName(symbol, full_name, name=None):
  if name is None:
    name = full_name
  symbol.full_name = full_name
  symbol.template_name = full_name
  symbol.name = name


def _CreateSizeInfo(aliases=None, containers=None):
  build_config = {}
  metadata = {}
  section_sizes = {'.text': 100, '.bss': 40}
  metrics_by_file = {
      'classes.dex': {
          'COUNT/HEADER': 1,
          'COUNT/STRING_ID': 11,
          'COUNT/CODE': 3,
          'COUNT/STRING_DATA': 11,
          'SIZE/HEADER': 1024,
          'SIZE/STRING_ID': 44,
          'SIZE/CODE': 1337,
          'SIZE/STRING_DATA': 888,
      },
  }
  if not containers:
    containers = [
        models.Container('',
                         metadata=metadata,
                         section_sizes=section_sizes,
                         metrics_by_file=metrics_by_file)
    ]
  models.BaseContainer.AssignShortNames(containers)
  TEXT = models.SECTION_TEXT
  symbols = [
      _MakeSym(models.SECTION_DEX_METHOD, 10, 'a', 'com.Foo#bar()'),
      _MakeSym(TEXT, 20, 'a', '.Lfoo'),
      _MakeSym(TEXT, 30, 'b'),
      _MakeSym(TEXT, 40, 'b'),
      _MakeSym(TEXT, 50, 'b'),
      _MakeSym(TEXT, 60, ''),
  ]
  for s in symbols:
    s.container = containers[0]
  if aliases:
    for tup in aliases:
      syms = symbols[tup[0]:tup[1]]
      for sym in syms:
        sym.aliases = syms
  return models.SizeInfo(build_config, containers, symbols)


class DiffTest(unittest.TestCase):

  def testIdentity(self):
    size_info1 = _CreateSizeInfo()
    size_info2 = _CreateSizeInfo()
    d = diff.Diff(size_info1, size_info2)
    self.assertEqual((0, 0, 0), d.raw_symbols.CountsByDiffStatus()[1:])
    self.assertEqual(0, d.raw_symbols.size)
    self.assertEqual(0, d.raw_symbols.padding)

  def testSimple_Add(self):
    size_info1 = _CreateSizeInfo()
    size_info2 = _CreateSizeInfo()
    size_info1.raw_symbols -= [size_info1.raw_symbols[0]]
    d = diff.Diff(size_info1, size_info2)
    self.assertEqual((0, 1, 0), d.raw_symbols.CountsByDiffStatus()[1:])
    self.assertEqual(10, d.raw_symbols.size)
    self.assertEqual(0, d.raw_symbols.padding)

  def testSimple_Delete(self):
    size_info1 = _CreateSizeInfo()
    size_info2 = _CreateSizeInfo()
    size_info2.raw_symbols -= [size_info2.raw_symbols[0]]
    d = diff.Diff(size_info1, size_info2)
    self.assertEqual((0, 0, 1), d.raw_symbols.CountsByDiffStatus()[1:])
    self.assertEqual(-10, d.raw_symbols.size)
    self.assertEqual(0, d.raw_symbols.padding)

  def testSimple_Change(self):
    size_info1 = _CreateSizeInfo()
    size_info2 = _CreateSizeInfo()
    size_info2.raw_symbols[0].size += 11
    size_info2.raw_symbols[0].padding += 20
    size_info2.raw_symbols[-1].size += 11
    d = diff.Diff(size_info1, size_info2)
    self.assertEqual((2, 1, 0), d.raw_symbols.CountsByDiffStatus()[1:])
    self.assertEqual(22, d.raw_symbols.size)
    self.assertEqual(20, d.raw_symbols.padding)

  def testDontMatchAcrossSections(self):
    size_info1 = _CreateSizeInfo()
    size_info1.raw_symbols += [
        _MakeSym(models.SECTION_TEXT, 11, 'asdf', name='Hello'),
    ]
    size_info2 = _CreateSizeInfo()
    size_info2.raw_symbols += [
        _MakeSym(models.SECTION_RODATA, 11, 'asdf', name='Hello'),
    ]
    # For simplicity, not associating |symbols| with |containers|.
    d = diff.Diff(size_info1, size_info2)
    self.assertEqual((0, 1, 1), d.raw_symbols.CountsByDiffStatus()[1:])
    self.assertEqual(0, d.raw_symbols.size)

  def testDontMatchAcrossContainers(self):
    container_a = models.Container('A',
                                   metadata={},
                                   section_sizes={},
                                   metrics_by_file={})
    container_b = models.Container('B',
                                   metadata={},
                                   section_sizes={},
                                   metrics_by_file={})
    containers = [container_a, container_b]
    size_info1 = _CreateSizeInfo(containers=containers)
    size_info1.raw_symbols[0].container = container_b
    size_info2 = _CreateSizeInfo(containers=containers)
    d = diff.Diff(size_info1, size_info2)
    # Should show as one add and one remove rather than a change.
    self.assertEqual((0, 1, 1), d.raw_symbols.CountsByDiffStatus()[1:])

  def testAliases_Remove(self):
    size_info1 = _CreateSizeInfo(aliases=[(0, 3)])
    size_info2 = _CreateSizeInfo(aliases=[(0, 2)])
    d = diff.Diff(size_info1, size_info2)
    # Aliases cause all sizes to change.
    self.assertEqual((3, 0, 0), d.raw_symbols.CountsByDiffStatus()[1:])
    self.assertEqual(0, d.raw_symbols.size)

  def testAliases_Add(self):
    size_info1 = _CreateSizeInfo(aliases=[(0, 2)])
    size_info2 = _CreateSizeInfo(aliases=[(0, 3)])
    d = diff.Diff(size_info1, size_info2)
    # Aliases cause all sizes to change.
    self.assertEqual((3, 0, 0), d.raw_symbols.CountsByDiffStatus()[1:])
    self.assertEqual(0, d.raw_symbols.size)

  def testAliases_ChangeGroup(self):
    size_info1 = _CreateSizeInfo(aliases=[(0, 2), (2, 5)])
    size_info2 = _CreateSizeInfo(aliases=[(0, 3), (3, 5)])
    d = diff.Diff(size_info1, size_info2)
    # Aliases cause all sizes to change.
    self.assertEqual((4, 0, 0), d.raw_symbols.CountsByDiffStatus()[1:])
    self.assertEqual(0, d.raw_symbols.size)

  def testStarSymbolNormalization(self):
    size_info1 = _CreateSizeInfo()
    _SetName(size_info1.raw_symbols[0], '* symbol gap 1 (end of section)')
    size_info2 = _CreateSizeInfo()
    _SetName(size_info2.raw_symbols[0], '* symbol gap 2 (end of section)')
    d = diff.Diff(size_info1, size_info2)
    self.assertEqual((0, 0, 0), d.raw_symbols.CountsByDiffStatus()[1:])
    self.assertEqual(0, d.raw_symbols.size)

  def testNumberNormalization(self):
    TEXT = models.SECTION_TEXT
    size_info1 = _CreateSizeInfo()
    size_info1.raw_symbols += [
        _MakeSym(TEXT, 11, 'a', name='.L__unnamed_1193'),
        _MakeSym(TEXT, 22, 'a', name='.L__unnamed_1194'),
        _MakeSym(TEXT, 33, 'a', name='SingleCategoryPreferences$3#this$0'),
        _MakeSym(TEXT, 44, 'a', name='.L.ref.tmp.2'),
    ]
    size_info2 = _CreateSizeInfo()
    size_info2.raw_symbols += [
        _MakeSym(TEXT, 11, 'a', name='.L__unnamed_2194'),
        _MakeSym(TEXT, 22, 'a', name='.L__unnamed_2195'),
        _MakeSym(TEXT, 33, 'a', name='SingleCategoryPreferences$9#this$009'),
        _MakeSym(TEXT, 44, 'a', name='.L.ref.tmp.137'),
    ]
    # For simplicity, not associating |symbols| with |containers|.
    d = diff.Diff(size_info1, size_info2)
    self.assertEqual((0, 0, 0), d.raw_symbols.CountsByDiffStatus()[1:])
    self.assertEqual(0, d.raw_symbols.size)

  def testChangedParams(self):
    # Ensure that params changes match up so long as path doesn't change.
    size_info1 = _CreateSizeInfo()
    size_info1.raw_symbols[0].full_name = 'Foo()'
    size_info1.raw_symbols[0].name = 'Foo'
    size_info2 = _CreateSizeInfo()
    size_info2.raw_symbols[0].full_name = 'Foo(bool)'
    size_info2.raw_symbols[0].name = 'Foo'
    d = diff.Diff(size_info1, size_info2)
    self.assertEqual((0, 0, 0), d.raw_symbols.CountsByDiffStatus()[1:])
    self.assertEqual(0, d.raw_symbols.size)

  def testChangedPaths_Native(self):
    # Ensure that non-globally-unique symbols are not matched when path changes.
    size_info1 = _CreateSizeInfo()
    size_info2 = _CreateSizeInfo()
    size_info2.raw_symbols[1].object_path = 'asdf'
    d = diff.Diff(size_info1, size_info2)
    self.assertEqual((0, 1, 1), d.raw_symbols.CountsByDiffStatus()[1:])
    self.assertEqual(0, d.raw_symbols.size)

  def testChangedPaths_StringLiterals(self):
    # Ensure that string literals are not matched up.
    size_info1 = _CreateSizeInfo()
    size_info1.raw_symbols[0].full_name = models.STRING_LITERAL_NAME
    size_info2 = _CreateSizeInfo()
    size_info2.raw_symbols[0].full_name = models.STRING_LITERAL_NAME
    size_info2.raw_symbols[0].object_path = 'asdf'
    d = diff.Diff(size_info1, size_info2)
    self.assertEqual((0, 1, 1), d.raw_symbols.CountsByDiffStatus()[1:])
    self.assertEqual(0, d.raw_symbols.size)

  def testChangedPaths_NamedStringLiteralsSameSize(self):
    # Ensure that named string literals are matched up with same size.
    size_info1 = _CreateSizeInfo()
    size_info1.raw_symbols[0].full_name = '"asdf..."'
    size_info2 = _CreateSizeInfo()
    size_info2.raw_symbols[0].full_name = '"asdf..."'
    size_info2.raw_symbols[0].object_path = 'asdf'
    d = diff.Diff(size_info1, size_info2)
    self.assertEqual((0, 0, 0), d.raw_symbols.CountsByDiffStatus()[1:])

  def testChangedPaths_NamedStringLiteralsDifferentSize(self):
    # Ensure that named string literals are matched up with same size.
    size_info1 = _CreateSizeInfo()
    size_info1.raw_symbols[0].full_name = '"asdf..."'
    size_info2 = _CreateSizeInfo()
    size_info2.raw_symbols[0].full_name = '"asdf..."'
    size_info2.raw_symbols[0].object_path = 'asdf'
    size_info2.raw_symbols[0].size += 10
    d = diff.Diff(size_info1, size_info2)
    self.assertEqual((0, 1, 1), d.raw_symbols.CountsByDiffStatus()[1:])
    self.assertEqual(10, d.raw_symbols.size)

  def testChangedPaths_Java(self):
    # Ensure that Java symbols are matched up.
    size_info1 = _CreateSizeInfo()
    size_info2 = _CreateSizeInfo()
    size_info2.raw_symbols[0].object_path = 'asdf'
    d = diff.Diff(size_info1, size_info2)
    self.assertEqual((0, 0, 0), d.raw_symbols.CountsByDiffStatus()[1:])
    self.assertEqual(0, d.raw_symbols.size)

  def testChangedPaths_ChangedParams(self):
    # Ensure that path changes are not matched when params also change.
    size_info1 = _CreateSizeInfo()
    size_info1.raw_symbols[0].full_name = 'Foo()'
    size_info1.raw_symbols[0].name = 'Foo'
    size_info2 = _CreateSizeInfo()
    size_info2.raw_symbols[0].full_name = 'Foo(bool)'
    size_info2.raw_symbols[0].name = 'Foo'
    size_info2.raw_symbols[0].object_path = 'asdf'
    d = diff.Diff(size_info1, size_info2)
    self.assertEqual((0, 1, 1), d.raw_symbols.CountsByDiffStatus()[1:])
    self.assertEqual(0, d.raw_symbols.size)


if __name__ == '__main__':
  unittest.main()