diff options
| author | Stefan Boberg <[email protected]> | 2026-03-21 21:43:22 +0100 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-03-21 21:43:22 +0100 |
| commit | 14ca5b35d0fc477ba30f10b80f937b523fd7e930 (patch) | |
| tree | 8aab2acfec8be1af4bf0dffdb4badc3b64bf8385 /scripts/test_windows | |
| parent | fix null stats provider crash when build store is not configured (#875) (diff) | |
| download | zen-14ca5b35d0fc477ba30f10b80f937b523fd7e930.tar.xz zen-14ca5b35d0fc477ba30f10b80f937b523fd7e930.zip | |
Interprocess pipe support (for stdout/stderr capture) (#866)
- **RAII pipe handles for child process stdout/stderr capture**: `StdoutPipeHandles` is now a proper RAII type with automatic cleanup, move semantics, and partial close support. This makes it safe to use pipes for capturing child process output without risking handle/fd leaks.
- **Optional separate stderr pipe**: `CreateProcOptions` now accepts a `StderrPipe` field so callers can capture stdout and stderr independently. When null (default), stderr shares the stdout pipe as before.
- **LogStreamListener with pluggable handler**: The TCP log stream listener accepts connections from remote processes and delivers parsed log lines through a `LogStreamHandler` interface, set dynamically via `SetHandler()`. This allows any client to receive log messages without depending on a specific console implementation.
- **TcpLogStreamSink for zen::logging**: A logging sink that forwards log messages to a `LogStreamListener` over TCP, using the native `zen::logging::Sink` infrastructure with proper thread-safe synchronization.
- **Reliable child process exit codes on Linux**: `waitpid` result handling is fixed so `ProcessHandle::GetExitCode()` returns the real exit code. `ProcessHandle::Reset()` reaps zombies directly, replacing the global `IgnoreChildSignals()` which prevented exit code collection entirely. Also fixes a TOCTOU race in `ProcessHandle::Wait()` on Linux/Mac.
- **Pipe capture test suite**: Tests covering stdout/stderr capture via pipes (both shared and separate modes), RAII cleanup, move semantics, and exit code propagation using `zentest-appstub` as the child process.
- **Service command integration tests**: Shell-based integration tests for `zen service` covering the full lifecycle (install, status, start, stop, uninstall) on all three platforms — Linux (systemd), macOS (launchd), and Windows (SCM via PowerShell).
- **Test script reorganization**: Platform-specific test scripts moved from `scripts/test_scripts/` into `scripts/test_linux/`, `test_mac/`, and `test_windows/`.
Diffstat (limited to 'scripts/test_windows')
| -rw-r--r-- | scripts/test_windows/block-clone-test.ps1 | 145 | ||||
| -rw-r--r-- | scripts/test_windows/service-test.ps1 | 353 |
2 files changed, 498 insertions, 0 deletions
diff --git a/scripts/test_windows/block-clone-test.ps1 b/scripts/test_windows/block-clone-test.ps1 new file mode 100644 index 000000000..aa6ec3a39 --- /dev/null +++ b/scripts/test_windows/block-clone-test.ps1 @@ -0,0 +1,145 @@ +# Test block-clone functionality on a temporary ReFS VHD. +# +# Requires: +# - Administrator privileges +# - Windows Server, or Windows 10/11 Pro for Workstations (ReFS support) +# - Hyper-V PowerShell module (for New-VHD), or diskpart fallback +# +# Usage: +# # From an elevated PowerShell prompt: +# .\scripts\test_windows\block-clone-test.ps1 [-TestBinary <path>] +# +# If -TestBinary is not given, defaults to build\windows\x64\debug\zencore-test.exe +# relative to the repository root. + +param( + [string]$TestBinary = "" +) + +$ErrorActionPreference = "Stop" + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$RepoRoot = (Resolve-Path "$ScriptDir\..\..").Path + +if (-not $TestBinary) { + $TestBinary = Join-Path $RepoRoot "build\windows\x64\debug\zencore-test.exe" +} + +$ImageSizeMB = 2048 +$TestCases = "TryCloneFile,CopyFile.Clone,SupportsBlockRefCounting,CloneQueryInterface" + +$VhdPath = "" +$MountLetter = "" + +function Cleanup { + $ErrorActionPreference = "SilentlyContinue" + + if ($MountLetter) { + Write-Host "Dismounting VHD ..." + Dismount-VHD -Path $VhdPath -ErrorAction SilentlyContinue + } + if ($VhdPath -and (Test-Path $VhdPath)) { + Remove-Item -Force $VhdPath -ErrorAction SilentlyContinue + } +} + +trap { + Cleanup + throw $_ +} + +# --- Preflight checks --- + +$IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator) +if (-not $IsAdmin) { + Write-Error "This script must be run as Administrator (for VHD mount/format)." + exit 1 +} + +if (-not (Test-Path $TestBinary)) { + Write-Error "Test binary not found: $TestBinary`nHint: build with 'xmake config -m debug && xmake build zencore-test'" + exit 1 +} + +# Check that ReFS formatting is available +$RefsAvailable = $true +try { + # A quick check: on non-Server/Workstation SKUs, Format-Volume -FileSystem ReFS will fail + $OsCaption = (Get-CimInstance Win32_OperatingSystem).Caption + if ($OsCaption -notmatch "Server|Workstation|Enterprise") { + Write-Warning "ReFS may not be available on this Windows edition: $OsCaption" + Write-Warning "Continuing anyway — format step will fail if unsupported." + } +} catch { + # Non-fatal, just proceed +} + +# --- Create and mount ReFS VHD --- + +$VhdPath = Join-Path $env:TEMP "refs-clone-test-$([guid]::NewGuid().ToString('N').Substring(0,8)).vhdx" + +Write-Host "Creating ${ImageSizeMB}MB VHDX at $VhdPath ..." + +try { + # Prefer Hyper-V cmdlet if available + New-VHD -Path $VhdPath -SizeBytes ($ImageSizeMB * 1MB) -Fixed | Out-Null +} catch { + # Fallback to diskpart + Write-Host "New-VHD not available, falling back to diskpart ..." + $DiskpartScript = @" +create vdisk file="$VhdPath" maximum=$ImageSizeMB type=fixed +"@ + $DiskpartScript | diskpart | Out-Null +} + +Write-Host "Mounting and initializing VHD ..." + +Mount-VHD -Path $VhdPath +$Disk = Get-VHD -Path $VhdPath | Get-Disk + +# Suppress Explorer's auto-open / "format disk?" prompts for the raw partition +Stop-Service ShellHWDetection -ErrorAction SilentlyContinue + +try { + Initialize-Disk -Number $Disk.Number -PartitionStyle GPT -ErrorAction SilentlyContinue + $Partition = New-Partition -DiskNumber $Disk.Number -UseMaximumSize -AssignDriveLetter + $MountLetter = $Partition.DriveLetter + + Write-Host "Formatting ${MountLetter}: as ReFS with integrity disabled ..." + Format-Volume -DriveLetter $MountLetter -FileSystem ReFS -NewFileSystemLabel "CloneTest" -Confirm:$false | Out-Null + + # Disable integrity streams (required for block cloning to work on ReFS) + Set-FileIntegrity "${MountLetter}:\" -Enable $false -ErrorAction SilentlyContinue +} finally { + Start-Service ShellHWDetection -ErrorAction SilentlyContinue +} + +$MountRoot = "${MountLetter}:\" + +# --- Copy test binary and run --- + +Write-Host "Copying test binary to ReFS volume ..." +Copy-Item $TestBinary "$MountRoot\zencore-test.exe" + +Write-Host "Running block-clone tests ..." +Write-Host "---" + +$proc = Start-Process -FilePath "$MountRoot\zencore-test.exe" ` + -ArgumentList "--test-suite=core.filesystem", "--test-case=$TestCases" ` + -NoNewWindow -Wait -PassThru + +Write-Host "---" + +if ($proc.ExitCode -ne 0) { + Write-Error "Tests failed with exit code $($proc.ExitCode)" + Cleanup + exit $proc.ExitCode +} + +Write-Host "ReFS: all block-clone tests passed." + +# --- Cleanup --- + +Cleanup +Write-Host "Done." diff --git a/scripts/test_windows/service-test.ps1 b/scripts/test_windows/service-test.ps1 new file mode 100644 index 000000000..4c484c63f --- /dev/null +++ b/scripts/test_windows/service-test.ps1 @@ -0,0 +1,353 @@ +# Test zen service command lifecycle on Windows (SCM). +# +# Requires: Administrator privileges +# +# Usage: +# # From an elevated PowerShell prompt: +# .\scripts\test_windows\service-test.ps1 [-ZenBinary <path>] +# +# If -ZenBinary is not given, defaults to build\windows\x64\debug\zen.exe +# relative to the repository root. + +param( + [string]$ZenBinary +) + +$ErrorActionPreference = "Stop" + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$RepoRoot = (Resolve-Path "$ScriptDir\..\..").Path + +if (-not $ZenBinary) { + $ZenBinary = Join-Path $RepoRoot "build\windows\x64\debug\zen.exe" +} +$ZenServerBinary = Join-Path (Split-Path -Parent $ZenBinary) "zenserver.exe" +$ServiceName = "ZenServerTest-$PID" + +$Script:Passed = 0 +$Script:Failed = 0 +$Script:TestsRun = 0 + +function Pass($Message) { + $Script:TestsRun++ + $Script:Passed++ + Write-Host " PASS: $Message" -ForegroundColor Green +} + +function Fail($Message, $Detail) { + $Script:TestsRun++ + $Script:Failed++ + Write-Host " FAIL: $Message" -ForegroundColor Red + if ($Detail) { + Write-Host " $Detail" + } +} + +function Cleanup { + Write-Host "" + Write-Host "--- Cleanup ---" + + # Stop the service if running + $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + if ($svc -and $svc.Status -eq "Running") { + Write-Host "Stopping test service..." + Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 + } + + # Delete the service if it exists + if (Get-Service -Name $ServiceName -ErrorAction SilentlyContinue) { + Write-Host "Removing test service..." + sc.exe delete $ServiceName 2>$null | Out-Null + } + + Write-Host "" + Write-Host "==============================" + Write-Host " Tests run: $Script:TestsRun" + Write-Host " Passed: $Script:Passed" -ForegroundColor Green + if ($Script:Failed -gt 0) { + Write-Host " Failed: $Script:Failed" -ForegroundColor Red + } else { + Write-Host " Failed: $Script:Failed" + } + Write-Host "==============================" +} + +# ── Preflight checks ────────────────────────────────────────────── + +$IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator) + +if (-not $IsAdmin) { + Write-Host "Error: this test must be run from an elevated (Administrator) prompt." -ForegroundColor Red + exit 1 +} + +if (-not (Test-Path $ZenBinary)) { + Write-Host "Error: zen binary not found at '$ZenBinary'" -ForegroundColor Red + Write-Host "Build with: xmake config -m debug && xmake build zen" + exit 1 +} + +if (-not (Test-Path $ZenServerBinary)) { + Write-Host "Error: zenserver binary not found at '$ZenServerBinary'" -ForegroundColor Red + Write-Host "Build with: xmake config -m debug && xmake build zenserver" + exit 1 +} + +Write-Host "zen binary: $ZenBinary" +Write-Host "zenserver binary: $ZenServerBinary" +Write-Host "service name: $ServiceName" +Write-Host "" + +try { + +# ── Test: status before install (should fail) ───────────────────── + +Write-Host "--- Test: status before install ---" + +$Output = & $ZenBinary service status $ServiceName 2>&1 | Out-String +if ($Output -match "not installed") { + Pass "status reports 'not installed' for non-existent service" +} else { + Fail "status should report 'not installed'" "got: $Output" +} + +# ── Test: install ───────────────────────────────────────────────── + +Write-Host "--- Test: install ---" + +$Output = & $ZenBinary service install $ZenServerBinary $ServiceName --allow-elevation 2>&1 | Out-String +$ExitCode = $LASTEXITCODE + +if ($ExitCode -eq 0) { + Pass "install exits with code 0" +} else { + Fail "install exits with code 0" "got exit code: $ExitCode, output: $Output" +} + +$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue +if ($svc) { + Pass "service registered in SCM" +} else { + Fail "service registered in SCM" +} + +# Verify service configuration +if ($svc) { + $svcWmi = Get-CimInstance -ClassName Win32_Service -Filter "Name='$ServiceName'" -ErrorAction SilentlyContinue + if ($svcWmi -and $svcWmi.PathName -match "zenserver") { + Pass "service binary path contains zenserver" + } else { + Fail "service binary path contains zenserver" "got: $($svcWmi.PathName)" + } + + if ($svcWmi -and $svcWmi.StartMode -eq "Auto") { + Pass "service is set to auto-start" + } else { + Fail "service is set to auto-start" "got: $($svcWmi.StartMode)" + } +} + +# ── Test: install again (already installed, no --full) ──────────── + +Write-Host "--- Test: install again (idempotent) ---" + +$Output = & $ZenBinary service install $ZenServerBinary $ServiceName --allow-elevation 2>&1 | Out-String +$ExitCode = $LASTEXITCODE + +if ($ExitCode -eq 0) { + Pass "re-install exits with code 0" +} else { + Fail "re-install exits with code 0" "got exit code: $ExitCode" +} + +if ($Output -match "already installed") { + Pass "re-install reports service already installed" +} else { + Fail "re-install reports service already installed" "got: $Output" +} + +# ── Test: status after install (not yet started) ────────────────── + +Write-Host "--- Test: status after install (stopped) ---" + +$Output = & $ZenBinary service status $ServiceName 2>&1 | Out-String +if ($Output -match "not running") { + Pass "status reports 'not running' for stopped service" +} else { + Fail "status reports 'not running' for stopped service" "got: $Output" +} + +# ── Test: start ─────────────────────────────────────────────────── + +Write-Host "--- Test: start ---" + +$Output = & $ZenBinary service start $ServiceName --allow-elevation 2>&1 | Out-String +$ExitCode = $LASTEXITCODE + +if ($ExitCode -eq 0) { + Pass "start exits with code 0" +} else { + Fail "start exits with code 0" "got exit code: $ExitCode, output: $Output" +} + +Start-Sleep -Seconds 2 + +$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue +if ($svc -and $svc.Status -eq "Running") { + Pass "service is running after start" + + # ── Test: status while running ──────────────────────────────── + + Write-Host "--- Test: status (running) ---" + + $Output = & $ZenBinary service status $ServiceName 2>&1 | Out-String + if ($Output -match "Running") { + Pass "status reports 'Running'" + } else { + Fail "status reports 'Running'" "got: $Output" + } + + if ($Output -match "zenserver") { + Pass "status shows executable path" + } else { + Fail "status shows executable path" "got: $Output" + } + + # ── Test: start again (already running) ─────────────────────── + + Write-Host "--- Test: start again (already running) ---" + + $Output = & $ZenBinary service start $ServiceName --allow-elevation 2>&1 | Out-String + if ($Output -match "already running") { + Pass "start reports service already running" + } else { + Fail "start reports service already running" "got: $Output" + } + + # ── Test: stop ──────────────────────────────────────────────── + + Write-Host "--- Test: stop ---" + + $Output = & $ZenBinary service stop $ServiceName --allow-elevation 2>&1 | Out-String + $ExitCode = $LASTEXITCODE + + if ($ExitCode -eq 0) { + Pass "stop exits with code 0" + } else { + Fail "stop exits with code 0" "got exit code: $ExitCode, output: $Output" + } + + Start-Sleep -Seconds 2 + + $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue + if ($svc -and $svc.Status -ne "Running") { + Pass "service is not running after stop" + } else { + Fail "service is not running after stop" + } +} else { + Fail "service is running after start" "(skipping start-dependent tests)" +} + +# ── Test: stop when already stopped ─────────────────────────────── + +Write-Host "--- Test: stop when already stopped ---" + +Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue +Start-Sleep -Seconds 1 + +$Output = & $ZenBinary service stop $ServiceName --allow-elevation 2>&1 | Out-String +if ($Output -match "not running") { + Pass "stop reports 'not running' when already stopped" +} else { + Fail "stop reports 'not running' when already stopped" "got: $Output" +} + +# ── Test: uninstall while running (should fail) ─────────────────── + +Write-Host "--- Test: uninstall while running (should fail) ---" + +& $ZenBinary service start $ServiceName --allow-elevation 2>&1 | Out-Null +Start-Sleep -Seconds 2 + +$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue +if ($svc -and $svc.Status -eq "Running") { + $Output = & $ZenBinary service uninstall $ServiceName --allow-elevation 2>&1 | Out-String + $ExitCode = $LASTEXITCODE + + if ($ExitCode -ne 0 -or $Output -match "running.*stop") { + Pass "uninstall refuses while service is running" + } else { + Fail "uninstall refuses while service is running" "got: exit=$ExitCode, output: $Output" + } + + # Stop it for the real uninstall test + & $ZenBinary service stop $ServiceName --allow-elevation 2>&1 | Out-Null + Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 2 +} else { + Write-Host " (skipped: could not start service for this test)" +} + +# ── Test: uninstall ─────────────────────────────────────────────── + +Write-Host "--- Test: uninstall ---" + +Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue +Start-Sleep -Seconds 1 + +$Output = & $ZenBinary service uninstall $ServiceName --allow-elevation 2>&1 | Out-String +$ExitCode = $LASTEXITCODE + +if ($ExitCode -eq 0) { + Pass "uninstall exits with code 0" +} else { + Fail "uninstall exits with code 0" "got exit code: $ExitCode, output: $Output" +} + +$svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue +if (-not $svc) { + Pass "service removed from SCM after uninstall" +} else { + Fail "service removed from SCM after uninstall" +} + +# ── Test: status after uninstall ────────────────────────────────── + +Write-Host "--- Test: status after uninstall ---" + +$Output = & $ZenBinary service status $ServiceName 2>&1 | Out-String +if ($Output -match "not installed") { + Pass "status reports 'not installed' after uninstall" +} else { + Fail "status reports 'not installed' after uninstall" "got: $Output" +} + +# ── Test: uninstall when not installed (idempotent) ─────────────── + +Write-Host "--- Test: uninstall when not installed ---" + +$Output = & $ZenBinary service uninstall $ServiceName --allow-elevation 2>&1 | Out-String +$ExitCode = $LASTEXITCODE + +if ($ExitCode -eq 0) { + Pass "uninstall of non-existent service exits with code 0" +} else { + Fail "uninstall of non-existent service exits with code 0" "got exit code: $ExitCode" +} + +if ($Output -match "not installed") { + Pass "uninstall reports service not installed" +} else { + Fail "uninstall reports service not installed" "got: $Output" +} + +} finally { + Cleanup +} + +if ($Script:Failed -gt 0) { + exit 1 +} |