1#!/usr/bin/env python3
2# Copyright (c) 2023 Intel Corporation
3#
4# SPDX-License-Identifier: Apache-2.0
5"""
6Tests for hardwaremap.py classes' methods
7"""
8
9import mock
10import pytest
11import sys
12
13from pathlib import Path
14
15from twisterlib.hardwaremap import(
16    DUT,
17    HardwareMap
18)
19
20
21@pytest.fixture
22def mocked_hm():
23    duts = [
24        DUT(platform='p1', id=1, serial='s1', product='pr1', connected=True),
25        DUT(platform='p2', id=2, serial='s2', product='pr2', connected=False),
26        DUT(platform='p3', id=3, serial='s3', product='pr3', connected=True),
27        DUT(platform='p4', id=4, serial='s4', product='pr4', connected=False),
28        DUT(platform='p5', id=5, serial='s5', product='pr5', connected=True),
29        DUT(platform='p6', id=6, serial='s6', product='pr6', connected=False),
30        DUT(platform='p7', id=7, serial='s7', product='pr7', connected=True),
31        DUT(platform='p8', id=8, serial='s8', product='pr8', connected=False)
32    ]
33
34    hm = HardwareMap(env=mock.Mock())
35    hm.duts = duts
36    hm.detected = duts[:5]
37
38    return hm
39
40
41TESTDATA_1 = [
42    (
43        {},
44        {'baud': 115200, 'lock': mock.ANY, 'flash_timeout': 60},
45        '<None (None) on None>'
46    ),
47    (
48        {
49            'id': 'dummy id',
50            'serial': 'dummy serial',
51            'serial_baud': 4400,
52            'platform': 'dummy platform',
53            'product': 'dummy product',
54            'serial_pty': 'dummy serial pty',
55            'connected': True,
56            'runner_params': ['dummy', 'runner', 'params'],
57            'pre_script': 'dummy pre script',
58            'post_script': 'dummy post script',
59            'post_flash_script': 'dummy post flash script',
60            'runner': 'dummy runner',
61            'flash_timeout': 30,
62            'flash_with_test': True,
63            'script_param': {
64                'pre_script_timeout' : 30,
65                'post_flash_timeout' : 30,
66                'post_script_timeout' : 30,
67                }
68        },
69        {
70            'lock': mock.ANY,
71            'id': 'dummy id',
72            'serial': 'dummy serial',
73            'baud': 4400,
74            'platform': 'dummy platform',
75            'product': 'dummy product',
76            'serial_pty': 'dummy serial pty',
77            'connected': True,
78            'runner_params': ['dummy', 'runner', 'params'],
79            'pre_script': 'dummy pre script',
80            'post_script': 'dummy post script',
81            'post_flash_script': 'dummy post flash script',
82            'runner': 'dummy runner',
83            'flash_timeout': 30,
84            'flash_with_test': True,
85            'script_param': {
86                'pre_script_timeout' : 30,
87                'post_flash_timeout' : 30,
88                'post_script_timeout' : 30,
89                }
90        },
91        '<dummy platform (dummy product) on dummy serial>'
92    ),
93]
94
95
96@pytest.mark.parametrize(
97    'kwargs, expected_dict, expected_repr',
98    TESTDATA_1,
99    ids=['no information', 'full information']
100)
101def test_dut(kwargs, expected_dict, expected_repr):
102    d = DUT(**kwargs)
103
104    assert d.available
105    assert d.counter == 0
106
107    d.available = False
108    d.counter = 1
109
110    assert not d.available
111    assert d.counter == 1
112
113    assert d.to_dict() == expected_dict
114    assert d.__repr__() == expected_repr
115
116
117TESTDATA_2 = [
118    ('ghm.yaml', mock.ANY, mock.ANY, [], mock.ANY, mock.ANY, mock.ANY, 0,
119     True, True, False, False, False, False, []),
120    (None, False, 'hm.yaml', [], mock.ANY, mock.ANY, mock.ANY, 0,
121     False, False, True, True, False, False, []),
122    (None, True, 'hm.yaml', [], mock.ANY, mock.ANY, ['fix'], 1,
123     False, False, True, False, False, True, ['p1', 'p3', 'p5', 'p7']),
124    (None, True, 'hm.yaml', ['pX'], mock.ANY, mock.ANY, ['fix'], 1,
125     False, False, True, False, False, True, ['pX']),
126    (None, True, None, ['p'], 's', None, ['fix'], 1,
127     False, False, False, False, True, True, ['p']),
128    (None, True, None, ['p'], None, 'spty', ['fix'], 1,
129     False, False, False, False, True, True, ['p']),
130]
131
132
133@pytest.mark.parametrize(
134    'generate_hardware_map, device_testing, hardware_map, platform,' \
135    ' device_serial, device_serial_pty, fixtures,' \
136    ' return_code, expect_scan, expect_save, expect_load,' \
137    ' expect_dump, expect_add_device, expect_fixtures, expected_platforms',
138    TESTDATA_2,
139    ids=['generate hardware map', 'existing hardware map',
140         'device testing with hardware map, no platform',
141         'device testing with hardware map with platform',
142         'device testing with device serial',
143         'device testing with device serial pty']
144)
145def test_hardwaremap_discover(
146    caplog,
147    mocked_hm,
148    generate_hardware_map,
149    device_testing,
150    hardware_map,
151    platform,
152    device_serial,
153    device_serial_pty,
154    fixtures,
155    return_code,
156    expect_scan,
157    expect_save,
158    expect_load,
159    expect_dump,
160    expect_add_device,
161    expect_fixtures,
162    expected_platforms
163):
164    def mock_load(*args):
165        mocked_hm.platform = platform
166
167    mocked_hm.scan = mock.Mock()
168    mocked_hm.save = mock.Mock()
169    mocked_hm.load = mock.Mock(side_effect=mock_load)
170    mocked_hm.dump = mock.Mock()
171    mocked_hm.add_device = mock.Mock()
172
173    mocked_hm.options.device_flash_with_test = True
174    mocked_hm.options.device_flash_timeout = 15
175    mocked_hm.options.pre_script = 'dummy pre script'
176    mocked_hm.options.platform = platform
177    mocked_hm.options.device_serial = device_serial
178    mocked_hm.options.device_serial_pty = device_serial_pty
179    mocked_hm.options.device_testing = device_testing
180    mocked_hm.options.hardware_map = hardware_map
181    mocked_hm.options.persistent_hardware_map = mock.Mock()
182    mocked_hm.options.generate_hardware_map = generate_hardware_map
183    mocked_hm.options.fixture = fixtures
184
185    returncode = mocked_hm.discover()
186
187    assert returncode == return_code
188
189    if expect_scan:
190        mocked_hm.scan.assert_called_once_with(
191            persistent=mocked_hm.options.persistent_hardware_map
192        )
193    if expect_save:
194        mocked_hm.save.assert_called_once_with(
195            mocked_hm.options.generate_hardware_map
196        )
197    if expect_load:
198        mocked_hm.load.assert_called_once_with(
199            mocked_hm.options.hardware_map
200        )
201    if expect_dump:
202        mocked_hm.dump.assert_called_once_with(
203            connected_only=True
204        )
205    if expect_add_device:
206        mocked_hm.add_device.assert_called_once()
207
208    if expect_fixtures:
209        assert all(
210            [all(
211                    [fixture in dut.fixtures for fixture in fixtures]
212            ) for dut in mocked_hm.duts]
213        )
214
215    assert sorted(expected_platforms) == sorted(mocked_hm.options.platform)
216
217
218def test_hardwaremap_summary(capfd, mocked_hm):
219    selected_platforms = ['p0', 'p1', 'p6', 'p7']
220
221    mocked_hm.summary(selected_platforms)
222
223    expected = """
224Hardware distribution summary:
225
226| Board   |   ID |   Counter |   Failures |
227|---------|------|-----------|------------|
228| p1      |    1 |         0 |          0 |
229| p7      |    7 |         0 |          0 |
230"""
231
232    out, err = capfd.readouterr()
233    sys.stdout.write(out)
234    sys.stderr.write(err)
235
236    assert expected in out
237
238
239TESTDATA_3 = [
240    (True),
241    (False)
242]
243
244
245@pytest.mark.parametrize(
246    'is_pty',
247    TESTDATA_3,
248    ids=['pty', 'not pty']
249)
250def test_hardwaremap_add_device(is_pty):
251    hm = HardwareMap(env=mock.Mock())
252
253    serial = 'dummy'
254    platform = 'p0'
255    pre_script = 'dummy pre script'
256    hm.add_device(serial, platform, pre_script, is_pty)
257
258    assert len(hm.duts) == 1
259    if is_pty:
260        assert hm.duts[0].serial_pty == 'dummy' if is_pty else None
261        assert hm.duts[0].serial is None
262    else:
263        assert hm.duts[0].serial_pty is None
264        assert hm.duts[0].serial == 'dummy'
265
266
267def test_hardwaremap_load():
268    map_file = \
269"""
270- id: id0
271  platform: p0
272  product: pr0
273  runner: r0
274  flash_with_test: True
275  flash_timeout: 15
276  baud: 14400
277  fixtures:
278  - dummy fixture 1
279  - dummy fixture 2
280  connected: True
281  serial: 'dummy'
282- id: id1
283  platform: p1
284  product: pr1
285  runner: r1
286  connected: True
287  serial_pty: 'dummy'
288- id: id2
289  platform: p2
290  product: pr2
291  runner: r2
292  connected: True
293"""
294    map_filename = 'map-file.yaml'
295
296    builtin_open = open
297
298    def mock_open(*args, **kwargs):
299        if args[0] == map_filename:
300            return mock.mock_open(read_data=map_file)(*args, **kwargs)
301        return builtin_open(*args, **kwargs)
302
303    hm = HardwareMap(env=mock.Mock())
304    hm.options.device_flash_timeout = 30
305    hm.options.device_flash_with_test = False
306
307    with mock.patch('builtins.open', mock_open):
308        hm.load(map_filename)
309
310    expected = {
311        'id0': {
312            'platform': 'p0',
313            'product': 'pr0',
314            'runner': 'r0',
315            'flash_timeout': 15,
316            'flash_with_test': True,
317            'baud': 14400,
318            'fixtures': ['dummy fixture 1', 'dummy fixture 2'],
319            'connected': True,
320            'serial': 'dummy',
321            'serial_pty': None,
322        },
323        'id1': {
324            'platform': 'p1',
325            'product': 'pr1',
326            'runner': 'r1',
327            'flash_timeout': 30,
328            'flash_with_test': False,
329            'baud': 115200,
330            'fixtures': [],
331            'connected': True,
332            'serial': None,
333            'serial_pty': 'dummy',
334        },
335    }
336
337    for dut in hm.duts:
338        assert dut.id in expected
339        assert all([getattr(dut, k) == v for k, v in expected[dut.id].items()])
340
341
342TESTDATA_4 = [
343    (
344        True,
345        'Linux',
346        ['<p1 (pr1) on s1>', '<p2 (pr2) on s2>', '<p3 (pr3) on s3>',
347         '<p4 (pr4) on s4>', '<p5 (pr5) on s5>',
348         '<unknown (TI product) on /dev/serial/by-id/basic-file1>',
349         '<unknown (product123) on dummy device>',
350         '<unknown (unknown) on /dev/serial/by-id/basic-file2-link>']
351    ),
352    (
353        True,
354        'nt',
355        ['<p1 (pr1) on s1>', '<p2 (pr2) on s2>', '<p3 (pr3) on s3>',
356         '<p4 (pr4) on s4>', '<p5 (pr5) on s5>',
357         '<unknown (TI product) on /dev/serial/by-id/basic-file1>',
358         '<unknown (product123) on dummy device>',
359         '<unknown (unknown) on /dev/serial/by-id/basic-file2>']
360    ),
361    (
362        False,
363        'Linux',
364        ['<p1 (pr1) on s1>', '<p2 (pr2) on s2>', '<p3 (pr3) on s3>',
365         '<p4 (pr4) on s4>', '<p5 (pr5) on s5>',
366         '<unknown (TI product) on /dev/serial/by-id/basic-file1>',
367         '<unknown (product123) on dummy device>',
368         '<unknown (unknown) on /dev/serial/by-id/basic-file2>']
369    )
370]
371
372
373@pytest.mark.parametrize(
374    'persistent, system, expected_reprs',
375    TESTDATA_4,
376    ids=['linux persistent map', 'no map (not linux)', 'no map (nonpersistent)']
377)
378def test_hardwaremap_scan(
379    caplog,
380    mocked_hm,
381    persistent,
382    system,
383    expected_reprs
384):
385    def mock_resolve(path):
386        if str(path).endswith('-link'):
387            return Path(str(path)[:-5])
388        return path
389
390    def mock_iterdir(path):
391        return [
392            Path(path / 'basic-file1'),
393            Path(path / 'basic-file2-link')
394        ]
395
396    def mock_exists(path):
397        return True
398
399    mocked_hm.manufacturer = ['dummy manufacturer', 'Texas Instruments']
400    mocked_hm.runner_mapping = {
401        'dummy runner': ['product[0-9]+',],
402        'other runner': ['other TI product', 'TI product']
403    }
404
405    comports_mock = [
406        mock.Mock(
407            manufacturer='wrong manufacturer',
408            location='wrong location',
409            serial_number='wrong number',
410            product='wrong product',
411            device='wrong device'
412        ),
413        mock.Mock(
414            manufacturer='dummy manufacturer',
415            location='dummy location',
416            serial_number='dummy number',
417            product=None,
418            device='/dev/serial/by-id/basic-file2'
419        ),
420        mock.Mock(
421            manufacturer='dummy manufacturer',
422            location='dummy location',
423            serial_number='dummy number',
424            product='product123',
425            device='dummy device'
426        ),
427        mock.Mock(
428            manufacturer='Texas Instruments',
429            location='serial1',
430            serial_number='TI1',
431            product='TI product',
432            device='TI device1'
433        ),
434        mock.Mock(
435            manufacturer='Texas Instruments',
436            location='serial0',
437            serial_number='TI0',
438            product='TI product',
439            device='/dev/serial/by-id/basic-file1'
440        ),
441    ]
442
443    with mock.patch('platform.system', return_value=system), \
444         mock.patch('serial.tools.list_ports.comports',
445                    return_value=comports_mock), \
446         mock.patch('twisterlib.hardwaremap.Path.resolve',
447                    autospec=True, side_effect=mock_resolve), \
448         mock.patch('twisterlib.hardwaremap.Path.iterdir',
449                    autospec=True, side_effect=mock_iterdir), \
450         mock.patch('twisterlib.hardwaremap.Path.exists',
451                    autospec=True, side_effect=mock_exists):
452        mocked_hm.scan(persistent)
453
454    assert sorted([d.__repr__() for d in mocked_hm.detected]) == \
455           sorted(expected_reprs)
456
457    assert 'Scanning connected hardware...' in caplog.text
458    assert 'Unsupported device (wrong manufacturer): %s' % comports_mock[0] \
459           in caplog.text
460
461
462TESTDATA_5 = [
463    (
464        None,
465        [{
466            'platform': 'p1',
467            'id': 1,
468            'runner': mock.ANY,
469            'serial': 's1',
470            'product': 'pr1',
471            'connected': True
472        },
473        {
474            'platform': 'p2',
475            'id': 2,
476            'runner': mock.ANY,
477            'serial': 's2',
478            'product': 'pr2',
479            'connected': False
480        },
481        {
482            'platform': 'p3',
483            'id': 3,
484            'runner': mock.ANY,
485            'serial': 's3',
486            'product': 'pr3',
487            'connected': True
488        },
489        {
490            'platform': 'p4',
491            'id': 4,
492            'runner': mock.ANY,
493            'serial': 's4',
494            'product': 'pr4',
495            'connected': False
496        },
497        {
498            'platform': 'p5',
499            'id': 5,
500            'runner': mock.ANY,
501            'serial': 's5',
502            'product': 'pr5',
503            'connected': True
504        }]
505    ),
506    (
507        '',
508        [{
509            'serial': 's1',
510            'baud': 115200,
511            'platform': 'p1',
512            'connected': True,
513            'id': 1,
514            'product': 'pr1',
515            'lock': mock.ANY,
516            'flash_timeout': 60
517        },
518        {
519            'serial': 's2',
520            'baud': 115200,
521            'platform': 'p2',
522            'id': 2,
523            'product': 'pr2',
524            'lock': mock.ANY,
525            'flash_timeout': 60
526        },
527        {
528            'serial': 's3',
529            'baud': 115200,
530            'platform': 'p3',
531            'connected': True,
532            'id': 3,
533            'product': 'pr3',
534            'lock': mock.ANY,
535            'flash_timeout': 60
536        },
537        {
538            'serial': 's4',
539            'baud': 115200,
540            'platform': 'p4',
541            'id': 4,
542            'product': 'pr4',
543            'lock': mock.ANY,
544            'flash_timeout': 60
545        },
546        {
547            'serial': 's5',
548            'baud': 115200,
549            'platform': 'p5',
550            'connected': True,
551            'id': 5,
552            'product': 'pr5',
553            'lock': mock.ANY,
554            'flash_timeout': 60
555        }]
556    ),
557    (
558"""
559- id: 4
560  platform: p4
561  product: pr4
562  connected: True
563  serial: s4
564- id: 0
565  platform: p0
566  product: pr0
567  connected: True
568  serial: s0
569- id: 10
570  platform: p10
571  product: pr10
572  connected: False
573  serial: s10
574- id: 5
575  platform: p5-5
576  product: pr5-5
577  connected: True
578  serial: s5-5
579""",
580        [{
581            'id': 0,
582            'platform': 'p0',
583            'product': 'pr0',
584            'connected': False,
585            'serial': None
586        },
587        {
588            'id': 4,
589            'platform': 'p4',
590            'product': 'pr4',
591            'connected': True,
592            'serial': 's4'
593        },
594        {
595            'id': 5,
596            'platform': 'p5-5',
597            'product': 'pr5-5',
598            'connected': False,
599            'serial': None
600        },
601        {
602            'id': 10,
603            'platform': 'p10',
604            'product': 'pr10',
605            'connected': False,
606            'serial': None
607        },
608        {
609            'serial': 's1',
610            'baud': 115200,
611            'platform': 'p1',
612            'connected': True,
613            'id': 1,
614            'product': 'pr1',
615            'lock': mock.ANY,
616            'flash_timeout': 60
617        },
618        {
619            'serial': 's2',
620            'baud': 115200,
621            'platform': 'p2',
622            'id': 2,
623            'product': 'pr2',
624            'lock': mock.ANY,
625            'flash_timeout': 60
626        },
627        {
628            'serial': 's3',
629            'baud': 115200,
630            'platform': 'p3',
631            'connected': True,
632            'id': 3,
633            'product': 'pr3',
634            'lock': mock.ANY,
635            'flash_timeout': 60
636        },
637        {
638            'serial': 's5',
639            'baud': 115200,
640            'platform': 'p5',
641            'connected': True,
642            'id': 5,
643            'product': 'pr5',
644            'lock': mock.ANY,
645            'flash_timeout': 60
646        }]
647    ),
648]
649
650
651@pytest.mark.parametrize(
652    'hwm, expected_dump',
653    TESTDATA_5,
654    ids=['no map', 'empty map', 'map exists']
655)
656def test_hardwaremap_save(mocked_hm, hwm, expected_dump):
657    read_mock = mock.mock_open(read_data=hwm)
658    write_mock = mock.mock_open()
659
660    def mock_open(filename, mode='r'):
661        if mode == 'r':
662            return read_mock()
663        elif mode == 'w':
664            return write_mock()
665
666
667    mocked_hm.load = mock.Mock()
668    mocked_hm.dump = mock.Mock()
669
670    open_mock = mock.Mock(side_effect=mock_open)
671    dump_mock = mock.Mock()
672
673    with mock.patch('os.path.exists', return_value=hwm is not None), \
674         mock.patch('builtins.open', open_mock), \
675         mock.patch('twisterlib.hardwaremap.yaml.dump', dump_mock):
676        mocked_hm.save('hwm.yaml')
677
678    dump_mock.assert_called_once_with(expected_dump, mock.ANY, Dumper=mock.ANY,
679                                      default_flow_style=mock.ANY)
680
681
682TESTDATA_6 = [
683    (
684        ['p1', 'p3', 'p5', 'p7'],
685        [],
686        True,
687        True,
688"""
689| Platform   |   ID | Serial device   |
690|------------|------|-----------------|
691| p1         |    1 | s1              |
692| p3         |    3 | s3              |
693| p5         |    5 | s5              |
694"""
695    ),
696    (
697        [],
698        ['?', '??', '???'],
699        False,
700        False,
701"""
702| ?   |   ?? | ???   |
703|-----|------|-------|
704| p1  |    1 | s1    |
705| p2  |    2 | s2    |
706| p3  |    3 | s3    |
707| p4  |    4 | s4    |
708| p5  |    5 | s5    |
709| p6  |    6 | s6    |
710| p7  |    7 | s7    |
711| p8  |    8 | s8    |
712"""
713    ),
714]
715
716
717@pytest.mark.parametrize(
718    'filtered, header, connected_only, detected, expected_out',
719    TESTDATA_6,
720    ids=['detected no header', 'all with header']
721)
722def test_hardwaremap_dump(
723    capfd,
724    mocked_hm,
725    filtered,
726    header,
727    connected_only,
728    detected,
729    expected_out
730):
731    mocked_hm.dump(filtered, header, connected_only, detected)
732
733    out, err = capfd.readouterr()
734    sys.stdout.write(out)
735    sys.stderr.write(err)
736
737    assert out.strip() == expected_out.strip()
738