# coding: utf-8
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import copy
import hashlib
import io
import itertools
import os
import string
import sys
import textwrap
import unittest

import pytest
import six
from mozfile.mozfile import NamedTemporaryFile
from mozunit import main

from mozbuild.util import (
    EnumString,
    EnumStringComparisonError,
    HierarchicalStringList,
    MozbuildDeletionError,
    ReadOnlyDict,
    StrictOrderingOnAppendList,
    StrictOrderingOnAppendListWithAction,
    StrictOrderingOnAppendListWithFlagsFactory,
    TypedList,
    TypedNamedTuple,
    UnsortedError,
    expand_variables,
    group_unified_files,
    hash_file,
    hexdump,
    memoize,
    memoized_property,
    pair,
    resolve_target_to_make,
    write_indented_repr,
)

if sys.version_info[0] == 3:
    str_type = "str"
else:
    str_type = "unicode"

data_path = os.path.abspath(os.path.dirname(__file__))
data_path = os.path.join(data_path, "data")


class TestHashing(unittest.TestCase):
    def test_hash_file_known_hash(self):
        """Ensure a known hash value is recreated."""
        data = b"The quick brown fox jumps over the lazy cog"
        expected = "de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3"

        temp = NamedTemporaryFile()
        temp.write(data)
        temp.flush()

        actual = hash_file(temp.name)

        self.assertEqual(actual, expected)

    def test_hash_file_large(self):
        """Ensure that hash_file seems to work with a large file."""
        data = b"x" * 1048576

        hasher = hashlib.sha1()
        hasher.update(data)
        expected = hasher.hexdigest()

        temp = NamedTemporaryFile()
        temp.write(data)
        temp.flush()

        actual = hash_file(temp.name)

        self.assertEqual(actual, expected)


class TestResolveTargetToMake(unittest.TestCase):
    def setUp(self):
        self.topobjdir = data_path

    def assertResolve(self, path, expected):
        # Handle Windows path separators.
        (reldir, target) = resolve_target_to_make(self.topobjdir, path)
        if reldir is not None:
            reldir = reldir.replace(os.sep, "/")
        if target is not None:
            target = target.replace(os.sep, "/")
        self.assertEqual((reldir, target), expected)

    def test_root_path(self):
        self.assertResolve("/test-dir", ("test-dir", None))
        self.assertResolve("/test-dir/with", ("test-dir/with", None))
        self.assertResolve("/test-dir/without", ("test-dir", None))
        self.assertResolve("/test-dir/without/with", ("test-dir/without/with", None))

    def test_dir(self):
        self.assertResolve("test-dir", ("test-dir", None))
        self.assertResolve("test-dir/with", ("test-dir/with", None))
        self.assertResolve("test-dir/with", ("test-dir/with", None))
        self.assertResolve("test-dir/without", ("test-dir", None))
        self.assertResolve("test-dir/without/with", ("test-dir/without/with", None))

    def test_top_level(self):
        self.assertResolve("package", (None, "package"))
        # Makefile handling shouldn't affect top-level targets.
        self.assertResolve("Makefile", (None, "Makefile"))

    def test_regular_file(self):
        self.assertResolve("test-dir/with/file", ("test-dir/with", "file"))
        self.assertResolve(
            "test-dir/with/without/file", ("test-dir/with", "without/file")
        )
        self.assertResolve(
            "test-dir/with/without/with/file", ("test-dir/with/without/with", "file")
        )

        self.assertResolve("test-dir/without/file", ("test-dir", "without/file"))
        self.assertResolve(
            "test-dir/without/with/file", ("test-dir/without/with", "file")
        )
        self.assertResolve(
            "test-dir/without/with/without/file",
            ("test-dir/without/with", "without/file"),
        )

    def test_Makefile(self):
        self.assertResolve("test-dir/with/Makefile", ("test-dir", "with/Makefile"))
        self.assertResolve(
            "test-dir/with/without/Makefile", ("test-dir/with", "without/Makefile")
        )
        self.assertResolve(
            "test-dir/with/without/with/Makefile",
            ("test-dir/with", "without/with/Makefile"),
        )

        self.assertResolve(
            "test-dir/without/Makefile", ("test-dir", "without/Makefile")
        )
        self.assertResolve(
            "test-dir/without/with/Makefile", ("test-dir", "without/with/Makefile")
        )
        self.assertResolve(
            "test-dir/without/with/without/Makefile",
            ("test-dir/without/with", "without/Makefile"),
        )


class TestHierarchicalStringList(unittest.TestCase):
    def setUp(self):
        self.EXPORTS = HierarchicalStringList()

    def test_exports_append(self):
        self.assertEqual(self.EXPORTS._strings, [])
        self.EXPORTS += ["foo.h"]
        self.assertEqual(self.EXPORTS._strings, ["foo.h"])
        self.EXPORTS += ["bar.h"]
        self.assertEqual(self.EXPORTS._strings, ["foo.h", "bar.h"])

    def test_exports_subdir(self):
        self.assertEqual(self.EXPORTS._children, {})
        self.EXPORTS.foo += ["foo.h"]
        six.assertCountEqual(self, self.EXPORTS._children, {"foo": True})
        self.assertEqual(self.EXPORTS.foo._strings, ["foo.h"])
        self.EXPORTS.bar += ["bar.h"]
        six.assertCountEqual(self, self.EXPORTS._children, {"foo": True, "bar": True})
        self.assertEqual(self.EXPORTS.foo._strings, ["foo.h"])
        self.assertEqual(self.EXPORTS.bar._strings, ["bar.h"])

    def test_exports_multiple_subdir(self):
        self.EXPORTS.foo.bar = ["foobar.h"]
        six.assertCountEqual(self, self.EXPORTS._children, {"foo": True})
        six.assertCountEqual(self, self.EXPORTS.foo._children, {"bar": True})
        six.assertCountEqual(self, self.EXPORTS.foo.bar._children, {})
        self.assertEqual(self.EXPORTS._strings, [])
        self.assertEqual(self.EXPORTS.foo._strings, [])
        self.assertEqual(self.EXPORTS.foo.bar._strings, ["foobar.h"])

    def test_invalid_exports_append(self):
        with self.assertRaises(ValueError) as ve:
            self.EXPORTS += "foo.h"
        six.assertRegex(
            self,
            str(ve.exception),
            "Expected a list of strings, not <(?:type|class) '%s'>" % str_type,
        )

    def test_invalid_exports_set(self):
        with self.assertRaises(ValueError) as ve:
            self.EXPORTS.foo = "foo.h"

        six.assertRegex(
            self,
            str(ve.exception),
            "Expected a list of strings, not <(?:type|class) '%s'>" % str_type,
        )

    def test_invalid_exports_append_base(self):
        with self.assertRaises(ValueError) as ve:
            self.EXPORTS += "foo.h"

        six.assertRegex(
            self,
            str(ve.exception),
            "Expected a list of strings, not <(?:type|class) '%s'>" % str_type,
        )

    def test_invalid_exports_bool(self):
        with self.assertRaises(ValueError) as ve:
            self.EXPORTS += [True]

        six.assertRegex(
            self,
            str(ve.exception),
            "Expected a list of strings, not an element of " "<(?:type|class) 'bool'>",
        )

    def test_del_exports(self):
        with self.assertRaises(MozbuildDeletionError):
            self.EXPORTS.foo += ["bar.h"]
            del self.EXPORTS.foo

    def test_unsorted(self):
        with self.assertRaises(UnsortedError):
            self.EXPORTS += ["foo.h", "bar.h"]

        with self.assertRaises(UnsortedError):
            self.EXPORTS.foo = ["foo.h", "bar.h"]

        with self.assertRaises(UnsortedError):
            self.EXPORTS.foo += ["foo.h", "bar.h"]

    def test_reassign(self):
        self.EXPORTS.foo = ["foo.h"]

        with self.assertRaises(KeyError):
            self.EXPORTS.foo = ["bar.h"]

    def test_walk(self):
        l = HierarchicalStringList()
        l += ["root1", "root2", "root3"]
        l.child1 += ["child11", "child12", "child13"]
        l.child1.grandchild1 += ["grandchild111", "grandchild112"]
        l.child1.grandchild2 += ["grandchild121", "grandchild122"]
        l.child2.grandchild1 += ["grandchild211", "grandchild212"]
        l.child2.grandchild1 += ["grandchild213", "grandchild214"]

        els = list((path, list(seq)) for path, seq in l.walk())
        self.assertEqual(
            els,
            [
                ("", ["root1", "root2", "root3"]),
                ("child1", ["child11", "child12", "child13"]),
                ("child1/grandchild1", ["grandchild111", "grandchild112"]),
                ("child1/grandchild2", ["grandchild121", "grandchild122"]),
                (
                    "child2/grandchild1",
                    [
                        "grandchild211",
                        "grandchild212",
                        "grandchild213",
                        "grandchild214",
                    ],
                ),
            ],
        )

    def test_merge(self):
        l1 = HierarchicalStringList()
        l1 += ["root1", "root2", "root3"]
        l1.child1 += ["child11", "child12", "child13"]
        l1.child1.grandchild1 += ["grandchild111", "grandchild112"]
        l1.child1.grandchild2 += ["grandchild121", "grandchild122"]
        l1.child2.grandchild1 += ["grandchild211", "grandchild212"]
        l1.child2.grandchild1 += ["grandchild213", "grandchild214"]
        l2 = HierarchicalStringList()
        l2.child1 += ["child14", "child15"]
        l2.child1.grandchild2 += ["grandchild123"]
        l2.child3 += ["child31", "child32"]

        l1 += l2
        els = list((path, list(seq)) for path, seq in l1.walk())
        self.assertEqual(
            els,
            [
                ("", ["root1", "root2", "root3"]),
                ("child1", ["child11", "child12", "child13", "child14", "child15"]),
                ("child1/grandchild1", ["grandchild111", "grandchild112"]),
                (
                    "child1/grandchild2",
                    ["grandchild121", "grandchild122", "grandchild123"],
                ),
                (
                    "child2/grandchild1",
                    [
                        "grandchild211",
                        "grandchild212",
                        "grandchild213",
                        "grandchild214",
                    ],
                ),
                ("child3", ["child31", "child32"]),
            ],
        )


class TestStrictOrderingOnAppendList(unittest.TestCase):
    def test_init(self):
        l = StrictOrderingOnAppendList()
        self.assertEqual(len(l), 0)

        l = StrictOrderingOnAppendList(["a", "b", "c"])
        self.assertEqual(len(l), 3)

        with self.assertRaises(UnsortedError):
            StrictOrderingOnAppendList(["c", "b", "a"])

        self.assertEqual(len(l), 3)

    def test_extend(self):
        l = StrictOrderingOnAppendList()
        l.extend(["a", "b"])
        self.assertEqual(len(l), 2)
        self.assertIsInstance(l, StrictOrderingOnAppendList)

        with self.assertRaises(UnsortedError):
            l.extend(["d", "c"])

        self.assertEqual(len(l), 2)

    def test_slicing(self):
        l = StrictOrderingOnAppendList()
        l[:] = ["a", "b"]
        self.assertEqual(len(l), 2)
        self.assertIsInstance(l, StrictOrderingOnAppendList)

        with self.assertRaises(UnsortedError):
            l[:] = ["b", "a"]

        self.assertEqual(len(l), 2)

    def test_add(self):
        l = StrictOrderingOnAppendList()
        l2 = l + ["a", "b"]
        self.assertEqual(len(l), 0)
        self.assertEqual(len(l2), 2)
        self.assertIsInstance(l2, StrictOrderingOnAppendList)

        with self.assertRaises(UnsortedError):
            l2 = l + ["b", "a"]

        self.assertEqual(len(l), 0)

    def test_iadd(self):
        l = StrictOrderingOnAppendList()
        l += ["a", "b"]
        self.assertEqual(len(l), 2)
        self.assertIsInstance(l, StrictOrderingOnAppendList)

        with self.assertRaises(UnsortedError):
            l += ["b", "a"]

        self.assertEqual(len(l), 2)

    def test_add_after_iadd(self):
        l = StrictOrderingOnAppendList(["b"])
        l += ["a"]
        l2 = l + ["c", "d"]
        self.assertEqual(len(l), 2)
        self.assertEqual(len(l2), 4)
        self.assertIsInstance(l2, StrictOrderingOnAppendList)
        with self.assertRaises(UnsortedError):
            l2 = l + ["d", "c"]

        self.assertEqual(len(l), 2)

    def test_add_StrictOrderingOnAppendList(self):
        l = StrictOrderingOnAppendList()
        l += ["c", "d"]
        l += ["a", "b"]
        l2 = StrictOrderingOnAppendList()
        with self.assertRaises(UnsortedError):
            l2 += list(l)
        # Adding a StrictOrderingOnAppendList to another shouldn't throw
        l2 += l


class TestStrictOrderingOnAppendListWithAction(unittest.TestCase):
    def setUp(self):
        self.action = lambda a: (a, id(a))

    def assertSameList(self, expected, actual):
        self.assertEqual(len(expected), len(actual))
        for idx, item in enumerate(actual):
            self.assertEqual(item, expected[idx])

    def test_init(self):
        l = StrictOrderingOnAppendListWithAction(action=self.action)
        self.assertEqual(len(l), 0)
        original = ["a", "b", "c"]
        l = StrictOrderingOnAppendListWithAction(["a", "b", "c"], action=self.action)
        expected = [self.action(i) for i in original]
        self.assertSameList(expected, l)

        with self.assertRaises(ValueError):
            StrictOrderingOnAppendListWithAction("abc", action=self.action)

        with self.assertRaises(ValueError):
            StrictOrderingOnAppendListWithAction()

    def test_extend(self):
        l = StrictOrderingOnAppendListWithAction(action=self.action)
        original = ["a", "b"]
        l.extend(original)
        expected = [self.action(i) for i in original]
        self.assertSameList(expected, l)

        with self.assertRaises(ValueError):
            l.extend("ab")

    def test_slicing(self):
        l = StrictOrderingOnAppendListWithAction(action=self.action)
        original = ["a", "b"]
        l[:] = original
        expected = [self.action(i) for i in original]
        self.assertSameList(expected, l)

        with self.assertRaises(ValueError):
            l[:] = "ab"

    def test_add(self):
        l = StrictOrderingOnAppendListWithAction(action=self.action)
        original = ["a", "b"]
        l2 = l + original
        expected = [self.action(i) for i in original]
        self.assertSameList(expected, l2)

        with self.assertRaises(ValueError):
            l + "abc"

    def test_iadd(self):
        l = StrictOrderingOnAppendListWithAction(action=self.action)
        original = ["a", "b"]
        l += original
        expected = [self.action(i) for i in original]
        self.assertSameList(expected, l)

        with self.assertRaises(ValueError):
            l += "abc"


class TestStrictOrderingOnAppendListWithFlagsFactory(unittest.TestCase):
    def test_strict_ordering_on_append_list_with_flags_factory(self):
        cls = StrictOrderingOnAppendListWithFlagsFactory(
            {
                "foo": bool,
                "bar": int,
            }
        )

        l = cls()
        l += ["a", "b"]

        with self.assertRaises(Exception):
            l["a"] = "foo"

        with self.assertRaises(Exception):
            l["c"]

        self.assertEqual(l["a"].foo, False)
        l["a"].foo = True
        self.assertEqual(l["a"].foo, True)

        with self.assertRaises(TypeError):
            l["a"].bar = "bar"

        self.assertEqual(l["a"].bar, 0)
        l["a"].bar = 42
        self.assertEqual(l["a"].bar, 42)

        l["b"].foo = True
        self.assertEqual(l["b"].foo, True)

        with self.assertRaises(AttributeError):
            l["b"].baz = False

        l["b"].update(foo=False, bar=12)
        self.assertEqual(l["b"].foo, False)
        self.assertEqual(l["b"].bar, 12)

        with self.assertRaises(AttributeError):
            l["b"].update(xyz=1)

    def test_strict_ordering_on_append_list_with_flags_factory_extend(self):
        FooList = StrictOrderingOnAppendListWithFlagsFactory(
            {"foo": bool, "bar": six.text_type}
        )
        foo = FooList(["a", "b", "c"])
        foo["a"].foo = True
        foo["b"].bar = "bar"

        # Don't allow extending lists with different flag definitions.
        BarList = StrictOrderingOnAppendListWithFlagsFactory(
            {"foo": six.text_type, "baz": bool}
        )
        bar = BarList(["d", "e", "f"])
        bar["d"].foo = "foo"
        bar["e"].baz = True
        with self.assertRaises(ValueError):
            foo + bar
        with self.assertRaises(ValueError):
            bar + foo

        # It's not obvious what to do with duplicate list items with possibly
        # different flag values, so don't allow that case.
        with self.assertRaises(ValueError):
            foo + foo

        def assertExtended(l):
            self.assertEqual(len(l), 6)
            self.assertEqual(l["a"].foo, True)
            self.assertEqual(l["b"].bar, "bar")
            self.assertTrue("c" in l)
            self.assertEqual(l["d"].foo, True)
            self.assertEqual(l["e"].bar, "bar")
            self.assertTrue("f" in l)

        # Test extend.
        zot = FooList(["d", "e", "f"])
        zot["d"].foo = True
        zot["e"].bar = "bar"
        zot.extend(foo)
        assertExtended(zot)

        # Test __add__.
        zot = FooList(["d", "e", "f"])
        zot["d"].foo = True
        zot["e"].bar = "bar"
        assertExtended(foo + zot)
        assertExtended(zot + foo)

        # Test __iadd__.
        foo += zot
        assertExtended(foo)

        # Test __setitem__.
        foo[3:] = []
        self.assertEqual(len(foo), 3)
        foo[3:] = zot
        assertExtended(foo)


class TestMemoize(unittest.TestCase):
    def test_memoize(self):
        self._count = 0

        @memoize
        def wrapped(a, b):
            self._count += 1
            return a + b

        self.assertEqual(self._count, 0)
        self.assertEqual(wrapped(1, 1), 2)
        self.assertEqual(self._count, 1)
        self.assertEqual(wrapped(1, 1), 2)
        self.assertEqual(self._count, 1)
        self.assertEqual(wrapped(2, 1), 3)
        self.assertEqual(self._count, 2)
        self.assertEqual(wrapped(1, 2), 3)
        self.assertEqual(self._count, 3)
        self.assertEqual(wrapped(1, 2), 3)
        self.assertEqual(self._count, 3)
        self.assertEqual(wrapped(1, 1), 2)
        self.assertEqual(self._count, 3)

    def test_memoize_method(self):
        class foo(object):
            def __init__(self):
                self._count = 0

            @memoize
            def wrapped(self, a, b):
                self._count += 1
                return a + b

        instance = foo()
        refcount = sys.getrefcount(instance)
        self.assertEqual(instance._count, 0)
        self.assertEqual(instance.wrapped(1, 1), 2)
        self.assertEqual(instance._count, 1)
        self.assertEqual(instance.wrapped(1, 1), 2)
        self.assertEqual(instance._count, 1)
        self.assertEqual(instance.wrapped(2, 1), 3)
        self.assertEqual(instance._count, 2)
        self.assertEqual(instance.wrapped(1, 2), 3)
        self.assertEqual(instance._count, 3)
        self.assertEqual(instance.wrapped(1, 2), 3)
        self.assertEqual(instance._count, 3)
        self.assertEqual(instance.wrapped(1, 1), 2)
        self.assertEqual(instance._count, 3)

        # Memoization of methods is expected to not keep references to
        # instances, so the refcount shouldn't have changed after executing the
        # memoized method.
        self.assertEqual(refcount, sys.getrefcount(instance))

    def test_memoized_property(self):
        class foo(object):
            def __init__(self):
                self._count = 0

            @memoized_property
            def wrapped(self):
                self._count += 1
                return 42

        instance = foo()
        self.assertEqual(instance._count, 0)
        self.assertEqual(instance.wrapped, 42)
        self.assertEqual(instance._count, 1)
        self.assertEqual(instance.wrapped, 42)
        self.assertEqual(instance._count, 1)


class TestTypedList(unittest.TestCase):
    def test_init(self):
        cls = TypedList(int)
        l = cls()
        self.assertEqual(len(l), 0)

        l = cls([1, 2, 3])
        self.assertEqual(len(l), 3)

        with self.assertRaises(ValueError):
            cls([1, 2, "c"])

    def test_extend(self):
        cls = TypedList(int)
        l = cls()
        l.extend([1, 2])
        self.assertEqual(len(l), 2)
        self.assertIsInstance(l, cls)

        with self.assertRaises(ValueError):
            l.extend([3, "c"])

        self.assertEqual(len(l), 2)

    def test_slicing(self):
        cls = TypedList(int)
        l = cls()
        l[:] = [1, 2]
        self.assertEqual(len(l), 2)
        self.assertIsInstance(l, cls)

        with self.assertRaises(ValueError):
            l[:] = [3, "c"]

        self.assertEqual(len(l), 2)

    def test_add(self):
        cls = TypedList(int)
        l = cls()
        l2 = l + [1, 2]
        self.assertEqual(len(l), 0)
        self.assertEqual(len(l2), 2)
        self.assertIsInstance(l2, cls)

        with self.assertRaises(ValueError):
            l2 = l + [3, "c"]

        self.assertEqual(len(l), 0)

    def test_iadd(self):
        cls = TypedList(int)
        l = cls()
        l += [1, 2]
        self.assertEqual(len(l), 2)
        self.assertIsInstance(l, cls)

        with self.assertRaises(ValueError):
            l += [3, "c"]

        self.assertEqual(len(l), 2)

    def test_add_coercion(self):
        objs = []

        class Foo(object):
            def __init__(self, obj):
                objs.append(obj)

        cls = TypedList(Foo)
        l = cls()
        l += [1, 2]
        self.assertEqual(len(objs), 2)
        self.assertEqual(type(l[0]), Foo)
        self.assertEqual(type(l[1]), Foo)

        # Adding a TypedList to a TypedList shouldn't trigger coercion again
        l2 = cls()
        l2 += l
        self.assertEqual(len(objs), 2)
        self.assertEqual(type(l2[0]), Foo)
        self.assertEqual(type(l2[1]), Foo)

        # Adding a TypedList to a TypedList shouldn't even trigger the code
        # that does coercion at all.
        l2 = cls()
        list.__setitem__(l, slice(0, -1), [1, 2])
        l2 += l
        self.assertEqual(len(objs), 2)
        self.assertEqual(type(l2[0]), int)
        self.assertEqual(type(l2[1]), int)

    def test_memoized(self):
        cls = TypedList(int)
        cls2 = TypedList(str)
        self.assertEqual(TypedList(int), cls)
        self.assertNotEqual(cls, cls2)


class TypedTestStrictOrderingOnAppendList(unittest.TestCase):
    def test_init(self):
        class Unicode(six.text_type):
            def __new__(cls, other):
                if not isinstance(other, six.text_type):
                    raise ValueError()
                return six.text_type.__new__(cls, other)

        cls = TypedList(Unicode, StrictOrderingOnAppendList)
        l = cls()
        self.assertEqual(len(l), 0)

        l = cls(["a", "b", "c"])
        self.assertEqual(len(l), 3)

        with self.assertRaises(UnsortedError):
            cls(["c", "b", "a"])

        with self.assertRaises(ValueError):
            cls(["a", "b", 3])

        self.assertEqual(len(l), 3)


class TestTypedNamedTuple(unittest.TestCase):
    def test_simple(self):
        FooBar = TypedNamedTuple("FooBar", [("foo", six.text_type), ("bar", int)])

        t = FooBar(foo="foo", bar=2)
        self.assertEqual(type(t), FooBar)
        self.assertEqual(t.foo, "foo")
        self.assertEqual(t.bar, 2)
        self.assertEqual(t[0], "foo")
        self.assertEqual(t[1], 2)

        FooBar("foo", 2)

        with self.assertRaises(TypeError):
            FooBar("foo", "not integer")
        with self.assertRaises(TypeError):
            FooBar(2, 4)

        # Passing a tuple as the first argument is the same as passing multiple
        # arguments.
        t1 = ("foo", 3)
        t2 = FooBar(t1)
        self.assertEqual(type(t2), FooBar)
        self.assertEqual(FooBar(t1), FooBar("foo", 3))


class TestGroupUnifiedFiles(unittest.TestCase):
    FILES = ["%s.cpp" % letter for letter in string.ascii_lowercase]

    def test_multiple_files(self):
        mapping = list(group_unified_files(self.FILES, "Unified", "cpp", 5))

        def check_mapping(index, expected_num_source_files):
            (unified_file, source_files) = mapping[index]

            self.assertEqual(unified_file, "Unified%d.cpp" % index)
            self.assertEqual(len(source_files), expected_num_source_files)

        all_files = list(itertools.chain(*[files for (_, files) in mapping]))
        self.assertEqual(len(all_files), len(self.FILES))
        self.assertEqual(set(all_files), set(self.FILES))

        expected_amounts = [5, 5, 5, 5, 5, 1]
        for i, amount in enumerate(expected_amounts):
            check_mapping(i, amount)


class TestMisc(unittest.TestCase):
    def test_pair(self):
        self.assertEqual(list(pair([1, 2, 3, 4, 5, 6])), [(1, 2), (3, 4), (5, 6)])

        self.assertEqual(
            list(pair([1, 2, 3, 4, 5, 6, 7])), [(1, 2), (3, 4), (5, 6), (7, None)]
        )

    def test_expand_variables(self):
        self.assertEqual(expand_variables("$(var)", {"var": "value"}), "value")

        self.assertEqual(
            expand_variables("$(a) and $(b)", {"a": "1", "b": "2"}), "1 and 2"
        )

        self.assertEqual(
            expand_variables("$(a) and $(undefined)", {"a": "1", "b": "2"}), "1 and "
        )

        self.assertEqual(
            expand_variables(
                "before $(string) between $(list) after",
                {"string": "abc", "list": ["a", "b", "c"]},
            ),
            "before abc between a b c after",
        )


class TestEnumString(unittest.TestCase):
    def test_string(self):
        CompilerType = EnumString.subclass("gcc", "clang", "clang-cl")

        type = CompilerType("gcc")
        self.assertEqual(type, "gcc")
        self.assertNotEqual(type, "clang")
        self.assertNotEqual(type, "clang-cl")
        self.assertIn(type, ("gcc", "clang-cl"))
        self.assertNotIn(type, ("clang", "clang-cl"))

        with self.assertRaises(EnumStringComparisonError):
            self.assertEqual(type, "foo")

        with self.assertRaises(EnumStringComparisonError):
            self.assertNotEqual(type, "foo")

        with self.assertRaises(EnumStringComparisonError):
            self.assertIn(type, ("foo", "gcc"))

        with self.assertRaises(ValueError):
            type = CompilerType("foo")


class TestIndentedRepr(unittest.TestCase):
    @unittest.skipUnless(six.PY2, "requires Python 2")
    def test_write_indented_repr_py2(self):
        data = textwrap.dedent(
            r"""
        {
            'a': 1,
            'b': b'abc',
            b'c': 'xyz',
            'd': False,
            'e': {
                'a': 1,
                'b': b'2',
                'c': '3',
            },
            'f': [
                1,
                b'2',
                '3',
            ],
            'pile_of_bytes': b'\xf0\x9f\x92\xa9',
            'pile_of_poo': '💩',
            'special_chars': '\\\'"\x08\n\t',
            'with_accents': 'éàñ',
        }
        """
        ).lstrip()

        obj = eval(data)
        buf = io.StringIO()
        write_indented_repr(buf, obj)

        self.assertEqual(buf.getvalue(), data)

    @unittest.skipUnless(six.PY3, "requires Python 3")
    def test_write_indented_repr(self):
        data = textwrap.dedent(
            r"""
        {   b'c': 'xyz',
            'a': 1,
            'b': b'abc',
            'd': False,
            'e': {'a': 1, 'b': b'2', 'c': '3'},
            'f': [1, b'2', '3'],
            'pile_of_bytes': b'\xf0\x9f\x92\xa9',
            'pile_of_poo': '💩',
            'special_chars': '\\\'"\x08\n\t',
            'with_accents': 'éàñ'}
        """
        ).lstrip()

        obj = eval(data)
        buf = six.StringIO()
        write_indented_repr(buf, obj)

        self.assertEqual(buf.getvalue(), data)


class TestHexDump(unittest.TestCase):
    @unittest.skipUnless(six.PY3, "requires Python 3")
    def test_hexdump(self):
        self.assertEqual(
            hexdump("abcdef123💩ZYXWVU".encode("utf-8")),
            [
                "00  61 62 63 64 65 66 31 32  33 f0 9f 92 a9 5a 59 58  |abcdef123....ZYX|\n",
                "10  57 56 55                                          |WVU             |\n",
            ],
        )


def test_read_only_dict():
    d = ReadOnlyDict(foo="bar")
    with pytest.raises(Exception):
        d["foo"] = "baz"

    with pytest.raises(Exception):
        d.update({"foo": "baz"})

    with pytest.raises(Exception):
        del d["foo"]

    # ensure copy still works
    d_copy = d.copy()
    assert d == d_copy
    # TODO Returning a dict here feels like a bug, but there are places in-tree
    # relying on this behaviour.
    assert isinstance(d_copy, dict)

    d_copy = copy.copy(d)
    assert d == d_copy
    assert isinstance(d_copy, ReadOnlyDict)

    d_copy = copy.deepcopy(d)
    assert d == d_copy
    assert isinstance(d_copy, ReadOnlyDict)


if __name__ == "__main__":
    main()
