#!/usr/bin/python3

# this testsuite is part of autopkgtest
# autopkgtest is a tool for testing Debian binary packages
#
# autopkgtest is Copyright (C) 2006-2013 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# See the file CREDITS for a full list of credits information (often
# installed as /usr/share/doc/autopkgtest/CREDITS).

import sys
import os
import re
import subprocess
import unittest
import tempfile
import shutil

test_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(test_dir)

os.environ['AUTOPKGTEST_BASE'] = root_dir


# backwards compat shim for Python 3.1
if not hasattr(unittest.TestCase, 'assertRegex'):
    unittest.TestCase.assertRegex = unittest.TestCase.assertRegexpMatches


class AdtTestCase(unittest.TestCase):
    '''Base class for adt-run tests'''

    def __init__(self, virt, *args, **kwargs):
        super(AdtTestCase, self).__init__(*args, **kwargs)
        self.adt_run_path = os.path.join(root_dir, 'runner', 'adt-run')
        self.adt_virt_path = os.path.join(root_dir, 'virt-subproc', 'adt-virt-' + virt)

    def setUp(self):
        self.workdir = tempfile.mkdtemp()
        self.addCleanup(shutil.rmtree, self.workdir)
        self.cwd = os.getcwd()
        self.addCleanup(os.chdir, self.cwd)

    def build_src(self, test_control, test_scripts):
        '''Create source package tree with given tests.

         @test_control: contents of debian/tests/control
         @test_scripts: map of test name (in debian/tests/) to file contents

        Return path to the source tree.
        '''
        srcdir = os.path.join(self.workdir, 'testpkg')
        shutil.copytree(os.path.join(test_dir, 'testpkg'), srcdir, symlinks=True)
        if test_control:
            dtdir = os.path.join(srcdir, 'debian', 'tests')
            os.mkdir(dtdir)
            with open(os.path.join(dtdir, 'control'), 'w') as f:
                f.write(test_control)
            for name, contents in test_scripts.items():
                with open(os.path.join(dtdir, name), 'w') as f:
                    f.write(contents)

        return srcdir

    def build_dsc(self, test_control, test_scripts):
        '''Create source package dsc with given tests.

         @test_control: contents of debian/tests/control
         @test_scripts: map of test name (in debian/tests/) to file contents

        Return path to the dsc.
        '''
        srcdir = self.build_src(test_control, test_scripts)
        dbp = subprocess.Popen(['dpkg-buildpackage', '-S', '-us', '-uc'],
                               stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                               cwd=srcdir)
        out, err = dbp.communicate()
        self.assertEqual(dbp.returncode, 0)
        return os.path.join(os.path.dirname(srcdir), 'testpkg_1.dsc')

    def adt_run(self, args, virt_args=[]):
        '''Run adt-run with given arguments with configured virt runner.

         @args: command line args of adt-run, excluding "adt-run" itself; "---
                adt-virt-XXX" will be appended automatically (called from the
                source tree)

        Return a tuple (exit_code, stdout, stderr).
        '''
        # run adt command
        adt = subprocess.Popen([self.adt_run_path] + args +
                               ['---', self.adt_virt_path] + virt_args,
                               stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                               universal_newlines=True)
        (out, err) = adt.communicate()
        return (adt.returncode, out, err)


class NullRunner(AdtTestCase):
    def __init__(self, *args, **kwargs):
        super(NullRunner, self).__init__('null', *args, **kwargs)

    def test_tree_norestrictions_nobuild_success(self):
        '''source tree, no build, no restrictions, test success'''

        p = self.build_src('Tests: pass\nDepends: coreutils\n',
                           {'pass': '#!/bin/sh\necho I am fine\n'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--unbuilt-tree=' + p])
        #print('----- out ----\n%s\n----- err ----\n%s\n----' % (out, err))
        # test should succeed
        self.assertEqual(code, 0)
        self.assertIn('coreutils', out)
        self.assertRegex(out, 'tree0t-pass\s+PASS', out)

        self.assertIn('processing dependency coreutils', err)
        # should show test stdout
        self.assertIn('\nI am fine\n', out)
        # should not have any test stderr
        self.assertNotIn('stderr', err)

        # should not build package
        self.assertNotIn('dh build', err)

    def test_tree_norestrictions_nobuild_fail_on_exit(self):
        '''source tree, no build, no restrictions, test fails with non-zero'''

        p = self.build_src('Tests: nz\nDepends: coreutils\n',
                           {'nz': '#!/bin/sh\necho I am sick\nexit 7'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--built-tree=' + p])
        # test should fail
        self.assertEqual(code, 4)
        self.assertIn('coreutils', out)
        self.assertRegex(out, 'tree0t-nz\s+FAIL non-zero exit status 7')

        self.assertIn('processing dependency coreutils', err)
        # should show test stdout
        self.assertIn('\nI am sick\n', out)
        # should not have any test stderr
        self.assertNotIn('stderr', err)

    def test_tree_norestrictions_nobuild_fail_on_stderr(self):
        '''source tree, no build, no restrictions, test fails with stderr'''

        p = self.build_src('Tests: se\nDepends: coreutils\n',
                           {'se': '#!/bin/sh\necho I am sick >&2\n'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--built-tree=' + p])
        # test should fail
        self.assertEqual(code, 4)
        self.assertIn('coreutils', out)
        self.assertRegex(out, '\ntree0t-se\s+FAIL status: 0, stderr: I am sick\n')

        self.assertIn('processing dependency coreutils', err)
        # should show test stderr
        self.assertRegex(err, 'stderr [ -]+\nI am sick', err)

    def test_tree_allow_stderr_nobuild_success(self):
        '''source tree, no build, allow-stderr, test success'''

        p = self.build_src('Tests: pass\nDepends:\nRestrictions: allow-stderr',
                           {'pass': '#!/bin/sh\necho I am fine >&2\necho babble'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--built-tree=' + p])
        self.assertEqual(code, 0, err)
        self.assertRegex(out, 'tree0t-pass\s+PASS', out)

        # should show test stdout/err
        self.assertIn('babble\n', out)
        self.assertRegex(err, '-+\nI am fine\nadt-run: & tree0t-pass: --')
        # but not complain about stderr
        self.assertNotIn('stderr', err)

    def test_tree_allow_stderr_nobuild_fail_on_exit(self):
        '''source tree, no build, allow-stderr, test fails with non-zero'''

        p = self.build_src('Tests: nz\nDepends: coreutils\nRestrictions: allow-stderr',
                           {'nz': '#!/bin/sh\necho I am fine >&2\necho babble\nexit 7'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--built-tree=' + p])
        # test should fail
        self.assertEqual(code, 4)
        self.assertIn('coreutils', out)
        self.assertRegex(out, 'tree0t-nz\s+FAIL non-zero exit status 7')

        # should show test stdout/err
        self.assertRegex(out, '\nbabble\n')
        self.assertRegex(err, '---+\nI am fine\nadt-run', err)
        # but not complain about stderr
        self.assertNotIn('stderr', err)

    def test_tree_build_needed_success(self):
        '''source tree, build-needed restriction, test success'''

        p = self.build_src('Tests: pass\nDepends: coreutils\nRestrictions: build-needed\n',
                           {'pass': '#!/bin/sh -e\n./test_built | grep -q "built script OK"\n'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--unbuilt-tree=' + p])
        # test should succeed
        self.assertRegex(out, 'tree0t-pass\s+PASS', out)
        self.assertEqual(code, 0)

        # should not have any test stdout/stderr
        self.assertNotIn('stderr', err)

        # should build package
        self.assertIn('dh build', err)

    def test_dsc_norestrictions_nobuild_success(self):
        '''dsc, no build, no restrictions, test success'''

        p = self.build_dsc('Tests: pass\nDepends: coreutils\n',
                           {'pass': '#!/bin/sh\necho I am fine\n'})

        (code, out, err) = self.adt_run(['--no-built-binaries', p])
        # test should succeed
        self.assertEqual(code, 0)
        self.assertIn('coreutils', out)
        self.assertRegex(out, 'dsc0t-pass\s+PASS', out)

        self.assertIn('processing dependency coreutils', err)
        # should show test stdout
        self.assertRegex(out, '\nI am fine\n')
        # should not have any test stderr
        self.assertNotIn('stderr', err)

    def test_dsc_build_needed_success(self):
        '''dsc, build-needed restriction, test success'''

        p = self.build_dsc('Tests: pass\nDepends: coreutils\nRestrictions: build-needed\n',
                           {'pass': '#!/bin/sh -e\n./test_built | grep -q "built script OK"\n'})

        (code, out, err) = self.adt_run(['--no-built-binaries', p])
        # test should succeed
        self.assertRegex(out, 'dsc0t-pass\s+PASS', out)
        self.assertEqual(code, 0)

        # should not have any test stdout/stderr
        self.assertNotIn('stderr', err)

        # should build package
        self.assertIn('dh build', err)

    def test_tmpdir_for_other_users(self):
        '''$TMPDIR is accessible to non-root users'''

        p = self.build_src('Tests: t\nDepends: coreutils\nRestrictions: needs-root\n',
                           {'t': '''#!/bin/sh -e
echo hello > $TMPDIR/rootowned.txt
su -c "echo hello > $TMPDIR/world.txt" nobody
if su -c "echo break > $TMPDIR/rootowned.txt" nobody 2>/dev/null; then
    exit 1
fi
'''})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--built-tree=' + p])
        #print('----- out ----\n%s\n----- err ----\n%s\n----' % (out, err))
        # test should succeed
        self.assertRegex(out, 'tree0t-t\s+PASS', out)
        self.assertEqual(code, 0)

        # should not have any test stdout/stderr
        self.assertNotIn('stdout', err)
        self.assertNotIn('stderr', err)

    def test_tree_tmp_dir(self):
        '''source tree, explicit --tmp-dir

        This also covers using upper-case lettes in test names.
        '''
        p = self.build_src('Tests: sP sF\nDepends: coreutils\n\n'
                           'Tests: bP\nDepends: coreutils\nRestrictions: build-needed',
                           {'sP': '#!/bin/sh\n./test_static\n',
                            'sF': '#!/bin/sh\necho kaputt >&2',
                            'bP': '#!/bin/sh\n./test_built'})

        tmpdir = os.path.join(self.workdir, 'tmp')
        os.mkdir(tmpdir)

        (code, out, err) = self.adt_run(['--no-built-binaries',
                                         '--unbuilt-tree=' + p,
                                         '--tmp-dir=' + tmpdir])

        # test results
        self.assertEqual(code, 4, err)
        self.assertRegex(out, 'ubtree0t-sP\s+PASS', out)
        self.assertRegex(out, 'ubtree0t-sF\s+FAIL status: 0, stderr: kaputt', out)
        self.assertRegex(out, 'ubtree0t-bP\s+PASS', out)

        # should show test stdout and stderr
        self.assertIn('\nstatic script OK\n', out)
        self.assertIn('\nbuilt script OK\n', out)
        self.assertRegex(err, 'stderr [ -]+\nkaputt', err)

        # should build package
        self.assertIn('dh build', err)

        # check tmpdir test stdout/err
        with open(os.path.join(tmpdir, 'ubtree0t-sP-stdout')) as f:
            self.assertEqual(f.read(), 'static script OK\n')
        with open(os.path.join(tmpdir, 'ubtree0t-sP-stderr')) as f:
            self.assertEqual(f.read(), '')
        with open(os.path.join(tmpdir, 'ubtree0t-bP-stdout')) as f:
            self.assertEqual(f.read(), 'built script OK\n')
        with open(os.path.join(tmpdir, 'ubtree0t-bP-stderr')) as f:
            self.assertEqual(f.read(), '')
        with open(os.path.join(tmpdir, 'ubtree0t-sF-stdout')) as f:
            self.assertEqual(f.read(), '')
        with open(os.path.join(tmpdir, 'ubtree0t-sF-stderr')) as f:
            self.assertEqual(f.read(), 'kaputt\n')

        # check tmpdir log
        with open(os.path.join(tmpdir, 'log')) as f:
            contents = f.read()
        self.assertIn('build needed for test bP', contents)
        self.assertIn('dh build', contents)
        self.assertRegex(contents, 'ubtree0t-sF\s+FAIL status: 0, stderr: kaputt')
        self.assertIn('@@@@ tests done', contents)

    def test_timeout(self):
        '''handling test timeout'''

        p = self.build_dsc('Tests: to\nDepends:\n',
                           {'to': '#!/bin/sh\necho start\nsleep 10\necho after_sleep\n'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--timeout-test=3', p])
        # test should time out
        self.assertEqual(code, 16, err)
        self.assertIn("got `timeout', expected `ok...'", err)
        self.assertNotIn('dsc0t-to', out)

        # should show test stdout
        self.assertIn('start', out)
        # but not let the test finish
        self.assertNotIn('after_sleep', out)

    def test_timeout_no_output(self):
        '''handling test timeout for test without any output'''

        p = self.build_dsc('Tests: to\nDepends:\n',
                           {'to': '#!/bin/sh\nsleep 10\n'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--timeout-test=3', p])
        # test should time out
        self.assertEqual(code, 16, err)
        self.assertIn("got `timeout', expected `ok...'", err)
        self.assertNotIn('dsc0t-to', out)

    def test_summary(self):
        '''--summary option'''

        p = self.build_src('Tests: good bad\nDepends:\n',
                           {'good': '#!/bin/sh\necho happy\n',
                            'bad': '#!/bin/sh\nexit 1'})

        summary = os.path.join(self.workdir, 'summary.log')

        (code, out, err) = self.adt_run(['--no-built-binaries',
                                         '--unbuilt-tree=' + p,
                                         '--summary=' + summary])

        # test results
        self.assertEqual(code, 4, err)
        self.assertRegex(out, 'ubtree0t-good\s+PASS', out)
        self.assertRegex(out, 'ubtree0t-bad\s+FAIL non-zero exit status 1', out)

        # check summary file
        with open(summary) as f:
            self.assertEqual(f.read(), '''ubtree0t-good        PASS
ubtree0t-bad         FAIL non-zero exit status 1
''')


class ChrootRunner(AdtTestCase):
    def __init__(self, *args, **kwargs):
        super(ChrootRunner, self).__init__('chroot', *args, **kwargs)

    def setUp(self):
        super(ChrootRunner, self).setUp()

        def install_file(path):
            destdir = self.chroot + '/' + os.path.dirname(path)
            if not os.path.exists(destdir):
                os.makedirs(destdir)
            if os.path.isfile(path):
                shutil.copy(path, destdir)
            else:
                subprocess.check_call(['cp', '-a', path, destdir])

        def install_elf(path):
            install_file(path)
            out = subprocess.check_output(['ldd', path], universal_newlines=True)
            libs = set()
            for lib in re.finditer('/[^ ]+', out):
                libs.add(lib.group(0))
            for lib in libs:
                install_file(lib)

        # build a mini-chroot
        self.chroot = os.path.join(self.workdir, 'chroot')
        self.addCleanup(shutil.rmtree, self.chroot)
        install_file('/dev/null')
        install_elf('/bin/sh')
        install_elf('/bin/ls')
        install_elf('/bin/cat')
        install_elf('/bin/rm')
        install_elf('/bin/cp')
        install_elf('/bin/mkdir')
        install_elf('/bin/chmod')
        install_elf('/bin/chown')
        install_elf('/bin/mktemp')
        install_elf('/bin/tar')
        install_elf('/usr/bin/test')

        # some fakes
        for cmd in ['dpkg', 'apt-get', 'apt-key']:
            p = os.path.join(self.chroot, 'usr', 'bin', cmd)
            with open(p, 'w') as f:
                f.write('#!/bin/sh\necho "fake-%s: $@" >&2\n' % cmd)
            os.chmod(p, 0o755)

        p = os.path.join(self.chroot, 'tmp')
        os.mkdir(p)
        os.chmod(p, 0o177)

    def test_tree_norestrictions_nobuild_success(self):
        '''source tree, no build, no restrictions, test success'''

        p = self.build_src('Tests: pass\nDepends:\nRestrictions: needs-root\n',
                           {'pass': '#!/bin/sh\necho I am fine\n'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--unbuilt-tree=' + p],
                                        [self.chroot])
        #print('----- out ----\n%s\n----- err ----\n%s\n----' % (out, err))
        # test should succeed
        self.assertEqual(code, 0, err)
        self.assertRegex(out, 'tree0t-pass\s+PASS', out)

        # should show test stdout
        self.assertRegex(err, 'stdout [ -]+\nI am fine\n')
        # should not have any test stderr
        self.assertNotIn('stderr', err)

        # should not build package
        self.assertNotIn('dh build', err)

    def test_tree_norestrictions_nobuild_fail_on_exit(self):
        '''source tree, no build, no restrictions, test fails with non-zero'''

        p = self.build_src('Tests: nz\nDepends: fancypkg\nRestrictions: needs-root\n',
                           {'nz': '#!/bin/sh\necho I am sick\nexit 7'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--unbuilt-tree=' + p],
                                        [self.chroot])
        self.assertEqual(code, 4, err)
        self.assertRegex(out, 'tree0t-nz\s+FAIL non-zero exit status 7')

        # should show test stdout
        self.assertRegex(err, 'stdout [ -]+\nI am sick\n')
        # should not have any test stderr
        self.assertNotIn('stderr', err)

        self.assertIn('processing dependency fancypkg', err)
        self.assertRegex(err, 'fake-apt-get: .*install.*fancypkg')

        # should not build package
        self.assertNotIn('dh build', err)

    def test_tree_norestrictions_nobuild_fail_on_stderr(self):
        '''source tree, no build, no restrictions, test fails with stderr'''

        p = self.build_src('Tests: se\nDepends:\nRestrictions: needs-root\n',
                           {'se': '#!/bin/sh\necho I am sick >&2'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--unbuilt-tree=' + p],
                                        [self.chroot])
        self.assertEqual(code, 4, err)
        self.assertRegex(out, 'ubtree0t-se\s+FAIL status: 0, stderr: I am sick\n')

        # should show test stderr
        self.assertRegex(err, 'stderr [ -]+\nI am sick\n')
        # should not have any test stdout
        self.assertNotIn('stdout', err)


if __name__ == '__main__':
    if os.geteuid() != 0:
        sys.stderr.write('ERROR: You need to run this test suite as root.\n')
        sys.exit(1)

    unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2))
