[PATCH v2 19/25] tests/functional: add a device-locator self-check
Emmanuel Blot via <[email protected]> Fri, 31 Jul 2026 12:45:18 +0200
| Newsgroups | org.nongnu.qemu-arm,org.nongnu.qemu-devel |
|---|---|
| Message-ID | <[email protected]> |
Launch the SanMiguel BMC paused (-S) and resolve its I2C temperature sensors and SPI flashes by position over QMP, exercising the DeviceLocator grammar: controller/bus hops, the address/type/on-bus-index selectors, the leaf type assertion and the ambiguous/unknown/out-of-range error paths. The test needs no disk image and completes in about a second, so it joins the quick arm test set. Signed-off-by: Emmanuel Blot <[email protected]> --- tests/functional/arm/meson.build | 1 + tests/functional/arm/test_device_locator.py | 120 ++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/tests/functional/arm/meson.build b/tests/functional/arm/meson.build index 61b00932db..bb78dcf3e4 100644 --- a/tests/functional/arm/meson.build +++ b/tests/functional/arm/meson.build @@ -29,6 +29,7 @@ test_arm_timeouts = { } tests_arm_system_quick = [ + 'device_locator', 'migration', ] diff --git a/tests/functional/arm/test_device_locator.py b/tests/functional/arm/test_device_locator.py new file mode 100644 index 0000000000..3807a49361 --- /dev/null +++ b/tests/functional/arm/test_device_locator.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# +# Functional self-check for the pure-Python DeviceLocator helper. +# +# It launches the SanMiguel BMC paused (-S) and resolves its I2C sensors and +# SPI flashes by position over QMP, so it needs no disk image and runs in +# about a second. +# +# SPDX-License-Identifier: GPL-2.0-or-later + +import sys + +from unittest import skipIf + +from qemu_test import QemuSystemTest + +# DeviceLocator's type hints require Python 3.10+; import it only when +# available so this module still loads (and skips) on the 3.9 baseline. +if sys.version_info >= (3, 10): + from qemu_test.locator import DeviceLocator, LocatorError + + +@skipIf(sys.version_info < (3, 10), 'DeviceLocator requires Python 3.10+') +class DeviceLocatorTest(QemuSystemTest): + + def setUp(self): + super().setUp() + self.set_machine('sanmiguel-bmc') + # Launch paused with defaults enabled so the aspeed board instantiates + # its SPI flashes (created only under defaults_enabled()); still no + # disk image is needed. + self.vm.add_args('-S', '-display', 'none') + self.vm.launch() + self.locator = DeviceLocator(self.vm.cmd) + + def _address(self, path): + return self.vm.cmd('qom-get', path=path, property='address') + + def _cs(self, path): + return self.vm.cmd('qom-get', path=path, property='cs') + + def test_resolve_tmp75_sensors(self): + # The SanMiguel board wires ti,tmp75 sensors on I2C buses 5, 9 and 10 + # behind the aspeed.i2c-ast2600 controller. + ctrl = '/machine/soc::aspeed.i2c-ast2600[0]' + for locator, addr in ((f'{ctrl}~i2c[5]~@0x48', 0x48), + (f'{ctrl}~i2c[9]~@0x4e', 0x4e), + (f'{ctrl}~i2c[10]~@0x48', 0x48)): + path = self.locator.resolve(locator, 'tmp75') + self.assertEqual(self._address(path), addr) + + def test_device_selectors(self): + bus = '/machine/soc::aspeed.i2c-ast2600[0]~i2c[5]' + by_addr = self.locator.resolve(f'{bus}~@0x48') + # A type selector combined with the address must reach the same device. + self.assertEqual(self.locator.resolve(f'{bus}~tmp75@0x48'), by_addr) + # The on-bus (BusChild) index selector addresses a device positionally. + by_index = self.locator.resolve(f'{bus}~#0') + self.assertTrue(by_index.startswith('/machine')) + # A bus hop also accepts the raw bus QOM type in place of the keyword. + raw = '/machine/soc::aspeed.i2c-ast2600[0]~i2c-bus[5]~@0x48' + self.assertEqual(self.locator.resolve(raw), by_addr) + + def test_type_assertion(self): + # The leaf type is validated when a type argument is passed. + self.assertRaises(LocatorError, self.locator.resolve, + '/machine/soc::aspeed.i2c-ast2600[0]~i2c[5]~@0x48', + 'ds1338') + + def test_resolution_errors(self): + ctrl = '/machine/soc::aspeed.i2c-ast2600[0]' + # No device sits at that address on the bus. + self.assertRaises(LocatorError, self.locator.resolve, + f'{ctrl}~i2c[5]~@0x99') + # A bare bus hop under /machine/soc spans every i2c bus on both the + # i2c and i3c controllers, so it is ambiguous without a controller hop. + self.assertRaises(LocatorError, self.locator.resolve, + '/machine/soc::i2c') + # Unknown object type in a controller hop. + self.assertRaises(LocatorError, self.locator.resolve, + '/machine/soc::nope') + # Out-of-range bus index. + self.assertRaises(LocatorError, self.locator.resolve, + f'{ctrl}~i2c[99]~@0x48') + # A stray or doubled '~' yields an empty hop, which is malformed. + self.assertRaises(LocatorError, self.locator.resolve, + f'{ctrl}~~i2c[5]~@0x48') + # A repeated or empty device selector is malformed, not last-wins. + for bad in (f'{ctrl}~i2c[5]~@0x48@0x4e', + f'{ctrl}~i2c[5]~#0#1', + f'{ctrl}~i2c[5]~tmp75=a=b', + f'{ctrl}~i2c[5]~tmp75='): + self.assertRaises(LocatorError, self.locator.resolve, bad) + + def test_resolve_spi_flashes(self): + # The FMC controller carries two flashes on chip-selects 0 and 1; the + # first SPI controller carries one on chip-select 0. + for locator, cs in ( + ('/machine/soc::aspeed.fmc-ast2600[0]~spi[0]~@0', 0), + ('/machine/soc::aspeed.fmc-ast2600[0]~spi[0]~@1', 1), + ('/machine/soc::aspeed.spi1-ast2600[0]~spi[0]~@0', 0)): + path = self.locator.resolve(locator, 'ssi-peripheral') + self.assertEqual(self._cs(path), cs) + + def test_spi_selectors(self): + bus = '/machine/soc::aspeed.fmc-ast2600[0]~spi[0]' + by_cs = self.locator.resolve(f'{bus}~@0') + # A type selector plus the chip-select reaches the same flash. + self.assertEqual( + self.locator.resolve(f'{bus}~ssi-peripheral@0'), by_cs) + # The on-bus index selector resolves positionally. + self.assertTrue( + self.locator.resolve(f'{bus}~#1').startswith('/machine')) + # A wrong-type assertion on an SPI leaf is rejected. + self.assertRaises(LocatorError, self.locator.resolve, + f'{bus}~@0', 'tmp75') + + +if __name__ == '__main__': + QemuSystemTest.main() -- 2.50.1