xref: /aosp_15_r20/external/pigweed/pw_emu/py/tests/renode_test.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1#!/usr/bin/env python
2# Copyright 2023 The Pigweed Authors
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may not
5# use this file except in compliance with the License. You may obtain a copy of
6# the License at
7#
8#     https://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations under
14# the License.
15"""renode emulator tests."""
16
17import json
18import os
19import sys
20import struct
21import tempfile
22import time
23import unittest
24
25from pathlib import Path
26from typing import Any
27
28from pw_emu.core import InvalidChannelName, InvalidChannelType
29from config_helper import check_prog, ConfigHelperWithEmulator
30
31
32# TODO: b/301382004 - The Python Pigweed package install (into python-venv)
33# races with running this test and there is no way to add that package as a test
34# depedency without creating circular depedencies. This means we can't rely on
35# using Pigweed tools like pw cli or the arm-none-eabi-gdb wrapper.
36#
37# run the arm_gdb.py wrapper directly
38_arm_none_eabi_gdb_path = Path(
39    os.path.join(
40        os.environ['PW_ROOT'],
41        'pw_env_setup',
42        'py',
43        'pw_env_setup',
44        'entry_points',
45        'arm_gdb.py',
46    )
47).resolve()
48
49
50class TestRenode(ConfigHelperWithEmulator):
51    """Tests for a valid renode configuration."""
52
53    _config = {
54        'gdb': ['python', str(_arm_none_eabi_gdb_path)],
55        'renode': {
56            'executable': 'renode',
57        },
58        'targets': {
59            'test-target': {
60                'renode': {
61                    'machine': 'platforms/boards/stm32f4_discovery-kit.repl',
62                }
63            }
64        },
65    }
66
67    def setUp(self) -> None:
68        super().setUp()
69        # no image to run so start paused
70        self._emu.start(target='test-target', pause=True)
71
72    def tearDown(self) -> None:
73        self._emu.stop()
74        super().tearDown()
75
76    def test_running(self) -> None:
77        self.assertTrue(self._emu.running())
78
79    def test_list_properties(self) -> None:
80        self.assertIsNotNone(self._emu.list_properties('sysbus.usart1'))
81
82    def test_get_property(self) -> None:
83        self.assertIsNotNone(
84            self._emu.get_property('sysbus.usart1', 'BaudRate')
85        )
86
87    def test_set_property(self) -> None:
88        self._emu.set_property('sysbus.timer1', 'Frequency', 100)
89        self.assertEqual(
90            int(self._emu.get_property('sysbus.timer1', 'Frequency'), 16), 100
91        )
92
93
94class TestRenodeInvalidChannelType(ConfigHelperWithEmulator):
95    """Test invalid channel type configuration."""
96
97    _config = {
98        'renode': {
99            'executable': 'renode',
100        },
101        'targets': {
102            'test-target': {
103                'renode': {
104                    'machine': 'platforms/boards/stm32f4_discovery-kit.repl',
105                    'channels': {
106                        'terminals': {
107                            'test_uart': {
108                                'device-path': 'sysbus.usart1',
109                                'type': 'invalid',
110                            }
111                        }
112                    },
113                }
114            }
115        },
116    }
117
118    def test_start(self) -> None:
119        with self.assertRaises(InvalidChannelType):
120            self._emu.start('test-target', pause=True)
121
122
123class TestRenodeChannels(ConfigHelperWithEmulator):
124    """Tests for a valid renode channels configuration."""
125
126    _config = {
127        'gdb': ['python', str(_arm_none_eabi_gdb_path)],
128        'renode': {
129            'executable': 'renode',
130        },
131        'targets': {
132            'test-target': {
133                'renode': {
134                    'machine': 'platforms/boards/stm32f4_discovery-kit.repl',
135                    'channels': {
136                        'terminals': {
137                            'test_uart': {
138                                'device-path': 'sysbus.usart1',
139                            }
140                        }
141                    },
142                }
143            }
144        },
145    }
146
147    def setUp(self) -> None:
148        super().setUp()
149        # no image to run so start paused
150        self._emu.start(target='test-target', pause=True)
151
152    def tearDown(self) -> None:
153        self._emu.stop()
154        super().tearDown()
155
156    def test_bad_channel_name(self) -> None:
157        with self.assertRaises(InvalidChannelName):
158            self._emu.get_channel_addr('serial1')
159
160    def set_reg(self, addr: int, val: int) -> None:
161        self._emu.run_gdb_cmds([f'set *(unsigned int*){addr}={val}'])
162
163    def get_reg(self, addr: int) -> int:
164        temp = tempfile.NamedTemporaryFile(delete=False)
165        temp.close()
166
167        res = self._emu.run_gdb_cmds(
168            [
169                f'dump val {temp.name} *(char*){addr}',
170                'disconnect',
171            ]
172        )
173        self.assertEqual(res.returncode, 0, res.stderr.decode('ascii'))
174
175        with open(temp.name, 'rb') as file:
176            ret = file.read(1)
177
178        self.assertNotEqual(ret, b'', res.stderr.decode('ascii'))
179
180        os.unlink(temp.name)
181
182        return struct.unpack('B', ret)[0]
183
184    def poll_data(self, timeout: int) -> int | None:
185        usart_sr = 0x40011000
186        usart_dr = 0x40011004
187        deadline = time.monotonic() + timeout
188        while self.get_reg(usart_sr) & 0x20 == 0:
189            time.sleep(0.1)
190            if time.monotonic() > deadline:
191                return None
192        return self.get_reg(usart_dr)
193
194    def test_channel_stream(self) -> None:
195        ok, msg = check_prog('arm-none-eabi-gdb')
196        if not ok:
197            self.skipTest(msg)
198
199        usart_cr1 = 0x4001100C
200        # enable RX and TX
201        self.set_reg(usart_cr1, 0xC)
202
203        stream = self._emu.get_channel_stream('test_uart')
204        stream.write('test\n'.encode('ascii'))
205
206        self.assertEqual(self.poll_data(5), ord('t'))
207        self.assertEqual(self.poll_data(5), ord('e'))
208        self.assertEqual(self.poll_data(5), ord('s'))
209        self.assertEqual(self.poll_data(5), ord('t'))
210
211
212class TestRenodeChannelsPty(TestRenodeChannels):
213    """Tests for configurations using PTY channels."""
214
215    _config: dict[str, Any] = {}
216    _config.update(json.loads(json.dumps(TestRenodeChannels._config)))
217    _config['renode']['channels'] = {'terminals': {'type': 'pty'}}
218
219    def setUp(self):
220        if sys.platform == 'win32':
221            self.skipTest('pty not supported on win32')
222        super().setUp()
223
224    def test_get_path(self) -> None:
225        self.assertTrue(os.path.exists(self._emu.get_channel_path('test_uart')))
226
227
228def main() -> None:
229    ok, msg = check_prog('renode')
230    if not ok:
231        print(f'skipping tests: {msg}')
232        sys.exit(0)
233
234    unittest.main()
235
236
237if __name__ == '__main__':
238    main()
239