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