quality-loop 0.4.0__tar.gz → 0.4.1__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.0/quality_loop.egg-info → quality_loop-0.4.1}/PKG-INFO +1 -1
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/__init__.py +1 -1
- quality_loop-0.4.1/quality_loop/awake.py +85 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/runner.py +3 -3
- {quality_loop-0.4.0 → quality_loop-0.4.1/quality_loop.egg-info}/PKG-INFO +1 -1
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop.egg-info/SOURCES.txt +2 -0
- quality_loop-0.4.1/tests/test_awake.py +45 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/LICENSE +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/README.md +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/pyproject.toml +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/OPERATOR.md +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/__main__.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/adapter.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/audit.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/cli.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/compare.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/config.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/drive/__init__.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/drive/_mac.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/drive/_windows.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/drive/keys.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/drive/terminal.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/guest/__init__.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/guest/boot/__init__.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/guest/boot/launch.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/guest/boot/screen_agent.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/guest/screens.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/install.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/mcp.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/operator.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/pages.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/processes.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/progress.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/report.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/room.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/scenario.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/screens/__init__.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/screens/_mac.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/screens/_windows.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/session.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/skill/SKILL.md +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/skill/references/filing.md +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/skill/references/setup.md +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/standin.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop/transcript.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop.egg-info/dependency_links.txt +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop.egg-info/entry_points.txt +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop.egg-info/requires.txt +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/quality_loop.egg-info/top_level.txt +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/setup.cfg +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/tests/test_core.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/tests/test_drive.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/tests/test_guest.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/tests/test_loop.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/tests/test_mcp.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/tests/test_operator.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/tests/test_scenario_report.py +0 -0
- {quality_loop-0.4.0 → quality_loop-0.4.1}/tests/test_screens.py +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""This machine kept from idle sleep while a run is going (issue #16).
|
|
2
|
+
|
|
3
|
+
A run is mostly waiting - on a session, on a VM - and a machine left alone
|
|
4
|
+
for it goes to sleep: a Mac on battery within minutes. Nothing fails; the
|
|
5
|
+
run just stops while it sleeps, and a VM guest stops with it. One run of
|
|
6
|
+
43 minutes had spent 40 of them asleep.
|
|
7
|
+
|
|
8
|
+
So the runner holds the platform's "the system is busy" assertion for the
|
|
9
|
+
length of the run, and lets go when it ends. The display may still sleep,
|
|
10
|
+
and closing a laptop's lid still sleeps it: those are the person's to choose.
|
|
11
|
+
|
|
12
|
+
- macOS: ``caffeinate -i -w <this pid>``, which also ends with this process
|
|
13
|
+
if it dies without letting go.
|
|
14
|
+
- Windows: ``SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)``
|
|
15
|
+
from the runner's thread, cleared at the end (and by Windows when the
|
|
16
|
+
process exits).
|
|
17
|
+
- Elsewhere nothing is held.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import contextlib
|
|
23
|
+
import os
|
|
24
|
+
import shutil
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
ES_CONTINUOUS = 0x80000000
|
|
29
|
+
ES_SYSTEM_REQUIRED = 0x00000001
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@contextlib.contextmanager
|
|
33
|
+
def held(say=print):
|
|
34
|
+
"""Idle sleep held off for the ``with`` block, where the platform allows."""
|
|
35
|
+
release = _hold()
|
|
36
|
+
if release is None:
|
|
37
|
+
yield False
|
|
38
|
+
return
|
|
39
|
+
say("awake: idle sleep is held off for the run (closing the lid still sleeps it)")
|
|
40
|
+
try:
|
|
41
|
+
yield True
|
|
42
|
+
finally:
|
|
43
|
+
release()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _hold():
|
|
47
|
+
if sys.platform == "darwin":
|
|
48
|
+
return _hold_mac()
|
|
49
|
+
if os.name == "nt":
|
|
50
|
+
return _hold_windows()
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _hold_mac():
|
|
55
|
+
caffeinate = shutil.which("caffeinate")
|
|
56
|
+
if caffeinate is None:
|
|
57
|
+
return None
|
|
58
|
+
try:
|
|
59
|
+
process = subprocess.Popen([caffeinate, "-i", "-w", str(os.getpid())],
|
|
60
|
+
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
|
61
|
+
stderr=subprocess.DEVNULL)
|
|
62
|
+
except OSError:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
def release():
|
|
66
|
+
process.terminate()
|
|
67
|
+
try:
|
|
68
|
+
process.wait(5)
|
|
69
|
+
except subprocess.TimeoutExpired:
|
|
70
|
+
process.kill()
|
|
71
|
+
|
|
72
|
+
return release
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _hold_windows():
|
|
76
|
+
import ctypes
|
|
77
|
+
try:
|
|
78
|
+
kernel32 = ctypes.WinDLL("kernel32")
|
|
79
|
+
except OSError:
|
|
80
|
+
return None
|
|
81
|
+
kernel32.SetThreadExecutionState.restype = ctypes.c_uint
|
|
82
|
+
kernel32.SetThreadExecutionState.argtypes = [ctypes.c_uint]
|
|
83
|
+
if not kernel32.SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED):
|
|
84
|
+
return None
|
|
85
|
+
return lambda: kernel32.SetThreadExecutionState(ES_CONTINUOUS)
|
|
@@ -41,8 +41,8 @@ import tempfile
|
|
|
41
41
|
import threading
|
|
42
42
|
|
|
43
43
|
import quality_loop
|
|
44
|
-
from quality_loop import (audit, drive, operator, pages, processes, progress,
|
|
45
|
-
room, scenario, screens, standin, transcript)
|
|
44
|
+
from quality_loop import (audit, awake, drive, operator, pages, processes, progress,
|
|
45
|
+
report, room, scenario, screens, standin, transcript)
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
class RunnerError(Exception):
|
|
@@ -200,7 +200,7 @@ def run(adapter, names: list[str] | None = None, model: str | None = None,
|
|
|
200
200
|
state = "done"
|
|
201
201
|
try:
|
|
202
202
|
bundle = adapter.bundle()
|
|
203
|
-
with adapter.product():
|
|
203
|
+
with awake.held(status.say), adapter.product():
|
|
204
204
|
def work(item):
|
|
205
205
|
case, variant = item
|
|
206
206
|
label = label_of(case, variant)
|
|
@@ -6,6 +6,7 @@ quality_loop/__init__.py
|
|
|
6
6
|
quality_loop/__main__.py
|
|
7
7
|
quality_loop/adapter.py
|
|
8
8
|
quality_loop/audit.py
|
|
9
|
+
quality_loop/awake.py
|
|
9
10
|
quality_loop/cli.py
|
|
10
11
|
quality_loop/compare.py
|
|
11
12
|
quality_loop/config.py
|
|
@@ -44,6 +45,7 @@ quality_loop/screens/_windows.py
|
|
|
44
45
|
quality_loop/skill/SKILL.md
|
|
45
46
|
quality_loop/skill/references/filing.md
|
|
46
47
|
quality_loop/skill/references/setup.md
|
|
48
|
+
tests/test_awake.py
|
|
47
49
|
tests/test_core.py
|
|
48
50
|
tests/test_drive.py
|
|
49
51
|
tests/test_guest.py
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""The runner keeps the machine from idle sleep while a run goes (#16)."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from quality_loop import awake
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.skipif(sys.platform != "darwin", reason="caffeinate is macOS's")
|
|
13
|
+
def test_a_mac_holds_caffeinate_on_this_process_for_the_block():
|
|
14
|
+
said = []
|
|
15
|
+
with awake.held(said.append) as holding:
|
|
16
|
+
assert holding
|
|
17
|
+
listed = subprocess.run(["pgrep", "-f", f"caffeinate -i -w {os.getpid()}"],
|
|
18
|
+
capture_output=True, text=True)
|
|
19
|
+
assert listed.stdout.strip()
|
|
20
|
+
assert said and said[0].startswith("awake:")
|
|
21
|
+
listed = subprocess.run(["pgrep", "-f", f"caffeinate -i -w {os.getpid()}"],
|
|
22
|
+
capture_output=True, text=True)
|
|
23
|
+
assert not listed.stdout.strip()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_where_nothing_can_be_held_the_run_goes_on_quietly(monkeypatch):
|
|
27
|
+
monkeypatch.setattr(awake.sys, "platform", "linux")
|
|
28
|
+
monkeypatch.setattr(awake.os, "name", "posix")
|
|
29
|
+
said = []
|
|
30
|
+
with awake.held(said.append) as holding:
|
|
31
|
+
assert holding is False
|
|
32
|
+
assert said == []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_a_mac_without_caffeinate_holds_nothing(monkeypatch):
|
|
36
|
+
monkeypatch.setattr(awake.sys, "platform", "darwin")
|
|
37
|
+
monkeypatch.setattr(awake.shutil, "which", lambda name: None)
|
|
38
|
+
with awake.held(lambda line: None) as holding:
|
|
39
|
+
assert holding is False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.skipif(os.name != "nt", reason="SetThreadExecutionState is Windows'")
|
|
43
|
+
def test_windows_sets_and_clears_the_execution_state():
|
|
44
|
+
with awake.held(lambda line: None) as holding:
|
|
45
|
+
assert holding
|
|
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
|