Files
flora2roa/Teste/vfp_ui_harness.ps1
Marius Mutu 2736cbd525 Initial: flux text FoxBin2Prg (git urmareste .??2 in-arbore, binarele VFP git-ignored)
Include si sistemul de depanare/testare headless din Teste\ (harness UI cu capturi, verificare de sintaxa prin compilare, wrappere text<->binar) si docs\depanare_testare_flora2roa.md.
2026-08-02 08:56:52 +03:00

232 lines
10 KiB
PowerShell

# vfp_ui_harness.ps1 - harness reutilizabil pentru teste UI VFP cu capturi de ecran.
# Lanseaza un test VFP care afiseaza un formular real, il conduce pas cu pas prin semafoare
# (ready_<n>.txt scris de test <-> cont_<n>.txt scris de harness), face un screenshot dupa
# fiecare pas si il valideaza. Testul VFP foloseste procedurile din ui_harness.prg.
#
# Exemplu:
# powershell -ExecutionPolicy Bypass -File vfp_ui_harness.ps1 `
# -TestPrg 'D:\...\test_x_ui.prg' -Steps @('afisat','actiune1','actiune2')
#
# Capcane rezolvate (vezi docs\testare-ui-vfp.md): precompilare izolata, lansare .fxp,
# detectie pornire pe mtime log, timeout-uri generoase, kill zombi vfp9.
#
# FARA FURT DE FOCUS (cerinta Marius, 17/07/2026): capturile NU mai folosesc
# Graphics.CopyFromScreen (poza ecranului intreg - fura focus / se corupe daca utilizatorul
# lucreaza in paralel). Se foloseste PrintWindow pe handle-ul ferestrei vfp9 + fereastra e
# mutata OFF-SCREEN (x=-4000, HWND_BOTTOM, SWP_NOACTIVATE) imediat ce apare handle-ul si
# re-impinsa inainte de fiecare pas (VFP isi poate re-activa fereastra la Show()/dialoguri).
# Fereastra ramane "visible" pentru GDI (PrintWindow merge), doar nu e pe ecranul vizibil
# utilizatorului si nu ia focus.
param(
[Parameter(Mandatory=$true)][string]$TestPrg,
[Parameter(Mandatory=$true)][string[]]$Steps, # etichete pentru pasii 0..N-1
[int]$StepTimeoutSec = 130, # cat astept fiecare ready_<n> (dupa pas 0)
[int]$ReadyTimeoutSec = 180, # cat astept ready_0 (afisarea formularului, incarcare lenta)
[string]$ShotsDir,
[string]$SyncDir,
[string]$Vfp = 'C:\Program Files (x86)\Microsoft Visual FoxPro 9\vfp9.exe'
)
$ErrorActionPreference = 'Stop'
$TestDir = [System.IO.Path]::GetDirectoryName($TestPrg)
$TestFxp = [System.IO.Path]::ChangeExtension($TestPrg, 'fxp')
$TestLog = [System.IO.Path]::ChangeExtension($TestPrg, $null) + '_log.txt'
if (-not $SyncDir) { $SyncDir = Join-Path $TestDir 'uisync' }
if (-not $ShotsDir) { $ShotsDir = Join-Path $TestDir 'screenshots' }
$Precompile = Join-Path $PSScriptRoot '_precompile.ps1'
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class Win32Ui {
[StructLayout(LayoutKind.Sequential)]
public struct RECT { public int Left, Top, Right, Bottom; }
[DllImport("user32.dll")]
public static extern bool PrintWindow(IntPtr hwnd, IntPtr hdc, uint nFlags);
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")]
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")]
public static extern bool IsWindow(IntPtr hWnd);
public static readonly IntPtr HWND_BOTTOM = new IntPtr(1);
public const uint SWP_NOSIZE = 0x0001;
public const uint SWP_NOACTIVATE = 0x0010;
public const uint SWP_NOZORDER = 0x0004;
}
"@
function Clear-Dir([string]$Dir) {
if (Test-Path $Dir) {
Get-ChildItem -Path $Dir -File -ErrorAction SilentlyContinue | ForEach-Object {
try { [System.IO.File]::Delete($_.FullName) } catch {}
}
} else { New-Item -ItemType Directory -Path $Dir | Out-Null }
}
# Muta fereastra vfp9 off-screen (x=-4000), fara sa o activeze/minimizeze - ramane randabila
# pentru PrintWindow, dar nu se vede si nu fura focus. Se cheama repetat (VFP isi reactiveaza
# singur fereastra la Show()/dialoguri modale).
function Push-Offscreen([IntPtr]$Hwnd) {
if ($Hwnd -eq [IntPtr]::Zero) { return }
if (-not [Win32Ui]::IsWindow($Hwnd)) { return }
[Win32Ui]::SetWindowPos($Hwnd, [Win32Ui]::HWND_BOTTOM, -4000, 0, 0, 0, ([Win32Ui]::SWP_NOSIZE -bor [Win32Ui]::SWP_NOACTIVATE)) | Out-Null
}
# Polleaza MainWindowHandle pana devine nenul (fereastra principala vfp9 aparuta).
function Wait-MainWindowHandle([System.Diagnostics.Process]$Proc, [int]$TimeoutSec) {
$deadline = (Get-Date).AddSeconds($TimeoutSec)
while ((Get-Date) -lt $deadline) {
try {
$Proc.Refresh()
if ($Proc.HasExited) { return [IntPtr]::Zero }
if ($Proc.MainWindowHandle -ne [IntPtr]::Zero) { return $Proc.MainWindowHandle }
} catch {}
Start-Sleep -Milliseconds 200
}
return [IntPtr]::Zero
}
function Wait-File([string]$Path, [int]$TimeoutSec, [IntPtr]$Hwnd = [IntPtr]::Zero) {
$deadline = (Get-Date).AddSeconds($TimeoutSec)
while (-not (Test-Path $Path)) {
if ((Get-Date) -gt $deadline) { return $false }
if ($Hwnd -ne [IntPtr]::Zero) { Push-Offscreen $Hwnd }
Start-Sleep -Milliseconds 300
}
return $true
}
# Captura pe HANDLE cu PrintWindow (nu depinde de foreground/ecran vizibil). flag 2 =
# PW_RENDERFULLCONTENT (randare completa, necesar pt. multe ferestre GDI+/themed); daca da
# imagine goala, se incearca fallback cu flag 0.
function Take-Screenshot([IntPtr]$Hwnd, [string]$Path) {
if ($Hwnd -eq [IntPtr]::Zero -or -not [Win32Ui]::IsWindow($Hwnd)) {
Write-Warning "Take-Screenshot: handle invalid, sar captura ($Path)."
return
}
$rect = New-Object Win32Ui+RECT
[Win32Ui]::GetWindowRect($Hwnd, [ref]$rect) | Out-Null
$w = $rect.Right - $rect.Left
$h = $rect.Bottom - $rect.Top
if ($w -le 0 -or $h -le 0) { $w = 1920; $h = 1080 }
$bmp = New-Object System.Drawing.Bitmap $w, $h
$g = [System.Drawing.Graphics]::FromImage($bmp)
$hdc = $g.GetHdc()
$ok = [Win32Ui]::PrintWindow($Hwnd, $hdc, 2)
$g.ReleaseHdc($hdc)
$g.Dispose()
if (-not $ok) {
$g2 = [System.Drawing.Graphics]::FromImage($bmp)
$hdc2 = $g2.GetHdc()
[Win32Ui]::PrintWindow($Hwnd, $hdc2, 0) | Out-Null
$g2.ReleaseHdc($hdc2)
$g2.Dispose()
}
$bmp.Save($Path, [System.Drawing.Imaging.ImageFormat]::Png)
$bmp.Dispose()
}
# omoara doar vfp9 proprii (linia de comanda contine folderul testului), nu instante straine
function Stop-VfpProprii([string]$Marker) {
Get-CimInstance Win32_Process -Filter "Name='vfp9.exe'" -ErrorAction SilentlyContinue | ForEach-Object {
if ($_.CommandLine -and $_.CommandLine.ToLower().Contains($Marker.ToLower())) {
Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue
}
}
}
Clear-Dir $SyncDir
Clear-Dir $ShotsDir
# 1) Precompilare izolata (proces copil complet, ca sa nu otraveasca sesiunea de lansare).
# -WindowStyle Hidden pe consola powershell copil (sigur - e consola, nu GUI VFP).
Write-Host "Precompilez .fxp (izolat): $Precompile -Prg $TestPrg"
Stop-VfpProprii $TestDir
Start-Sleep -Milliseconds 500
$pc = Start-Process powershell -WindowStyle Hidden -ArgumentList @('-ExecutionPolicy','Bypass','-File', "`"$Precompile`"", '-Prg', "`"$TestPrg`"") -Wait -PassThru
Start-Sleep -Seconds 2
Stop-VfpProprii $TestDir
Start-Sleep -Seconds 2
if (-not (Test-Path $TestFxp)) { Write-Warning "Nu s-a generat $TestFxp - compilare esuata." }
# 2) Lansez .FXP-ul (nu .prg-ul). vfp9 -A porneste uneori procesul FARA sa execute (log gol);
# discriminator: START e prima linie a testului (mtime log > momentul lansarii) => in ~2s daca
# ruleaza. Relansez doar daca log-ul nu e scris in 30s (instanta moarta). Apoi astept ready_0.
# Imediat ce apare MainWindowHandle, fereastra e impinsa off-screen (fara activare).
$ready0 = Join-Path $SyncDir 'ready_0.txt'
$started = $false
$hwnd = [IntPtr]::Zero
foreach ($try in 1..8) {
if (Test-Path $ready0) { [System.IO.File]::Delete($ready0) }
if (Test-Path $TestLog) { [System.IO.File]::Delete($TestLog) }
$t0 = Get-Date
Write-Host "Lansez VFP (incercarea $try): $Vfp -A $TestFxp"
$proc = Start-Process -FilePath $Vfp -ArgumentList @('-A', "`"$TestFxp`"") -PassThru
$dl = (Get-Date).AddSeconds(30)
while ((Get-Date) -lt $dl) {
if ($hwnd -eq [IntPtr]::Zero -and -not $proc.HasExited) {
$proc.Refresh()
if ($proc.MainWindowHandle -ne [IntPtr]::Zero) {
$hwnd = $proc.MainWindowHandle
Push-Offscreen $hwnd
}
} else {
Push-Offscreen $hwnd
}
if ((Test-Path $TestLog) -and ((Get-Item $TestLog).LastWriteTime -gt $t0)) { $started = $true; break }
if (Test-Path $ready0) { $started = $true; break }
Start-Sleep -Milliseconds 500
}
if ($started) { break }
Write-Warning "Incercarea ${try}: testul nu a scris START in 30s (instanta moarta); relansez."
if ($proc -and -not $proc.HasExited) { try { $proc.Kill() } catch {} }
Stop-VfpProprii $TestDir
Start-Sleep -Milliseconds 1500
$hwnd = [IntPtr]::Zero
}
# Daca handle-ul principal inca nu a aparut (formular aditional creat mai tarziu), mai astept putin.
if ($hwnd -eq [IntPtr]::Zero) { $hwnd = Wait-MainWindowHandle $proc 15 }
Push-Offscreen $hwnd
if (-not (Wait-File $ready0 $ReadyTimeoutSec $hwnd)) {
Write-Warning "Testul nu a ajuns la ready_0 in ${ReadyTimeoutSec}s. Ultima linie log: '$(Get-Content $TestLog -Tail 1 -ErrorAction SilentlyContinue)'"
}
# 3) Parcurg pasii: astept ready_<n> (re-impingand fereastra off-screen periodic), screenshot
# pe handle (PrintWindow), scriu cont_<n>.
$ok = $true
for ($n = 0; $n -lt $Steps.Count; $n++) {
$label = $Steps[$n]
$ready = Join-Path $SyncDir ("ready_{0}.txt" -f $n)
$cont = Join-Path $SyncDir ("cont_{0}.txt" -f $n)
Write-Host "Astept pasul $n ($label)"
if (-not (Wait-File $ready $StepTimeoutSec $hwnd)) {
Write-Warning "TIMEOUT la pasul $n."
$ok = $false
break
}
Write-Host (" ready: {0}" -f (Get-Content $ready -Raw).Trim())
# daca fereastra principala s-a schimbat intre timp (formular nou), reincerc handle-ul
if ($hwnd -eq [IntPtr]::Zero -or -not [Win32Ui]::IsWindow($hwnd)) { $hwnd = Wait-MainWindowHandle $proc 5 }
Push-Offscreen $hwnd
Start-Sleep -Milliseconds 1200 # las formularul sa se deseneze complet
Push-Offscreen $hwnd
$png = Join-Path $ShotsDir ("step_{0}_{1}.png" -f $n, $label)
Take-Screenshot $hwnd $png
Write-Host " screenshot: $png"
Set-Content -Path $cont -Value 'go' -Encoding ascii
}
# 4) Astept done.txt si inchid.
if ($ok) {
$done = Join-Path $SyncDir 'done.txt'
if (Wait-File $done 30 $hwnd) { Write-Host "done: $((Get-Content $done -Raw).Trim())" }
}
Start-Sleep -Milliseconds 800
if ($proc -and -not $proc.HasExited) { try { $proc.Kill() } catch {} }
Stop-VfpProprii $TestDir
Write-Host "GATA. Screenshots in $ShotsDir"