#!/usr/bin/python3

import os
import sys
import unittest
import tempfile
import textwrap
import shutil
import io
import subprocess
from contextlib import contextmanager

from unittest.mock import patch

test_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(1, os.path.join(os.path.dirname(test_dir), 'lib'))

import adtlog
import testdesc


have_autodep8 = subprocess.call(['sh', '-ec', 'command -v autodep8'], stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE) == 0


class TestHelper(unittest.TestCase):
    @contextmanager
    def todo(self):
        try:
            yield self.subTest('TODO')
        except AssertionError as e:
            adtlog.warning(str(e))
        else:
            adtlog.warning('Assertion did not fail, has the bug been fixed?')


class Rfc822(TestHelper):
    def test_control(self):
        '''Parse a debian/control like file'''

        control = tempfile.NamedTemporaryFile(prefix='control.')
        control.write('''Source: foo
Maintainer: Üñïcøδ€ <u@x.com>
Build-Depends: bd1, # moo
  bd2,
  bd3,
XS-Testsuite: autopkgtest
'''.encode())
        control.flush()
        parser = testdesc.parse_rfc822(control.name)
        r = parser.__next__()
        self.assertRaises(StopIteration, parser.__next__)
        control.close()

        self.assertEqual(r['Source'], 'foo')
        self.assertEqual(r['Xs-testsuite'], 'autopkgtest')
        self.assertEqual(r['Maintainer'], 'Üñïcøδ€ <u@x.com>')
        self.assertEqual(r['Build-depends'], 'bd1, bd2, bd3,')

    def test_dsc(self):
        '''Parse a signed dsc file'''

        control = tempfile.NamedTemporaryFile(prefix='dsc.')
        control.write('''-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

Format: 3.0 (quilt)
Source: foo
Binary: foo-bin, foo-doc
Package-List:
 foo-bin deb utils optional arch=any
 foo-doc deb doc extra arch=all
Files:
 deadbeef 10000 foo_1.orig.tar.gz
 11111111 1000 foo_1-1.debian.tar.xz

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1

BloB11
-----END PGP SIGNATURE-----

'''.encode())
        control.flush()
        parser = testdesc.parse_rfc822(control.name)
        r = parser.__next__()
        self.assertRaises(StopIteration, parser.__next__)
        control.close()

        self.assertEqual(r['Format'], '3.0 (quilt)')
        self.assertEqual(r['Source'], 'foo')
        self.assertEqual(r['Binary'], 'foo-bin, foo-doc')
        self.assertEqual(r['Package-list'], ' foo-bin deb utils optional arch=any'
                         ' foo-doc deb doc extra arch=all')
        self.assertEqual(r['Files'], ' deadbeef 10000 foo_1.orig.tar.gz'
                         ' 11111111 1000 foo_1-1.debian.tar.xz')

    def test_invalid(self):
        '''Parse an invalid file'''

        control = tempfile.NamedTemporaryFile(prefix='bogus.')
        control.write('''Bo Gus: something
muhaha'''.encode())
        control.flush()
        parser = testdesc.parse_rfc822(control.name)
        self.assertRaises(StopIteration, parser.__next__)
        control.close()


class Test(TestHelper):
    def test_valid_path(self):
        '''valid Test instantiation with path'''

        t = testdesc.Test('foo', 'tests/do_foo', None, ['needs-root'],
                          ['unknown_feature'], ['coreutils >= 7'], [])
        self.assertEqual(t.name, 'foo')
        self.assertEqual(t.path, 'tests/do_foo')
        self.assertEqual(t.command, None)
        self.assertEqual(t.result, None)

    def test_valid_command(self):
        '''valid Test instantiation with command'''

        t = testdesc.Test('foo', None, 'echo hi', ['needs-root'],
                          ['unknown_feature'], ['coreutils >= 7'], [])
        self.assertEqual(t.name, 'foo')
        self.assertEqual(t.path, None)
        self.assertEqual(t.command, 'echo hi')
        self.assertEqual(t.result, None)

    def test_invalid_name(self):
        '''Test with invalid name'''

        with self.assertRaises(testdesc.Unsupported) as cm:
            testdesc.Test('foo/bar', 'do_foo', None, [], [], [], [])
        self.assertIn('may not contain /', str(cm.exception))

    def test_unknown_restriction(self):
        '''Test with unknown restriction'''

        testdesc.Test('foo', 'tests/do_foo', None, ['needs-red'], [], [], [])

    def test_neither_path_nor_command(self):
        '''Test without path nor command'''

        with self.assertRaises(testdesc.InvalidControl) as cm:
            testdesc.Test('foo', None, None, [], [], [], [])
        self.assertIn('either path or command', str(cm.exception))

    def test_both_path_and_command(self):
        '''Test with path and command'''

        with self.assertRaises(testdesc.InvalidControl) as cm:
            testdesc.Test('foo', 'do_foo', 'echo hi', [], [], [], [])
        self.assertIn('either path or command', str(cm.exception))

    def test_capabilities_compat(self):
        '''Test compatibility with testbed capabilities'''

        t = testdesc.Test('foo', 'tests/do_foo', None,
                          ['needs-root', 'isolation-container'], [], [], [])

        self.assertRaises(testdesc.Unsupported,
                          t.check_testbed_compat, ['isolation-container'])
        self.assertRaises(testdesc.Unsupported,
                          t.check_testbed_compat, ['root-on-testbed'])
        t.check_testbed_compat(['isolation-container', 'root-on-testbed'])
        self.assertRaises(testdesc.Unsupported,
                          t.check_testbed_compat, ['needs-quantum-computer'])
        t.check_testbed_compat([],
                               ignore_restrictions=['needs-root',
                                                    'isolation-container'])


class Debian(TestHelper):
    def setUp(self):
        self.pkgdir = tempfile.mkdtemp(prefix='testdesc.')
        os.makedirs(os.path.join(self.pkgdir, 'debian', 'tests'))
        self.addCleanup(shutil.rmtree, self.pkgdir)

    def call_parse(self, testcontrol, pkgcontrol=None, caps=[], test_arch_is_foreign=False):
        if testcontrol:
            with open(os.path.join(self.pkgdir, 'debian', 'tests', 'control'), 'w', encoding='UTF-8') as f:
                f.write(testcontrol)
        if pkgcontrol:
            with open(os.path.join(self.pkgdir, 'debian', 'control'), 'w', encoding='UTF-8') as f:
                f.write(pkgcontrol)
        return testdesc.parse_debian_source(self.pkgdir, caps, 'amd64', test_arch_is_foreign)

    def test_bad_recommends(self):
        with open(os.path.join(self.pkgdir, 'debian', 'control'), 'w', encoding='UTF-8') as f:
            f.write('''
Source: bla

Package: bli
Architecture: all
Recommends: ${package:one}, two (<= 10) <!nocheck>, three (<= ${ver:three}~), four ( <= 4 )
        ''')
        (ts, skipped) = self.call_parse(
            'Tests: one\nDepends: @recommends@')
        self.assertEqual(ts[0].depends, [['two (<= 10) <!nocheck>'], ['three'], ['four ( <= 4 )']])
        self.assertEqual(ts[0].package_under_test_depends, [])

    def test_no_control(self):
        '''no test control file'''

        (ts, skipped) = self.call_parse(None, 'Source: foo\n')
        self.assertEqual(ts, [])
        self.assertFalse(skipped)

    def test_single(self):
        '''single test, simplest possible'''

        (ts, skipped) = self.call_parse('Tests: one\nDepends:')
        self.assertEqual(len(ts), 1)
        t = ts[0]
        self.assertEqual(t.name, 'one')
        self.assertEqual(t.path, 'debian/tests/one')
        self.assertEqual(t.command, None)
        self.assertEqual(t.restrictions, set())
        self.assertEqual(t.features, set())
        self.assertEqual(t.depends, [])
        self.assertEqual(t.package_under_test_depends, [])
        self.assertFalse(skipped)

    def test_default_depends(self):
        '''default Depends: is @'''

        (ts, skipped) = self.call_parse(
            'Tests: t1 t2',
            'Source: nums\n\nPackage: one\nArchitecture: any\n\n'
            'Package: two\nPackage-Type: deb\nArchitecture: all\n\n'
            'Package: two-udeb\nXC-Package-Type: udeb\nArchitecture: any\n\n'
            'Package: three-udeb\nPackage-Type: udeb\nArchitecture: any')
        self.assertEqual(len(ts), 2)
        self.assertEqual(ts[0].name, 't1')
        self.assertEqual(ts[0].path, 'debian/tests/t1')
        self.assertEqual(ts[1].name, 't2')
        self.assertEqual(ts[1].path, 'debian/tests/t2')
        for t in ts:
            self.assertEqual(t.restrictions, set())
            self.assertEqual(t.features, set())
            self.assertEqual(t.depends, [['one'], ['two']])
            self.assertEqual(t.package_under_test_depends, [['one'], ['two']])
        self.assertFalse(skipped)

    def test_default_depends_foreign_test(self):
        '''default Depends: is @, foreign arch test'''

        (ts, skipped) = self.call_parse(
            'Tests: t1 t2',
            'Source: nums\n\nPackage: one\nArchitecture: any\n\n'
            'Package: two\nPackage-Type: deb\nArchitecture: all\n\n'
            'Package: two-udeb\nXC-Package-Type: udeb\nArchitecture: any\n\n'
            'Package: three-udeb\nPackage-Type: udeb\nArchitecture: any',
            test_arch_is_foreign=True,
        )
        self.assertEqual(len(ts), 2)
        self.assertEqual(ts[0].name, 't1')
        self.assertEqual(ts[0].path, 'debian/tests/t1')
        self.assertEqual(ts[1].name, 't2')
        self.assertEqual(ts[1].path, 'debian/tests/t2')
        for t in ts:
            self.assertEqual(t.restrictions, set())
            self.assertEqual(t.features, set())
            self.assertEqual(t.depends, [['one:amd64'], ['two']])
            self.assertEqual(t.package_under_test_depends, [['one:amd64'], ['two']])
        self.assertFalse(skipped)

    def test_arch_specific(self):
        '''@ expansion with architecture specific binaries'''

        (ts, skipped) = self.call_parse(
            'Tests: t',
            'Source: nums\n\nPackage: one\nArchitecture: any\n\n'
            'Package: two\nArchitecture: linux-any any-uclibc-linux-mipsr6el\n\n'
            'Package: three\nArchitecture: s390 darwin-ppc64')
        self.assertEqual(len(ts), 1)
        self.assertEqual(ts[0].name, 't')
        self.assertEqual(ts[0].path, 'debian/tests/t')
        self.assertEqual(ts[0].restrictions, set())
        self.assertEqual(ts[0].features, set())
        self.assertEqual(ts[0].depends, [['one'], ['two']])
        self.assertEqual(ts[0].package_under_test_depends, [['one'], ['two']])
        self.assertFalse(skipped)

    def test_arch_specific_foreign_test(self):
        '''@ expansion with architecture specific binaries, foreign arch test'''

        (ts, skipped) = self.call_parse(
            'Tests: t',
            'Source: nums\n\nPackage: one\nArchitecture: any\n\n'
            'Package: two\nArchitecture: linux-any\n\n'
            'Package: three\nArchitecture: s390 darwin-ppc64',
            test_arch_is_foreign=True,
        )
        self.assertEqual(len(ts), 1)
        self.assertEqual(ts[0].name, 't')
        self.assertEqual(ts[0].path, 'debian/tests/t')
        self.assertEqual(ts[0].restrictions, set())
        self.assertEqual(ts[0].features, set())
        self.assertEqual(ts[0].depends, [['one:amd64'], ['two:amd64']])
        self.assertEqual(ts[0].package_under_test_depends, [['one:amd64'], ['two:amd64']])
        self.assertFalse(skipped)

    def test_depends_own_alternatives(self):
        '''Depends: has alternatives listing own packages'''

        (ts, skipped) = self.call_parse(
            textwrap.dedent(
                '''\
                Tests: t1
                Depends: one [linux-any] | coreutils (<< 273.15) | coreutils (>= 1e100)

                Tests: t2
                Depends: coreutils (= 0) [linux-any] | one | one

                Tests: t3
                Depends: one [linux-any] | two | one

                Tests: t4
                Depends: one
                '''
            ),
            textwrap.dedent(
                '''\
                Source: nums

                Package: one
                Architecture: any

                Package: two
                Package-Type: deb
                Architecture: all
                '''
            ),
        )
        self.assertEqual(len(ts), 4)
        self.assertEqual(ts[0].name, 't1')
        self.assertEqual(ts[0].path, 'debian/tests/t1')
        self.assertEqual(ts[1].name, 't2')
        self.assertEqual(ts[1].path, 'debian/tests/t2')
        self.assertEqual(ts[2].name, 't3')
        self.assertEqual(ts[2].path, 'debian/tests/t3')
        self.assertEqual(ts[3].name, 't4')
        self.assertEqual(ts[3].path, 'debian/tests/t4')

        t = ts[0]
        self.assertEqual(t.restrictions, set())
        self.assertEqual(t.features, set())
        self.assertEqual(t.depends, [['one [linux-any]', 'coreutils (<< 273.15)', 'coreutils (>= 1e100)']])
        self.assertEqual(t.package_under_test_depends, [])

        t = ts[1]
        self.assertEqual(t.restrictions, set())
        self.assertEqual(t.features, set())
        self.assertEqual(t.depends, [['coreutils (= 0) [linux-any]', 'one', 'one']])
        self.assertEqual(t.package_under_test_depends, [])

        t = ts[2]
        self.assertEqual(t.restrictions, set())
        self.assertEqual(t.features, set())
        self.assertEqual(t.depends, [['one [linux-any]', 'two', 'one']])
        self.assertEqual(t.package_under_test_depends, [['one', 'two', 'one']])

        t = ts[3]
        self.assertEqual(t.restrictions, set())
        self.assertEqual(t.features, set())
        self.assertEqual(t.depends, [['one']])
        self.assertEqual(t.package_under_test_depends, [['one']])

        self.assertFalse(skipped)

    def test_depends_own_alternatives_foreign_test(self):
        '''Depends: has alternatives listing own packages, foreign arch test'''

        (ts, skipped) = self.call_parse(
            textwrap.dedent(
                '''\
                Tests: t1
                Depends: one [linux-any] | coreutils (<< 273.15) | coreutils (>= 1e100)

                Tests: t2
                Depends: coreutils (= 0) [linux-any] | one | one

                Tests: t3
                Depends: one [linux-any] | two | one

                Tests: t4
                Depends: one
                '''
            ),
            textwrap.dedent(
                '''\
                Source: nums

                Package: one
                Architecture: any

                Package: two
                Package-Type: deb
                Architecture: all
                '''
            ),
            test_arch_is_foreign=True,
        )
        self.assertEqual(len(ts), 4)
        self.assertEqual(ts[0].name, 't1')
        self.assertEqual(ts[0].path, 'debian/tests/t1')
        self.assertEqual(ts[1].name, 't2')
        self.assertEqual(ts[1].path, 'debian/tests/t2')
        self.assertEqual(ts[2].name, 't3')
        self.assertEqual(ts[2].path, 'debian/tests/t3')
        self.assertEqual(ts[3].name, 't4')
        self.assertEqual(ts[3].path, 'debian/tests/t4')

        t = ts[0]
        self.assertEqual(t.restrictions, set())
        self.assertEqual(t.features, set())
        self.assertEqual(t.depends, [['one:amd64 [linux-any]', 'coreutils (<< 273.15)', 'coreutils (>= 1e100)']])
        self.assertEqual(t.package_under_test_depends, [])

        t = ts[1]
        self.assertEqual(t.restrictions, set())
        self.assertEqual(t.features, set())
        self.assertEqual(t.depends, [['coreutils (= 0) [linux-any]', 'one:amd64', 'one:amd64']])
        self.assertEqual(t.package_under_test_depends, [])

        t = ts[2]
        self.assertEqual(t.restrictions, set())
        self.assertEqual(t.features, set())
        self.assertEqual(t.depends, [['one:amd64 [linux-any]', 'two', 'one:amd64']])
        with self.todo():
            # TODO: package_under_test_depends is empty, but why isn't it this?
            self.assertEqual(t.package_under_test_depends, [['one:amd64', 'two', 'one:amd64']])

        t = ts[3]
        self.assertEqual(t.restrictions, set())
        self.assertEqual(t.features, set())
        self.assertEqual(t.depends, [['one:amd64']])
        with self.todo():
            # TODO: package_under_test_depends is empty, but why isn't it this?
            self.assertEqual(t.package_under_test_depends, [['one:amd64', 'two', 'one:amd64']])

        self.assertFalse(skipped)

    def test_test_name_feature(self):
        '''Features: test-name=foobar'''

        (ts, skipped) = self.call_parse(
            'Test-Command: t1\n'
            'Depends: foo\n'
            'Features: test-name=foobar')
        self.assertEqual(len(ts), 1)
        self.assertEqual(ts[0].features, {'test-name=foobar'})
        self.assertEqual(ts[0].name, 'foobar')
        self.assertFalse(skipped)

    def test_test_name_feature_too_many(self, *args):
        '''only one test-name= feature is allowed'''

        with self.assertRaises(testdesc.InvalidControl) as cm:
            self.call_parse(
                'Test-Command: t1\n'
                'Depends: foo\n'
                'Features: test-name=foo,test-name=bar')
        self.assertEqual(
            str(cm.exception),
            'InvalidControl test *: only one test-name feature allowed')

    def test_test_name_feature_with_other_features(self):
        '''Features: test-name=foobar, blue'''

        (ts, skipped) = self.call_parse(
            'Test-Command: t1\n'
            'Depends: foo\n'
            'Features: test-name=foo,blue')
        self.assertEqual(len(ts), 1)
        self.assertEqual(ts[0].features, {'test-name=foo', 'blue'})
        self.assertFalse(skipped)

    def test_test_name_missing_name(self, *args):
        '''Features: test-name'''

        with self.assertRaises(testdesc.InvalidControl) as cm:
            self.call_parse(
                'Test-Command: t1\n'
                'Depends: foo\n'
                'Features: test-name')
        self.assertEqual(
            str(cm.exception),
            'InvalidControl test *: test-name feature with no argument')

    def test_test_name_incompatible_with_tests(self, *args):
        '''Tests: with Features: test-name=foo'''

        with self.assertRaises(testdesc.InvalidControl) as cm:
            self.call_parse(
                'Tests: t1\n'
                'Depends: foo\n'
                'Features: test-name=foo')
        self.assertEqual(
            str(cm.exception),
            'InvalidControl test *: test-name feature incompatible with Tests')

    def test_known_restrictions(self):
        '''known restrictions'''

        (ts, skipped) = self.call_parse(
            'Tests: t1 t2\nDepends: foo\nRestrictions: build-needed allow-stderr\nFeatures: blue\n\n'
            'Tests: three\nDepends:\nRestrictions: needs-recommends')
        self.assertEqual(len(ts), 3)

        self.assertEqual(ts[0].name, 't1')
        self.assertEqual(ts[0].restrictions, {'build-needed', 'allow-stderr'})
        self.assertEqual(ts[0].features, {'blue'})
        self.assertEqual(ts[0].depends, [['foo']])
        self.assertEqual(ts[0].package_under_test_depends, [])

        self.assertEqual(ts[1].name, 't2')
        self.assertEqual(ts[1].restrictions, {'build-needed', 'allow-stderr'})
        self.assertEqual(ts[1].features, {'blue'})
        self.assertEqual(ts[1].depends, [['foo']])
        self.assertEqual(ts[1].package_under_test_depends, [])

        self.assertEqual(ts[2].name, 'three')
        self.assertEqual(ts[2].path, 'debian/tests/three')
        self.assertEqual(ts[2].restrictions, {'needs-recommends'})
        self.assertEqual(ts[2].features, set())
        self.assertEqual(ts[2].depends, [])
        self.assertEqual(ts[2].package_under_test_depends, [])

        self.assertFalse(skipped)

    @patch('adtlog.report')
    def test_unknown_restriction(self, *args):
        '''unknown restriction'''

        (ts, skipped) = self.call_parse('Tests: t\nDepends:\nRestrictions: explodes-spontaneously')
        self.assertEqual(ts, [])
        self.assertTrue(skipped)
        adtlog.report.assert_called_once_with('t', 'SKIP unknown restriction explodes-spontaneously')

    @patch('adtlog.report')
    def test_unknown_field(self, *args):
        '''unknown field'''

        (ts, skipped) = self.call_parse('Tests: s\nFuture: quantum\n\nTests: t\nDepends:')
        self.assertEqual(len(ts), 1)
        self.assertEqual(ts[0].name, 't')
        self.assertTrue(skipped)
        adtlog.report.assert_called_once_with(
            's', 'SKIP unknown field Future')

    def test_invalid_control(self):
        '''invalid control file'''

        # no tests field
        with self.assertRaises(testdesc.InvalidControl) as cm:
            self.call_parse('Depends:')
        self.assertIn('missing "Tests"', str(cm.exception))

    def test_invalid_control_empty_test(self):
        '''another invalid control file'''

        # empty tests field
        with self.assertRaises(testdesc.InvalidControl) as cm:
            self.call_parse('Tests:')
        self.assertIn('"Tests" field is empty', str(cm.exception))

    def test_tests_dir(self):
        '''non-standard Tests-Directory'''

        (ts, skipped) = self.call_parse(
            'Tests: t1\nDepends:\nTests-Directory: src/checks\n\n'
            'Tests: t2 t3\nDepends:\nTests-Directory: lib/t')

        self.assertEqual(len(ts), 3)
        self.assertEqual(ts[0].path, 'src/checks/t1')
        self.assertEqual(ts[1].path, 'lib/t/t2')
        self.assertEqual(ts[2].path, 'lib/t/t3')
        self.assertFalse(skipped)

    def test_builddeps(self):
        '''@builddeps@ expansion'''

        (ts, skipped) = self.call_parse(
            'Tests: t\nDepends: @, @builddeps@, foo (>= 7)',
            'Source: nums\nBuild-Depends: bd1, bd2 [armhf], bd3:native (>= 7) | bd4 [linux-any]\n'
            'Build-Depends-Indep: bdi1, bdi2 [amd64]\n'
            'Build-Depends-Arch: bda1, bda2 [amd64]\n'
            '\n'
            'Package: one\nArchitecture: any')
        self.assertEqual(
            ts[0].depends,
            [
                ['one'],
                ['bd1'],
                ['bd2 [armhf]'],
                ['bd3:native (>= 7)', 'bd4 [linux-any]'],
                ['bdi1'],
                ['bdi2 [amd64]'],
                ['bda1'],
                ['bda2 [amd64]'],
                ['build-essential'],
                ['foo (>= 7)'],
            ],
        )
        self.assertEqual(ts[0].package_under_test_depends, [['one']])
        self.assertFalse(skipped)

    def test_builddeps_foreign_test(self):
        '''@builddeps@ expansion, foreign arch test'''

        (ts, skipped) = self.call_parse(
            'Tests: t\nDepends: @, @builddeps@, foo (>= 7)',
            'Source: nums\nBuild-Depends: bd1, bd2 [armhf], bd3:native (>= 7) | bd4 [linux-any]\n'
            'Build-Depends-Indep: bdi1, bdi2 [amd64]\n'
            'Build-Depends-Arch: bda1, bda2 [amd64]\n'
            '\n'
            'Package: one\nArchitecture: any',
            test_arch_is_foreign=True,
        )
        self.assertEqual(ts[0].depends, [['one:amd64'], ['bd1'], ['bd2 [armhf]'], ['bd3:native (>= 7)', 'bd4 [linux-any]'],
                                         ['bdi1'], ['bdi2 [amd64]'], ['bda1'], ['bda2 [amd64]'], ['foo (>= 7)'],
                                         ['build-essential:native'], ['crossbuild-essential-amd64:native'],
                                         ['libc-dev:amd64'], ['libstdc++-dev:amd64']])
        self.assertEqual(ts[0].package_under_test_depends, [['one:amd64']])
        self.assertFalse(skipped)

    def test_builddeps_profiles(self):
        '''@builddeps@ expansion with build profiles'''

        (ts, skipped) = self.call_parse(
            'Tests: t\nDepends: @, @builddeps@',
            'Source: nums\nBuild-Depends: bd1, bd2 <!check>, bd3 <!cross>, bdnotme <stage1> <cross>\n'
            '\n'
            'Package: one\nArchitecture: any')
        self.assertEqual(
            ts[0].depends,
            [['one'], ['bd1'], ['bd2 <!check>'], ['bd3 <!cross>'], ['bdnotme <stage1> <cross>'], ['build-essential']],
        )
        self.assertEqual(ts[0].package_under_test_depends, [['one']])
        self.assertFalse(skipped)

    def test_complex_deps(self):
        '''complex test dependencies'''

        (ts, skipped) = self.call_parse(
            'Tests: t\nDepends: @,\n foo (>= 7) [linux-any],\n'
            ' bd3:native (>= 4) | bd4 [armhf megacpu],\n',
            'Source: nums\n\nPackage: one\nArchitecture: any')
        self.assertEqual(
            ts[0].depends,
            [
                ['one'],
                ['foo (>= 7) [linux-any]'],
                ['bd3:native (>= 4)', 'bd4 [armhf megacpu]'],
            ],
        )
        self.assertEqual(ts[0].package_under_test_depends, [['one']])
        self.assertFalse(skipped)

    def test_complex_deps_foreign_arch(self):
        '''complex test dependencies, foreign arch test'''

        (ts, skipped) = self.call_parse(
            'Tests: t\nDepends: @,\n foo (>= 7) [linux-any],\n'
            ' bd3:native (>= 4) | bd4 [armhf megacpu],\n',
            'Source: nums\n\nPackage: one\nArchitecture: any',
            test_arch_is_foreign=True,
        )
        self.assertEqual(ts[0].depends, [['one:amd64'], ['foo (>= 7) [linux-any]'],
                                         ['bd3:native (>= 4)', 'bd4 [armhf megacpu]']])
        self.assertEqual(ts[0].package_under_test_depends, [['one:amd64']])
        self.assertFalse(skipped)

    def test_deps_negative_arch(self):
        '''test dependencies with negative architecture'''

        (ts, skipped) = self.call_parse(
            'Tests: t\nDepends: foo-notc64 [!c64]\n',
            'Source: nums\n\nPackage: one\nArchitecture: any')
        self.assertEqual(ts[0].depends, [['foo-notc64 [!c64]']])
        self.assertEqual(ts[0].package_under_test_depends, [])
        self.assertFalse(skipped)

    def test_foreign_arch_test_dep(self):
        '''foreign architecture test dependencies'''

        (ts, skipped) = self.call_parse('Tests: t\nDepends: blah, foo:amd64, bar:i386 (>> 1)')
        self.assertEqual(ts[0].depends, [['blah'], ['foo:amd64'], ['bar:i386 (>> 1)']])
        self.assertEqual(ts[0].package_under_test_depends, [])
        self.assertFalse(skipped)

    def test_invalid_test_deps(self):
        '''invalid test dependencies'''

        with self.assertRaises(testdesc.InvalidControl) as cm:
            self.call_parse('Tests: t\nDepends: blah, foo:, bar (<> 1)')
        self.assertIn('Depends field contains an invalid dependency', str(cm.exception))
        self.assertIn('foo:', str(cm.exception))

    def test_comments(self):
        '''comments in control files with Unicode'''

        (ts, skipped) = self.call_parse(
            'Tests: t\n# ♪ ï\nDepends: @, @builddeps@',
            'Source: nums\nMaintainer: Üñïcøδ€ <u@x.com>\nBuild-Depends: bd1 # moo\n'
            '# more c☺ mments\n'
            '   # indented comment\n'
            ' , bd2\n'
            '\n'
            'Package: one\nArchitecture: any')
        self.assertEqual(ts[0].depends, [['one'], ['bd1'], ['bd2'], ['build-essential']])
        self.assertEqual(ts[0].package_under_test_depends, [['one']])
        self.assertFalse(skipped)

    def test_comma(self):
        '''comma in control files at end of depends'''

        (ts, skipped) = self.call_parse(
            'Tests: t\nDepends: @, @builddeps@',
            'Source: nums\nBuild-Depends: bd2, \n')
        self.assertEqual(ts[0].depends, [['bd2'], ['build-essential']])
        self.assertEqual(ts[0].package_under_test_depends, [])
        self.assertFalse(skipped)

    @patch('adtlog.report')
    def test_testbed_unavail_root(self, *args):
        '''restriction needs-root incompatible with testbed'''

        (ts, skipped) = self.call_parse('Tests: t\nDepends:\nRestrictions: needs-root')
        self.assertEqual(ts, [])
        self.assertTrue(skipped)
        adtlog.report.assert_called_once_with(
            't', 'SKIP Test restriction "needs-root" requires testbed capability "root-on-testbed"')

    @patch('adtlog.report')
    def test_testbed_unavail_reboot(self, *args):
        '''restriction needs-reboot incompatible with testbed'''
        (ts, skipped) = self.call_parse('Tests: t\nDepends:\nRestrictions: needs-reboot')
        self.assertEqual(ts, [])
        self.assertTrue(skipped)
        adtlog.report.assert_called_once_with(
            't', 'SKIP Test restriction "needs-reboot" requires testbed capability "reboot"')

    @patch('adtlog.report')
    def test_testbed_unavail_container(self, *args):
        '''restriction isolation-container incompatible with testbed'''

        (ts, skipped) = self.call_parse('Tests: t\nDepends:\nRestrictions: isolation-container')
        self.assertEqual(ts, [])
        self.assertTrue(skipped)
        adtlog.report.assert_called_once_with(
            't', 'SKIP Test restriction "isolation-container" requires testbed capability "isolation-container" and/or "isolation-machine"')

    def test_custom_control_path(self):
        '''custom control file path'''

        os.makedirs(os.path.join(self.pkgdir, 'stuff'))
        c_path = os.path.join(self.pkgdir, 'stuff', 'ctrl')
        with open(c_path, 'w') as f:
            f.write('Tests: one\nDepends: foo')

        (ts, skipped) = testdesc.parse_debian_source(self.pkgdir, [], 'amd64', 'amd64',
                                                     control_path=c_path)
        self.assertEqual(len(ts), 1)
        t = ts[0]
        self.assertEqual(t.name, 'one')
        self.assertEqual(t.path, 'debian/tests/one')
        self.assertEqual(t.restrictions, set())
        self.assertEqual(t.features, set())
        self.assertEqual(t.depends, [['foo']])
        self.assertEqual(t.package_under_test_depends, [])
        self.assertFalse(skipped)

    def test_test_command(self):
        '''single test, test command'''

        (ts, skipped) = self.call_parse('Test-Command: some -t --hing "foo"\nDepends:')
        self.assertEqual(len(ts), 1)
        t = ts[0]
        self.assertEqual(t.name, 'command1')
        self.assertEqual(t.path, None)
        self.assertEqual(t.command, 'some -t --hing "foo"')
        self.assertEqual(t.restrictions, set())
        self.assertEqual(t.features, set())
        self.assertEqual(t.depends, [])
        self.assertEqual(t.package_under_test_depends, [])
        self.assertFalse(skipped)

    def test_test_command_and_tests(self):
        '''Both Tests: and Test-Command:'''

        with self.assertRaises(testdesc.InvalidControl) as cm:
            self.call_parse('Tests: t1\nTest-Command: true\nDepends:')
        self.assertIn('Tests', str(cm.exception))
        self.assertIn('Test-Command', str(cm.exception))
        self.assertIn(' or ', str(cm.exception))

    @patch('adtlog.report')
    def test_test_command_skip(self, *args):
        '''single test, skipped test command'''

        (ts, skipped) = self.call_parse('Test-Command: some --thing\nRestrictions: needs-root')
        self.assertEqual(ts, [])
        self.assertTrue(skipped)
        adtlog.report.assert_called_once_with(
            'command1', 'SKIP Test restriction "needs-root" requires testbed capability "root-on-testbed"')

    def test_classes(self):
        '''Classes: field'''

        (ts, skipped) = self.call_parse('Tests: one\nDepends:\nClasses: foo bar')
        self.assertEqual(len(ts), 1)
        t = ts[0]
        self.assertEqual(t.name, 'one')
        self.assertEqual(t.path, 'debian/tests/one')
        self.assertEqual(t.command, None)
        self.assertEqual(t.restrictions, set())
        self.assertEqual(t.features, set())
        self.assertEqual(t.depends, [])
        self.assertEqual(t.package_under_test_depends, [])
        self.assertFalse(skipped)

    def test_comma_sep(self):
        '''comma separator in fields'''

        (ts, skipped) = self.call_parse(
            'Tests: t1, t2\nRestrictions: build-needed, allow-stderr\n'
            'Features: blue, green\n\n')
        self.assertEqual(len(ts), 2)

        self.assertEqual(ts[0].name, 't1')
        self.assertEqual(ts[1].name, 't2')
        for t in ts:
            self.assertEqual(t.restrictions, {'build-needed', 'allow-stderr'})
            self.assertEqual(t.features, {'blue', 'green'})

        self.assertFalse(skipped)

    def test_autodep8_ruby(self):
        '''autodep8 tests for Ruby packages'''

        with open(os.path.join(self.pkgdir, 'debian', 'ruby-tests.rb'), 'w') as f:
            f.write('exit(0)\n')
        (ts, skipped) = self.call_parse(None, 'Source: ruby-foo\n'
                                        'Build-Depends: gem2deb, rake\n\n'
                                        'Package: ruby-foo\nArchitecture: all')

        if have_autodep8:
            self.assertGreaterEqual(len(ts), 1)
            self.assertIn('gem2deb', ts[0].command)
        else:
            self.assertEqual(len(ts), 0)

    def test_autodep8_perl(self):
        '''autodep8 tests for Perl packages'''

        with open(os.path.join(self.pkgdir, 'Makefile.PL'), 'w') as f:
            f.write('use ExtUtils::MakeMaker;\n')
        os.makedirs(os.path.join(self.pkgdir, 't'))
        (ts, skipped) = self.call_parse(None, 'Source: libfoo-perl\n\n'
                                        'Package: libfoo-perl\nArchitecture: all')

        if have_autodep8:
            self.assertGreaterEqual(len(ts), 1)
            self.assertIn('pkg-perl-autopkgtest', ts[0].command)
            self.assertIn(['pkg-perl-autopkgtest'], ts[0].depends)
            self.assertIn(['libfoo-perl'], ts[0].depends)
            self.assertNotIn(['pkg-perl-autopkgtest'], ts[0].package_under_test_depends)
            self.assertIn(['libfoo-perl'], ts[0].package_under_test_depends)
        else:
            self.assertEqual(len(ts), 0)

    def test_recommends_of_multiple_packages(self):
        '''see #1080981'''

        with open(os.path.join(self.pkgdir, 'debian', 'control'), 'w', encoding='UTF-8') as f:
            f.write('''
Source: bla

Package: bli
Architecture: all
Recommends: one

Package: blu
Architecture: all
Recommends: two

Package: blo
Architecture: all
Recommends: three, four,

Package: bly
Architecture: all
Recommends: five,
        ''')
        (ts, skipped) = self.call_parse(
            'Tests: one\nDepends: @recommends@')
        self.assertEqual(ts[0].depends, [['one'], ['two'], ['three'], ['four'], ['five']])
        self.assertEqual(ts[0].package_under_test_depends, [])


if __name__ == '__main__':
    # Force encoding to UTF-8 even in non-UTF-8 locales.
    real_stdout = sys.stdout
    assert isinstance(real_stdout, io.TextIOBase)
    sys.stdout = io.TextIOWrapper(real_stdout.detach(), encoding="UTF-8", line_buffering=True)
    unittest.main(testRunner=unittest.TextTestRunner(stream=sys.stdout, verbosity=2))
