- add sources.
[platform/framework/web/crosswalk.git] / src / build / android / pylib / linker / test_case.py
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 content_linker_test_apk
33      build/android/test_runner.py linker
34
35 """
36
37 import logging
38 import os
39 import re
40 import StringIO
41 import subprocess
42 import tempfile
43 import time
44
45 from pylib import constants
46 from pylib import android_commands
47 from pylib import flag_changer
48 from pylib.base import base_test_result
49
50 ResultType = base_test_result.ResultType
51
52 _PACKAGE_NAME='org.chromium.content_linker_test_apk'
53 _ACTIVITY_NAME='.ContentLinkerTestActivity'
54 _COMMAND_LINE_FILE='/data/local/tmp/content-linker-test-command-line'
55
56 # Path to the Linker.java source file.
57 _LINKER_JAVA_SOURCE_PATH = \
58     'content/public/android/java/src/org/chromium/content/app/Linker.java'
59
60 # A regular expression used to extract the browser shared RELRO configuration
61 # from the Java source file above.
62 _RE_LINKER_BROWSER_CONFIG = \
63     re.compile(r'.*BROWSER_SHARED_RELRO_CONFIG\s+=\s+' + \
64                'BROWSER_SHARED_RELRO_CONFIG_(\S+)\s*;.*',
65                re.MULTILINE | re.DOTALL)
66
67 # Logcat filters used during each test. Only the 'chromium' one is really
68 # needed, but the logs are added to the TestResult in case of error, and
69 # it is handy to have the 'content_android_linker' ones as well when
70 # troubleshooting.
71 _LOGCAT_FILTERS = [ '*:s', 'chromium:v', 'content_android_linker:v' ]
72 #_LOGCAT_FILTERS = [ '*:v' ]  ## DEBUG
73
74 # Regular expression used to match status lines in logcat.
75 re_status_line = re.compile(r'(BROWSER|RENDERER)_LINKER_TEST: (FAIL|SUCCESS)')
76
77 # Regular expression used to mach library load addresses in logcat.
78 re_library_address = re.compile(
79     r'(BROWSER|RENDERER)_LIBRARY_ADDRESS: (\S+) ([0-9A-Fa-f]+)')
80
81
82 def _GetBrowserSharedRelroConfig():
83   """Returns a string corresponding to the Linker's configuration of shared
84      RELRO sections in the browser process. This parses the Java linker source
85      file to get the appropriate information.
86   Return:
87       None in case of error (e.g. could not locate the source file).
88      'NEVER' if the browser process shall never use shared RELROs.
89      'LOW_RAM_ONLY' if if uses it only on low-end devices.
90      'ALWAYS' if it always uses a shared RELRO.
91   """
92   source_path = \
93       os.path.join(constants.DIR_SOURCE_ROOT, _LINKER_JAVA_SOURCE_PATH)
94   if not os.path.exists(source_path):
95     logging.error('Could not find linker source file: ' + source_path)
96     return None
97
98   with open(source_path) as f:
99     configs = _RE_LINKER_BROWSER_CONFIG.findall(f.read())
100     if not configs:
101       logging.error(
102           'Can\'t find browser shared RELRO configuration value in ' + \
103           source_path)
104       return None
105
106     if configs[0] not in ['NEVER', 'LOW_RAM_ONLY', 'ALWAYS']:
107       logging.error('Unexpected browser config value: ' + configs[0])
108       return None
109
110     logging.info('Found linker browser shared RELRO config: ' + configs[0])
111     return configs[0]
112
113
114 def _WriteCommandLineFile(adb, command_line, command_line_file):
115   """Create a command-line file on the device. This does not use FlagChanger
116      because its implementation assumes the device has 'su', and thus does
117      not work at all with production devices."""
118   adb.RunShellCommand('echo "%s" > %s' % (command_line, command_line_file))
119
120
121 def _CheckLinkerTestStatus(logcat):
122   """Parse the content of |logcat| and checks for both a browser and
123      renderer status line.
124
125   Args:
126     logcat: A string to parse. Can include line separators.
127
128   Returns:
129     A tuple, result[0] is True if there is a complete match, then
130     result[1] and result[2] will be True or False to reflect the
131     test status for the browser and renderer processes, respectively.
132   """
133   browser_found = False
134   renderer_found = False
135   for m in re_status_line.finditer(logcat):
136     process_type, status = m.groups()
137     if process_type == 'BROWSER':
138       browser_found = True
139       browser_success = (status == 'SUCCESS')
140     elif process_type == 'RENDERER':
141       renderer_found = True
142       renderer_success = (status == 'SUCCESS')
143     else:
144       assert False, 'Invalid process type ' + process_type
145
146   if browser_found and renderer_found:
147     return (True, browser_success, renderer_success)
148
149   # Didn't find anything.
150   return (False, None, None)
151
152
153 def _WaitForLinkerTestStatus(adb, timeout):
154   """Wait up to |timeout| seconds until the full linker test status lines appear
155      in the logcat being recorded with |adb|.
156   Args:
157     adb: An AndroidCommands instance. This assumes adb.StartRecordingLogcat()
158          was called previously.
159     timeout: Timeout in seconds.
160   Returns:
161     ResultType.TIMEOUT in case of timeout, ResulType.PASS if both status lines
162     report 'SUCCESS', or ResulType.FAIL otherwise.
163   """
164
165
166 def _StartActivityAndWaitForLinkerTestStatus(adb, timeout):
167   """Force-start an activity and wait up to |timeout| seconds until the full
168      linker test status lines appear in the logcat, recorded through |adb|.
169   Args:
170     adb: An AndroidCommands instance.
171     timeout: Timeout in seconds
172   Returns:
173     A (status, logs) tuple, where status is a ResultType constant, and logs
174     if the final logcat output as a string.
175   """
176   # 1. Start recording logcat with appropriate filters.
177   adb.StartRecordingLogcat(clear=True, filters=_LOGCAT_FILTERS)
178
179   try:
180     # 2. Force-start activity.
181     adb.StartActivity(package=_PACKAGE_NAME,
182                       activity=_ACTIVITY_NAME,
183                       force_stop=True)
184
185     # 3. Wait up to |timeout| seconds until the test status is in the logcat.
186     num_tries = 0
187     max_tries = timeout
188     found = False
189     while num_tries < max_tries:
190       time.sleep(1)
191       num_tries += 1
192       found, browser_ok, renderer_ok = _CheckLinkerTestStatus(
193           adb.GetCurrentRecordedLogcat())
194       if found:
195         break
196
197   finally:
198     logs = adb.StopRecordingLogcat()
199
200   if num_tries >= max_tries:
201     return ResultType.TIMEOUT, logs
202
203   if browser_ok and renderer_ok:
204     return ResultType.PASS, logs
205
206   return ResultType.FAIL, logs
207
208
209 class LibraryLoadMap(dict):
210   """A helper class to pretty-print a map of library names to load addresses."""
211   def __str__(self):
212     items = ['\'%s\': 0x%x' % (name, address) for \
213         (name, address) in self.iteritems()]
214     return '{%s}' % (', '.join(items))
215
216   def __repr__(self):
217     return 'LibraryLoadMap(%s)' % self.__str__()
218
219
220 class AddressList(list):
221   """A helper class to pretty-print a list of load addresses."""
222   def __str__(self):
223     items = ['0x%x' % address for address in self]
224     return '[%s]' % (', '.join(items))
225
226   def __repr__(self):
227     return 'AddressList(%s)' % self.__str__()
228
229
230 def _ExtractLibraryLoadAddressesFromLogcat(logs):
231   """Extract the names and addresses of shared libraries loaded in the
232      browser and renderer processes.
233   Args:
234     logs: A string containing logcat output.
235   Returns:
236     A tuple (browser_libs, renderer_libs), where each item is a map of
237     library names (strings) to library load addresses (ints), for the
238     browser and renderer processes, respectively.
239   """
240   browser_libs = LibraryLoadMap()
241   renderer_libs = LibraryLoadMap()
242   for m in re_library_address.finditer(logs):
243     process_type, lib_name, lib_address = m.groups()
244     lib_address = int(lib_address, 16)
245     if process_type == 'BROWSER':
246       browser_libs[lib_name] = lib_address
247     elif process_type == 'RENDERER':
248       renderer_libs[lib_name] = lib_address
249     else:
250       assert False, 'Invalid process type'
251
252   return browser_libs, renderer_libs
253
254
255 def _CheckLoadAddressRandomization(lib_map_list, process_type):
256   """Check that a map of library load addresses is random enough.
257   Args:
258     lib_map_list: a list of dictionaries that map library names (string)
259       to load addresses (int). Each item in the list corresponds to a
260       different run / process start.
261     process_type: a string describing the process type.
262   Returns:
263     (status, logs) tuple, where <status> is True iff the load addresses are
264     randomized, False otherwise, and <logs> is a string containing an error
265     message detailing the libraries that are not randomized properly.
266   """
267   # Collect, for each library, its list of load addresses.
268   lib_addr_map = {}
269   for lib_map in lib_map_list:
270     for lib_name, lib_address in lib_map.iteritems():
271       if lib_name not in lib_addr_map:
272         lib_addr_map[lib_name] = AddressList()
273       lib_addr_map[lib_name].append(lib_address)
274
275   logging.info('%s library load map: %s', process_type, lib_addr_map)
276
277   # For each library, check the randomness of its load addresses.
278   bad_libs = {}
279   success = True
280   for lib_name, lib_address_list in lib_addr_map.iteritems():
281     # If all addresses are different, skip to next item.
282     lib_address_set = set(lib_address_list)
283     # Consider that if there is more than one pair of identical addresses in
284     # the list, then randomization is broken.
285     if len(lib_address_set) < len(lib_address_list) - 1:
286       bad_libs[lib_name] = lib_address_list
287
288
289   if bad_libs:
290     return False, '%s libraries failed randomization: %s' % \
291         (process_type, bad_libs)
292
293   return True, '%s libraries properly randomized: %s' % \
294       (process_type, lib_addr_map)
295
296
297 class LinkerTestCaseBase(object):
298   """Base class for linker test cases."""
299
300   def __init__(self, is_low_memory=False):
301     """Create a test case.
302     Args:
303       is_low_memory: True to simulate a low-memory device, False otherwise.
304     """
305     self.is_low_memory = is_low_memory
306     if is_low_memory:
307         test_suffix = 'ForLowMemoryDevice'
308     else:
309         test_suffix = 'ForRegularDevice'
310     class_name = self.__class__.__name__
311     self.qualified_name = '%s.%s' % (class_name, test_suffix)
312     self.tagged_name = self.qualified_name
313
314   def _RunTest(self, adb):
315     """Run the test, must be overriden.
316     Args:
317       adb: An AndroidCommands instance to the device.
318     Returns:
319       A (status, log) tuple, where <status> is a ResultType constant, and <log>
320       is the logcat output captured during the test in case of error, or None
321       in case of success.
322     """
323     return ResultType.FAIL, 'Unimplemented _RunTest() method!'
324
325   def Run(self, device):
326     """Run the test on a given device.
327     Args:
328       device: Name of target device where to run the test.
329     Returns:
330       A base_test_result.TestRunResult() instance.
331     """
332     margin = 8
333     print '[ %-*s ] %s' % (margin, 'RUN', self.tagged_name)
334     logging.info('Running linker test: %s', self.tagged_name)
335     adb = android_commands.AndroidCommands(device)
336
337     # Create command-line file on device.
338     command_line_flags = ''
339     if self.is_low_memory:
340       command_line_flags = '--low-memory-device'
341     _WriteCommandLineFile(adb, command_line_flags, _COMMAND_LINE_FILE)
342
343     # Run the test.
344     status, logs = self._RunTest(adb)
345
346     result_text = 'OK'
347     if status == ResultType.FAIL:
348       result_text = 'FAILED'
349     elif status == ResultType.TIMEOUT:
350       result_text = 'TIMEOUT'
351     print '[ %*s ] %s' % (margin, result_text, self.tagged_name)
352
353     results = base_test_result.TestRunResults()
354     results.AddResult(
355         base_test_result.BaseTestResult(
356             self.tagged_name,
357             status,
358             logs))
359
360     return results
361
362   def __str__(self):
363     return self.tagged_name
364
365   def __repr__(self):
366     return self.tagged_name
367
368
369 class LinkerSharedRelroTest(LinkerTestCaseBase):
370   """A linker test case to check the status of shared RELRO sections.
371
372     The core of the checks performed here are pretty simple:
373
374       - Clear the logcat and start recording with an appropriate set of filters.
375       - Create the command-line appropriate for the test-case.
376       - Start the activity (always forcing a cold start).
377       - Every second, look at the current content of the filtered logcat lines
378         and look for instances of the following:
379
380             BROWSER_LINKER_TEST: <status>
381             RENDERER_LINKER_TEST: <status>
382
383         where <status> can be either FAIL or SUCCESS. These lines can appear
384         in any order in the logcat. Once both browser and renderer status are
385         found, stop the loop. Otherwise timeout after 30 seconds.
386
387         Note that there can be other lines beginning with BROWSER_LINKER_TEST:
388         and RENDERER_LINKER_TEST:, but are not followed by a <status> code.
389
390       - The test case passes if the <status> for both the browser and renderer
391         process are SUCCESS. Otherwise its a fail.
392   """
393   def _RunTest(self, adb):
394     # Wait up to 30 seconds until the linker test status is in the logcat.
395     return _StartActivityAndWaitForLinkerTestStatus(adb, timeout=30)
396
397
398 class LinkerLibraryAddressTest(LinkerTestCaseBase):
399   """A test case that verifies library load addresses.
400
401      The point of this check is to ensure that the libraries are loaded
402      according to the following rules:
403
404      - For low-memory devices, they should always be loaded at the same address
405        in both browser and renderer processes, both below 0x4000_0000.
406
407      - For regular devices, the browser process should load libraries above
408        0x4000_0000, and renderer ones below it.
409   """
410   def _RunTest(self, adb):
411     result, logs = _StartActivityAndWaitForLinkerTestStatus(adb, timeout=30)
412
413     # Return immediately in case of timeout.
414     if result == ResultType.TIMEOUT:
415       return result, logs
416
417     # Collect the library load addresses in the browser and renderer processes.
418     browser_libs, renderer_libs = _ExtractLibraryLoadAddressesFromLogcat(logs)
419
420     logging.info('Browser libraries: %s', browser_libs)
421     logging.info('Renderer libraries: %s', renderer_libs)
422
423     # Check that the same libraries are loaded into both processes:
424     browser_set = set(browser_libs.keys())
425     renderer_set = set(renderer_libs.keys())
426     if browser_set != renderer_set:
427       logging.error('Library set mistmach browser=%s renderer=%s',
428           browser_libs.keys(), renderer_libs.keys())
429       return ResultType.FAIL, logs
430
431     # And that there are not empty.
432     if not browser_set:
433       logging.error('No libraries loaded in any process!')
434       return ResultType.FAIL, logs
435
436     # Check that the renderer libraries are loaded at 'low-addresses'. i.e.
437     # below 0x4000_0000, for every kind of device.
438     memory_boundary = 0x40000000
439     bad_libs = []
440     for lib_name, lib_address in renderer_libs.iteritems():
441       if lib_address >= memory_boundary:
442         bad_libs.append((lib_name, lib_address))
443
444     if bad_libs:
445       logging.error('Renderer libraries loaded at high addresses: %s', bad_libs)
446       return ResultType.FAIL, logs
447
448     browser_config = _GetBrowserSharedRelroConfig()
449     if not browser_config:
450       return ResultType.FAIL, 'Bad linker source configuration'
451
452     if browser_config == 'ALWAYS' or \
453         (browser_config == 'LOW_RAM_ONLY' and self.is_low_memory):
454       # The libraries must all be loaded at the same addresses. This also
455       # implicitly checks that the browser libraries are at low addresses.
456       addr_mismatches = []
457       for lib_name, lib_address in browser_libs.iteritems():
458         lib_address2 = renderer_libs[lib_name]
459         if lib_address != lib_address2:
460           addr_mismatches.append((lib_name, lib_address, lib_address2))
461
462       if addr_mismatches:
463         logging.error('Library load address mismatches: %s',
464             addr_mismatches)
465         return ResultType.FAIL, logs
466
467     # Otherwise, check that libraries are loaded at 'high-addresses'.
468     # Note that for low-memory devices, the previous checks ensure that they
469     # were loaded at low-addresses.
470     else:
471       bad_libs = []
472       for lib_name, lib_address in browser_libs.iteritems():
473         if lib_address < memory_boundary:
474           bad_libs.append((lib_name, lib_address))
475
476       if bad_libs:
477         logging.error('Browser libraries loaded at low addresses: %s', bad_libs)
478         return ResultType.FAIL, logs
479
480     # Everything's ok.
481     return ResultType.PASS, logs
482
483
484 class LinkerRandomizationTest(LinkerTestCaseBase):
485   """A linker test case to check that library load address randomization works
486      properly between successive starts of the test program/activity.
487
488      This starts the activity several time (each time forcing a new process
489      creation) and compares the load addresses of the libraries in them to
490      detect that they have changed.
491
492      In theory, two successive runs could (very rarely) use the same load
493      address, so loop 5 times and compare the values there. It is assumed
494      that if there are more than one pair of identical addresses, then the
495      load addresses are not random enough for this test.
496   """
497   def _RunTest(self, adb):
498     max_loops = 5
499     browser_lib_map_list = []
500     renderer_lib_map_list = []
501     logs_list = []
502     for loop in range(max_loops):
503       # Start the activity.
504       result, logs = _StartActivityAndWaitForLinkerTestStatus(adb, timeout=30)
505       if result == ResultType.TIMEOUT:
506         # Something bad happened. Return immediately.
507         return result, logs
508
509       # Collect library addresses.
510       browser_libs, renderer_libs = _ExtractLibraryLoadAddressesFromLogcat(logs)
511       browser_lib_map_list.append(browser_libs)
512       renderer_lib_map_list.append(renderer_libs)
513       logs_list.append(logs)
514
515     # Check randomization in the browser libraries.
516     logs = '\n'.join(logs_list)
517
518     browser_status, browser_logs = _CheckLoadAddressRandomization(
519         browser_lib_map_list, 'Browser')
520
521     renderer_status, renderer_logs = _CheckLoadAddressRandomization(
522         renderer_lib_map_list, 'Renderer')
523
524     browser_config = _GetBrowserSharedRelroConfig()
525     if not browser_config:
526       return ResultType.FAIL, 'Bad linker source configuration'
527
528     if not browser_status:
529       if browser_config == 'ALWAYS' or \
530           (browser_config == 'LOW_RAM_ONLY' and self.is_low_memory):
531         return ResultType.FAIL, browser_logs
532
533       # IMPORTANT NOTE: The system's ASLR implementation seems to be very poor
534       # when starting an activity process in a loop with "adb shell am start".
535       #
536       # When simulating a regular device, loading libraries in the browser
537       # process uses a simple mmap(NULL, ...) to let the kernel device where to
538       # load the file (this is similar to what System.loadLibrary() does).
539       #
540       # Unfortunately, at least in the context of this test, doing so while
541       # restarting the activity with the activity manager very, very, often
542       # results in the system using the same load address for all 5 runs, or
543       # sometimes only 4 out of 5.
544       #
545       # This has been tested experimentally on both Android 4.1.2 and 4.3.
546       #
547       # Note that this behaviour doesn't seem to happen when starting an
548       # application 'normally', i.e. when using the application launcher to
549       # start the activity.
550       logging.info('Ignoring system\'s low randomization of browser libraries' +
551                    ' for regular devices')
552
553     if not renderer_status:
554       return ResultType.FAIL, renderer_logs
555
556     return ResultType.PASS, logs
557
558
559 class LinkerLowMemoryThresholdTest(LinkerTestCaseBase):
560   """This test checks that the definitions for the low-memory device physical
561      RAM threshold are identical in the base/ and linker sources. Because these
562      two components should absolutely not depend on each other, it's difficult
563      to perform this check correctly at runtime inside the linker test binary
564      without introducing hairy dependency issues in the build, or complicated
565      plumbing at runtime.
566
567      To work-around this, this test looks directly into the sources for a
568      definition of the same constant that should look like:
569
570        #define ANDROID_LOW_MEMORY_DEVICE_THRESHOLD_MB  <number>
571
572      And will check that the values for <number> are identical in all of
573      them."""
574
575   # A regular expression used to find the definition of the threshold in all
576   # sources:
577   _RE_THRESHOLD_DEFINITION = re.compile(
578       r'^\s*#\s*define\s+ANDROID_LOW_MEMORY_DEVICE_THRESHOLD_MB\s+(\d+)\s*$',
579       re.MULTILINE)
580
581   # The list of source files, relative to DIR_SOURCE_ROOT, which must contain
582   # a line that matches the re above.
583   _SOURCES_LIST = [
584       'base/android/sys_utils.cc',
585       'content/common/android/linker/linker_jni.cc' ]
586
587   def _RunTest(self, adb):
588     failure = False
589     values = []
590     # First, collect all the values in all input sources.
591     re = LinkerLowMemoryThresholdTest._RE_THRESHOLD_DEFINITION
592     for source in LinkerLowMemoryThresholdTest._SOURCES_LIST:
593       source_path = os.path.join(constants.DIR_SOURCE_ROOT, source);
594       if not os.path.exists(source_path):
595         logging.error('Missing source file: ' + source_path)
596         failure = True
597         continue
598       with open(source_path) as f:
599         source_text = f.read()
600         # For some reason, re.match() never works here.
601         source_values = re.findall(source_text)
602         if not source_values:
603           logging.error('Missing low-memory threshold definition in ' + \
604                         source_path)
605           logging.error('Source:\n%s\n' % source_text)
606           failure = True
607           continue
608         values += source_values
609
610     # Second, check that they are all the same.
611     if not failure:
612       for value in values[1:]:
613         if value != values[0]:
614           logging.error('Value mismatch: ' + repr(values))
615           failure = True
616
617     if failure:
618       return ResultType.FAIL, 'Incorrect low-end memory threshold definitions!'
619
620     return ResultType.PASS, ''