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