1# SPDX-License-Identifier: GPL-2.0+
2# Copyright 2020 Google LLC
3#
4
5"""Tests for the src_scan module
6
7This includes unit tests for scanning of the source code
8"""
9
10import copy
11import os
12import shutil
13import tempfile
14import unittest
15from unittest import mock
16
17from dtoc import src_scan
18from u_boot_pylib import terminal
19from u_boot_pylib import test_util
20from u_boot_pylib import tools
21
22OUR_PATH = os.path.dirname(os.path.realpath(__file__))
23
24EXPECT_WARN = {'rockchip_rk3288_grf':
25                   {'WARNING: the driver rockchip_rk3288_grf was not found in the driver list'}}
26
27class FakeNode:
28    """Fake Node object for testing"""
29    def __init__(self):
30        self.name = None
31        self.props = {}
32
33class FakeProp:
34    """Fake Prop object for testing"""
35    def __init__(self):
36        self.name = None
37        self.value = None
38
39# This is a test so is allowed to access private things in the module it is
40# testing
41# pylint: disable=W0212
42
43class TestSrcScan(unittest.TestCase):
44    """Tests for src_scan"""
45    @classmethod
46    def setUpClass(cls):
47        tools.prepare_output_dir(None)
48
49    @classmethod
50    def tearDownClass(cls):
51        tools.finalise_output_dir()
52
53    def test_simple(self):
54        """Simple test of scanning drivers"""
55        scan = src_scan.Scanner(None, None)
56        scan.scan_drivers()
57        self.assertIn('sandbox_gpio', scan._drivers)
58        self.assertIn('sandbox_gpio_alias', scan._driver_aliases)
59        self.assertEqual('sandbox_gpio',
60                         scan._driver_aliases['sandbox_gpio_alias'])
61        self.assertNotIn('sandbox_gpio_alias2', scan._driver_aliases)
62
63    def test_additional(self):
64        """Test with additional drivers to scan"""
65        scan = src_scan.Scanner(
66            None, [None, '', 'tools/dtoc/test/dtoc_test_scan_drivers.cxx'])
67        scan.scan_drivers()
68        self.assertIn('sandbox_gpio_alias2', scan._driver_aliases)
69        self.assertEqual('sandbox_gpio',
70                         scan._driver_aliases['sandbox_gpio_alias2'])
71
72    def test_unicode_error(self):
73        """Test running dtoc with an invalid unicode file
74
75        To be able to perform this test without adding a weird text file which
76        would produce issues when using checkpatch.pl or patman, generate the
77        file at runtime and then process it.
78        """
79        driver_fn = '/tmp/' + next(tempfile._get_candidate_names())
80        with open(driver_fn, 'wb+') as fout:
81            fout.write(b'\x81')
82
83        scan = src_scan.Scanner(None, [driver_fn])
84        with terminal.capture() as (stdout, _):
85            scan.scan_drivers()
86        self.assertRegex(stdout.getvalue(),
87                         r"Skipping file '.*' due to unicode error\s*")
88
89    def test_driver(self):
90        """Test the Driver class"""
91        i2c = 'I2C_UCLASS'
92        compat = {'rockchip,rk3288-grf': 'ROCKCHIP_SYSCON_GRF',
93                  'rockchip,rk3288-srf': None}
94        drv1 = src_scan.Driver('fred', 'fred.c')
95        drv2 = src_scan.Driver('mary', 'mary.c')
96        drv3 = src_scan.Driver('fred', 'fred.c')
97        drv1.uclass_id = i2c
98        drv1.compat = compat
99        drv2.uclass_id = i2c
100        drv2.compat = compat
101        drv3.uclass_id = i2c
102        drv3.compat = compat
103        self.assertEqual(
104            "Driver(name='fred', used=False, uclass_id='I2C_UCLASS', "
105            "compat={'rockchip,rk3288-grf': 'ROCKCHIP_SYSCON_GRF', "
106            "'rockchip,rk3288-srf': None}, priv=)", str(drv1))
107        self.assertEqual(drv1, drv3)
108        self.assertNotEqual(drv1, drv2)
109        self.assertNotEqual(drv2, drv3)
110
111    def test_scan_dirs(self):
112        """Test scanning of source directories"""
113        def add_file(fname):
114            pathname = os.path.join(indir, fname)
115            dirname = os.path.dirname(pathname)
116            os.makedirs(dirname, exist_ok=True)
117            tools.write_file(pathname, '', binary=False)
118            fname_list.append(pathname)
119
120        try:
121            indir = tempfile.mkdtemp(prefix='dtoc.')
122
123            fname_list = []
124            add_file('fname.c')
125            add_file('.git/ignoreme.c')
126            add_file('dir/fname2.c')
127            add_file('build-sandbox/ignoreme2.c')
128
129            # Mock out scan_driver and check that it is called with the
130            # expected files
131            with mock.patch.object(src_scan.Scanner, "scan_driver")  as mocked:
132                scan = src_scan.Scanner(indir, None)
133                scan.scan_drivers()
134            self.assertEqual(2, len(mocked.mock_calls))
135            self.assertEqual(mock.call(fname_list[0]),
136                             mocked.mock_calls[0])
137            # .git file should be ignored
138            self.assertEqual(mock.call(fname_list[2]),
139                             mocked.mock_calls[1])
140        finally:
141            shutil.rmtree(indir)
142
143    def test_scan(self):
144        """Test scanning of a driver"""
145        fname = os.path.join(OUR_PATH, '..', '..', 'drivers/i2c/tegra_i2c.c')
146        buff = tools.read_file(fname, False)
147        scan = src_scan.Scanner(None, None)
148        scan._parse_driver(fname, buff)
149        self.assertIn('i2c_tegra', scan._drivers)
150        drv = scan._drivers['i2c_tegra']
151        self.assertEqual('i2c_tegra', drv.name)
152        self.assertEqual('UCLASS_I2C', drv.uclass_id)
153        self.assertEqual(
154            {'nvidia,tegra114-i2c': 'TYPE_114',
155             'nvidia,tegra124-i2c': 'TYPE_114',
156             'nvidia,tegra20-i2c': 'TYPE_STD',
157             'nvidia,tegra20-i2c-dvc': 'TYPE_DVC'}, drv.compat)
158        self.assertEqual('i2c_bus', drv.priv)
159        self.assertEqual(1, len(scan._drivers))
160        self.assertEqual({}, scan._warnings)
161
162    def test_normalized_name(self):
163        """Test operation of get_normalized_compat_name()"""
164        prop = FakeProp()
165        prop.name = 'compatible'
166        prop.value = 'rockchip,rk3288-grf'
167        node = FakeNode()
168        node.props = {'compatible': prop}
169
170        # get_normalized_compat_name() uses this to check for root node
171        node.parent = FakeNode()
172
173        scan = src_scan.Scanner(None, None)
174        with terminal.capture() as (stdout, _):
175            name, aliases = scan.get_normalized_compat_name(node)
176        self.assertEqual('rockchip_rk3288_grf', name)
177        self.assertEqual([], aliases)
178        self.assertEqual(1, len(scan._missing_drivers))
179        self.assertEqual({'rockchip_rk3288_grf'}, scan._missing_drivers)
180        self.assertEqual('', stdout.getvalue().strip())
181        self.assertEqual(EXPECT_WARN, scan._warnings)
182
183        i2c = 'I2C_UCLASS'
184        compat = {'rockchip,rk3288-grf': 'ROCKCHIP_SYSCON_GRF',
185                  'rockchip,rk3288-srf': None}
186        drv = src_scan.Driver('fred', 'fred.c')
187        drv.uclass_id = i2c
188        drv.compat = compat
189        scan._drivers['rockchip_rk3288_grf'] = drv
190
191        scan._driver_aliases['rockchip_rk3288_srf'] = 'rockchip_rk3288_grf'
192
193        with terminal.capture() as (stdout, _):
194            name, aliases = scan.get_normalized_compat_name(node)
195        self.assertEqual('', stdout.getvalue().strip())
196        self.assertEqual('rockchip_rk3288_grf', name)
197        self.assertEqual([], aliases)
198        self.assertEqual(EXPECT_WARN, scan._warnings)
199
200        prop.value = 'rockchip,rk3288-srf'
201        with terminal.capture() as (stdout, _):
202            name, aliases = scan.get_normalized_compat_name(node)
203        self.assertEqual('', stdout.getvalue().strip())
204        self.assertEqual('rockchip_rk3288_grf', name)
205        self.assertEqual(['rockchip_rk3288_srf'], aliases)
206        self.assertEqual(EXPECT_WARN, scan._warnings)
207
208    def test_scan_errors(self):
209        """Test detection of scanning errors"""
210        buff = '''
211static const struct udevice_id tegra_i2c_ids2[] = {
212	{ .compatible = "nvidia,tegra114-i2c", .data = TYPE_114 },
213	{ }
214};
215
216U_BOOT_DRIVER(i2c_tegra) = {
217	.name	= "i2c_tegra",
218	.id	= UCLASS_I2C,
219	.of_match = tegra_i2c_ids,
220};
221'''
222        scan = src_scan.Scanner(None, None)
223        with self.assertRaises(ValueError) as exc:
224            scan._parse_driver('file.c', buff)
225        self.assertIn(
226            "file.c: Unknown compatible var 'tegra_i2c_ids' (found: tegra_i2c_ids2)",
227            str(exc.exception))
228
229    def test_of_match(self):
230        """Test detection of of_match_ptr() member"""
231        buff = '''
232static const struct udevice_id tegra_i2c_ids[] = {
233	{ .compatible = "nvidia,tegra114-i2c", .data = TYPE_114 },
234	{ }
235};
236
237U_BOOT_DRIVER(i2c_tegra) = {
238	.name	= "i2c_tegra",
239	.id	= UCLASS_I2C,
240	.of_match = of_match_ptr(tegra_i2c_ids),
241};
242'''
243        scan = src_scan.Scanner(None, None)
244        scan._parse_driver('file.c', buff)
245        self.assertIn('i2c_tegra', scan._drivers)
246        drv = scan._drivers['i2c_tegra']
247        self.assertEqual('i2c_tegra', drv.name)
248        self.assertEqual('', drv.phase)
249        self.assertEqual([], drv.headers)
250
251    def test_priv(self):
252        """Test collection of struct info from drivers"""
253        buff = '''
254static const struct udevice_id test_ids[] = {
255	{ .compatible = "nvidia,tegra114-i2c", .data = TYPE_114 },
256	{ }
257};
258
259U_BOOT_DRIVER(testing) = {
260	.name	= "testing",
261	.id	= UCLASS_I2C,
262	.of_match = test_ids,
263	.priv_auto	= sizeof(struct some_priv),
264	.plat_auto = sizeof(struct some_plat),
265	.per_child_auto	= sizeof(struct some_cpriv),
266	.per_child_plat_auto = sizeof(struct some_cplat),
267	DM_PHASE(tpl)
268	DM_HEADER(<i2c.h>)
269	DM_HEADER(<asm/clk.h>)
270};
271'''
272        scan = src_scan.Scanner(None, None)
273        scan._parse_driver('file.c', buff)
274        self.assertIn('testing', scan._drivers)
275        drv = scan._drivers['testing']
276        self.assertEqual('testing', drv.name)
277        self.assertEqual('UCLASS_I2C', drv.uclass_id)
278        self.assertEqual(
279            {'nvidia,tegra114-i2c': 'TYPE_114'}, drv.compat)
280        self.assertEqual('some_priv', drv.priv)
281        self.assertEqual('some_plat', drv.plat)
282        self.assertEqual('some_cpriv', drv.child_priv)
283        self.assertEqual('some_cplat', drv.child_plat)
284        self.assertEqual('tpl', drv.phase)
285        self.assertEqual(['<i2c.h>', '<asm/clk.h>'], drv.headers)
286        self.assertEqual(1, len(scan._drivers))
287
288    def test_uclass_scan(self):
289        """Test collection of uclass-driver info"""
290        buff = '''
291UCLASS_DRIVER(i2c) = {
292	.id		= UCLASS_I2C,
293	.name		= "i2c",
294	.flags		= DM_UC_FLAG_SEQ_ALIAS,
295	.priv_auto	= sizeof(struct some_priv),
296	.per_device_auto	= sizeof(struct per_dev_priv),
297	.per_device_plat_auto	= sizeof(struct per_dev_plat),
298	.per_child_auto	= sizeof(struct per_child_priv),
299	.per_child_plat_auto	= sizeof(struct per_child_plat),
300	.child_post_bind = i2c_child_post_bind,
301};
302
303'''
304        scan = src_scan.Scanner(None, None)
305        scan._parse_uclass_driver('file.c', buff)
306        self.assertIn('UCLASS_I2C', scan._uclass)
307        drv = scan._uclass['UCLASS_I2C']
308        self.assertEqual('i2c', drv.name)
309        self.assertEqual('UCLASS_I2C', drv.uclass_id)
310        self.assertEqual('some_priv', drv.priv)
311        self.assertEqual('per_dev_priv', drv.per_dev_priv)
312        self.assertEqual('per_dev_plat', drv.per_dev_plat)
313        self.assertEqual('per_child_priv', drv.per_child_priv)
314        self.assertEqual('per_child_plat', drv.per_child_plat)
315        self.assertEqual(1, len(scan._uclass))
316
317        drv2 = copy.deepcopy(drv)
318        self.assertEqual(drv, drv2)
319        drv2.priv = 'other_priv'
320        self.assertNotEqual(drv, drv2)
321
322        # The hashes only depend on the uclass ID, so should be equal
323        self.assertEqual(drv.__hash__(), drv2.__hash__())
324
325        self.assertEqual("UclassDriver(name='i2c', uclass_id='UCLASS_I2C')",
326                         str(drv))
327
328    def test_uclass_scan_errors(self):
329        """Test detection of uclass scanning errors"""
330        buff = '''
331UCLASS_DRIVER(i2c) = {
332	.name		= "i2c",
333};
334
335'''
336        scan = src_scan.Scanner(None, None)
337        with self.assertRaises(ValueError) as exc:
338            scan._parse_uclass_driver('file.c', buff)
339        self.assertIn("file.c: Cannot parse uclass ID in driver 'i2c'",
340                      str(exc.exception))
341
342    def test_struct_scan(self):
343        """Test collection of struct info"""
344        buff = '''
345/* some comment */
346struct some_struct1 {
347	struct i2c_msg *msgs;
348	uint nmsgs;
349};
350'''
351        scan = src_scan.Scanner(None, None)
352        scan._basedir = os.path.join(OUR_PATH, '..', '..')
353        scan._parse_structs('arch/arm/include/asm/file.h', buff)
354        self.assertIn('some_struct1', scan._structs)
355        struc = scan._structs['some_struct1']
356        self.assertEqual('some_struct1', struc.name)
357        self.assertEqual('asm/file.h', struc.fname)
358
359        buff = '''
360/* another comment */
361struct another_struct {
362	int speed_hz;
363	int max_transaction_bytes;
364};
365'''
366        scan._parse_structs('include/file2.h', buff)
367        self.assertIn('another_struct', scan._structs)
368        struc = scan._structs['another_struct']
369        self.assertEqual('another_struct', struc.name)
370        self.assertEqual('file2.h', struc.fname)
371
372        self.assertEqual(2, len(scan._structs))
373
374        self.assertEqual("Struct(name='another_struct', fname='file2.h')",
375                         str(struc))
376
377    def test_struct_scan_errors(self):
378        """Test scanning a header file with an invalid unicode file"""
379        output = tools.get_output_filename('output.h')
380        tools.write_file(output, b'struct this is a test \x81 of bad unicode')
381
382        scan = src_scan.Scanner(None, None)
383        with terminal.capture() as (stdout, _):
384            scan.scan_header(output)
385        self.assertIn('due to unicode error', stdout.getvalue())
386
387    def setup_dup_drivers(self, name, phase=''):
388        """Set up for a duplcate test
389
390        Returns:
391            tuple:
392                Scanner to use
393                Driver record for first driver
394                Text of second driver declaration
395                Node for driver 1
396        """
397        driver1 = '''
398static const struct udevice_id test_ids[] = {
399	{ .compatible = "nvidia,tegra114-i2c", .data = TYPE_114 },
400	{ }
401};
402
403U_BOOT_DRIVER(%s) = {
404	.name	= "testing",
405	.id	= UCLASS_I2C,
406	.of_match = test_ids,
407	%s
408};
409''' % (name, 'DM_PHASE(%s)' % phase if phase else '')
410        driver2 = '''
411static const struct udevice_id test_ids[] = {
412	{ .compatible = "nvidia,tegra114-dvc" },
413	{ }
414};
415
416U_BOOT_DRIVER(%s) = {
417	.name	= "testing",
418	.id	= UCLASS_RAM,
419	.of_match = test_ids,
420};
421''' % name
422        scan = src_scan.Scanner(None, None, phase)
423        scan._parse_driver('file1.c', driver1)
424        self.assertIn(name, scan._drivers)
425        drv1 = scan._drivers[name]
426
427        prop = FakeProp()
428        prop.name = 'compatible'
429        prop.value = 'nvidia,tegra114-i2c'
430        node = FakeNode()
431        node.name = 'testing'
432        node.props = {'compatible': prop}
433
434        # get_normalized_compat_name() uses this to check for root node
435        node.parent = FakeNode()
436
437        return scan, drv1, driver2, node
438
439    def test_dup_drivers(self):
440        """Test handling of duplicate drivers"""
441        name = 'nvidia_tegra114_i2c'
442        scan, drv1, driver2, node = self.setup_dup_drivers(name)
443        self.assertEqual('', drv1.phase)
444
445        # The driver should not have a duplicate yet
446        self.assertEqual([], drv1.dups)
447
448        scan._parse_driver('file2.c', driver2)
449
450        # The first driver should now be a duplicate of the second
451        drv2 = scan._drivers[name]
452        self.assertEqual('', drv2.phase)
453        self.assertEqual(1, len(drv2.dups))
454        self.assertEqual([drv1], drv2.dups)
455
456        # There is no way to distinguish them, so we should expect a warning
457        self.assertTrue(drv2.warn_dups)
458
459        # We should see a warning
460        with terminal.capture() as (stdout, _):
461            scan.mark_used([node])
462        self.assertEqual(
463            "Warning: Duplicate driver name 'nvidia_tegra114_i2c' (orig=file2.c, dups=file1.c)",
464            stdout.getvalue().strip())
465
466    def test_dup_drivers_phase(self):
467        """Test handling of duplicate drivers but with different phases"""
468        name = 'nvidia_tegra114_i2c'
469        scan, drv1, driver2, node = self.setup_dup_drivers(name, 'spl')
470        scan._parse_driver('file2.c', driver2)
471        self.assertEqual('spl', drv1.phase)
472
473        # The second driver should now be a duplicate of the second
474        self.assertEqual(1, len(drv1.dups))
475        drv2 = drv1.dups[0]
476
477        # The phase is different, so we should not warn of dups
478        self.assertFalse(drv1.warn_dups)
479
480        # We should not see a warning
481        with terminal.capture() as (stdout, _):
482            scan.mark_used([node])
483        self.assertEqual('', stdout.getvalue().strip())
484
485    def test_sequence(self):
486        """Test assignment of sequence numnbers"""
487        scan = src_scan.Scanner(None, None, '')
488        node = FakeNode()
489        uc = src_scan.UclassDriver('UCLASS_I2C')
490        node.uclass = uc
491        node.driver = True
492        node.seq = -1
493        node.path = 'mypath'
494        uc.alias_num_to_node[2] = node
495
496        # This should assign 3 (after the 2 that exists)
497        seq = scan.assign_seq(node)
498        self.assertEqual(3, seq)
499        self.assertEqual({'mypath': 3}, uc.alias_path_to_num)
500        self.assertEqual({2: node, 3: node}, uc.alias_num_to_node)
501
502    def test_scan_warnings(self):
503        """Test detection of scanning warnings"""
504        buff = '''
505static const struct udevice_id tegra_i2c_ids[] = {
506	{ .compatible = "nvidia,tegra114-i2c", .data = TYPE_114 },
507	{ }
508};
509
510U_BOOT_DRIVER(i2c_tegra) = {
511	.name	= "i2c_tegra",
512	.id	= UCLASS_I2C,
513	.of_match = tegra_i2c_ids + 1,
514};
515'''
516        # The '+ 1' above should generate a warning
517
518        prop = FakeProp()
519        prop.name = 'compatible'
520        prop.value = 'rockchip,rk3288-grf'
521        node = FakeNode()
522        node.props = {'compatible': prop}
523
524        # get_normalized_compat_name() uses this to check for root node
525        node.parent = FakeNode()
526
527        scan = src_scan.Scanner(None, None)
528        scan._parse_driver('file.c', buff)
529        self.assertEqual(
530            {'i2c_tegra':
531                 {"file.c: Warning: unexpected suffix ' + 1' on .of_match line for compat 'tegra_i2c_ids'"}},
532            scan._warnings)
533
534        tprop = FakeProp()
535        tprop.name = 'compatible'
536        tprop.value = 'nvidia,tegra114-i2c'
537        tnode = FakeNode()
538        tnode.props = {'compatible': tprop}
539
540        # get_normalized_compat_name() uses this to check for root node
541        tnode.parent = FakeNode()
542
543        with terminal.capture() as (stdout, _):
544            scan.get_normalized_compat_name(node)
545            scan.get_normalized_compat_name(tnode)
546        self.assertEqual('', stdout.getvalue().strip())
547
548        self.assertEqual(2, len(scan._missing_drivers))
549        self.assertEqual({'rockchip_rk3288_grf', 'nvidia_tegra114_i2c'},
550                         scan._missing_drivers)
551        with terminal.capture() as (stdout, _):
552            scan.show_warnings()
553        self.assertIn('rockchip_rk3288_grf', stdout.getvalue())
554
555        # This should show just the rockchip warning, since the tegra driver
556        # is not in self._missing_drivers
557        scan._missing_drivers.remove('nvidia_tegra114_i2c')
558        with terminal.capture() as (stdout, _):
559            scan.show_warnings()
560        self.assertIn('rockchip_rk3288_grf', stdout.getvalue())
561        self.assertNotIn('tegra_i2c_ids', stdout.getvalue())
562
563        # Do a similar thing with used drivers. By marking the tegra driver as
564        # used, the warning related to that driver will be shown
565        drv = scan._drivers['i2c_tegra']
566        drv.used = True
567        with terminal.capture() as (stdout, _):
568            scan.show_warnings()
569        self.assertIn('rockchip_rk3288_grf', stdout.getvalue())
570        self.assertIn('tegra_i2c_ids', stdout.getvalue())
571
572        # Add a warning to make sure multiple warnings are shown
573        scan._warnings['i2c_tegra'].update(
574            scan._warnings['nvidia_tegra114_i2c'])
575        del scan._warnings['nvidia_tegra114_i2c']
576        with terminal.capture() as (stdout, _):
577            scan.show_warnings()
578        self.assertEqual('''i2c_tegra: WARNING: the driver nvidia_tegra114_i2c was not found in the driver list
579         : file.c: Warning: unexpected suffix ' + 1' on .of_match line for compat 'tegra_i2c_ids'
580
581rockchip_rk3288_grf: WARNING: the driver rockchip_rk3288_grf was not found in the driver list
582
583''',
584            stdout.getvalue())
585        self.assertIn('tegra_i2c_ids', stdout.getvalue())
586
587    def scan_uclass_warning(self):
588        """Test a missing .uclass in the driver"""
589        buff = '''
590static const struct udevice_id tegra_i2c_ids[] = {
591	{ .compatible = "nvidia,tegra114-i2c", .data = TYPE_114 },
592	{ }
593};
594
595U_BOOT_DRIVER(i2c_tegra) = {
596	.name	= "i2c_tegra",
597	.of_match = tegra_i2c_ids,
598};
599'''
600        scan = src_scan.Scanner(None, None)
601        scan._parse_driver('file.c', buff)
602        self.assertEqual(
603            {'i2c_tegra': {'Missing .uclass in file.c'}},
604            scan._warnings)
605
606    def scan_compat_warning(self):
607        """Test a missing .compatible in the driver"""
608        buff = '''
609static const struct udevice_id tegra_i2c_ids[] = {
610	{ .compatible = "nvidia,tegra114-i2c", .data = TYPE_114 },
611	{ }
612};
613
614U_BOOT_DRIVER(i2c_tegra) = {
615	.name	= "i2c_tegra",
616	.id	= UCLASS_I2C,
617};
618'''
619        scan = src_scan.Scanner(None, None)
620        scan._parse_driver('file.c', buff)
621        self.assertEqual(
622            {'i2c_tegra': {'Missing .compatible in file.c'}},
623            scan._warnings)
624