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