MIL-013: Debian container and Windows Sandbox checks
A Debian image (python:3.13-slim-bookworm with Xvfb and GNOME's window manager) runs the linters, the whole test suite, the screenshots and the smoke test of a PyInstaller Linux binary and of the source program, so no window opens on the development machine. The screenshot tool works on Linux, a smoke driver presses the known button with the virtual mouse, and Windows Sandbox scripts are written (not run, they open a window on the host). README and the wiki Install pages describe it. Review records RC-077 (Python) and RC-078 (shell). Task: MIL-013#2 Task: MIL-013#3 Task: MIL-013#4 Task: MIL-013#5 Task: MIL-013#6 Task: MIL-013#7 Task: MIL-013#9 Task: MIL-013#10
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
<!--
|
||||
Windows Sandbox configuration of the flash card smoke test (ADR-0005).
|
||||
Do not open this file directly: tools/sandbox/run.ps1 replaces the three
|
||||
@...@ placeholders with absolute host paths and starts the sandbox.
|
||||
|
||||
dist read-only the built flash-card.exe
|
||||
scripts read-only smoke.ps1, which runs inside the sandbox
|
||||
out read-write result.json and screenshots written by smoke.ps1
|
||||
-->
|
||||
<Configuration>
|
||||
<VGpu>Disable</VGpu>
|
||||
<Networking>Disable</Networking>
|
||||
<AudioInput>Disable</AudioInput>
|
||||
<VideoInput>Disable</VideoInput>
|
||||
<ClipboardRedirection>Disable</ClipboardRedirection>
|
||||
<MappedFolders>
|
||||
<MappedFolder>
|
||||
<HostFolder>@DIST@</HostFolder>
|
||||
<SandboxFolder>C:\fc\dist</SandboxFolder>
|
||||
<ReadOnly>true</ReadOnly>
|
||||
</MappedFolder>
|
||||
<MappedFolder>
|
||||
<HostFolder>@SCRIPTS@</HostFolder>
|
||||
<SandboxFolder>C:\fc\scripts</SandboxFolder>
|
||||
<ReadOnly>true</ReadOnly>
|
||||
</MappedFolder>
|
||||
<MappedFolder>
|
||||
<HostFolder>@OUT@</HostFolder>
|
||||
<SandboxFolder>C:\fc\out</SandboxFolder>
|
||||
<ReadOnly>false</ReadOnly>
|
||||
</MappedFolder>
|
||||
</MappedFolders>
|
||||
<LogonCommand>
|
||||
<Command>powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\fc\scripts\smoke.ps1 -Runs @RUNS@</Command>
|
||||
</LogonCommand>
|
||||
</Configuration>
|
||||
@@ -0,0 +1,56 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Start the Windows Sandbox smoke test of dist\flash-card.exe from the host.
|
||||
|
||||
.DESCRIPTION
|
||||
Writes a configuration from flash-card.wsb with absolute paths, starts
|
||||
Windows Sandbox, waits for out\result.json and prints the summary.
|
||||
The tests themselves (window, click, progress file) run inside the sandbox
|
||||
with the sandbox's own mouse; the host's mouse and focus are not used.
|
||||
Windows Sandbox shows its own window while it runs. Needs Windows 11 Pro
|
||||
with the feature "Windows Sandbox" enabled.
|
||||
|
||||
.EXAMPLE
|
||||
powershell -File tools\sandbox\run.ps1 -Runs 5
|
||||
#>
|
||||
param(
|
||||
[int]$Runs = 5,
|
||||
[int]$TimeoutMinutes = 15
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
|
||||
$dist = Join-Path $root 'dist'
|
||||
$scripts = $PSScriptRoot
|
||||
$out = Join-Path $root 'build\sandbox-out'
|
||||
|
||||
if (-not (Test-Path (Join-Path $dist 'flash-card.exe'))) {
|
||||
throw "dist\flash-card.exe not found: build it first with python tools\build_executable.py"
|
||||
}
|
||||
if (-not (Get-Command WindowsSandbox.exe -ErrorAction SilentlyContinue)) {
|
||||
throw "WindowsSandbox.exe not found: enable the Windows feature 'Windows Sandbox' (Windows 11 Pro)"
|
||||
}
|
||||
if (Get-Process -Name 'WindowsSandbox*' -ErrorAction SilentlyContinue) {
|
||||
throw "A Windows Sandbox is already running: close it first (only one can run at a time)"
|
||||
}
|
||||
|
||||
Remove-Item $out -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Path $out -Force | Out-Null
|
||||
|
||||
$config = (Get-Content (Join-Path $scripts 'flash-card.wsb') -Raw) `
|
||||
-replace '@DIST@', $dist -replace '@SCRIPTS@', $scripts -replace '@OUT@', $out -replace '@RUNS@', $Runs
|
||||
$wsb = Join-Path $out 'run.wsb'
|
||||
Set-Content -Path $wsb -Value $config -Encoding UTF8
|
||||
|
||||
Write-Host "Starting Windows Sandbox ($Runs runs); results in $out"
|
||||
Start-Process -FilePath WindowsSandbox.exe -ArgumentList "`"$wsb`""
|
||||
|
||||
$result = Join-Path $out 'result.json'
|
||||
$end = (Get-Date).AddMinutes($TimeoutMinutes)
|
||||
while ((Get-Date) -lt $end -and -not (Test-Path $result)) { Start-Sleep -Seconds 5 }
|
||||
if (-not (Test-Path $result)) { throw "No result.json after $TimeoutMinutes minutes" }
|
||||
Start-Sleep -Seconds 3
|
||||
$summary = Get-Content $result -Raw | ConvertFrom-Json
|
||||
Write-Host ("passed {0} of {1} runs in {2}" -f $summary.passed, $summary.runs, $summary.os)
|
||||
$summary.results | Format-Table run, window, clicked, progress_rows, ok, seconds
|
||||
if ($summary.passed -ne $summary.runs) { exit 1 }
|
||||
@@ -0,0 +1,132 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Smoke test of flash-card.exe, run INSIDE Windows Sandbox (ADR-0005).
|
||||
|
||||
.DESCRIPTION
|
||||
Starts the executable on the sandbox's own desktop, presses the known button
|
||||
with the sandbox's own mouse, reads the progress file, takes screenshots and
|
||||
writes result.json to C:\fc\out. Then it shuts the sandbox down.
|
||||
It never touches the host: the host sees only the mapped folders.
|
||||
#>
|
||||
param(
|
||||
[int]$Runs = 5,
|
||||
[string]$Exe = 'C:\fc\dist\flash-card.exe',
|
||||
[string]$Out = 'C:\fc\out'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
Add-Type @'
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class Win {
|
||||
public delegate bool EnumProc(IntPtr h, IntPtr l);
|
||||
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc p, IntPtr l);
|
||||
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
|
||||
[DllImport("user32.dll")] public static extern bool GetClientRect(IntPtr h, out RECT r);
|
||||
[DllImport("user32.dll")] public static extern bool ClientToScreen(IntPtr h, ref POINT p);
|
||||
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
|
||||
[DllImport("user32.dll")] public static extern void mouse_event(uint f, uint x, uint y, uint d, UIntPtr e);
|
||||
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
||||
[DllImport("shcore.dll")] public static extern int SetProcessDpiAwareness(int v);
|
||||
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
|
||||
[StructLayout(LayoutKind.Sequential)] public struct POINT { public int X, Y; }
|
||||
public static IntPtr Find(string title) {
|
||||
IntPtr found = IntPtr.Zero;
|
||||
EnumWindows(delegate (IntPtr h, IntPtr l) {
|
||||
if (IsWindowVisible(h)) {
|
||||
var sb = new StringBuilder(256);
|
||||
GetWindowText(h, sb, 256);
|
||||
if (sb.ToString() == title) { found = h; return false; }
|
||||
}
|
||||
return true;
|
||||
}, IntPtr.Zero);
|
||||
return found;
|
||||
}
|
||||
}
|
||||
'@
|
||||
[void][Win]::SetProcessDpiAwareness(2)
|
||||
|
||||
function Wait-Window([int]$Seconds) {
|
||||
$end = (Get-Date).AddSeconds($Seconds)
|
||||
while ((Get-Date) -lt $end) {
|
||||
$h = [Win]::Find('Flashy')
|
||||
if ($h -ne [IntPtr]::Zero) { return $h }
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
return [IntPtr]::Zero
|
||||
}
|
||||
|
||||
function Save-Screen([string]$Path) {
|
||||
$b = [System.Windows.Forms.SystemInformation]::VirtualScreen
|
||||
$bmp = New-Object System.Drawing.Bitmap $b.Width, $b.Height
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$g.CopyFromScreen($b.Left, $b.Top, 0, 0, $bmp.Size)
|
||||
$bmp.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
$g.Dispose(); $bmp.Dispose()
|
||||
}
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
|
||||
$work = Join-Path $env:TEMP 'fc-smoke'
|
||||
$results = @()
|
||||
for ($n = 1; $n -le $Runs; $n++) {
|
||||
$run = [ordered]@{ run = $n; window = $false; clicked = $false; progress_rows = $null; ok = $false; seconds = 0 }
|
||||
$t0 = Get-Date
|
||||
$dir = Join-Path $work "run$n"
|
||||
Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Path (Join-Path $dir 'data') -Force | Out-Null
|
||||
Copy-Item $Exe (Join-Path $dir 'flash-card.exe')
|
||||
# three words: after one known word the progress file has a header and two cards
|
||||
[IO.File]::WriteAllText((Join-Path $dir 'data\ms-da.csv'), "ms;da`naaa;bbb`nccc;ddd`neee;fff`n", (New-Object Text.UTF8Encoding $false))
|
||||
$p = Start-Process -FilePath (Join-Path $dir 'flash-card.exe') -ArgumentList '--level', 'all', '--no-speech' -WorkingDirectory $dir -PassThru
|
||||
try {
|
||||
$h = Wait-Window 60
|
||||
if ($h -ne [IntPtr]::Zero) {
|
||||
$run.window = $true
|
||||
Start-Sleep -Milliseconds 1500
|
||||
if ($n -eq 1) { Save-Screen (Join-Path $Out 'sandbox-card.png') }
|
||||
$r = New-Object Win+RECT
|
||||
[void][Win]::GetClientRect($h, [ref]$r)
|
||||
$pt = New-Object Win+POINT
|
||||
[void][Win]::ClientToScreen($h, [ref]$pt)
|
||||
$x = $pt.X + 650
|
||||
$y = $pt.Y + $r.Bottom - 101
|
||||
[void][Win]::SetForegroundWindow($h)
|
||||
Start-Sleep -Milliseconds 400
|
||||
[void][Win]::SetCursorPos($x - 12, $y - 12)
|
||||
Start-Sleep -Milliseconds 300
|
||||
[void][Win]::SetCursorPos($x, $y)
|
||||
Start-Sleep -Milliseconds 400
|
||||
[Win]::mouse_event(0x0002, 0, 0, 0, [UIntPtr]::Zero)
|
||||
Start-Sleep -Milliseconds 120
|
||||
[Win]::mouse_event(0x0004, 0, 0, 0, [UIntPtr]::Zero)
|
||||
$run.clicked = $true
|
||||
$prog = Join-Path $dir 'data\words_to_learn.csv'
|
||||
$end = (Get-Date).AddSeconds(6)
|
||||
while ((Get-Date) -lt $end -and -not (Test-Path $prog)) { Start-Sleep -Milliseconds 250 }
|
||||
if (Test-Path $prog) {
|
||||
$rows = ([IO.File]::ReadAllLines($prog) | Where-Object { $_.Trim() -ne '' }).Count
|
||||
$run.progress_rows = $rows
|
||||
$run.ok = ($rows -eq 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (-not $p.HasExited) { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue }
|
||||
Get-Process -Name 'flash-card' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
$run.seconds = [math]::Round(((Get-Date) - $t0).TotalSeconds, 1)
|
||||
$results += [pscustomobject]$run
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
|
||||
$summary = [ordered]@{
|
||||
runs = $Runs
|
||||
passed = ($results | Where-Object { $_.ok }).Count
|
||||
os = (Get-CimInstance Win32_OperatingSystem).Caption
|
||||
results = $results
|
||||
}
|
||||
$summary | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $Out 'result.json') -Encoding UTF8
|
||||
shutdown.exe /s /t 5
|
||||
Reference in New Issue
Block a user