quality-loop 0.4.1__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {quality_loop-0.4.1/quality_loop.egg-info → quality_loop-0.4.2}/PKG-INFO +1 -1
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/__init__.py +1 -1
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/guest/__init__.py +36 -4
- {quality_loop-0.4.1 → quality_loop-0.4.2/quality_loop.egg-info}/PKG-INFO +1 -1
- {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_guest.py +24 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/LICENSE +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/README.md +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/pyproject.toml +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/OPERATOR.md +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/__main__.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/adapter.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/audit.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/awake.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/cli.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/compare.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/config.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/drive/__init__.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/drive/_mac.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/drive/_windows.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/drive/keys.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/drive/terminal.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/guest/boot/__init__.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/guest/boot/launch.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/guest/boot/screen_agent.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/guest/screens.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/install.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/mcp.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/operator.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/pages.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/processes.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/progress.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/report.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/room.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/runner.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/scenario.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/screens/__init__.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/screens/_mac.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/screens/_windows.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/session.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/skill/SKILL.md +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/skill/references/filing.md +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/skill/references/setup.md +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/standin.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop/transcript.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop.egg-info/SOURCES.txt +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop.egg-info/dependency_links.txt +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop.egg-info/entry_points.txt +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop.egg-info/requires.txt +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/quality_loop.egg-info/top_level.txt +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/setup.cfg +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_awake.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_core.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_drive.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_loop.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_mcp.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_operator.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_scenario_report.py +0 -0
- {quality_loop-0.4.1 → quality_loop-0.4.2}/tests/test_screens.py +0 -0
|
@@ -483,8 +483,14 @@ class TartMachine:
|
|
|
483
483
|
class VMwareMachine:
|
|
484
484
|
"""A VMware Fusion VM reverted to a snapshot before the run.
|
|
485
485
|
|
|
486
|
-
|
|
487
|
-
|
|
486
|
+
Take the snapshot with the VM shut down, so that every run boots it. A
|
|
487
|
+
snapshot taken while it ran comes back running, signed in and with SSH
|
|
488
|
+
up, in seconds - and with whatever was half done at that moment, every
|
|
489
|
+
time: Keyhac's image came back with Notepad's hand-off to its packaged
|
|
490
|
+
app broken, so `notepad.exe` started the inbox one, which crashed within
|
|
491
|
+
seconds on every run, while the same VM booted was fine. On Windows, the
|
|
492
|
+
run waits for the automatic sign-in, which the image must have. The SMB
|
|
493
|
+
share that was mounted on
|
|
488
494
|
the host does not survive the revert, so it is mounted again. The VM is
|
|
489
495
|
shut down afterwards, its share unmounted first: the next run reverts it
|
|
490
496
|
anyway, and a VM `vmrun` left running headless is one the Fusion app
|
|
@@ -495,8 +501,7 @@ class VMwareMachine:
|
|
|
495
501
|
Learnt on Fusion 26 with a vTPM-encrypted Windows 11 VM: `vmrun` cannot
|
|
496
502
|
linked-clone it ("Cannot read the virtual machine configuration file"),
|
|
497
503
|
nor snapshot it while it runs ("Authentication for encrypted virtual
|
|
498
|
-
machine failed")
|
|
499
|
-
`vmrun suspend` (a headless VM does not show there).
|
|
504
|
+
machine failed"). Shut down, `vmrun -vp <password> snapshot` takes it.
|
|
500
505
|
"""
|
|
501
506
|
|
|
502
507
|
VMRUN = "/Applications/VMware Fusion.app/Contents/Library/vmrun"
|
|
@@ -533,9 +538,36 @@ class VMwareMachine:
|
|
|
533
538
|
f"to {self.snapshot!r}")
|
|
534
539
|
self._mount()
|
|
535
540
|
self._wait_for_the_clock()
|
|
541
|
+
if self.guest.windows:
|
|
542
|
+
self._wait_for_the_desktop()
|
|
536
543
|
print(f"vm: {self.guest.alias} reverted to {self.snapshot!r}")
|
|
537
544
|
return self
|
|
538
545
|
|
|
546
|
+
def _wait_for_the_desktop(self, settle: float = 15.0) -> None:
|
|
547
|
+
"""Wait until the guest's user is signed in, and has been for
|
|
548
|
+
``settle`` seconds.
|
|
549
|
+
|
|
550
|
+
A booted VM answers SSH before the automatic sign-in has made the
|
|
551
|
+
desktop session `launch` starts programs in, and that session's
|
|
552
|
+
first seconds are its startup programs; the run starts after both.
|
|
553
|
+
A running-state snapshot is past them at once.
|
|
554
|
+
"""
|
|
555
|
+
deadline = time.monotonic() + self.patience
|
|
556
|
+
while time.monotonic() < deadline:
|
|
557
|
+
said = self.guest.ssh(
|
|
558
|
+
"$e = Get-Process explorer -ErrorAction SilentlyContinue | "
|
|
559
|
+
"Where-Object { $_.SessionId -gt 0 } | Select-Object -First 1; "
|
|
560
|
+
"if ($e) { [int]((Get-Date) - $e.StartTime).TotalSeconds } else { -1 }")
|
|
561
|
+
try:
|
|
562
|
+
up = int(said.stdout.strip())
|
|
563
|
+
except ValueError:
|
|
564
|
+
up = -1
|
|
565
|
+
if up >= settle:
|
|
566
|
+
return
|
|
567
|
+
time.sleep(settle - up if up >= 0 else 3.0)
|
|
568
|
+
raise GuestError(f"{self.guest.alias}: no signed-in desktop after "
|
|
569
|
+
f"{self.patience:.0f}s - is automatic sign-in set up?")
|
|
570
|
+
|
|
539
571
|
def _mount(self) -> None:
|
|
540
572
|
url = self.guest.settings.get("smb")
|
|
541
573
|
if not url:
|
|
@@ -208,6 +208,30 @@ def test_a_terminal_script_is_staged_and_run_with_the_guest_python(share, monkey
|
|
|
208
208
|
assert (stage / "scripts" / "deploy.py").exists()
|
|
209
209
|
|
|
210
210
|
|
|
211
|
+
def test_a_booted_windows_guest_is_waited_for_until_its_desktop_has_settled(share, monkeypatch):
|
|
212
|
+
"""A powered-off snapshot boots: SSH answers before the automatic sign-in
|
|
213
|
+
has made the desktop, and then its startup programs run."""
|
|
214
|
+
guest = _guest(share, windows=True, vmx="/v.vmx", snapshot="s")
|
|
215
|
+
answers = iter(["-1", "-1", "4", "16"])
|
|
216
|
+
ssh = FakeSSH(lambda c: (next(answers) + "\n", 0))
|
|
217
|
+
monkeypatch.setattr(guest, "ssh", ssh)
|
|
218
|
+
slept = []
|
|
219
|
+
monkeypatch.setattr(guests.time, "sleep", slept.append)
|
|
220
|
+
guests.VMwareMachine(guest)._wait_for_the_desktop(settle=15)
|
|
221
|
+
assert len(ssh.commands) == 4 and "explorer" in ssh.commands[0]
|
|
222
|
+
assert slept == [3.0, 3.0, 11]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_no_desktop_is_an_error_that_names_automatic_sign_in(share, monkeypatch):
|
|
226
|
+
guest = _guest(share, windows=True, vmx="/v.vmx", snapshot="s")
|
|
227
|
+
monkeypatch.setattr(guest, "ssh", FakeSSH(lambda c: ("-1\n", 0)))
|
|
228
|
+
clock = iter(range(0, 1000, 50))
|
|
229
|
+
monkeypatch.setattr(guests.time, "monotonic", lambda: next(clock))
|
|
230
|
+
monkeypatch.setattr(guests.time, "sleep", lambda s: None)
|
|
231
|
+
with pytest.raises(GuestError, match="automatic sign-in"):
|
|
232
|
+
guests.VMwareMachine(guest, patience=120)._wait_for_the_desktop()
|
|
233
|
+
|
|
234
|
+
|
|
211
235
|
def test_machine_follows_the_settings(share):
|
|
212
236
|
assert isinstance(_guest(share, tart="base").machine(), guests.TartMachine)
|
|
213
237
|
assert isinstance(_guest(share, vmx="/v.vmx", snapshot="s").machine(), guests.VMwareMachine)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|