# 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/.

from __future__ import unicode_literals

import itertools
import hashlib
import os
import unittest
import shutil
import string
import sys
import tempfile
import textwrap

from mozfile.mozfile import NamedTemporaryFile
from mozunit import (
    main,
    MockedOpen,
)

from mozbuild.util import (
    expand_variables,
    FileAvoidWrite,
    group_unified_files,
    hash_file,
    indented_repr,
    memoize,
    memoized_property,
    pair,
    resolve_target_to_make,
    MozbuildDeletionError,
    HierarchicalStringList,
    EnumString,
    EnumStringComparisonError,
    ListWithAction,
    StrictOrderingOnAppendList,
    StrictOrderingOnAppendListWithFlagsFactory,
    TypedList,
    TypedNamedTuple,
    UnsortedError,
)

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 TestFileAvoidWrite(unittest.TestCase):
    def test_file_avoid_write(self):
        with MockedOpen({'file': 'content'}):
            # Overwriting an existing file replaces its content
            faw = FileAvoidWrite('file')
            faw.write('bazqux')
            self.assertEqual(faw.close(), (True, True))
            self.assertEqual(open('file', 'r').read(), 'bazqux')

            # Creating a new file (obviously) stores its content
            faw = FileAvoidWrite('file2')
            faw.write('content')
            self.assertEqual(faw.close(), (False, True))
            self.assertEqual(open('file2').read(), 'content')

        with MockedOpen({'file': 'content'}):
            with FileAvoidWrite('file') as file:
                file.write('foobar')

            self.assertEqual(open('file', 'r').read(), 'foobar')

        class MyMockedOpen(MockedOpen):
            '''MockedOpen extension to raise an exception if something
            attempts to write in an opened file.
            '''
            def __call__(self, name, mode):
                if 'w' in mode:
                    raise Exception('Unexpected open with write mode')
                return MockedOpen.__call__(self, name, mode)

        with MyMockedOpen({'file': 'content'}):
            # Validate that MyMockedOpen works as intended
            file = FileAvoidWrite('file')
            file.write('foobar')
            self.assertRaises(Exception, file.close)

            # Check that no write actually happens when writing the
            # same content as what already is in the file
            faw = FileAvoidWrite('file')
            faw.write('content')
            self.assertEqual(faw.close(), (True, False))

    def test_diff_not_default(self):
        """Diffs are not produced by default."""

        with MockedOpen({'file': 'old'}):
            faw = FileAvoidWrite('file')
            faw.write('dummy')
            faw.close()
            self.assertIsNone(faw.diff)

    def test_diff_update(self):
        """Diffs are produced on file update."""

        with MockedOpen({'file': 'old'}):
            faw = FileAvoidWrite('file', capture_diff=True)
            faw.write('new')
            faw.close()

            diff = '\n'.join(faw.diff)
            self.assertIn('-old', diff)
            self.assertIn('+new', diff)

    def test_diff_create(self):
        """Diffs are produced when files are created."""

        tmpdir = tempfile.mkdtemp()
        try:
            path = os.path.join(tmpdir, 'file')
            faw = FileAvoidWrite(path, capture_diff=True)
            faw.write('new')
            faw.close()

            diff = '\n'.join(faw.diff)
            self.assertIn('+new', diff)
        finally:
            shutil.rmtree(tmpdir)

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"]
        self.assertItemsEqual(self.EXPORTS._children, {"foo" : True})
        self.assertEqual(self.EXPORTS.foo._strings, ["foo.h"])
        self.EXPORTS.bar += ["bar.h"]
        self.assertItemsEqual(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"]
        self.assertItemsEqual(self.EXPORTS._children, {"foo" : True})
        self.assertItemsEqual(self.EXPORTS.foo._children, {"bar" : True})
        self.assertItemsEqual(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"
        self.assertEqual(str(ve.exception),
                         "Expected a list of strings, not <type '%s'>" % str_type)

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

        self.assertEqual(str(ve.exception),
                         "Expected a list of strings, not <type '%s'>" % str_type)

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

        self.assertEqual(str(ve.exception),
                         "Expected a list of strings, not <type '%s'>" % str_type)

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

        self.assertEqual(str(ve.exception),
                         "Expected a list of strings, not an element of "
                         "<type 'bool'>")

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

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

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

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

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

        with self.assertRaises(KeyError) as ee:
            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 TestListWithAction(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 = ListWithAction(action=self.action)
        self.assertEqual(len(l), 0)
        original = ['a', 'b', 'c']
        l = ListWithAction(['a', 'b', 'c'], action=self.action)
        expected = map(self.action, original)
        self.assertSameList(expected, l)

        with self.assertRaises(ValueError):
            ListWithAction('abc', action=self.action)

        with self.assertRaises(ValueError):
            ListWithAction()

    def test_extend(self):
        l = ListWithAction(action=self.action)
        original = ['a', 'b']
        l.extend(original)
        expected = map(self.action, original)
        self.assertSameList(expected, l)

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

    def test_slicing(self):
        l = ListWithAction(action=self.action)
        original = ['a', 'b']
        l[:] = original
        expected = map(self.action, original)
        self.assertSameList(expected, l)

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

    def test_add(self):
        l = ListWithAction(action=self.action)
        original = ['a', 'b']
        l2 = l + original
        expected = map(self.action, original)
        self.assertSameList(expected, l2)

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

    def test_iadd(self):
        l = ListWithAction(action=self.action)
        original = ['a', 'b']
        l += original
        expected = map(self.action, 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):
            c = 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': unicode
        })
        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': unicode, '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 __setslice__.
        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.__setslice__(l, 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(unicode):
            def __init__(self, other):
                if not isinstance(other, unicode):
                    raise ValueError()
                super(Unicode, self).__init__(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', unicode), ('bar', int)])

        t = FooBar(foo='foo', bar=2)
        self.assertEquals(type(t), FooBar)
        self.assertEquals(t.foo, 'foo')
        self.assertEquals(t.bar, 2)
        self.assertEquals(t[0], 'foo')
        self.assertEquals(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.assertEquals(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)

    def test_unsorted_files(self):
        unsorted_files = ['a%d.cpp' % i for i in range(11)]
        sorted_files = sorted(unsorted_files)
        mapping = list(group_unified_files(unsorted_files, 'Unified', 'cpp', 5))

        self.assertEqual(mapping[0][1], sorted_files[0:5])
        self.assertEqual(mapping[1][1], sorted_files[5:10])
        self.assertEqual(mapping[2][1], sorted_files[10:])


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('msvc', 'gcc', 'clang', 'clang-cl')

        type = CompilerType('msvc')
        self.assertEquals(type, 'msvc')
        self.assertNotEquals(type, 'gcc')
        self.assertNotEquals(type, 'clang')
        self.assertNotEquals(type, 'clang-cl')
        self.assertIn(type, ('msvc', 'clang-cl'))
        self.assertNotIn(type, ('gcc', 'clang'))

        with self.assertRaises(EnumStringComparisonError):
            self.assertEquals(type, 'foo')

        with self.assertRaises(EnumStringComparisonError):
            self.assertNotEquals(type, 'foo')

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

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


class TestIndentedRepr(unittest.TestCase):
    def test_indented_repr(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)

        self.assertEqual(indented_repr(obj), data)


if __name__ == '__main__':
    main()
