#!/usr/bin/python3

# ruff: noqa: E402

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\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(
            "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_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))
