@samitouri / QOSamiQemu / commits / f9827a9b9a

tests/functional: add e2e test for dynamic QMP monitor hotplug

Add functional tests that exercise dynamic monitor hotplug with real socket connections: - Hotplug cycle: chardev-add a unix socket, object-add, connect to the socket, receive the QMP greeting, negotiate capabilities, send query-version, disconnect, remove the monitor and chardev, then repeat the entire cycle a second time to verify cleanup and reuse. - Self-removal: a dynamically-added monitor sends object-del targeting itself, verifying that the request is rejected - Large response: send query-qmp-schema on a dynamic monitor to exercise the output buffer flush path with a large response payload. - Events after negotiation: trigger STOP/RESUME events via the main monitor and verify they are delivered on the dynamic monitor. This complements the qtest unit tests by verifying that a real QMP client can connect to a dynamically-added monitor and exchange messages. Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org> [DB: modified to use object-add/object-del; adjust self-removal test to validate rejection of request] Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com> Tested-by: Peter Krempa <pkrempa@redhat.com> Signed-off-by: Daniel P. Berrangé <berrange@redhat.com> Message-ID: <20260706135824.2623960-30-berrange@redhat.com> Signed-off-by: Markus Armbruster <armbru@redhat.com>

Christian Brauner committed Jul 6, 2026 at 14:58 UTC f9827a9b9a9463b46411ac8c9efc7c911d7caf17
3 files changed +170
MAINTAINERS
+1
@@ -3672,6 +3672,7 @@ F: docs/interop/*qmp-*
3672 F: scripts/qmp/
3673 F: tests/qtest/qmp-test.c
3674 F: tests/qtest/qmp-cmd-test.c
3675 +F: tests/functional/generic/test_monitor_hotplug.py
3676 T: git https://repo.or.cz/qemu/armbru.git qapi-next
3677
3678 qtest
tests/functional/generic/meson.build
+1
@@ -4,6 +4,7 @@ tests_generic_system = [
4 'empty_cpu_model',
5 'info_usernet',
6 'linters',
7 + 'monitor_hotplug',
8 'version',
9 'vnc',
10 ]
tests/functional/generic/test_monitor_hotplug.py new
+168
@@ -0,0 +1,168 @@
1 +#!/usr/bin/env python3
2 +#
3 +# SPDX-License-Identifier: GPL-2.0-or-later
4 +#
5 +# Functional test for dynamic QMP monitor hotplug
6 +#
7 +# Copyright (c) 2026 Christian Brauner
8 +
9 +import os
10 +
11 +from qemu_test import QemuSystemTest
12 +
13 +from qemu.qmp.legacy import QEMUMonitorProtocol
14 +
15 +
16 +class MonitorHotplug(QemuSystemTest):
17 +
18 + def setUp(self):
19 + super().setUp()
20 + sock_dir = self.socket_dir()
21 + self._sock_path = os.path.join(sock_dir.name, 'hotplug.sock')
22 +
23 + def _add_monitor(self):
24 + """Create a chardev + monitor and return the socket path."""
25 + sock = self._sock_path
26 + self.vm.cmd('chardev-add', id='hotplug-chr', backend={
27 + 'type': 'socket',
28 + 'data': {
29 + 'addr': {
30 + 'type': 'unix',
31 + 'data': {'path': sock}
32 + },
33 + 'server': True,
34 + 'wait': False,
35 + }
36 + })
37 + self.vm.cmd('object-add', id='hotplug-mon',
38 + qom_type='monitor-qmp',
39 + chardev='hotplug-chr')
40 + return sock
41 +
42 + def _remove_monitor(self):
43 + """Remove the monitor + chardev."""
44 + self.vm.cmd('object-del', id='hotplug-mon')
45 + self.vm.cmd('chardev-remove', id='hotplug-chr')
46 +
47 + def _connect_and_handshake(self, sock_path):
48 + """
49 + Connect to the dynamic monitor socket, perform the QMP
50 + greeting and capability negotiation, send a command, then
51 + disconnect.
52 + """
53 + qmp = QEMUMonitorProtocol(sock_path)
54 +
55 + # connect(negotiate=True) receives the greeting, validates it,
56 + # and sends qmp_capabilities automatically.
57 + greeting = qmp.connect(negotiate=True)
58 + self.assertIn('QMP', greeting)
59 + self.assertIn('version', greeting['QMP'])
60 + self.assertIn('capabilities', greeting['QMP'])
61 +
62 + # Send a real command to prove the session is fully functional
63 + resp = qmp.cmd_obj({'execute': 'query-version'})
64 + self.assertIn('return', resp)
65 + self.assertIn('qemu', resp['return'])
66 +
67 + qmp.close()
68 +
69 + def test_hotplug_cycle(self):
70 + """
71 + Hotplug a monitor, do the full QMP handshake, unplug it,
72 + then repeat the whole cycle a second time.
73 + """
74 + self.set_machine('none')
75 + self.vm.add_args('-nodefaults')
76 + self.vm.launch()
77 +
78 + # First cycle
79 + sock = self._add_monitor()
80 + self._connect_and_handshake(sock)
81 + self._remove_monitor()
82 +
83 + # Second cycle -- same ids, same path, must work
84 + sock = self._add_monitor()
85 + self._connect_and_handshake(sock)
86 + self._remove_monitor()
87 +
88 + def test_self_removal(self):
89 + """
90 + A dynamically-added monitor sends object-del targeting
91 + itself. Verify the request is rejected, but the monitor
92 + can still be deleted from outside its own context.
93 + """
94 + self.set_machine('none')
95 + self.vm.add_args('-nodefaults')
96 + self.vm.launch()
97 +
98 + sock = self._add_monitor()
99 +
100 + qmp = QEMUMonitorProtocol(sock)
101 + greeting = qmp.connect(negotiate=True)
102 + self.assertIn('QMP', greeting)
103 +
104 + # Self-removal: the dynamic monitor raises error
105 + resp = qmp.cmd_obj({'execute': 'object-del',
106 + 'arguments': {'id': 'hotplug-mon'}})
107 + self.assertIn('error', resp)
108 +
109 + qmp.close()
110 +
111 + resp = self.vm.cmd('object-del', id='hotplug-mon')
112 +
113 + # Clean up the chardev
114 + self.vm.cmd('chardev-remove', id='hotplug-chr')
115 +
116 + def test_large_response(self):
117 + """
118 + Send a command with a large response (query-qmp-schema) on a
119 + dynamically-added monitor to exercise the output buffer flush
120 + path.
121 + """
122 + self.set_machine('none')
123 + self.vm.add_args('-nodefaults')
124 + self.vm.launch()
125 +
126 + sock = self._add_monitor()
127 +
128 + qmp = QEMUMonitorProtocol(sock)
129 + qmp.connect(negotiate=True)
130 +
131 + resp = qmp.cmd_obj({'execute': 'query-qmp-schema'})
132 + self.assertIn('return', resp)
133 + self.assertIsInstance(resp['return'], list)
134 + self.assertGreater(len(resp['return']), 0)
135 +
136 + qmp.close()
137 + self._remove_monitor()
138 +
139 + def test_events_after_negotiation(self):
140 + """
141 + Verify that QMP events are delivered on a dynamically-added
142 + monitor after capability negotiation completes.
143 + """
144 + self.set_machine('none')
145 + self.vm.add_args('-nodefaults')
146 + self.vm.launch()
147 +
148 + sock = self._add_monitor()
149 +
150 + qmp = QEMUMonitorProtocol(sock)
151 + qmp.connect(negotiate=True)
152 +
153 + # Trigger a STOP event via the main monitor, then read it
154 + # from the dynamic monitor.
155 + self.vm.cmd('stop')
156 + resp = qmp.pull_event(wait=True)
157 + self.assertEqual(resp['event'], 'STOP')
158 +
159 + self.vm.cmd('cont')
160 + resp = qmp.pull_event(wait=True)
161 + self.assertEqual(resp['event'], 'RESUME')
162 +
163 + qmp.close()
164 + self._remove_monitor()
165 +
166 +
167 +if __name__ == '__main__':
168 + QemuSystemTest.main()