#!/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
import fnmatch

test_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(test_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, 'run-from-checkout')
        self.virt = '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', encoding='UTF-8') 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.virt] + virt_args,
                               stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        (out, err) = adt.communicate()
        return (adt.returncode, out.decode('UTF-8'), err.decode('UTF-8'))


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(['-B', '--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_norestrictions_nobuild_fail_on_stderr_and_exit(self):
        '''source tree, no build, no restrictions, test fails with stderr+exit'''

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

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

        # should show test stderr, but inline
        self.assertRegex(err, 'stderr [ -]+\nI am sick', err)
        self.assertNotIn('stdout', 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 inline
        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('stdout', err)
        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'''

        prev_mask = os.umask(0o077)
        self.addCleanup(os.umask, prev_mask)
        p = self.build_src('Tests: t\nDepends: coreutils\nRestrictions: needs-root\n',
                           {'t': '''#!/bin/sh -e
echo hello > $TMPDIR/rootowned.txt
su -s /bin/sh -c "echo hello > $TMPDIR/world.txt" nobody
if su -s /bin/sh -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_user(self):
        '''--user option'''

        p = self.build_src('Tests: t\nDepends:\n',
                           {'t': '''#!/bin/sh -e
                                    echo world > $TMPDIR/hello.txt
                                    cat $TMPDIR/hello.txt
                                    stat -c %U .
                                    stat -c %U debian
                                    stat -c %U Makefile
                                    whoami'''})

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

        # test should succeed
        self.assertRegex(out, 'tree0t-t\s+PASS', out)

        # has output from cat and whoami
        self.assertIn('world\nnobody\nnobody\nnobody\nnobody\n', out)

    def test_tree_output_dir(self):
        '''source tree, explicit --output-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'})

        outdir = os.path.join(self.workdir, 'out')
        os.mkdir(outdir)
        # put some cruft into it, to check that it gets cleaned up
        with open(os.path.join(outdir, 'cruft.txt'), 'w') as f:
            f.write('hello world')

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

        # 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 outdir test stdout/err
        with open(os.path.join(outdir, 'ubtree0t-sP-stdout')) as f:
            self.assertEqual(f.read(), 'static script OK\n')
        with open(os.path.join(outdir, 'ubtree0t-sP-stderr')) as f:
            self.assertEqual(f.read(), '')
        with open(os.path.join(outdir, 'ubtree0t-bP-stdout')) as f:
            self.assertEqual(f.read(), 'built script OK\n')
        with open(os.path.join(outdir, 'ubtree0t-bP-stderr')) as f:
            self.assertEqual(f.read(), '')
        with open(os.path.join(outdir, 'ubtree0t-sF-stdout')) as f:
            self.assertEqual(f.read(), '')
        with open(os.path.join(outdir, 'ubtree0t-sF-stderr')) as f:
            self.assertEqual(f.read(), 'kaputt\n')

        # check outdir log
        with open(os.path.join(outdir, '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)

        # check for cruft in outdir
        # --no-built-binaries, we don't expect any debs
        files = [i for i in os.listdir(outdir) if not fnmatch.fnmatch(i, 'ubtree*-std*')]
        self.assertEqual(set(files), set(['log']))

    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_timeout_long_test(self):
        '''long-running test with custom timeouts

        This verifies that the right timeout is being used for tests.
        '''
        p = self.build_dsc('Tests: p\nDepends:\nRestrictions: build-needed',
                           {'p': '#!/bin/sh\necho start\nsleep 5\n./test_built\n'})

        (code, out, err) = self.adt_run(['-B', '--timeout-test=6',
                                         '--timeout-build=3',
                                         '--timeout-install=3', p])
        # test should not time out
        self.assertEqual(code, 0, err)
        self.assertNotIn('timeout', err)

        # should show test stdout
        self.assertIn('start\n', out)
        self.assertIn('built script OK\n', out)

    def test_timeout_long_build(self):
        '''long-running build with custom timeouts

        This verifies that the right timeout is being used for builds.
        '''
        p = self.build_src('Tests: p\nDepends:\nRestrictions: build-needed',
                           {'p': '#!/bin/sh\necho start\n\n./test_built\n'})

        # make build take 4s
        subprocess.check_call(['sed', '-i', '/^build:/ s/$/\\n\\tsleep 4/',
                               os.path.join(p, 'Makefile')])

        (code, out, err) = self.adt_run(['--timeout-test=1', '--timeout-build=5',
                                         '--timeout-install=3', '-B',
                                         '--unbuilt-tree', p])
        # should build package
        self.assertIn('dh build', err)

        # test should not time out
        self.assertEqual(code, 0, err)
        self.assertNotIn('timeout', err)

        # should show test stdout
        self.assertIn('start\n', out)
        self.assertIn('built script OK\n', out)

    def test_timeout_long_build_fail(self):
        '''long-running build times out

        This verifies that the right timeout is being used for builds.
        '''
        p = self.build_src('Tests: p\nDepends:\nRestrictions: build-needed',
                           {'p': '#!/bin/sh\necho start\n\n./test_built\n'})

        # make build take 6s
        subprocess.check_call(['sed', '-i', '/^build:/ s/$/\\n\\tsleep 6/',
                               os.path.join(p, 'Makefile')])

        (code, out, err) = self.adt_run(['--timeout-test=1', '--timeout-build=5',
                                         '--timeout-install=3', '-B',
                                         '--unbuilt-tree', p])
        # should start building package
        self.assertIn('dh build', err)

        # build should time out
        self.assertEqual(code, 16, err)
        self.assertIn("got `timeout', expected `ok...'", err)

        # should not start tests
        self.assertNotIn('start\n', 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
''')

    def test_logfile_success(self):
        '''--log-file option, success'''

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

        logfile = os.path.join(self.workdir, 'adt.log')
        (code, out, err) = self.adt_run(['--no-built-binaries',
                                         '--unbuilt-tree=' + p,
                                         '--log-file=' + logfile])
        #print('----- out ----\n%s\n----- err ----\n%s\n----' % (out, err))
        # test should succeed
        self.assertEqual(code, 0, err)

        with open(logfile) as f:
            log = f.read()
        self.assertIn('coreutils', out)
        self.assertIn('coreutils', log)
        self.assertRegex(out, 'tree0t-pass\s+PASS')
        self.assertRegex(log, 'tree0t-pass\s+PASS')

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

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

    def test_logfile_failure(self):
        '''--log-file option, failure'''

        p = self.build_src('Tests: nz\nDepends: coreutils\nRestrictions: build-needed\n',
                           {'nz': '#!/bin/sh\n./test_built\necho I am sick >&2\nexit 7'})

        logfile = os.path.join(self.workdir, 'adt.log')
        (code, out, err) = self.adt_run(['--no-built-binaries',
                                         '--unbuilt-tree=' + p,
                                         '--log-file=' + logfile])
        with open(logfile) as f:
            log = f.read()

        # test should fail
        self.assertEqual(code, 4)
        self.assertRegex(out, 'tree0t-nz\s+FAIL non-zero exit status 7')
        self.assertRegex(log, 'tree0t-nz\s+FAIL non-zero exit status 7')

        self.assertIn('processing dependency coreutils', err)
        self.assertIn('processing dependency coreutils', log)

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

        # should show test stdout
        self.assertIn('\nbuilt script OK\n', out)
        self.assertIn('\nbuilt script OK\n', log)
        # should show test stderr
        self.assertRegex(err, 'stderr [ -]+\nI am sick\n')
        self.assertRegex(log, 'stderr [ -]+\nI am sick\n')

    def test_dev_stdouterr_access(self):
        '''write to /dev/stdout and /dev/stderr in a test'''

        p = self.build_src('Tests: pass\nDepends:\nRestrictions: allow-stderr',
                           {'pass': '#!/bin/sh\necho I am fine >/dev/stdout\n'
                            'echo SomeDebug >/dev/stderr\n'})

        (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('I am fine\n', out)
        # should show test stderr
        self.assertIn('\nSomeDebug\n', err)

    def test_breaks_testbed(self):
        '''breaks-testbed restriction'''

        p = self.build_src('Tests: rambo\nDepends:\nRestrictions: needs-root breaks-testbed',
                           {'rambo': '#!/bin/sh\ntouch /var/tmp/zap\n'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--unbuilt-tree=' + p])
        # test should be skipped as null runner doesn't provide revert
        self.assertEqual(code, 10)
        self.assertRegex(out, 'rambo\s+SKIP Test breaks testbed')
        self.assertFalse(os.path.exists('/var/tmp/zap'))

    def test_unicode(self):
        '''Unicode test output'''

        p = self.build_src('Tests: se\nDepends:\n',
                           {'se': '#!/bin/sh\necho ‘a♩’; echo fancy ‴ʎɔuɐɟ″!>&2'})

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

        (code, out, err) = self.adt_run(['--no-built-binaries',
                                         '--unbuilt-tree=' + p,
                                         '--summary=' + summary])
        self.assertEqual(code, 4, err)
        self.assertRegex(out, 'ubtree0t-se\s+FAIL status: 0, stderr: fancy ‴ʎɔuɐɟ″!\n')

        # should show test stdout/stderr
        self.assertIn('‘a♩’\n', out)
        self.assertRegex(err, 'stderr [ -]+\nfancy ‴ʎɔuɐɟ″!\n')

        with open(summary, encoding='UTF-8') as f:
            self.assertEqual(f.read(), 'ubtree0t-se          FAIL status: 0, stderr: fancy ‴ʎɔuɐɟ″!\n')


@unittest.skipIf('cowdancer' in os.environ.get('LD_PRELOAD', ''),
                 'chroot tests do not work under cowdancer')
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', 'apt-cache']:
            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)
                if cmd == 'apt-get':
                    f.write('if [ "$1" = source ]; then cp -r /aptget-src $2-1; fi\n')
            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)

        self.assertNotIn('@@@@@@ test bed setup', 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)

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

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

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

        # should show test stderr separately (no real-time output for chroot)
        self.assertRegex(err, 'stderr [ -]+\nI am sick\n')
        self.assertNotIn('stdout', 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 needs-root',
                           {'pass': '#!/bin/sh\necho I am fine >&2\necho babble'})

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

        # should show test stdout/err
        self.assertRegex(err, 'stdout [ -]+\nbabble\n')
        self.assertRegex(err, 'stderr [ -]+\nI am fine\n')

    def test_fancy_deps(self):
        '''wrapped and versioned test dependencies'''

        p = self.build_src('''Tests: pass
Depends: fancypkg,
         coreutils | vanilla (>= 10),
         chocolate,
Restrictions: needs-root
''', {'pass': '#!/bin/sh\ntrue'})

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

        self.assertIn('processing dependency fancypkg\n', err)
        self.assertIn('processing dependency coreutils | vanilla (>= 10)\n', err)
        self.assertIn('processing dependency chocolate\n', err)
        self.assertRegex(err, 'fake-apt-get: .*install.*fancypkg coreutils chocolate')

    def test_build_deps(self):
        '''test depends on build dependencies'''

        p = self.build_src('''Tests: pass
Depends: @, testdep1, @builddeps@, testdep2
Restrictions: needs-root
''', {'pass': '#!/bin/sh\ntrue'})

        # add extra build dependencies to testpkg
        subprocess.check_call(['sed', '-i', '/^Build-Depends:/ s/:.*/: bdep1 , bdep2\\n'
                                            'Build-Depends-Indep: bdep3/',
                               os.path.join(p, 'debian', 'control')])

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

        self.assertIn('synthesised dependency testpkg\n', err)
        self.assertIn('processing dependency testdep1\n', err)
        self.assertIn('synthesised dependency bdep1\n', err)
        self.assertIn('synthesised dependency bdep2\n', err)
        self.assertIn('synthesised dependency bdep3\n', err)
        self.assertIn('processing dependency testdep2\n', err)
        self.assertRegex(err, 'fake-apt-get: .*install.*testpkg testdep1 bdep1 bdep2 bdep3 testdep2')

    def test_logfile(self):
        '''--log-file option'''

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

        logfile = os.path.join(self.workdir, 'adt.log')
        (code, out, err) = self.adt_run(['--no-built-binaries',
                                         '--unbuilt-tree=' + p,
                                         '--log-file=' + logfile],
                                        [self.chroot])
        with open(logfile) as f:
            log = f.read()

        # test should fail
        self.assertEqual(code, 4, err)
        self.assertRegex(out, 'tree0t-nz\s+FAIL non-zero exit status 7')
        self.assertRegex(log, 'tree0t-nz\s+FAIL non-zero exit status 7')

        self.assertIn('processing dependency coreutils', err)
        self.assertIn('processing dependency coreutils', log)

        # should show test stdout
        self.assertRegex(err, 'stdout [ -]+\nstatic script OK\n')
        self.assertRegex(log, 'stdout [ -]+\nstatic script OK\n')
        # should show test stderr
        self.assertRegex(err, 'stderr [ -]+\nI am sick\n')
        self.assertRegex(log, 'stderr [ -]+\nI am sick\n')

    def test_artifacts(self):
        '''tests producing additional artifacts'''

        p = self.build_src('Tests: pass\nDepends:\nRestrictions: needs-root\n',
                           {'pass': '#!/bin/sh -e\n[ -d "$ADT_ARTIFACTS" ]\n'
                                    'echo I am fine > $ADT_ARTIFACTS/health.txt\n'})

        outdir = os.path.join(self.workdir, 'out')

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

        # test should succeed
        self.assertEqual(code, 0, err)
        self.assertRegex(out, 'tree0t-pass\s+PASS', out)

        # check for cruft in output dir
        files = [f for f in os.listdir(outdir) if not fnmatch.fnmatch(f, 'ubtree*-std*')]
        self.assertEqual(set(files), set(['log', 'artifacts']))

        # check artifact
        with open(os.path.join(outdir, 'artifacts', 'health.txt')) as f:
            self.assertEqual(f.read(), 'I am fine\n')

    def test_slash_in_test_name(self):
        '''test names must not contain /'''

        p = self.build_src('Tests: pass subdir/p\nDepends:\nRestrictions: needs-root',
                           {'pass': '#!/bin/sh\ntrue'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--unbuilt-tree=' + p],
                                        [self.chroot])
        # invalid test gets skipped
        self.assertEqual(code, 2, err)
        self.assertRegex(out, 'subdir/p\s+SKIP test name may not contain /.* line 1')

        # valid test still gets run
        self.assertFalse(re.match('pass\s+SKIP', out), out)
        self.assertRegex(out, 'ubtree0t-pass\s+PASS', out)

    def test_breaks_testbed(self):
        '''breaks-testbed restriction'''

        p = self.build_src('Tests: rambo\nDepends:\nRestrictions: needs-root breaks-testbed',
                           {'rambo': '#!/bin/sh\ntouch /zap\n'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--unbuilt-tree=' + p],
                                        [self.chroot])
        # test should be skipped as chroot runner doesn't provide revert
        self.assertEqual(code, 10)
        self.assertRegex(out, 'rambo\s+SKIP Test breaks testbed')
        self.assertFalse(os.path.exists(os.path.join(self.chroot, 'zap')))

    def test_unicode(self):
        '''Unicode test output'''

        p = self.build_src('Tests: se\nDepends:\nRestrictions: needs-root\n',
                           {'se': '#!/bin/sh\necho ‘a♩’; echo fancy ‴ʎɔuɐɟ″!>&2'})

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

        (code, out, err) = self.adt_run(['--no-built-binaries',
                                         '--unbuilt-tree=' + p,
                                         '--summary=' + summary],
                                        [self.chroot])
        self.assertEqual(code, 4, err)
        self.assertRegex(out, 'ubtree0t-se\s+FAIL status: 0, stderr: fancy ‴ʎɔuɐɟ″!\n')

        # should show test stdout/stderr
        self.assertRegex(err, 'stdout [ -]+\n‘a♩’\n')
        self.assertRegex(err, 'stderr [ -]+\nfancy ‴ʎɔuɐɟ″!\n')

        with open(summary, encoding='UTF-8') as f:
            self.assertEqual(f.read(), 'ubtree0t-se          FAIL status: 0, stderr: fancy ‴ʎɔuɐɟ″!\n')

    def test_apt_source_no_restrictions(self):
        '''apt source, no build, no restrictions'''

        p = self.build_src('Tests: pass\nDepends:\nRestrictions: needs-root\n',
                           {'pass': '#!/bin/sh\necho I am fine\n'})
        # copy that into the chroot where fake apt-get source can find it
        shutil.copytree(p, os.path.join(self.chroot, 'aptget-src'))

        (code, out, err) = self.adt_run(['testpkg'], [self.chroot])
        # test should succeed
        self.assertEqual(code, 0, err)
        self.assertRegex(out, 'apt0t-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)

        self.assertRegex(err, 'fake-apt-get: source testpkg')

    def test_setup_commands_string(self):
        '''--setup-commands with command string'''

        p = self.build_src('Tests: pass\nDepends:\nRestrictions: needs-root\n',
                           {'pass': '#!/bin/sh -e\n[ -x /bin/cp_cp ]; cat /setup.log\n'})

        (code, out, err) = self.adt_run(['-B', '--unbuilt-tree=' + p,
                                         '--setup-commands', 'cp /bin/cp /bin/cp_cp; '
                                         'echo setup_success > /setup.log\n'
                                         'cp /bin/cp /bin/cp_cp'],
                                        [self.chroot])
        # test should succeed
        self.assertEqual(code, 0, err)
        self.assertRegex(out, 'tree0t-pass\s+PASS', out)

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

        self.assertIn('@@@@@@ test bed setup', err)

    def test_setup_commands_file(self):
        '''--setup-commands with command file'''

        p = self.build_src('Tests: pass\nDepends:\nRestrictions: needs-root\n',
                           {'pass': '#!/bin/sh -e\n[ -x /bin/cp_cp ]; cat /setup.log\n'})

        cmds = os.path.join(self.workdir, 'setup.sh')
        with open(cmds, 'w') as f:
            f.write('cp /bin/cp /bin/cp_cp\necho setup_success > /setup.log\n')
            f.flush()

        (code, out, err) = self.adt_run(['-B', '-d', '--unbuilt-tree=' + p,
                                         '--setup-commands', cmds],
                                        [self.chroot])
        # test should succeed
        self.assertEqual(code, 0, err)
        self.assertRegex(out, 'tree0t-pass\s+PASS', out)

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

        self.assertIn('@@@@@@ test bed setup', err)


@unittest.skipUnless('ADT_TEST_SCHROOT' in os.environ,
                     'Set $ADT_TEST_SCHROOT to an existing schroot')
class SchrootRunner(AdtTestCase):
    def __init__(self, *args, **kwargs):
        super(SchrootRunner, self).__init__('schroot', *args, **kwargs)
        self.schroot_name = os.environ.get('ADT_TEST_SCHROOT')

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

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

        (code, out, err) = self.adt_run(['-d', '--no-built-binaries', '--unbuilt-tree=' + p],
                                        [self.schroot_name])
        # test should succeed
        self.assertEqual(code, 0, err)
        self.assertIn('coreutils', out)
        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:\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.schroot_name])
        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)

        # 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.schroot_name])
        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)

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

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

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

        # should show test stderr separately (no real-time output for chroot)
        self.assertRegex(err, 'stderr [ -]+\nI am sick\n')
        self.assertNotIn('stdout', 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.schroot_name])
        self.assertEqual(code, 0, err)
        self.assertRegex(out, 'tree0t-pass\s+PASS', out)

        # should show test stdout/err
        self.assertRegex(err, 'stdout [ -]+\nbabble\n')
        self.assertRegex(err, 'stderr [ -]+\nI am fine\n')

    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"\necho GOOD'})

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

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

        # should show test stdout
        self.assertRegex(err, 'stdout [ -]+\nGOOD\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],
                                        [self.schroot_name])
        # test should succeed
        self.assertRegex(out, 'dsc0t-pass\s+PASS', out)
        self.assertEqual(code, 0)

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

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

    def test_tree_built_binaries(self):
        '''source tree, install built binaries'''

        # no "build-needed" restriction here, it should be built because we
        # need to install the package as a dependency (@)
        p = self.build_src('Tests: pass\n',
                           {'pass': '#!/bin/sh -e\n/usr/bin/test_built | grep -q "built script OK"\necho GOOD'})

        # add extra dependency to testpkg, to ensure autopkgtest does not stop
        # and ask for confirmation in apt-get
        subprocess.check_call(['sed', '-i', '/^Depends:/ s/$/, aspell-doc/',
                               os.path.join(p, 'debian', 'control')])

        (code, out, err) = self.adt_run(['--unbuilt-tree=' + p], [self.schroot_name])

        # should build and install package
        self.assertIn('dh build', err)
        self.assertRegex(out, 'ubtree0t-pass\s+PASS', out)
        self.assertIn('Unpacking testpkg', out)
        self.assertIn('binaries/./testpkg.deb', out)
        self.assertIn('Unpacking aspell-doc', out)
        # does not install Recommends by default
        self.assertNotIn('Unpacking cpp-doc', out)
        self.assertNotIn('cpp-doc', err)

        # test should succeed
        self.assertRegex(out, 'ubtree0t-pass\s+PASS', out)
        self.assertEqual(code, 0)

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

    def test_explicit_binary_for_test(self):
        '''--binary for test'''

        p = self.build_src('Tests: pass\n',
                           {'pass': '#!/bin/sh -e\n/usr/bin/test_built'})

        # build the package
        subprocess.check_call(['dpkg-buildpackage', '-b', '-us', '-uc', '-tc'],
                              stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                              cwd=p)
        deb = os.path.join(os.path.dirname(p), 'testpkg_1_all.deb')
        self.assertTrue(os.path.exists(deb))
        # destroy debian/rules, to ensure it does not try to build the package
        # again
        with open(os.path.join(p, 'debian', 'rules'), 'w') as f:
            f.write('bwhahahahano!')

        (code, out, err) = self.adt_run(['-d', '-B', '--binary', deb, '--unbuilt-tree=' + p],
                                        [self.schroot_name])

        # should install package
        self.assertNotIn('dh build', err)
        self.assertIn('Unpacking testpkg', out)
        self.assertIn('binaries/./testpkg.deb', out)

        # test should succeed
        self.assertRegex(out, '-pass\s+PASS', out)
        self.assertEqual(code, 0)

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

        # our deb should still be there
        self.assertTrue(os.path.exists(deb))

    def test_logfile(self):
        '''--log-file option'''

        p = self.build_src('Tests: nz\nDepends: coreutils\nRestrictions: build-needed\n',
                           {'nz': '#!/bin/sh\n./test_built\necho I am sick >&2\nexit 7'})

        logfile = os.path.join(self.workdir, 'adt.log')
        (code, out, err) = self.adt_run(['--no-built-binaries',
                                         '--unbuilt-tree=' + p,
                                         '--log-file=' + logfile],
                                        [self.schroot_name])
        with open(logfile) as f:
            log = f.read()

        # test should fail
        self.assertEqual(code, 4)
        self.assertRegex(out, 'tree0t-nz\s+FAIL non-zero exit status 7')
        self.assertRegex(log, 'tree0t-nz\s+FAIL non-zero exit status 7')

        self.assertIn('processing dependency coreutils', err)
        self.assertIn('processing dependency coreutils', log)

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

        # should show test stdout
        self.assertRegex(err, 'stdout [ -]+\nbuilt script OK\n')
        self.assertRegex(log, 'stdout [ -]+\nbuilt script OK\n')
        # should show test stderr
        self.assertRegex(err, 'stderr [ -]+\nI am sick\n')
        self.assertRegex(log, 'stderr [ -]+\nI am sick\n')

    def test_tree_output_dir(self):
        '''source tree, explicit --output-dir'''

        p = self.build_src('Tests: ok broken\n',
                           {'ok': '#!/bin/sh\n/usr/bin/test_built',
                            'broken': '#!/bin/sh\necho kaputt >&2'})

        outdir = os.path.join(self.workdir, 'out')

        (code, out, err) = self.adt_run(['--unbuilt-tree=' + p,
                                         '--output-dir=' + outdir],
                                        [self.schroot_name])

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

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

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

        # check outdir test stdout/err
        with open(os.path.join(outdir, 'ubtree0t-ok-stdout')) as f:
            self.assertEqual(f.read(), 'built script OK\n')
        with open(os.path.join(outdir, 'ubtree0t-ok-stderr')) as f:
            self.assertEqual(f.read(), '')
        with open(os.path.join(outdir, 'ubtree0t-broken-stdout')) as f:
            self.assertEqual(f.read(), '')
        with open(os.path.join(outdir, 'ubtree0t-broken-stderr')) as f:
            self.assertEqual(f.read(), 'kaputt\n')

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

        # check binaries
        bins = os.listdir(os.path.join(outdir, 'binaries'))
        self.assertEqual(set(bins),
                         set(['Release.gpg', 'archive-key.pgp', 'Release',
                              'Packages.gz', 'Packages', 'testpkg.deb']))

        # check for cruft in outdir
        files = [i for i in os.listdir(outdir) if not fnmatch.fnmatch(i, 'ubtree*-std*')]
        self.assertEqual(set(files), set(['log', 'binaries']))

    def test_user(self):
        '''--user option'''

        p = self.build_src('Tests: t\nDepends: aspell-doc\n',
                           {'t': '''#!/bin/sh -e
                                    echo world > $TMPDIR/hello.txt
                                    cat $TMPDIR/hello.txt
                                    whoami'''})

        (code, out, err) = self.adt_run(['--no-built-binaries',
                                         '--unbuilt-tree=' + p,
                                         '--user=nobody'],
                                        [self.schroot_name])
        self.assertEqual(code, 0, err)

        # should install dependencies
        self.assertIn('processing dependency aspell-doc', err)
        self.assertIn('Unpacking aspell-doc', out)

        # test should succeed
        self.assertRegex(out, 'tree0t-t\s+PASS', out)

        # has output from cat and whoami
        self.assertRegex(err, 'stdout [ -]+\nworld\nnobody\n')

    def test_user_needs_root(self):
        '''--user option with needs-root restriction'''

        p = self.build_src('Tests: t\nDepends: aspell-doc\nRestrictions: needs-root',
                           {'t': '''#!/bin/sh -e
                                    echo world > $TMPDIR/hello.txt
                                    cat $TMPDIR/hello.txt
                                    whoami'''})

        (code, out, err) = self.adt_run(['--no-built-binaries',
                                         '--unbuilt-tree=' + p,
                                         '--user=nobody'],
                                        [self.schroot_name])
        self.assertEqual(code, 0, err)

        # should install dependencies
        self.assertIn('processing dependency aspell-doc', err)
        self.assertIn('Unpacking aspell-doc', out)

        # test should succeed
        self.assertRegex(out, 'tree0t-t\s+PASS', out)

        # has output from cat and whoami
        self.assertRegex(err, 'stdout [ -]+\nworld\nroot\n')

    def test_breaks_testbed(self):
        '''breaks-testbed restriction'''

        p = self.build_src('Tests: rambo\nDepends:\nRestrictions: needs-root breaks-testbed',
                           {'rambo': '#!/bin/sh\ntouch /zap\n'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--unbuilt-tree=' + p],
                                        [self.schroot_name])
        # test should be skipped as chroot runner doesn't provide revert
        self.assertEqual(code, 10)
        self.assertRegex(out, 'rambo\s+SKIP Test breaks testbed')


@unittest.skipUnless('ADT_TEST_LXC' in os.environ,
                     'Set $ADT_TEST_LXC to an existing ubuntu-cloud container')
class LxcRunner(AdtTestCase):
    def __init__(self, *args, **kwargs):
        super(LxcRunner, self).__init__('lxc', *args, **kwargs)
        self.container_name = os.environ.get('ADT_TEST_LXC')

    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],
                                        ['--ephemeral', self.container_name])
        # test should succeed
        self.assertEqual(code, 0, err)
        self.assertIn('coreutils', out)
        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:\n',
                           {'nz': '#!/bin/sh\necho I am sick\nexit 7'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--unbuilt-tree=' + p],
                                        ['--ephemeral', self.container_name])
        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)

        # 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:\n',
                           {'se': '#!/bin/sh\necho I am sick >&2'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--unbuilt-tree=' + p],
                                        ['--ephemeral', self.container_name])
        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)

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

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

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

        # should show test stderr separately (no real-time output for chroot)
        self.assertRegex(err, 'stderr [ -]+\nI am sick\n')
        self.assertNotIn('stdout', 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],
                                        ['--ephemeral', self.container_name])
        self.assertEqual(code, 0, err)
        self.assertRegex(out, 'tree0t-pass\s+PASS', out)

        # should show test stdout/err
        self.assertRegex(err, 'stdout [ -]+\nbabble\n')
        self.assertRegex(err, 'stderr [ -]+\nI am fine\n')

    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"\necho GOOD'})

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

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

        # should show test stdout
        self.assertRegex(err, 'stdout [ -]+\nGOOD\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],
                                        ['--ephemeral', self.container_name])
        # test should succeed
        self.assertRegex(out, 'dsc0t-pass\s+PASS', out)
        self.assertEqual(code, 0)

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

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

    def test_tree_built_binaries(self):
        '''source tree, install built binaries'''

        # no "build-needed" restriction here, it should be built because we
        # need to install the package as a dependency (@)
        p = self.build_src('Tests: pass\n',
                           {'pass': '#!/bin/sh -e\n/usr/bin/test_built | grep -q "built script OK"\necho GOOD'})

        # add extra dependency to testpkg, to ensure autopkgtest does not stop
        # and ask for confirmation in apt-get
        subprocess.check_call(['sed', '-i', '/^Depends:/ s/$/, aspell-doc/',
                               os.path.join(p, 'debian', 'control')])

        (code, out, err) = self.adt_run(['--unbuilt-tree=' + p],
                                        ['--ephemeral', self.container_name])

        # should build and install package
        self.assertIn('dh build', err)
        self.assertRegex(out, 'ubtree0t-pass\s+PASS', out)
        self.assertRegex(out, 'testpkg .*binaries/./testpkg.deb')
        self.assertRegex(out, 'aspell-doc .*\.\.\./aspell-doc_\d.*_all.deb\)')

        # test should succeed
        self.assertRegex(out, 'ubtree0t-pass\s+PASS', out)
        self.assertEqual(code, 0)

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

    def test_tree_clone(self):
        '''source tree, no --ephemeral option (using clone)'''

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

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

        # should show test stderr separately (no real-time output for chroot)
        self.assertRegex(err, 'stderr [ -]+\nI am sick\n')
        self.assertNotIn('stdout', err)

    def test_breaks_testbed(self):
        '''breaks-testbed restriction'''

        p = self.build_src('Tests: zap boom\nDepends:\nRestrictions: needs-root breaks-testbed',
                           {'zap': '#!/bin/sh\ntouch /zap\n',
                            'boom': '#!/bin/sh\n[ ! -e /zap ]; touch /boom'})

        (code, out, err) = self.adt_run(['--no-built-binaries', '--unbuilt-tree=' + p, '-d'],
                                        ['--ephemeral', self.container_name])
        # both tests should run; the second one (boom) should not see the
        # effect of the first one (/zap existing)
        self.assertEqual(code, 0)
        self.assertRegex(out, 'ubtree0t-zap\s+PASS')
        self.assertRegex(out, 'ubtree0t-boom\s+PASS')

    def test_setup_commands(self):
        '''--setup-commands'''

        p = self.build_src('Tests: t1 t2\nDepends:\n',
                           {'t1': '#!/bin/sh -e\ncat /setup.log; echo t1ok\n',
                            't2': '#!/bin/sh -e\ncat /setup.log; echo t2ok\n'})

        (code, out, err) = self.adt_run(['-B', '-d', p + '//',
                                         '--setup-commands', 'echo setupok >> /setup.log'],
                                        ['--ephemeral', self.container_name])

        # test should succeed
        self.assertEqual(code, 0, err)
        self.assertRegex(out, 'tree0t-t1\s+PASS', out)
        self.assertRegex(out, 'tree0t-t2\s+PASS', out)

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

        self.assertIn('@@@@@@ test bed setup', 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))
