Home | History | Annotate | Download | only in linker
      1 # Copyright 2013 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Base class for linker-specific test cases.
      6 
      7    The custom dynamic linker can only be tested through a custom test case
      8    for various technical reasons:
      9 
     10      - It's an 'invisible feature', i.e. it doesn't expose a new API or
     11        behaviour, all it does is save RAM when loading native libraries.
     12 
     13      - Checking that it works correctly requires several things that do not
     14        fit the existing GTest-based and instrumentation-based tests:
     15 
     16          - Native test code needs to be run in both the browser and renderer
     17            process at the same time just after loading native libraries, in
     18            a completely asynchronous way.
     19 
     20          - Each test case requires restarting a whole new application process
     21            with a different command-line.
     22 
     23          - Enabling test support in the Linker code requires building a special
     24            APK with a flag to activate special test-only support code in the
     25            Linker code itself.
     26 
     27        Host-driven tests have also been tried, but since they're really
     28        sub-classes of instrumentation tests, they didn't work well either.
     29 
     30    To build and run the linker tests, do the following:
     31 
     32      ninja -C out/Debug chromium_linker_test_apk
     33      build/android/test_runner.py linker
     34 
     35 """
     36 # pylint: disable=R0201
     37 
     38 import logging
     39 import re
     40 
     41 from devil.android import device_errors
     42 from devil.android.sdk import intent
     43 from pylib.base import base_test_result
     44 
     45 
     46 ResultType = base_test_result.ResultType
     47 
     48 _PACKAGE_NAME = 'org.chromium.chromium_linker_test_apk'
     49 _ACTIVITY_NAME = '.ChromiumLinkerTestActivity'
     50 _COMMAND_LINE_FILE = '/data/local/tmp/chromium-linker-test-command-line'
     51 
     52 # Logcat filters used during each test. Only the 'chromium' one is really
     53 # needed, but the logs are added to the TestResult in case of error, and
     54 # it is handy to have others as well when troubleshooting.
     55 _LOGCAT_FILTERS = ['*:s', 'chromium:v', 'cr_chromium:v',
     56                    'cr_ChromiumAndroidLinker:v', 'cr_LibraryLoader:v',
     57                    'cr_LinkerTest:v']
     58 #_LOGCAT_FILTERS = ['*:v']  ## DEBUG
     59 
     60 # Regular expression used to match status lines in logcat.
     61 _RE_BROWSER_STATUS_LINE = re.compile(r' BROWSER_LINKER_TEST: (FAIL|SUCCESS)$')
     62 _RE_RENDERER_STATUS_LINE = re.compile(r' RENDERER_LINKER_TEST: (FAIL|SUCCESS)$')
     63 
     64 def _StartActivityAndWaitForLinkerTestStatus(device, timeout):
     65   """Force-start an activity and wait up to |timeout| seconds until the full
     66      linker test status lines appear in the logcat, recorded through |device|.
     67   Args:
     68     device: A DeviceUtils instance.
     69     timeout: Timeout in seconds
     70   Returns:
     71     A (status, logs) tuple, where status is a ResultType constant, and logs
     72     if the final logcat output as a string.
     73   """
     74 
     75   # 1. Start recording logcat with appropriate filters.
     76   with device.GetLogcatMonitor(filter_specs=_LOGCAT_FILTERS) as logmon:
     77 
     78     # 2. Force-start activity.
     79     device.StartActivity(
     80         intent.Intent(package=_PACKAGE_NAME, activity=_ACTIVITY_NAME),
     81         force_stop=True)
     82 
     83     # 3. Wait up to |timeout| seconds until the test status is in the logcat.
     84     result = ResultType.PASS
     85     try:
     86       browser_match = logmon.WaitFor(_RE_BROWSER_STATUS_LINE, timeout=timeout)
     87       logging.debug('Found browser match: %s', browser_match.group(0))
     88       renderer_match = logmon.WaitFor(_RE_RENDERER_STATUS_LINE,
     89                                       timeout=timeout)
     90       logging.debug('Found renderer match: %s', renderer_match.group(0))
     91       if (browser_match.group(1) != 'SUCCESS'
     92           or renderer_match.group(1) != 'SUCCESS'):
     93         result = ResultType.FAIL
     94     except device_errors.CommandTimeoutError:
     95       result = ResultType.TIMEOUT
     96 
     97     return result, '\n'.join(device.adb.Logcat(dump=True))
     98 
     99 
    100 class LibraryLoadMap(dict):
    101   """A helper class to pretty-print a map of library names to load addresses."""
    102   def __str__(self):
    103     items = ['\'%s\': 0x%x' % (name, address) for \
    104         (name, address) in self.iteritems()]
    105     return '{%s}' % (', '.join(items))
    106 
    107   def __repr__(self):
    108     return 'LibraryLoadMap(%s)' % self.__str__()
    109 
    110 
    111 class AddressList(list):
    112   """A helper class to pretty-print a list of load addresses."""
    113   def __str__(self):
    114     items = ['0x%x' % address for address in self]
    115     return '[%s]' % (', '.join(items))
    116 
    117   def __repr__(self):
    118     return 'AddressList(%s)' % self.__str__()
    119 
    120 
    121 class LinkerTestCaseBase(object):
    122   """Base class for linker test cases."""
    123 
    124   def __init__(self, is_modern_linker=False, is_low_memory=False):
    125     """Create a test case.
    126     Args:
    127       is_modern_linker: True to test ModernLinker, False to test LegacyLinker.
    128       is_low_memory: True to simulate a low-memory device, False otherwise.
    129     """
    130     self.is_modern_linker = is_modern_linker
    131     if is_modern_linker:
    132       test_suffix = 'ForModernLinker'
    133     else:
    134       test_suffix = 'ForLegacyLinker'
    135     self.is_low_memory = is_low_memory
    136     if is_low_memory:
    137       test_suffix += 'LowMemoryDevice'
    138     else:
    139       test_suffix += 'RegularDevice'
    140     class_name = self.__class__.__name__
    141     self.qualified_name = '%s.%s' % (class_name, test_suffix)
    142     self.tagged_name = self.qualified_name
    143 
    144   def _RunTest(self, _device):
    145     """Run the test, must be overriden.
    146     Args:
    147       _device: A DeviceUtils interface.
    148     Returns:
    149       A (status, log) tuple, where <status> is a ResultType constant, and <log>
    150       is the logcat output captured during the test in case of error, or None
    151       in case of success.
    152     """
    153     return ResultType.FAIL, 'Unimplemented _RunTest() method!'
    154 
    155   def Run(self, device):
    156     """Run the test on a given device.
    157     Args:
    158       device: Name of target device where to run the test.
    159     Returns:
    160       A base_test_result.TestRunResult() instance.
    161     """
    162     margin = 8
    163     print '[ %-*s ] %s' % (margin, 'RUN', self.tagged_name)
    164     logging.info('Running linker test: %s', self.tagged_name)
    165 
    166     # Create command-line file on device.
    167     if self.is_modern_linker:
    168       command_line_flags = '--use-linker=modern'
    169     else:
    170       command_line_flags = '--use-linker=legacy'
    171     if self.is_low_memory:
    172       command_line_flags += ' --low-memory-device'
    173     device.WriteFile(_COMMAND_LINE_FILE, command_line_flags)
    174 
    175     # Run the test.
    176     status, logs = self._RunTest(device)
    177 
    178     result_text = 'OK'
    179     if status == ResultType.FAIL:
    180       result_text = 'FAILED'
    181     elif status == ResultType.TIMEOUT:
    182       result_text = 'TIMEOUT'
    183     print '[ %*s ] %s' % (margin, result_text, self.tagged_name)
    184 
    185     results = base_test_result.TestRunResults()
    186     results.AddResult(
    187         base_test_result.BaseTestResult(
    188             self.tagged_name,
    189             status,
    190             log=logs))
    191 
    192     return results
    193 
    194   def __str__(self):
    195     return self.tagged_name
    196 
    197   def __repr__(self):
    198     return self.tagged_name
    199 
    200 
    201 class LinkerSharedRelroTest(LinkerTestCaseBase):
    202   """A linker test case to check the status of shared RELRO sections.
    203 
    204     The core of the checks performed here are pretty simple:
    205 
    206       - Clear the logcat and start recording with an appropriate set of filters.
    207       - Create the command-line appropriate for the test-case.
    208       - Start the activity (always forcing a cold start).
    209       - Every second, look at the current content of the filtered logcat lines
    210         and look for instances of the following:
    211 
    212             BROWSER_LINKER_TEST: <status>
    213             RENDERER_LINKER_TEST: <status>
    214 
    215         where <status> can be either FAIL or SUCCESS. These lines can appear
    216         in any order in the logcat. Once both browser and renderer status are
    217         found, stop the loop. Otherwise timeout after 30 seconds.
    218 
    219         Note that there can be other lines beginning with BROWSER_LINKER_TEST:
    220         and RENDERER_LINKER_TEST:, but are not followed by a <status> code.
    221 
    222       - The test case passes if the <status> for both the browser and renderer
    223         process are SUCCESS. Otherwise its a fail.
    224   """
    225   def _RunTest(self, device):
    226     # Wait up to 30 seconds until the linker test status is in the logcat.
    227     return _StartActivityAndWaitForLinkerTestStatus(device, timeout=30)
    228