#!/usr/bin/python3

import os
import sys
import unittest
import tempfile
import shutil
import io
import subprocess

try:
    # Python >= 3.3
    from unittest.mock import patch
    patch  # pyflakes
except ImportError:
    # fall back to separate package
    from 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(['which', 'autodep8'], stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE) == 0
dpkg_deps_ver = subprocess.check_output(['perl', '-MDpkg::Deps', '-e', 'print $Dpkg::Deps::VERSION'],
                                        universal_newlines=True)
have_dpkg_build_profiles = (dpkg_deps_ver >= '1.04')


class Rfc822(unittest.TestCase):
    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(unittest.TestCase):
    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.clicks, [])
        self.assertEqual(t.installed_clicks, [])
        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'''

        with self.assertRaises(testdesc.Unsupported) as cm:
            testdesc.Test('foo', 'tests/do_foo', None, ['needs-red'], [], [],
                          [], [], [])
        self.assertIn('unknown restriction needs-red', str(cm.exception))

    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'])


class Debian(unittest.TestCase):
    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=[]):
        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')

    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, [])
        self.assertEqual(t.features, [])
        self.assertEqual(t.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, [])
            self.assertEqual(t.features, [])
            self.assertEqual(t.depends, ['one', '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\n\n'
            'Package: three\nArchitecture: c64 vax')
        self.assertEqual(len(ts), 1)
        self.assertEqual(ts[0].name, 't')
        self.assertEqual(ts[0].path, 'debian/tests/t')
        self.assertEqual(ts[0].restrictions, [])
        self.assertEqual(ts[0].features, [])
        self.assertEqual(ts[0].depends,
                         ['one', 'two [linux-any]',
                          'three [c64 vax]'])
        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[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[2].name, 'three')
        self.assertEqual(ts[2].path, 'debian/tests/three')
        self.assertEqual(ts[2].restrictions, ['needs-recommends'])
        self.assertEqual(ts[2].features, [])
        self.assertEqual(ts[2].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_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', 'bd3:native (>= 7) | bd4',
                                         'bdi1', 'bdi2', 'bda1', 'bda2',
                                         'build-essential', 'foo (>= 7)'])
        self.assertFalse(skipped)

    @unittest.skipUnless(have_dpkg_build_profiles,
                         'dpkg version does not yet support build profiles')
    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', 'bd3', 'build-essential'])
        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.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.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.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'
            '   # intented comment\n'
            ' , bd2\n'
            '\n'
            'Package: one\nArchitecture: any')
        self.assertEqual(ts[0].depends, ['one', 'bd1', 'bd2', 'build-essential'])
        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 needs root on testbed which is not available')

    @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 needs to reboot testbed but testbed does not provide reboot capability')

    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',
                                                     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, [])
        self.assertEqual(t.features, [])
        self.assertEqual(t.depends, ['foo'])
        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, [])
        self.assertEqual(t.features, [])
        self.assertEqual(t.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 needs root on testbed which is not available')

    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, [])
        self.assertEqual(t.features, [])
        self.assertEqual(t.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)
        else:
            self.assertEqual(len(ts), 0)


class Click(unittest.TestCase):
    @classmethod
    def setUpClass(kls):
        kls.click = os.path.join(test_dir, 'testclick_0.1_all.click')
        kls.click_src = os.path.join(test_dir, 'testclick')

        # bzr fake
        kls.fake_bins = tempfile.TemporaryDirectory(prefix='autopkgtest-fake-bins.')
        with open(os.path.join(kls.fake_bins.name, 'bzr'), 'w') as f:
            f.write('''#!/bin/sh -e
[ "$1" = checkout ]
[ "$2" = "--lightweight" ]
[ -d "$4" ]
if [ "$3" = bzr+fake://test-click ]; then
    cp -r %s/* "$4"
else
    echo "unknown repository" >&2
    exit 1
fi''' % kls.click_src)
        os.chmod(os.path.join(kls.fake_bins.name, 'bzr'), 0o700)
        kls.orig_path = os.environ['PATH']
        os.environ['PATH'] = '%s:%s' % (kls.fake_bins.name, os.environ['PATH'])

    @classmethod
    def tearDownClass(kls):
        os.environ['PATH'] = kls.orig_path

    def test_all_fields(self):
        '''parsing manifest test with all possible fields'''

        (srcdir, ts, skipped) = testdesc.parse_click_manifest('''{
  "name": "foo",
  "x-test": {
    "t": {
      "path": "tests/do_t",
      "depends": ["foo (>= 1)", "bar | baz"],
      "restrictions": ["allow-stderr", "needs-recommends"],
      "features": ["turns-blue", "bites-back"],
      "classes": ["foo", "bar"]
    }
  }
}''', [], ['/foo/myapp.click'], False, '/src')
        self.assertEqual(len(ts), 1)
        self.assertEqual(ts[0].name, "t")
        self.assertEqual(ts[0].path, 'tests/do_t')
        self.assertEqual(ts[0].command, None)
        self.assertEqual(ts[0].depends, ['foo (>= 1)', 'bar | baz'])
        self.assertEqual(ts[0].clicks, ['/foo/myapp.click'])
        self.assertEqual(ts[0].installed_clicks, [])
        self.assertEqual(ts[0].restrictions, ['allow-stderr', 'needs-recommends'])
        self.assertEqual(ts[0].features, ['turns-blue', 'bites-back'])

        self.assertEqual(srcdir, '/src')
        self.assertFalse(skipped)

    def test_no_tests(self):
        '''parsing manifest without tests'''

        self.assertEqual(
            testdesc.parse_click_manifest('{"name":"foo"}', [], [], False, '/'),
            ('/', [], False))

    def test_path_only(self):
        '''test description is a single path'''

        (srcdir, ts, skipped) = testdesc.parse_click_manifest('''{
  "name": "foo",
  "x-test": {
      "t1": "tests/do_t",
      "t2": "runtests"
  }
}''', [], ['/foo/myapp.click'], False, '/src')
        self.assertEqual(len(ts), 2)
        self.assertEqual(ts[0].name, "t1")
        self.assertEqual(ts[0].path, 'tests/do_t')
        self.assertEqual(ts[1].name, "t2")
        self.assertEqual(ts[1].path, 'runtests')
        for i in [0, 1]:
            self.assertEqual(ts[i].command, None)
            self.assertEqual(ts[i].depends, [])
            self.assertEqual(ts[i].clicks, ['/foo/myapp.click'])
            self.assertEqual(ts[i].restrictions, [])
            self.assertEqual(ts[i].features, [])

        self.assertEqual(srcdir, '/src')
        self.assertFalse(skipped)

    def test_installed(self):
        '''parsing manifest test with already installed click'''

        (srcdir, ts, skipped) = testdesc.parse_click_manifest('''{
  "name": "foo",
  "x-test": {
    "t": "tests/do_t"
  }
}''', [], [], True, '/src')
        self.assertEqual(len(ts), 1)
        self.assertEqual(ts[0].name, "t")
        self.assertEqual(ts[0].path, 'tests/do_t')
        self.assertEqual(ts[0].command, None)
        self.assertEqual(ts[0].clicks, [])
        self.assertEqual(ts[0].installed_clicks, ['foo'])
        self.assertEqual(srcdir, '/src')
        self.assertFalse(skipped)

    def test_autopilot_simple(self):
        '''simple autopilot test specification'''

        (_, ts, skipped) = testdesc.parse_click_manifest('''{
  "name": "foo",
  "x-test": {
      "autopilot": "foo_tests"
  }
}''', [], ['/foo/myapp.click'], '/')
        self.assertEqual(len(ts), 1)
        self.assertEqual(ts[0].name, 'autopilot')
        self.assertEqual(ts[0].path, None)
        self.assertIn('PYTHONPATH=', ts[0].command)
        self.assertIn('autopilot', ts[0].command)
        self.assertIn('autopilot', ts[0].depends[0])
        self.assertEqual(ts[0].clicks, ['/foo/myapp.click'])
        self.assertEqual(ts[0].restrictions, ['allow-stderr'])
        self.assertEqual(ts[0].features, [])

        self.assertFalse(skipped)

    def test_autopilot_extradeps(self):
        '''autopilot test specification with extra info'''

        (_, ts, skipped) = testdesc.parse_click_manifest('''{
  "name": "foo",
  "x-test": {
      "my_ap_test": {
          "autopilot_module": "foo_tests",
          "depends": ["extradep1"],
          "restrictions": ["allow-stderr"]
      }
  }
}''', [], ['/foo/myapp.click'], '/')
        self.assertEqual(len(ts), 1)
        self.assertEqual(ts[0].name, 'my_ap_test')
        self.assertEqual(ts[0].path, None)
        self.assertIn('PYTHONPATH=', ts[0].command)
        self.assertIn('autopilot', ts[0].command)
        self.assertIn('autopilot', ts[0].depends[0])
        self.assertIn('extradep1', ts[0].depends)
        self.assertEqual(ts[0].clicks, ['/foo/myapp.click'])
        self.assertEqual(ts[0].restrictions, ['allow-stderr'])
        self.assertEqual(ts[0].features, [])

        self.assertFalse(skipped)

    def test_autopilot_manual_simple(self):
        '''simple manual autopilot test specification'''

        (_, ts, skipped) = testdesc.parse_click_manifest('''{
  "name": "foo",
  "x-test": {
      "autopilot": "tests/run_tests"
  }
}''', [], ['/foo/myapp.click'], '/')
        self.assertEqual(len(ts), 1)
        self.assertEqual(ts[0].name, "autopilot")
        self.assertEqual(ts[0].path, "tests/run_tests")
        self.assertEqual(ts[0].command, None)
        self.assertEqual(ts[0].clicks, ['/foo/myapp.click'])
        self.assertEqual(ts[0].depends, [])
        self.assertEqual(ts[0].restrictions, [])
        self.assertEqual(ts[0].features, [])

        self.assertFalse(skipped)

    def test_autopilot_manual_complex(self):
        '''complex manual autopilot test specification'''

        (_, ts, skipped) = testdesc.parse_click_manifest('''{
  "name": "foo",
  "x-test": {
      "autopilot": {
          "path": "tests/run_tests"
      }
  }
}''', [], ['/foo/myapp.click'], '/')
        self.assertEqual(len(ts), 1)
        self.assertEqual(ts[0].name, "autopilot")
        self.assertEqual(ts[0].path, "tests/run_tests")
        self.assertEqual(ts[0].command, None)
        self.assertEqual(ts[0].clicks, ['/foo/myapp.click'])
        self.assertEqual(ts[0].depends, [])
        self.assertEqual(ts[0].restrictions, [])
        self.assertEqual(ts[0].features, [])

        self.assertFalse(skipped)

    def test_invalid_json_syntax(self):
        '''parsing manifest with invalid JSON syntax'''

        with self.assertRaises(testdesc.InvalidControl) as cm:
            testdesc.parse_click_manifest('''{
  "x-test": {
    "t": { "depends": ["foo (>= 1)", }
  }}''', [], [], '/')

        self.assertIn('not valid JSON', str(cm.exception))

    def test_invalid_json_type(self):
        '''parsing manifest with invalid JSON type'''

        with self.assertRaises(testdesc.InvalidControl) as cm:
            testdesc.parse_click_manifest('{"x-test": ["tests/foo"]}', [], [], '/')
        self.assertIn('must be a dict', str(cm.exception))

        with self.assertRaises(testdesc.InvalidControl) as cm:
            testdesc.parse_click_manifest('{"x-test": {"t": ["tests/foo"]}}', [], [], '/')
        self.assertIn('must be strings or dicts', str(cm.exception))

    @patch('adtlog.report')
    def test_skip_caps(self, *args):
        '''skipped test due to insufficient testbed capabilities'''

        (_, ts, skipped) = testdesc.parse_click_manifest('''{
  "x-test": {
    "t": {
      "path": "tests/do_t",
      "restrictions": ["needs-root"]
    }
  }
}''', ['isolation-container'], [], '/')
        self.assertEqual(len(ts), 0)
        self.assertTrue(skipped)
        adtlog.report.assert_called_once_with(
            't', 'SKIP Test needs root on testbed which is not available')

    @patch('adtlog.report')
    def test_skip_command_and_path(self, *args):
        '''skipped test due to specifying command and path'''

        with self.assertRaises(testdesc.InvalidControl) as cm:
            testdesc.parse_click_manifest('''{
  "x-test": {
    "t": {
      "path": "tests/do_t",
      "command": "echo ok"
    }
  }
}''', [], [], '/')
        self.assertIn('must have either path or command', str(cm.exception))

    def test_click_with_srcdir(self):
        '''parsing click with explicit source dir'''

        (srcdir, ts, skipped) = testdesc.parse_click(self.click, [],
                                                     srcdir=self.click_src)
        self.assertEqual(srcdir, self.click_src)
        self.assertEqual(len(ts), 5)
        self.assertFalse(skipped)

        # tests should be sorted alphabetically
        self.assertEqual(ts[0].name, "broken")
        self.assertEqual(ts[0].path, 'tests/printerr')
        self.assertEqual(ts[0].command, None)
        self.assertEqual(ts[0].depends, [])
        self.assertEqual(ts[0].clicks, [self.click])
        self.assertEqual(ts[0].restrictions, [])
        self.assertEqual(ts[0].features, [])

        self.assertEqual(ts[1].name, "inst")
        self.assertEqual(ts[1].path, "tests/inst")
        self.assertEqual(ts[1].command, None)
        self.assertEqual(ts[1].depends, ['python3-evdev'])
        self.assertEqual(ts[1].clicks, [self.click])
        self.assertEqual(ts[1].restrictions, [])
        self.assertEqual(ts[1].features, [])

        self.assertEqual(ts[2].name, "serr")
        self.assertEqual(ts[2].path, "tests/printerr")
        self.assertEqual(ts[2].command, None)
        self.assertEqual(ts[2].depends, [])
        self.assertEqual(ts[2].clicks, [self.click])
        self.assertEqual(ts[2].restrictions, ['allow-stderr'])
        self.assertEqual(ts[2].features, [])

        self.assertEqual(ts[3].name, "shell")
        self.assertEqual(ts[3].path, None)
        self.assertEqual(ts[3].command, 'grep ^root /etc/passwd')
        self.assertEqual(ts[3].depends, [])
        self.assertEqual(ts[3].clicks, [self.click])
        self.assertEqual(ts[3].restrictions, [])
        self.assertEqual(ts[3].features, ['bites-back'])

        self.assertEqual(ts[4].name, "simple")
        self.assertEqual(ts[4].path, "tests/simple")
        self.assertEqual(ts[4].command, None)
        self.assertEqual(ts[4].depends, [])
        self.assertEqual(ts[4].clicks, [self.click])
        self.assertEqual(ts[4].restrictions, [])
        self.assertEqual(ts[4].features, [])

    @patch('adtlog.info')
    @patch('adtlog.error')
    def test_click_source_download(self, *args):
        '''Automatic click source download'''

        (srcdir, ts, skipped) = testdesc.parse_click_manifest('''{
  "name": "foo",
  "x-source": {
    "vcs-bzr": "bzr+fake://test-click"
  },
  "x-test": {
    "t": {
      "command": "doit"
    }
  }
}''', [], ['/foo/myapp.click'], None)
        self.assertEqual(len(ts), 1)
        self.assertNotEqual(srcdir, None)
        self.assertTrue(os.path.exists(os.path.join(srcdir, 'CMakeLists.txt')))
        self.assertFalse(skipped)

        self.assertFalse(adtlog.error.called)
        adtlog.info.assert_called_once_with(
            'checking out click source from bzr+fake://test-click')

    @patch('adtlog.error')
    def test_click_source_download_no_vcs(self, *args):
        '''Automatic click source download: fails, no vcs-* tag'''

        (srcdir, ts, skipped) = testdesc.parse_click_manifest('''{
  "name": "foo",
  "x-test": {
    "t": {
      "command": "doit"
    }
  }
}''', [], ['/foo/myapp.click'], None)

        self.assertEqual(len(ts), 1)
        self.assertFalse(skipped)
        self.assertEqual(srcdir, None)
        adtlog.error.assert_called_once_with(
            'cannot download click source: manifest does not have "x-source"')


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