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.
This commit is contained in:
2026-08-02 08:56:52 +03:00
commit 2736cbd525
49 changed files with 16011 additions and 0 deletions

66
Teste/_precompile.ps1 Normal file
View File

@@ -0,0 +1,66 @@
# _precompile.ps1 -Prg <cale.prg> - regenereaza .fxp-ul target intr-un proces IZOLAT.
# De ce izolat: precompilarea (vfp9 -A ... COMPILE) atarna dupa COMPILE si e omorata; daca
# ruleaza in ACEEASI sesiune powershell care lanseaza apoi testul, lansarile ulterioare de
# vfp9 esueaza (deschid editorul in loc sa ruleze). Rulat ca proces separat (Start-Process
# -Wait din harness), nu otraveste sesiunea care lanseaza testul.
param([Parameter(Mandatory=$true)][string]$Prg)
$Vfp = 'C:\Program Files (x86)\Microsoft Visual FoxPro 9\vfp9.exe'
$Fxp = [System.IO.Path]::ChangeExtension($Prg, 'fxp')
$dir = [System.IO.Path]::GetDirectoryName($Prg)
$tmpPrg = Join-Path $dir '_compile_tmp.prg'
# prg temporar care compileaza target-ul (COMPILE cu variabila; forma cu literal in ghilimele da eroare)
Set-Content -Path $tmpPrg -Value ("lcT = `"$Prg`"`r`nCOMPILE (lcT)`r`nQUIT`r`n") -Encoding ascii
# 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
}
}
}
Stop-VfpProprii $dir
Start-Sleep -Milliseconds 500
if (Test-Path $Fxp) { [System.IO.File]::Delete($Fxp) }
# Fara furt de focus: fereastra vfp9 a compilarii (chiar tranzitorie) e impinsa off-screen,
# la fel ca in vfp_ui_harness.ps1 (SetWindowPos HWND_BOTTOM + NOACTIVATE, x=-4000).
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class Win32UiPc {
[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;
}
"@
function Push-OffscreenPc([IntPtr]$Hwnd) {
if ($Hwnd -eq [IntPtr]::Zero -or -not [Win32UiPc]::IsWindow($Hwnd)) { return }
[Win32UiPc]::SetWindowPos($Hwnd, [Win32UiPc]::HWND_BOTTOM, -4000, 0, 0, 0, ([Win32UiPc]::SWP_NOSIZE -bor [Win32UiPc]::SWP_NOACTIVATE)) | Out-Null
}
$ok = $false
foreach ($try in 1..3) {
$pcProc = Start-Process -FilePath $Vfp -ArgumentList @('-A', "`"$tmpPrg`"") -PassThru
$dl = (Get-Date).AddSeconds(30)
$pcHwnd = [IntPtr]::Zero
while (-not (Test-Path $Fxp) -and (Get-Date) -lt $dl) {
if ($pcHwnd -eq [IntPtr]::Zero -and -not $pcProc.HasExited) {
$pcProc.Refresh()
if ($pcProc.MainWindowHandle -ne [IntPtr]::Zero) { $pcHwnd = $pcProc.MainWindowHandle }
}
Push-OffscreenPc $pcHwnd
Start-Sleep -Milliseconds 300
}
Start-Sleep -Seconds 3
Stop-VfpProprii $dir
Start-Sleep -Seconds 1
if (Test-Path $Fxp) { $ok = $true; break }
}
foreach ($f in @($tmpPrg, [System.IO.Path]::ChangeExtension($tmpPrg, 'fxp'))) {
if (Test-Path $f) { try { [System.IO.File]::Delete($f) } catch {} }
}
if ($ok) { exit 0 } else { exit 1 }

49
Teste/bin_din_text.ps1 Normal file
View File

@@ -0,0 +1,49 @@
<#
.SYNOPSIS
Scrie inapoi in proiect binarele VFP (.scx+.sct / .vcx+.vct) regenerate din textul editat.
.DESCRIPTION
Wrapper peste D:\ROA\UTIL\foxbin2prg\txt2vcx.ps1 cu parametrii FLORA2ROA. Flux in-arbore:
textul .sc2/.vc2 sta LANGA binar, deci -CacheRoot = radacina proiectului.
Regenereaza in staging, face fidelity check (bin -> text, comparat pe octeti) si abia apoi
copiaza peste binarul din proiect. Refuza daca binarul e blocat (IDE sau flora2roa.exe
deschis) sau daca e mai nou decat textul (fara -Force).
Editarea textului se face byte-safe, in Windows-1252:
$t = [IO.File]::ReadAllText($p, [Text.Encoding]::GetEncoding(1252))
[IO.File]::WriteAllText($p, $t, [Text.Encoding]::GetEncoding(1252))
NU cu tool-uri care scriu UTF-8: corup diacriticele din TOT fisierul.
Vezi COMUN\docs\flux-editare-vfp-text.md si conventie_encoding_cp1252.md.
Dupa write-back binarul e modificat in SVN: se comite in SVN doar cu acordul utilizatorului.
.PARAMETER TextFile
Unul sau mai multe fisiere .sc2/.vc2 din arbore (ex. frm_import.sc2).
.EXAMPLE
powershell -ExecutionPolicy Bypass -File Teste\bin_din_text.ps1 -TextFile frm_import.sc2 -DryRun
.EXAMPLE
powershell -ExecutionPolicy Bypass -File Teste\bin_din_text.ps1 -TextFile frm_import.sc2
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string[]]$TextFile,
[string]$ProjectRoot,
[switch]$Force,
[switch]$NoVerify,
[switch]$DryRun
)
$ErrorActionPreference = 'Stop'
if (-not $ProjectRoot) { $ProjectRoot = Split-Path $PSScriptRoot -Parent }
$tool = 'D:\ROA\UTIL\foxbin2prg\txt2vcx.ps1'
if (-not (Test-Path $tool)) { throw "Nu gasesc $tool (tooling-ul partajat FoxBin2Prg)." }
$TextFile = $TextFile | ForEach-Object { if ([IO.Path]::IsPathRooted($_)) { $_ } else { Join-Path $ProjectRoot $_ } }
# flux in-arbore: CacheRoot = ProjectRoot (maparea text->binar devine identitate)
& $tool -TextFile $TextFile -ProjectRoot $ProjectRoot -CacheRoot $ProjectRoot `
-Force:$Force -NoVerify:$NoVerify -DryRun:$DryRun
exit $LASTEXITCODE

View File

@@ -0,0 +1,21 @@
* mock_amessagebox.prg - inlocuieste FUNCTION amessagebox (myutils.prg) in testele headless,
* ca dialogurile modale sa nu blocheze procesul. Returneaza 6 (Da/OK) implicit.
* Se incarca cu SET PROCEDURE ... ADDITIVE dupa initializarea mediului: la nume duplicat de
* procedura VFP alege fisierul deschis cel mai recent.
* gnMockRaspuns (PUBLIC, optional): valoarea returnata (implicit 6).
* gcMockUltimMesaj / gnMockUltimTip (PUBLIC, optional): daca testul declara gcMockUltimMesaj ca
* sir, mock-ul memoreaza mesajul si tipul ultimului apel, pentru aserturi pe textul dialogului.
FUNCTION amessagebox
LPARAMETERS tcMessage, tnDialogBoxType, tcTitle, tcFont, tnTimeOut, tnTimeoutValue
LOCAL lnRaspuns
lnRaspuns = IIF(TYPE('gnMockRaspuns') = 'N', gnMockRaspuns, 6)
IF TYPE('gcMockUltimMesaj') = 'C'
gcMockUltimMesaj = TRANSFORM(tcMessage)
gnMockUltimTip = IIF(TYPE('tnDialogBoxType') = 'N', tnDialogBoxType, 0)
ENDIF
TestLog('MOCK amessagebox: [' + ALLTRIM(TRANSFORM(tcMessage)) + '] -> returnez ' + TRANSFORM(lnRaspuns))
RETURN lnRaspuns
ENDFUNC
* MESSAGEBOX nativ nu se poate umbri; formularul apeleaza si MESSAGEBOX() direct in cateva
* locuri. Testele care ating acele ramuri trebuie sa evite conditia care le declanseaza.

74
Teste/ruleaza_teste.ps1 Normal file
View File

@@ -0,0 +1,74 @@
<#
.SYNOPSIS
Ruleaza suita de teste FLORA2ROA (verificare de sintaxa + teste UI cu capturi).
.DESCRIPTION
Ordinea e cea din COMUN\docs\depanare_testare_vfp.md: intai compilarea de verificare (ieftina,
prinde sintaxa), apoi testele functionale pe fluxul real al utilizatorului.
Suitele NU se ruleaza in paralel: harness-ul isi omoara instantele vfp9 proprii inainte de
fiecare lansare (filtrat pe linia de comanda, instantele straine nu sunt atinse).
.PARAMETER Doar
Ruleaza numai testele al caror nume contine acest sir.
.PARAMETER FaraSintaxa
Sare peste verificarea de sintaxa.
.EXAMPLE
powershell -ExecutionPolicy Bypass -File Teste\ruleaza_teste.ps1
.EXAMPLE
powershell -ExecutionPolicy Bypass -File Teste\ruleaza_teste.ps1 -Doar export
#>
[CmdletBinding()]
param(
[string]$Doar,
[switch]$FaraSintaxa
)
$ErrorActionPreference = 'Stop'
$Teste = $PSScriptRoot
$Root = Split-Path $Teste -Parent
# nume test -> etichetele pasilor asteptate de vfp_ui_harness.ps1 (o captura per pas)
$suita = @(
@{ Prg = 'test_frm_import_ui.prg'; Steps = @('afisat', 'trace', 'date') }
@{ Prg = 'test_export_date.prg'; Steps = @('afisat', 'importat') }
)
if ($Doar) { $suita = $suita | Where-Object { $_.Prg -like "*$Doar*" } }
$rezultate = @()
if (-not $FaraSintaxa) {
Write-Host '=== Verificare de sintaxa (compilare headless) ===' -ForegroundColor Cyan
& (Join-Path $Teste 'verifica_sintaxa.ps1')
$rezultate += [pscustomobject]@{ Test = 'verifica_sintaxa'; Rezultat = $(if ($LASTEXITCODE -eq 0) { 'PASS' } else { 'FAIL' }) }
}
foreach ($t in $suita) {
$prg = Join-Path $Teste $t.Prg
if (-not (Test-Path $prg)) { Write-Warning "lipsa: $prg"; continue }
Write-Host ''
Write-Host ("=== {0} ===" -f $t.Prg) -ForegroundColor Cyan
# capturile fiecarui test in folderul lui, ca sa nu se suprascrie intre teste
$shots = Join-Path (Join-Path $Teste 'screenshots') ([IO.Path]::GetFileNameWithoutExtension($t.Prg))
& (Join-Path $Teste 'vfp_ui_harness.ps1') -TestPrg $prg -Steps $t.Steps -ShotsDir $shots -StepTimeoutSec 300
# verdictul il da testul: ultima linie "REZULTAT:" din log
$log = Join-Path $Teste ([IO.Path]::GetFileNameWithoutExtension($t.Prg) + '_log.txt')
$verdict = 'FARA LOG'
if (Test-Path $log) {
$linie = Select-String -Path $log -Pattern '^REZULTAT:' | Select-Object -Last 1
if ($linie) { $verdict = $linie.Line -replace '^REZULTAT:\s*', '' }
$failuri = @(Select-String -Path $log -Pattern '^FAIL:')
foreach ($f in $failuri) { Write-Warning $f.Line }
}
$rezultate += [pscustomobject]@{ Test = $t.Prg; Rezultat = $verdict }
}
Write-Host ''
Write-Host '=== SUMAR ===' -ForegroundColor Cyan
$rezultate | Format-Table -AutoSize
$nrFail = @($rezultate | Where-Object { $_.Rezultat -notlike 'PASS*' }).Count
exit ([int]($nrFail -gt 0))

View File

@@ -0,0 +1,63 @@
* test_conectare_roa.prg - verifica drumul de initializare CU conectare la Oracle.
* Ruleaza cu datele din settings.ini; daca baza nu e accesibila, testul trebuie sa raporteze
* esecul si sa iasa - NU sa ramana agatat pe un dialog de login (SQLSetProp DispLogin).
*
* vfp9.exe -A -T "Teste\test_conectare_roa.prg"
* Nu face parte din suita implicita: depinde de reteaua/serverul Oracle.
SET SAFETY OFF
SET TALK OFF
PUBLIC gcUILog, gcSyncDir, gnTestFail
gnTestFail = 0
LOCAL lcTestDir, llConectat
lcTestDir = ADDBS(JUSTPATH(SYS(16, 0)))
gcUILog = lcTestDir + 'test_conectare_roa_log.txt'
gcSyncDir = lcTestDir + 'uisync\'
IF !DIRECTORY(gcSyncDir)
MD (gcSyncDir)
ENDIF
STRTOFILE('START ' + TTOC(DATETIME()) + CHR(13) + CHR(10), gcUILog)
* fara dialog de login: headless, un prompt ODBC ar bloca procesul la nesfarsit
SQLSETPROP(0, 'DispLogin', 3)
llConectat = DO_CONECTARE(lcTestDir)
HarnessLog('gnHandle = ' + TRANSFORM(gnHandle))
TestAssert(TYPE('goConn') = 'O', 'goConn instantiat chiar si fara conectare')
TestAssert(TYPE('gnHandle') = 'N', 'gnHandle este numeric')
IF llConectat
TestAssert(gnHandle > 0, 'conectare reusita la ' + ALLTRIM(goApp.oSettings.host))
LOCAL lnSucces
lnSucces = goExecutor.oExecute(GetHash('cSql=>select 1 as unu from dual??cCursor=>cPing'))
TestAssertEqual(1, lnSucces, 'interogare de test executata')
IF USED('cPing')
TestAssertEqual(1, cPing.unu, 'baza a raspuns corect')
USE IN (SELECT('cPing'))
ENDIF
goConn.Disconnect()
TestAssert(goConn.GetHandle() <= 0, 'deconectare reusita')
ELSE
HarnessLog('Conectare esuata (host ' + ALLTRIM(goApp.oSettings.host) + ;
'). Testul a iesit controlat, fara sa ramana agatat.')
TestAssert(gnHandle <= 0, 'esecul de conectare e raportat, nu ascuns')
ENDIF
HarnessLog('REZULTAT: ' + TestRezultat())
DO HarnessDone WITH TestRezultat()
QUIT
FUNCTION DO_CONECTARE
LPARAMETERS tcTestDir
LOCAL llOk
llOk = .F.
DO (tcTestDir + 'test_init_env_auto.prg') WITH .T.
SET PROCEDURE TO (tcTestDir + 'ui_harness.prg') ADDITIVE
SET PROCEDURE TO (tcTestDir + 'mock_amessagebox.prg') ADDITIVE
DO HarnessInit
llOk = (TYPE('gnHandle') = 'N' AND gnHandle > 0)
RETURN llOk
ENDFUNC

111
Teste/test_export_date.prg Normal file
View File

@@ -0,0 +1,111 @@
* test_export_date.prg - test functional pe fluxul REAL de citire a fisierelor de import.
* Instantiaza frm_import, seteaza intervalul din controale si apeleaza ExportDate() (pasul pe
* care il declanseaza butonul "Import Fidelio&Biz > ROA"), apoi verifica cursoarele rezultate.
* Nu are nevoie de Oracle: ExportDate citeste doar XML-urile din folderele din settings.ini.
*
* Parametri optionali: data de inceput si data de sfarsit ca AAAALLZZ.
* vfp9.exe -A -T "Teste\test_export_date.prg" 20250601 20250630
* Rulare cu capturi:
* powershell -ExecutionPolicy Bypass -File Teste\vfp_ui_harness.ps1 `
* -TestPrg 'D:\PROIECTE\FLORA\FLORA2ROA\Teste\test_export_date.prg' -Steps @('afisat','importat')
LPARAMETERS tcData1, tcData2
SET SAFETY OFF
SET TALK OFF
PUBLIC gcUILog, gcSyncDir, gnTestFail, gnMockRaspuns, gcMockUltimMesaj, gnMockUltimTip
gnTestFail = 0
gnMockRaspuns = 6
gcMockUltimMesaj = ''
gnMockUltimTip = 0
LOCAL lcTestDir, ldData1, ldData2
lcTestDir = ADDBS(JUSTPATH(SYS(16, 0)))
gcUILog = lcTestDir + 'test_export_date_log.txt'
gcSyncDir = lcTestDir + 'uisync\'
IF !DIRECTORY(gcSyncDir)
MD (gcSyncDir)
ENDIF
STRTOFILE('START ' + TTOC(DATETIME()) + CHR(13) + CHR(10), gcUILog)
ldData1 = IIF(TYPE('tcData1') = 'C' AND LEN(ALLTRIM(TRANSFORM(tcData1))) = 8, ;
DATE(VAL(LEFT(tcData1, 4)), VAL(SUBSTR(tcData1, 5, 2)), VAL(RIGHT(tcData1, 2))), DATE(2025, 6, 1))
ldData2 = IIF(TYPE('tcData2') = 'C' AND LEN(ALLTRIM(TRANSFORM(tcData2))) = 8, ;
DATE(VAL(LEFT(tcData2, 4)), VAL(SUBSTR(tcData2, 5, 2)), VAL(RIGHT(tcData2, 2))), DATE(2025, 6, 30))
DO (lcTestDir + 'test_init_env_auto.prg') WITH .F.
SET PROCEDURE TO (lcTestDir + 'ui_harness.prg') ADDITIVE
SET PROCEDURE TO (lcTestDir + 'mock_amessagebox.prg') ADDITIVE
DO HarnessInit
HarnessLog('Interval cerut: ' + DTOC(ldData1) + ' - ' + DTOC(ldData2))
HarnessLog('cale_import_fidelio = ' + goApp.oSettings.cale_import_fidelio)
HarnessLog('cale_import_biz = ' + goApp.oSettings.cale_import_biz)
HarnessLog('cale_import = ' + goApp.oSettings.cale_import)
TestAssert(DIRECTORY(ADDBS(ALLTRIM(goApp.oSettings.cale_import_fidelio))), 'exista directorul Fidelio')
TestAssert(DIRECTORY(ADDBS(ALLTRIM(goApp.oSettings.cale_import_biz))), 'exista directorul Biz')
TestAssert(DIRECTORY(ADDBS(ALLTRIM(goApp.oSettings.cale_import))), 'exista directorul de import')
PUBLIC goImportFrm
DO FORM (gcAppPath + 'frm_import') NAME goImportFrm NOSHOW
goImportFrm.WindowType = 0
goImportFrm.Show()
DOEVENTS FORCE
DO HarnessStep WITH 0, 'formular afisat'
* fluxul real: utilizatorul completeaza intervalul in controale, apoi apasa butonul de import
goImportFrm.txtData1.Value = ldData1
goImportFrm.txtData2.Value = ldData2
goImportFrm.chkTest.Value = 1
LOCAL lnSucces
lnSucces = goImportFrm.ExportDate()
DOEVENTS FORCE
HarnessLog('ExportDate a returnat ' + TRANSFORM(lnSucces))
TestAssertEqual(1, lnSucces, 'ExportDate returneaza CT_SUCCES')
LOCAL lnFidelio, lnIntrari, lnIesiri, lnVanzari, lnPart
lnFidelio = IIF(USED('cFacturiFidelio'), RECCOUNT('cFacturiFidelio'), -1)
lnIntrari = IIF(USED('cIntrariRestaurant'), RECCOUNT('cIntrariRestaurant'), -1)
lnIesiri = IIF(USED('cIesiriRestaurant'), RECCOUNT('cIesiriRestaurant'), -1)
lnVanzari = IIF(USED('cVanzariRestaurant'), RECCOUNT('cVanzariRestaurant'), -1)
lnPart = IIF(USED('nPart'), RECCOUNT('nPart'), -1)
HarnessLog('Randuri citite: facturi Fidelio=' + TRANSFORM(lnFidelio) + ' intrari=' + TRANSFORM(lnIntrari) + ;
' iesiri=' + TRANSFORM(lnIesiri) + ' vanzari=' + TRANSFORM(lnVanzari) + ' parteneri=' + TRANSFORM(lnPart))
TestAssert(lnFidelio >= 0, 'cursorul cFacturiFidelio exista')
TestAssert(lnIntrari >= 0, 'cursorul cIntrariRestaurant exista')
TestAssert(lnIesiri >= 0, 'cursorul cIesiriRestaurant exista')
TestAssert(lnVanzari >= 0, 'cursorul cVanzariRestaurant exista')
TestAssert(lnPart >= 0, 'cursorul nPart exista')
TestAssert(lnFidelio + lnIntrari + lnIesiri + lnVanzari > 0, ;
's-au citit randuri din fisierele intervalului')
* datele citite trebuie sa fie DOAR din intervalul cerut (filtrul pe numele fisierului)
IF lnVanzari > 0
LOCAL lnInAfara
SELECT cVanzariRestaurant
COUNT FOR !BETWEEN(CTOD(LEFT(ALLTRIM(NVL(datadocument, '')), 10)), ldData1, ldData2) ;
AND !EMPTY(ALLTRIM(NVL(datadocument, ''))) TO lnInAfara
TestAssertEqual(0, lnInAfara, 'vanzarile citite sunt in intervalul cerut')
ENDIF
* cheia externa de partener se calculeaza pentru toate randurile
IF lnPart > 0
LOCAL lnFaraCod
SELECT nPart
COUNT FOR EMPTY(NVL(cod, '')) TO lnFaraCod
TestAssertEqual(0, lnFaraCod, 'toti partenerii au cod cheie externa calculat')
ENDIF
DO HarnessStep WITH 1, 'import citit: ' + TRANSFORM(lnFidelio + lnIntrari + lnIesiri + lnVanzari) + ' randuri'
HarnessLog('REZULTAT: ' + TestRezultat())
DO HarnessDone WITH TestRezultat()
goImportFrm.Release()
QUIT

View File

@@ -0,0 +1,82 @@
* test_frm_import_ui.prg - test UI smoke pentru formularul real frm_import.
* Verifica pornirea aplicatiei fara Oracle: mediul se initializeaza, formularul se instantiaza,
* Init completeaza intervalul de date din settings.ini, iar metoda Trace scrie in edtInfo.
*
* Rulare (orchestrator + capturi):
* powershell -ExecutionPolicy Bypass -File Teste\vfp_ui_harness.ps1 `
* -TestPrg 'D:\PROIECTE\FLORA\FLORA2ROA\Teste\test_frm_import_ui.prg' `
* -Steps @('afisat','trace','date')
* Rulare fara capturi: vfp9.exe -A -T "Teste\test_frm_import_ui.prg" (auto-continue la 30s/pas)
SET SAFETY OFF
SET TALK OFF
PUBLIC gcUILog, gcSyncDir, gnTestFail, gnMockRaspuns, gcMockUltimMesaj, gnMockUltimTip
gnTestFail = 0
gnMockRaspuns = 6
gcMockUltimMesaj = ''
gnMockUltimTip = 0
LOCAL lcTestDir
lcTestDir = ADDBS(JUSTPATH(SYS(16, 0)))
gcUILog = lcTestDir + 'test_frm_import_ui_log.txt'
gcSyncDir = lcTestDir + 'uisync\'
IF !DIRECTORY(gcSyncDir)
MD (gcSyncDir)
ENDIF
STRTOFILE('START ' + TTOC(DATETIME()) + CHR(13) + CHR(10), gcUILog)
DO (lcTestDir + 'test_init_env_auto.prg') WITH .F.
SET PROCEDURE TO (lcTestDir + 'ui_harness.prg') ADDITIVE
* mock-ul de dialoguri se incarca ULTIMUL, ca sa umbreasca amessagebox din myutils.prg
SET PROCEDURE TO (lcTestDir + 'mock_amessagebox.prg') ADDITIVE
DO HarnessInit
HarnessLog('Mediu initializat. goApp=' + TYPE('goApp') + ' goExecutor=' + TYPE('goExecutor') + ;
' goConn=' + TYPE('goConn'))
TestAssert(TYPE('goApp') = 'O', 'goApp instantiat')
TestAssert(TYPE('goExecutor') = 'O', 'goExecutor instantiat')
TestAssert(TYPE('goConn') = 'O', 'goConn instantiat')
TestAssert(TYPE('goApp.oSettings') = 'O', 'settings.ini citit in goApp.oSettings')
PUBLIC goImportFrm
DO FORM (gcAppPath + 'frm_import') NAME goImportFrm NOSHOW
* modeless: Show(1) pe un formular modal nu returneaza si blocheaza testul headless
goImportFrm.WindowType = 0
goImportFrm.Show()
goImportFrm.WindowState = 2
DOEVENTS FORCE
TestAssert(TYPE('goImportFrm') = 'O', 'frm_import instantiat')
TestAssert(TYPE('goImportFrm.txtData1.Value') = 'D', 'txtData1 are valoare data')
TestAssert(TYPE('goImportFrm.txtData2.Value') = 'D', 'txtData2 are valoare data')
TestAssert(goImportFrm.txtData1.Value <= goImportFrm.txtData2.Value, ;
'interval valid: ' + DTOC(goImportFrm.txtData1.Value) + ' - ' + DTOC(goImportFrm.txtData2.Value))
TestAssertEqual(1, goImportFrm.chkTest.Value, 'bifa Test activa implicit (nu scrie note in ROA)')
DO HarnessStep WITH 0, 'formular afisat'
* Trace este canalul prin care formularul raporteaza progresul importului
goImportFrm.Trace('Verificare harness: mesaj de test')
DOEVENTS FORCE
TestAssert('Verificare harness' $ goImportFrm.edtInfo.Value, 'Trace scrie in edtInfo')
DO HarnessStep WITH 1, 'trace scris in edtInfo'
* schimbarea intervalului se reflecta in controale (fluxul real al utilizatorului)
goImportFrm.txtData1.Value = DATE(2025, 6, 1)
goImportFrm.txtData2.Value = DATE(2025, 6, 30)
goImportFrm.Refresh()
DOEVENTS FORCE
TestAssertEqual(DATE(2025, 6, 1), goImportFrm.txtData1.Value, 'data inceput modificata')
TestAssertEqual(DATE(2025, 6, 30), goImportFrm.txtData2.Value, 'data sfarsit modificata')
DO HarnessStep WITH 2, 'interval 06/2025 setat'
HarnessLog('REZULTAT: ' + TestRezultat())
DO HarnessDone WITH TestRezultat()
goImportFrm.Release()
QUIT

55
Teste/test_helpers.prg Normal file
View File

@@ -0,0 +1,55 @@
* test_helpers.prg - proceduri comune testelor FLORA2ROA (handler de erori headless + aserturi).
* Se incarca automat din test_init_env_auto.prg cu SET PROCEDURE ... ADDITIVE.
* TestErrorHandler - ON ERROR pentru rulare fara consola: logheaza si continua
* TestAssert(cond, mesaj) - PASS/FAIL in log; incrementeaza gnTestFail la esec
* TestAssertEqual(asteptat, obtinut, mesaj)
* TestRezultat() - 'PASS'/'FAIL' + numarul de aserturi picate
* TestLog(mesaj) - scrie in gcUILog (harness) sau in gcTestErrLog
PROCEDURE TestErrorHandler
LPARAMETERS tnError, tcMessage, tcProgram, tnLine
LOCAL lcLinie
lcLinie = TTOC(DATETIME()) + ' EROARE ' + TRANSFORM(tnError) + ': ' + TRANSFORM(tcMessage) + ;
' | program: ' + TRANSFORM(tcProgram) + ' | linia: ' + TRANSFORM(tnLine)
IF TYPE('gcTestErrLog') = 'C' AND !EMPTY(gcTestErrLog)
SET SAFETY OFF
STRTOFILE(lcLinie + CHR(13) + CHR(10), gcTestErrLog, .T.)
ENDIF
TestLog(lcLinie)
ENDPROC
PROCEDURE TestLog
LPARAMETERS tcMesaj
IF TYPE('gcUILog') = 'C' AND !EMPTY(gcUILog)
DO HarnessLog WITH TRANSFORM(tcMesaj)
ENDIF
ENDPROC
FUNCTION TestAssert
LPARAMETERS tlConditie, tcMesaj
IF TYPE('gnTestFail') <> 'N'
PUBLIC gnTestFail
gnTestFail = 0
ENDIF
IF tlConditie
TestLog('PASS: ' + TRANSFORM(tcMesaj))
ELSE
gnTestFail = gnTestFail + 1
TestLog('FAIL: ' + TRANSFORM(tcMesaj))
ENDIF
RETURN tlConditie
ENDFUNC
FUNCTION TestAssertEqual
LPARAMETERS tuAsteptat, tuObtinut, tcMesaj
LOCAL llEgal
llEgal = (TRANSFORM(tuAsteptat) == TRANSFORM(tuObtinut))
RETURN TestAssert(llEgal, TRANSFORM(tcMesaj) + ' [asteptat: ' + TRANSFORM(tuAsteptat) + ;
' | obtinut: ' + TRANSFORM(tuObtinut) + ']')
ENDFUNC
FUNCTION TestRezultat
LOCAL lnFail
lnFail = IIF(TYPE('gnTestFail') = 'N', gnTestFail, 0)
RETURN IIF(lnFail = 0, 'PASS', 'FAIL (' + TRANSFORM(lnFail) + ' aserturi picate)')
ENDFUNC

View File

@@ -0,0 +1,102 @@
* test_init_env_auto.prg
* Initializare NEINTERACTIVA a mediului FLORA2ROA pentru rulare headless
* (vfp9.exe -A -T "script.prg"), echivalentul programului principal main.prg fara
* DO FORM / READ EVENTS.
*
* Utilizare dintr-un script de test:
* DO Teste\test_init_env_auto WITH .F. && fara conectare Oracle (implicit)
* DO Teste\test_init_env_auto WITH .T. && conectare cu datele din settings.ini
* DO Teste\test_init_env_auto WITH .T., 'ROA', 'XENOTI', 'parola'
*
* Parametri:
* tlConnect - .T. = se conecteaza la Oracle (goConn.Connect); implicit .F.
* tcHost/tcUser/tcPassword - suprascriu datele din settings.ini
* tcAppPath - radacina aplicatiei; implicit se deduce din calea acestui program
*
* Dupa apel sunt disponibile: gcAppName, gcAppPath, gcTempPath, goLog, goApp, goConn,
* goExecutor, gnHandle si SET PROCEDURE / SET LIBRARY ca in main.prg. Se poate face
* CREATEOBJECT('frm_import') sau DO FORM frm_import si apela metodele punctual.
*
* ATENTIE: ErrorHandler-ul din main.prg deschide MESSAGEBOX (blocheaza headless). Aici se
* instaleaza TestErrorHandler (test_helpers.prg), care logheaza in gcTestErrLog si continua.
LPARAMETERS tlConnect, tcHost, tcUser, tcPassword, tcAppPath
SET SAFETY OFF
SET TALK OFF
CLOSE DATABASES ALL
SET DELETED ON
SET EXCLUSIVE ON
SET ANSI ON
SET EXACT ON
SET CENTURY ON
SET DATE DMY
SET NULLDISPLAY TO ''
PUBLIC gcAppName, gcAppPath, gcTempPath, gcTestErrLog
PUBLIC goApp, goLog, goConn, goExecutor, goImportFrm, gnHandle
LOCAL lcHere
lcHere = ADDBS(JUSTPATH(SYS(16, 0)))
IF TYPE('tcAppPath') = 'C' AND !EMPTY(tcAppPath)
gcAppPath = ADDBS(tcAppPath)
ELSE
* scriptul sta in <radacina>\Teste\, deci radacina e cu un nivel mai sus
gcAppPath = ADDBS(JUSTPATH(SUBSTR(lcHere, 1, LEN(lcHere) - 1)))
ENDIF
gcAppName = 'FLORA2ROA'
gcTempPath = GETENV("TEMP")
gcTestErrLog = gcAppPath + 'Teste\_erori_test.txt'
SET DEFAULT TO (gcAppPath)
SET PATH TO (gcAppPath) ADDITIVE
SET PATH TO (gcAppPath + 'Teste') ADDITIVE
* aceleasi fisiere de proceduri ca in main.prg; mock-urile se incarca DUPA, in test
SET PROCEDURE TO (gcAppPath + 'capp.prg') ADDITIVE
SET PROCEDURE TO (gcAppPath + 'app.prg') ADDITIVE
SET PROCEDURE TO (gcAppPath + 'myutils.prg') ADDITIVE
SET PROCEDURE TO (gcAppPath + 'vfpconnection_utils.prg') ADDITIVE
SET PROCEDURE TO (gcAppPath + 'myapp.prg') ADDITIVE
SET PROCEDURE TO (gcAppPath + 'Teste\test_helpers.prg') ADDITIVE
LOCAL lnLib, lcLib
FOR lnLib = 1 TO 2
lcLib = IIF(lnLib = 1, 'vfpconnection.fll', 'vfpencryption.fll')
IF FILE(gcAppPath + lcLib)
TRY
SET LIBRARY TO (gcAppPath + lcLib) ADDITIVE
CATCH
STRTOFILE('Nu s-a putut incarca ' + lcLib + CHR(13) + CHR(10), gcTestErrLog, .T.)
ENDTRY
ENDIF
ENDFOR
ON ERROR DO TestErrorHandler WITH ERROR(), MESSAGE(), PROGRAM(), LINENO()
goImportFrm = NULL
goConn = CREATEOBJECT("oConn")
goExecutor = CREATEOBJECT("oExecutor")
goLog = CREATEOBJECT("Log", gcAppPath + "log\log_test_" + gcAppName + "_" + DTOS(DATE()) + ".txt")
goApp = CREATEOBJECT("MyApp")
gnHandle = 0
IF TYPE('tcHost') = 'C' AND !EMPTY(tcHost)
goApp.oSettings.host = ALLTRIM(tcHost)
ENDIF
IF TYPE('tcUser') = 'C' AND !EMPTY(tcUser)
goApp.oSettings.user = ALLTRIM(tcUser)
ENDIF
IF TYPE('tcPassword') = 'C' AND !EMPTY(tcPassword)
goApp.oSettings.password = ALLTRIM(tcPassword)
ENDIF
IF TYPE('tlConnect') = 'L' AND tlConnect
goConn.Connect()
gnHandle = goConn.GetHandle()
IF gnHandle <= 0
RETURN .F.
ENDIF
ENDIF
RETURN .T.

35
Teste/text_din_bin.ps1 Normal file
View File

@@ -0,0 +1,35 @@
<#
.SYNOPSIS
Genereaza/improspateaza versiunile TEXT (.sc2/.vc2/.pj2) ale binarelor VFP din FLORA2ROA.
.DESCRIPTION
Wrapper peste D:\ROA\UTIL\foxbin2prg\git_sync.ps1 cu parametrii acestui proiect (flux
in-arbore: textul sta LANGA binar, nu intr-un cache extern). Git urmareste textele .??2;
binarele VFP sunt git-ignored, iar SVN ramane sursa de adevar pentru ele.
Se ruleaza la inceput de sesiune si INAINTE de orice git commit, ca textele sa reflecte
binarele curente. Conversia e incrementala si nu atinge niciodata binarele.
Fluxul complet si capcanele: COMUN\docs\flux-editare-vfp-text.md,
COMUN\docs\inrolare-proiect-git-text.md.
.PARAMETER Force
Reconverteste chiar daca textul e la zi.
.EXAMPLE
powershell -ExecutionPolicy Bypass -File Teste\text_din_bin.ps1
#>
[CmdletBinding()]
param(
[string]$ProjectRoot,
[switch]$Force
)
$ErrorActionPreference = 'Stop'
if (-not $ProjectRoot) { $ProjectRoot = Split-Path $PSScriptRoot -Parent }
$tool = 'D:\ROA\UTIL\foxbin2prg\git_sync.ps1'
if (-not (Test-Path $tool)) { throw "Nu gasesc $tool (tooling-ul partajat FoxBin2Prg)." }
# -DbfList gol: proiectul nu are tabele de configurare de urmarit ca text
& $tool -ProjectRoot $ProjectRoot -DbfList @() -Force:$Force
exit $LASTEXITCODE

64
Teste/ui_harness.prg Normal file
View File

@@ -0,0 +1,64 @@
* ui_harness.prg - proceduri reutilizabile pentru teste UI VFP cu handshake pe semafoare.
* Un test seteaza PUBLIC gcSyncDir (folder semafoare, cu \ final) si gcUILog (fisier log),
* apoi SET PROCEDURE TO ui_harness ADDITIVE si foloseste:
* HarnessLog(mesaj) - scrie o linie in log
* HarnessStep(n, mesaj [, autoSec]) - semnaleaza pasul n gata + asteapta continuarea
* HarnessReady(n, mesaj) / HarnessWaitContinue(n [, autoSec]) - variantele separate
* HarnessDone([status]) - scrie done.txt
* HarnessWaitContinue asteapta cont_<n>.txt; daca nu apare in autoSec secunde (implicit 30),
* continua singur (auto-continue) => testul merge si FARA orchestrator (doar log, fara capturi).
PROCEDURE HarnessInit
*-- defensiv: SAFETY OFF ca STRTOFILE peste un fisier existent sa nu scoata dialogul modal
*-- "overwrite?" (blocheaza headless); TALK OFF ca SUM/CALCULATE sa nu deseneze pe desktop.
*-- Un test ar trebui sa faca oricum SET SAFETY OFF INAINTE de prima lui scriere de log.
SET SAFETY OFF
SET TALK OFF
ENDPROC
PROCEDURE HarnessLog
LPARAMETERS tcMsg
IF TYPE('gcUILog') <> 'C' OR EMPTY(gcUILog)
RETURN
ENDIF
SET SAFETY OFF
LOCAL lcL
lcL = IIF(FILE(gcUILog), FILETOSTR(gcUILog), '')
STRTOFILE(lcL + tcMsg + CHR(13)+CHR(10), gcUILog)
ENDPROC
PROCEDURE HarnessReady
LPARAMETERS tnStep, tcNote
SET SAFETY OFF
STRTOFILE(TTOC(DATETIME()) + ' ' + tcNote + CHR(13)+CHR(10), gcSyncDir + 'ready_' + TRANSFORM(tnStep) + '.txt')
HarnessLog('READY ' + TRANSFORM(tnStep) + ' - ' + tcNote)
ENDPROC
FUNCTION HarnessWaitContinue
LPARAMETERS tnStep, tnAutoSec
LOCAL lcCont, lnMax, lnI, llPrimit
lcCont = gcSyncDir + 'cont_' + TRANSFORM(tnStep) + '.txt'
lnMax = IIF(TYPE('tnAutoSec') = 'N' AND tnAutoSec > 0, tnAutoSec, 30)
lnI = 0
DO WHILE !FILE(lcCont) AND lnI < (lnMax * 5)
lnI = lnI + 1
DOEVENTS FORCE
= INKEY(0.2)
ENDDO
llPrimit = FILE(lcCont)
HarnessLog('CONTINUE ' + TRANSFORM(tnStep) + IIF(llPrimit, ' (semnal primit)', ' (auto-continue dupa ' + TRANSFORM(lnMax) + 's)'))
RETURN llPrimit
ENDFUNC
PROCEDURE HarnessStep
LPARAMETERS tnStep, tcNote, tnAutoSec
HarnessReady(tnStep, tcNote)
HarnessWaitContinue(tnStep, tnAutoSec)
ENDPROC
PROCEDURE HarnessDone
LPARAMETERS tcStatus
SET SAFETY OFF
STRTOFILE(IIF(EMPTY(tcStatus), 'done', tcStatus) + ' ' + TTOC(DATETIME()) + CHR(13)+CHR(10), gcSyncDir + 'done.txt')
HarnessLog('GATA')
ENDPROC

128
Teste/verifica_sintaxa.ps1 Normal file
View File

@@ -0,0 +1,128 @@
<#
.SYNOPSIS
Verificare de sintaxa prin compilare headless a fisierelor .prg din FLORA2ROA.
.DESCRIPTION
Cel mai ieftin test dupa orice editare de .prg (vezi COMUN\docs\depanare_testare_vfp.md, sect. 4).
Compileaza COPII ale fisierelor intr-un folder temporar, ca .fxp-urile si .err-urile generate
sa nu murdareasca arborele proiectului. VFP nu rezolva simboluri externe la compilare, deci
copia izolata e suficienta pentru sintaxa.
Toate fisierele se compileaza intr-o SINGURA rulare vfp9: lansarile repetate de vfp9 din
aceeasi sesiune PowerShell esueaza tacut (procesul porneste dar nu executa - vezi
COMUN\docs\testare-ui-vfp.md, capcana d).
Verdict per fisier: existenta unui <nume>.err dupa compilare. Se raporteaza si liniile mai
lungi de 250 de caractere (capcana VFP: literal/linie prea lunga = eroare de compilare sau
eroare 11 derutanta la runtime).
NU inlocuieste testul functional: prinde sintaxa, nu apeluri catre metode inexistente.
.PARAMETER Files
Fisiere .prg de verificat. Implicit toate .prg-urile din radacina proiectului si din Teste\.
.PARAMETER ProjectRoot
Radacina proiectului (implicit folderul parinte al acestui script).
.EXAMPLE
powershell -ExecutionPolicy Bypass -File Teste\verifica_sintaxa.ps1
.EXAMPLE
powershell -ExecutionPolicy Bypass -File Teste\verifica_sintaxa.ps1 -Files myutils.prg,app.prg
#>
[CmdletBinding()]
param(
[string[]]$Files,
[string]$ProjectRoot,
[int]$MaxLineLen = 250,
[string]$Vfp = 'C:\Program Files (x86)\Microsoft Visual FoxPro 9\vfp9.exe'
)
$ErrorActionPreference = 'Stop'
if (-not $ProjectRoot) { $ProjectRoot = Split-Path $PSScriptRoot -Parent }
if (-not (Test-Path $Vfp)) { throw "Nu gasesc vfp9.exe: $Vfp" }
if (-not $Files) {
$Files = @(Get-ChildItem -Path $ProjectRoot -Filter '*.prg' -File | ForEach-Object { $_.FullName })
$Files += @(Get-ChildItem -Path (Join-Path $ProjectRoot 'Teste') -Filter '*.prg' -File -ErrorAction SilentlyContinue | ForEach-Object { $_.FullName })
}
$Files = $Files | ForEach-Object { if ([IO.Path]::IsPathRooted($_)) { $_ } else { Join-Path $ProjectRoot $_ } }
$work = Join-Path $env:TEMP ('flora2roa_syntax_' + [Guid]::NewGuid().ToString('N').Substring(0,8))
New-Item -ItemType Directory -Force -Path $work | Out-Null
# fisierele .h referite prin #INCLUDE trebuie sa fie langa copie, altfel compilarea da eroare
Get-ChildItem -Path $ProjectRoot -Filter '*.h' -File -ErrorAction SilentlyContinue |
ForEach-Object { Copy-Item $_.FullName (Join-Path $work $_.Name) -Force }
# omoara doar instantele vfp9 proprii (linia de comanda contine folderul de lucru temporar)
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
}
}
}
# copiaza sursele + strange liniile prea lungi (analiza statica, fara VFP)
$copii = @()
$liniiLungi = @{}
foreach ($f in $Files) {
if (-not (Test-Path $f)) { Write-Warning "lipsa: $f"; continue }
$nume = [IO.Path]::GetFileName($f)
$copie = Join-Path $work $nume
Copy-Item -LiteralPath $f -Destination $copie -Force
$copii += [pscustomobject]@{ Nume = $nume; Sursa = $f; Copie = $copie }
$lungi = @()
$linii = [IO.File]::ReadAllLines($f, [Text.Encoding]::GetEncoding(1252))
for ($i = 0; $i -lt $linii.Length; $i++) {
if ($linii[$i].Length -gt $MaxLineLen) { $lungi += ('{0} (len={1})' -f ($i + 1), $linii[$i].Length) }
}
if ($lungi) { $liniiLungi[$nume] = ($lungi -join '; ') }
}
if (-not $copii) { Write-Host 'Nimic de verificat.'; return }
# un singur .prg care compileaza toate copiile (SAFETY OFF: fara dialog "overwrite?" headless)
$linii = @('SET SAFETY OFF', 'SET TALK OFF')
foreach ($c in $copii) { $linii += ('lcT = "{0}"' -f $c.Copie); $linii += 'COMPILE (lcT)' }
$linii += 'STRTOFILE("gata", "' + (Join-Path $work '_gata.txt') + '")'
$linii += 'QUIT'
$tmpPrg = Join-Path $work '_compile_all.prg'
Set-Content -Path $tmpPrg -Value (($linii -join "`r`n") + "`r`n") -Encoding ascii
# -A ignora config.fpw implicit, -T sare peste splash; ambele obligatorii headless
$p = Start-Process -FilePath $Vfp -ArgumentList @('-A', '-T', "`"$tmpPrg`"") -PassThru -WindowStyle Minimized
if (-not $p.WaitForExit(180000)) { Write-Warning 'Timeout la compilare (180s).'; try { $p.Kill() } catch {} }
Stop-VfpProprii $work
if (-not (Test-Path (Join-Path $work '_gata.txt'))) {
Write-Warning 'Compilarea nu a ajuns la final - verdictul per fisier poate fi incomplet.'
}
$rezultate = foreach ($c in $copii) {
$err = [IO.Path]::ChangeExtension($c.Copie, 'err')
$fxp = [IO.Path]::ChangeExtension($c.Copie, 'fxp')
$erori = if (Test-Path $err) { (Get-Content -Raw $err).Trim() } else { '' }
[pscustomobject]@{
Fisier = $c.Nume
Sintaxa = if ($erori) { 'ERORI' } elseif (Test-Path $fxp) { 'OK' } else { 'NECOMPILAT' }
Erori = $erori
LiniiLungi = $(if ($liniiLungi.ContainsKey($c.Nume)) { $liniiLungi[$c.Nume] } else { '' })
}
}
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
$rezultate | Format-Table Fisier, Sintaxa -AutoSize
foreach ($r in $rezultate | Where-Object { $_.Erori }) {
Write-Host ''
Write-Warning ("ERORI in {0}:`n{1}" -f $r.Fisier, $r.Erori)
}
foreach ($r in $rezultate | Where-Object { $_.LiniiLungi }) {
Write-Host ''
Write-Warning ("Linii peste $MaxLineLen caractere in {0}: {1}" -f $r.Fisier, $r.LiniiLungi)
}
$nrProbleme = @($rezultate | Where-Object { $_.Sintaxa -ne 'OK' }).Count
Write-Host ''
Write-Host ("Gata. Fisiere verificate: {0} Cu probleme: {1}" -f @($rezultate).Count, $nrProbleme)
exit ([int]($nrProbleme -gt 0))

231
Teste/vfp_ui_harness.ps1 Normal file
View File

@@ -0,0 +1,231 @@
# 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"