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