Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 58 additions & 7 deletions hyperwall/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,8 +429,11 @@ def _find_file(root: Path, names: tuple[str, ...]) -> Path | None:
return None


def _power_sleep_summary(path: Path | None) -> dict[str, Any]:
"""Summarize independent AC/lid/sleep evidence without calling macOS tools."""
def _power_sleep_summary(
path: Path | None,
run_env_path: Path | None = None,
) -> dict[str, Any]:
"""Summarize AC, sleep, and open-lid/docked-clamshell evidence."""
if path is None:
return {
"path": None,
Expand All @@ -439,13 +442,29 @@ def _power_sleep_summary(path: Path | None) -> dict[str, Any]:
"assertion_samples": 0,
"ac_power_samples": 0,
"lid_open_samples": 0,
"lid_closed_samples": 0,
"lid_evidence_samples": 0,
"ac_power_observed": False,
"lid_open_observed": False,
"lid_closed_observed": False,
"external_display_observed": False,
"docked_clamshell_observed": False,
"lid_evidence_mode": None,
}
try:
text = path.read_text(encoding="utf-8", errors="replace")
except OSError:
text = ""
if run_env_path is None:
candidate = path.parent / "run.env"
run_env_path = candidate if candidate.is_file() else None
try:
run_env = (
run_env_path.read_text(encoding="utf-8", errors="replace")
if run_env_path is not None else ""
)
except OSError:
run_env = ""
sections = re.split(r"(?m)^=== .* ===$", text)[1:]
samples = len(sections)
assertion_samples = sum(
Expand All @@ -472,17 +491,48 @@ def _power_sleep_summary(path: Path | None) -> dict[str, Any]:
)
for section in sections
)
ac_power = ac_power_samples > 0
lid_closed_samples = sum(
bool(
re.search(
r'"?AppleClamshellState"?\s*=\s*(?:Yes|1|true)',
section,
re.IGNORECASE,
)
)
for section in sections
)
external_display_observed = bool(
re.search(r"(?im)^\s*external_display_observed\s*=\s*1\s*$", run_env)
)
lid_open = lid_open_samples > 0
lid_closed = lid_closed_samples > 0
docked_clamshell = lid_closed and external_display_observed
lid_evidence_samples = max(
lid_open_samples,
lid_closed_samples if external_display_observed else 0,
)
lid_evidence_mode = (
"open"
if lid_open_samples > 0
else "docked_clamshell"
if docked_clamshell
else None
)
return {
"path": str(path),
"present": bool(text.strip()),
"samples": samples,
"assertion_samples": assertion_samples,
"ac_power_samples": ac_power_samples,
"lid_open_samples": lid_open_samples,
"ac_power_observed": ac_power,
"lid_closed_samples": lid_closed_samples,
"lid_evidence_samples": lid_evidence_samples,
"ac_power_observed": ac_power_samples > 0,
"lid_open_observed": lid_open,
"lid_closed_observed": lid_closed,
"external_display_observed": external_display_observed,
"docked_clamshell_observed": docked_clamshell,
"lid_evidence_mode": lid_evidence_mode,
}


Expand Down Expand Up @@ -793,7 +843,7 @@ def analyze_run(
stats_candidates = sorted(_safe_children(root, "hyperwall_stats_*.json"))
stats_path = stats_candidates[0] if len(stats_candidates) == 1 else None
power_path = _find_file(root, ("power_sleep.log",))
power_summary = _power_sleep_summary(power_path)
power_summary = _power_sleep_summary(power_path, root / "run.env")
stats_valid = _has_valid_stats(stats_path)
stats_summary = _stats_summary(stats_path)
manifest = (
Expand Down Expand Up @@ -973,7 +1023,7 @@ def analyze_run(
power_summary["samples"] >= minimum_power_samples
and power_summary["assertion_samples"] >= minimum_power_samples
and power_summary["ac_power_samples"] >= minimum_power_samples
and power_summary["lid_open_samples"] >= minimum_power_samples
and power_summary["lid_evidence_samples"] >= minimum_power_samples
)
power_complete = (
power_summary["present"]
Expand All @@ -982,7 +1032,8 @@ def analyze_run(
gates["power_sleep_evidence"] = _gate(
"PASS" if power_complete else "BLOCK" if expected_duration_seconds is not None else "WARNING",
power_summary,
"A live soak requires independent AC-power, lid, and sleep-assertion evidence; "
"A live soak requires independent AC-power, sleep assertions, and "
"either an open lid or verified docked-clamshell external-display evidence; "
"caffeinate alone is not proof that the Mac stayed awake.",
)
for key, note in (
Expand Down
8 changes: 7 additions & 1 deletion soak_wall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ PIDS=()
printf 'host='; sw_vers
printf 'hardware='; system_profiler SPHardwareDataType 2>/dev/null || true
printf 'env=HYPERWALL_SOAK_MINUTES=%s HYPERWALL_SOAK_DWELL_S=%s HYPERWALL_SOAK_PROFILE=%s HYPERWALL_HWDEC=%s HYPERWALL_CACHE_BUDGET_MB=%s HYPERWALL_DEMUXER_PER_CELL_MB=%s\n' "$HYPERWALL_SOAK_MINUTES" "$HYPERWALL_SOAK_DWELL_S" "$HYPERWALL_SOAK_PROFILE" "${HYPERWALL_HWDEC:-}" "${HYPERWALL_CACHE_BUDGET_MB:-}" "${HYPERWALL_DEMUXER_PER_CELL_MB:-}"
printf 'power_evidence_required=AC_power_lid_open_no_sleep\n'
display_probe="$(system_profiler SPDisplaysDataType 2>/dev/null || true)"
if printf '%s\n' "$display_probe" | grep -Eqi 'Display Type: External|Connection Type: (DisplayPort|HDMI|DVI|USB|Thunderbolt|AirPlay)'; then
printf 'external_display_observed=1\n'
else
printf 'external_display_observed=0\n'
fi
printf 'power_evidence_required=AC_power_sleep_assertions_open_lid_or_docked_clamshell\n'
printf 'power_evidence_artifact=power_sleep.log\n'
} > "$REPORT_DIR/run.env"

Expand Down
86 changes: 86 additions & 0 deletions tests/test_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,92 @@ def test_power_sleep_summary_accepts_quoted_ioreg_clamshell_key():
assert summary["lid_open_observed"] is True


def test_power_sleep_summary_accepts_docked_clamshell_with_external_display():
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
power_path = root / "power_sleep.log"
power_path.write_text(
"=== sample ===\n"
"--- pmset -g ps ---\n"
"Now drawing from 'AC Power'\n"
"--- pmset -g assertions ---\n"
"PreventSystemSleep 1\n"
"--- ioreg AppleClamshellState ---\n"
'"AppleClamshellState" = Yes\n',
encoding="utf-8",
)
(root / "run.env").write_text(
"external_display_observed=1\n",
encoding="utf-8",
)
summary = _power_sleep_summary(power_path)

assert summary["lid_closed_observed"] is True
assert summary["external_display_observed"] is True
assert summary["docked_clamshell_observed"] is True


def test_analyze_run_accepts_docked_clamshell_power_evidence():
cells = [
{
"cell": 0,
"totals": {"frame-drop-count": 0},
"info": {"hwdec-current": "no"},
"freezes": 0,
"freeze_seconds": 0,
}
]
records = [
{"event": "start", "baseline": {"ws_mb": 1}},
{
"event": "sample",
"wall_seconds": 30,
"cells": 1,
"actions": {},
"resources": {"ws_mb": 1},
},
{
"event": "finish",
"wall_seconds": 60,
"resources": {"ws_mb": 1},
"invariant_violations": 0,
},
]
power_sample = (
"=== sample ===\n"
"Now drawing from 'AC Power'\n"
"PreventSystemSleep 1\n"
'"AppleClamshellState" = Yes\n'
)
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "hyperwall.log").write_text(
"Runtime: Hyperwall\nSOAK start:\n",
encoding="utf-8",
)
(root / "hyperwall_stats_a.json").write_text(
json.dumps({"cells": cells}),
encoding="utf-8",
)
(root / "hyperwall_soak_a.jsonl").write_text(
"\n".join(json.dumps(record) for record in records) + "\n",
encoding="utf-8",
)
(root / "power_sleep.log").write_text(power_sample * 4, encoding="utf-8")
(root / "run.env").write_text(
"external_display_observed=1\n",
encoding="utf-8",
)
result = analyze_run(
root,
expected_cells=1,
expected_duration_seconds=60,
)

assert result["gates"]["power_sleep_evidence"]["status"] == "PASS"
assert result["gates"]["power_sleep_evidence"]["value"]["docked_clamshell_observed"] is True


def test_power_sleep_summary_rejects_empty_assertion_output():
with tempfile.TemporaryDirectory() as directory:
path = Path(directory, "power_sleep.log")
Expand Down
2 changes: 2 additions & 0 deletions tests/test_soak_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ def test_macos_soak_launcher_collects_system_telemetry():
"HYPERWALL_STATS=1",
"HYPERWALL_PERFTRACE=1",
"powermetrics",
"SPDisplaysDataType",
"external_display_observed",
"nettop",
"vm_stat",
"chmod 700",
Expand Down
Loading