#!/usr/bin/python

import tempfile, shutil, os.path, os, subprocess, signal, time, stat, sys

import problem_report

crashdump_helper_sysctl = '/proc/sys/kernel/crashdump-helper'
test_executable = '/bin/cat'
test_package = 'coreutils'
test_source = 'coreutils'

required_fields = ['ProblemType', 'CoreDump', 'Date', 'ExecutablePath',
    'ProcCmdline', 'ProcEnviron', 'ProcMaps', 'Signal']

local_reportdir = None

def run(argv, msg=None, stdout=False):
    '''Execute the given program with arguments and verify that it exits
    successfully. 
    
    Set stdout to True to see the program's standard output. msg is the
    assertion error message in case of failure.'''

    if stdout:
	result = subprocess.call(argv)
    else:
	result = subprocess.call(argv, stdout=subprocess.PIPE)
    assert result == 0, msg


def clean_preload_lib():
    print '* Cleaning preloadlib directory'
    run(['make', '-C', 'preloadlib', 'clean'], 'Cleaning preloadlib directory')

def build_preload_lib():
    print '* Building preload library with local parameters'
    run(['make', '-C', 'preloadlib', 'AGENTPATH=%s' % os.path.join(os.getcwd(), 'apport')], 'Building preloadlib')

def enable_preloadlib():
    assert os.access('preloadlib/libapport.so', os.X_OK)
    os.environ['LD_PRELOAD'] = os.path.join(os.getcwd(), 'preloadlib', 'libapport.so')

def create_test_process():
    assert os.access(test_executable, os.X_OK), test_executable + ' is not executable'
    assert subprocess.call(['pidof', test_executable]) == 1, 'no running test executable processes'
    pid = os.fork()
    if pid == 0:
	os.setsid()
	os.execv(test_executable, [test_executable])
	assert False, 'Could not execute ' + test_executable
    return pid

def check_crash(expect_core=True, sig=signal.SIGSEGV):
    global mode # hack to avoid passing the mode every time

    assert not os.path.exists('core'), 'no core dump in current directory'
    pid = create_test_process()
    os.kill(pid, sig)
    result = os.waitpid(pid, 0)[1]
    assert not os.WIFEXITED(result), 'test process did not exit normally'
    assert os.WIFSIGNALED(result), 'test process died due to signal'
    if expect_core and mode == 'kernel':
	assert os.WCOREDUMP(result), 'result: 0x%02X should include WCOREDUMP' % result
    else:
	assert not os.WCOREDUMP(result), 'result: 0x%02X should not have WCOREDUMP' % result
    assert os.WSTOPSIG(result) == 0, 'test process was not signaled to stop'
    assert os.WTERMSIG(result) == sig, 'test process died due to proper signal'
    assert subprocess.call(['pidof', '-x', 'apport']) == 1, 'no running apport processes'
    assert subprocess.call(['pidof', test_executable]) == 1, 'no running test executable processes'
    assert not os.path.exists('core'), 'does not leave core file behind'

def check_safe_environment(env):
    allowed_vars = ['SHELL', 'PATH', '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']

    for l in env.splitlines():
	(k, v) = l.split('=', 1)
	assert k in allowed_vars, '%s is insensitive environment variable' % k

#
# main
#

if len(sys.argv) != 2 or sys.argv[1] not in ('lib', 'kernel'):
    print 'Usage: %s lib|kernel' % sys.argv[0]
    sys.exit(1)
mode = sys.argv[1]

try:
    # check if kernel crashdump helper is already active
    if mode == 'lib':
	assert not os.path.exists(crashdump_helper_sysctl) or not open(crashdump_helper_sysctl).read().strip(), \
		'kernel crash dump helper is still active; please disable before running this test.'

	# set up temporary report directory
	local_reportdir = tempfile.mkdtemp()
	os.environ['APPORT_REPORT_DIR'] = local_reportdir
	print '* Setting up temporary report directory', local_reportdir

	# setup preload library
	clean_preload_lib()
	build_preload_lib()
    else:
	assert os.path.exists(crashdump_helper_sysctl) and open(crashdump_helper_sysctl).read().strip(), \
		'kernel crash dump helper is not active; please enable before running this test.'

    # must not be imported before setting up $APPORT_REPORT_DIR
    import apport_utils

    assert apport_utils.get_all_reports() == [], 'no reports already present'

    if mode == 'lib':
	print '* Check test process creation/killing without apport'
	check_crash(False)
	assert apport_utils.get_all_reports() == [], 'no report created without apport'

    print '* Check test process creation/killing with apport'
    if mode == 'lib':
	enable_preloadlib()
    check_crash()

    # check crash report
    report = os.path.join(apport_utils.report_dir, '%s.%i.crash' %
	(test_executable.replace('/', '_'), os.getuid()))
    assert apport_utils.get_all_reports() == [report], 'report was created'
    st = os.stat(report)
    assert stat.S_IMODE(st.st_mode) == 0600, 'report has correct permissions'

    print '* Check that a subsequent crash does not alter unseen report'
    check_crash()
    st2 = os.stat(report)
    assert st == st2, 'original unread report did not change'

    print '* Check that a subsequent crash alters seen report'
    apport_utils.mark_report_seen(report)
    check_crash()
    st2 = os.stat(report)
    assert st != st2, 'original read report changed'

    print '* Check that report has required fields'
    pr = problem_report.ProblemReport()
    pr.load(open(report))
    assert set(required_fields).issubset(set(pr.keys())), 'report has required fields'
    assert pr['ExecutablePath'] == test_executable
    assert pr['ProcCmdline'] == test_executable
    assert pr['Signal'] == '%i' % signal.SIGSEGV

    print '* Check that dumped environment only has insensitive variables'
    check_safe_environment(pr['ProcEnviron'])

    apport_utils.delete_report(report)
    assert apport_utils.get_all_reports() == [], \
	'no reports present after deleting (present: %s)' % str(apport_utils.get_all_reports())

    print '* Check that non-packaged executables do not create a report'
    orig_test_executable = test_executable
    (fd, test_executable) = tempfile.mkstemp()
    os.write(fd, open(orig_test_executable).read())
    os.close(fd)
    os.chmod(test_executable, 0755)
    check_crash()
    os.unlink(test_executable)
    test_executable = orig_test_executable
    assert apport_utils.get_all_reports() == [], \
	'no reports present after a non-packaged crashed process (present: %s)' % \
	str(apport_utils.get_all_reports())

    print '* Check that apport ignores SIGQUIT'
    check_crash(sig=signal.SIGQUIT)
    assert apport_utils.get_all_reports() == [], \
	'no reports present after SIGQUIT (present: %s)' % \
	str(apport_utils.get_all_reports())

    print '* Check that non-packaged scripts do not create a report'
    orig_test_executable = test_executable
    (fd, test_executable) = tempfile.mkstemp()
    os.write(fd, '#!/bin/sh\nkill -SEGV $$')
    os.close(fd)
    os.chmod(test_executable, 0755)
    check_crash()
    assert apport_utils.get_all_reports() == [], \
	'no reports present after a non-packaged crashed script (present: %s)' % \
	str(apport_utils.get_all_reports())

    # relative path case
    old_cwd = os.getcwd()
    os.chdir(os.path.dirname(test_executable))
    test_executable = './' + os.path.basename(test_executable)
    check_crash()
    os.unlink(test_executable)
    test_executable = orig_test_executable
    os.chdir(old_cwd)
    assert apport_utils.get_all_reports() == [], \
	'no reports present after a non-packaged crashed script with relative path(present: %s)' % \
	str(apport_utils.get_all_reports())

    print '* Test limitation of flooding: iteration',
    count = 0
    while count < 7:
	print count,
	sys.stdout.flush()
	check_crash()
	if mode == 'lib':
	    time.sleep(1)
	if not apport_utils.get_new_reports():
	    break
	apport_utils.mark_report_seen(apport_utils.get_new_reports()[0])
	count += 1
    print
    assert count >= 1, 'gets at least 2 repeated crashes'
    assert count < 7, 'stops flooding after less than 7 repeated crashes'

    apport_utils.delete_report(report)
    assert apport_utils.get_all_reports() == [], \
	'no reports present after deleting (present: %s)' % str(apport_utils.get_all_reports())

    print '* Check that core dump works for non-writable cwds'
    old_cwd = os.getcwd()
    try:
	os.chdir('/')
	check_crash(False) # no core dump expected while this test is EXFAIL
    finally:
	os.chdir(old_cwd)
    pr = problem_report.ProblemReport()
    pr.load(open(report))
    if not set(required_fields).issubset(set(pr.keys())):
	print >> sys.stderr, 'report does not have required fields (EXFAIL)'

finally:
    # clean up temporary work directory
    try:
	apport_utils.delete_report(report)
    except:
	pass
    if local_reportdir:
	print '* Removing temporary report directory', local_reportdir
	shutil.rmtree(local_reportdir)
    if mode == 'lib':
	clean_preload_lib()
