Files
hermes-agent/tests/scripts/test_powershell_script_syntax.py
ethernet e0735f8891 docs(tests): name platforms() instead of the retired OS markers
linux_only / macos_only / windows_only were replaced by platforms(...)
and conftest now rejects them, but test docstrings and comments still
explained gating in terms of the old names, which points readers at a
marker they cannot use. Reword them to platforms("<os>") and the
-m platforms lane. ci.yaml's comment already says platforms("windows").
2026-09-24 11:50:30 -04:00

116 lines
4.3 KiB
Python

"""The repo's PowerShell scripts must parse — checked with the REAL parser.
install.ps1 is fetched standalone (``irm | iex``) by every Windows user, so
a parse error ships instantly and breaks installs at line 1. Linux CI cannot
execute PowerShell, which is why the other install_ps1 tests are source-regex
probes; this test runs on the ``platforms("windows")`` lane (tests-os.yml), where a
PowerShell host is part of the OS, and asks the actual language parser.
The harness is written to a file and invoked with ``-File`` because inline
``-Command`` needs every ``$`` and quote to survive a shell hop, and
``[ref]$null`` fails inside ``-Command`` with "[ref] cannot be applied to a
variable that does not exist" — an error easy to misread as a finding
against the file under test.
"""
from __future__ import annotations
import shutil
import subprocess
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
# Every PowerShell script a user or CI machine actually runs. Discovered,
# not hardcoded: a new script in scripts/ gets gated automatically.
PS1_SCRIPTS = sorted(
list(REPO_ROOT.glob("scripts/*.ps1"))
+ list(REPO_ROOT.glob("tests/install/*.ps1"))
+ list(REPO_ROOT.glob("*.ps1")) # repo-root scripts (setup-hermes.ps1, activate.ps1)
)
_HARNESS = """\
param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Paths)
$failed = $false
foreach ($p in $Paths) {
$tokens = $null
$errors = $null
[System.Management.Automation.Language.Parser]::ParseFile(
$p, [ref]$tokens, [ref]$errors) | Out-Null
if ($errors.Count) {
$failed = $true
Write-Output "PS SYNTAX ERRORS: $p"
$errors | Select-Object -First 8 | ForEach-Object {
Write-Output (" {0}: {1}" -f $_.Extent.StartLineNumber, $_.Message)
}
} else {
Write-Output "PS SYNTAX OK: $p"
}
}
if ($failed) { exit 1 }
"""
# Scripts users fetch and run as a string, never from disk:
# `& ([scriptblock]::Create((irm <raw url>)))` (website/docs/user-guide/
# windows-native.md, scripts/update-test/HANDOFF.md). Windows PowerShell
# 5.1's irm keeps a UTF-8 BOM as a literal U+FEFF, so `param(` is no longer
# the first statement and the script dies with "The assignment expression is
# not valid". ParseFile above treats the BOM as an encoding marker, so it
# cannot catch this; the BOM has been stripped twice and re-added once.
# (The GUI bootstrap adds a BOM to its *cached* copy on purpose, because it
# runs that copy with -File; see install_script.rs::prepare_cached_script_bytes.)
FETCHED_AS_STRING = (
REPO_ROOT / "scripts/install.ps1",
REPO_ROOT / "scripts/update-test/hermes-update-rehearsal.ps1",
)
@pytest.mark.parametrize("script", FETCHED_AS_STRING, ids=lambda p: p.name)
def test_fetched_scripts_have_no_utf8_bom(script: Path) -> None:
assert not script.read_bytes().startswith(b"\xef\xbb\xbf"), (
f"{script.relative_to(REPO_ROOT)} starts with a UTF-8 BOM; "
"[scriptblock]::Create((irm ...)) cannot parse it on PowerShell 5.1"
)
def _powershell_host() -> str | None:
for name in ("pwsh", "powershell"):
found = shutil.which(name)
if found:
return found
return None
def test_repo_has_powershell_scripts_to_gate() -> None:
"""The glob must keep finding the scripts this gate exists for."""
names = {p.name for p in PS1_SCRIPTS}
assert "install.ps1" in names, PS1_SCRIPTS
@pytest.mark.platforms("windows")
def test_powershell_scripts_parse(tmp_path: Path) -> None:
host = _powershell_host()
assert host is not None, "no PowerShell host on a Windows runner"
harness = tmp_path / "ps-syntax-check.ps1"
harness.write_text(_HARNESS, encoding="utf-8")
result = subprocess.run(
[host, "-NoProfile", "-ExecutionPolicy", "Bypass",
"-File", str(harness)]
+ [str(p) for p in PS1_SCRIPTS],
capture_output=True,
text=True,
timeout=120,
)
assert result.returncode == 0, (
f"PowerShell parse errors:\n{result.stdout}\n{result.stderr}"
)
# Belt and braces: the harness printed a verdict for every script, so a
# harness that silently checked nothing cannot pass.
for script in PS1_SCRIPTS:
assert f"PS SYNTAX OK: {script}" in result.stdout, result.stdout