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.
232 lines
10 KiB
PowerShell
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"
|