ShellBanner
System:Linux MiraNet 3.0.0-14-generic-pae #23-Ubuntu SMP Mon Nov 21 22:07:10 UTC 2011 i686
Software:Apache. PHP/5.3.6-13ubuntu3.10
ID:uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
Safe Mode:OFF
Open_Basedir:OFF
Freespace:30.77 GB of 70.42 GB (43.7%)
MySQL: ON MSSQL: OFF Oracle: OFF PostgreSQL: OFF Curl: OFF Sockets: ON Fetch: OFF Wget: ON Perl: ON
Disabled Functions: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,

/ usr/ share/ pyshared/ apport/ - drwxr-xr-x

Directory:
Viewing file:     report.py (97.29 KB)      -rw-r--r--
Select action/file-type:
(+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
'''Representation of and data collection for a problem report.'''

# Copyright (C) 2006 - 2009 Canonical Ltd.
# Author: Martin Pitt <martin.pitt@ubuntu.com>

# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
# the full text of the license.

import subprocess, tempfile, os.path, urllib, re, pwd, grp, os, sys
import fnmatch, glob, atexit, traceback, errno

import xml.dom, xml.dom.minidom
from xml.parsers.expat import ExpatError

import problem_report
import apport
import apport.fileutils
from apport.packaging_impl import impl as packaging

_data_dir = os.environ.get('APPORT_DATA_DIR','/usr/share/apport')
_hook_dir = '%s/package-hooks/' % (_data_dir)
_common_hook_dir = '%s/general-hooks/' % (_data_dir)

# path of the ignore file
_ignore_file = '~/.apport-ignore.xml'

# system-wide blacklist
_blacklist_dir = '/etc/apport/blacklist.d'

# programs that we consider interpreters
interpreters = ['sh', 'bash', 'dash', 'csh', 'tcsh', 'python*',
    'ruby*', 'php', 'perl*', 'mono*', 'awk']

#
# helper functions
#

def _transitive_dependencies(package, depends_set):
    '''Recursively add dependencies of package to depends_set.'''

    try:
        cur_ver = packaging.get_version(package)
    except ValueError:
        return
    for d in packaging.get_dependencies(package):
        if not d in depends_set:
            depends_set.add(d)
            _transitive_dependencies(d, depends_set)

def _read_file(path):
    '''Read file content.
    
    Return its content, or return a textual error if it failed.
    '''
    try:
        return open(path).read().strip()
    except (OSError, IOError) as e:
        return 'Error: ' + str(e)

def _read_maps(pid):
    '''Read /proc/pid/maps.

    Since /proc/$pid/maps may become unreadable unless we are ptracing the
    process, detect this, and attempt to attach/detach.
    '''
    maps = 'Error: unable to read /proc maps file'
    try:
        maps = open('/proc/%d/maps' % pid).read().strip()
    except (OSError,IOError) as e:
        return 'Error: ' + str(e)
    return maps

def _command_output(command, input = None, stderr = subprocess.STDOUT):
    '''Run command and capture its output.

    Try to execute given command (argv list) and return its stdout, or return
    a textual error if it failed.
    '''
    sp = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=stderr, close_fds=True)

    (out, err) = sp.communicate(input)
    if sp.returncode == 0:
        return out
    else:
       raise OSError('Error: command %s failed with exit code %i: %s' % (
           str(command), sp.returncode, err))

def _check_bug_pattern(report, pattern):
    '''Check if given report matches the given bug pattern XML DOM node.
    
    Return the bug URL on match, otherwise None.
    '''
    if not pattern.attributes.has_key('url'):
        return None

    for c in pattern.childNodes:
        # regular expression condition
        if c.nodeType == xml.dom.Node.ELEMENT_NODE and c.nodeName == 're' and \
            c.attributes.has_key('key'):
            key = c.attributes['key'].nodeValue
            if not report.has_key(key):
                return None
            c.normalize()
            if c.hasChildNodes() and \
                c.childNodes[0].nodeType == xml.dom.Node.TEXT_NODE:
                regexp = c.childNodes[0].nodeValue.encode('UTF-8')
                try:
                    v = report[key]
                    if isinstance(v, problem_report.CompressedValue):
                        v = v.get_value()
                    if not re.search(regexp, v):
                        return None
                except:
                    return None

    return pattern.attributes['url'].nodeValue.encode('UTF-8')

def _check_bug_patterns(report, patterns):
    try:
        dom = xml.dom.minidom.parseString(patterns)
    except ExpatError:
        return None

    for pattern in dom.getElementsByTagName('pattern'):
        url = _check_bug_pattern(report, pattern)
        if url:
            return url

    return None

def _dom_remove_space(node):
    '''Recursively remove whitespace from given XML DOM node.'''

    for c in node.childNodes:
        if c.nodeType == xml.dom.Node.TEXT_NODE and c.nodeValue.strip() == '':
            c.unlink()
            node.removeChild(c)
        else:
            _dom_remove_space(c)

#
# Report class
#

class Report(problem_report.ProblemReport):
    '''A problem report specific to apport (crash or bug).

    This class wraps a standard ProblemReport and adds methods for collecting
    standard debugging data.'''

    def __init__(self, type='Crash', date=None):
        '''Initialize a fresh problem report.

        date is the desired date/time string; if None (default), the current
        local time is used.

        If the report is attached to a process ID, this should be set in
        self.pid, so that e. g. hooks can use it to collect additional data.
        '''
        problem_report.ProblemReport.__init__(self, type, date)
        self.pid = None

    def _pkg_modified_suffix(self, package):
        '''Return a string suitable for appending to Package/Dependencies.

        If package has only unmodified files, return the empty string. If not,
        return ' [modified: ...]' with a list of modified files.
        '''
        mod = packaging.get_modified_files(package)
        if mod:
            return ' [modified: %s]' % ' '.join(mod)
        else:
            return ''

    def add_package_info(self, package = None):
        '''Add packaging information.

        If package is not given, the report must have ExecutablePath.
        This adds:
        - Package: package name and installed version
        - SourcePackage: source package name
        - PackageArchitecture: processor architecture this package was built
          for
        - Dependencies: package names and versions of all dependencies and
          pre-dependencies; this also checks if the files are unmodified and
          appends a list of all modified files
        '''
        if not package:
            # the kernel does not have a executable path but a package
            if (not 'ExecutablePath' in self and
                self['ProblemType'] == 'KernelCrash'):
                package = self['Package']
            else:
                package = apport.fileutils.find_file_package(self['ExecutablePath'])
            if not package:
                return

        try:
            version = packaging.get_version(package)
        except ValueError:
            # package not installed
            version = None
        self['Package'] = '%s %s%s' % (package, version or '(not installed)',
            self._pkg_modified_suffix(package))
        self['SourcePackage'] = packaging.get_source(package)
        if not version:
            return

        self['PackageArchitecture'] = packaging.get_architecture(package)

        # get set of all transitive dependencies
        dependencies = set([])
        _transitive_dependencies(package, dependencies)

        # get dependency versions
        self['Dependencies'] = ''
        for dep in sorted(dependencies):
            try:
                v = packaging.get_version(dep)
            except ValueError:
                # can happen with uninstalled alternate dependencies
                continue

            if self['Dependencies']:
                self['Dependencies'] += '\n'
            self['Dependencies'] += '%s %s%s' % (dep, v,
                self._pkg_modified_suffix(dep))

    def add_os_info(self):
        '''Add operating system information.

        This adds:
        - DistroRelease: lsb_release -sir output
        - Architecture: system architecture in distro specific notation
        - Uname: uname -srm output
        - NonfreeKernelModules: loaded kernel modules which are not free (if
            there are none, this field will not be present)
        '''
        p = subprocess.Popen(['lsb_release', '-sir'], stdout=subprocess.PIPE,
            stderr=subprocess.PIPE, close_fds=True)
        self['DistroRelease'] = p.communicate()[0].strip().replace('\n', ' ')

        u = os.uname()
        self['Uname'] = '%s %s %s' % (u[0], u[2], u[4])
        self['Architecture'] = packaging.get_system_architecture()

    def add_user_info(self):
        '''Add information about the user.

        This adds:
        - UserGroups: system groups the user is in
        '''
        user = pwd.getpwuid(os.getuid()).pw_name
        groups = [name for name, p, gid, memb in grp.getgrall()
            if user in memb and gid < 1000]
        groups.sort()
        self['UserGroups'] = ' '.join(groups)

    def _check_interpreted(self):
        '''Check if process is a script.

        Use ExecutablePath, ProcStatus and ProcCmdline to determine if
        process is an interpreted script. If so, set InterpreterPath
        accordingly.
        '''
        if not self.has_key('ExecutablePath'):
            return

        exebasename = os.path.basename(self['ExecutablePath'])

        # check if we consider ExecutablePath an interpreter; we have to do
        # this, otherwise 'gedit /tmp/foo.txt' would be detected as interpreted
        # script as well
        if not filter(lambda i: fnmatch.fnmatch(exebasename, i), interpreters):
            return

        # first, determine process name
        name = None
        for l in self['ProcStatus'].splitlines():
            try:
                (k, v) = l.split('\t', 1)
            except ValueError:
                continue
            if k == 'Name:':
                name = v
                break
        if not name:
            return

        cmdargs = self['ProcCmdline'].split('\0')
        bindirs = ['/bin/', '/sbin/', '/usr/bin/', '/usr/sbin/']

        # filter out interpreter options
        while len(cmdargs) >= 2 and cmdargs[1].startswith('-'):
            del cmdargs[1]

        # catch scripts explicitly called with interpreter
        if len(cmdargs) >= 2:
            # ensure that cmdargs[1] is an absolute path
            if cmdargs[1].startswith('.') and self.has_key('ProcCwd'):
                cmdargs[1] = os.path.join(self['ProcCwd'], cmdargs[1])
            if os.access(cmdargs[1], os.R_OK):
                self['InterpreterPath'] = self['ExecutablePath']
                self['ExecutablePath'] = os.path.realpath(cmdargs[1])
                return

        # catch directly executed scripts
        if name != exebasename:
            argvexes = filter(lambda p: os.access(p, os.R_OK), [p+cmdargs[0] for p in bindirs])
            if argvexes and os.path.basename(os.path.realpath(argvexes[0])) == name:
                self['InterpreterPath'] = self['ExecutablePath']
                self['ExecutablePath'] = argvexes[0]
                return

    def add_proc_info(self, pid=None, extraenv=[]):
        '''Add /proc/pid information.

        If neither pid nor self.pid are given, it defaults to the process'
        current pid and sets self.pid.

        This adds the following fields:
        - ExecutablePath: /proc/pid/exe contents; if the crashed process is
          interpreted, this contains the script path instead
        - InterpreterPath: /proc/pid/exe contents if the crashed process is
          interpreted; otherwise this key does not exist
        - ProcEnviron: A subset of the process' environment (only some standard
          variables that do not disclose potentially sensitive information, plus
          the ones mentioned in extraenv)
        - ProcCmdline: /proc/pid/cmdline contents
        - ProcStatus: /proc/pid/status contents
        - ProcMaps: /proc/pid/maps contents
        - ProcAttrCurrent: /proc/pid/attr/current contents, if not "unconfined"
        '''
        if not pid:
            pid = self.pid or os.getpid()
        if not self.pid:
            self.pid = int(pid)
        pid = str(pid)

        try:
            self['ProcCwd'] = os.readlink('/proc/' + pid + '/cwd')
        except OSError:
            pass
        self.add_proc_environ(pid, extraenv)
        self['ProcStatus'] = _read_file('/proc/' + pid + '/status')
        self['ProcCmdline'] = _read_file('/proc/' + pid + '/cmdline').rstrip('\0')
        self['ProcMaps'] = _read_maps(int(pid))
        try:
            self['ExecutablePath'] = os.readlink('/proc/' + pid + '/exe')
        except OSError as e:
            if e.errno == errno.ENOENT:
                raise ValueError('invalid process')
            else:
                raise
        for p in ('rofs', 'rwfs', 'squashmnt', 'persistmnt'):
            if self['ExecutablePath'].startswith('/%s/' % p):
                self['ExecutablePath'] = self['ExecutablePath'][len('/%s' % p):]
                break
        assert os.path.exists(self['ExecutablePath'])

        # check if we have an interpreted program
        self._check_interpreted()

        # make ProcCmdline ASCII friendly, do shell escaping
        self['ProcCmdline'] = self['ProcCmdline'].replace('\\', '\\\\').replace(' ', '\\ ').replace('\0', ' ')

        # grab AppArmor or SELinux context
        # If no LSM is loaded, reading will return -EINVAL
        try:
            # On Linux 2.6.28+, 'current' is world readable, but read() gives
            # EPERM; Python 2.5.3+ crashes on that (LP: #314065)
            if os.getuid() == 0:
                val = open('/proc/' + pid + '/attr/current').read().strip()
                if val != 'unconfined':
                    self['ProcAttrCurrent'] = val
        except (IOError, OSError):
            pass

    def add_proc_environ(self, pid=None, extraenv=[]):
        '''Add environment information.

        If pid is not given, it defaults to the process' current pid.

        This adds the following fields:
        - ProcEnviron: A subset of the process' environment (only some standard
          variables that do not disclose potentially sensitive information, plus
          the ones mentioned in extraenv)
        '''
        safe_vars = ['SHELL', 'LANGUAGE', 'LANG', 'LC_CTYPE',
            'LC_COLLATE', 'LC_TIME', 'LC_NUMERIC', 'LC_MONETARY', 'LC_MESSAGES',
            'LC_PAPER', 'LC_NAME', 'LC_ADDRESS', 'LC_TELEPHONE', 'LC_MEASUREMENT',
            'LC_IDENTIFICATION', 'LOCPATH'] + extraenv

        if not pid:
            pid = os.getpid()
        pid = str(pid)

        self['ProcEnviron'] = ''
        env = _read_file('/proc/'+ pid + '/environ').replace('\n', '\\n')
        if env.startswith('Error:'):
            self['ProcEnviron'] = env
        else:
            for l in env.split('\0'):
                if l.split('=', 1)[0] in safe_vars:
                    if self['ProcEnviron']:
                        self['ProcEnviron'] += '\n'
                    self['ProcEnviron'] += l
                elif l.startswith('PATH='):
                    p = l.split('=', 1)[1]
                    if '/home' in p or '/tmp' in p:
                        if self['ProcEnviron']:
                            self['ProcEnviron'] += '\n'
                        self['ProcEnviron'] += 'PATH=(custom, user)'
                    elif p != '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games':
                        if self['ProcEnviron']:
                            self['ProcEnviron'] += '\n'
                        self['ProcEnviron'] += 'PATH=(custom, no user)'

    def add_kernel_crash_info(self, debugdir=None):
        '''Add information from kernel crash.

        This needs a VmCore in the Report.
        '''
        if not self.has_key('VmCore'):
            return
        unlink_core = False
        ret = False
        try:
            if hasattr(self['VmCore'], 'find'):
                (fd, core) = tempfile.mkstemp()
                os.write(fd, self['VmCore'])
                os.close(fd)
                unlink_core = True
            kver = self['Uname'].split()[1]
            command = ['crash',
                       '/usr/lib/debug/boot/vmlinux-%s' % kver,
                       core,
                       ]
            try:
                p = subprocess.Popen(command, 
                                     stdin=subprocess.PIPE,
                                     stdout=subprocess.PIPE,
                                     stderr=subprocess.STDOUT)
            except OSError:
                return False
            p.stdin.write('bt -a -f\n')
            p.stdin.write('ps\n')
            p.stdin.write('runq\n')
            p.stdin.write('quit\n')
            # FIXME: split it up nicely etc
            out = p.stdout.read()
            ret = (p.wait() == 0)
            if ret:
                self['Stacktrace'] = out
        finally:
            if unlink_core:
                os.unlink(core)
        return ret

    def add_gdb_info(self, rootdir=None):
        '''Add information from gdb.

        This requires that the report has a CoreDump and an
        ExecutablePath. This adds the following fields:
        - Registers: Output of gdb's 'info registers' command
        - Disassembly: Output of gdb's 'x/16i $pc' command
        - Stacktrace: Output of gdb's 'bt full' command
        - ThreadStacktrace: Output of gdb's 'thread apply all bt full' command
        - StacktraceTop: simplified stacktrace (topmost 5 functions) for inline
          inclusion into bug reports and easier processing
        - AssertionMessage: Value of __abort_msg, if present

        The optional rootdir can specify a root directory which has the
        executable, libraries, and debug symbols. This does not require
        chroot() or root privileges, it just instructs gdb to search for the
        files there.
        '''
        if not self.has_key('CoreDump') or not self.has_key('ExecutablePath'):
            return

        unlink_core = False
        try:
            if hasattr(self['CoreDump'], 'find'):
                (fd, core) = tempfile.mkstemp()
                unlink_core = True
                os.write(fd, self['CoreDump'])
                os.close(fd)
            elif hasattr(self['CoreDump'], 'gzipvalue'):
                (fd, core) = tempfile.mkstemp()
                unlink_core = True
                os.close(fd)
                self['CoreDump'].write(open(core, 'w'))
            else:
                core = self['CoreDump'][0]

            gdb_reports = {
                           'Registers': 'info registers',
                           'Disassembly': 'x/16i $pc',
                           'Stacktrace': 'bt full',
                           'ThreadStacktrace': 'thread apply all bt full',
                           'AssertionMessage': 'print (char*) __abort_msg',
                          }

            command = ['gdb', '--batch']
            executable = self.get('InterpreterPath', self['ExecutablePath'])
            if rootdir:
                command += ['--ex', 'set debug-file-directory %s/usr/lib/debug' % rootdir,
                            '--ex', 'set solib-absolute-prefix ' + rootdir]
                executable = rootdir + '/' + executable
            command += ['--ex', 'file ' + executable, '--ex', 'core-file ' + core]
            # limit maximum backtrace depth (to avoid looped stacks)
            command += ['--ex', 'set backtrace limit 2000']
            value_keys = []
            # append the actual commands and something that acts as a separator
            for name, cmd in gdb_reports.iteritems():
                value_keys.append(name)
                command += ['--ex', 'p -99', '--ex', cmd]

            assert os.path.exists(executable)

            # call gdb
            try:
                out = _command_output(command)
            except OSError:
                return

            # split the output into the various fields
            part_re = re.compile('^\$\d+\s*=\s*-99$', re.MULTILINE)
            parts = part_re.split(out)
            # drop the gdb startup text prior to first separator
            parts.pop(0)
            for part in parts:
                self[value_keys.pop(0)] = part.replace('\n\n', '\n.\n').strip()
        finally:
            if unlink_core:
                os.unlink(core)

        # clean up AssertionMessage
        if 'AssertionMessage' in self:
            # chop off "$n = 0x...." prefix, drop empty ones
            m = re.match('^\$\d+\s+=\s+0x[0-9a-fA-F]+\s+"(.*)"\s*$',
                self['AssertionMessage'])
            if m:
                self['AssertionMessage'] = m.group(1)
                if self['AssertionMessage'].endswith('\\n'):
                    self['AssertionMessage'] = self['AssertionMessage'][0:-2]
            else:
                del self['AssertionMessage']

        if self.has_key('Stacktrace'):
            self._gen_stacktrace_top()

    def _gen_stacktrace_top(self):
        '''Build field StacktraceTop as the top five functions of Stacktrace. 

        Signal handler invocations and related functions are skipped since they
        are generally not useful for triaging and duplicate detection.
        '''
        unwind_functions = set(['g_logv', 'g_log', 'IA__g_log', 'IA__g_logv',
            'g_assert_warning', 'IA__g_assert_warning', '__GI_abort'])
        toptrace = [''] * 5
        depth = 0
        unwound = False
        unwinding = False
        bt_fn_re = re.compile('^#(\d+)\s+(?:0x(?:\w+)\s+in\s+\*?(.*)|(<signal handler called>)\s*)$')
        bt_fn_noaddr_re = re.compile('^#(\d+)\s+(?:(.*)|(<signal handler called>)\s*)$')

        for line in self['Stacktrace'].splitlines():
            m = bt_fn_re.match(line)
            if not m:
                m = bt_fn_noaddr_re.match(line)
                if not m:
                    continue

            if not unwound or unwinding:
                if m.group(2):
                    fn = m.group(2).split()[0].split('(')[0]
                else:
                    fn = None
                if m.group(3) or fn in unwind_functions:
                    unwinding = True
                    depth = 0
                    toptrace = [''] * 5
                    unwound = True
                    continue
                else:
                    unwinding = False

            if depth < len(toptrace):
                toptrace[depth] = m.group(2) or m.group(3)
                depth += 1
        self['StacktraceTop'] = '\n'.join(toptrace).strip()

    def add_hooks_info(self, ui, package=None, srcpackage=None):
        '''Run hook script for collecting package specific data.

        A hook script needs to be in _hook_dir/<Package>.py or in
        _common_hook_dir/*.py and has to contain a function 'add_info(report,
        ui)' that takes and modifies a Report, and gets an UserInterface
        reference for interactivity.

        return True if the hook requested to stop the report filing process,
        False otherwise.
        '''
        symb = {}

        # common hooks
        for hook in glob.glob(_common_hook_dir + '/*.py'):
            try:
                execfile(hook, symb)
                try:
                    symb['add_info'](self, ui)
                except TypeError as e:
                    if e.message.startswith('add_info()'):
                        # older versions of apport did not pass UI, and hooks that
                        # do not require it don't need to take it
                        symb['add_info'](self)
                    else:
                        raise
            except StopIteration:
                return True
            except:
                apport.error('hook %s crashed:', hook)
                traceback.print_exc()
                pass

        # binary package hook
        if not package:
            package = self.get('Package')
        if package:
            hook = '%s/%s.py' % (_hook_dir, package.split()[0])
            if os.path.exists(hook):
                try:
                    execfile(hook, symb)
                    try:
                        symb['add_info'](self, ui)
                    except TypeError as e:
                        if e.message.startswith('add_info()'):
                            # older versions of apport did not pass UI, and hooks that
                            # do not require it don't need to take it
                            symb['add_info'](self)
                        else:
                            raise
                except StopIteration:
                    return True
                except:
                    apport.error('hook %s crashed:', hook)
                    traceback.print_exc()
                    pass

        # source package hook
        if not srcpackage:
            srcpackage = self.get('SourcePackage')
        if srcpackage:
            hook = '%s/source_%s.py' % (_hook_dir, srcpackage.split()[0])
            if os.path.exists(hook):
                try:
                    execfile(hook, symb)
                    try:
                        symb['add_info'](self, ui)
                    except TypeError as e:
                        if e.message.startswith('add_info()'):
                            # older versions of apport did not pass UI, and hooks that
                            # do not require it don't need to take it
                            symb['add_info'](self)
                        else:
                            raise
                except StopIteration:
                    return True
                except:
                    apport.error('hook %s crashed:', hook)
                    traceback.print_exc()
                    pass

        return False

    def search_bug_patterns(self, url):
        '''Check bug patterns loaded from the specified url. 
        
        Return bug URL on match, or None otherwise.

        The url must refer to a valid XML document with the following syntax:
        root element := <patterns>
        patterns := <pattern url="http://bug.url"> *
        pattern := <re key="report_key">regular expression*</re> +

        For example:
        <?xml version="1.0"?>
        <patterns>
            <pattern url="http://bugtracker.net/bugs/1">
                <re key="Foo">ba.*r</re>
            </pattern>
            <pattern url="http://bugtracker.net/bugs/2">
                <re key="Package">^\S* 1-2$</re> <!-- test for a particular version -->
                <re key="Foo">write_(hello|goodbye)</re>
            </pattern>
        </patterns>
        '''
        # some distros might not want to support these
        if not url:
            return

        try:
            patterns = urllib.urlopen(url).read()
        except:
            # doesn't exist or failed to load
            return

        if '<title>404 Not Found' in patterns:
            return

        url = _check_bug_patterns(self, patterns)
        if url:
            return url

        return None

    def _get_ignore_dom(self):
        '''Read ignore list XML file and return a DOM tree. 
        
        Return an empty DOM tree if file does not exist.

        Raises ValueError if the file exists but is invalid XML.
        '''
        ifpath = os.path.expanduser(_ignore_file)
        if not os.access(ifpath, os.R_OK) or os.path.getsize(ifpath) == 0:
            # create a document from scratch
            dom = xml.dom.getDOMImplementation().createDocument(None, 'apport', None)
        else:
            try:
                dom = xml.dom.minidom.parse(ifpath)
            except ExpatError as e:
                raise ValueError('%s has invalid format: %s' % (_ignore_file, str(e)))

        # remove whitespace so that writing back the XML does not accumulate
        # whitespace
        dom.documentElement.normalize()
        _dom_remove_space(dom.documentElement)

        return dom

    def check_ignored(self):
        '''Check if current report should not be presented.

        Reports can be suppressed by per-user blacklisting in
        ~/.apport-ignore.xml (in the real UID's home) and
        /etc/apport/blacklist.d/.

        This requires the ExecutablePath attribute. Throws a ValueError if the
        file has an invalid format.
        '''
        assert self.has_key('ExecutablePath')

        # check blacklist
        try:
            for f in os.listdir(_blacklist_dir):
                try:
                    fd = open(os.path.join(_blacklist_dir, f))
                except IOError:
                    continue
                for line in fd:
                    if line.strip() == self['ExecutablePath']:
                        return True
        except OSError:
            pass

        dom = self._get_ignore_dom()

        try:
            cur_mtime = int(os.stat(self['ExecutablePath']).st_mtime)
        except OSError:
            # if it does not exist any more, do nothing
            return False

        # search for existing entry and update it
        for ignore in dom.getElementsByTagName('ignore'):
            if ignore.getAttribute('program') == self['ExecutablePath']:
                if float(ignore.getAttribute('mtime')) >= cur_mtime:
                    return True

        return False

    def mark_ignore(self):
        '''Ignore future crashes of this executable.

        Add a ignore list entry for this report to ~/.apport-ignore.xml, so
        that future reports for this ExecutablePath are not presented to the
        user any more.

        Throws a ValueError if the file already exists and has an invalid
        format.
        '''
        assert self.has_key('ExecutablePath')

        dom = self._get_ignore_dom()
        mtime = str(int(os.stat(self['ExecutablePath']).st_mtime))

        # search for existing entry and update it
        for ignore in dom.getElementsByTagName('ignore'):
            if ignore.getAttribute('program') == self['ExecutablePath']:
                ignore.setAttribute('mtime', mtime)
                break
        else:
            # none exists yet, create new ignore node if none exists yet
            e = dom.createElement('ignore')
            e.setAttribute('program', self['ExecutablePath'])
            e.setAttribute('mtime', mtime)
            dom.documentElement.appendChild(e)

        # write back file
        dom.writexml(open(os.path.expanduser(_ignore_file), 'w'),
            addindent='  ', newl='\n')

        dom.unlink()

    def has_useful_stacktrace(self):
        '''Check whether StackTrace can be considered 'useful'.

        The current heuristic is to consider it useless if it either is shorter
        than three lines and has any unknown function, or for longer traces, a
        minority of known functions.
        '''
        if not self.get('StacktraceTop'):
            return False
        
        unknown_fn = [f.startswith('??') for f in self['StacktraceTop'].splitlines()]

        if len(unknown_fn) < 3:
            return unknown_fn.count(True) == 0

        return unknown_fn.count(True) <= len(unknown_fn)/2.

    def standard_title(self):
        '''Create an appropriate title for a crash database entry.

        This contains the topmost function name from the stack trace and the
        signal (for signal crashes) or the Python exception (for unhandled
        Python exceptions).

        Return None if the report is not a crash or a default title could not
        be generated.
        '''
        # assertion failure
        if self.get('Signal') == '6' and \
                self.has_key('ExecutablePath') and \
                self.has_key('AssertionMessage'):
            return '%s assert failure: %s' % (
                os.path.basename(self['ExecutablePath']),
                self['AssertionMessage'])

        # signal crash
        if self.has_key('Signal') and \
            self.has_key('ExecutablePath') and \
            self.has_key('StacktraceTop'):

            signal_names = {
                '4': 'SIGILL',
                '6': 'SIGABRT',
                '8': 'SIGFPE',
                '11': 'SIGSEGV',
                '13': 'SIGPIPE'
            }

            fn = ''
            for l in self['StacktraceTop'].splitlines():
                fname = l.split('(')[0].strip()
                if fname != '??':
                    fn = ' in %s()' % fname
                    break

            arch_mismatch = ''
            if self.has_key('Architecture') and \
                self.has_key('PackageArchitecture') and \
                self['Architecture'] != self['PackageArchitecture'] and \
                self['PackageArchitecture'] != 'all':
                arch_mismatch = ' [non-native %s package]' % self['PackageArchitecture']

            return '%s crashed with %s%s%s' % (
                os.path.basename(self['ExecutablePath']),
                signal_names.get(self.get('Signal'),
                    'signal ' + self.get('Signal')),
                fn, arch_mismatch
            )

        # Python exception
        if self.has_key('Traceback') and \
            self.has_key('ExecutablePath'):

            trace = self['Traceback'].splitlines()

            if len(trace) < 1:
                return None
            if len(trace) < 3:
                return '%s crashed with %s' % (
                    os.path.basename(self['ExecutablePath']),
                    trace[0])

            trace_re = re.compile('^\s*File\s*"(\S+)".* in (.+)$')
            i = len(trace)-1
            function = 'unknown'
            while i >= 0:
                m = trace_re.match(trace[i])
                if m:
                    module_path = m.group(1)
                    function = m.group(2)
                    break
                i -= 1

            path = os.path.basename(self['ExecutablePath'])
            last_line = trace[-1]
            exception = last_line.split(':')[0]
            m = re.match('^%s: (.+)$' % re.escape(exception), last_line)
            if m:
                message = m.group(1)
            else:
                message = None

            if function == '<module>':
                if module_path == self['ExecutablePath']:
                    context = '__main__'
                else:
                    # Maybe use os.path.basename?
                    context = module_path
            else:
                context = '%s()' % function

            title = '%s crashed with %s in %s' % (
                path,
                exception,
                context
            )

            if message:
                title += ': %s' % message

            return title

        # package problem
        if self.get('ProblemType') == 'Package' and \
            self.has_key('Package'):

            title = 'package %s failed to install/upgrade' % \
                self['Package']
            if self.get('ErrorMessage'):
                title += ': ' + self['ErrorMessage'].splitlines()[-1]

            return title

        if self.get('ProblemType') == 'KernelOops' and \
            self.has_key('OopsText'):

            oops = self['OopsText']
            if oops.startswith('------------[ cut here ]------------'):
                title = oops.split('\n', 2)[1]
            else:
                title = oops.split('\n', 1)[0]

            return title

        if self.get('ProblemType') == 'KernelOops' and \
            self.has_key('Failure'):
            
            # Title the report with suspend or hibernate as appropriate,
            # and mention any non-free modules loaded up front.
            title = ''
            if 'MachineType' in self:
                title += '[' + self['MachineType'] + '] '
            title += self['Failure'] + ' failure'
            if 'NonfreeKernelModules' in self:
                title += ' [non-free: ' + self['NonfreeKernelModules'] + ']'
            title += '\n'

            return title


        return None

    def obsolete_packages(self):
        '''Return list of obsolete packages in Package and Dependencies.'''

        obsolete = []
        for l in (self.get('Package', '') + '\n' + self.get('Dependencies', '')).splitlines():
            if not l:
                continue
            pkg, ver = l.split()[:2]
            avail = packaging.get_available_version(pkg)
            if ver != None and ver != 'None' and avail != None and \
                packaging.compare_versions(ver, avail) < 0:
                obsolete.append(pkg)
        return obsolete

    def crash_signature(self):
        '''Get a signature string for a crash.
        
        This is suitable for identifying duplicates.

        For signal crashes this the concatenation of ExecutablePath, Signal
        number, and StacktraceTop function names, separated by a colon. If
        StacktraceTop has unknown functions or the report lacks any of those
        fields, return None.

        For assertion failures, it is the concatenation of ExecutablePath
        and assertion message, separated by colons.
        
        For Python crashes, this concatenates the ExecutablePath, exception
        name, and Traceback function names, again separated by a colon.
        '''
        if (not self.has_key('ExecutablePath') and 
            not self['ProblemType'] == 'KernelCrash'):
            return None

        # kernel crash
        if self.has_key('Stacktrace') and self['ProblemType'] == 'KernelCrash':
            sig = 'kernel'
            regex = re.compile ('^\s*\#\d+\s\[\w+\]\s(\w+)')
            for line in self['Stacktrace'].splitlines():
                m = regex.match(line)
                if m:
                    sig += ':' + (m.group(1))
            return sig

        # assertion failures
        if self.get('Signal') == '6' and self.has_key('AssertionMessage'):
            sig = self['ExecutablePath'] + ':' + self['AssertionMessage']
            # filter out addresses, to help match duplicates more sanely
            return re.sub(r'0x[0-9a-f]{6,}','ADDR', sig)

        # signal crashes
        if self.has_key('StacktraceTop') and self.has_key('Signal'):
            sig = '%s:%s' % (self['ExecutablePath'], self['Signal'])
            bt_fn_re = re.compile('^(?:([\w:~]+).*|(<signal handler called>)\s*)$')

            lines = self['StacktraceTop'].splitlines()
            if len(lines) < 2:
                return None

            for line in lines:
                m = bt_fn_re.match(line)
                if m:
                    sig += ':' + (m.group(1) or m.group(2))
                else:
                    # this will also catch ??
                    return None
            return sig

        # Python crashes
        if self.has_key('Traceback'):
            trace = self['Traceback'].splitlines()

            sig = ''
            if len(trace) == 1:
                # sometimes, Python exceptions do not have file references
                m = re.match('(\w+): ', trace[0])
                if m:
                    return self['ExecutablePath'] + ':' + m.group(1)
                else:
                    return None
            elif len(trace) < 3:
                return None

            for l in trace:
                if l.startswith('  File'):
                    sig += ':' + l.split()[-1]

            return self['ExecutablePath'] + ':' + trace[-1].split(':')[0] + sig

        return None

    def anonymize(self):
        '''Remove user identifying strings from the report.

        This particularly removes the user name, host name, and IPs
        from attributes which contain data read from the environment, and
        removes the ProcCwd attribute completely.
        '''
        replacements = {}
        if (os.getuid() > 0):
            # do not replace "root"
            p = pwd.getpwuid(os.getuid())
            if len(p[0]) >= 2:
                replacements[p[0]] = 'username'
            replacements[p[5]] = '/home/username'

            for s in p[4].split(','):
                s = s.strip()
                if len(s) > 2:
                    replacements[s] = 'User Name'

        hostname = os.uname()[1]
        if len(hostname) >= 2:
            replacements[hostname] = 'hostname'

        try:
            del self['ProcCwd']
        except KeyError:
            pass

        for k in self:
            if (k.startswith('Proc') and \
                not k in ['ProcCpuinfo','ProcMaps','ProcStatus', \
                          'ProcInterrupts','ProcModules']) or \
                'Stacktrace' in k or \
                k in ['Traceback', 'PythonArgs']:
                for old, new in replacements.iteritems():
                    if hasattr(self[k], 'isspace'):
                        self[k] = self[k].replace(old, new)

#
# Unit test
#

import unittest, shutil, signal, time
try:
    from cStringIO import StringIO
except ImportError:
    from io import StringIO

class _T(unittest.TestCase):
    def test_add_package_info(self):
        '''add_package_info().'''

        # determine bash version
        bashversion = packaging.get_version('bash')

        pr = Report()
        self.assertRaises(ValueError, pr.add_package_info, 'nonexistant_package')

        pr.add_package_info('bash')
        self.assertEqual(pr['Package'], 'bash ' + bashversion.strip())
        self.assertEqual(pr['SourcePackage'], 'bash')
        self.assertTrue('libc' in pr['Dependencies'])

        # test without specifying a package, but with ExecutablePath
        pr = Report()
        self.assertRaises(KeyError, pr.add_package_info)
        pr['ExecutablePath'] = '/bin/bash'
        pr.add_package_info()
        self.assertEqual(pr['Package'], 'bash ' + bashversion.strip())
        self.assertEqual(pr['SourcePackage'], 'bash')
        self.assertTrue('libc' in pr['Dependencies'])
        # check for stray empty lines
        self.assertTrue('\n\n' not in pr['Dependencies'])
        self.assertTrue(pr.has_key('PackageArchitecture'))

        pr = Report()
        pr['ExecutablePath'] = '/nonexisting'
        pr.add_package_info()
        self.assertTrue(not pr.has_key('Package'))

    def test_add_os_info(self):
        '''add_os_info().'''

        pr = Report()
        pr.add_os_info()
        self.assertTrue(pr['Uname'].startswith('Linux'))
        self.assertTrue(type(pr['DistroRelease']) == type(''))
        self.assertTrue(pr['Architecture'])

    def test_add_user_info(self):
        '''add_user_info().'''

        pr = Report()
        pr.add_user_info()
        self.assertTrue(pr.has_key('UserGroups'))

        # double-check that user group names are removed
        for g in pr['UserGroups'].split():
            self.assertTrue(grp.getgrnam(g).gr_gid < 1000)
        self.assertTrue(grp.getgrgid(os.getgid()).gr_name not in pr['UserGroups'])

    def test_add_proc_info(self):
        '''add_proc_info().'''

        # set test environment
        assert os.environ.has_key('LANG'), 'please set $LANG for this test'
        assert os.environ.has_key('USER'), 'please set $USER for this test'
        assert os.environ.has_key('PWD'), '$PWD is not set'

        # check without additional safe environment variables
        pr = Report()
        self.assertEqual(pr.pid, None)
        pr.add_proc_info()
        self.assertEqual(pr.pid, os.getpid())
        self.assertTrue(set(['ProcEnviron', 'ProcMaps', 'ProcCmdline',
            'ProcMaps']).issubset(set(pr.keys())), 'report has required fields')
        self.assertTrue('LANG='+os.environ['LANG'] in pr['ProcEnviron'])
        self.assertTrue('USER' not in pr['ProcEnviron'])
        self.assertTrue('PWD' not in pr['ProcEnviron'])

        # check with one additional safe environment variable
        pr = Report()
        pr.add_proc_info(extraenv=['PWD'])
        self.assertTrue('USER' not in pr['ProcEnviron'])
        self.assertTrue('PWD='+os.environ['PWD'] in pr['ProcEnviron'])

        # check process from other user
        assert os.getuid() != 0, 'please do not run this test as root for this check.'
        pr = Report()
        self.assertRaises(OSError, pr.add_proc_info, 1) # EPERM for init process
        self.assertEqual(pr.pid, 1)
        self.assertTrue('init' in pr['ProcStatus'], pr['ProcStatus'])
        self.assertTrue(pr['ProcEnviron'].startswith('Error:'), pr['ProcEnviron'])
        self.assertTrue(not pr.has_key('InterpreterPath'))

        # check escaping of ProcCmdline
        p = subprocess.Popen(['cat', '/foo bar', '\\h', '\\ \\', '-'],
            stdin=subprocess.PIPE, stdout=subprocess.PIPE,
            stderr=subprocess.PIPE, close_fds=True)
        assert p.pid
        # wait until /proc/pid/cmdline exists
        while not open('/proc/%i/cmdline' % p.pid).read():
            time.sleep(0.1)
        pr = Report()
        pr.add_proc_info(pid=p.pid)
        self.assertEqual(pr.pid, p.pid)
        p.communicate('\n')
        self.assertEqual(pr['ProcCmdline'], 'cat /foo\ bar \\\\h \\\\\\ \\\\ -')
        self.assertEqual(pr['ExecutablePath'], '/bin/cat')
        self.assertTrue(not pr.has_key('InterpreterPath'))
        self.assertTrue('/bin/cat' in pr['ProcMaps'])
        self.assertTrue('[stack]' in pr['ProcMaps'])

        # check correct handling of executable symlinks
        assert os.path.islink('/bin/sh'), '/bin/sh needs to be a symlink for this test'
        p = subprocess.Popen(['sh'], stdin=subprocess.PIPE,
            close_fds=True)
        assert p.pid
        # wait until /proc/pid/cmdline exists
        while not open('/proc/%i/cmdline' % p.pid).read():
            time.sleep(0.1)
        pr = Report()
        pr.pid = p.pid
        pr.add_proc_info()
        p.communicate('exit\n')
        self.assertFalse(pr.has_key('InterpreterPath'), pr.get('InterpreterPath'))
        self.assertEqual(pr['ExecutablePath'], os.path.realpath('/bin/sh'))

        # check correct handling of interpreted executables: shell
        p = subprocess.Popen(['zgrep', 'foo'], stdin=subprocess.PIPE,
            close_fds=True)
        assert p.pid
        # wait until /proc/pid/cmdline exists
        while not open('/proc/%i/cmdline' % p.pid).read():
            time.sleep(0.1)
        pr = Report()
        pr.add_proc_info(pid=p.pid)
        p.communicate('\n')
        self.assertTrue(pr['ExecutablePath'].endswith('bin/zgrep'))
        self.assertEqual(pr['InterpreterPath'],
            os.path.realpath(open(pr['ExecutablePath']).readline().strip()[2:]))
        self.assertTrue('[stack]' in pr['ProcMaps'])

        # check correct handling of interpreted executables: python
        (fd, testscript) = tempfile.mkstemp()
        os.write(fd, '''#!/usr/bin/python
import sys
sys.stdin.readline()
''')
        os.close(fd)
        os.chmod(testscript, 0o755)
        p = subprocess.Popen([testscript], stdin=subprocess.PIPE,
            stderr=subprocess.PIPE, close_fds=True)
        assert p.pid
        # wait until /proc/pid/cmdline exists
        while not open('/proc/%i/cmdline' % p.pid).read():
            time.sleep(0.1)
        pr = Report()
        pr.add_proc_info(pid=p.pid)
        p.communicate('\n')
        os.unlink(testscript)
        self.assertEqual(pr['ExecutablePath'], testscript)
        self.assertTrue('python' in pr['InterpreterPath'])
        self.assertTrue('python' in pr['ProcMaps'])
        self.assertTrue('[stack]' in pr['ProcMaps'])

        # test process is gone, should complain about nonexisting PID
        self.assertRaises(ValueError, pr.add_proc_info, p.pid)

    def test_add_path_classification(self):
        '''classification of $PATH.'''

        # system default
        p = subprocess.Popen(['cat'], stdin=subprocess.PIPE, 
            env={'PATH': '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games'})
        time.sleep(0.1)
        r = Report()
        r.add_proc_environ(pid=p.pid)
        p.communicate('')
        self.assertFalse('PATH' in r['ProcEnviron'], 
            'system default $PATH should be filtered out')

        # no user paths
        p = subprocess.Popen(['cat'], stdin=subprocess.PIPE, 
            env={'PATH': '/usr/sbin:/usr/bin:/sbin:/bin'})
        time.sleep(0.1)
        r = Report()
        r.add_proc_environ(pid=p.pid)
        p.communicate('')
        self.assertTrue('PATH=(custom, no user)' in r['ProcEnviron'], 
            'PATH is customized without user paths')

        # user paths
        p = subprocess.Popen(['cat'], stdin=subprocess.PIPE, 
            env={'PATH': '/home/pitti:/usr/sbin:/usr/bin:/sbin:/bin'})
        time.sleep(0.1)
        r = Report()
        r.add_proc_environ(pid=p.pid)
        p.communicate('')
        self.assertTrue('PATH=(custom, user)' in r['ProcEnviron'], 
            'PATH is customized with user paths')

    def test_check_interpreted(self):
        '''_check_interpreted().'''

        # standard ELF binary
        f = tempfile.NamedTemporaryFile()
        pr = Report()
        pr['ExecutablePath'] = '/usr/bin/gedit'
        pr['ProcStatus'] = 'Name:\tgedit'
        pr['ProcCmdline'] = 'gedit\0/' + f.name
        pr._check_interpreted()
        self.assertEqual(pr['ExecutablePath'], '/usr/bin/gedit')
        self.assertFalse(pr.has_key('InterpreterPath'))
        f.close()

        # bogus argv[0]
        pr = Report()
        pr['ExecutablePath'] = '/bin/dash'
        pr['ProcStatus'] = 'Name:\tznonexisting'
        pr['ProcCmdline'] = 'nonexisting\0/foo'
        pr._check_interpreted()
        self.assertEqual(pr['ExecutablePath'], '/bin/dash')
        self.assertFalse(pr.has_key('InterpreterPath'))

        # standard sh script
        pr = Report()
        pr['ExecutablePath'] = '/bin/dash'
        pr['ProcStatus'] = 'Name:\tzgrep'
        pr['ProcCmdline'] = '/bin/sh\0/bin/zgrep\0foo'
        pr._check_interpreted()
        self.assertEqual(pr['ExecutablePath'], '/bin/zgrep')
        self.assertEqual(pr['InterpreterPath'], '/bin/dash')

        # standard sh script when being called explicitly with interpreter
        pr = Report()
        pr['ExecutablePath'] = '/bin/dash'
        pr['ProcStatus'] = 'Name:\tdash'
        pr['ProcCmdline'] = '/bin/sh\0/bin/zgrep\0foo'
        pr._check_interpreted()
        self.assertEqual(pr['ExecutablePath'], '/bin/zgrep')
        self.assertEqual(pr['InterpreterPath'], '/bin/dash')

        # special case mono scheme: beagled-helper (use zgrep to make the test
        # suite work if mono or beagle are not installed)
        pr = Report()
        pr['ExecutablePath'] = '/usr/bin/mono'
        pr['ProcStatus'] = 'Name:\tzgrep'
        pr['ProcCmdline'] = 'zgrep\0--debug\0/bin/zgrep'
        pr._check_interpreted()
        self.assertEqual(pr['ExecutablePath'], '/bin/zgrep')
        self.assertEqual(pr['InterpreterPath'], '/usr/bin/mono')

        # special case mono scheme: banshee (use zgrep to make the test
        # suite work if mono or beagle are not installed)
        pr = Report()
        pr['ExecutablePath'] = '/usr/bin/mono'
        pr['ProcStatus'] = 'Name:\tzgrep'
        pr['ProcCmdline'] = 'zgrep\0/bin/zgrep'
        pr._check_interpreted()
        self.assertEqual(pr['ExecutablePath'], '/bin/zgrep')
        self.assertEqual(pr['InterpreterPath'], '/usr/bin/mono')

        # fail on files we shouldn't have access to when name!=argv[0]
        pr = Report()
        pr['ExecutablePath'] = '/usr/bin/python'
        pr['ProcStatus'] = 'Name:\tznonexisting'
        pr['ProcCmdline'] = 'python\0/etc/shadow'
        pr._check_interpreted()
        self.assertEqual(pr['ExecutablePath'], '/usr/bin/python')
        self.assertFalse(pr.has_key('InterpreterPath'))

        # succeed on files we should have access to when name!=argv[0]
        pr = Report()
        pr['ExecutablePath'] = '/usr/bin/python'
        pr['ProcStatus'] = 'Name:\tznonexisting'
        pr['ProcCmdline'] = 'python\0/etc/passwd'
        pr._check_interpreted()
        self.assertEqual(pr['InterpreterPath'], '/usr/bin/python')
        self.assertEqual(pr['ExecutablePath'], '/etc/passwd')

        # fail on files we shouldn't have access to when name==argv[0]
        pr = Report()
        pr['ExecutablePath'] = '/usr/bin/python'
        pr['ProcStatus'] = 'Name:\tshadow'
        pr['ProcCmdline'] = '../etc/shadow'
        pr._check_interpreted()
        self.assertEqual(pr['ExecutablePath'], '/usr/bin/python')
        self.assertFalse(pr.has_key('InterpreterPath'))

        # succeed on files we should have access to when name==argv[0]
        pr = Report()
        pr['ExecutablePath'] = '/usr/bin/python'
        pr['ProcStatus'] = 'Name:\tpasswd'
        pr['ProcCmdline'] = '../etc/passwd'
        pr._check_interpreted()
        self.assertEqual(pr['InterpreterPath'], '/usr/bin/python')
        self.assertEqual(pr['ExecutablePath'], '/bin/../etc/passwd')

        # interactive python process
        pr = Report()
        pr['ExecutablePath'] = '/usr/bin/python'
        pr['ProcStatus'] = 'Name:\tpython'
        pr['ProcCmdline'] = 'python'
        pr._check_interpreted()
        self.assertEqual(pr['ExecutablePath'], '/usr/bin/python')
        self.assertFalse(pr.has_key('InterpreterPath'))

        # python script (abuse /bin/bash since it must exist)
        pr = Report()
        pr['ExecutablePath'] = '/usr/bin/python'
        pr['ProcStatus'] = 'Name:\tbash'
        pr['ProcCmdline'] = 'python\0/bin/bash'
        pr._check_interpreted()
        self.assertEqual(pr['InterpreterPath'], '/usr/bin/python')
        self.assertEqual(pr['ExecutablePath'], '/bin/bash')

        # python script with options (abuse /bin/bash since it must exist)
        pr = Report()
        pr['ExecutablePath'] = '/usr/bin/python'
        pr['ProcStatus'] = 'Name:\tbash'
        pr['ProcCmdline'] = 'python\0-OO\0/bin/bash'
        pr._check_interpreted()
        self.assertEqual(pr['InterpreterPath'], '/usr/bin/python')
        self.assertEqual(pr['ExecutablePath'], '/bin/bash')

        # python script with a versioned interpreter
        pr = Report()
        pr['ExecutablePath'] = '/usr/bin/python2.7'
        pr['ProcStatus'] = 'Name:\tbash'
        pr['ProcCmdline'] = '/usr/bin/python\0/bin/bash'
        pr._check_interpreted()
        self.assertEqual(pr['InterpreterPath'], '/usr/bin/python2.7')
        self.assertEqual(pr['ExecutablePath'], '/bin/bash')

    @classmethod
    def _generate_sigsegv_report(klass, file=None, signal='11', code='''
int f(x) {
    int* p = 0; *p = x;
    return x+1;
}
int main() { return f(42); }
'''):
        '''Create a test executable which will die with a SIGSEGV, generate a
        core dump for it, create a problem report with those two arguments
        (ExecutablePath and CoreDump) and call add_gdb_info().

        If file is given, the report is written into it. Return the Report.'''

        workdir = None
        orig_cwd = os.getcwd()
        pr = Report()
        try:
            workdir = tempfile.mkdtemp()
            atexit.register(shutil.rmtree, workdir)
            os.chdir(workdir)

            # create a test executable
            open('crash.c', 'w').write(code)
            assert subprocess.call(['gcc', '-g', 'crash.c', '-o', 'crash']) == 0
            assert os.path.exists('crash')

            # call it through gdb and dump core
            subprocess.call(['gdb', '--batch', '--ex', 'run', '--ex',
                'generate-core-file core', './crash'], stdout=subprocess.PIPE)
            assert os.path.exists('core')
            assert subprocess.call(['readelf', '-n', 'core'],
                stdout=subprocess.PIPE) == 0

            pr['ExecutablePath'] = os.path.join(workdir, 'crash')
            pr['CoreDump'] = (os.path.join(workdir, 'core'),)
            pr['Signal'] = signal

            pr.add_gdb_info()
            if file:
                pr.write(file)
                file.flush()
        finally:
            os.chdir(orig_cwd)

        return pr

    def _validate_gdb_fields(self,pr):
        self.assertTrue(pr.has_key('Stacktrace'))
        self.assertTrue(pr.has_key('ThreadStacktrace'))
        self.assertTrue(pr.has_key('StacktraceTop'))
        self.assertTrue(pr.has_key('Registers'))
        self.assertTrue(pr.has_key('Disassembly'))
        self.assertTrue('(no debugging symbols found)' not in pr['Stacktrace'])
        self.assertTrue('Core was generated by' not in pr['Stacktrace'], pr['Stacktrace'])
        self.assertTrue(not re.match(r'(?s)(^|.*\n)#0  [^\n]+\n#0  ',
                                  pr['Stacktrace']))
        self.assertTrue('#0  0x' in pr['Stacktrace'])
        self.assertTrue('#1  0x' in pr['Stacktrace'])
        self.assertTrue('#0  0x' in pr['ThreadStacktrace'])
        self.assertTrue('#1  0x' in pr['ThreadStacktrace'])
        self.assertTrue('Thread 1 (' in pr['ThreadStacktrace'])
        self.assertTrue(len(pr['StacktraceTop'].splitlines()) <= 5)

    def test_add_gdb_info(self):
        '''add_gdb_info() with core dump file reference.'''

        pr = Report()
        # should not throw an exception for missing fields
        pr.add_gdb_info()

        # normal crash
        pr = self._generate_sigsegv_report()
        self._validate_gdb_fields(pr)
        self.assertEqual(pr['StacktraceTop'], 'f (x=42) at crash.c:3\nmain () at crash.c:6', pr['StacktraceTop'])
        self.assertFalse ('AssertionMessage' in pr)

        # crash where gdb generates output on stderr
        pr = self._generate_sigsegv_report(code='''
int main() {
    void     (*function)(void);
    function = 0;
    function();
}
''')
        self._validate_gdb_fields(pr)
        self.assertTrue('Cannot access memory at address 0x0' in pr['Disassembly'], pr['Disassembly'])
        self.assertFalse ('AssertionMessage' in pr)

    def test_add_gdb_info_load(self):
        '''add_gdb_info() with inline core dump.'''

        rep = tempfile.NamedTemporaryFile()
        self._generate_sigsegv_report(rep)
        rep.seek(0)

        pr = Report()
        pr.load(open(rep.name))
        pr.add_gdb_info()

        self._validate_gdb_fields(pr)

    def test_add_zz_parse_segv_details(self):
        '''parse-segv produces sensible results'''
        rep = tempfile.NamedTemporaryFile()
        self._generate_sigsegv_report(rep)
        rep.seek(0)

        pr = Report()
        pr.load(open(rep.name))
        pr['Signal'] = '1'
        pr.add_hooks_info('fake_ui')
        self.assertTrue('SegvAnalysis' not in pr.keys())

        pr = Report()
        pr.load(open(rep.name))
        pr.add_hooks_info('fake_ui')
        self.assertTrue('Skipped: missing required field "Architecture"' in pr['SegvAnalysis'],
                     pr['SegvAnalysis'])

        pr.add_os_info()
        pr.add_hooks_info('fake_ui')
        self.assertTrue('Skipped: missing required field "ProcMaps"' in pr['SegvAnalysis'],
                     pr['SegvAnalysis'])

        pr.add_proc_info()
        pr.add_hooks_info('fake_ui')
        self.assertTrue('not located in a known VMA region' in pr['SegvAnalysis'],
                     pr['SegvAnalysis'])

    def test_add_gdb_info_script(self):
        '''add_gdb_info() with a script.'''

        (fd, script) = tempfile.mkstemp()
        coredump = os.path.join(os.path.dirname(script), 'core')
        assert not os.path.exists(coredump)
        try:
            os.close(fd)

            # create a test script which produces a core dump for us
            open(script, 'w').write('''#!/bin/bash
cd `dirname $0`
ulimit -c unlimited
kill -SEGV $$
''')
            os.chmod(script, 0o755)

            # call script and verify that it gives us a proper ELF core dump
            assert subprocess.call([script]) != 0
            assert subprocess.call(['readelf', '-n', coredump],
                stdout=subprocess.PIPE) == 0

            pr = Report()
            pr['InterpreterPath'] = '/bin/bash'
            pr['ExecutablePath'] = script
            pr['CoreDump'] = (coredump,)
            pr.add_gdb_info()
        finally:
            os.unlink(coredump)
            os.unlink(script)

        self._validate_gdb_fields(pr)
        self.assertTrue('libc.so' in pr['Stacktrace'] or 'in execute_command' in pr['Stacktrace'])

    def test_add_gdb_info_abort(self):
        '''add_gdb_info() with SIGABRT/assert()

        If these come from an assert(), the report should have the assertion
        message. Otherwise it should be marked as not reportable.
        '''
        # abort with assert
        (fd, script) = tempfile.mkstemp()
        assert not os.path.exists('core')
        try:
            os.close(fd)

            # create a test script which produces a core dump for us
            open(script, 'w').write('''#!/bin/sh
gcc -o $0.bin -x c - <<EOF
#include <assert.h>
int main() { assert(1 < 0); }
EOF
ulimit -c unlimited
$0.bin 2>/dev/null
''')
            os.chmod(script, 0o755)

            # call script and verify that it gives us a proper ELF core dump
            assert subprocess.call([script]) != 0
            assert subprocess.call(['readelf', '-n', 'core'],
                stdout=subprocess.PIPE) == 0

            pr = Report()
            pr['ExecutablePath'] = script + '.bin'
            pr['CoreDump'] = ('core',)
            pr.add_gdb_info()
        finally:
            os.unlink(script)
            os.unlink(script + '.bin')
            os.unlink('core')

        self._validate_gdb_fields(pr)
        self.assertTrue("<stdin>:2: main: Assertion `1 < 0' failed." in
                pr['AssertionMessage'], pr['AssertionMessage'])
        self.assertFalse(pr['AssertionMessage'].startswith('$'), pr['AssertionMessage'])
        self.assertFalse('= 0x' in pr['AssertionMessage'], pr['AssertionMessage'])
        self.assertFalse(pr['AssertionMessage'].endswith('\\n'), pr['AssertionMessage'])

        # abort with internal error
        (fd, script) = tempfile.mkstemp()
        assert not os.path.exists('core')
        try:
            os.close(fd)

            # create a test script which produces a core dump for us
            open(script, 'w').write('''#!/bin/sh
gcc -O2 -D_FORTIFY_SOURCE=2 -o $0.bin -x c - <<EOF
#include <string.h>
int main(int argc, char *argv[]) {
    char buf[8];
    strcpy(buf, argv[1]);
    return 0;
}
EOF
ulimit -c unlimited
LIBC_FATAL_STDERR_=1 $0.bin aaaaaaaaaaaaaaaa 2>/dev/null
''')
            os.chmod(script, 0o755)

            # call script and verify that it gives us a proper ELF core dump
            assert subprocess.call([script]) != 0
            assert subprocess.call(['readelf', '-n', 'core'],
                stdout=subprocess.PIPE) == 0

            pr = Report()
            pr['ExecutablePath'] = script + '.bin'
            pr['CoreDump'] = ('core',)
            pr.add_gdb_info()
        finally:
            os.unlink(script)
            os.unlink(script + '.bin')
            os.unlink('core')

        self._validate_gdb_fields(pr)
        self.assertTrue("** buffer overflow detected ***: %s.bin terminated" % (script) in
                pr['AssertionMessage'], pr['AssertionMessage'])
        self.assertFalse(pr['AssertionMessage'].startswith('$'), pr['AssertionMessage'])
        self.assertFalse('= 0x' in pr['AssertionMessage'], pr['AssertionMessage'])
        self.assertFalse(pr['AssertionMessage'].endswith('\\n'), pr['AssertionMessage'])

        # abort without assertion
        (fd, script) = tempfile.mkstemp()
        assert not os.path.exists('core')
        try:
            os.close(fd)

            # create a test script which produces a core dump for us
            open(script, 'w').write('''#!/bin/sh
gcc -o $0.bin -x c - <<EOF
#include <stdlib.h>
int main() { abort(); }
EOF
ulimit -c unlimited
$0.bin 2>/dev/null
''')
            os.chmod(script, 0o755)

            # call script and verify that it gives us a proper ELF core dump
            assert subprocess.call([script]) != 0
            assert subprocess.call(['readelf', '-n', 'core'],
                stdout=subprocess.PIPE) == 0

            pr = Report()
            pr['ExecutablePath'] = script + '.bin'
            pr['CoreDump'] = ('core',)
            pr.add_gdb_info()
        finally:
            os.unlink(script)
            os.unlink(script + '.bin')
            os.unlink('core')

        self._validate_gdb_fields(pr)
        self.assertFalse ('AssertionMessage' in pr, pr.get('AssertionMessage'))

    def test_search_bug_patterns(self):
        '''search_bug_patterns().'''

        patterns = tempfile.NamedTemporaryFile(prefix='apport-')
        # create some test patterns
        patterns.write('''<?xml version="1.0"?>
<patterns>
    <pattern url="http://bugtracker.net/bugs/1">
        <re key="Package">^bash </re>
        <re key="Foo">ba.*r</re>
    </pattern>
    <pattern url="http://bugtracker.net/bugs/2">
        <re key="Package">^bash 1-2$</re>
        <re key="Foo">write_(hello|goodbye)</re>
    </pattern>
    <pattern url="http://bugtracker.net/bugs/3">
        <re key="Package">^coreutils </re>
        <re key="Bar">^1$</re>
    </pattern>
    <pattern url="http://bugtracker.net/bugs/4">
        <re key="Package">^coreutils </re>
        <re key="Bar">*</re> <!-- invalid RE -->
    </pattern>
    <pattern url="http://bugtracker.net/bugs/5">
        <re key="SourcePackage">^bazaar$</re>
        <re key="LogFile">AssertionError</re>
    </pattern>
</patterns>''')
        patterns.flush()

        # invalid XML
        invalid = tempfile.NamedTemporaryFile(prefix='apport-')
        invalid.write('''<?xml version="1.0"?>
</patterns>''')
        invalid.flush()

        # create some reports
        r_bash = Report()
        r_bash['Package'] = 'bash 1-2'
        r_bash['Foo'] = 'bazaar'

        r_bazaar = Report()
        r_bazaar['Package'] = 'bazaar 2-1'
        r_bazaar['SourcePackage'] = 'bazaar'
        r_bazaar['LogFile'] = 'AssertionError'

        r_coreutils = Report()
        r_coreutils['Package'] = 'coreutils 1'
        r_coreutils['Bar'] = '1'

        r_invalid = Report()
        r_invalid['Package'] = 'invalid 1'

        # positive match cases
        self.assertEqual(r_bash.search_bug_patterns(patterns.name), 'http://bugtracker.net/bugs/1')
        r_bash['Foo'] = 'write_goodbye'
        self.assertEqual(r_bash.search_bug_patterns(patterns.name), 'http://bugtracker.net/bugs/2')
        self.assertEqual(r_coreutils.search_bug_patterns(patterns.name), 'http://bugtracker.net/bugs/3')
        self.assertEqual(r_bazaar.search_bug_patterns(patterns.name), 'http://bugtracker.net/bugs/5')

        # also works for CompressedValues
        r_bash_compressed = r_bash.copy()
        r_bash_compressed['Foo'] = problem_report.CompressedValue('bazaar')
        self.assertEqual(r_bash_compressed.search_bug_patterns(patterns.name), 'http://bugtracker.net/bugs/1')

        # negative match cases
        r_bash['Package'] = 'bash-static 1-2'
        self.assertEqual(r_bash.search_bug_patterns(patterns.name), None)
        r_bash['Package'] = 'bash 1-21'
        self.assertEqual(r_bash.search_bug_patterns(patterns.name), None,
            'does not match on wrong bash version')
        r_bash['Foo'] = 'zz'
        self.assertEqual(r_bash.search_bug_patterns(patterns.name), None,
            'does not match on wrong Foo value')
        r_coreutils['Bar'] = '11'
        self.assertEqual(r_coreutils.search_bug_patterns(patterns.name), None,
            'does not match on wrong Bar value')
        r_bazaar['SourcePackage'] = 'launchpad'
        self.assertEqual(r_bazaar.search_bug_patterns(patterns.name), None,
            'does not match on wrong source package')
        r_bazaar['LogFile'] = ''
        self.assertEqual(r_bazaar.search_bug_patterns(patterns.name), None,
            'does not match on empty attribute')

        # various errors to check for robustness (no exceptions, just None
        # return value)
        del r_coreutils['Bar']
        self.assertEqual(r_coreutils.search_bug_patterns(patterns.name), None,
            'does not match on nonexisting key')
        self.assertEqual(r_invalid.search_bug_patterns(invalid.name), None,
            'gracefully handles invalid XML')
        r_coreutils['Package'] = 'other 2'
        self.assertEqual(r_bash.search_bug_patterns('file:///nonexisting/directory/'), None,
            'gracefully handles nonexisting base path')
        # existing host, but no bug patterns
        self.assertEqual(r_bash.search_bug_patterns('http://security.ubuntu.com/'), None,
            'gracefully handles base path without bug patterns')
        # nonexisting host
        self.assertEqual(r_bash.search_bug_patterns('http://nonexisting.domain/'), None,
            'gracefully handles nonexisting URL domain')

    def test_add_hooks_info(self):
        '''add_hooks_info().'''

        global _hook_dir
        global _common_hook_dir
        orig_hook_dir = _hook_dir
        _hook_dir = tempfile.mkdtemp()
        orig_common_hook_dir = _common_hook_dir
        _common_hook_dir = tempfile.mkdtemp()
        try:
            open(os.path.join(_hook_dir, 'foo.py'), 'w').write('''
import sys
def add_info(report):
    report['Field1'] = 'Field 1'
    report['Field2'] = 'Field 2\\nBla'
    if 'Spethial' in report:
        raise StopIteration
''')

            open(os.path.join(_common_hook_dir, 'foo1.py'), 'w').write('''
def add_info(report):
    report['CommonField1'] = 'CommonField 1'
    if report['Package'] == 'commonspethial':
        raise StopIteration
''')
            open(os.path.join(_common_hook_dir, 'foo2.py'), 'w').write('''
def add_info(report):
    report['CommonField2'] = 'CommonField 2'
''')
            open(os.path.join(_common_hook_dir, 'foo3.py'), 'w').write('''
def add_info(report, ui):
    report['CommonField3'] = str(ui)
''')

            # should only catch .py files
            open(os.path.join(_common_hook_dir, 'notme'), 'w').write('''
def add_info(report):
    report['BadField'] = 'XXX'
''')
            r = Report()
            r['Package'] = 'bar'
            # should not throw any exceptions
            self.assertEqual(r.add_hooks_info('fake_ui'), False)
            self.assertEqual(set(r.keys()), set(['ProblemType', 'Date',
                'Package', 'CommonField1', 'CommonField2', 'CommonField3']), 
                'report has required fields')

            r = Report()
            r['Package'] = 'baz 1.2-3'
            # should not throw any exceptions
            self.assertEqual(r.add_hooks_info('fake_ui'), False)
            self.assertEqual(set(r.keys()), set(['ProblemType', 'Date',
                'Package', 'CommonField1', 'CommonField2', 'CommonField3']), 
                'report has required fields')

            r = Report()
            r['Package'] = 'foo'
            self.assertEqual(r.add_hooks_info('fake_ui'), False)
            self.assertEqual(set(r.keys()), set(['ProblemType', 'Date',
                'Package', 'Field1', 'Field2', 'CommonField1',
                'CommonField2', 'CommonField3']), 'report has required fields')
            self.assertEqual(r['Field1'], 'Field 1')
            self.assertEqual(r['Field2'], 'Field 2\nBla')
            self.assertEqual(r['CommonField1'], 'CommonField 1')
            self.assertEqual(r['CommonField2'], 'CommonField 2')
            self.assertEqual(r['CommonField3'], 'fake_ui')

            r = Report()
            r['Package'] = 'foo 4.5-6'
            self.assertEqual(r.add_hooks_info('fake_ui'), False)
            self.assertEqual(set(r.keys()), set(['ProblemType', 'Date',
                'Package', 'Field1', 'Field2', 'CommonField1',
                'CommonField2', 'CommonField3']), 'report has required fields')
            self.assertEqual(r['Field1'], 'Field 1')
            self.assertEqual(r['Field2'], 'Field 2\nBla')
            self.assertEqual(r['CommonField1'], 'CommonField 1')
            self.assertEqual(r['CommonField2'], 'CommonField 2')

            # test hook abort
            r['Spethial'] = '1'
            self.assertEqual(r.add_hooks_info('fake_ui'), True)
            r = Report()
            r['Package'] = 'commonspethial'
            self.assertEqual(r.add_hooks_info('fake_ui'), True)

            # source package hook
            open(os.path.join(_hook_dir, 'source_foo.py'), 'w').write('''
def add_info(report, ui):
    report['Field1'] = 'Field 1'
    report['Field2'] = 'Field 2\\nBla'
    if report['Package'] == 'spethial':
        raise StopIteration
''')
            r = Report()
            r['SourcePackage'] = 'foo'
            r['Package'] = 'libfoo 3'
            self.assertEqual(r.add_hooks_info('fake_ui'), False)
            self.assertEqual(set(r.keys()), set(['ProblemType', 'Date',
                'Package', 'SourcePackage', 'Field1', 'Field2', 'CommonField1',
                'CommonField2', 'CommonField3']), 'report has required fields')
            self.assertEqual(r['Field1'], 'Field 1')
            self.assertEqual(r['Field2'], 'Field 2\nBla')
            self.assertEqual(r['CommonField1'], 'CommonField 1')
            self.assertEqual(r['CommonField2'], 'CommonField 2')
            self.assertEqual(r['CommonField3'], 'fake_ui')

            # test hook abort
            r['Package'] = 'spethial'
            self.assertEqual(r.add_hooks_info('fake_ui'), True)

        finally:
            shutil.rmtree(_hook_dir)
            shutil.rmtree(_common_hook_dir)
            _hook_dir = orig_hook_dir
            _common_hook_dir = orig_common_hook_dir

    def test_ignoring(self):
        '''mark_ignore() and check_ignored().'''

        global _ignore_file
        orig_ignore_file = _ignore_file
        workdir = tempfile.mkdtemp()
        _ignore_file = os.path.join(workdir, 'ignore.xml')
        try:
            open(os.path.join(workdir, 'bash'), 'w').write('bash')
            open(os.path.join(workdir, 'crap'), 'w').write('crap')

            bash_rep = Report()
            bash_rep['ExecutablePath'] = os.path.join(workdir, 'bash')
            crap_rep = Report()
            crap_rep['ExecutablePath'] = os.path.join(workdir, 'crap')
            # must be able to deal with executables that do not exist any more
            cp_rep = Report()
            cp_rep['ExecutablePath'] = os.path.join(workdir, 'cp')

            # no ignores initially
            self.assertEqual(bash_rep.check_ignored(), False)
            self.assertEqual(crap_rep.check_ignored(), False)
            self.assertEqual(cp_rep.check_ignored(), False)

            # ignore crap now
            crap_rep.mark_ignore()
            self.assertEqual(bash_rep.check_ignored(), False)
            self.assertEqual(crap_rep.check_ignored(), True)
            self.assertEqual(cp_rep.check_ignored(), False)

            # ignore bash now
            bash_rep.mark_ignore()
            self.assertEqual(bash_rep.check_ignored(), True)
            self.assertEqual(crap_rep.check_ignored(), True)
            self.assertEqual(cp_rep.check_ignored(), False)

            # poke crap so that it has a newer timestamp
            time.sleep(1)
            open(os.path.join(workdir, 'crap'), 'w').write('crapnew')
            self.assertEqual(bash_rep.check_ignored(), True)
            self.assertEqual(crap_rep.check_ignored(), False)
            self.assertEqual(cp_rep.check_ignored(), False)

            # do not complain about an empty ignore file
            open(_ignore_file, 'w').write('')
            self.assertEqual(bash_rep.check_ignored(), False)
            self.assertEqual(crap_rep.check_ignored(), False)
            self.assertEqual(cp_rep.check_ignored(), False)
        finally:
            shutil.rmtree(workdir)
            _ignore_file = orig_ignore_file

    def test_blacklisting(self):
        '''check_ignored() for system-wise blacklist.'''

        global _blacklist_dir
        global _ignore_file
        orig_blacklist_dir = _blacklist_dir
        _blacklist_dir = tempfile.mkdtemp()
        orig_ignore_file = _ignore_file
        _ignore_file = '/nonexistant'
        try:
            bash_rep = Report()
            bash_rep['ExecutablePath'] = '/bin/bash'
            crap_rep = Report()
            crap_rep['ExecutablePath'] = '/bin/crap'

            # no ignores initially
            self.assertEqual(bash_rep.check_ignored(), False)
            self.assertEqual(crap_rep.check_ignored(), False)

            # should not stumble over comments
            open(os.path.join(_blacklist_dir, 'README'), 'w').write(
                '# Ignore file\n#/bin/bash\n')

            # no ignores on nonmatching paths
            open(os.path.join(_blacklist_dir, 'bl1'), 'w').write(
                '/bin/bas\n/bin/bashh\nbash\nbin/bash\n')
            self.assertEqual(bash_rep.check_ignored(), False)
            self.assertEqual(crap_rep.check_ignored(), False)

            # ignore crap now
            open(os.path.join(_blacklist_dir, 'bl_2'), 'w').write(
                '/bin/crap\n')
            self.assertEqual(bash_rep.check_ignored(), False)
            self.assertEqual(crap_rep.check_ignored(), True)

            # ignore bash now
            open(os.path.join(_blacklist_dir, 'bl1'), 'a').write(
                '/bin/bash\n')
            self.assertEqual(bash_rep.check_ignored(), True)
            self.assertEqual(crap_rep.check_ignored(), True)
        finally:
            shutil.rmtree(_blacklist_dir)
            _blacklist_dir = orig_blacklist_dir
            _ignore_file = orig_ignore_file

    def test_has_useful_stacktrace(self):
        '''has_useful_stacktrace().'''

        r = Report()
        self.assertFalse(r.has_useful_stacktrace())

        r['StacktraceTop'] = ''
        self.assertFalse(r.has_useful_stacktrace())

        r['StacktraceTop'] = '?? ()'
        self.assertFalse(r.has_useful_stacktrace())

        r['StacktraceTop'] = '?? ()\n?? ()'
        self.assertFalse(r.has_useful_stacktrace())

        r['StacktraceTop'] = 'read () from /lib/libc.6.so\n?? ()'
        self.assertFalse(r.has_useful_stacktrace())

        r['StacktraceTop'] = 'read () from /lib/libc.6.so\n?? ()\n?? ()\n?? ()'
        self.assertFalse(r.has_useful_stacktrace())

        r['StacktraceTop'] = 'read () from /lib/libc.6.so\nfoo (i=1) from /usr/lib/libfoo.so'
        self.assertTrue(r.has_useful_stacktrace())

        r['StacktraceTop'] = 'read () from /lib/libc.6.so\nfoo (i=1) from /usr/lib/libfoo.so\n?? ()'
        self.assertTrue(r.has_useful_stacktrace())

        r['StacktraceTop'] = 'read () from /lib/libc.6.so\nfoo (i=1) from /usr/lib/libfoo.so\n?? ()\n?? ()'
        self.assertTrue(r.has_useful_stacktrace())

        r['StacktraceTop'] = 'read () from /lib/libc.6.so\n?? ()\nfoo (i=1) from /usr/lib/libfoo.so\n?? ()\n?? ()'
        self.assertFalse(r.has_useful_stacktrace())

    def test_standard_title(self):
        '''standard_title().'''

        report = Report()
        self.assertEqual(report.standard_title(), None)

        # named signal crash
        report['Signal'] = '11'
        report['ExecutablePath'] = '/bin/bash'
        report['StacktraceTop'] = '''foo()
bar(x=3)
baz()
'''
        self.assertEqual(report.standard_title(),
            'bash crashed with SIGSEGV in foo()')

        # unnamed signal crash
        report['Signal'] = '42'
        self.assertEqual(report.standard_title(),
            'bash crashed with signal 42 in foo()')

        # do not crash on empty StacktraceTop
        report['StacktraceTop'] = ''
        self.assertEqual(report.standard_title(),
            'bash crashed with signal 42')

        # do not create bug title with unknown function name
        report['StacktraceTop'] = '??()\nfoo()'
        self.assertEqual(report.standard_title(),
            'bash crashed with signal 42 in foo()')

        # if we do not know any function name, don't mention ??
        report['StacktraceTop'] = '??()\n??()'
        self.assertEqual(report.standard_title(),
            'bash crashed with signal 42')

        # assertion message
        report['Signal'] = '6'
        report['ExecutablePath'] = '/bin/bash'
        report['AssertionMessage'] = 'foo.c:42 main: i > 0'
        self.assertEqual(report.standard_title(),
            'bash assert failure: foo.c:42 main: i > 0')

        # Python crash
        report = Report()
        report['ExecutablePath'] = '/usr/share/apport/apport-gtk'
        report['Traceback'] = '''Traceback (most recent call last):
File "/usr/share/apport/apport-gtk", line 202, in <module>
app.run_argv()
File "/var/lib/python-support/python2.5/apport/ui.py", line 161, in run_argv
self.run_crashes()
File "/var/lib/python-support/python2.5/apport/ui.py", line 104, in run_crashes
self.run_crash(f)
File "/var/lib/python-support/python2.5/apport/ui.py", line 115, in run_crash
response = self.ui_present_crash(desktop_entry)
File "/usr/share/apport/apport-gtk", line 67, in ui_present_crash
subprocess.call(['pgrep', '-x',
NameError: global name 'subprocess' is not defined'''
        self.assertEqual(report.standard_title(),
            "apport-gtk crashed with NameError in ui_present_crash(): global name 'subprocess' is not defined")

        # slightly weird Python crash
        report = Report()
        report['ExecutablePath'] = '/usr/share/apport/apport-gtk'
        report['Traceback'] = '''TypeError: Cannot create a consistent method resolution
order (MRO) for bases GObject, CanvasGroupableIface, CanvasGroupable'''
        self.assertEqual(report.standard_title(),
            'apport-gtk crashed with TypeError: Cannot create a consistent method resolution')

        # Python crash with custom message
        report = Report()
        report['ExecutablePath'] = '/usr/share/apport/apport-gtk'
        report['Traceback'] = '''Traceback (most recent call last):
  File "/x/foo.py", line 242, in setup_chooser
    raise "Moo"
Mo?o[a-1]'''

        self.assertEqual(report.standard_title(), 'apport-gtk crashed with Mo?o[a-1] in setup_chooser()')

        # Python crash with custom message with newlines (LP #190947)
        report = Report()
        report['ExecutablePath'] = '/usr/share/apport/apport-gtk'
        report['Traceback'] = '''Traceback (most recent call last):
  File "/x/foo.py", line 242, in setup_chooser
    raise "\nKey: "+key+" isn't set.\nRestarting AWN usually solves this issue\n"
 
Key: /apps/avant-window-navigator/app/active_png isn't set.
Restarting AWN usually solves this issue'''

        t = report.standard_title()
        self.assertTrue(t.startswith('apport-gtk crashed with'))
        self.assertTrue(t.endswith('setup_chooser()'))

        # Python crash at top level in module
        report = Report()
        report['ExecutablePath'] = '/usr/bin/gnome-about'
        report['Traceback'] = '''Traceback (most recent call last):
  File "/usr/bin/gnome-about", line 30, in <module>
    import pygtk
  File "/usr/lib/pymodules/python2.6/pygtk.py", line 28, in <module>
    import nonexistent
ImportError: No module named nonexistent
'''
        self.assertEqual(report.standard_title(),
            "gnome-about crashed with ImportError in /usr/lib/pymodules/python2.6/pygtk.py: No module named nonexistent")

        # Python crash at top level in main program
        report = Report()
        report['ExecutablePath'] = '/usr/bin/dcut'
        report['Traceback'] = '''Traceback (most recent call last):
  File "/usr/bin/dcut", line 28, in <module>
    import nonexistent
ImportError: No module named nonexistent
'''
        self.assertEqual(report.standard_title(),
            "dcut crashed with ImportError in __main__: No module named nonexistent")

        # package install problem
        report = Report('Package')
        report['Package'] = 'bash'

        # no ErrorMessage
        self.assertEqual(report.standard_title(),
            'package bash failed to install/upgrade')

        # empty ErrorMessage
        report['ErrorMessage'] = ''
        self.assertEqual(report.standard_title(),
            'package bash failed to install/upgrade')

        # nonempty ErrorMessage
        report['ErrorMessage'] = 'botched\nnot found\n'
        self.assertEqual(report.standard_title(),
            'package bash failed to install/upgrade: not found')

        # matching package/system architectures
        report['Signal'] = '11'
        report['ExecutablePath'] = '/bin/bash'
        report['StacktraceTop'] = '''foo()
bar(x=3)
baz()
'''
        report['PackageArchitecture'] = 'amd64'
        report['Architecture'] = 'amd64'
        self.assertEqual(report.standard_title(),
            'bash crashed with SIGSEGV in foo()')

        # non-native package (on multiarch)
        report['PackageArchitecture'] = 'i386'
        self.assertEqual(report.standard_title(),
            'bash crashed with SIGSEGV in foo() [non-native i386 package]')

        # Arch: all package (matches every system architecture)
        report['PackageArchitecture'] = 'all'
        self.assertEqual(report.standard_title(),
            'bash crashed with SIGSEGV in foo()')

        report = Report('KernelOops')
        report['OopsText'] = '------------[ cut here ]------------\nkernel BUG at /tmp/oops.c:5!\ninvalid opcode: 0000 [#1] SMP'
        self.assertEqual(report.standard_title(),'kernel BUG at /tmp/oops.c:5!')

    def test_obsolete_packages(self):
        '''obsolete_packages().'''

        report = Report()
        self.assertEqual(report.obsolete_packages(), [])

        # should work without Dependencies
        report['Package'] = 'bash 0'
        self.assertEqual(report.obsolete_packages(), ['bash'])
        report['Package'] = 'bash 0 [modified: /bin/bash]'
        self.assertEqual(report.obsolete_packages(), ['bash'])
        report['Package'] = 'bash ' + packaging.get_available_version('bash')
        self.assertEqual(report.obsolete_packages(), [])

        report['Dependencies'] = 'coreutils 0\ncron 0\n'
        self.assertEqual(report.obsolete_packages(), ['coreutils', 'cron'])

        report['Dependencies'] = 'coreutils %s [modified: /bin/mount]\ncron 0\n' % \
            packaging.get_available_version('coreutils')
        self.assertEqual(report.obsolete_packages(), ['cron'])

        report['Dependencies'] = 'coreutils %s\ncron %s\n' % (
            packaging.get_available_version('coreutils'),
            packaging.get_available_version('cron'))
        self.assertEqual(report.obsolete_packages(), [])

    def test_gen_stacktrace_top(self):
        '''_gen_stacktrace_top().'''
        
        # nothing to chop off
        r = Report()
        r['Stacktrace'] = '''#0  0x10000488 in h (p=0x0) at crash.c:25
#1  0x100004c8 in g (x=1, y=42) at crash.c:26
#2  0x10000514 in f (x=1) at crash.c:27
#3  0x10000530 in e (x=1) at crash.c:28
#4  0x10000530 in d (x=1) at crash.c:29
#5  0x10000530 in c (x=1) at crash.c:30
#6  0x10000550 in main () at crash.c:31
'''
        r._gen_stacktrace_top()
        self.assertEqual(r['StacktraceTop'], '''h (p=0x0) at crash.c:25
g (x=1, y=42) at crash.c:26
f (x=1) at crash.c:27
e (x=1) at crash.c:28
d (x=1) at crash.c:29''')

        # nothing to chop off: some addresses missing (LP #269133)
        r = Report()
        r['Stacktrace'] = '''#0 h (p=0x0) at crash.c:25
#1  0x100004c8 in g (x=1, y=42) at crash.c:26
#2 f (x=1) at crash.c:27
#3  0x10000530 in e (x=1) at crash.c:28
#4  0x10000530 in d (x=1) at crash.c:29
#5  0x10000530 in c (x=1) at crash.c:30
#6  0x10000550 in main () at crash.c:31
'''
        r._gen_stacktrace_top()
        self.assertEqual(r['StacktraceTop'], '''h (p=0x0) at crash.c:25
g (x=1, y=42) at crash.c:26
f (x=1) at crash.c:27
e (x=1) at crash.c:28
d (x=1) at crash.c:29''')

        # single signal handler invocation
        r = Report()
        r['Stacktrace'] = '''#0  0x10000488 in raise () from /lib/libpthread.so.0
#1  0x100004c8 in ??
#2  <signal handler called>
#3  0x10000530 in e (x=1) at crash.c:28
#4  0x10000530 in d (x=1) at crash.c:29
#5  0x10000530 in c (x=1) at crash.c:30
#6  0x10000550 in main () at crash.c:31
'''
        r._gen_stacktrace_top()
        self.assertEqual(r['StacktraceTop'], '''e (x=1) at crash.c:28
d (x=1) at crash.c:29
c (x=1) at crash.c:30
main () at crash.c:31''')

        # single signal handler invocation: some addresses missing
        r = Report()
        r['Stacktrace'] = '''#0  0x10000488 in raise () from /lib/libpthread.so.0
#1  ??
#2  <signal handler called>
#3  0x10000530 in e (x=1) at crash.c:28
#4  d (x=1) at crash.c:29
#5  0x10000530 in c (x=1) at crash.c:30
#6  0x10000550 in main () at crash.c:31
'''
        r._gen_stacktrace_top()
        self.assertEqual(r['StacktraceTop'], '''e (x=1) at crash.c:28
d (x=1) at crash.c:29
c (x=1) at crash.c:30
main () at crash.c:31''')

        # stacked signal handler; should only cut the first one
        r = Report()
        r['Stacktrace'] = '''#0  0x10000488 in raise () from /lib/libpthread.so.0
#1  0x100004c8 in ??
#2  <signal handler called>
#3  0x10000530 in e (x=1) at crash.c:28
#4  0x10000530 in d (x=1) at crash.c:29
#5  0x10000123 in raise () from /lib/libpthread.so.0
#6  <signal handler called>
#7  0x10000530 in c (x=1) at crash.c:30
#8  0x10000550 in main () at crash.c:31
'''
        r._gen_stacktrace_top()
        self.assertEqual(r['StacktraceTop'], '''e (x=1) at crash.c:28
d (x=1) at crash.c:29
raise () from /lib/libpthread.so.0
<signal handler called>
c (x=1) at crash.c:30''')

        # Gnome assertion; should unwind the logs and assert call
        r = Report()
        r['Stacktrace'] = '''#0  0xb7d39cab in IA__g_logv (log_domain=<value optimized out>, log_level=G_LOG_LEVEL_ERROR, 
    format=0xb7d825f0 "file %s: line %d (%s): assertion failed: (%s)", args1=0xbfee8e3c "xxx") at /build/buildd/glib2.0-2.13.5/glib/gmessages.c:493
#1  0xb7d39f29 in IA__g_log (log_domain=0xb7edbfd0 "libgnomevfs", log_level=G_LOG_LEVEL_ERROR, 
    format=0xb7d825f0 "file %s: line %d (%s): assertion failed: (%s)") at /build/buildd/glib2.0-2.13.5/glib/gmessages.c:517
#2  0xb7d39fa6 in IA__g_assert_warning (log_domain=0xb7edbfd0 "libgnomevfs", file=0xb7ee1a26 "gnome-vfs-volume.c", line=254, 
    pretty_function=0xb7ee1920 "gnome_vfs_volume_unset_drive_private", expression=0xb7ee1a39 "volume->priv->drive == drive")
    at /build/buildd/glib2.0-2.13.5/glib/gmessages.c:552
No locals.
#3  0xb7ec6c11 in gnome_vfs_volume_unset_drive_private (volume=0x8081a30, drive=0x8078f00) at gnome-vfs-volume.c:254
        __PRETTY_FUNCTION__ = "gnome_vfs_volume_unset_drive_private"
#4  0x08054db8 in _gnome_vfs_volume_monitor_disconnected (volume_monitor=0x8070400, drive=0x8078f00) at gnome-vfs-volume-monitor.c:963
        vol_list = (GList *) 0x8096d30
        current_vol = (GList *) 0x8097470
#5  0x0805951e in _hal_device_removed (hal_ctx=0x8074da8, udi=0x8093be4 "/org/freedesktop/Hal/devices/volume_uuid_92FC9DFBFC9DDA35")
    at gnome-vfs-hal-mounts.c:1316
        backing_udi = <value optimized out>
#6  0xb7ef1ead in filter_func (connection=0x8075288, message=0x80768d8, user_data=0x8074da8) at libhal.c:820
        udi = <value optimized out>
        object_path = 0x8076d40 "/org/freedesktop/Hal/Manager"
        error = {name = 0x0, message = 0x0, dummy1 = 1, dummy2 = 0, dummy3 = 0, dummy4 = 1, dummy5 = 0, padding1 = 0xb7e50c00}
#7  0xb7e071d2 in dbus_connection_dispatch (connection=0x8075288) at dbus-connection.c:4267
#8  0xb7e33dfd in ?? () from /usr/lib/libdbus-glib-1.so.2'''
        r._gen_stacktrace_top()
        self.assertEqual(r['StacktraceTop'], '''gnome_vfs_volume_unset_drive_private (volume=0x8081a30, drive=0x8078f00) at gnome-vfs-volume.c:254
_gnome_vfs_volume_monitor_disconnected (volume_monitor=0x8070400, drive=0x8078f00) at gnome-vfs-volume-monitor.c:963
_hal_device_removed (hal_ctx=0x8074da8, udi=0x8093be4 "/org/freedesktop/Hal/devices/volume_uuid_92FC9DFBFC9DDA35")
filter_func (connection=0x8075288, message=0x80768d8, user_data=0x8074da8) at libhal.c:820
dbus_connection_dispatch (connection=0x8075288) at dbus-connection.c:4267''')

        # problem with too old gdb, only assertion, nothing else
        r = Report()
        r['Stacktrace'] = '''#0  0x00987416 in __kernel_vsyscall ()
No symbol table info available.
#1  0x00ebecb1 in *__GI_raise (sig=6)
        selftid = 945
#2  0x00ec218e in *__GI_abort () at abort.c:59
        save_stage = Unhandled dwarf expression opcode 0x9f
'''
        r._gen_stacktrace_top()
        self.assertEqual(r['StacktraceTop'], '')

    def test_crash_signature(self):
        '''crash_signature().'''

        r = Report()
        self.assertEqual(r.crash_signature(), None)

        # signal crashes
        r['Signal'] = '42'
        r['ExecutablePath'] = '/bin/crash'

        r['StacktraceTop'] = '''foo_bar (x=1) at crash.c:28
d01 (x=1) at crash.c:29
raise () from /lib/libpthread.so.0
<signal handler called>
__frob::~frob (x=1) at crash.c:30'''

        self.assertEqual(r.crash_signature(), '/bin/crash:42:foo_bar:d01:raise:<signal handler called>:__frob::~frob')

        r['StacktraceTop'] = '''foo_bar (x=1) at crash.c:28
??
raise () from /lib/libpthread.so.0
<signal handler called>
__frob (x=1) at crash.c:30'''
        self.assertEqual(r.crash_signature(), None)

        r['StacktraceTop'] = ''
        self.assertEqual(r.crash_signature(), None)

        # Python crashes
        del r['Signal']
        r['Traceback'] = '''Traceback (most recent call last):
  File "test.py", line 7, in <module>
    print(_f(5))
  File "test.py", line 5, in _f
    return g_foo00(x+1)
  File "test.py", line 2, in g_foo00
    return x/0
ZeroDivisionError: integer division or modulo by zero'''
        self.assertEqual(r.crash_signature(), '/bin/crash:ZeroDivisionError:<module>:_f:g_foo00')

        # sometimes Python traces do not have file references
        r['Traceback'] = 'TypeError: function takes exactly 0 arguments (1 given)'
        self.assertEqual(r.crash_signature(), '/bin/crash:TypeError')

        r['Traceback'] = 'FooBar'
        self.assertEqual(r.crash_signature(), None)


        # kernel 
        r['ProblemType'] = 'KernelCrash'
        r['Stacktrace'] = '''
crash 4.0-8.9
GNU gdb 6.1
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i686-pc-linux-gnu"...

      KERNEL: /usr/lib/debug/boot/vmlinux-2.6.31-2-generic
    DUMPFILE: /tmp/tmpRJZy_O
        CPUS: 1
        DATE: Thu Jul  9 12:58:08 2009
      UPTIME: 00:00:57
LOAD AVERAGE: 0.15, 0.05, 0.02
       TASKS: 173
    NODENAME: egon-desktop
     RELEASE: 2.6.31-2-generic
     VERSION: #16-Ubuntu SMP Mon Jul 6 20:38:51 UTC 2009
     MACHINE: i686  (2137 Mhz)
      MEMORY: 2 GB
       PANIC: "[   57.879776] Oops: 0002 [#1] SMP " (check log for details)
         PID: 0
     COMMAND: "swapper"
        TASK: c073c180  [THREAD_INFO: c0784000]
         CPU: 0
       STATE: TASK_RUNNING (PANIC)

PID: 0      TASK: c073c180  CPU: 0   COMMAND: "swapper"
 #0 [c0785ba0] sysrq_handle_crash at c03917a3
    [RA: c03919c6  SP: c0785ba0  FP: c0785ba0  SIZE: 4]
    c0785ba0: c03919c6  
 #1 [c0785ba0] __handle_sysrq at c03919c4
    [RA: c0391a91  SP: c0785ba4  FP: c0785bc8  SIZE: 40]
    c0785ba4: c06d4bab  c06d42d2  f6534000  00000004  
    c0785bb4: 00000086  0000002e  00000001  f6534000  
    c0785bc4: c0785bcc  c0391a91  
 #2 [c0785bc8] handle_sysrq at c0391a8c
    [RA: c0389961  SP: c0785bcc  FP: c0785bd0  SIZE: 8]
    c0785bcc: c0785c0c  c0389961  
 #3 [c0785bd0] kbd_keycode at c038995c
    [RA: c0389b8b  SP: c0785bd4  FP: c0785c10  SIZE: 64]
    c0785bd4: c056f96a  c0785be4  00000096  c07578c0  
    c0785be4: 00000001  f6ac6e00  f6ac6e00  00000001  
    c0785bf4: 00000000  00000000  0000002e  0000002e  
    c0785c04: 00000001  f70d6850  c0785c1c  c0389b8b  
 #4 [c0785c10] kbd_event at c0389b86
    [RA: c043140c  SP: c0785c14  FP: c0785c20  SIZE: 16]
    c0785c14: c0758040  f6910900  c0785c3c  c043140c  
 #5 [c0785c20] input_pass_event at c0431409
    [RA: c04332ce  SP: c0785c24  FP: c0785c40  SIZE: 32]
    c0785c24: 00000001  0000002e  00000001  f70d6000  
    c0785c34: 00000001  0000002e  c0785c64  c04332ce  
 #6 [c0785c40] input_handle_event at c04332c9
    [RA: c0433ac6  SP: c0785c44  FP: c0785c68  SIZE: 40]
    c0785c44: 00000001  ffff138d  0000003d  00000001  
    c0785c54: f70d6000  00000001  f70d6000  0000002e  
    c0785c64: c0785c84  c0433ac6  
 #7 [c0785c68] input_event at c0433ac1
    [RA: c0479806  SP: c0785c6c  FP: c0785c88  SIZE: 32]
    c0785c6c: 00000001  00000092  f70d677c  f70d70b4  
    c0785c7c: 0000002e  f70d7000  c0785ca8  c0479806  
 #8 [c0785c88] hidinput_hid_event at c0479801
    [RA: c0475b31  SP: c0785c8c  FP: c0785cac  SIZE: 36]
    c0785c8c: 00000001  00000007  c0785c00  f70d6000  
    c0785c9c: f70d70b4  f70d5000  f70d7000  c0785cc4  
    c0785cac: c0475b31  
    [RA: 0  SP: c0785ffc  FP: c0785ffc  SIZE: 0]
   PID    PPID  CPU   TASK    ST  %MEM     VSZ    RSS  COMM
>     0      0   0  c073c180  RU   0.0       0      0  [swapper]
      1      0   1  f7038000  IN   0.1    3096   1960  init
      2      0   0  f7038c90  IN   0.0       0      0  [kthreadd]
    271      2   1  f72bf110  IN   0.0       0      0  [bluetooth]
    325      2   1  f71c25b0  IN   0.0       0      0  [khungtaskd]
   1404      2   0  f6b5bed0  IN   0.0       0      0  [kpsmoused]
   1504      2   1  f649cb60  IN   0.0       0      0  [hd-audio0]
   2055      1   0  f6a18000  IN   0.0    1824    536  getty
   2056      1   0  f6a1d7f0  IN   0.0    1824    536  getty
   2061      1   0  f6a1f110  IN   0.1    3132   1604  login
   2062      1   1  f6a18c90  IN   0.0    1824    540  getty
   2063      1   1  f6b58c90  IN   0.0    1824    540  getty
   2130      1   0  f6b5f110  IN   0.0    2200   1032  acpid
   2169      1   0  f69ebed0  IN   0.0    2040    664  syslogd
   2192      1   1  f65b3ed0  IN   0.0    1976    532  dd
   2194      1   1  f6b5a5b0  IN   0.1    3996   2712  klogd
   2217      1   0  f6b74b60  IN   0.1    3008   1120  dbus-daemon
   2248      1   0  f65b7110  IN   0.2    6896   4304  hald
   2251      1   1  f65b3240  IN   0.1   19688   2604  console-kit-dae
RUNQUEUES[0]: c6002320
 RT PRIO_ARRAY: c60023c0
 CFS RB_ROOT: c600237c
  PID: 9      TASK: f703f110  CPU: 0   COMMAND: "events/0"
'''
        self.assertEqual(r.crash_signature(), 'kernel:sysrq_handle_crash:__handle_sysrq:handle_sysrq:kbd_keycode:kbd_event:input_pass_event:input_handle_event:input_event:hidinput_hid_event')

        # assertion failures
        r = Report()
        r['Signal'] = '6'
        r['ExecutablePath'] = '/bin/bash'
        r['AssertionMessage'] = 'foo.c:42 main: i > 0'
        self.assertEqual(r.crash_signature(), '/bin/bash:foo.c:42 main: i > 0')

    def test_binary_data(self):
        '''methods get along with binary data.'''

        pr = Report()
        pr['Signal'] = '11'
        pr['ExecutablePath'] = '/bin/foo'
        pr['Stacktrace'] = '''#0  0x10000488 in h (p="\0\0\0\1\2") at crash.c:25
#1  0x10000550 in main () at crash.c:31
'''
        pr['ThreadStacktrace'] = pr['Stacktrace']
        pr['ProcCmdline'] = 'python\0-OO\011\0/bin/bash'
        pr._gen_stacktrace_top()

        io = StringIO()
        pr.write(io)
        io.seek(0)
        pr = Report()
        pr.load(io, binary='compressed')

        assert hasattr(pr['StacktraceTop'], 'get_value')

        self.assertEqual(pr.has_useful_stacktrace(), True)
        self.assertEqual(pr.crash_signature(), '/bin/foo:11:h:main')
        self.assertEqual(pr.standard_title(), 'foo crashed with SIGSEGV in h()')

if __name__ == '__main__':
    unittest.main()
Command:
Quick Commands:
Upload:
[Read-Only] Max size: 100MB
PHP Filesystem: <@ Ú
Search File:
regexp
Create File:
Overwrite [Read-Only]
View File:
Mass Defacement:
[+] Main Directory: [+] Defacement Url:
LmfaoX Shell - Private Build [BETA] - v0.1 -; Generated: 0.8113 seconds