1# Copyright (c) 2023 Nordic Semiconductor ASA
2#
3# SPDX-License-Identifier: Apache-2.0
4
5import os
6import time
7from pathlib import Path
8from unittest import mock
9
10import pytest
11
12from twister_harness.device.hardware_adapter import HardwareAdapter
13from twister_harness.exceptions import TwisterHarnessException
14from twister_harness.twister_harness_config import DeviceConfig
15
16
17@pytest.fixture(name='device')
18def fixture_adapter(tmp_path) -> HardwareAdapter:
19    build_dir = tmp_path / 'build_dir'
20    os.mkdir(build_dir)
21    device_config = DeviceConfig(
22        type='hardware',
23        build_dir=build_dir,
24        runner='runner',
25        platform='platform',
26        id='p_id',
27        base_timeout=5.0,
28    )
29    return HardwareAdapter(device_config)
30
31
32@mock.patch('shutil.which', return_value=None)
33def test_if_hardware_adapter_raise_exception_when_west_not_found(patched_which, device: HardwareAdapter) -> None:
34    with pytest.raises(TwisterHarnessException, match='west not found'):
35        device.generate_command()
36
37
38@mock.patch('shutil.which', return_value='west')
39def test_if_get_command_returns_proper_string_1(patched_which, device: HardwareAdapter) -> None:
40    device.device_config.build_dir = Path('build')
41    device.generate_command()
42    assert isinstance(device.command, list)
43    assert device.command == ['west', 'flash', '--skip-rebuild', '--build-dir', 'build', '--runner', 'runner']
44
45
46@mock.patch('shutil.which', return_value='west')
47def test_if_get_command_returns_proper_string_2(patched_which, device: HardwareAdapter) -> None:
48    device.device_config.build_dir = Path('build')
49    device.device_config.runner = 'pyocd'
50    device.generate_command()
51    assert isinstance(device.command, list)
52    assert device.command == [
53        'west', 'flash', '--skip-rebuild', '--build-dir', 'build', '--runner', 'pyocd', '--', '--board-id', 'p_id'
54    ]
55
56
57@mock.patch('shutil.which', return_value='west')
58def test_if_get_command_returns_proper_string_3(patched_which, device: HardwareAdapter) -> None:
59    device.device_config.build_dir = Path('build')
60    device.device_config.runner = 'nrfjprog'
61    device.generate_command()
62    assert isinstance(device.command, list)
63    assert device.command == [
64        'west', 'flash', '--skip-rebuild', '--build-dir', 'build', '--runner', 'nrfjprog', '--', '--dev-id', 'p_id'
65    ]
66
67
68@mock.patch('shutil.which', return_value='west')
69def test_if_get_command_returns_proper_string_4(patched_which, device: HardwareAdapter) -> None:
70    device.device_config.build_dir = Path('build')
71    device.device_config.runner = 'openocd'
72    device.device_config.product = 'STM32 STLink'
73    device.generate_command()
74    assert isinstance(device.command, list)
75    assert device.command == [
76        'west', 'flash', '--skip-rebuild', '--build-dir', 'build', '--runner', 'openocd',
77        '--', '--cmd-pre-init', 'hla_serial p_id'
78    ]
79
80
81@mock.patch('shutil.which', return_value='west')
82def test_if_get_command_returns_proper_string_5(patched_which, device: HardwareAdapter) -> None:
83    device.device_config.build_dir = Path('build')
84    device.device_config.runner = 'openocd'
85    device.device_config.product = 'EDBG CMSIS-DAP'
86    device.generate_command()
87    assert isinstance(device.command, list)
88    assert device.command == [
89        'west', 'flash', '--skip-rebuild', '--build-dir', 'build', '--runner', 'openocd',
90        '--', '--cmd-pre-init', 'cmsis_dap_serial p_id'
91    ]
92
93
94@mock.patch('shutil.which', return_value='west')
95def test_if_get_command_returns_proper_string_6(patched_which, device: HardwareAdapter) -> None:
96    device.device_config.build_dir = Path('build')
97    device.device_config.runner = 'jlink'
98    device.generate_command()
99    assert isinstance(device.command, list)
100    assert device.command == [
101        'west', 'flash', '--skip-rebuild', '--build-dir', 'build', '--runner', 'jlink',
102        '--dev-id', 'p_id'
103    ]
104
105
106@mock.patch('shutil.which', return_value='west')
107def test_if_get_command_returns_proper_string_7(patched_which, device: HardwareAdapter) -> None:
108    device.device_config.build_dir = Path('build')
109    device.device_config.runner = 'stm32cubeprogrammer'
110    device.generate_command()
111    assert isinstance(device.command, list)
112    assert device.command == [
113        'west', 'flash', '--skip-rebuild', '--build-dir', 'build', '--runner', 'stm32cubeprogrammer',
114        '--tool-opt=sn=p_id'
115    ]
116
117
118@mock.patch('shutil.which', return_value='west')
119def test_if_get_command_returns_proper_string_8(patched_which, device: HardwareAdapter) -> None:
120    device.device_config.build_dir = Path('build')
121    device.device_config.runner = 'openocd'
122    device.device_config.product = 'STLINK-V3'
123    device.generate_command()
124    assert isinstance(device.command, list)
125    assert device.command == [
126        'west', 'flash', '--skip-rebuild', '--build-dir', 'build',
127        '--runner', 'openocd', '--', '--cmd-pre-init', 'hla_serial p_id'
128    ]
129
130
131@mock.patch('shutil.which', return_value='west')
132def test_if_get_command_returns_proper_string_with_runner_params_1(patched_which, device: HardwareAdapter) -> None:
133    device.device_config.build_dir = Path('build')
134    device.device_config.runner_params = ['--runner-param1', 'runner-param2']
135    device.generate_command()
136    assert isinstance(device.command, list)
137    assert device.command == [
138        'west', 'flash', '--skip-rebuild', '--build-dir', 'build',
139        '--runner', 'runner', '--', '--runner-param1', 'runner-param2'
140    ]
141
142
143@mock.patch('shutil.which', return_value='west')
144def test_if_get_command_returns_proper_string_with_runner_params_2(patched_which, device: HardwareAdapter) -> None:
145    device.device_config.build_dir = Path('build')
146    device.device_config.runner = 'openocd'
147    device.device_config.runner_params = [
148        '--cmd-pre-init', 'adapter serial FT1LRSRD',
149        '--cmd-pre-init', 'source [find interface/ftdi/jtag-lock-pick_tiny_2.cfg]',
150        '--cmd-pre-init', 'transport select swd',
151        '--cmd-pre-init', 'source [find target/nrf52.cfg]',
152        '--cmd-pre-init', 'adapter speed 10000',
153    ]
154    device.device_config.product = 'JTAG-lock-pick Tiny 2'
155    device.generate_command()
156    assert isinstance(device.command, list)
157    assert device.command == [
158        'west', 'flash', '--skip-rebuild', '--build-dir', 'build',
159        '--runner', 'openocd', '--',
160        '--cmd-pre-init', 'adapter serial FT1LRSRD',
161        '--cmd-pre-init', 'source [find interface/ftdi/jtag-lock-pick_tiny_2.cfg]',
162        '--cmd-pre-init', 'transport select swd',
163        '--cmd-pre-init', 'source [find target/nrf52.cfg]',
164        '--cmd-pre-init', 'adapter speed 10000',
165    ]
166
167
168@mock.patch('shutil.which', return_value='west')
169def test_if_get_command_returns_proper_string_with_west_flash_extra_args(
170    patched_which, device: HardwareAdapter
171) -> None:
172    device.device_config.build_dir = Path('build')
173    device.device_config.west_flash_extra_args = ['--board-id=foobar', '--erase']
174    device.device_config.runner = 'pyocd'
175    device.device_config.id = ''
176    device.generate_command()
177    assert isinstance(device.command, list)
178    assert device.command == [
179        'west', 'flash', '--skip-rebuild', '--build-dir', 'build', '--runner', 'pyocd',
180        '--', '--board-id=foobar', '--erase'
181    ]
182
183
184def test_if_hardware_adapter_raises_exception_empty_command(device: HardwareAdapter) -> None:
185    device.command = []
186    exception_msg = 'Flash command is empty, please verify if it was generated properly.'
187    with pytest.raises(TwisterHarnessException, match=exception_msg):
188        device._flash_and_run()
189
190
191@mock.patch('twister_harness.device.hardware_adapter.subprocess.Popen')
192def test_device_log_correct_error_handle(patched_popen, device: HardwareAdapter, tmp_path: Path) -> None:
193    popen_mock = mock.Mock()
194    popen_mock.communicate.return_value = (b'flashing error', b'')
195    patched_popen.return_value = popen_mock
196    device.device_config.build_dir = tmp_path
197    device.command = [
198        'west', 'flash', '--skip-rebuild', '--build-dir', str(tmp_path),
199        '--runner', 'nrfjprog', '--', '--dev-id', 'p_id'
200    ]
201    with pytest.raises(expected_exception=TwisterHarnessException, match='Could not flash device p_id'):
202        device._flash_and_run()
203    assert os.path.isfile(device.device_log_path)
204    with open(device.device_log_path, 'r') as file:
205        assert 'flashing error' in file.readlines()
206
207
208@mock.patch('twister_harness.device.hardware_adapter.subprocess.Popen')
209@mock.patch('twister_harness.device.hardware_adapter.serial.Serial')
210def test_if_hardware_adapter_uses_serial_pty(
211    patched_serial, patched_popen, device: HardwareAdapter, monkeypatch: pytest.MonkeyPatch
212):
213    device.device_config.serial_pty = 'script.py'
214
215    popen_mock = mock.Mock()
216    popen_mock.communicate.return_value = (b'output', b'error')
217    patched_popen.return_value = popen_mock
218
219    monkeypatch.setattr('twister_harness.device.hardware_adapter.pty.openpty', lambda: (123, 456))
220    monkeypatch.setattr('twister_harness.device.hardware_adapter.os.ttyname', lambda x: f'/pty/ttytest/{x}')
221
222    serial_mock = mock.Mock()
223    serial_mock.port = '/pty/ttytest/456'
224    patched_serial.return_value = serial_mock
225
226    device._device_run.set()
227    device.connect()
228    assert device._serial_connection.port == '/pty/ttytest/456'  # type: ignore[union-attr]
229    assert device._serial_pty_proc
230    patched_popen.assert_called_with(
231        ['script.py'],
232        stdout=123,
233        stdin=123,
234        stderr=123
235    )
236
237    device.disconnect()
238    assert not device._serial_pty_proc
239
240
241def test_if_hardware_adapter_properly_send_data_to_subprocess(
242    device: HardwareAdapter, shell_simulator_path: str
243) -> None:
244    """
245    Run shell_simulator.py under serial_pty, send "zen" command and verify
246    output. Flashing command is mocked by "dummy" echo command.
247    """
248    device.command = ['echo', 'TEST']  # only to mock flashing command
249    device.device_config.serial_pty = f'python3 {shell_simulator_path}'
250    device.launch()
251    time.sleep(0.1)
252    device.write(b'zen\n')
253    time.sleep(1)
254    lines = device.readlines_until(regex='Namespaces are one honking great idea')
255    assert 'The Zen of Python, by Tim Peters' in lines
256    device.write(b'quit\n')
257