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