crash: Refactor test for user crash reporting

The logging_UserCrash test contains code that should be shared with the
ARC counterpart, specifically the crasher executable and Python helpers.
This CL pulls this into a UserCrashTest base class. The only functional
change is that the crasher executable is copied if an alternate path is
passed to _run_crasher_process. The ARC test needs a copy in the chroot.
This CL also fixes several lint errors.

BUG=b:27503472
TEST=logging_UserCrash

Change-Id: Ia8d92d1041b97624a369a7731a7fa927abeb9b0e
Reviewed-on: https://chromium-review.googlesource.com/338846
Commit-Ready: Dominik Laskowski <domlaskowski@google.com>
Tested-by: Dominik Laskowski <domlaskowski@google.com>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
diff --git a/client/cros/crash/__init__.py b/client/cros/crash/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/client/cros/crash/__init__.py
diff --git a/client/cros/crash/common.py b/client/cros/crash/common.py
new file mode 100644
index 0000000..882a563
--- /dev/null
+++ b/client/cros/crash/common.py
@@ -0,0 +1,18 @@
+# Copyright 2016 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""This module provides access to the autotest_lib.client namespace. It must be
+   included before any of the modules from that namespace."""
+
+import os, sys
+
+dirname = os.path.dirname(sys.modules[__name__].__file__)
+client_dir = os.path.abspath(os.path.join(dirname, "..", ".."))
+sys.path.insert(0, client_dir)
+
+import setup_modules
+
+sys.path.pop(0)
+setup_modules.setup(base_path=client_dir,
+                    root_module_name="autotest_lib.client")
diff --git a/client/cros/crash_test.py b/client/cros/crash/crash_test.py
similarity index 99%
rename from client/cros/crash_test.py
rename to client/cros/crash/crash_test.py
index 09f4c24..4a7cc7d 100644
--- a/client/cros/crash_test.py
+++ b/client/cros/crash/crash_test.py
@@ -4,9 +4,10 @@
 
 import contextlib, fcntl, logging, os, re, shutil
 
-import common, constants, cros_logging
+import common
 from autotest_lib.client.bin import test, utils
 from autotest_lib.client.common_lib import error
+from autotest_lib.client.cros import constants, cros_logging
 
 
 class CrashTest(test.test):
@@ -206,7 +207,7 @@
 
         @param has_consent: True to indicate consent, False otherwise
         """
-        autotest_cros_dir = os.path.dirname(__file__)
+        autotest_cros_dir = os.path.join(os.path.dirname(__file__), '..')
         if has_consent:
             if os.path.isdir(constants.WHITELIST_DIR):
                 # Create policy file that enables metrics/consent.
diff --git a/client/site_tests/logging_UserCrash/src/Makefile b/client/cros/crash/crasher/Makefile
similarity index 100%
rename from client/site_tests/logging_UserCrash/src/Makefile
rename to client/cros/crash/crasher/Makefile
diff --git a/client/site_tests/logging_UserCrash/src/bomb.cc b/client/cros/crash/crasher/bomb.cc
similarity index 100%
rename from client/site_tests/logging_UserCrash/src/bomb.cc
rename to client/cros/crash/crasher/bomb.cc
diff --git a/client/site_tests/logging_UserCrash/src/crasher.cc b/client/cros/crash/crasher/crasher.cc
similarity index 100%
rename from client/site_tests/logging_UserCrash/src/crasher.cc
rename to client/cros/crash/crasher/crasher.cc
diff --git a/client/cros/crash/user_crash_test.py b/client/cros/crash/user_crash_test.py
new file mode 100644
index 0000000..74f5260
--- /dev/null
+++ b/client/cros/crash/user_crash_test.py
@@ -0,0 +1,408 @@
+# Copyright 2016 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import grp
+import logging
+import os
+import pwd
+import re
+import shutil
+import signal
+import stat
+import subprocess
+
+import crash_test
+from autotest_lib.client.bin import utils
+from autotest_lib.client.common_lib import error
+
+
+class UserCrashTest(crash_test.CrashTest):
+    """
+    Base class for tests that verify crash reporting for user processes. Shared
+    functionality includes installing a crasher executable, generating Breakpad
+    symbols, running the crasher process, and verifying collection and sending.
+    """
+
+
+    def setup(self):
+        crasher_dir = os.path.join(os.path.dirname(__file__), 'crasher')
+        shutil.copytree(crasher_dir, self.srcdir)
+
+        os.chdir(self.srcdir)
+        utils.make()
+
+
+    def _prepare_crasher(self):
+        """Extract the crasher and set its permissions.
+
+        crasher is only gzipped to subvert Portage stripping.
+        """
+        self._crasher_path = os.path.join(self.srcdir, 'crasher_nobreakpad')
+        utils.system('cd %s; tar xzf crasher.tgz-unmasked' %
+                     self.srcdir)
+        # Make sure all users (specifically chronos) have access to
+        # this directory and its decendents in order to run crasher
+        # executable as different users.
+        utils.system('chmod -R a+rx ' + self.bindir)
+
+
+    def _populate_symbols(self):
+        """Set up Breakpad's symbol structure.
+
+        Breakpad's minidump processor expects symbols to be in a directory
+        hierarchy:
+          <symbol-root>/<module_name>/<file_id>/<module_name>.sym
+        """
+        # Dump the symbols from the crasher
+        self._symbol_dir = os.path.join(self.srcdir, 'symbols')
+        utils.system('rm -rf %s' % self._symbol_dir)
+        os.mkdir(self._symbol_dir)
+
+        basename = os.path.basename(self._crasher_path)
+        utils.system('/usr/bin/dump_syms %s > %s.sym' %
+                     (self._crasher_path,
+                      basename))
+        sym_name = '%s.sym' % basename
+        symbols = utils.read_file(sym_name)
+        # First line should be like:
+        # MODULE Linux x86 7BC3323FBDBA2002601FA5BA3186D6540 crasher_XXX
+        #  or
+        # MODULE Linux arm C2FE4895B203D87DD4D9227D5209F7890 crasher_XXX
+        first_line = symbols.split('\n')[0]
+        tokens = first_line.split()
+        if tokens[0] != 'MODULE' or tokens[1] != 'Linux':
+          raise error.TestError('Unexpected symbols format: %s',
+                                first_line)
+        file_id = tokens[3]
+        target_dir = os.path.join(self._symbol_dir, basename, file_id)
+        os.makedirs(target_dir)
+        os.rename(sym_name, os.path.join(target_dir, sym_name))
+
+
+    def _is_frame_in_stack(self, frame_index, module_name,
+                           function_name, file_name,
+                           line_number, stack):
+        """Search for frame entries in the given stack dump text.
+
+        A frame entry looks like (alone on a line):
+          16  crasher_nobreakpad!main [crasher.cc : 21 + 0xb]
+
+        Args:
+          frame_index: number of the stack frame (0 is innermost frame)
+          module_name: name of the module (executable or dso)
+          function_name: name of the function in the stack
+          file_name: name of the file containing the function
+          line_number: line number
+          stack: text string of stack frame entries on separate lines.
+
+        Returns:
+          Boolean indicating if an exact match is present.
+
+        Note:
+          We do not care about the full function signature - ie, is it
+          foo or foo(ClassA *).  These are present in function names
+          pulled by dump_syms for Stabs but not for DWARF.
+        """
+        regexp = (r'\n\s*%d\s+%s!%s.*\[\s*%s\s*:\s*%d\s.*\]' %
+                  (frame_index, module_name,
+                   function_name, file_name,
+                   line_number))
+        logging.info('Searching for regexp %s', regexp)
+        return re.search(regexp, stack) is not None
+
+
+    def _verify_stack(self, stack, basename, from_crash_reporter):
+        logging.debug('Crash stackwalk was: %s', stack)
+
+        # Should identify cause as SIGSEGV at address 0x16
+        match = re.search(r'Crash reason:\s+(.*)', stack)
+        expected_address = '0x16'
+        if from_crash_reporter:
+            # We cannot yet determine the crash address when coming
+            # through core files via crash_reporter.
+            expected_address = '0x0'
+        if not match or match.group(1) != 'SIGSEGV':
+            raise error.TestFail('Did not identify SIGSEGV cause')
+        match = re.search(r'Crash address:\s+(.*)', stack)
+        if not match or match.group(1) != expected_address:
+            raise error.TestFail('Did not identify crash address %s' %
+                                 expected_address)
+
+        # Should identify crash at *(char*)0x16 assignment line
+        if not self._is_frame_in_stack(0, basename,
+                                       'recbomb', 'bomb.cc', 9, stack):
+            raise error.TestFail('Did not show crash line on stack')
+
+        # Should identify recursion line which is on the stack
+        # for 15 levels
+        if not self._is_frame_in_stack(15, basename, 'recbomb',
+                                       'bomb.cc', 12, stack):
+            raise error.TestFail('Did not show recursion line on stack')
+
+        # Should identify main line
+        if not self._is_frame_in_stack(16, basename, 'main',
+                                       'crasher.cc', 20, stack):
+            raise error.TestFail('Did not show main on stack')
+
+
+    def _run_crasher_process(self, username, cause_crash=True, consent=True,
+                             crasher_path=None):
+        """Runs the crasher process.
+
+        Will wait up to 5 seconds for crash_reporter to report the crash.
+        crash_reporter_caught will be marked as true when the "Received crash
+        notification message..." appears. While associated logs are likely to be
+        available at this point, the function does not guarantee this.
+
+        Returns:
+          A dictionary with keys:
+            returncode: return code of the crasher
+            crashed: did the crasher return segv error code
+            crash_reporter_caught: did crash_reporter catch a segv
+            output: stderr/stdout output of the crasher process
+        """
+        if crasher_path is None:
+            crasher_path = self._crasher_path
+        else:
+            utils.system('cp -a "%s" "%s"' % (self._crasher_path, crasher_path))
+
+        self.enable_crash_filtering(os.path.basename(crasher_path))
+
+        if username != 'root':
+            crasher_command = ['su', username, '-c']
+            expected_result = 128 + signal.SIGSEGV
+        else:
+            crasher_command = []
+            expected_result = -signal.SIGSEGV
+
+        crasher_command.append(crasher_path)
+        basename = os.path.basename(crasher_path)
+        if not cause_crash:
+            crasher_command.append('--nocrash')
+        self._set_consent(consent)
+        crasher = subprocess.Popen(crasher_command,
+                                   stdout=subprocess.PIPE,
+                                   stderr=subprocess.PIPE)
+        output = crasher.communicate()[1]
+        logging.debug('Output from %s: %s', crasher_command, output)
+
+        # Grab the pid from the process output.  We can't just use
+        # crasher.pid unfortunately because that may be the PID of su.
+        match = re.search(r'pid=(\d+)', output)
+        if not match:
+            raise error.TestFail('Could not find pid output from crasher: %s' %
+                                 output)
+        pid = int(match.group(1))
+
+        expected_uid = pwd.getpwnam(username)[2]
+        if consent:
+            handled_string = 'handling'
+        else:
+            handled_string = 'ignoring - no consent'
+        expected_message = (
+            'Received crash notification for %s[%d] sig 11, user %d (%s)' %
+            (basename, pid, expected_uid, handled_string))
+
+        # Wait until no crash_reporter is running.
+        utils.poll_for_condition(
+            lambda: utils.system('pgrep -f crash_reporter.*:%s' % basename,
+                                 ignore_status=True) != 0,
+            timeout=10,
+            exception=error.TestError(
+                'Timeout waiting for crash_reporter to finish: ' +
+                self._log_reader.get_logs()))
+
+        logging.debug('crash_reporter_caught message: %s', expected_message)
+        is_caught = False
+        try:
+            utils.poll_for_condition(
+                lambda: self._log_reader.can_find(expected_message),
+                timeout=5)
+            is_caught = True
+        except utils.TimeoutError:
+            pass
+
+        result = {'crashed': crasher.returncode == expected_result,
+                  'crash_reporter_caught': is_caught,
+                  'output': output,
+                  'returncode': crasher.returncode}
+        logging.debug('Crasher process result: %s', result)
+        return result
+
+
+    def _check_crash_directory_permissions(self, crash_dir):
+        stat_info = os.stat(crash_dir)
+        user = pwd.getpwuid(stat_info.st_uid)[0]
+        group = grp.getgrgid(stat_info.st_gid)[0]
+        mode = stat.S_IMODE(stat_info.st_mode)
+
+        if crash_dir == '/var/spool/crash':
+            expected_user = 'root'
+            expected_group = 'root'
+            expected_mode = 01755
+        else:
+            expected_user = 'chronos'
+            expected_group = 'chronos'
+            expected_mode = 0755
+
+        if user != expected_user or group != expected_group:
+            raise error.TestFail(
+                'Expected %s.%s ownership of %s (actual %s.%s)' %
+                (expected_user, expected_group, crash_dir, user, group))
+        if mode != expected_mode:
+            raise error.TestFail(
+                'Expected %s to have mode %o (actual %o)' %
+                (crash_dir, expected_mode, mode))
+
+
+    def _check_minidump_stackwalk(self, minidump_path, basename,
+                                  from_crash_reporter):
+        # Now stackwalk the minidump
+        stack = utils.system_output('/usr/bin/minidump_stackwalk %s %s' %
+                                    (minidump_path, self._symbol_dir))
+        self._verify_stack(stack, basename, from_crash_reporter)
+
+
+    def _check_generated_report_sending(self, meta_path, payload_path,
+                                        username, exec_name, report_kind,
+                                        expected_sig=None):
+        # Now check that the sending works
+        result = self._call_sender_one_crash(
+            username=username,
+            report=os.path.basename(payload_path))
+        if (not result['send_attempt'] or not result['send_success'] or
+            result['report_exists']):
+            raise error.TestFail('Report not sent properly')
+        if result['exec_name'] != exec_name:
+            raise error.TestFail('Executable name incorrect')
+        if result['report_kind'] != report_kind:
+            raise error.TestFail('Expected a minidump report')
+        if result['report_payload'] != payload_path:
+            raise error.TestFail('Sent the wrong minidump payload')
+        if result['meta_path'] != meta_path:
+            raise error.TestFail('Used the wrong meta file')
+        if expected_sig is None:
+            if result['sig'] is not None:
+                raise error.TestFail('Report should not have signature')
+        else:
+            if not 'sig' in result or result['sig'] != expected_sig:
+                raise error.TestFail('Report signature mismatch: %s vs %s' %
+                                     (result['sig'], expected_sig))
+
+        # Check version matches.
+        lsb_release = utils.read_file('/etc/lsb-release')
+        version_match = re.search(r'CHROMEOS_RELEASE_VERSION=(.*)', lsb_release)
+        if not ('Version: %s' % version_match.group(1)) in result['output']:
+            raise error.TestFail('Did not find version %s in log output' %
+                                 version_match.group(1))
+
+
+    def _run_crasher_process_and_analyze(self, username,
+                                         cause_crash=True, consent=True,
+                                         crasher_path=None):
+        self._log_reader.set_start_by_current()
+
+        result = self._run_crasher_process(username, cause_crash=cause_crash,
+                                           consent=consent,
+                                           crasher_path=crasher_path)
+
+        if not result['crashed'] or not result['crash_reporter_caught']:
+            return result;
+
+        crash_dir = self._get_crash_dir(username)
+
+        if not consent:
+            if os.path.exists(crash_dir):
+                raise error.TestFail('Crash directory should not exist')
+            return result
+
+        crash_contents = os.listdir(crash_dir)
+        basename = os.path.basename(crasher_path or self._crasher_path)
+
+        breakpad_minidump = None
+        crash_reporter_minidump = None
+        crash_reporter_meta = None
+        crash_reporter_log = None
+
+        self._check_crash_directory_permissions(crash_dir)
+
+        logging.debug('Contents in %s: %s', crash_dir, crash_contents)
+
+        for filename in crash_contents:
+            if filename.endswith('.core'):
+                # Ignore core files.  We'll test them later.
+                pass
+            elif (filename.startswith(basename) and
+                  filename.endswith('.dmp')):
+                # This appears to be a minidump created by the crash reporter.
+                if not crash_reporter_minidump is None:
+                    raise error.TestFail('Crash reporter wrote multiple '
+                                         'minidumps')
+                crash_reporter_minidump = os.path.join(crash_dir, filename)
+            elif (filename.startswith(basename) and
+                  filename.endswith('.meta')):
+                if not crash_reporter_meta is None:
+                    raise error.TestFail('Crash reporter wrote multiple '
+                                         'meta files')
+                crash_reporter_meta = os.path.join(crash_dir, filename)
+            elif (filename.startswith(basename) and
+                  filename.endswith('.log')):
+                if not crash_reporter_log is None:
+                    raise error.TestFail('Crash reporter wrote multiple '
+                                         'log files')
+                crash_reporter_log = os.path.join(crash_dir, filename)
+            else:
+                # This appears to be a breakpad created minidump.
+                if not breakpad_minidump is None:
+                    raise error.TestFail('Breakpad wrote multimpe minidumps')
+                breakpad_minidump = os.path.join(crash_dir, filename)
+
+        if breakpad_minidump:
+            raise error.TestFail('%s did generate breakpad minidump' % basename)
+
+        if not crash_reporter_meta:
+            raise error.TestFail('crash reporter did not generate meta')
+
+        result['minidump'] = crash_reporter_minidump
+        result['basename'] = basename
+        result['meta'] = crash_reporter_meta
+        result['log'] = crash_reporter_log
+        return result
+
+
+    def _check_crashed_and_caught(self, result):
+        if not result['crashed']:
+            raise error.TestFail('crasher did not do its job of crashing: %d' %
+                                 result['returncode'])
+
+        if not result['crash_reporter_caught']:
+            logging.debug('Messages that should have included segv: %s',
+                          self._log_reader.get_logs())
+            raise error.TestFail('Did not find segv message')
+
+
+    def _check_crashing_process(self, username, consent=True):
+        result = self._run_crasher_process_and_analyze(username,
+                                                       consent=consent)
+
+        self._check_crashed_and_caught(result)
+
+        if not consent:
+            return
+
+        if not result['minidump']:
+            raise error.TestFail('crash reporter did not generate minidump')
+
+        if not self._log_reader.can_find('Stored minidump to ' +
+                                         result['minidump']):
+            raise error.TestFail('crash reporter did not announce minidump')
+
+        self._check_minidump_stackwalk(result['minidump'],
+                                       result['basename'],
+                                       from_crash_reporter=True)
+        self._check_generated_report_sending(result['meta'],
+                                             result['minidump'],
+                                             username,
+                                             result['basename'],
+                                             'minidump')
diff --git a/client/site_tests/logging_CrashSender/logging_CrashSender.py b/client/site_tests/logging_CrashSender/logging_CrashSender.py
index 337330a..21005e3 100644
--- a/client/site_tests/logging_CrashSender/logging_CrashSender.py
+++ b/client/site_tests/logging_CrashSender/logging_CrashSender.py
@@ -5,7 +5,7 @@
 import logging, os, re
 from autotest_lib.client.bin import utils
 from autotest_lib.client.common_lib import error
-from autotest_lib.client.cros import crash_test
+from autotest_lib.client.cros.crash import crash_test
 
 
 _25_HOURS_AGO = -25 * 60 * 60
diff --git a/client/site_tests/logging_CrashServices/logging_CrashServices.py b/client/site_tests/logging_CrashServices/logging_CrashServices.py
index 51cfe16..6f58af5 100644
--- a/client/site_tests/logging_CrashServices/logging_CrashServices.py
+++ b/client/site_tests/logging_CrashServices/logging_CrashServices.py
@@ -6,9 +6,10 @@
 from autotest_lib.client.bin import test, utils
 from autotest_lib.client.common_lib import error
 from autotest_lib.client.common_lib.cros import chrome
-from autotest_lib.client.cros.crash_test import CrashTest
+from autotest_lib.client.cros.crash.crash_test import CrashTest
 
 class logging_CrashServices(test.test):
+    """Verifies crash collection for system services."""
     version = 3
 
     process_list = {
@@ -69,7 +70,7 @@
         for entry in entries:
             (filename, ext) = os.path.splitext(entry)
             if ext == filetype and filename.startswith(process_name):
-                logging.info('the path is %s' % os.path)
+                logging.info('the path is %s', os.path)
                 if os.path.getsize(path + '/' + entry) > 0 :
                     return entry
         return None
diff --git a/client/site_tests/logging_KernelCrash/logging_KernelCrash.py b/client/site_tests/logging_KernelCrash/logging_KernelCrash.py
index 9fd7dfb..24308af 100644
--- a/client/site_tests/logging_KernelCrash/logging_KernelCrash.py
+++ b/client/site_tests/logging_KernelCrash/logging_KernelCrash.py
@@ -5,7 +5,7 @@
 import logging, os, re
 from autotest_lib.client.bin import utils
 from autotest_lib.client.common_lib import error
-from autotest_lib.client.cros import crash_test
+from autotest_lib.client.cros.crash import crash_test
 
 
 class logging_KernelCrash(crash_test.CrashTest):
diff --git a/client/site_tests/logging_UdevCrash/logging_UdevCrash.py b/client/site_tests/logging_UdevCrash/logging_UdevCrash.py
index ed43fd4..010d795 100644
--- a/client/site_tests/logging_UdevCrash/logging_UdevCrash.py
+++ b/client/site_tests/logging_UdevCrash/logging_UdevCrash.py
@@ -5,7 +5,7 @@
 import gzip, logging, os, utils
 from autotest_lib.client.bin import utils
 from autotest_lib.client.common_lib import error
-from autotest_lib.client.cros import crash_test
+from autotest_lib.client.cros.crash import crash_test
 
 
 class logging_UdevCrash(crash_test.CrashTest):
diff --git a/client/site_tests/logging_UserCrash/logging_UserCrash.py b/client/site_tests/logging_UserCrash/logging_UserCrash.py
index 3f23b61..b9e0cdd 100644
--- a/client/site_tests/logging_UserCrash/logging_UserCrash.py
+++ b/client/site_tests/logging_UserCrash/logging_UserCrash.py
@@ -2,11 +2,11 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import grp, logging, os, pwd, re, stat, subprocess
-from signal import SIGSEGV
+import logging, os
 from autotest_lib.client.bin import utils
 from autotest_lib.client.common_lib import error
-from autotest_lib.client.cros import crash_test, cros_ui, upstart
+from autotest_lib.client.cros import cros_ui, upstart
+from autotest_lib.client.cros.crash import user_crash_test
 
 
 _COLLECTION_ERROR_SIGNATURE = 'crash_reporter-user-collection'
@@ -15,16 +15,11 @@
 _MAX_CRASH_DIRECTORY_SIZE = 32
 
 
-class logging_UserCrash(crash_test.CrashTest):
+class logging_UserCrash(user_crash_test.UserCrashTest):
+    """Verifies crash reporting for user processes."""
     version = 1
 
 
-    def setup(self):
-        os.chdir(self.srcdir)
-        utils.make('clean')
-        utils.make('all')
-
-
     def _test_reporter_startup(self):
         """Test that the core_pattern is set up by crash reporter."""
         # Turn off crash filtering so we see the original setting.
@@ -53,382 +48,6 @@
                                  output)
 
 
-    def _prepare_crasher(self):
-        """Extract the crasher and set its permissions.
-
-        crasher is only gzipped to subvert Portage stripping.
-        """
-        self._crasher_path = os.path.join(self.srcdir, 'crasher_nobreakpad')
-        utils.system('cd %s; tar xzf crasher.tgz-unmasked' %
-                     self.srcdir)
-        # Make sure all users (specifically chronos) have access to
-        # this directory and its decendents in order to run crasher
-        # executable as different users.
-        utils.system('chmod -R a+rx ' + self.bindir)
-
-
-    def _populate_symbols(self):
-        """Set up Breakpad's symbol structure.
-
-        Breakpad's minidump processor expects symbols to be in a directory
-        hierarchy:
-          <symbol-root>/<module_name>/<file_id>/<module_name>.sym
-        """
-        # Dump the symbols from the crasher
-        self._symbol_dir = os.path.join(self.srcdir, 'symbols')
-        utils.system('rm -rf %s' % self._symbol_dir)
-        os.mkdir(self._symbol_dir)
-
-        basename = os.path.basename(self._crasher_path)
-        utils.system('/usr/bin/dump_syms %s > %s.sym' %
-                     (self._crasher_path,
-                      basename))
-        sym_name = '%s.sym' % basename
-        symbols = utils.read_file(sym_name)
-        # First line should be like:
-        # MODULE Linux x86 7BC3323FBDBA2002601FA5BA3186D6540 crasher_XXX
-        #  or
-        # MODULE Linux arm C2FE4895B203D87DD4D9227D5209F7890 crasher_XXX
-        first_line = symbols.split('\n')[0]
-        tokens = first_line.split()
-        if tokens[0] != 'MODULE' or tokens[1] != 'Linux':
-          raise error.TestError('Unexpected symbols format: %s',
-                                first_line)
-        file_id = tokens[3]
-        target_dir = os.path.join(self._symbol_dir, basename, file_id)
-        os.makedirs(target_dir)
-        os.rename(sym_name, os.path.join(target_dir, sym_name))
-
-
-    def _is_frame_in_stack(self, frame_index, module_name,
-                           function_name, file_name,
-                           line_number, stack):
-        """Search for frame entries in the given stack dump text.
-
-        A frame entry looks like (alone on a line):
-          16  crasher_nobreakpad!main [crasher.cc : 21 + 0xb]
-
-        Args:
-          frame_index: number of the stack frame (0 is innermost frame)
-          module_name: name of the module (executable or dso)
-          function_name: name of the function in the stack
-          file_name: name of the file containing the function
-          line_number: line number
-          stack: text string of stack frame entries on separate lines.
-
-        Returns:
-          Boolean indicating if an exact match is present.
-
-        Note:
-          We do not care about the full function signature - ie, is it
-          foo or foo(ClassA *).  These are present in function names
-          pulled by dump_syms for Stabs but not for DWARF.
-        """
-        regexp = (r'\n\s*%d\s+%s!%s.*\[\s*%s\s*:\s*%d\s.*\]' %
-                  (frame_index, module_name,
-                   function_name, file_name,
-                   line_number))
-        logging.info('Searching for regexp ' + regexp)
-        return re.search(regexp, stack) is not None
-
-
-    def _verify_stack(self, stack, basename, from_crash_reporter):
-        logging.debug('Crash stackwalk was: %s' % stack)
-
-        # Should identify cause as SIGSEGV at address 0x16
-        match = re.search(r'Crash reason:\s+(.*)', stack)
-        expected_address = '0x16'
-        if from_crash_reporter:
-            # We cannot yet determine the crash address when coming
-            # through core files via crash_reporter.
-            expected_address = '0x0'
-        if not match or match.group(1) != 'SIGSEGV':
-            raise error.TestFail('Did not identify SIGSEGV cause')
-        match = re.search(r'Crash address:\s+(.*)', stack)
-        if not match or match.group(1) != expected_address:
-            raise error.TestFail('Did not identify crash address %s' %
-                                 expected_address)
-
-        # Should identify crash at *(char*)0x16 assignment line
-        if not self._is_frame_in_stack(0, basename,
-                                       'recbomb', 'bomb.cc', 9, stack):
-            raise error.TestFail('Did not show crash line on stack')
-
-        # Should identify recursion line which is on the stack
-        # for 15 levels
-        if not self._is_frame_in_stack(15, basename, 'recbomb',
-                                       'bomb.cc', 12, stack):
-            raise error.TestFail('Did not show recursion line on stack')
-
-        # Should identify main line
-        if not self._is_frame_in_stack(16, basename, 'main',
-                                       'crasher.cc', 20, stack):
-            raise error.TestFail('Did not show main on stack')
-
-
-    def _run_crasher_process(self, username, cause_crash=True, consent=True,
-                             crasher_path=None):
-        """Runs the crasher process.
-
-        Will wait up to 5 seconds for crash_reporter to report the crash.
-        crash_reporter_caught will be marked as true when the "Received crash
-        notification message..." appears. While associated logs are likely to be
-        available at this point, the function does not guarantee this.
-
-        Args:
-          username: runs as given user
-          extra_args: additional parameters to pass to crasher process
-
-        Returns:
-          A dictionary with keys:
-            returncode: return code of the crasher
-            crashed: did the crasher return segv error code
-            crash_reporter_caught: did crash_reporter catch a segv
-            output: stderr/stdout output of the crasher process
-        """
-        if crasher_path is None: crasher_path = self._crasher_path
-        self.enable_crash_filtering(os.path.basename(crasher_path))
-
-        if username != 'root':
-            crasher_command = ['su', username, '-c']
-            expected_result = 128 + SIGSEGV
-        else:
-            crasher_command = []
-            expected_result = -SIGSEGV
-
-        crasher_command.append(crasher_path)
-        basename = os.path.basename(crasher_path)
-        if not cause_crash:
-            crasher_command.append('--nocrash')
-        self._set_consent(consent)
-        crasher = subprocess.Popen(crasher_command,
-                                   stdout=subprocess.PIPE,
-                                   stderr=subprocess.PIPE)
-        output = crasher.communicate()[1]
-        logging.debug('Output from %s: %s' %
-                      (crasher_command, output))
-
-        # Grab the pid from the process output.  We can't just use
-        # crasher.pid unfortunately because that may be the PID of su.
-        match = re.search(r'pid=(\d+)', output)
-        if not match:
-            raise error.TestFail('Could not find pid output from crasher: %s' %
-                                 output)
-        pid = int(match.group(1))
-
-        expected_uid = pwd.getpwnam(username)[2]
-        if consent:
-            handled_string = 'handling'
-        else:
-            handled_string = 'ignoring - no consent'
-        expected_message = (
-            'Received crash notification for %s[%d] sig 11, user %d (%s)' %
-            (basename, pid, expected_uid, handled_string))
-
-        # Wait until no crash_reporter is running.
-        utils.poll_for_condition(
-            lambda: utils.system('pgrep -f crash_reporter.*:%s' % basename,
-                                 ignore_status=True) != 0,
-            timeout=10,
-            exception=error.TestError(
-                'Timeout waiting for crash_reporter to finish: ' +
-                self._log_reader.get_logs()))
-
-        logging.debug('crash_reporter_caught message: ' + expected_message)
-        is_caught = False
-        try:
-            utils.poll_for_condition(
-                lambda: self._log_reader.can_find(expected_message),
-                timeout=5)
-            is_caught = True
-        except utils.TimeoutError:
-            pass
-
-        result = {'crashed': crasher.returncode == expected_result,
-                  'crash_reporter_caught': is_caught,
-                  'output': output,
-                  'returncode': crasher.returncode}
-        logging.debug('Crasher process result: %s' % result)
-        return result
-
-
-    def _check_crash_directory_permissions(self, crash_dir):
-        stat_info = os.stat(crash_dir)
-        user = pwd.getpwuid(stat_info.st_uid)[0]
-        group = grp.getgrgid(stat_info.st_gid)[0]
-        mode = stat.S_IMODE(stat_info.st_mode)
-
-        if crash_dir == '/var/spool/crash':
-            expected_user = 'root'
-            expected_group = 'root'
-            expected_mode = 01755
-        else:
-            expected_user = 'chronos'
-            expected_group = 'chronos'
-            expected_mode = 0755
-
-        if user != expected_user or group != expected_group:
-            raise error.TestFail(
-                'Expected %s.%s ownership of %s (actual %s.%s)' %
-                (expected_user, expected_group, crash_dir, user, group))
-        if mode != expected_mode:
-            raise error.TestFail(
-                'Expected %s to have mode %o (actual %o)' %
-                (crash_dir, expected_mode, mode))
-
-
-    def _check_minidump_stackwalk(self, minidump_path, basename,
-                                  from_crash_reporter):
-        # Now stackwalk the minidump
-        stack = utils.system_output('/usr/bin/minidump_stackwalk %s %s' %
-                                    (minidump_path, self._symbol_dir))
-        self._verify_stack(stack, basename, from_crash_reporter)
-
-
-    def _check_generated_report_sending(self, meta_path, payload_path,
-                                        username, exec_name, report_kind,
-                                        expected_sig=None):
-        # Now check that the sending works
-        result = self._call_sender_one_crash(
-            username=username,
-            report=os.path.basename(payload_path))
-        if (not result['send_attempt'] or not result['send_success'] or
-            result['report_exists']):
-            raise error.TestFail('Report not sent properly')
-        if result['exec_name'] != exec_name:
-            raise error.TestFail('Executable name incorrect')
-        if result['report_kind'] != report_kind:
-            raise error.TestFail('Expected a minidump report')
-        if result['report_payload'] != payload_path:
-            raise error.TestFail('Sent the wrong minidump payload')
-        if result['meta_path'] != meta_path:
-            raise error.TestFail('Used the wrong meta file')
-        if expected_sig is None:
-            if result['sig'] is not None:
-                raise error.TestFail('Report should not have signature')
-        else:
-            if not 'sig' in result or result['sig'] != expected_sig:
-                raise error.TestFail('Report signature mismatch: %s vs %s' %
-                                     (result['sig'], expected_sig))
-
-        # Check version matches.
-        lsb_release = utils.read_file('/etc/lsb-release')
-        version_match = re.search(r'CHROMEOS_RELEASE_VERSION=(.*)', lsb_release)
-        if not ('Version: %s' % version_match.group(1)) in result['output']:
-            raise error.TestFail('Did not find version %s in log output' %
-                                 version_match.group(1))
-
-
-    def _run_crasher_process_and_analyze(self, username,
-                                         cause_crash=True, consent=True,
-                                         crasher_path=None):
-        self._log_reader.set_start_by_current()
-
-        if crasher_path is None: crasher_path = self._crasher_path
-        result = self._run_crasher_process(username, cause_crash=cause_crash,
-                                           consent=consent,
-                                           crasher_path=crasher_path)
-
-        if not result['crashed'] or not result['crash_reporter_caught']:
-            return result;
-
-        crash_dir = self._get_crash_dir(username)
-
-        if not consent:
-            if os.path.exists(crash_dir):
-                raise error.TestFail('Crash directory should not exist')
-            return result
-
-        crash_contents = os.listdir(crash_dir)
-        basename = os.path.basename(crasher_path)
-
-        breakpad_minidump = None
-        crash_reporter_minidump = None
-        crash_reporter_meta = None
-        crash_reporter_log = None
-
-        self._check_crash_directory_permissions(crash_dir)
-
-        logging.debug('Contents in %s: %s' % (crash_dir, crash_contents))
-
-        for filename in crash_contents:
-            if filename.endswith('.core'):
-                # Ignore core files.  We'll test them later.
-                pass
-            elif (filename.startswith(basename) and
-                  filename.endswith('.dmp')):
-                # This appears to be a minidump created by the crash reporter.
-                if not crash_reporter_minidump is None:
-                    raise error.TestFail('Crash reporter wrote multiple '
-                                         'minidumps')
-                crash_reporter_minidump = os.path.join(crash_dir, filename)
-            elif (filename.startswith(basename) and
-                  filename.endswith('.meta')):
-                if not crash_reporter_meta is None:
-                    raise error.TestFail('Crash reporter wrote multiple '
-                                         'meta files')
-                crash_reporter_meta = os.path.join(crash_dir, filename)
-            elif (filename.startswith(basename) and
-                  filename.endswith('.log')):
-                if not crash_reporter_log is None:
-                    raise error.TestFail('Crash reporter wrote multiple '
-                                         'log files')
-                crash_reporter_log = os.path.join(crash_dir, filename)
-            else:
-                # This appears to be a breakpad created minidump.
-                if not breakpad_minidump is None:
-                    raise error.TestFail('Breakpad wrote multimpe minidumps')
-                breakpad_minidump = os.path.join(crash_dir, filename)
-
-        if breakpad_minidump:
-            raise error.TestFail('%s did generate breakpad minidump' % basename)
-
-        if not crash_reporter_meta:
-            raise error.TestFail('crash reporter did not generate meta')
-
-        result['minidump'] = crash_reporter_minidump
-        result['basename'] = basename
-        result['meta'] = crash_reporter_meta
-        result['log'] = crash_reporter_log
-        return result
-
-
-    def _check_crashed_and_caught(self, result):
-        if not result['crashed']:
-            raise error.TestFail('crasher did not do its job of crashing: %d' %
-                                 result['returncode'])
-
-        if not result['crash_reporter_caught']:
-            logging.debug('Messages that should have included segv: %s' %
-                          self._log_reader.get_logs())
-            raise error.TestFail('Did not find segv message')
-
-
-    def _check_crashing_process(self, username, consent=True):
-        result = self._run_crasher_process_and_analyze(username,
-                                                       consent=consent)
-
-        self._check_crashed_and_caught(result)
-
-        if not consent:
-            return
-
-        if not result['minidump']:
-            raise error.TestFail('crash reporter did not generate minidump')
-
-        if not self._log_reader.can_find('Stored minidump to ' +
-                                         result['minidump']):
-            raise error.TestFail('crash reporter did not announce minidump')
-
-        self._check_minidump_stackwalk(result['minidump'],
-                                       result['basename'],
-                                       from_crash_reporter=True)
-        self._check_generated_report_sending(result['meta'],
-                                             result['minidump'],
-                                             username,
-                                             result['basename'],
-                                             'minidump')
-
     def _test_no_crash(self):
         """Test a program linked against libcrash_dumper can exit normally."""
         self._log_reader.set_start_by_current()
@@ -512,14 +131,14 @@
         crash_dir_size = len(os.listdir(crash_dir))
         # For debugging
         utils.system('ls -l %s' % crash_dir)
-        logging.info('Crash directory had %d entries' % crash_dir_size)
+        logging.info('Crash directory had %d entries', crash_dir_size)
 
         # Crash a bunch more times, but make sure no new reports
         # are enqueued.
         for i in range(0, 10):
           self._log_reader.set_start_by_current()
           result = self._run_crasher_process(username)
-          logging.info('New log messages: %s' % self._log_reader.get_logs())
+          logging.info('New log messages: %s', self._log_reader.get_logs())
           if not result['crashed']:
             raise error.TestFail('failure after setting up queue: %d' %
                                  result['returncode'])
@@ -552,7 +171,7 @@
             if not result['log']:
                 raise error.TestFail('failed collection had no log')
             log_contents = utils.read_file(result['log'])
-            logging.debug('Log contents were: ' + log_contents)
+            logging.debug('Log contents were: %s', log_contents)
             if not failure_string in log_contents:
                 raise error.TestFail('Expected logged error '
                                      '\"%s\" was \"%s\"' %
@@ -584,12 +203,9 @@
 
 
     def _test_crash_logs_creation(self):
+        # Copy and rename crasher to trigger crash_reporter_logs.conf rule.
         logs_triggering_crasher = os.path.join(os.path.dirname(self.bindir),
                                                'crash_log_test')
-        # Copy crasher_path to a test location with correct mode and a
-        # special name to trigger crash log creation.
-        utils.system('cp -a "%s" "%s"' % (self._crasher_path,
-                                          logs_triggering_crasher))
         result = self._run_crasher_process_and_analyze(
             'root', crasher_path=logs_triggering_crasher)
         self._check_crashed_and_caught(result)
@@ -601,6 +217,7 @@
 
 
     def _test_crash_log_infinite_recursion(self):
+        # Copy and rename crasher to trigger crash_reporter_logs.conf rule.
         recursion_triggering_crasher = os.path.join(
             os.path.dirname(self.bindir), 'crash_log_recursion_test')
         # The configuration file hardcodes this path, so make sure it's still
@@ -608,10 +225,6 @@
         if (recursion_triggering_crasher !=
             '/usr/local/autotest/tests/crash_log_recursion_test'):
           raise error.TestError('Path to recursion test changed')
-        # Copy crasher_path to a test location with correct mode and a
-        # special name to trigger crash log creation.
-        utils.system('cp -a "%s" "%s"' % (self._crasher_path,
-                                          recursion_triggering_crasher))
         # Simply completing this command means that we avoided
         # infinite recursion.
         result = self._run_crasher_process(
@@ -629,7 +242,7 @@
         crash_contents = os.listdir(self._get_crash_dir('root'))
 
         logging.debug('Contents of crash directory: %s', crash_contents)
-        logging.debug('Log messages: %s' % self._log_reader.get_logs())
+        logging.debug('Log messages: %s', self._log_reader.get_logs())
 
         if expect_persist:
             if not self._log_reader.can_find('Leaving core file at'):
@@ -672,7 +285,7 @@
 
 
     def initialize(self):
-        super(logging_UserCrash, self).initialize()
+        user_crash_test.UserCrashTest.initialize(self)
 
         # If the device has a GUI, return the device to the sign-in screen, as
         # some tests will fail inside a user session.