# 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 os
import unittest

from mozunit import main

from mozbuild.frontend.context import (
    AbsolutePath,
    Context,
    ContextDerivedTypedHierarchicalStringList,
    ContextDerivedTypedList,
    ContextDerivedTypedListWithItems,
    ContextDerivedTypedRecord,
    Files,
    FUNCTIONS,
    ObjDirPath,
    Path,
    SourcePath,
    SPECIAL_VARIABLES,
    SUBCONTEXTS,
    VARIABLES,
)

from mozbuild.util import StrictOrderingOnAppendListWithFlagsFactory
from mozpack import path as mozpath


class TestContext(unittest.TestCase):
    def test_defaults(self):
        test = Context({
            'foo': (int, int, ''),
            'bar': (bool, bool, ''),
            'baz': (dict, dict, ''),
        })

        self.assertEqual(test.keys(), [])

        self.assertEqual(test['foo'], 0)

        self.assertEqual(set(test.keys()), { 'foo' })

        self.assertEqual(test['bar'], False)

        self.assertEqual(set(test.keys()), { 'foo', 'bar' })

        self.assertEqual(test['baz'], {})

        self.assertEqual(set(test.keys()), { 'foo', 'bar', 'baz' })

        with self.assertRaises(KeyError):
            test['qux']

        self.assertEqual(set(test.keys()), { 'foo', 'bar', 'baz' })

    def test_type_check(self):
        test = Context({
            'foo': (int, int, ''),
            'baz': (dict, list, ''),
        })

        test['foo'] = 5

        self.assertEqual(test['foo'], 5)

        with self.assertRaises(ValueError):
            test['foo'] = {}

        self.assertEqual(test['foo'], 5)

        with self.assertRaises(KeyError):
            test['bar'] = True

        test['baz'] = [('a', 1), ('b', 2)]

        self.assertEqual(test['baz'], { 'a': 1, 'b': 2 })

    def test_update(self):
        test = Context({
            'foo': (int, int, ''),
            'bar': (bool, bool, ''),
            'baz': (dict, list, ''),
        })

        self.assertEqual(test.keys(), [])

        with self.assertRaises(ValueError):
            test.update(bar=True, foo={})

        self.assertEqual(test.keys(), [])

        test.update(bar=True, foo=1)

        self.assertEqual(set(test.keys()), { 'foo', 'bar' })
        self.assertEqual(test['foo'], 1)
        self.assertEqual(test['bar'], True)

        test.update([('bar', False), ('foo', 2)])
        self.assertEqual(test['foo'], 2)
        self.assertEqual(test['bar'], False)

        test.update([('foo', 0), ('baz', { 'a': 1, 'b': 2 })])
        self.assertEqual(test['foo'], 0)
        self.assertEqual(test['baz'], { 'a': 1, 'b': 2 })

        test.update([('foo', 42), ('baz', [('c', 3), ('d', 4)])])
        self.assertEqual(test['foo'], 42)
        self.assertEqual(test['baz'], { 'c': 3, 'd': 4 })

    def test_context_paths(self):
        test = Context()

        # Newly created context has no paths.
        self.assertIsNone(test.main_path)
        self.assertIsNone(test.current_path)
        self.assertEqual(test.all_paths, set())
        self.assertEqual(test.source_stack, [])

        foo = os.path.abspath('foo')
        test.add_source(foo)

        # Adding the first source makes it the main and current path.
        self.assertEqual(test.main_path, foo)
        self.assertEqual(test.current_path, foo)
        self.assertEqual(test.all_paths, set([foo]))
        self.assertEqual(test.source_stack, [foo])

        bar = os.path.abspath('bar')
        test.add_source(bar)

        # Adding the second source makes leaves main and current paths alone.
        self.assertEqual(test.main_path, foo)
        self.assertEqual(test.current_path, foo)
        self.assertEqual(test.all_paths, set([bar, foo]))
        self.assertEqual(test.source_stack, [foo])

        qux = os.path.abspath('qux')
        test.push_source(qux)

        # Pushing a source makes it the current path
        self.assertEqual(test.main_path, foo)
        self.assertEqual(test.current_path, qux)
        self.assertEqual(test.all_paths, set([bar, foo, qux]))
        self.assertEqual(test.source_stack, [foo, qux])

        hoge = os.path.abspath('hoge')
        test.push_source(hoge)
        self.assertEqual(test.main_path, foo)
        self.assertEqual(test.current_path, hoge)
        self.assertEqual(test.all_paths, set([bar, foo, hoge, qux]))
        self.assertEqual(test.source_stack, [foo, qux, hoge])

        fuga = os.path.abspath('fuga')

        # Adding a source after pushing doesn't change the source stack
        test.add_source(fuga)
        self.assertEqual(test.main_path, foo)
        self.assertEqual(test.current_path, hoge)
        self.assertEqual(test.all_paths, set([bar, foo, fuga, hoge, qux]))
        self.assertEqual(test.source_stack, [foo, qux, hoge])

        # Adding a source twice doesn't change anything
        test.add_source(qux)
        self.assertEqual(test.main_path, foo)
        self.assertEqual(test.current_path, hoge)
        self.assertEqual(test.all_paths, set([bar, foo, fuga, hoge, qux]))
        self.assertEqual(test.source_stack, [foo, qux, hoge])

        last = test.pop_source()

        # Popping a source returns the last pushed one, not the last added one.
        self.assertEqual(last, hoge)
        self.assertEqual(test.main_path, foo)
        self.assertEqual(test.current_path, qux)
        self.assertEqual(test.all_paths, set([bar, foo, fuga, hoge, qux]))
        self.assertEqual(test.source_stack, [foo, qux])

        last = test.pop_source()
        self.assertEqual(last, qux)
        self.assertEqual(test.main_path, foo)
        self.assertEqual(test.current_path, foo)
        self.assertEqual(test.all_paths, set([bar, foo, fuga, hoge, qux]))
        self.assertEqual(test.source_stack, [foo])

        # Popping the main path is allowed.
        last = test.pop_source()
        self.assertEqual(last, foo)
        self.assertEqual(test.main_path, foo)
        self.assertIsNone(test.current_path)
        self.assertEqual(test.all_paths, set([bar, foo, fuga, hoge, qux]))
        self.assertEqual(test.source_stack, [])

        # Popping past the main path asserts.
        with self.assertRaises(AssertionError):
            test.pop_source()

        # Pushing after the main path was popped asserts.
        with self.assertRaises(AssertionError):
            test.push_source(foo)

        test = Context()
        test.push_source(foo)
        test.push_source(bar)

        # Pushing the same file twice is allowed.
        test.push_source(bar)
        test.push_source(foo)
        self.assertEqual(last, foo)
        self.assertEqual(test.main_path, foo)
        self.assertEqual(test.current_path, foo)
        self.assertEqual(test.all_paths, set([bar, foo]))
        self.assertEqual(test.source_stack, [foo, bar, bar, foo])

    def test_context_dirs(self):
        class Config(object): pass
        config = Config()
        config.topsrcdir = mozpath.abspath(os.curdir)
        config.topobjdir = mozpath.abspath('obj')
        test = Context(config=config)
        foo = mozpath.abspath('foo')
        test.push_source(foo)

        self.assertEqual(test.srcdir, config.topsrcdir)
        self.assertEqual(test.relsrcdir, '')
        self.assertEqual(test.objdir, config.topobjdir)
        self.assertEqual(test.relobjdir, '')

        foobar = os.path.abspath('foo/bar')
        test.push_source(foobar)
        self.assertEqual(test.srcdir, mozpath.join(config.topsrcdir, 'foo'))
        self.assertEqual(test.relsrcdir, 'foo')
        self.assertEqual(test.objdir, config.topobjdir)
        self.assertEqual(test.relobjdir, '')


class TestSymbols(unittest.TestCase):
    def _verify_doc(self, doc):
        # Documentation should be of the format:
        # """SUMMARY LINE
        #
        # EXTRA PARAGRAPHS
        # """

        self.assertNotIn('\r', doc)

        lines = doc.split('\n')

        # No trailing whitespace.
        for line in lines[0:-1]:
            self.assertEqual(line, line.rstrip())

        self.assertGreater(len(lines), 0)
        self.assertGreater(len(lines[0].strip()), 0)

        # Last line should be empty.
        self.assertEqual(lines[-1].strip(), '')

    def test_documentation_formatting(self):
        for typ, inp, doc in VARIABLES.values():
            self._verify_doc(doc)

        for attr, args, doc in FUNCTIONS.values():
            self._verify_doc(doc)

        for func, typ, doc in SPECIAL_VARIABLES.values():
            self._verify_doc(doc)

        for name, cls in SUBCONTEXTS.items():
            self._verify_doc(cls.__doc__)

            for name, v in cls.VARIABLES.items():
                self._verify_doc(v[2])


class TestPaths(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        class Config(object): pass
        cls.config = config = Config()
        config.topsrcdir = mozpath.abspath(os.curdir)
        config.topobjdir = mozpath.abspath('obj')
        config.external_source_dir = None

    def test_path(self):
        config = self.config
        ctxt1 = Context(config=config)
        ctxt1.push_source(mozpath.join(config.topsrcdir, 'foo', 'moz.build'))
        ctxt2 = Context(config=config)
        ctxt2.push_source(mozpath.join(config.topsrcdir, 'bar', 'moz.build'))

        path1 = Path(ctxt1, 'qux')
        self.assertIsInstance(path1, SourcePath)
        self.assertEqual(path1, 'qux')
        self.assertEqual(path1.full_path,
                         mozpath.join(config.topsrcdir, 'foo', 'qux'))

        path2 = Path(ctxt2, '../foo/qux')
        self.assertIsInstance(path2, SourcePath)
        self.assertEqual(path2, '../foo/qux')
        self.assertEqual(path2.full_path,
                         mozpath.join(config.topsrcdir, 'foo', 'qux'))

        self.assertEqual(path1, path2)

        self.assertEqual(path1.join('../../bar/qux').full_path,
                         mozpath.join(config.topsrcdir, 'bar', 'qux'))

        path1 = Path(ctxt1, '/qux/qux')
        self.assertIsInstance(path1, SourcePath)
        self.assertEqual(path1, '/qux/qux')
        self.assertEqual(path1.full_path,
                         mozpath.join(config.topsrcdir, 'qux', 'qux'))

        path2 = Path(ctxt2, '/qux/qux')
        self.assertIsInstance(path2, SourcePath)
        self.assertEqual(path2, '/qux/qux')
        self.assertEqual(path2.full_path,
                         mozpath.join(config.topsrcdir, 'qux', 'qux'))

        self.assertEqual(path1, path2)

        path1 = Path(ctxt1, '!qux')
        self.assertIsInstance(path1, ObjDirPath)
        self.assertEqual(path1, '!qux')
        self.assertEqual(path1.full_path,
                         mozpath.join(config.topobjdir, 'foo', 'qux'))

        path2 = Path(ctxt2, '!../foo/qux')
        self.assertIsInstance(path2, ObjDirPath)
        self.assertEqual(path2, '!../foo/qux')
        self.assertEqual(path2.full_path,
                         mozpath.join(config.topobjdir, 'foo', 'qux'))

        self.assertEqual(path1, path2)

        path1 = Path(ctxt1, '!/qux/qux')
        self.assertIsInstance(path1, ObjDirPath)
        self.assertEqual(path1, '!/qux/qux')
        self.assertEqual(path1.full_path,
                         mozpath.join(config.topobjdir, 'qux', 'qux'))

        path2 = Path(ctxt2, '!/qux/qux')
        self.assertIsInstance(path2, ObjDirPath)
        self.assertEqual(path2, '!/qux/qux')
        self.assertEqual(path2.full_path,
                         mozpath.join(config.topobjdir, 'qux', 'qux'))

        self.assertEqual(path1, path2)

        path1 = Path(ctxt1, path1)
        self.assertIsInstance(path1, ObjDirPath)
        self.assertEqual(path1, '!/qux/qux')
        self.assertEqual(path1.full_path,
                         mozpath.join(config.topobjdir, 'qux', 'qux'))

        path2 = Path(ctxt2, path2)
        self.assertIsInstance(path2, ObjDirPath)
        self.assertEqual(path2, '!/qux/qux')
        self.assertEqual(path2.full_path,
                         mozpath.join(config.topobjdir, 'qux', 'qux'))

        self.assertEqual(path1, path2)

        path1 = Path(path1)
        self.assertIsInstance(path1, ObjDirPath)
        self.assertEqual(path1, '!/qux/qux')
        self.assertEqual(path1.full_path,
                         mozpath.join(config.topobjdir, 'qux', 'qux'))

        self.assertEqual(path1, path2)

        path2 = Path(path2)
        self.assertIsInstance(path2, ObjDirPath)
        self.assertEqual(path2, '!/qux/qux')
        self.assertEqual(path2.full_path,
                         mozpath.join(config.topobjdir, 'qux', 'qux'))

        self.assertEqual(path1, path2)

    def test_source_path(self):
        config = self.config
        ctxt = Context(config=config)
        ctxt.push_source(mozpath.join(config.topsrcdir, 'foo', 'moz.build'))

        path = SourcePath(ctxt, 'qux')
        self.assertEqual(path, 'qux')
        self.assertEqual(path.full_path,
                         mozpath.join(config.topsrcdir, 'foo', 'qux'))
        self.assertEqual(path.translated,
                         mozpath.join(config.topobjdir, 'foo', 'qux'))

        path = SourcePath(ctxt, '../bar/qux')
        self.assertEqual(path, '../bar/qux')
        self.assertEqual(path.full_path,
                         mozpath.join(config.topsrcdir, 'bar', 'qux'))
        self.assertEqual(path.translated,
                         mozpath.join(config.topobjdir, 'bar', 'qux'))

        path = SourcePath(ctxt, '/qux/qux')
        self.assertEqual(path, '/qux/qux')
        self.assertEqual(path.full_path,
                         mozpath.join(config.topsrcdir, 'qux', 'qux'))
        self.assertEqual(path.translated,
                         mozpath.join(config.topobjdir, 'qux', 'qux'))

        with self.assertRaises(ValueError):
            SourcePath(ctxt, '!../bar/qux')

        with self.assertRaises(ValueError):
            SourcePath(ctxt, '!/qux/qux')

        path = SourcePath(path)
        self.assertIsInstance(path, SourcePath)
        self.assertEqual(path, '/qux/qux')
        self.assertEqual(path.full_path,
                         mozpath.join(config.topsrcdir, 'qux', 'qux'))
        self.assertEqual(path.translated,
                         mozpath.join(config.topobjdir, 'qux', 'qux'))

        path = Path(path)
        self.assertIsInstance(path, SourcePath)

    def test_objdir_path(self):
        config = self.config
        ctxt = Context(config=config)
        ctxt.push_source(mozpath.join(config.topsrcdir, 'foo', 'moz.build'))

        path = ObjDirPath(ctxt, '!qux')
        self.assertEqual(path, '!qux')
        self.assertEqual(path.full_path,
                         mozpath.join(config.topobjdir, 'foo', 'qux'))

        path = ObjDirPath(ctxt, '!../bar/qux')
        self.assertEqual(path, '!../bar/qux')
        self.assertEqual(path.full_path,
                         mozpath.join(config.topobjdir, 'bar', 'qux'))

        path = ObjDirPath(ctxt, '!/qux/qux')
        self.assertEqual(path, '!/qux/qux')
        self.assertEqual(path.full_path,
                         mozpath.join(config.topobjdir, 'qux', 'qux'))

        with self.assertRaises(ValueError):
            path = ObjDirPath(ctxt, '../bar/qux')

        with self.assertRaises(ValueError):
            path = ObjDirPath(ctxt, '/qux/qux')

        path = ObjDirPath(path)
        self.assertIsInstance(path, ObjDirPath)
        self.assertEqual(path, '!/qux/qux')
        self.assertEqual(path.full_path,
                         mozpath.join(config.topobjdir, 'qux', 'qux'))

        path = Path(path)
        self.assertIsInstance(path, ObjDirPath)

    def test_absolute_path(self):
        config = self.config
        ctxt = Context(config=config)
        ctxt.push_source(mozpath.join(config.topsrcdir, 'foo', 'moz.build'))

        path = AbsolutePath(ctxt, '%/qux')
        self.assertEqual(path, '%/qux')
        self.assertEqual(path.full_path, '/qux')

        with self.assertRaises(ValueError):
            path = AbsolutePath(ctxt, '%qux')

    def test_path_with_mixed_contexts(self):
        config = self.config
        ctxt1 = Context(config=config)
        ctxt1.push_source(mozpath.join(config.topsrcdir, 'foo', 'moz.build'))
        ctxt2 = Context(config=config)
        ctxt2.push_source(mozpath.join(config.topsrcdir, 'bar', 'moz.build'))

        path1 = Path(ctxt1, 'qux')
        path2 = Path(ctxt2, path1)
        self.assertEqual(path2, path1)
        self.assertEqual(path2, 'qux')
        self.assertEqual(path2.context, ctxt1)
        self.assertEqual(path2.full_path,
                         mozpath.join(config.topsrcdir, 'foo', 'qux'))

        path1 = Path(ctxt1, '../bar/qux')
        path2 = Path(ctxt2, path1)
        self.assertEqual(path2, path1)
        self.assertEqual(path2, '../bar/qux')
        self.assertEqual(path2.context, ctxt1)
        self.assertEqual(path2.full_path,
                         mozpath.join(config.topsrcdir, 'bar', 'qux'))

        path1 = Path(ctxt1, '/qux/qux')
        path2 = Path(ctxt2, path1)
        self.assertEqual(path2, path1)
        self.assertEqual(path2, '/qux/qux')
        self.assertEqual(path2.context, ctxt1)
        self.assertEqual(path2.full_path,
                         mozpath.join(config.topsrcdir, 'qux', 'qux'))

        path1 = Path(ctxt1, '!qux')
        path2 = Path(ctxt2, path1)
        self.assertEqual(path2, path1)
        self.assertEqual(path2, '!qux')
        self.assertEqual(path2.context, ctxt1)
        self.assertEqual(path2.full_path,
                         mozpath.join(config.topobjdir, 'foo', 'qux'))

        path1 = Path(ctxt1, '!../bar/qux')
        path2 = Path(ctxt2, path1)
        self.assertEqual(path2, path1)
        self.assertEqual(path2, '!../bar/qux')
        self.assertEqual(path2.context, ctxt1)
        self.assertEqual(path2.full_path,
                         mozpath.join(config.topobjdir, 'bar', 'qux'))

        path1 = Path(ctxt1, '!/qux/qux')
        path2 = Path(ctxt2, path1)
        self.assertEqual(path2, path1)
        self.assertEqual(path2, '!/qux/qux')
        self.assertEqual(path2.context, ctxt1)
        self.assertEqual(path2.full_path,
                         mozpath.join(config.topobjdir, 'qux', 'qux'))

    def test_path_typed_list(self):
        config = self.config
        ctxt1 = Context(config=config)
        ctxt1.push_source(mozpath.join(config.topsrcdir, 'foo', 'moz.build'))
        ctxt2 = Context(config=config)
        ctxt2.push_source(mozpath.join(config.topsrcdir, 'bar', 'moz.build'))

        paths = [
            '!../bar/qux',
            '!/qux/qux',
            '!qux',
            '../bar/qux',
            '/qux/qux',
            'qux',
        ]

        MyList = ContextDerivedTypedList(Path)
        l = MyList(ctxt1)
        l += paths

        for p_str, p_path in zip(paths, l):
            self.assertEqual(p_str, p_path)
            self.assertEqual(p_path, Path(ctxt1, p_str))
            self.assertEqual(p_path.join('foo'),
                             Path(ctxt1, mozpath.join(p_str, 'foo')))

        l2 = MyList(ctxt2)
        l2 += paths

        for p_str, p_path in zip(paths, l2):
            self.assertEqual(p_str, p_path)
            self.assertEqual(p_path, Path(ctxt2, p_str))

        # Assigning with Paths from another context doesn't rebase them
        l2 = MyList(ctxt2)
        l2 += l

        for p_str, p_path in zip(paths, l2):
            self.assertEqual(p_str, p_path)
            self.assertEqual(p_path, Path(ctxt1, p_str))

        MyListWithFlags = ContextDerivedTypedListWithItems(
            Path, StrictOrderingOnAppendListWithFlagsFactory({
                'foo': bool,
            }))
        l = MyListWithFlags(ctxt1)
        l += paths

        for p in paths:
            l[p].foo = True

        for p_str, p_path in zip(paths, l):
            self.assertEqual(p_str, p_path)
            self.assertEqual(p_path, Path(ctxt1, p_str))
            self.assertEqual(l[p_str].foo, True)
            self.assertEqual(l[p_path].foo, True)

    def test_path_typed_hierarchy_list(self):
        config = self.config
        ctxt1 = Context(config=config)
        ctxt1.push_source(mozpath.join(config.topsrcdir, 'foo', 'moz.build'))
        ctxt2 = Context(config=config)
        ctxt2.push_source(mozpath.join(config.topsrcdir, 'bar', 'moz.build'))

        paths = [
            '!../bar/qux',
            '!/qux/qux',
            '!qux',
            '../bar/qux',
            '/qux/qux',
            'qux',
        ]

        MyList = ContextDerivedTypedHierarchicalStringList(Path)
        l = MyList(ctxt1)
        l += paths
        l.subdir += paths

        for _, files in l.walk():
            for p_str, p_path in zip(paths, files):
                self.assertEqual(p_str, p_path)
                self.assertEqual(p_path, Path(ctxt1, p_str))
                self.assertEqual(p_path.join('foo'),
                                 Path(ctxt1, mozpath.join(p_str, 'foo')))

        l2 = MyList(ctxt2)
        l2 += paths
        l2.subdir += paths

        for _, files in l2.walk():
            for p_str, p_path in zip(paths, files):
                self.assertEqual(p_str, p_path)
                self.assertEqual(p_path, Path(ctxt2, p_str))

        # Assigning with Paths from another context doesn't rebase them
        l2 = MyList(ctxt2)
        l2 += l

        for _, files in l2.walk():
            for p_str, p_path in zip(paths, files):
                self.assertEqual(p_str, p_path)
                self.assertEqual(p_path, Path(ctxt1, p_str))


class TestTypedRecord(unittest.TestCase):

    def test_fields(self):
        T = ContextDerivedTypedRecord(('field1', unicode),
                                      ('field2', list))
        inst = T(None)
        self.assertEqual(inst.field1, '')
        self.assertEqual(inst.field2, [])

        inst.field1 = 'foo'
        inst.field2 += ['bar']

        self.assertEqual(inst.field1, 'foo')
        self.assertEqual(inst.field2, ['bar'])

        with self.assertRaises(AttributeError):
            inst.field3 = []

    def test_coercion(self):
        T = ContextDerivedTypedRecord(('field1', unicode),
                                      ('field2', list))
        inst = T(None)
        inst.field1 = 3
        inst.field2 += ('bar',)
        self.assertEqual(inst.field1, '3')
        self.assertEqual(inst.field2, ['bar'])

        with self.assertRaises(TypeError):
            inst.field2 = object()


class TestFiles(unittest.TestCase):
    def test_aggregate_empty(self):
        c = Context({})

        files = {'moz.build': Files(c, pattern='**')}

        self.assertEqual(Files.aggregate(files), {
            'bug_component_counts': [],
            'recommended_bug_component': None,
        })

    def test_single_bug_component(self):
        c = Context({})
        f = Files(c, pattern='**')
        f['BUG_COMPONENT'] = (u'Product1', u'Component1')

        files = {'moz.build': f}
        self.assertEqual(Files.aggregate(files), {
            'bug_component_counts': [((u'Product1', u'Component1'), 1)],
            'recommended_bug_component': (u'Product1', u'Component1'),
        })

    def test_multiple_bug_components(self):
        c = Context({})
        f1 = Files(c, pattern='**')
        f1['BUG_COMPONENT'] = (u'Product1', u'Component1')

        f2 = Files(c, pattern='**')
        f2['BUG_COMPONENT'] = (u'Product2', u'Component2')

        files = {'a': f1, 'b': f2, 'c': f1}
        self.assertEqual(Files.aggregate(files), {
            'bug_component_counts': [
                ((u'Product1', u'Component1'), 2),
                ((u'Product2', u'Component2'), 1),
            ],
            'recommended_bug_component': (u'Product1', u'Component1'),
        })

    def test_no_recommended_bug_component(self):
        """If there is no clear count winner, we don't recommend a bug component."""
        c = Context({})
        f1 = Files(c, pattern='**')
        f1['BUG_COMPONENT'] = (u'Product1', u'Component1')

        f2 = Files(c, pattern='**')
        f2['BUG_COMPONENT'] = (u'Product2', u'Component2')

        files = {'a': f1, 'b': f2}
        self.assertEqual(Files.aggregate(files), {
            'bug_component_counts': [
                ((u'Product1', u'Component1'), 1),
                ((u'Product2', u'Component2'), 1),
            ],
            'recommended_bug_component': None,
        })


if __name__ == '__main__':
    main()
