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 38import logging 39import re 40 41from devil.android import device_errors 42from devil.android.sdk import intent 43from pylib.base import base_test_result 44 45 46ResultType = 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 64def _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 100class 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 111class 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 121class 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 201class 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