Ben Murdoch | 097c5b2 | 2016-05-18 11:27:45 +0100 | [diff] [blame^] | 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) |