1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5from __future__ import absolute_import, print_function, unicode_literals
6
7import os
8import six
9import unittest
10
11from mozunit import main
12
13from mozbuild.frontend.context import (
14    AbsolutePath,
15    Context,
16    ContextDerivedTypedHierarchicalStringList,
17    ContextDerivedTypedList,
18    ContextDerivedTypedListWithItems,
19    ContextDerivedTypedRecord,
20    Files,
21    FUNCTIONS,
22    ObjDirPath,
23    Path,
24    SourcePath,
25    SPECIAL_VARIABLES,
26    SUBCONTEXTS,
27    VARIABLES,
28)
29
30from mozbuild.util import StrictOrderingOnAppendListWithFlagsFactory
31from mozpack import path as mozpath
32
33
34class TestContext(unittest.TestCase):
35    def test_defaults(self):
36        test = Context(
37            {
38                "foo": (int, int, ""),
39                "bar": (bool, bool, ""),
40                "baz": (dict, dict, ""),
41            }
42        )
43
44        self.assertEqual(list(test), [])
45
46        self.assertEqual(test["foo"], 0)
47
48        self.assertEqual(set(test.keys()), {"foo"})
49
50        self.assertEqual(test["bar"], False)
51
52        self.assertEqual(set(test.keys()), {"foo", "bar"})
53
54        self.assertEqual(test["baz"], {})
55
56        self.assertEqual(set(test.keys()), {"foo", "bar", "baz"})
57
58        with self.assertRaises(KeyError):
59            test["qux"]
60
61        self.assertEqual(set(test.keys()), {"foo", "bar", "baz"})
62
63    def test_type_check(self):
64        test = Context(
65            {
66                "foo": (int, int, ""),
67                "baz": (dict, list, ""),
68            }
69        )
70
71        test["foo"] = 5
72
73        self.assertEqual(test["foo"], 5)
74
75        with self.assertRaises(ValueError):
76            test["foo"] = {}
77
78        self.assertEqual(test["foo"], 5)
79
80        with self.assertRaises(KeyError):
81            test["bar"] = True
82
83        test["baz"] = [("a", 1), ("b", 2)]
84
85        self.assertEqual(test["baz"], {"a": 1, "b": 2})
86
87    def test_update(self):
88        test = Context(
89            {
90                "foo": (int, int, ""),
91                "bar": (bool, bool, ""),
92                "baz": (dict, list, ""),
93            }
94        )
95
96        self.assertEqual(list(test), [])
97
98        with self.assertRaises(ValueError):
99            test.update(bar=True, foo={})
100
101        self.assertEqual(list(test), [])
102
103        test.update(bar=True, foo=1)
104
105        self.assertEqual(set(test.keys()), {"foo", "bar"})
106        self.assertEqual(test["foo"], 1)
107        self.assertEqual(test["bar"], True)
108
109        test.update([("bar", False), ("foo", 2)])
110        self.assertEqual(test["foo"], 2)
111        self.assertEqual(test["bar"], False)
112
113        test.update([("foo", 0), ("baz", {"a": 1, "b": 2})])
114        self.assertEqual(test["foo"], 0)
115        self.assertEqual(test["baz"], {"a": 1, "b": 2})
116
117        test.update([("foo", 42), ("baz", [("c", 3), ("d", 4)])])
118        self.assertEqual(test["foo"], 42)
119        self.assertEqual(test["baz"], {"c": 3, "d": 4})
120
121    def test_context_paths(self):
122        test = Context()
123
124        # Newly created context has no paths.
125        self.assertIsNone(test.main_path)
126        self.assertIsNone(test.current_path)
127        self.assertEqual(test.all_paths, set())
128        self.assertEqual(test.source_stack, [])
129
130        foo = os.path.abspath("foo")
131        test.add_source(foo)
132
133        # Adding the first source makes it the main and current path.
134        self.assertEqual(test.main_path, foo)
135        self.assertEqual(test.current_path, foo)
136        self.assertEqual(test.all_paths, set([foo]))
137        self.assertEqual(test.source_stack, [foo])
138
139        bar = os.path.abspath("bar")
140        test.add_source(bar)
141
142        # Adding the second source makes leaves main and current paths alone.
143        self.assertEqual(test.main_path, foo)
144        self.assertEqual(test.current_path, foo)
145        self.assertEqual(test.all_paths, set([bar, foo]))
146        self.assertEqual(test.source_stack, [foo])
147
148        qux = os.path.abspath("qux")
149        test.push_source(qux)
150
151        # Pushing a source makes it the current path
152        self.assertEqual(test.main_path, foo)
153        self.assertEqual(test.current_path, qux)
154        self.assertEqual(test.all_paths, set([bar, foo, qux]))
155        self.assertEqual(test.source_stack, [foo, qux])
156
157        hoge = os.path.abspath("hoge")
158        test.push_source(hoge)
159        self.assertEqual(test.main_path, foo)
160        self.assertEqual(test.current_path, hoge)
161        self.assertEqual(test.all_paths, set([bar, foo, hoge, qux]))
162        self.assertEqual(test.source_stack, [foo, qux, hoge])
163
164        fuga = os.path.abspath("fuga")
165
166        # Adding a source after pushing doesn't change the source stack
167        test.add_source(fuga)
168        self.assertEqual(test.main_path, foo)
169        self.assertEqual(test.current_path, hoge)
170        self.assertEqual(test.all_paths, set([bar, foo, fuga, hoge, qux]))
171        self.assertEqual(test.source_stack, [foo, qux, hoge])
172
173        # Adding a source twice doesn't change anything
174        test.add_source(qux)
175        self.assertEqual(test.main_path, foo)
176        self.assertEqual(test.current_path, hoge)
177        self.assertEqual(test.all_paths, set([bar, foo, fuga, hoge, qux]))
178        self.assertEqual(test.source_stack, [foo, qux, hoge])
179
180        last = test.pop_source()
181
182        # Popping a source returns the last pushed one, not the last added one.
183        self.assertEqual(last, hoge)
184        self.assertEqual(test.main_path, foo)
185        self.assertEqual(test.current_path, qux)
186        self.assertEqual(test.all_paths, set([bar, foo, fuga, hoge, qux]))
187        self.assertEqual(test.source_stack, [foo, qux])
188
189        last = test.pop_source()
190        self.assertEqual(last, qux)
191        self.assertEqual(test.main_path, foo)
192        self.assertEqual(test.current_path, foo)
193        self.assertEqual(test.all_paths, set([bar, foo, fuga, hoge, qux]))
194        self.assertEqual(test.source_stack, [foo])
195
196        # Popping the main path is allowed.
197        last = test.pop_source()
198        self.assertEqual(last, foo)
199        self.assertEqual(test.main_path, foo)
200        self.assertIsNone(test.current_path)
201        self.assertEqual(test.all_paths, set([bar, foo, fuga, hoge, qux]))
202        self.assertEqual(test.source_stack, [])
203
204        # Popping past the main path asserts.
205        with self.assertRaises(AssertionError):
206            test.pop_source()
207
208        # Pushing after the main path was popped asserts.
209        with self.assertRaises(AssertionError):
210            test.push_source(foo)
211
212        test = Context()
213        test.push_source(foo)
214        test.push_source(bar)
215
216        # Pushing the same file twice is allowed.
217        test.push_source(bar)
218        test.push_source(foo)
219        self.assertEqual(last, foo)
220        self.assertEqual(test.main_path, foo)
221        self.assertEqual(test.current_path, foo)
222        self.assertEqual(test.all_paths, set([bar, foo]))
223        self.assertEqual(test.source_stack, [foo, bar, bar, foo])
224
225    def test_context_dirs(self):
226        class Config(object):
227            pass
228
229        config = Config()
230        config.topsrcdir = mozpath.abspath(os.curdir)
231        config.topobjdir = mozpath.abspath("obj")
232        test = Context(config=config)
233        foo = mozpath.abspath("foo")
234        test.push_source(foo)
235
236        self.assertEqual(test.srcdir, config.topsrcdir)
237        self.assertEqual(test.relsrcdir, "")
238        self.assertEqual(test.objdir, config.topobjdir)
239        self.assertEqual(test.relobjdir, "")
240
241        foobar = os.path.abspath("foo/bar")
242        test.push_source(foobar)
243        self.assertEqual(test.srcdir, mozpath.join(config.topsrcdir, "foo"))
244        self.assertEqual(test.relsrcdir, "foo")
245        self.assertEqual(test.objdir, config.topobjdir)
246        self.assertEqual(test.relobjdir, "")
247
248
249class TestSymbols(unittest.TestCase):
250    def _verify_doc(self, doc):
251        # Documentation should be of the format:
252        # """SUMMARY LINE
253        #
254        # EXTRA PARAGRAPHS
255        # """
256
257        self.assertNotIn("\r", doc)
258
259        lines = doc.split("\n")
260
261        # No trailing whitespace.
262        for line in lines[0:-1]:
263            self.assertEqual(line, line.rstrip())
264
265        self.assertGreater(len(lines), 0)
266        self.assertGreater(len(lines[0].strip()), 0)
267
268        # Last line should be empty.
269        self.assertEqual(lines[-1].strip(), "")
270
271    def test_documentation_formatting(self):
272        for typ, inp, doc in VARIABLES.values():
273            self._verify_doc(doc)
274
275        for attr, args, doc in FUNCTIONS.values():
276            self._verify_doc(doc)
277
278        for func, typ, doc in SPECIAL_VARIABLES.values():
279            self._verify_doc(doc)
280
281        for name, cls in SUBCONTEXTS.items():
282            self._verify_doc(cls.__doc__)
283
284            for name, v in cls.VARIABLES.items():
285                self._verify_doc(v[2])
286
287
288class TestPaths(unittest.TestCase):
289    @classmethod
290    def setUpClass(cls):
291        class Config(object):
292            pass
293
294        cls.config = config = Config()
295        config.topsrcdir = mozpath.abspath(os.curdir)
296        config.topobjdir = mozpath.abspath("obj")
297
298    def test_path(self):
299        config = self.config
300        ctxt1 = Context(config=config)
301        ctxt1.push_source(mozpath.join(config.topsrcdir, "foo", "moz.build"))
302        ctxt2 = Context(config=config)
303        ctxt2.push_source(mozpath.join(config.topsrcdir, "bar", "moz.build"))
304
305        path1 = Path(ctxt1, "qux")
306        self.assertIsInstance(path1, SourcePath)
307        self.assertEqual(path1, "qux")
308        self.assertEqual(path1.full_path, mozpath.join(config.topsrcdir, "foo", "qux"))
309
310        path2 = Path(ctxt2, "../foo/qux")
311        self.assertIsInstance(path2, SourcePath)
312        self.assertEqual(path2, "../foo/qux")
313        self.assertEqual(path2.full_path, mozpath.join(config.topsrcdir, "foo", "qux"))
314
315        self.assertEqual(path1, path2)
316
317        self.assertEqual(
318            path1.join("../../bar/qux").full_path,
319            mozpath.join(config.topsrcdir, "bar", "qux"),
320        )
321
322        path1 = Path(ctxt1, "/qux/qux")
323        self.assertIsInstance(path1, SourcePath)
324        self.assertEqual(path1, "/qux/qux")
325        self.assertEqual(path1.full_path, mozpath.join(config.topsrcdir, "qux", "qux"))
326
327        path2 = Path(ctxt2, "/qux/qux")
328        self.assertIsInstance(path2, SourcePath)
329        self.assertEqual(path2, "/qux/qux")
330        self.assertEqual(path2.full_path, mozpath.join(config.topsrcdir, "qux", "qux"))
331
332        self.assertEqual(path1, path2)
333
334        path1 = Path(ctxt1, "!qux")
335        self.assertIsInstance(path1, ObjDirPath)
336        self.assertEqual(path1, "!qux")
337        self.assertEqual(path1.full_path, mozpath.join(config.topobjdir, "foo", "qux"))
338
339        path2 = Path(ctxt2, "!../foo/qux")
340        self.assertIsInstance(path2, ObjDirPath)
341        self.assertEqual(path2, "!../foo/qux")
342        self.assertEqual(path2.full_path, mozpath.join(config.topobjdir, "foo", "qux"))
343
344        self.assertEqual(path1, path2)
345
346        path1 = Path(ctxt1, "!/qux/qux")
347        self.assertIsInstance(path1, ObjDirPath)
348        self.assertEqual(path1, "!/qux/qux")
349        self.assertEqual(path1.full_path, mozpath.join(config.topobjdir, "qux", "qux"))
350
351        path2 = Path(ctxt2, "!/qux/qux")
352        self.assertIsInstance(path2, ObjDirPath)
353        self.assertEqual(path2, "!/qux/qux")
354        self.assertEqual(path2.full_path, mozpath.join(config.topobjdir, "qux", "qux"))
355
356        self.assertEqual(path1, path2)
357
358        path1 = Path(ctxt1, path1)
359        self.assertIsInstance(path1, ObjDirPath)
360        self.assertEqual(path1, "!/qux/qux")
361        self.assertEqual(path1.full_path, mozpath.join(config.topobjdir, "qux", "qux"))
362
363        path2 = Path(ctxt2, path2)
364        self.assertIsInstance(path2, ObjDirPath)
365        self.assertEqual(path2, "!/qux/qux")
366        self.assertEqual(path2.full_path, mozpath.join(config.topobjdir, "qux", "qux"))
367
368        self.assertEqual(path1, path2)
369
370        path1 = Path(path1)
371        self.assertIsInstance(path1, ObjDirPath)
372        self.assertEqual(path1, "!/qux/qux")
373        self.assertEqual(path1.full_path, mozpath.join(config.topobjdir, "qux", "qux"))
374
375        self.assertEqual(path1, path2)
376
377        path2 = Path(path2)
378        self.assertIsInstance(path2, ObjDirPath)
379        self.assertEqual(path2, "!/qux/qux")
380        self.assertEqual(path2.full_path, mozpath.join(config.topobjdir, "qux", "qux"))
381
382        self.assertEqual(path1, path2)
383
384    def test_source_path(self):
385        config = self.config
386        ctxt = Context(config=config)
387        ctxt.push_source(mozpath.join(config.topsrcdir, "foo", "moz.build"))
388
389        path = SourcePath(ctxt, "qux")
390        self.assertEqual(path, "qux")
391        self.assertEqual(path.full_path, mozpath.join(config.topsrcdir, "foo", "qux"))
392        self.assertEqual(path.translated, mozpath.join(config.topobjdir, "foo", "qux"))
393
394        path = SourcePath(ctxt, "../bar/qux")
395        self.assertEqual(path, "../bar/qux")
396        self.assertEqual(path.full_path, mozpath.join(config.topsrcdir, "bar", "qux"))
397        self.assertEqual(path.translated, mozpath.join(config.topobjdir, "bar", "qux"))
398
399        path = SourcePath(ctxt, "/qux/qux")
400        self.assertEqual(path, "/qux/qux")
401        self.assertEqual(path.full_path, mozpath.join(config.topsrcdir, "qux", "qux"))
402        self.assertEqual(path.translated, mozpath.join(config.topobjdir, "qux", "qux"))
403
404        with self.assertRaises(ValueError):
405            SourcePath(ctxt, "!../bar/qux")
406
407        with self.assertRaises(ValueError):
408            SourcePath(ctxt, "!/qux/qux")
409
410        path = SourcePath(path)
411        self.assertIsInstance(path, SourcePath)
412        self.assertEqual(path, "/qux/qux")
413        self.assertEqual(path.full_path, mozpath.join(config.topsrcdir, "qux", "qux"))
414        self.assertEqual(path.translated, mozpath.join(config.topobjdir, "qux", "qux"))
415
416        path = Path(path)
417        self.assertIsInstance(path, SourcePath)
418
419    def test_objdir_path(self):
420        config = self.config
421        ctxt = Context(config=config)
422        ctxt.push_source(mozpath.join(config.topsrcdir, "foo", "moz.build"))
423
424        path = ObjDirPath(ctxt, "!qux")
425        self.assertEqual(path, "!qux")
426        self.assertEqual(path.full_path, mozpath.join(config.topobjdir, "foo", "qux"))
427
428        path = ObjDirPath(ctxt, "!../bar/qux")
429        self.assertEqual(path, "!../bar/qux")
430        self.assertEqual(path.full_path, mozpath.join(config.topobjdir, "bar", "qux"))
431
432        path = ObjDirPath(ctxt, "!/qux/qux")
433        self.assertEqual(path, "!/qux/qux")
434        self.assertEqual(path.full_path, mozpath.join(config.topobjdir, "qux", "qux"))
435
436        with self.assertRaises(ValueError):
437            path = ObjDirPath(ctxt, "../bar/qux")
438
439        with self.assertRaises(ValueError):
440            path = ObjDirPath(ctxt, "/qux/qux")
441
442        path = ObjDirPath(path)
443        self.assertIsInstance(path, ObjDirPath)
444        self.assertEqual(path, "!/qux/qux")
445        self.assertEqual(path.full_path, mozpath.join(config.topobjdir, "qux", "qux"))
446
447        path = Path(path)
448        self.assertIsInstance(path, ObjDirPath)
449
450    def test_absolute_path(self):
451        config = self.config
452        ctxt = Context(config=config)
453        ctxt.push_source(mozpath.join(config.topsrcdir, "foo", "moz.build"))
454
455        path = AbsolutePath(ctxt, "%/qux")
456        self.assertEqual(path, "%/qux")
457        self.assertEqual(path.full_path, "/qux")
458
459        with self.assertRaises(ValueError):
460            path = AbsolutePath(ctxt, "%qux")
461
462    def test_path_with_mixed_contexts(self):
463        config = self.config
464        ctxt1 = Context(config=config)
465        ctxt1.push_source(mozpath.join(config.topsrcdir, "foo", "moz.build"))
466        ctxt2 = Context(config=config)
467        ctxt2.push_source(mozpath.join(config.topsrcdir, "bar", "moz.build"))
468
469        path1 = Path(ctxt1, "qux")
470        path2 = Path(ctxt2, path1)
471        self.assertEqual(path2, path1)
472        self.assertEqual(path2, "qux")
473        self.assertEqual(path2.context, ctxt1)
474        self.assertEqual(path2.full_path, mozpath.join(config.topsrcdir, "foo", "qux"))
475
476        path1 = Path(ctxt1, "../bar/qux")
477        path2 = Path(ctxt2, path1)
478        self.assertEqual(path2, path1)
479        self.assertEqual(path2, "../bar/qux")
480        self.assertEqual(path2.context, ctxt1)
481        self.assertEqual(path2.full_path, mozpath.join(config.topsrcdir, "bar", "qux"))
482
483        path1 = Path(ctxt1, "/qux/qux")
484        path2 = Path(ctxt2, path1)
485        self.assertEqual(path2, path1)
486        self.assertEqual(path2, "/qux/qux")
487        self.assertEqual(path2.context, ctxt1)
488        self.assertEqual(path2.full_path, mozpath.join(config.topsrcdir, "qux", "qux"))
489
490        path1 = Path(ctxt1, "!qux")
491        path2 = Path(ctxt2, path1)
492        self.assertEqual(path2, path1)
493        self.assertEqual(path2, "!qux")
494        self.assertEqual(path2.context, ctxt1)
495        self.assertEqual(path2.full_path, mozpath.join(config.topobjdir, "foo", "qux"))
496
497        path1 = Path(ctxt1, "!../bar/qux")
498        path2 = Path(ctxt2, path1)
499        self.assertEqual(path2, path1)
500        self.assertEqual(path2, "!../bar/qux")
501        self.assertEqual(path2.context, ctxt1)
502        self.assertEqual(path2.full_path, mozpath.join(config.topobjdir, "bar", "qux"))
503
504        path1 = Path(ctxt1, "!/qux/qux")
505        path2 = Path(ctxt2, path1)
506        self.assertEqual(path2, path1)
507        self.assertEqual(path2, "!/qux/qux")
508        self.assertEqual(path2.context, ctxt1)
509        self.assertEqual(path2.full_path, mozpath.join(config.topobjdir, "qux", "qux"))
510
511    def test_path_typed_list(self):
512        config = self.config
513        ctxt1 = Context(config=config)
514        ctxt1.push_source(mozpath.join(config.topsrcdir, "foo", "moz.build"))
515        ctxt2 = Context(config=config)
516        ctxt2.push_source(mozpath.join(config.topsrcdir, "bar", "moz.build"))
517
518        paths = [
519            "!../bar/qux",
520            "!/qux/qux",
521            "!qux",
522            "../bar/qux",
523            "/qux/qux",
524            "qux",
525        ]
526
527        MyList = ContextDerivedTypedList(Path)
528        l = MyList(ctxt1)
529        l += paths
530
531        for p_str, p_path in zip(paths, l):
532            self.assertEqual(p_str, p_path)
533            self.assertEqual(p_path, Path(ctxt1, p_str))
534            self.assertEqual(
535                p_path.join("foo"), Path(ctxt1, mozpath.join(p_str, "foo"))
536            )
537
538        l2 = MyList(ctxt2)
539        l2 += paths
540
541        for p_str, p_path in zip(paths, l2):
542            self.assertEqual(p_str, p_path)
543            self.assertEqual(p_path, Path(ctxt2, p_str))
544
545        # Assigning with Paths from another context doesn't rebase them
546        l2 = MyList(ctxt2)
547        l2 += l
548
549        for p_str, p_path in zip(paths, l2):
550            self.assertEqual(p_str, p_path)
551            self.assertEqual(p_path, Path(ctxt1, p_str))
552
553        MyListWithFlags = ContextDerivedTypedListWithItems(
554            Path,
555            StrictOrderingOnAppendListWithFlagsFactory(
556                {
557                    "foo": bool,
558                }
559            ),
560        )
561        l = MyListWithFlags(ctxt1)
562        l += paths
563
564        for p in paths:
565            l[p].foo = True
566
567        for p_str, p_path in zip(paths, l):
568            self.assertEqual(p_str, p_path)
569            self.assertEqual(p_path, Path(ctxt1, p_str))
570            self.assertEqual(l[p_str].foo, True)
571            self.assertEqual(l[p_path].foo, True)
572
573    def test_path_typed_hierarchy_list(self):
574        config = self.config
575        ctxt1 = Context(config=config)
576        ctxt1.push_source(mozpath.join(config.topsrcdir, "foo", "moz.build"))
577        ctxt2 = Context(config=config)
578        ctxt2.push_source(mozpath.join(config.topsrcdir, "bar", "moz.build"))
579
580        paths = [
581            "!../bar/qux",
582            "!/qux/qux",
583            "!qux",
584            "../bar/qux",
585            "/qux/qux",
586            "qux",
587        ]
588
589        MyList = ContextDerivedTypedHierarchicalStringList(Path)
590        l = MyList(ctxt1)
591        l += paths
592        l.subdir += paths
593
594        for _, files in l.walk():
595            for p_str, p_path in zip(paths, files):
596                self.assertEqual(p_str, p_path)
597                self.assertEqual(p_path, Path(ctxt1, p_str))
598                self.assertEqual(
599                    p_path.join("foo"), Path(ctxt1, mozpath.join(p_str, "foo"))
600                )
601
602        l2 = MyList(ctxt2)
603        l2 += paths
604        l2.subdir += paths
605
606        for _, files in l2.walk():
607            for p_str, p_path in zip(paths, files):
608                self.assertEqual(p_str, p_path)
609                self.assertEqual(p_path, Path(ctxt2, p_str))
610
611        # Assigning with Paths from another context doesn't rebase them
612        l2 = MyList(ctxt2)
613        l2 += l
614
615        for _, files in l2.walk():
616            for p_str, p_path in zip(paths, files):
617                self.assertEqual(p_str, p_path)
618                self.assertEqual(p_path, Path(ctxt1, p_str))
619
620
621class TestTypedRecord(unittest.TestCase):
622    def test_fields(self):
623        T = ContextDerivedTypedRecord(("field1", six.text_type), ("field2", list))
624        inst = T(None)
625        self.assertEqual(inst.field1, "")
626        self.assertEqual(inst.field2, [])
627
628        inst.field1 = "foo"
629        inst.field2 += ["bar"]
630
631        self.assertEqual(inst.field1, "foo")
632        self.assertEqual(inst.field2, ["bar"])
633
634        with self.assertRaises(AttributeError):
635            inst.field3 = []
636
637    def test_coercion(self):
638        T = ContextDerivedTypedRecord(("field1", six.text_type), ("field2", list))
639        inst = T(None)
640        inst.field1 = 3
641        inst.field2 += ("bar",)
642        self.assertEqual(inst.field1, "3")
643        self.assertEqual(inst.field2, ["bar"])
644
645        with self.assertRaises(TypeError):
646            inst.field2 = object()
647
648
649class TestFiles(unittest.TestCase):
650    def test_aggregate_empty(self):
651        c = Context({})
652
653        files = {"moz.build": Files(c, "**")}
654
655        self.assertEqual(
656            Files.aggregate(files),
657            {
658                "bug_component_counts": [],
659                "recommended_bug_component": None,
660            },
661        )
662
663    def test_single_bug_component(self):
664        c = Context({})
665        f = Files(c, "**")
666        f["BUG_COMPONENT"] = ("Product1", "Component1")
667
668        files = {"moz.build": f}
669        self.assertEqual(
670            Files.aggregate(files),
671            {
672                "bug_component_counts": [(("Product1", "Component1"), 1)],
673                "recommended_bug_component": ("Product1", "Component1"),
674            },
675        )
676
677    def test_multiple_bug_components(self):
678        c = Context({})
679        f1 = Files(c, "**")
680        f1["BUG_COMPONENT"] = ("Product1", "Component1")
681
682        f2 = Files(c, "**")
683        f2["BUG_COMPONENT"] = ("Product2", "Component2")
684
685        files = {"a": f1, "b": f2, "c": f1}
686        self.assertEqual(
687            Files.aggregate(files),
688            {
689                "bug_component_counts": [
690                    (("Product1", "Component1"), 2),
691                    (("Product2", "Component2"), 1),
692                ],
693                "recommended_bug_component": ("Product1", "Component1"),
694            },
695        )
696
697    def test_no_recommended_bug_component(self):
698        """If there is no clear count winner, we don't recommend a bug component."""
699        c = Context({})
700        f1 = Files(c, "**")
701        f1["BUG_COMPONENT"] = ("Product1", "Component1")
702
703        f2 = Files(c, "**")
704        f2["BUG_COMPONENT"] = ("Product2", "Component2")
705
706        files = {"a": f1, "b": f2}
707        self.assertEqual(
708            Files.aggregate(files),
709            {
710                "bug_component_counts": [
711                    (("Product1", "Component1"), 1),
712                    (("Product2", "Component2"), 1),
713                ],
714                "recommended_bug_component": None,
715            },
716        )
717
718    def test_multiple_patterns(self):
719        c = Context({})
720        f1 = Files(c, "a/**")
721        f1["BUG_COMPONENT"] = ("Product1", "Component1")
722        f2 = Files(c, "b/**", "a/bar")
723        f2["BUG_COMPONENT"] = ("Product2", "Component2")
724
725        files = {"a/foo": f1, "a/bar": f2, "b/foo": f2}
726        self.assertEqual(
727            Files.aggregate(files),
728            {
729                "bug_component_counts": [
730                    (("Product2", "Component2"), 2),
731                    (("Product1", "Component1"), 1),
732                ],
733                "recommended_bug_component": ("Product2", "Component2"),
734            },
735        )
736
737
738if __name__ == "__main__":
739    main()
740