#!/usr/bin/python3


import io
import os
import shutil
import subprocess
import sys
import tempfile
import textwrap
import unittest
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"""

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

        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"""

        with tempfile.NamedTemporaryFile(prefix="dsc.") as control:
            control.write(
                b"""\
-----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-----

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

        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"""

        with tempfile.NamedTemporaryFile(prefix="bogus.") as control:
            control.write(b"Bo Gus: something\nmuhaha")
            control.flush()
            parser = testdesc.parse_rfc822(control.name)
            self.assertRaises(StopIteration, parser.__next__)


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=None, test_arch_is_foreign=False
    ):
        if caps is 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", 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_recommends_own_packages(self):
        """Classify own packages from @recommends@ like explicit dependencies."""
        depends = "foo-extra (>= 2), foo-extra | foo-other, foo-extra | external"
        (ts, skipped) = self.call_parse(
            "Tests: recommended\nDepends: @recommends@\n\n"
            f"Tests: explicit\nDepends: {depends}\n",
            "Source: foo\n\nPackage: foo\nArchitecture: all\n"
            f"Recommends: {depends}\n\n"
            "Package: foo-extra\nArchitecture: all\n\n"
            "Package: foo-other\nArchitecture: all\n",
        )
        self.assertFalse(skipped)
        self.assertEqual(len(ts), 2)
        for t in ts:
            self.assertEqual(
                t.depends,
                (
                    ("foo-extra (>= 2)",),
                    ("foo-extra", "foo-other"),
                    ("foo-extra", "external"),
                ),
            )
            self.assertEqual(
                t.package_under_test_depends,
                (("foo-extra",), ("foo-extra", "foo-other")),
            )

    def test_builddeps_own_packages(self):
        """Classify own packages from @builddeps@, including build profiles."""
        (ts, skipped) = self.call_parse(
            "Tests: builddeps\nDepends: @builddeps@\n",
            "Source: foo\n"
            "Build-Depends: foo-extra (>= 2) <!nocheck>, "
            "foo-extra | foo-other, foo-extra | external\n\n"
            "Package: foo-extra\nArchitecture: all\n\n"
            "Package: foo-other\nArchitecture: all\n",
        )
        self.assertFalse(skipped)
        self.assertEqual(len(ts), 1)
        self.assertEqual(
            ts[0].depends,
            (
                ("foo-extra (>= 2) <!nocheck>",),
                ("foo-extra", "foo-other"),
                ("foo-extra", "external"),
                ("build-essential",),
            ),
        )
        self.assertEqual(
            ts[0].package_under_test_depends,
            (("foo-extra",), ("foo-extra", "foo-other")),
        )

    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_invalid_test_architecture(self):
        """Architecture field rejects invalid syntax"""

        for architecture in (
            "[amd64]",
            "[!armhf]",
            "!!amd64",
            "AMD64",
            "-any",
            "!-any",
        ):
            with self.subTest(architecture=architecture):
                with self.assertRaises(testdesc.InvalidControl) as cm:
                    self.call_parse(f"Tests: t\nArchitecture: {architecture}\nDepends:")
                self.assertEqual(
                    str(cm.exception),
                    "InvalidControl test t: Invalid architecture name "
                    f"in Architecture field: {architecture}",
                )

    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"),))
        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",),))
        self.assertEqual(t.package_under_test_depends, (("one:amd64",),))

        self.assertFalse(skipped)

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

        (ts, skipped) = self.call_parse(
            "Test-Command: t1\nDepends: foo\nFeatures: 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\nDepends: foo\nFeatures: 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\nDepends: foo\nFeatures: 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\nDepends: foo\nFeatures: 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\nDepends: foo\nFeatures: 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(
            "Test-Command: echo #foo\n# ♪ ï\nDepends: @, @builddeps@",
            "Source: nums\nMaintainer: Üñïcøδ€ <u@x.com>\nBuild-Depends: bd1\n"
            "# more c☺ mments\n"
            " , bd2\n"
            "\n"
            "Package: one\nArchitecture: any",
        )
        self.assertEqual(ts[0].command, "echo #foo")
        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_leading(self):
        """comma in control files at beginning 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)

    def test_comma_trailing(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)

    def test_comma_duplicate(self):
        """duplicate commas in depends fields"""

        (ts, skipped) = self.call_parse(
            "Tests: t\nDepends: @ ,, , @builddeps@ ,",
            "Source: nums\nBuild-Depends: ,,bd1, bd2,,,bd3 ,  , , bd4, , \n",
        )
        self.assertEqual(
            ts[0].depends,
            (("bd1",), ("bd2",), ("bd3",), ("bd4",), ("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\nPackage: 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))
