fix: preserve guest network identities and refresh session profiles

This commit is contained in:
Codex
2026-10-13 05:15:06 +03:00
parent ccc521b9e1
commit 2f48de0aa2
10 changed files with 493 additions and 26 deletions
+9
View File
@@ -65,6 +65,15 @@ class ProfilesTests(unittest.TestCase):
with self.subTest(field=field, value=value), self.assertRaises(ValueError):
Profile('guest', self.iso, **{field: value})
def test_explicit_mac_roundtrip_and_validation(self):
dest = self.root / 'guest.toml'
save_profile(dest, Profile('guest', mac='02:ab:cd:ef:01:23'))
self.assertEqual(load_profile(dest).mac, '02:ab:cd:ef:01:23')
for value in ('ff:00:00:00:00:01', '00:00:00:00:00:01',
'02:00:00:00:00:zz', '02:00:00:00:00', True):
with self.subTest(mac=value), self.assertRaises(ValueError):
Profile('guest', mac=value)
def test_refresh_vertical_descriptor_boundaries(self):
for value in (0, 50000, 255000):
path = self.root / 'guest.toml'
+41 -1
View File
@@ -88,6 +88,46 @@ class RuntimeTests(unittest.TestCase):
except VMError:
pass
def test_mac_persisted_distinct_and_stable_across_restarts(self):
self.runtime.add(Profile('other', network='bridge'))
other = self.runtime.profile('other')
guest = self.runtime.profile('guest')
self.assertIsNotNone(guest.mac)
self.assertNotEqual(guest.mac, other.mac)
octet = int(guest.mac.split(':')[0], 16)
self.assertEqual(octet & 3, 2) # locally administered, unicast
def nic(profile, runtime):
return next(arg for arg in build_qemu_argv(profile, runtime.paths(profile.name))
if arg.startswith('virtio-net-pci,'))
expected = nic(guest, self.runtime)
self.runtime.start('guest')
self.runtime.stop('guest', force=True)
self.runtime.start('guest')
self.runtime.stop('guest', force=True)
self.assertEqual(nic(self.runtime.profile('guest'), self.runtime), expected)
changed_runtime = Runtime(self.runtime.config, self.runtime.state, self.root / 'different-run')
self.assertEqual(nic(changed_runtime.profile('guest'), changed_runtime), expected)
self.runtime.media('guest', None)
self.assertEqual(self.runtime.profile('guest').mac, guest.mac)
def test_legacy_profiles_gain_stable_owner_specific_argv_mac(self):
file = self.runtime.config / 'legacy.toml'
save_profile(file, Profile('legacy', network='bridge'))
original = file.read_bytes()
legacy = self.runtime.profile('legacy')
self.assertIsNone(legacy.mac)
paths = self.runtime.paths('legacy')
def mac():
return next(arg for arg in build_qemu_argv(legacy, paths) if arg.startswith('virtio-net-pci,'))
with patch('os.getuid', return_value=1000):
first = mac()
self.assertEqual(mac(), first)
self.assertNotEqual(first, next(arg for arg in build_qemu_argv(
legacy, self.runtime.paths('other')) if arg.startswith('virtio-net-pci,')))
with patch('os.getuid', return_value=1001):
self.assertNotEqual(mac(), first)
self.assertEqual(file.read_bytes(), original)
def test_disk_boot_omits_media_and_requests_display(self):
profile = Profile('guest', width=1920, height=1080, refresh_millihz=144000,
graphics='software')
@@ -145,7 +185,7 @@ class RuntimeTests(unittest.TestCase):
self.assertIn('disable-agent-file-xfer=on', spice)
self.assertIn('run,,comma', spice)
self.assertIn('virtio-blk-pci,drive=disk', argv)
self.assertIn('virtio-net-pci,netdev=net', argv)
self.assertTrue(any(arg.startswith('virtio-net-pci,netdev=net,mac=') for arg in argv))
self.assertNotIn('-vnc', argv)
self.assertNotIn('-virtfs', argv)
+44
View File
@@ -55,6 +55,11 @@ class SessionTests(unittest.TestCase):
self.assertEqual(tree.findall('./mouse/context/mousebind/action'), [])
self.assertEqual(tree.findtext('./libinput/device/tap'), 'yes')
def test_empty_session_has_one_workspace_without_empty_names(self):
tree = ET.fromstring(session.build_labwc_config([], ['foot']))
self.assertEqual(tree.find('./desktops').get('number'), '1')
self.assertIsNone(tree.find('./desktops/names'))
def test_viewer_argv_is_private_safe_and_manual_usb_only(self):
paths = self.manager.paths('alpha')
argv = session.build_viewer_argv(paths, '/tool path/viewer')
@@ -173,6 +178,45 @@ else:
self.assertEqual(main(['_session-helper', 'recover', str(manifest)]), 1)
self.assertIn('manifest', error.getvalue().lower())
def test_empty_launch_refresh_rebuilds_profile_snapshot(self):
manager = Runtime(self.root / 'empty-config', self.root / 'empty-state', self.root / 'empty-run')
capture = self.root / 'refresh-capture.json'
compositor = self.root / 'refresh-labwc'
compositor.write_text('#!' + os.sys.executable + '\n' + r'''import json, os, subprocess, sys
from pathlib import Path
if '--version' in sys.argv:
print('labwc 0.20.2')
sys.exit(0)
config = Path(sys.argv[sys.argv.index('-C') + 1])
manifest = config.parent / 'session.json'
data = json.loads(manifest.read_text())
capture = Path(os.environ['CAPTURE'])
seen = json.loads(capture.read_text()) if capture.exists() else []
seen.append({'names': data['names'], 'xml': (config / 'rc.xml').read_text()})
capture.write_text(json.dumps(seen))
if len(seen) == 1:
launcher = os.environ['HYDRA_SOURCE_LAUNCHER']
for name in ('first', 'second'):
subprocess.run([sys.executable, launcher, '--config', data['config'], '--state', data['state'],
'--runtime', data['runtime'], 'add', name], check=True)
subprocess.run([sys.executable, launcher, '_session-helper', 'recover', str(manifest)],
input=':refresh\n', text=True, check=True)
sys.exit(67)
sys.exit(0)
''')
compositor.chmod(0o700)
with patch.dict(os.environ, {'HYDRA_LABWC': str(compositor), 'HYDRA_TERMINAL': str(compositor),
'HYDRA_BRIGHTNESS': str(compositor), 'CAPTURE': str(capture),
'HYDRA_SOURCE_LAUNCHER': str(Path('bin/hydra').resolve())}), contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(session.launch_session(manager), 0)
seen = json.loads(capture.read_text())
self.assertEqual([entry['names'] for entry in seen], [[], ['first', 'second']])
tree = ET.fromstring(seen[1]['xml'])
self.assertEqual(tree.find('desktops').get('number'), '2')
self.assertEqual([rule.get('title') for rule in tree.findall('./windowRules/windowRule')],
['HydraOS: first', 'HydraOS: second'])
self.assertFalse(manager.paths('first').disk.exists())
def test_no_running_vms_opens_recovery_without_start_or_stop(self):
terminal = self.root / 'terminal'
capture = self.root / 'recovery-argv.json'