#!/usr/bin/python
# Copyright (c) 2012 The Native Client Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Tests that a set of symbols are truly pruned from the translator.

Compares the pruned down "on-device" translator with the "fat" host build
which has not been pruned down.
"""

import glob
import re
import subprocess
import sys
import unittest

class SymbolInfo(object):

  def __init__(self, lib_name, sym_name, t, size):
    self.lib_name = lib_name
    self.sym_name = sym_name
    self.type = t
    self.size = size


def is_weak(t):
  t = t.upper()
  return t == 'V' or t == 'W'


def is_local(t):
  # According to the NM documentation:
  #   "If lowercase, the symbol is usually local...  There are however a few
  #   lowercase symbols that are shown for special global symbols
  #   ("u", "v" and "w")."
  return t != 'u' and not is_weak(t) and t.islower()


def merge_symbols(sdict1, sdict2):
  for sym_name, v2 in sdict2.iteritems():
    # Check for duplicate symbols.
    if sym_name in sdict1:
      v1 = sdict1[sym_name]
      # Only print warning if they are not weak / differently sized.
      if (not (is_weak(v2.type) or is_weak(v1.type)) and
          v1.size != v2.size):
        print 'Warning symbol %s defined in both %s(%d, %s) and %s(%d, %s)' % (
            sym_name,
            v1.lib_name, v1.size, v1.type,
            v2.lib_name, v2.size, v2.type)
      # Arbitrarily take the max.  The sizes are approximate anyway,
      # since the host binaries are built from a different compiler.
      v1.size = max(v1.size, v2.size)
      continue
    # Otherwise just copy info over to sdict2.
    sdict1[sym_name] = sdict2[sym_name]
  return sdict1


class TestTranslatorPruned(unittest.TestCase):

  pruned_symbols = {}
  unpruned_symbols = {}

  @classmethod
  def get_symbol_info(cls, nm_tool, bin_name):
    results = {}
    nm_cmd = [nm_tool, '--size-sort', '--demangle', bin_name]
    print 'Getting symbols and sizes by running:\n' + ' '.join(nm_cmd)
    for line in iter(subprocess.check_output(nm_cmd).splitlines()):
      (hex_size, t, sym_name) = line.split(' ', 2)
      # Only track defined and non-BSS symbols.
      if t != 'U' and t.upper() != 'B':
        info = SymbolInfo(bin_name, sym_name, t, int(hex_size, 16))
        # For local symbols, tack the library name on as a prefix.
        # That should still match the regexes later.
        if is_local(t):
          key = bin_name + '$' + sym_name
        else:
          key = sym_name
        # The same library can have the same local symbol. Just sum up sizes.
        if key in results:
          old = results[key]
          old.size = old.size + info.size
        else:
          results[key] = info
    return results

  @classmethod
  def setUpClass(cls):
    nm_tool = sys.argv[1]
    host_binaries = glob.glob(sys.argv[2])
    target_binary = sys.argv[3]
    print 'Getting symbol info from %s (host) and %s (target)' % (
        sys.argv[2], sys.argv[3])
    assert host_binaries, ('Did not glob any binaries from: ' % sys.argv[2])
    for b in host_binaries:
      cls.unpruned_symbols = merge_symbols(cls.unpruned_symbols,
                                           cls.get_symbol_info(nm_tool, b))
    cls.pruned_symbols = cls.get_symbol_info(nm_tool, target_binary)
    # Do an early check that these aren't stripped binaries.
    assert cls.unpruned_symbols, 'No symbols from host?'
    assert cls.pruned_symbols, 'No symbols from target?'

  def size_of_matching_syms(self, sym_regex, sym_infos):
    # Check if a given sym_infos has symbols matching sym_regex, and
    # return the total size of all matching symbols.
    total = 0
    for sym_name, sym_info in sym_infos.iteritems():
      if re.search(sym_regex, sym_info.sym_name):
        total += sym_info.size
    return total

  def test_prunedNotFullyStripped(self):
    """Make sure that the test isn't accidentally passing.

    The test can accidentally pass if the translator is stripped of symbols.
    Then it would look like everything is pruned out.  Look for a symbol
    that's guaranteed not to be pruned out.
    """
    pruned = self.size_of_matching_syms('main',
                                        TestTranslatorPruned.pruned_symbols)
    self.assertNotEqual(pruned, 0)

  def test_didPrune(self):
    """Check for classes/namespaces/symbols that we have intentionally pruned.

    Check that the symbols are not present anymore in the translator,
    and check that the symbols actually do exist in the developer tools.
    That prevents the test from accidentally passing if the symbols
    have been renamed to something else.
    """
    total = 0
    pruned_list = [
        'LLParser', 'LLLexer',
        'MCAsmParser', '::AsmParser',
        'ARMAsmParser', 'X86AsmParser',
        'ELFAsmParser', 'COFFAsmParser', 'DarwinAsmParser',
        'MCAsmLexer', '::AsmLexer',
        # Gigantic Asm MatchTable (globbed for all targets),
        'MatchTable',
        # Pruned out PBQP mostly, except the getCustomPBQPConstraints virtual.
        'RegAllocPBQP', 'PBQP::RegAlloc',
        # Can only check *InstPrinter::print*, not *::getRegisterName():
        # https://code.google.com/p/nativeclient/issues/detail?id=3326
        'ARMInstPrinter::print', 'X86.*InstPrinter::print',
        # Currently pruned by hacking Triple.h. That covers most things,
        # but not all.  E.g., container-specific relocation handling.
        '.*MachObjectWriter', 'TargetLoweringObjectFileMachO',
        'MCMachOStreamer', '.*MCAsmInfoDarwin',
        '.*COFFObjectWriter', 'TargetLoweringObjectFileCOFF',
        '.*COFFStreamer', '.*AsmInfoGNUCOFF',
        # This is not pruned out: 'MCSectionMachO', 'MCSectionCOFF',
        # 'MachineModuleInfoMachO', ...
        ]
    for sym_regex in pruned_list:
      unpruned = self.size_of_matching_syms(
          sym_regex, TestTranslatorPruned.unpruned_symbols)
      pruned = self.size_of_matching_syms(
          sym_regex, TestTranslatorPruned.pruned_symbols)
      self.assertNotEqual(unpruned, 0, 'Unpruned never had ' + sym_regex)
      self.assertEqual(pruned, 0, 'Pruned still has ' + sym_regex)
      # Bytes pruned is approximate since the host build is different
      # from the target build (different inlining / optimizations).
      print 'Pruned out approx %d bytes worth of %s symbols' % (unpruned,
                                                                sym_regex)
      total += unpruned
    print 'Total %d bytes' % total


if __name__ == '__main__':
  if len(sys.argv) != 4:
    print 'Usage: %s <nm_tool> <unpruned_host_binary> <pruned_target_binary>'
    sys.exit(1)
  suite = unittest.TestLoader().loadTestsFromTestCase(TestTranslatorPruned)
  result = unittest.TextTestRunner(verbosity=2).run(suite)
  if result.wasSuccessful():
    sys.exit(0)
  else:
    sys.exit(1)
