test harness: smoke-pass B-enforce, fix four issues
Harness now passes 9/9 across baseline + heal + idempotent phases on the win11 VM (Standard/Machine), with 6 drift scenarios applied + healed between the baseline and heal cycles in ~30s total. Fixes: 1. lib/qga-run.py - extracted the qga round-trip out of an inline `python3 - <<PY` heredoc. The inline form clobbered stdin (heredoc replaces stdin to feed python the script, leaving sys.stdin empty for the PowerShell snippet the function caller piped in). 2. lib/qga.sh - dropped `set -euo pipefail`. When sourced, it leaked into the harness shell. Then any captured `out=$(qga_run_ps ...)` that exited non-zero (verify-state.ps1 returns 1 on any FAIL, normal during drift phases) would silently abort the harness. Callers handle non-zero with `|| rc=$?`. 3. B-enforce/run.sh do_verify - rewritten to capture rc, parse summary line, distinguish expect_pass=true vs false, route to ok / fail helper without aborting the harness on a normal non-zero verify. 4. matrix.json WJF Defect Tracker entry - switched detection from File to Registry (uninstall key DisplayVersion). The MSI does not drop the Defect_Tracker.exe artifact at the documented path even though the manifest's File detection treats it as installed; the uninstall reg entry is the reliable install marker. v2 manifest's File detection path may also need fixing, separate task. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -114,30 +114,32 @@ if ($f) { Get-Content $f.FullName -Tail 25 } else { "no enforce log" }
|
|||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
run_verify() {
|
# Verify wrapper: capture output, parse summary line. PS verify returns
|
||||||
local phase_label="$1"
|
# rc=1 when any check fails - capture that without aborting the harness.
|
||||||
log "verify ($phase_label)..."
|
|
||||||
if qga_run_ps <<'EOF' | tee /dev/stderr | grep -q '=== verify summary' && qga_run_ps <<'EOF2' | grep -qE '0/[0-9]+ passed|^=== verify summary: ([0-9]+)/\1 passed'; then :; fi
|
|
||||||
& 'C:\Tools\test-harness\verify-state.ps1' -MatrixPath 'C:\Tools\test-harness\matrix.json' -PCType '$PCTYPE' -PCSubType '$PCSUBTYPE'
|
|
||||||
EOF
|
|
||||||
EOF2
|
|
||||||
}
|
|
||||||
|
|
||||||
# Simpler verify wrapper: capture output, parse summary line
|
|
||||||
do_verify() {
|
do_verify() {
|
||||||
local phase_label="$1" expect_pass="$2"
|
local phase_label="$1" expect_pass="$2"
|
||||||
log "verify ($phase_label, expect_pass=$expect_pass)"
|
log "verify ($phase_label, expect_pass=$expect_pass)"
|
||||||
local out
|
local out rc=0
|
||||||
out=$(qga_run_ps <<EOF
|
out=$(qga_run_ps <<EOF
|
||||||
& 'C:\\Tools\\test-harness\\verify-state.ps1' -MatrixPath 'C:\\Tools\\test-harness\\matrix.json' -PCType '$PCTYPE' -PCSubType '$PCSUBTYPE'
|
& 'C:\\Tools\\test-harness\\verify-state.ps1' -MatrixPath 'C:\\Tools\\test-harness\\matrix.json' -PCType '$PCTYPE' -PCSubType '$PCSUBTYPE'
|
||||||
EOF
|
EOF
|
||||||
)
|
) || rc=$?
|
||||||
echo "$out"
|
echo "$out"
|
||||||
if echo "$out" | grep -qE 'verify summary: [0-9]+/[0-9]+ passed' && echo "$out" | grep -qE '\[FAIL\]'; then
|
local summary
|
||||||
# there are failures
|
summary=$(echo "$out" | grep -oE 'verify summary: [0-9]+/[0-9]+ passed' | head -1)
|
||||||
if [[ "$expect_pass" == "true" ]]; then fail "$phase_label: had failures, expected all pass"; fi
|
[[ -z "$summary" ]] && summary='(no summary line)'
|
||||||
elif echo "$out" | grep -qE 'verify summary: [0-9]+/[0-9]+ passed' && ! echo "$out" | grep -qE '\[FAIL\]'; then
|
if echo "$out" | grep -qE '\[FAIL\]'; then
|
||||||
if [[ "$expect_pass" == "true" ]]; then ok "$phase_label: all pass"; fi
|
if [[ "$expect_pass" == "true" ]]; then
|
||||||
|
fail "$phase_label: $summary - failures present, expected all pass"
|
||||||
|
else
|
||||||
|
ok "$phase_label: $summary - failures present (expected)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ "$expect_pass" == "true" ]]; then
|
||||||
|
ok "$phase_label: $summary"
|
||||||
|
else
|
||||||
|
fail "$phase_label: $summary - all pass, expected failures (drift not applied?)"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
42
playbook/shopfloor-setup/common/test/lib/qga-run.py
Normal file
42
playbook/shopfloor-setup/common/test/lib/qga-run.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# qga-run.py - run a PowerShell snippet inside the win11 VM via qemu-guest-agent.
|
||||||
|
# Stdin = PS snippet. Stdout = combined stdout, then optional STDERR block,
|
||||||
|
# then "--- exit N ---". Exit code 0 on PS rc 0, 1 otherwise.
|
||||||
|
import base64, json, os, subprocess, sys, time
|
||||||
|
|
||||||
|
DOMAIN = os.environ.get('VM_DOMAIN', 'win11')
|
||||||
|
TIMEOUT = int(os.environ.get('QGA_TIMEOUT', '300'))
|
||||||
|
|
||||||
|
def virsh(cmd):
|
||||||
|
p = subprocess.run(
|
||||||
|
['virsh', '-c', 'qemu:///system', 'qemu-agent-command', DOMAIN, json.dumps(cmd)],
|
||||||
|
capture_output=True, text=True, timeout=120,
|
||||||
|
)
|
||||||
|
if p.returncode != 0:
|
||||||
|
sys.exit(f"virsh err: {p.stderr.strip()}")
|
||||||
|
return json.loads(p.stdout)['return']
|
||||||
|
|
||||||
|
snippet = sys.stdin.read()
|
||||||
|
if not snippet.strip():
|
||||||
|
sys.exit("qga-run.py: empty PowerShell snippet on stdin")
|
||||||
|
|
||||||
|
args = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', snippet]
|
||||||
|
pid = virsh({'execute': 'guest-exec',
|
||||||
|
'arguments': {'path': 'powershell.exe', 'arg': args, 'capture-output': True}})['pid']
|
||||||
|
|
||||||
|
deadline = time.time() + TIMEOUT
|
||||||
|
while time.time() < deadline:
|
||||||
|
st = virsh({'execute': 'guest-exec-status', 'arguments': {'pid': pid}})
|
||||||
|
if st.get('exited'):
|
||||||
|
out = base64.b64decode(st.get('out-data', '')).decode('utf-8', 'replace')
|
||||||
|
err = base64.b64decode(st.get('err-data', '')).decode('utf-8', 'replace')
|
||||||
|
rc = st.get('exitcode')
|
||||||
|
sys.stdout.write(out)
|
||||||
|
if err:
|
||||||
|
sys.stdout.write('\n--- STDERR ---\n')
|
||||||
|
sys.stdout.write(err)
|
||||||
|
sys.stdout.write(f'\n--- exit {rc} ---\n')
|
||||||
|
sys.exit(0 if rc == 0 else 1)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
sys.exit(f"timeout waiting for pid {pid}")
|
||||||
@@ -6,8 +6,13 @@
|
|||||||
# All commands run as NT AUTHORITY\SYSTEM inside the VM (qemu-ga's service
|
# All commands run as NT AUTHORITY\SYSTEM inside the VM (qemu-ga's service
|
||||||
# context). See reference-vm-qga-as-system memory note for why this is
|
# context). See reference-vm-qga-as-system memory note for why this is
|
||||||
# preferred over WinRM for dispatcher / manifest-engine tests.
|
# preferred over WinRM for dispatcher / manifest-engine tests.
|
||||||
|
#
|
||||||
set -euo pipefail
|
# Sourced by harness scripts. Deliberately does NOT enable set -e because
|
||||||
|
# qga_run_ps returns non-zero whenever the inner PowerShell exits non-zero
|
||||||
|
# (expected during drift/verify phases), and a sourced set -e would silently
|
||||||
|
# abort the calling shell on every $(qga_run_ps ...) capture of a failing run.
|
||||||
|
# Callers that want strict mode should set it themselves AND wrap qga_run_ps
|
||||||
|
# captures with `|| rc=$?` so the non-zero exit does not propagate.
|
||||||
|
|
||||||
VM_DOMAIN="${VM_DOMAIN:-win11}"
|
VM_DOMAIN="${VM_DOMAIN:-win11}"
|
||||||
VM_IP="${VM_IP:-192.168.122.225}"
|
VM_IP="${VM_IP:-192.168.122.225}"
|
||||||
@@ -26,38 +31,17 @@ qga() {
|
|||||||
|| die "qga call failed: $payload"
|
|| die "qga call failed: $payload"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run a PowerShell snippet inside the VM as SYSTEM. Stdin = snippet,
|
# Run a PowerShell snippet inside the VM as SYSTEM. Stdin = snippet.
|
||||||
# stdout = exit-decorated combined stdout+stderr from the PS process.
|
# Stdout = combined PS stdout, then optional "--- STDERR ---" block,
|
||||||
|
# then "--- exit N ---". Returns 0 iff the PS process returned 0.
|
||||||
|
#
|
||||||
|
# Implementation note: keep the python script in a sibling file rather
|
||||||
|
# than inlined via heredoc. An inline `python3 - <<PY ... PY` redirects
|
||||||
|
# stdin to the heredoc, which clobbers the PowerShell snippet the
|
||||||
|
# caller piped in.
|
||||||
|
QGA_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
qga_run_ps() {
|
qga_run_ps() {
|
||||||
python3 - <<'PY'
|
python3 "$QGA_LIB_DIR/qga-run.py"
|
||||||
import base64, json, subprocess, sys, time, os
|
|
||||||
DOMAIN = os.environ.get('VM_DOMAIN', 'win11')
|
|
||||||
TIMEOUT = int(os.environ.get('QGA_TIMEOUT', '300'))
|
|
||||||
def virsh(cmd):
|
|
||||||
p = subprocess.run(['virsh','-c','qemu:///system','qemu-agent-command',DOMAIN, json.dumps(cmd)],
|
|
||||||
capture_output=True, text=True, timeout=120)
|
|
||||||
if p.returncode != 0:
|
|
||||||
sys.exit(f"virsh err: {p.stderr.strip()}")
|
|
||||||
return json.loads(p.stdout)['return']
|
|
||||||
snippet = sys.stdin.read()
|
|
||||||
args = ['-NoProfile','-ExecutionPolicy','Bypass','-Command', snippet]
|
|
||||||
pid = virsh({'execute':'guest-exec','arguments':{'path':'powershell.exe','arg':args,'capture-output':True}})['pid']
|
|
||||||
deadline = time.time() + TIMEOUT
|
|
||||||
while time.time() < deadline:
|
|
||||||
st = virsh({'execute':'guest-exec-status','arguments':{'pid':pid}})
|
|
||||||
if st.get('exited'):
|
|
||||||
out = base64.b64decode(st.get('out-data','')).decode('utf-8','replace')
|
|
||||||
err = base64.b64decode(st.get('err-data','')).decode('utf-8','replace')
|
|
||||||
rc = st.get('exitcode')
|
|
||||||
sys.stdout.write(out)
|
|
||||||
if err:
|
|
||||||
sys.stdout.write('\n--- STDERR ---\n')
|
|
||||||
sys.stdout.write(err)
|
|
||||||
sys.stdout.write(f'\n--- exit {rc} ---\n')
|
|
||||||
sys.exit(0 if rc == 0 else 1)
|
|
||||||
time.sleep(0.5)
|
|
||||||
sys.exit(f"timeout waiting for pid {pid}")
|
|
||||||
PY
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Wait until qemu-ga responds to guest-ping
|
# Wait until qemu-ga responds to guest-ping
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"scopes": ["common", "standard-machine"],
|
"scopes": ["common", "standard-machine"],
|
||||||
"apps": [
|
"apps": [
|
||||||
{ "name": "Adobe Acrobat Reader DC", "verify": { "method": "Registry", "path": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{AC76BA86-7AD7-1033-7B44-AC0F074E4100}", "name": "DisplayVersion", "value": "25.001.20531" } },
|
{ "name": "Adobe Acrobat Reader DC", "verify": { "method": "Registry", "path": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{AC76BA86-7AD7-1033-7B44-AC0F074E4100}", "name": "DisplayVersion", "value": "25.001.20531" } },
|
||||||
{ "name": "WJF Defect Tracker", "verify": { "method": "File", "path": "C:\\Program Files (x86)\\WJF_Defect_Tracker\\Defect_Tracker.exe" } },
|
{ "name": "WJF Defect Tracker", "verify": { "method": "Registry", "path": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{CC1B4D32-1606-4A3F-8F24-31312F723D5C}", "name": "DisplayVersion", "value": "01.00.0102" } },
|
||||||
{ "name": "3OF9 barcode font", "verify": { "method": "File", "path": "C:\\Windows\\Fonts\\3OF9.ttf" } },
|
{ "name": "3OF9 barcode font", "verify": { "method": "File", "path": "C:\\Windows\\Fonts\\3OF9.ttf" } },
|
||||||
{ "name": "Edge IE-Mode site list", "verify": { "method": "Hash", "path": "C:\\ProgramData\\Edge\\enterprise-mode-site-list.xml", "value": "16F2A6E45EFA19ED7B1C54B264D6B33597678D3A5303255BC7CEB7E8510C60FC" } },
|
{ "name": "Edge IE-Mode site list", "verify": { "method": "Hash", "path": "C:\\ProgramData\\Edge\\enterprise-mode-site-list.xml", "value": "16F2A6E45EFA19ED7B1C54B264D6B33597678D3A5303255BC7CEB7E8510C60FC" } },
|
||||||
{ "name": "OpenText HostExplorer", "verify": { "method": "Registry", "path": "HKLM:\\SOFTWARE\\GE\\OpenText", "name": "Installed", "value": "15.0.SP1.2" } },
|
{ "name": "OpenText HostExplorer", "verify": { "method": "Registry", "path": "HKLM:\\SOFTWARE\\GE\\OpenText", "name": "Installed", "value": "15.0.SP1.2" } },
|
||||||
|
|||||||
Reference in New Issue
Block a user