From a05687a38f663b90bd864083a0d2430762be51dd Mon Sep 17 00:00:00 2001 From: Marius Mutu Date: Thu, 16 Jul 2026 11:04:23 +0300 Subject: [PATCH] Flux git-text in-arbore: git_sync.ps1 + teste, txt2vcx in-place, filemap pjx/dbf - git_sync.ps1 nou: conversie recursiva bin->text in arbore (staging temp, incremental, orfani, RoundtripExempt, DbfList cu date, anti-dialog cfg) - vfp_filemap.ps1: mapari .pjx->.pj2 si .dbf->.db2 - txt2vcx.ps1: CacheRoot implicit = ProjectRoot (text in-arbore) + cfg anti-dialog in staging - test_roundtrip.ps1: parametrizat per proiect, restaurare din backup temp (nu git checkout) - test_git_sync.ps1 nou: incremental, orfani, esec partial, protectie binar, scutire roundtrip - CLAUDE.md: fluxul nou documentat Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 15 +- git_sync.ps1 | 313 +++++++++++++++++++++++++ test_git_sync.ps1 | 223 ++++++++++++++++++ test_roundtrip.ps1 | 551 ++++++++++++++++++++++++++------------------- txt2vcx.ps1 | 8 +- vfp_filemap.ps1 | 1 + 6 files changed, 872 insertions(+), 239 deletions(-) create mode 100644 git_sync.ps1 create mode 100644 test_git_sync.ps1 diff --git a/CLAUDE.md b/CLAUDE.md index 106e64d..406fe6e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,18 +20,21 @@ It serves two purposes: ## Local additions (ROA-specific, not part of upstream) -- **`vcx2txt.ps1`** — PowerShell script that converts the VFP binaries of a project (read from its `.pjx`) into text under a cache folder, so the code inside `.vcx/.scx/.frx/.mnx/.dbc` binaries becomes greppable. Incremental (skips up-to-date files), does not need the VFP IDE open (uses the compiled `FoxBin2Prg.EXE`). -- **`txt2vcx.ps1`** — the write-back counterpart: regenerates `.vcx/.scx` binaries from edited `.vc2/.sc2` cache text and copies them back into the project (see "Golden rule" below for scope/limits). -- **`vfp_filemap.ps1`** — shared bin↔memo↔text extension map (`.vcx`/`.vct`/`.vc2`, `.scx`/`.sct`/`.sc2`, …), dot-sourced by both `vcx2txt.ps1` and `txt2vcx.ps1` so the mapping stays in one place. -- **`test_roundtrip.ps1`** — standalone fidelity/regression test for the bin→text→bin→text roundtrip and for `txt2vcx.ps1`'s guard rails (staleness, COMUN, missing binary, negative/corrupt input, multi-file runs). See examples below. -- **Text caches** live **outside this folder**, under `D:\ROA\_vfp_textcache\` (one subfolder per project: `roacnpro`, `roagest`, `roaimob`) — per-project regenerable text caches. Safe to delete; never version them. **They must stay outside `foxbin2prg`**: `ReCreate_FoxBin2Prg.prg` does a recursive `Prg2Bin` over the whole tool folder (`get_FilesFromDirectory` recurses into every subfolder, foxbin2prg.prg:6135) and would reconvert any `.??2` under it back into stray binaries. +- **`git_sync.ps1`** — the **in-tree** flow (primary for migrated ROA projects): folder-mode recursive over the whole project tree (incl. `COMUN\`), generates the `.??2` text **next to each binary** (not in an external cache) so git tracks readable text instead of binaries. Incremental, converts in a temp staging area and copies back only the text (never touches the SVN working-copy binaries), cleans up orphan `.??2`, continues past per-file failures and exits nonzero on any. Params: `-ProjectRoot`, `-DbfList` (tables converted to `.db2` with data), `-RoundtripExempt`, `-SubDir`. Run at session start and before every `git commit`; do not commit while it reports failures. +- **`vcx2txt.ps1`** — PowerShell script that converts the VFP binaries of a project (read from its `.pjx`) into text under a cache folder, so the code inside `.vcx/.scx/.frx/.mnx/.dbc` binaries becomes greppable. Incremental (skips up-to-date files), does not need the VFP IDE open (uses the compiled `FoxBin2Prg.EXE`). Search-only / non-migrated projects; migrated projects use `git_sync.ps1` instead. +- **`txt2vcx.ps1`** — the write-back counterpart: regenerates `.vcx/.scx` binaries from edited `.vc2/.sc2` text and copies them back into the project (see "Golden rule" below for scope/limits). `-CacheRoot` defaults to `-ProjectRoot` (in-tree flow: cache→project mapping is identity); pass a separate root only for the old external-cache flow. +- **`vfp_filemap.ps1`** — shared bin↔memo↔text extension map (`.vcx`/`.vct`/`.vc2`, `.scx`/`.sct`/`.sc2`, `.pjx`/`.pjt`/`.pj2`, `.dbf`/`.fpt`/`.db2`, …), dot-sourced by `git_sync.ps1`, `vcx2txt.ps1` and `txt2vcx.ps1` so the mapping stays in one place. Also hosts `Invoke-FoxBin2PrgSafe` (timeout + `taskkill`). +- **`test_roundtrip.ps1`** — standalone fidelity/regression test for the bin→text→bin→text roundtrip and for `txt2vcx.ps1`'s guard rails (staleness, COMUN, missing binary, negative/corrupt input, multi-file runs); parametrized per project. See examples below. +- **`test_git_sync.ps1`** — standalone tests for `git_sync.ps1` on temp fixtures: incremental (up-to-date files skipped), orphan cleanup, nonzero exit on partial failure, and binaries byte-identical after any run. +- **Text caches** (external-cache flow, non-migrated / search-only projects) live **outside this folder**, under `D:\ROA\_vfp_textcache\` (one subfolder per project: `roacnpro`, `roagest`, `roaimob`) — per-project regenerable text caches. Migrated projects (ROACONT, COMUN, …) keep their `.??2` in-tree via `git_sync.ps1` instead of a cache here. Safe to delete; never version them. **They must stay outside `foxbin2prg`**: `ReCreate_FoxBin2Prg.prg` does a recursive `Prg2Bin` over the whole tool folder (`get_FilesFromDirectory` recurses into every subfolder, foxbin2prg.prg:6135) and would reconvert any `.??2` under it back into stray binaries. - **`PROMPT_cautare_vfp.md`** — reusable prompt (Romanian) explaining how to set up this search flow in any VFP project. Read it before working on VFP code search. **Golden rule:** the text cache is for searching, and — for `.vcx`/`.scx` only — for making real code edits via `txt2vcx.ps1`. Caveats: - The generated text format is **position-sensitive**: don't reformat or reflow lines when editing; preserve the exact line layout. - Properties/methods are emitted **alphabetically sorted** — a newly added property must be inserted in alphabetical order, not appended. -- `.mnx`/`.frx` remain **read-only** (write-back not supported): menus still need GENMENU in the IDE to produce `.mpr`, and the `.frx` report format is too fragile for round-trip write-back. Edit those in the VFP IDE as before. +- Only `.vcx`/`.scx` support write-back. `.frx/.mnx/.lbx/.pjx/.dbc/.dbf` are **read-only text** (edit in the VFP IDE): menus still need GENMENU to produce `.mpr`, and the other formats are too fragile for round-trip write-back. `git_sync.ps1` still generates their `.??2` for readable diffs, but never writes them back. +- Six FFC/third-party classes under `COMUN\` are in `git_sync.ps1`'s `-RoundtripExempt` list (`#INCLUDE` makes the isolated roundtrip a false-negative): `accessibility.vcx`, `oinventar.vcx`, `_gdiplus.vcx`, `_reportlistener.vcx` (in `COMUN\clase\`), `foxcharts.vcx` (`COMUN\utile\foxcharts\`), `_webview.vcx` (`COMUN\utile\web\`). Their text is valid (bin→text runs) but roundtrip is not guaranteed — edit them only in the IDE. - Targets under `COMUN\` require `-AllowComun` **and** explicit approval — a change there affects every ROA app. - **After any VFP IDE session** (even one that only opened a class/form), re-run `vcx2txt.ps1` to refresh the cache before editing text — otherwise `txt2vcx.ps1`'s staleness check will (correctly) refuse to write back over unseen IDE changes. - **Encoding**: `.vc2`/`.sc2` files declare `CPID="1252"` in their header, but for ROAGEST at least the Romanian-diacritics bytes are actually **cp1250** (`0xE3`=ă, `0xBA`=ş, etc.) — the header lies. Don't decode/re-encode the text with a codepage inferred from the header; treat it byte-preserving (read/write/copy as raw bytes, compare on bytes) and this doesn't matter in practice — it only bites if a script tries to interpret the text as a specific codepage string. diff --git a/git_sync.ps1 b/git_sync.ps1 new file mode 100644 index 0000000..bdd1637 --- /dev/null +++ b/git_sync.ps1 @@ -0,0 +1,313 @@ +<# +.SYNOPSIS + Sincronizeaza in ARBORE (langa binare) versiunile text FoxBin2Prg ale binarelor VFP + ale unui proiect ROA, pentru ca git sa urmareasca text lizibil in loc de binare. + +.DESCRIPTION + Folder-mode recursiv pe TOT arborele proiectului (inclusiv COMUN\), NU project-mode din .pjx: + altfel binarele nereferentiate in .pjx ar disparea din git. Pentru fiecare binar suportat + genereaza fisierul text (.vc2/.sc2/.fr2/.mn2/.lb2/.pj2/.dc2/.db2) IN ACELASI FOLDER cu binarul. + + Tipuri convertite: vcx, scx, frx, mnx, lbx, pjx, dbc (recursiv) + dbf-urile dintr-o lista + explicita per proiect (-DbfList). Incremental: sare peste text-ul deja la zi (mtime binar vs text). + + CONVERSIE IN STAGING TEMP (nu in arbore): FoxBin2Prg.EXE lasa artefacte (.err, _*.BAK) si + vcx2txt sterge copia binara la final - cu destinatia = arborele ar sterge binarul din working + copy-ul SVN. Aici copiem binar+memo (+.dbf.cfg) intr-un temp, convertim acolo, si copiem inapoi + DOAR textul (raw bytes, byte-preserving - encoding real cp1250 sub header CPID=1252, nu decodam). + Binarele din arbore NU sunt atinse niciodata. + + Fidelitate per tip: + - vcx/scx: roundtrip complet (bin->text->bin->text, comparare pe octeti) - au write-back. + - frx/mnx/lbx/pjx/dbc/dbf: doar bin->text cu exit-code + timeout + text nevid (write-back nesuportat). + + Esec partial: la esecul unui fisier continua cu restul, colecteaza esecurile, le raporteaza la + final si iese cu exit code nenul. + + Curatare orfani: textele .??2 fara binar corespondent (binar sters din SVN) se sterg si se + raporteaza; la fel .db2-urile ale caror tabele au IESIT din lista -DbfList. + + Toate invocarile FoxBin2Prg.EXE trec prin Invoke-FoxBin2PrgSafe (timeout + taskkill). + +.PARAMETER ProjectRoot + Radacina proiectului (implicit D:\ROA\ROACONT). + +.PARAMETER Types + Tipurile binare (fara dbf) convertite recursiv (implicit vcx,scx,frx,mnx,lbx,pjx,dbc). + +.PARAMETER DbfList + Lista de pattern-uri relative la ProjectRoot pentru dbf-urile convertite in .db2 cu DATE + (implicit lista ROACONT: Locale\locale*.dbf, Help\*.dbf, DATE\Optiuni_*.dbf). + +.PARAMETER SubDir + Optional. Limiteaza rularea la un subfolder (relativ la ProjectRoot) - util pentru rulari in pasi. + Curatarea de orfani se face doar cand se ruleaza pe tot arborele (SubDir gol). + +.PARAMETER TimeoutSeconds + Timeout per invocare FoxBin2Prg (implicit 60s). + +.PARAMETER MaxDb2Mb + Prag: un .db2 peste aceasta dimensiune e raportat (tabela ar trebui scoasa din lista). Implicit 5. + +.PARAMETER Force + Reconverteste chiar daca textul e la zi. + +.PARAMETER NoOrphanCleanup + Nu sterge textele orfane (doar le raporteaza). + +.EXAMPLE + powershell -ExecutionPolicy Bypass -File D:\ROA\UTIL\foxbin2prg\git_sync.ps1 -ProjectRoot D:\ROA\ROACONT +#> +[CmdletBinding()] +param( + [string]$ProjectRoot = 'D:\ROA\ROACONT', + [string[]]$Types = @('vcx','scx','frx','mnx','lbx','pjx','dbc'), + [string[]]$DbfList = @('Locale\locale*.dbf','Help\*.dbf','DATE\Optiuni_*.dbf', + 'COMUN\clase\Locale\*.dbf','COMUN\datemenu\*.dbf','COMUN\datemenu\xold\*.dbf', + 'COMUN\Drepturi utilizatori\*\*.dbf','COMUN\utile\GridExtras\gridextras.dbf', + 'COMUN\utile\ListProperty\lproperty.dbf','COMUN\utile\hpdf\*.dbf','COMUN\utile\hpdf\ReportOutput\*.dbf'), + # clase FFC/third-party cu #INCLUDE: roundtrip izolat = fals-negativ; editare doar in IDE, fara write-back + [string[]]$RoundtripExempt = @('COMUN\clase\accessibility.vcx','COMUN\clase\oinventar.vcx','COMUN\clase\_gdiplus.vcx','COMUN\clase\_reportlistener.vcx','COMUN\utile\foxcharts\foxcharts.vcx','COMUN\utile\web\_webview.vcx'), + [string]$SubDir = '', + [int]$TimeoutSeconds = 60, + [int]$MaxDb2Mb = 5, + [switch]$Force, + [switch]$NoOrphanCleanup +) + +$ErrorActionPreference = 'Stop' +$exe = 'D:\ROA\UTIL\foxbin2prg\FoxBin2Prg.EXE' +if (-not (Test-Path $exe)) { + throw "Nu gasesc $exe. Compileaza intai FoxBin2Prg (deschide foxbin2prg.pj2 in VFP si Build)." +} +if (-not (Test-Path -LiteralPath $ProjectRoot -PathType Container)) { + throw "ProjectRoot nu exista: $ProjectRoot" +} +. (Join-Path $PSScriptRoot 'vfp_filemap.ps1') +$map = $script:VfpFileMap +$ProjectRoot = (Get-Item -LiteralPath $ProjectRoot).FullName + +# extensiile text valide (pt curatare orfani), lowercase +$script:TextExts = @{} +foreach ($k in $map.Keys) { $script:TextExts[$map[$k][1]] = $k } # .vc2 -> .vcx + +# set cai absolute (lowercase) ale binarelor scutite de garda roundtrip - doar bin->text valid +$script:ExemptSet = New-Object System.Collections.Generic.HashSet[string] +foreach ($rel in $RoundtripExempt) { [void]$script:ExemptSet.Add((Join-Path $ProjectRoot $rel).ToLower()) } + +$runStamp = "{0}-{1}" -f $PID, (Get-Date -Format 'yyyyMMddHHmmssfff') +$stagingRoot = Join-Path $env:TEMP ("git_sync_" + (Split-Path $ProjectRoot -Leaf) + "\$runStamp") + +function Invoke-Fb2p { + # toate apelurile trec prin watchdog (timeout + taskkill) - vezi vfp_filemap.ps1 + param([string]$InputFile, [string]$Recompile = '') + if ($Recompile) { + $r = Invoke-FoxBin2PrgSafe -ExePath $exe -ExeArgs @($InputFile,'','','','1','0','1','',$Recompile) -TimeoutSeconds $TimeoutSeconds + } else { + $r = Invoke-FoxBin2PrgSafe -ExePath $exe -ExeArgs @($InputFile,'','','','1','0','1') -TimeoutSeconds $TimeoutSeconds + } + return $r +} + +function Find-InDir([string]$dir, [string]$name) { + # gaseste un fisier dupa nume (case-insensitive) intr-un folder; $null daca lipseste + if (-not (Test-Path -LiteralPath $dir -PathType Container)) { return $null } + return (Get-ChildItem -LiteralPath $dir -Filter $name -File -ErrorAction SilentlyContinue | Select-Object -First 1) +} + +function Test-BytesEqual([byte[]]$a, [byte[]]$b) { + # normalizeaza CRLF/CR->LF si compara ca string Latin1 (byte<->char lossless) - rapid pe fisiere mari; + # bucla per-octet in PowerShell era patologic de lenta (minute/fisier) pe textele de sute de KB. + $enc = [System.Text.Encoding]::GetEncoding(28591) + $sa = $enc.GetString($a) -replace "`r`n?", "`n" + $sb = $enc.GetString($b) -replace "`r`n?", "`n" + return $sa -eq $sb +} + +# --- Converteste un binar in staging si copiaza inapoi DOAR textul. Intoarce status string. --- +function Convert-One { + param([Parameter(Mandatory)][System.IO.FileInfo]$Bin) + + $ext = $Bin.Extension.ToLower() + $memoX = $map[$ext][0] + $txtX = $map[$ext][1] + $dir = $Bin.DirectoryName + $base = [IO.Path]::GetFileNameWithoutExtension($Bin.Name) + $memo = Find-InDir $dir ($base + $memoX) + + # text tinta existent (case-insensitive) pentru incremental + destinatie + $txtExisting = Find-InDir $dir ($base + $txtX) + if (-not $Force -and $txtExisting) { + $newestBin = $Bin.LastWriteTime + if ($memo -and $memo.LastWriteTime -gt $newestBin) { $newestBin = $memo.LastWriteTime } + if ($txtExisting.LastWriteTime -ge $newestBin) { return 'uptodate' } + } + + $stageDir = Join-Path $stagingRoot ([Guid]::NewGuid().ToString('N').Substring(0,12)) + New-Item -ItemType Directory -Force -Path $stageDir | Out-Null + try { + # cfg in staging (mostenit si de subfolderul rc): DontShowErrors suprima MessageBox-ul propriu + # "End of Process (with errors)"; ShowProgressbar 0 = fara fereastra de progres (rulare headless). + $cfgLines = [System.Collections.Generic.List[string]]@('DontShowErrors: 1','ShowProgressbar: 0') + if ($ext -eq '.dbf') { $cfgLines.Add('DBF_Conversion_Support: 4') } # forteaza exportul DATELOR + Set-Content -LiteralPath (Join-Path $stageDir 'foxbin2prg.cfg') -Value $cfgLines -Encoding Ascii + + $stageBin = Join-Path $stageDir $Bin.Name + Copy-Item -LiteralPath $Bin.FullName -Destination $stageBin -Force + if ($memo) { Copy-Item -LiteralPath $memo.FullName -Destination (Join-Path $stageDir $memo.Name) -Force } + + if ($ext -eq '.dbc') { + # dbc = tabela cu index structural .dcx; fara el FoxBin2Prg cere indexul intr-un dialog modal (hang) + $dcx = Find-InDir $dir ($base + '.dcx') + if ($dcx) { Copy-Item -LiteralPath $dcx.FullName -Destination (Join-Path $stageDir $dcx.Name) -Force } + } + + if ($ext -eq '.dbf') { + # index structural + (daca e tabela legata de DBC) containerul: FoxBin2Prg le cere ca sa deschida tabela + $cdx = Find-InDir $dir ($base + '.cdx') + if ($cdx) { Copy-Item -LiteralPath $cdx.FullName -Destination (Join-Path $stageDir $cdx.Name) -Force } + Get-ChildItem -LiteralPath $dir -File -ErrorAction SilentlyContinue | + Where-Object { '.dbc','.dct','.dcx' -contains $_.Extension.ToLower() } | + ForEach-Object { Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $stageDir $_.Name) -Force } + # onoreaza order/condition custom din .dbf.cfg comis, daca exista + $cfg = Find-InDir $dir ($base + '.dbf.cfg') + if ($cfg) { Copy-Item -LiteralPath $cfg.FullName -Destination (Join-Path $stageDir $cfg.Name) -Force } + } + + $r = Invoke-Fb2p -InputFile $stageBin + if ($r.TimedOut) { return 'fail:timeout bin->text' } + if ($r.ExitCode -ne 0) { return "fail:bin->text ErrorLevel=$($r.ExitCode)" } + + $stageTxt = Find-InDir $stageDir ($base + $txtX) + if (-not $stageTxt) { return 'fail:text negenerat' } + if ($stageTxt.Length -eq 0) { return 'fail:text gol' } + + # scutite (clase FFC cu #INCLUDE): roundtrip izolat = fals-negativ; doar bin->text valid, fara write-back + $exempt = $script:ExemptSet.Contains($Bin.FullName.ToLower()) + + # fidelitate per tip: roundtrip complet doar vcx/scx (mai putin cele scutite) + if (($ext -eq '.vcx' -or $ext -eq '.scx') -and -not $exempt) { + $rc2 = Join-Path $stageDir 'rc' + New-Item -ItemType Directory -Force -Path $rc2 | Out-Null + $txt1 = Join-Path $rc2 $stageTxt.Name + Copy-Item -LiteralPath $stageTxt.FullName -Destination $txt1 -Force + $r2 = Invoke-Fb2p -InputFile $txt1 -Recompile $ProjectRoot + if ($r2.TimedOut -or $r2.ExitCode -ne 0) { return "fail:roundtrip text->bin ErrorLevel=$($r2.ExitCode)" } + $bin2 = Find-InDir $rc2 ($base + $ext) + if (-not $bin2) { return 'fail:roundtrip bin nerecreat' } + $r3 = Invoke-Fb2p -InputFile $bin2.FullName + if ($r3.TimedOut -or $r3.ExitCode -ne 0) { return "fail:roundtrip bin->text2 ErrorLevel=$($r3.ExitCode)" } + $txt2 = Find-InDir $rc2 ($base + $txtX) + if (-not $txt2) { return 'fail:roundtrip text2 negenerat' } + if (-not (Test-BytesEqual ([IO.File]::ReadAllBytes($stageTxt.FullName)) ([IO.File]::ReadAllBytes($txt2.FullName)))) { + return 'fail:roundtrip text1 != text2' + } + } + + if ($ext -eq '.dbf' -and $stageTxt.Length -gt ($MaxDb2Mb * 1MB)) { + $script:BigDb2 += ("{0} ({1:N1} MB)" -f (Get-RelPath $Bin.FullName), ($stageTxt.Length / 1MB)) + } + + # copiaza inapoi DOAR textul (raw bytes), pastrand numele produs de FoxBin2Prg + $destTxt = if ($txtExisting) { $txtExisting.FullName } else { Join-Path $dir $stageTxt.Name } + Copy-Item -LiteralPath $stageTxt.FullName -Destination $destTxt -Force + # mtime text >= mtime binar, altfel pare stale si se reconverteste mereu + $destItem = Get-Item -LiteralPath $destTxt + $newestBin = $Bin.LastWriteTime + if ($memo -and $memo.LastWriteTime -gt $newestBin) { $newestBin = $memo.LastWriteTime } + if ($destItem.LastWriteTime -lt $newestBin) { $destItem.LastWriteTime = $newestBin.AddSeconds(1) } + if ($exempt) { return 'ok-exempt' } + return 'ok' + } finally { + Remove-Item -Recurse -Force -LiteralPath $stageDir -ErrorAction SilentlyContinue + } +} + +function Get-RelPath([string]$full) { + if ($full.ToLower().StartsWith($ProjectRoot.ToLower())) { return $full.Substring($ProjectRoot.Length).TrimStart('\') } + return $full +} + +# =========================== MAIN =========================== +$scanRoot = if ($SubDir) { Join-Path $ProjectRoot $SubDir } else { $ProjectRoot } +if (-not (Test-Path -LiteralPath $scanRoot)) { throw "SubDir nu exista: $scanRoot" } + +Write-Host "=== git_sync: ProjectRoot=$ProjectRoot SubDir=$(if($SubDir){$SubDir}else{'(tot)'}) Types=$($Types -join ',') ===" + +# --- lista dbf din patternurile per proiect (cai absolute, set pt orfani) --- +$script:DbfInList = New-Object System.Collections.Generic.HashSet[string] +foreach ($pat in $DbfList) { + $full = Join-Path $ProjectRoot $pat + Get-ChildItem -Path $full -File -ErrorAction SilentlyContinue | + Where-Object { $_.Extension.ToLower() -eq '.dbf' } | + ForEach-Object { [void]$script:DbfInList.Add($_.FullName.ToLower()) } +} + +# --- construieste lista de binare de convertit --- +$script:BigDb2 = @() +$binList = @() +$typeExts = $Types | ForEach-Object { '.' + $_.ToLower() } +$binList += Get-ChildItem -LiteralPath $scanRoot -Recurse -File -ErrorAction SilentlyContinue | + Where-Object { $typeExts -contains $_.Extension.ToLower() } +# dbf-urile din lista aflate sub scanRoot +$binList += $script:DbfInList | ForEach-Object { Get-Item -LiteralPath $_ -ErrorAction SilentlyContinue } | + Where-Object { $_ -and $_.FullName.ToLower().StartsWith(((Get-Item -LiteralPath $scanRoot).FullName).ToLower()) } + +$ok=0; $up=0; $failures=@() +foreach ($b in $binList) { + $status = Convert-One -Bin $b + $rel = Get-RelPath $b.FullName + switch -Wildcard ($status) { + 'ok' { $ok++; Write-Host "OK $rel" } + 'ok-exempt' { $ok++; Write-Host "OK $rel (roundtrip scutit)" } + 'uptodate' { $up++ } + 'fail:*' { $failures += [PSCustomObject]@{ File=$rel; Reason=$status.Substring(5) }; Write-Warning "ESEC $rel -- $($status.Substring(5))" } + } +} + +# --- curatare orfani (doar pe tot arborele) --- +$orphansDeleted = @() +if (-not $SubDir) { + $allText = Get-ChildItem -LiteralPath $ProjectRoot -Recurse -File -ErrorAction SilentlyContinue | + Where-Object { $script:TextExts.ContainsKey($_.Extension.ToLower()) } + foreach ($t in $allText) { + $tExt = $t.Extension.ToLower() + $binExt = $script:TextExts[$tExt] + $base = [IO.Path]::GetFileNameWithoutExtension($t.Name) + $bin = Find-InDir $t.DirectoryName ($base + $binExt) + $isOrphan = $false + if (-not $bin) { + $isOrphan = $true # binar sters + } elseif ($tExt -eq '.db2' -and -not $script:DbfInList.Contains($bin.FullName.ToLower())) { + $isOrphan = $true # tabela a iesit din lista + } + if ($isOrphan) { + $orphansDeleted += (Get-RelPath $t.FullName) + if (-not $NoOrphanCleanup) { Remove-Item -LiteralPath $t.FullName -Force -ErrorAction SilentlyContinue } + } + } +} + +Remove-Item -Recurse -Force -LiteralPath $stagingRoot -ErrorAction SilentlyContinue + +# --- raport --- +Write-Host '' +Write-Host "--- Sumar git_sync ---" +Write-Host "Convertite: $ok Sarite(la zi): $up Esuate: $($failures.Count)" +if ($orphansDeleted.Count -gt 0) { + $verb = if ($NoOrphanCleanup) { 'raportate (nesterse)' } else { 'sterse' } + Write-Host "Orfani ${verb}: $($orphansDeleted.Count)" + $orphansDeleted | ForEach-Object { Write-Host " orfan: $_" } +} +if ($script:BigDb2.Count -gt 0) { + Write-Warning "db2 peste ${MaxDb2Mb} MB (scoate tabela din lista, ramane binara):" + $script:BigDb2 | ForEach-Object { Write-Warning " $_" } +} +if ($failures.Count -gt 0) { + Write-Host '' + Write-Host "ESECURI (nu comite pana nu sunt explicate):" + $failures | ForEach-Object { Write-Host " $($_.File) -- $($_.Reason)" } + exit 1 +} +Write-Host "OK: fara esecuri." +exit 0 diff --git a/test_git_sync.ps1 b/test_git_sync.ps1 new file mode 100644 index 0000000..d3d8c62 --- /dev/null +++ b/test_git_sync.ps1 @@ -0,0 +1,223 @@ +<# +.SYNOPSIS + Teste standalone (fara Pester) pentru git_sync.ps1, pe fixture-uri construite in TEMP. + Acopera cele 4 comportamente noi cerute de plan (Sectiunea 8, decizie eng-review D2): + (A) incremental - a doua rulare pe fixture neschimbat nu reconverteste nimic (Convertite=0, Sarite>0); + (B) curatare orfani - un .vc2 fara binar se sterge si se raporteaza; un .db2 a carui + tabela a iesit din -DbfList se sterge si se raporteaza; + (C) esec partial - un .vcx corupt produce exit nenul si apare in esecuri, DAR fisierele + valide din aceeasi rulare se convertesc totusi; + (D) protectia binarului - dupa orice rulare, toate binarele fixture-ului sunt byte-identice + (SHA256 inainte/dupa). Verificat si dupa fiecare rulare din A/B/C. + + Fixture: copie a celui mai mic .vcx+.vct real din Clase si a setului Locale (dbf+memo+indecsi+ + container dbc) din proiectul sursa. NU se scrie nimic in proiectul sursa - doar se citeste/copiaza. + Se copiaza DOAR binarele si memo-urile, niciodata textul .??2 deja prezent in sursa. + +.PARAMETER SourceProject + Proiectul din care se copiaza esantioanele de binare (implicit ROACONT). +#> +[CmdletBinding()] +param( + [string]$SourceProject = 'D:\ROA\ROACONT' +) + +$ErrorActionPreference = 'Stop' +$script:GitSync = 'D:\ROA\UTIL\foxbin2prg\git_sync.ps1' +$script:Failures = @() +# extensiile considerate "binar" in fixture (tot ce NU e text .??2 generat) +$script:BinExts = @('.vcx','.vct','.dbf','.fpt','.cdx','.dbc','.dct','.dcx') + +function Assert([bool]$cond, [string]$msg) { + if ($cond) { Write-Host " PASS: $msg" -ForegroundColor Green } + else { Write-Host " FAIL: $msg" -ForegroundColor Red; $script:Failures += $msg } +} + +# --- Ruleaza git_sync.ps1 ca proces copil si parseaza sumarul + orfanii + exit code --- +function Invoke-GitSync { + param([string]$Fixture, [string[]]$DbfList, [string[]]$Exempt, [int]$TimeoutSeconds = 30) + # -Command (nu -File): powershell.exe -File colapseaza 'a','b' intr-un singur token, DbfList ar fi gol + $dbfLit = if ($DbfList -and $DbfList.Count) { '@(' + (($DbfList | ForEach-Object { "'" + $_ + "'" }) -join ',') + ')' } else { '@()' } + $exLit = if ($Exempt -and $Exempt.Count) { '@(' + (($Exempt | ForEach-Object { "'" + $_ + "'" }) -join ',') + ')' } else { '@()' } + $cmd = "& '$script:GitSync' -ProjectRoot '$Fixture' -DbfList $dbfLit -RoundtripExempt $exLit -TimeoutSeconds $TimeoutSeconds" + # EAP=Continue local: git_sync ruleaza FoxBin2Prg care scrie pe stderr (ex. 'not a table'); + # sub EAP=Stop, capturarea 2>&1 a acelui stderr ar arunca NativeCommandError si ar opri testul. + $prevEAP = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + $sw = [Diagnostics.Stopwatch]::StartNew() + $raw = & powershell -ExecutionPolicy Bypass -Command $cmd 2>&1 | Out-String + $exit = $LASTEXITCODE + $sw.Stop() + $ErrorActionPreference = $prevEAP + $conv = $null; $skip = $null; $fail = $null + $m = [regex]::Match($raw, 'Convertite:\s*(\d+)\s+Sarite\(la zi\):\s*(\d+)\s+Esuate:\s*(\d+)') + if ($m.Success) { $conv = [int]$m.Groups[1].Value; $skip = [int]$m.Groups[2].Value; $fail = [int]$m.Groups[3].Value } + $orphans = @() + foreach ($line in ($raw -split "`r?`n")) { + $om = [regex]::Match($line, '^\s*orfan:\s*(.+?)\s*$') + if ($om.Success) { $orphans += $om.Groups[1].Value } + } + return [PSCustomObject]@{ Exit=$exit; Convertite=$conv; Sarite=$skip; Esuate=$fail; Orphans=$orphans; Raw=$raw; ElapsedSec=[math]::Round($sw.Elapsed.TotalSeconds,1); TimeoutSec=$TimeoutSeconds } +} + +# --- Snapshot SHA256 al tuturor binarelor din fixture (pt protectia binarului) --- +function Get-BinSnapshot([string]$root) { + $h = @{} + Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue | + Where-Object { $script:BinExts -contains $_.Extension.ToLower() } | + ForEach-Object { $h[$_.FullName.ToLower()] = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash } + return $h +} + +function Assert-BinariesUnchanged([hashtable]$before, [string]$root, [string]$ctx) { + $after = Get-BinSnapshot $root + $why = '' + if ($before.Count -ne $after.Count) { $why = "numar binare $($before.Count)->$($after.Count)" } + else { + foreach ($k in $before.Keys) { + if (-not $after.ContainsKey($k)) { $why = "lipsa dupa rulare: $k"; break } + if ($after[$k] -ne $before[$k]) { $why = "modificat: $k"; break } + } + } + Assert ($why -eq '') "${ctx}: toate binarele byte-identice dupa rulare$(if($why){" ($why)"})" +} + +# --- Construieste un fixture in TEMP: atentie.vcx+.vct + setul Locale (fara text .??2) --- +function New-Fixture([string]$tag) { + $fx = Join-Path $env:TEMP ("gs_test_{0}_{1}_{2}" -f $tag, $PID, ([Guid]::NewGuid().ToString('N').Substring(0,8))) + New-Item -ItemType Directory -Force -Path (Join-Path $fx 'Clase'), (Join-Path $fx 'Locale') | Out-Null + # doar .vcx + .vct (memo); NU .vc2 care exista deja in sursa dintr-o rulare anterioara + Get-ChildItem (Join-Path $SourceProject 'Clase') -Filter 'atentie.*' -File | + Where-Object { @('.vcx','.vct') -contains $_.Extension.ToLower() } | + ForEach-Object { Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $fx 'Clase') -Force } + # set Locale: doua tabele (pt scenariul de scoatere din lista) + indecsi + container dbc + foreach ($f in 'locale.dbf','locale.FPT','locale.CDX','locale.dbc','locale.DCT','locale.DCX','locale_lang.dbf','locale_lang.FPT','locale_lang.cdx') { + $s = Join-Path $SourceProject "Locale\$f" + if (Test-Path -LiteralPath $s) { Copy-Item -LiteralPath $s -Destination (Join-Path $fx 'Locale') -Force } + } + return $fx +} + +# Sterge fixture-ul + folderul-parinte de staging lasat de git_sync (TEMP\git_sync_, ramane gol) +function Remove-Fixture([string]$fx) { + Remove-Item -Recurse -Force -LiteralPath $fx -ErrorAction SilentlyContinue + $stagingParent = Join-Path $env:TEMP ("git_sync_" + (Split-Path $fx -Leaf)) + Remove-Item -Recurse -Force -LiteralPath $stagingParent -ErrorAction SilentlyContinue +} + +$script:BothDbf = @('Locale\locale.dbf','Locale\locale_lang.dbf') + +# ============================== SCENARIILE ============================== + +function Test-Incremental { + Write-Host "`n--- (A) incremental: a doua rulare nu reconverteste nimic ---" + $fx = New-Fixture 'inc' + try { + $snap = Get-BinSnapshot $fx + $r1 = Invoke-GitSync -Fixture $fx -DbfList $script:BothDbf + Assert ($r1.Exit -eq 0) "A: prima rulare exit 0 (exit=$($r1.Exit))" + Assert ($r1.Convertite -gt 0) "A: prima rulare converteste (Convertite=$($r1.Convertite) > 0)" + Assert-BinariesUnchanged $snap $fx "A(run1)" + + $r2 = Invoke-GitSync -Fixture $fx -DbfList $script:BothDbf + Assert ($r2.Exit -eq 0) "A: a doua rulare exit 0 (exit=$($r2.Exit))" + Assert ($r2.Convertite -eq 0) "A: a doua rulare NU reconverteste (Convertite=$($r2.Convertite) = 0)" + Assert ($r2.Sarite -gt 0) "A: a doua rulare sare fisierele la zi (Sarite=$($r2.Sarite) > 0)" + Assert-BinariesUnchanged $snap $fx "A(run2)" + } finally { Remove-Fixture $fx } +} + +function Test-Orphans { + Write-Host "`n--- (B) curatare orfani: .vc2 fara binar + .db2 iesit din lista ---" + $fx = New-Fixture 'orf' + try { + # orfan de tip 1: text .vc2 fara binar .vcx corespondent + $orfVc2 = Join-Path $fx 'Clase\_orfan.vc2' + Set-Content -LiteralPath $orfVc2 -Value '* text orfan fara binar corespondent' -Encoding Ascii + $snap = Get-BinSnapshot $fx + + # run1: ambele tabele in lista -> produce db2-uri; orfanul fara binar se sterge + $r1 = Invoke-GitSync -Fixture $fx -DbfList $script:BothDbf + Assert (-not (Test-Path -LiteralPath $orfVc2)) "B: .vc2 orfan (fara binar) sters" + Assert (($r1.Orphans -join ';') -match '(?i)_orfan\.vc2') "B: .vc2 orfan raportat in sumar" + Assert (Test-Path -LiteralPath (Join-Path $fx 'Locale\locale_lang.db2')) "B: locale_lang.db2 creat in run1" + Assert-BinariesUnchanged $snap $fx "B(run1)" + + # run2: locale_lang scos din lista -> db2-ul lui iese din lista -> orfan + $r2 = Invoke-GitSync -Fixture $fx -DbfList @('Locale\locale.dbf') + Assert (-not (Test-Path -LiteralPath (Join-Path $fx 'Locale\locale_lang.db2'))) "B: db2 al tabelei iesite din lista sters" + Assert (($r2.Orphans -join ';') -match '(?i)locale_lang\.db2') "B: db2 iesit din lista raportat in sumar" + Assert (Test-Path -LiteralPath (Join-Path $fx 'Locale\locale.db2')) "B: db2 al tabelei ramase in lista pastrat" + Assert-BinariesUnchanged $snap $fx "B(run2)" + } finally { Remove-Fixture $fx } +} + +function Test-PartialFailure { + Write-Host "`n--- (C) esec partial: binar corupt esueaza, restul se convertesc ---" + $fx = New-Fixture 'fail' + try { + # binar corupt: octeti garbage in .vcx si .vct (FoxBin2Prg: 'not a table', ErrorLevel != 0) + $g1 = New-Object byte[] 800; (New-Object Random).NextBytes($g1) + [IO.File]::WriteAllBytes((Join-Path $fx 'Clase\corupt.vcx'), $g1) + $g2 = New-Object byte[] 600; (New-Object Random).NextBytes($g2) + [IO.File]::WriteAllBytes((Join-Path $fx 'Clase\corupt.vct'), $g2) + $snap = Get-BinSnapshot $fx + + $r = Invoke-GitSync -Fixture $fx -DbfList @('Locale\locale.dbf') + Assert ($r.Exit -ne 0) "C: exit nenul la esec partial (exit=$($r.Exit))" + Assert ($r.Esuate -ge 1) "C: cel putin un esec raportat (Esuate=$($r.Esuate))" + Assert ($r.Raw -match '(?i)corupt\.vcx') "C: fisierul corupt apare in lista de esecuri" + Assert ($r.Convertite -ge 1) "C: fisierele valide se convertesc totusi (Convertite=$($r.Convertite))" + Assert (Test-Path -LiteralPath (Join-Path $fx 'Clase\atentie.vc2')) "C: text valid (atentie.vc2) produs in ciuda esecului" + # esecul trebuie sa fie rapid (ErrorLevel, nu dialog modal): sub watchdog inseamna ca nu a aparut MessageBox VFP + Assert ($r.ElapsedSec -lt $r.TimeoutSec) "C: esec rapid fara dialog modal ($($r.ElapsedSec)s < watchdog $($r.TimeoutSec)s)" + Assert-BinariesUnchanged $snap $fx "C" + } finally { Remove-Fixture $fx } +} + +function Test-BinaryProtection { + Write-Host "`n--- (D) protectia binarului: byte-identic dupa conversie completa ---" + $fx = New-Fixture 'bin' + try { + $snap = Get-BinSnapshot $fx + Assert ($snap.Count -gt 0) "D: fixture-ul contine binare de protejat ($($snap.Count))" + $r = Invoke-GitSync -Fixture $fx -DbfList $script:BothDbf + Assert ($r.Exit -eq 0) "D: rulare curata (exit=$($r.Exit))" + Assert-BinariesUnchanged $snap $fx "D" + } finally { Remove-Fixture $fx } +} + +function Test-Exempt { + Write-Host "`n--- (E) scutire roundtrip: fisierul din -RoundtripExempt produce text fara garda roundtrip ---" + $fx = New-Fixture 'exempt' + try { + $snap = Get-BinSnapshot $fx + $r = Invoke-GitSync -Fixture $fx -DbfList @('Locale\locale.dbf') -Exempt @('Clase\atentie.vcx') + Assert ($r.Exit -eq 0) "E: rulare curata cu scutire (exit=$($r.Exit))" + Assert ($r.Raw -match '(?i)atentie\.vcx \(roundtrip scutit\)') "E: fisierul scutit raportat 'roundtrip scutit'" + Assert (Test-Path -LiteralPath (Join-Path $fx 'Clase\atentie.vc2')) "E: text produs pentru fisierul scutit" + Assert-BinariesUnchanged $snap $fx "E" + } finally { Remove-Fixture $fx } +} + +# ============================== MAIN ============================== +if (-not (Test-Path -LiteralPath $script:GitSync)) { throw "Nu gasesc git_sync.ps1: $script:GitSync" } +if (-not (Test-Path -LiteralPath $SourceProject)) { throw "Proiect sursa inexistent: $SourceProject" } + +Write-Host "=== test_git_sync.ps1 -- SourceProject=$SourceProject ===" +Test-Incremental +Test-Orphans +Test-PartialFailure +Test-BinaryProtection +Test-Exempt + +Write-Host "`n=== REZULTAT ===" +if ($script:Failures.Count -eq 0) { + Write-Host "TOATE testele au trecut." -ForegroundColor Green + exit 0 +} else { + Write-Host "$($script:Failures.Count) test(e) esuate:" -ForegroundColor Red + $script:Failures | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } + exit 1 +} + diff --git a/test_roundtrip.ps1 b/test_roundtrip.ps1 index 8b0e37d..36e45c1 100644 --- a/test_roundtrip.ps1 +++ b/test_roundtrip.ps1 @@ -1,40 +1,49 @@ <# .SYNOPSIS Teste standalone (fara Pester) pentru fluxul bin<->text VFP (FoxBin2Prg) si scriptul - de write-back txt2vcx.ps1. Implicit ruleaza pe ROAGEST. + de write-back txt2vcx.ps1. Parametrizat per proiect (implicit ROAGEST). .DESCRIPTION Faze: - - fidelity : roundtrip pur bin->text1->bin->text2 (doar in temp, fara scriere in proiect), - pentru .vcx/.scx (blocant) si .mnx/.frx (warn-only - read-only in flux). - - edit : editare reala pe copie de proiect (ROAGEST), via txt2vcx.ps1, cu revert git. + - fidelity : esantion din FIECARE tip convertit, auto-descoperit din arborele proiectului. + vcx/scx -> roundtrip complet bin->text1->bin->text2 (comparare pe octeti); + frx/mnx/lbx/dbc/pjx/dbf -> doar validare bin->text (exit code + timeout + text nevid). + Totul in temp, nimic scris in proiect. + - edit : editare reala via txt2vcx.ps1 pe fixture-uri cunoscute (implicit ROAGEST); + daca fixture-ul lipseste in proiect, se sare (warn). Restaurarea binarelor + se face din backup temp (NU git checkout - binarele sunt git-ignored dupa migrare). - negative : text stricat, fisier gol, guard-uri (staleness, binar lipsa, cale in afara cache-ului, extensie nesuportata, COMUN, folder-foxbin2prg, -DryRun), - test multi-fisier (unul valid + unul stricat in aceeasi rulare). - - smoke : Faza A (fidelity) pe cate un .vcx mic non-COMUN din ROAIMOB si ROAACNPRO. + test multi-fisier. Textul stricat ruleaza DOAR in cache temp, niciodata in arbore. + - smoke : fidelity pe cate un .vcx din proiectele frate (-SmokeProjects). - all : toate cele de mai sus, in ordine. - Precondita: git status --porcelain trebuie sa fie gol PENTRU FISIERELE TINTA (nu tot - repo-ul) inainte de a incepe. La final (in finally), git checkout -- pe orice binar - atins + refresh cache, indiferent de rezultat. + Restaurare: orice binar atins de write-back e copiat in backup temp INAINTE si restaurat + la final (in finally), cu asertie byte-identic fata de original. Cache-ul text implicit e + un director temporar creat pentru rulare (decuplat de arborele proiectului). - Exit 1 daca orice Assert a esuat, CU EXCEPTIA celor marcate warn-only (mnx/frx in Faza A). + Exit 1 daca orice Assert a esuat, CU EXCEPTIA celor marcate warn-only. .PARAMETER ProjectRoot Radacina proiectului principal testat (implicit ROAGEST). .PARAMETER CacheRoot - Cache-ul text al proiectului principal. + Cache-ul text folosit de teste. Gol (implicit) => director temporar creat/sters de test, + ca sa nu se scrie niciodata text in arborele proiectului. .PARAMETER Phase all | fidelity | edit | negative | smoke + +.PARAMETER SmokeProjects + Lista proiectelor frate pentru faza smoke. #> [CmdletBinding()] param( [string]$ProjectRoot = 'D:\ROA\ROAGEST', - [string]$CacheRoot = 'D:\ROA\_vfp_textcache\roagest', + [string]$CacheRoot = '', [ValidateSet('all','fidelity','edit','negative','smoke')] - [string]$Phase = 'all' + [string]$Phase = 'all', + [string[]]$SmokeProjects = @('D:\ROA\ROAIMOB','D:\ROA\ROAACNPRO') ) $ErrorActionPreference = 'Stop' @@ -45,7 +54,8 @@ $txt2vcx = 'D:\ROA\UTIL\foxbin2prg\txt2vcx.ps1' $script:Failures = @() $script:Enc = [Text.Encoding]::GetEncoding(1252) -$script:TouchedProjectFiles = New-Object System.Collections.Generic.List[string] # cai relative la ProjectRoot, pt cleanup git checkout -- +$script:BackupRoot = Join-Path $env:TEMP ("fb2p_test_backup_{0}_{1}" -f $PID, ([Guid]::NewGuid().ToString('N').Substring(0,8))) +$script:Backups = @{} # cheie: cale binar (lower) -> info backup, pt restaurare la final function Assert([bool]$cond, [string]$msg) { if ($cond) { Write-Host " PASS: $msg" -ForegroundColor Green } @@ -76,10 +86,13 @@ function Test-BytesEqual([byte[]]$a, [byte[]]$b) { } function Invoke-Fb2p { - # Watchdog obligatoriu - vezi nota din txt2vcx.ps1: FoxBin2Prg poate afisa un - # MessageBox modal nativ VFP (necontrolat de cDontShowErrors) la text stricat; - # fara asta, Faza D (negativ) ar putea ramane agatata la infinit intr-o rulare automata. + # Watchdog obligatoriu - FoxBin2Prg poate afisa un MessageBox modal nativ VFP la text stricat; + # fara asta, faza negativa ar ramane agatata la infinit intr-o rulare automata. param([string]$InputFile, [string]$Recompile = '', [int]$TimeoutSeconds = 60) + # cfg in dir-ul input-ului: DontShowErrors suprima MessageBox-ul propriu FoxBin2Prg (ca in git_sync) + $cfgLines = [System.Collections.Generic.List[string]]@('DontShowErrors: 1','ShowProgressbar: 0') + if ([IO.Path]::GetExtension($InputFile).ToLower() -eq '.dbf') { $cfgLines.Add('DBF_Conversion_Support: 4') } + Set-Content -LiteralPath (Join-Path (Split-Path -Parent $InputFile) 'foxbin2prg.cfg') -Value $cfgLines -Encoding Ascii if ($Recompile) { $r = Invoke-FoxBin2PrgSafe -ExePath $exe -ExeArgs @($InputFile,'','','','1','0','1','',$Recompile) -TimeoutSeconds $TimeoutSeconds } else { @@ -94,21 +107,73 @@ function Get-FileHashBytes([string]$path) { return (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash } -# --- Faza A: fidelitate pura (bin->text1->bin->text2, doar in temp) --- +function Get-RelPath([string]$Root, [string]$Full) { + return $Full.Substring($Root.TrimEnd('\').Length).TrimStart('\') +} + +# --- Auto-descoperire: un esantion mic non-COMUN de tipul cerut, cu memo companion --- +function Find-SampleBinary { + param( + [Parameter(Mandatory)][string]$ProjRoot, + [Parameter(Mandatory)][string]$Ext, + [bool]$RequireMemo = $true, + [int]$MaxBytes = 500000 + ) + if (-not (Test-Path -LiteralPath $ProjRoot)) { return $null } + $memoExt = $script:VfpFileMap[$Ext][0] + Get-ChildItem -LiteralPath $ProjRoot -Recurse -File -Filter ("*" + $Ext) -ErrorAction SilentlyContinue | + Where-Object { + $_.FullName -notmatch '(?i)\\COMUN\\' -and + $_.Length -le $MaxBytes -and $_.Length -gt 0 -and + (-not $RequireMemo -or (Test-Path -LiteralPath ([IO.Path]::ChangeExtension($_.FullName, $memoExt)))) + } | + Sort-Object Length | + Select-Object -First 1 +} + +# --- Backup/restore binare din temp (inlocuieste git checkout - binarele sunt git-ignored) --- +function Backup-ProjectBinary([string]$fullBin) { + $key = $fullBin.ToLower() + if ($script:Backups.ContainsKey($key)) { return } + $ext = [IO.Path]::GetExtension($fullBin).ToLower() + $memoExt = $script:VfpFileMap[$ext][0] + $dir = Split-Path $fullBin -Parent + $base = [IO.Path]::GetFileNameWithoutExtension($fullBin) + $memoEntry = Get-ChildItem -LiteralPath $dir -Filter ($base + $memoExt) -ErrorAction SilentlyContinue | Select-Object -First 1 + $memoFull = if ($memoEntry) { $memoEntry.FullName } else { [IO.Path]::ChangeExtension($fullBin, $memoExt) } + $bdir = Join-Path $script:BackupRoot ([Guid]::NewGuid().ToString('N').Substring(0,8)) + New-Item -ItemType Directory -Force -Path $bdir | Out-Null + $binBak = Join-Path $bdir (Split-Path $fullBin -Leaf) + Copy-Item -LiteralPath $fullBin -Destination $binBak -Force + $memoBak = $null + if (Test-Path -LiteralPath $memoFull) { + $memoBak = Join-Path $bdir (Split-Path $memoFull -Leaf) + Copy-Item -LiteralPath $memoFull -Destination $memoBak -Force + } + $script:Backups[$key] = @{ BinFull=$fullBin; BinBak=$binBak; MemoFull=$memoFull; MemoBak=$memoBak; OrigHash=(Get-FileHashBytes $fullBin) } +} + +function Restore-ProjectBinaries { + foreach ($k in @($script:Backups.Keys)) { + $b = $script:Backups[$k] + Copy-Item -LiteralPath $b.BinBak -Destination $b.BinFull -Force + if ($b.MemoBak) { Copy-Item -LiteralPath $b.MemoBak -Destination $b.MemoFull -Force } + $now = Get-FileHashBytes $b.BinFull + Assert ($now -eq $b.OrigHash) "Restore: $(Split-Path $b.BinFull -Leaf) byte-identic cu originalul dupa restore" + } +} + +# --- Fidelitate: roundtrip complet bin->text1->bin->text2 (doar in temp) - pt vcx/scx --- function Test-FidelityRoundtrip { param( - [Parameter(Mandatory)][string]$RelPath, # relativ la $ProjRoot, ex Clase\oavize.vcx + [Parameter(Mandatory)][string]$RelPath, [Parameter(Mandatory)][string]$ProjRoot, [bool]$WarnOnly = $false ) $full = Join-Path $ProjRoot $RelPath - if (-not (Test-Path -LiteralPath $full)) { - AssertWarn $false "Faza A: fisier lipsa, sarit: $RelPath" - return - } + if (-not (Test-Path -LiteralPath $full)) { AssertWarn $false "Faza A: fisier lipsa, sarit: $RelPath"; return } $ext = [IO.Path]::GetExtension($full).ToLower() - $pair = $null - foreach ($k in $script:VfpFileMap.Keys) { if ($k -eq $ext) { $pair = $script:VfpFileMap[$k]; break } } + $pair = $script:VfpFileMap[$ext] if (-not $pair) { AssertWarn $false "Faza A: extensie necunoscuta, sarit: $RelPath"; return } $memoExt = $pair[0]; $txtExt = $pair[1] $memoFull = [IO.Path]::ChangeExtension($full, $memoExt) @@ -123,8 +188,7 @@ function Test-FidelityRoundtrip { $rc1 = Invoke-Fb2p -InputFile $bin1 $text1 = Join-Path $work ($baseName + $txtExt) if ($rc1 -ne 0 -or -not (Test-Path -LiteralPath $text1)) { - AssertWarn (-not $WarnOnly) "Faza A ($RelPath): Bin2Prg #1 a esuat (ErrorLevel=$rc1)" - if ($WarnOnly) { return } + if ($WarnOnly) { AssertWarn $false "Faza A ($RelPath): Bin2Prg #1 a esuat (ErrorLevel=$rc1)"; return } Assert $false "Faza A ($RelPath): Bin2Prg #1 a esuat (ErrorLevel=$rc1)" return } @@ -149,262 +213,287 @@ function Test-FidelityRoundtrip { } } -# --- Faza B: editare reala prin txt2vcx.ps1, cu revert git --- +# --- Validare bin->text (fara roundtrip) - pt frx/mnx/lbx/dbc/pjx/dbf (write-back nesuportat) --- +function Test-BinToTextValid { + param( + [Parameter(Mandatory)][string]$RelPath, + [Parameter(Mandatory)][string]$ProjRoot, + [bool]$WarnOnly = $false + ) + $full = Join-Path $ProjRoot $RelPath + if (-not (Test-Path -LiteralPath $full)) { AssertWarn $false "Faza A: fisier lipsa, sarit: $RelPath"; return } + $ext = [IO.Path]::GetExtension($full).ToLower() + $pair = $script:VfpFileMap[$ext] + if (-not $pair) { AssertWarn $false "Faza A: extensie necunoscuta, sarit: $RelPath"; return } + $memoExt = $pair[0]; $txtExt = $pair[1] + $memoFull = [IO.Path]::ChangeExtension($full, $memoExt) + + $work = Join-Path $env:TEMP ("fb2p_valid_{0}_{1}" -f $PID, ([Guid]::NewGuid().ToString('N').Substring(0,8))) + New-Item -ItemType Directory -Force -Path $work | Out-Null + try { + $baseName = [IO.Path]::GetFileNameWithoutExtension($full) + $bin1 = Join-Path $work ($baseName + $ext) + Copy-Item -LiteralPath $full -Destination $bin1 -Force + if (Test-Path -LiteralPath $memoFull) { Copy-Item -LiteralPath $memoFull -Destination (Join-Path $work ($baseName + $memoExt)) -Force } + $rc = Invoke-Fb2p -InputFile $bin1 + $text1 = Join-Path $work ($baseName + $txtExt) + $ok = ($rc -eq 0) -and (Test-Path -LiteralPath $text1) -and ((Get-Item -LiteralPath $text1).Length -gt 0) + $msg = "Faza A ($RelPath): bin->text valid (exit=$rc, text nevid)" + if ($WarnOnly) { AssertWarn $ok $msg } else { Assert $ok $msg } + } finally { + Remove-Item -Recurse -Force -LiteralPath $work -ErrorAction SilentlyContinue + } +} + +# specificatie per tip convertit: roundtrip doar vcx/scx; restul doar validare bin->text +$script:FidelitySpec = @( + @{ Ext='.vcx'; Roundtrip=$true; WarnOnly=$false; RequireMemo=$true } + @{ Ext='.scx'; Roundtrip=$true; WarnOnly=$false; RequireMemo=$true } + @{ Ext='.frx'; Roundtrip=$false; WarnOnly=$true; RequireMemo=$true } + @{ Ext='.mnx'; Roundtrip=$false; WarnOnly=$true; RequireMemo=$true } + @{ Ext='.lbx'; Roundtrip=$false; WarnOnly=$true; RequireMemo=$true } + @{ Ext='.dbc'; Roundtrip=$false; WarnOnly=$true; RequireMemo=$true } + @{ Ext='.pjx'; Roundtrip=$false; WarnOnly=$true; RequireMemo=$true } + @{ Ext='.dbf'; Roundtrip=$false; WarnOnly=$true; RequireMemo=$false } +) + +function Invoke-FidelityForProject([string]$ProjRoot) { + foreach ($s in $script:FidelitySpec) { + $sample = Find-SampleBinary -ProjRoot $ProjRoot -Ext $s.Ext -RequireMemo $s.RequireMemo + if (-not $sample) { AssertWarn $false "Faza A: niciun esantion $($s.Ext) in $ProjRoot, sarit"; continue } + $rel = Get-RelPath $ProjRoot $sample.FullName + Write-Host " esantion $($s.Ext): $rel ($([int]($sample.Length/1024)) KB)" + if ($s.Roundtrip) { Test-FidelityRoundtrip -RelPath $rel -ProjRoot $ProjRoot -WarnOnly $s.WarnOnly } + else { Test-BinToTextValid -RelPath $rel -ProjRoot $ProjRoot -WarnOnly $s.WarnOnly } + } +} + +# --- Editare reala prin txt2vcx.ps1, cu backup/restore (nu git) --- function Test-EditRoundtrip { param( - [Parameter(Mandatory)][string]$RelPath, # ex Clase\oavize.vcx + [Parameter(Mandatory)][string]$RelPath, [Parameter(Mandatory)][string]$CaptionOld, [Parameter(Mandatory)][string]$CaptionNew, - [Parameter(Mandatory)][string]$ProcedureMarker # ex "PROCEDURE Init" + [Parameter(Mandatory)][string]$ProcedureMarker ) + $full = Join-Path $ProjectRoot $RelPath + if (-not (Test-Path -LiteralPath $full)) { AssertWarn $false "Faza B: fixture lipsa in proiect, sarit: $RelPath"; return } $ext = [IO.Path]::GetExtension($RelPath).ToLower() $pair = $script:VfpFileMap[$ext] - $memoExt = $pair[0]; $txtExt = $pair[1] + $txtExt = $pair[1] $relDir = Split-Path $RelPath -Parent - $baseName = [IO.Path]::GetFileNameWithoutExtension($RelPath) - # rezolva casing-ul REAL al memo-ului din proiect (poate fi .SCT/.VCT majuscul, vezi fundal.SCT) - $memoEntry = Get-ChildItem -LiteralPath (Join-Path $ProjectRoot $relDir) -Filter ($baseName + $memoExt) -ErrorAction SilentlyContinue | Select-Object -First 1 - $relMemo = if ($memoEntry) { Join-Path $relDir $memoEntry.Name } else { Join-Path $relDir ($baseName + $memoExt) } - $relText = Join-Path $relDir ($baseName + $txtExt) + $base = [IO.Path]::GetFileNameWithoutExtension($RelPath) + $relText = if ($relDir) { Join-Path $relDir ($base + $txtExt) } else { $base + $txtExt } Write-Host "-- Faza B: $RelPath --" - & $vcx2txt -Source (Join-Path $ProjectRoot $RelPath) -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null + Backup-ProjectBinary $full + $origHash = $script:Backups[$full.ToLower()].OrigHash + + & $vcx2txt -Source $full -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null $cacheText = Join-Path $CacheRoot $relText Assert (Test-Path -LiteralPath $cacheText) "Faza B ($RelPath): cache text regenerat" - $bytes = [IO.File]::ReadAllBytes($cacheText) - $text = $script:Enc.GetString($bytes) + $text = $script:Enc.GetString([IO.File]::ReadAllBytes($cacheText)) Assert ($text.Contains($CaptionOld)) "Faza B ($RelPath): linia Caption originala gasita" $text = $text.Replace($CaptionOld, $CaptionNew) $marker = "`t" + $ProcedureMarker + "`r`n" $idx = $text.IndexOf($marker) Assert ($idx -ge 0) "Faza B ($RelPath): marker '$ProcedureMarker' gasit" - $insertPos = $idx + $marker.Length - $text = $text.Substring(0,$insertPos) + "`t`t* test roundtrip`r`n" + $text.Substring($insertPos) + if ($idx -ge 0) { + $insertPos = $idx + $marker.Length + $text = $text.Substring(0,$insertPos) + "`t`t* test roundtrip`r`n" + $text.Substring($insertPos) + } [IO.File]::WriteAllBytes($cacheText, $script:Enc.GetBytes($text)) - $script:TouchedProjectFiles.Add($RelPath) | Out-Null - $script:TouchedProjectFiles.Add($relMemo) | Out-Null - & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot $rc = $LASTEXITCODE Assert ($rc -eq 0) "Faza B ($RelPath): txt2vcx.ps1 exit 0" - Push-Location $ProjectRoot - try { - $porcelain = (git status --porcelain -- $RelPath $relMemo) -join "`n" - $lines = $porcelain -split "`n" | Where-Object { $_.Trim() -ne '' } - Assert ($lines.Count -eq 2) "Faza B ($RelPath): git status arata exact 2 fisiere modificate ($($lines.Count) gasite)" - $untracked = git status --porcelain | Where-Object { $_ -match '^\?\?' -and $_ -match [regex]::Escape((Split-Path $RelPath -Parent)) } - Assert (-not $untracked -or ($untracked | Where-Object { $_ -match '\.bak$' }).Count -eq 0) "Faza B ($RelPath): fara fisiere .bak untracked" - } finally { Pop-Location } + Assert ((Get-FileHashBytes $full) -ne $origHash) "Faza B ($RelPath): binarul din proiect a fost rescris (hash diferit de original)" + $bakStray = Join-Path (Join-Path $ProjectRoot $relDir) ($base + '.bak') + Assert (-not (Test-Path -LiteralPath $bakStray)) "Faza B ($RelPath): fara .bak lasat in proiect" $regenText = $script:Enc.GetString([IO.File]::ReadAllBytes($cacheText)) Assert ($regenText.Contains($CaptionNew)) "Faza B ($RelPath): textul regenerat contine Caption editat" Assert ($regenText.Contains('* test roundtrip')) "Faza B ($RelPath): textul regenerat contine comentariul adaugat" - - if ($RelPath -match 'oavize') { - Assert ($regenText.Contains($CaptionNew)) "Faza B ($RelPath) [assert diacritice]: string cu diacritice supravietuieste roundtrip-ului byte-identic" - } - - Push-Location $ProjectRoot - try { git checkout -- $RelPath $relMemo } finally { Pop-Location } - & $vcx2txt -Source (Join-Path $ProjectRoot $RelPath) -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null + # binarul e restaurat din backup in finally-ul global } # ============================== MAIN ============================== +$ownCache = $false +if ([string]::IsNullOrWhiteSpace($CacheRoot)) { + $CacheRoot = Join-Path $env:TEMP ("fb2p_test_cache_{0}_{1}" -f $PID, ([Guid]::NewGuid().ToString('N').Substring(0,8))) + $ownCache = $true +} +if (-not (Test-Path -LiteralPath $CacheRoot)) { New-Item -ItemType Directory -Force -Path $CacheRoot | Out-Null } +New-Item -ItemType Directory -Force -Path $script:BackupRoot | Out-Null + Write-Host "=== test_roundtrip.ps1 -- ProjectRoot=$ProjectRoot CacheRoot=$CacheRoot Phase=$Phase ===" -# Precondita: git status curat pentru fisierele tinta cunoscute dinainte -$knownTargets = @('Clase\oavize.vcx','Clase\oavize.vct','Ferestre\fundal.scx','Ferestre\fundal.SCT') -Push-Location $ProjectRoot -try { - $pre = (git status --porcelain -- $knownTargets) -join "`n" - if ($pre.Trim() -ne '') { - throw "Precondita esuata: fisierele tinta nu sunt curate in git:`n$pre" - } -} finally { Pop-Location } +# fixture-uri de editare cunoscute (implicit ROAGEST); se sar automat daca lipsesc in proiect +$editFixtures = @( + @{ Rel='Clase\oavize.vcx'; Old='Lb_titlu_alb_b121.Caption = "Date aviz"'; New='Lb_titlu_alb_b121.Caption = "Atentie-diacritice-ok"'; Marker='PROCEDURE Init' } + @{ Rel='Ferestre\fundal.scx'; Old='Caption = ""'; New='Caption = "test"'; Marker='PROCEDURE Show' } +) try { if ($Phase -eq 'all' -or $Phase -eq 'fidelity') { - Write-Host "`n--- FAZA A: fidelitate (roundtrip pur) ---" - Test-FidelityRoundtrip -RelPath 'Clase\oavize.vcx' -ProjRoot $ProjectRoot # inlocuieste atentie.vcx (gol, fara clase reale - vezi deviere raportata) - Test-FidelityRoundtrip -RelPath 'Ferestre\fundal.scx' -ProjRoot $ProjectRoot - Test-FidelityRoundtrip -RelPath 'Meniuri\achi_transfer.mnx' -ProjRoot $ProjectRoot -WarnOnly $true - Test-FidelityRoundtrip -RelPath 'Rapoarte\fisa.frx' -ProjRoot $ProjectRoot -WarnOnly $true + Write-Host "`n--- FAZA A: fidelitate (esantion din fiecare tip convertit) ---" + Invoke-FidelityForProject $ProjectRoot } if ($Phase -eq 'all' -or $Phase -eq 'edit') { - Write-Host "`n--- FAZA B: editare reala (ROAGEST) ---" - Test-EditRoundtrip -RelPath 'Clase\oavize.vcx' ` - -CaptionOld 'Lb_titlu_alb_b121.Caption = "Date aviz"' ` - -CaptionNew 'Lb_titlu_alb_b121.Caption = "Atentie-diacritice-ok"' ` - -ProcedureMarker 'PROCEDURE Init' - Test-EditRoundtrip -RelPath 'Ferestre\fundal.scx' ` - -CaptionOld 'Caption = ""' ` - -CaptionNew 'Caption = "test"' ` - -ProcedureMarker 'PROCEDURE Show' + Write-Host "`n--- FAZA B: editare reala (write-back) ---" + foreach ($fx in $editFixtures) { + Test-EditRoundtrip -RelPath $fx.Rel -CaptionOld $fx.Old -CaptionNew $fx.New -ProcedureMarker $fx.Marker + } } if ($Phase -eq 'all' -or $Phase -eq 'smoke') { - Write-Host "`n--- FAZA C: smoke pe siblings ---" - if (Test-Path 'D:\ROA\ROAIMOB\Clase\onom_imob.vcx') { - Test-FidelityRoundtrip -RelPath 'Clase\onom_imob.vcx' -ProjRoot 'D:\ROA\ROAIMOB' - } else { AssertWarn $false "Faza C: onom_imob.vcx lipsa in ROAIMOB, sarit" } - if (Test-Path 'D:\ROA\ROAACNPRO\Clase\ofundal_facturare.vcx') { - Test-FidelityRoundtrip -RelPath 'Clase\ofundal_facturare.vcx' -ProjRoot 'D:\ROA\ROAACNPRO' - } else { AssertWarn $false "Faza C: ofundal_facturare.vcx lipsa in ROAACNPRO, sarit" } + Write-Host "`n--- FAZA C: smoke pe proiecte frate ---" + foreach ($sp in $SmokeProjects) { + if (-not (Test-Path -LiteralPath $sp)) { AssertWarn $false "Faza C: proiect lipsa $sp, sarit"; continue } + $s = Find-SampleBinary -ProjRoot $sp -Ext '.vcx' + if ($s) { Test-FidelityRoundtrip -RelPath (Get-RelPath $sp $s.FullName) -ProjRoot $sp } + else { AssertWarn $false "Faza C: niciun .vcx in $sp, sarit" } + } } if ($Phase -eq 'all' -or $Phase -eq 'negative') { Write-Host "`n--- FAZA D: negativ + guard-uri + multi-fisier ---" - & $vcx2txt -Source (Join-Path $ProjectRoot 'Clase\oavize.vcx') -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null - $cacheText = Join-Path $CacheRoot 'Clase\oavize.vc2' - $origBytes = [IO.File]::ReadAllBytes($cacheText) - $origVcxHash = Get-FileHashBytes (Join-Path $ProjectRoot 'Clase\oavize.vcx') - $origVctHash = Get-FileHashBytes (Join-Path $ProjectRoot 'Clase\oavize.vct') + $vcxSample = Find-SampleBinary -ProjRoot $ProjectRoot -Ext '.vcx' + if (-not $vcxSample) { + AssertWarn $false "Faza D: niciun .vcx in $ProjectRoot, faza sarita" + } else { + $vcxRel = Get-RelPath $ProjectRoot $vcxSample.FullName + $vcxFull = $vcxSample.FullName + $vcxDir = Split-Path $vcxRel -Parent + $vcxBase = [IO.Path]::GetFileNameWithoutExtension($vcxRel) + $vcxRelText = if ($vcxDir) { Join-Path $vcxDir ($vcxBase + '.vc2') } else { $vcxBase + '.vc2' } + Write-Host " esantion negativ .vcx: $vcxRel" + Backup-ProjectBinary $vcxFull + $origVcxHash = Get-FileHashBytes $vcxFull # baseline curent (independent de backup) pt asertii "neatins" - # (a) text stricat: sterge un ENDPROC - $work = Join-Path $env:TEMP ("fb2p_negD_{0}" -f ([Guid]::NewGuid().ToString('N').Substring(0,8))) - New-Item -ItemType Directory -Force -Path $work | Out-Null - $corruptText = Join-Path $work 'oavize.vc2' - $text = $script:Enc.GetString($origBytes) - $text2 = $text -replace "(?s)(PROCEDURE do_cauta_altele.*?)\r?\n\s*ENDPROC", '$1' - Assert ($text2 -ne $text) "Faza D (a): am reusit sa sterg un ENDPROC din textul de test" - [IO.File]::WriteAllBytes($corruptText, $script:Enc.GetBytes($text2)) - # txt2vcx cere ca fisierul sa fie sub CacheRoot -> copiem in cache temporar sub un nume separat - $corruptCacheText = Join-Path $CacheRoot 'Clase\_test_corrupt.vc2' - Copy-Item -LiteralPath $corruptText -Destination $corruptCacheText -Force - # simulam ca tinta e oavize (acelasi binar), redenumind local -- de fapt txt2vcx mapeaza dupa numele fisierului text, - # deci pentru acest test punem textul corupt chiar in oavize.vc2 din cache (e restaurat mai jos) - Copy-Item -LiteralPath $corruptText -Destination $cacheText -Force - Remove-Item -LiteralPath $corruptCacheText -Force -ErrorAction SilentlyContinue - & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot - $rcA = $LASTEXITCODE - Assert ($rcA -ne 0) "Faza D (a): txt2vcx.ps1 exit != 0 pe text stricat (ENDPROC lipsa)" - Assert ((Get-FileHashBytes (Join-Path $ProjectRoot 'Clase\oavize.vcx')) -eq $origVcxHash) "Faza D (a): oavize.vcx neatins in proiect" - Assert ((Get-FileHashBytes (Join-Path $ProjectRoot 'Clase\oavize.vct')) -eq $origVctHash) "Faza D (a): oavize.vct neatins in proiect" - [IO.File]::WriteAllBytes($cacheText, $origBytes) + & $vcx2txt -Source $vcxFull -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null + $cacheText = Join-Path $CacheRoot $vcxRelText + Assert (Test-Path -LiteralPath $cacheText) "Faza D: text esantion generat pentru $vcxRel" + $origBytes = [IO.File]::ReadAllBytes($cacheText) - # (b) fisier text gol (0 bytes) - [IO.File]::WriteAllBytes($cacheText, [byte[]]@()) - & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot - $rcB = $LASTEXITCODE - Assert ($rcB -ne 0) "Faza D (b): txt2vcx.ps1 exit != 0 pe fisier text gol" - Assert ((Get-FileHashBytes (Join-Path $ProjectRoot 'Clase\oavize.vcx')) -eq $origVcxHash) "Faza D (b): oavize.vcx neatins in proiect" - [IO.File]::WriteAllBytes($cacheText, $origBytes) - - # --- Test guard-uri --- - # staleness: dam mtime-ul binarului in viitor -> refuz fara -Force - $vcxPath = Join-Path $ProjectRoot 'Clase\oavize.vcx' - $origMtime = (Get-Item -LiteralPath $vcxPath).LastWriteTime - (Get-Item -LiteralPath $vcxPath).LastWriteTime = (Get-Date).AddDays(1) - try { - [IO.File]::WriteAllBytes($cacheText, $origBytes) # text neschimbat, dar staleness trebuie sa refuze oricum + # (a) text stricat: adauga o clasa neinchisa -> recompilare esueaza (doar in cache temp) + $text = $script:Enc.GetString($origBytes) + $brokenSuffix = "`r`nDEFINE CLASS _brokenzzz AS Custom`r`n" + [IO.File]::WriteAllBytes($cacheText, $script:Enc.GetBytes($text + $brokenSuffix)) & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot - $rcStale = $LASTEXITCODE - Assert ($rcStale -ne 0) "Faza D (guard staleness): refuz fara -Force cand binarul e mai nou decat cache-ul" - Assert ((Get-FileHashBytes $vcxPath) -eq $origVcxHash) "Faza D (guard staleness): oavize.vcx neatins" - } finally { - (Get-Item -LiteralPath $vcxPath).LastWriteTime = $origMtime + Assert ($LASTEXITCODE -ne 0) "Faza D (a): txt2vcx exit != 0 pe text stricat (DEFINE CLASS neinchis)" + Assert ((Get-FileHashBytes $vcxFull) -eq $origVcxHash) "Faza D (a): binarul esantion neatins in proiect" + [IO.File]::WriteAllBytes($cacheText, $origBytes) + + # (b) fisier text gol + [IO.File]::WriteAllBytes($cacheText, [byte[]]@()) + & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot + Assert ($LASTEXITCODE -ne 0) "Faza D (b): txt2vcx exit != 0 pe fisier text gol" + Assert ((Get-FileHashBytes $vcxFull) -eq $origVcxHash) "Faza D (b): binarul esantion neatins in proiect" + [IO.File]::WriteAllBytes($cacheText, $origBytes) + + # (guard) staleness: binar mai nou decat textul -> refuz fara -Force + $origMtime = (Get-Item -LiteralPath $vcxFull).LastWriteTime + (Get-Item -LiteralPath $vcxFull).LastWriteTime = (Get-Date).AddDays(1) + try { + [IO.File]::WriteAllBytes($cacheText, $origBytes) + & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot + Assert ($LASTEXITCODE -ne 0) "Faza D (guard staleness): refuz fara -Force cand binarul e mai nou" + Assert ((Get-FileHashBytes $vcxFull) -eq $origVcxHash) "Faza D (guard staleness): binar neatins" + } finally { + (Get-Item -LiteralPath $vcxFull).LastWriteTime = $origMtime + } + + # (guard) binar tinta lipsa -> refuz PERMANENT (si cu -Force) + $missingCacheText = Join-Path $CacheRoot $(if ($vcxDir) { Join-Path $vcxDir '_nu_exista_deloc.vc2' } else { '_nu_exista_deloc.vc2' }) + New-Item -ItemType Directory -Force -Path (Split-Path $missingCacheText -Parent) | Out-Null + [IO.File]::WriteAllBytes($missingCacheText, $script:Enc.GetBytes("* fake`r`nDEFINE CLASS x AS y`r`nENDDEFINE`r`n")) + try { + & $txt2vcx -TextFile $missingCacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force + Assert ($LASTEXITCODE -ne 0) "Faza D (guard binar lipsa): refuz chiar si cu -Force" + } finally { Remove-Item -LiteralPath $missingCacheText -Force -ErrorAction SilentlyContinue } + + # (guard) cale text in afara CacheRoot -> refuz + $outsideText = Join-Path $env:TEMP ('_outside_' + [Guid]::NewGuid().ToString('N').Substring(0,8) + '.vc2') + [IO.File]::WriteAllBytes($outsideText, $origBytes) + try { + & $txt2vcx -TextFile $outsideText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot + Assert ($LASTEXITCODE -ne 0) "Faza D (guard cale in afara CacheRoot): refuz" + } finally { Remove-Item -LiteralPath $outsideText -Force -ErrorAction SilentlyContinue } + + # (guard) extensie nesuportata (.mn2) + $unsupported = Join-Path $CacheRoot $(if ($vcxDir) { Join-Path $vcxDir '_fake.mn2' } else { '_fake.mn2' }) + [IO.File]::WriteAllBytes($unsupported, $script:Enc.GetBytes("* fake mn2`r`n")) + try { + & $txt2vcx -TextFile $unsupported -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot + Assert ($LASTEXITCODE -ne 0) "Faza D (guard extensie nesuportata .mn2): refuz" + } finally { Remove-Item -LiteralPath $unsupported -Force -ErrorAction SilentlyContinue } + + # (guard) COMUN fara -AllowComun -> refuz (-DryRun, zero scrieri) + $comunText = Join-Path $CacheRoot 'COMUN\clase\_fake.vc2' + New-Item -ItemType Directory -Force -Path (Split-Path $comunText -Parent) | Out-Null + [IO.File]::WriteAllBytes($comunText, $origBytes) + try { + & $txt2vcx -TextFile $comunText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -DryRun + Assert ($LASTEXITCODE -ne 0) "Faza D (guard COMUN, fara -AllowComun): refuz" + } finally { Remove-Item -Recurse -Force -LiteralPath (Split-Path $comunText -Parent) -ErrorAction SilentlyContinue } + + # (guard) folder-foxbin2prg -> refuz necondiționat (-DryRun) + $fb2pCache = Join-Path $env:TEMP ('_fb2pguard_' + [Guid]::NewGuid().ToString('N').Substring(0,8)) + New-Item -ItemType Directory -Force -Path $fb2pCache | Out-Null + $fb2pText = Join-Path $fb2pCache 'fake.vc2' + [IO.File]::WriteAllBytes($fb2pText, $origBytes) + try { + & $txt2vcx -TextFile $fb2pText -ProjectRoot 'D:\ROA\UTIL\foxbin2prg' -CacheRoot $fb2pCache -DryRun + Assert ($LASTEXITCODE -ne 0) "Faza D (guard folder-foxbin2prg): refuz necondiționat" + } finally { Remove-Item -Recurse -Force -LiteralPath $fb2pCache -ErrorAction SilentlyContinue } + + # (-DryRun caz valid): nimic modificat in proiect, exit 0 + [IO.File]::WriteAllBytes($cacheText, $origBytes) + & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -DryRun + Assert ($LASTEXITCODE -eq 0) "Faza D (-DryRun, caz valid): exit 0" + Assert ((Get-FileHashBytes $vcxFull) -eq $origVcxHash) "Faza D (-DryRun, caz valid): binar neatins in proiect" + + # --- multi-fisier: un scx valid (scris) + vcx stricat (refuzat) in aceeasi rulare --- + $scxSample = Find-SampleBinary -ProjRoot $ProjectRoot -Ext '.scx' + if ($scxSample) { + $scxRel = Get-RelPath $ProjectRoot $scxSample.FullName + $scxFull = $scxSample.FullName + $scxDir = Split-Path $scxRel -Parent + $scxBase = [IO.Path]::GetFileNameWithoutExtension($scxRel) + $scxRelText = if ($scxDir) { Join-Path $scxDir ($scxBase + '.sc2') } else { $scxBase + '.sc2' } + Backup-ProjectBinary $scxFull + $scxOrigMtime = (Get-Item -LiteralPath $scxFull).LastWriteTime + + & $vcx2txt -Source $scxFull -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null + $scxCacheText = Join-Path $CacheRoot $scxRelText + $scxText = $script:Enc.GetString([IO.File]::ReadAllBytes($scxCacheText)) + $m = [regex]::Match($scxText, "(?m)^\tPROCEDURE \w+\r?\n") + if ($m.Success) { + $pos = $m.Index + $m.Length + $scxText = $scxText.Substring(0,$pos) + "`t`t* test multi`r`n" + $scxText.Substring($pos) + [IO.File]::WriteAllBytes($scxCacheText, $script:Enc.GetBytes($scxText)) + } + [IO.File]::WriteAllBytes($cacheText, $script:Enc.GetBytes($text + $brokenSuffix)) + + & $txt2vcx -TextFile @($scxCacheText, $cacheText) -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot + Assert ($LASTEXITCODE -eq 1) "Faza D (multi-fisier): exit agregat = 1 (unul din doua a esuat)" + Assert ((Get-FileHashBytes $vcxFull) -eq $origVcxHash) "Faza D (multi-fisier): vcx stricat neatins in proiect" + Assert (((Get-Item -LiteralPath $scxFull).LastWriteTime) -ne $scxOrigMtime) "Faza D (multi-fisier): scx valid a fost scris in proiect (mtime schimbat)" + [IO.File]::WriteAllBytes($cacheText, $origBytes) + } else { + AssertWarn $false "Faza D (multi-fisier): niciun .scx in $ProjectRoot, sarit" + } } - - # binar tinta lipsa -> refuz PERMANENT (si cu -Force) - $missingCacheText = Join-Path $CacheRoot 'Clase\_nu_exista_deloc.vc2' - [IO.File]::WriteAllBytes($missingCacheText, $script:Enc.GetBytes("* fake`r`nDEFINE CLASS x AS y`r`nENDDEFINE`r`n")) - try { - & $txt2vcx -TextFile $missingCacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force - $rcMissing = $LASTEXITCODE - Assert ($rcMissing -ne 0) "Faza D (guard binar lipsa): refuz chiar si cu -Force" - } finally { - Remove-Item -LiteralPath $missingCacheText -Force -ErrorAction SilentlyContinue - } - - # cale text in afara CacheRoot -> eroare dura - $outsideText = Join-Path $env:TEMP 'oavize_outside.vc2' - [IO.File]::WriteAllBytes($outsideText, $origBytes) - try { - & $txt2vcx -TextFile $outsideText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot - $rcOutside = $LASTEXITCODE - Assert ($rcOutside -ne 0) "Faza D (guard cale in afara CacheRoot): refuz" - } finally { Remove-Item -LiteralPath $outsideText -Force -ErrorAction SilentlyContinue } - - # extensie nesuportata (.mn2) - $unsupported = Join-Path $CacheRoot 'Clase\_fake.mn2' - [IO.File]::WriteAllBytes($unsupported, $script:Enc.GetBytes("* fake mn2`r`n")) - try { - & $txt2vcx -TextFile $unsupported -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot - $rcUnsup = $LASTEXITCODE - Assert ($rcUnsup -ne 0) "Faza D (guard extensie nesuportata .mn2): refuz" - } finally { Remove-Item -LiteralPath $unsupported -Force -ErrorAction SilentlyContinue } - - # guard COMUN (cu -DryRun, refuz inainte de staging, zero scrieri) - $comunCandidate = Get-ChildItem -Path (Join-Path $CacheRoot 'comun\clase') -Filter '*.vc2' -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($comunCandidate) { - & $txt2vcx -TextFile $comunCandidate.FullName -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -DryRun - $rcComun = $LASTEXITCODE - Assert ($rcComun -ne 0) "Faza D (guard COMUN, fara -AllowComun): refuz" - } else { AssertWarn $false "Faza D (guard COMUN): niciun candidat .vc2 gasit sub cache\comun\clase, sarit" } - - # guard folder-foxbin2prg (cu -DryRun) - $fb2pFakeCache = Join-Path $CacheRoot 'UTIL_foxbin2prg_fake' - New-Item -ItemType Directory -Force -Path $fb2pFakeCache | Out-Null - $fb2pFakeText = Join-Path $fb2pFakeCache 'fake.vc2' - [IO.File]::WriteAllBytes($fb2pFakeText, $origBytes) - try { - & $txt2vcx -TextFile $fb2pFakeText -ProjectRoot 'D:\ROA\UTIL\foxbin2prg' -CacheRoot $fb2pFakeCache -DryRun - $rcFb2p = $LASTEXITCODE - Assert ($rcFb2p -ne 0) "Faza D (guard folder-foxbin2prg): refuz necondiționat" - } finally { Remove-Item -Recurse -Force -LiteralPath $fb2pFakeCache -ErrorAction SilentlyContinue } - - # -DryRun pe caz valid: nimic modificat in proiect, exit reflecta staging-ul (0 = OK) - [IO.File]::WriteAllBytes($cacheText, $origBytes) - & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -DryRun - $rcDry = $LASTEXITCODE - Assert ($rcDry -eq 0) "Faza D (-DryRun, caz valid): exit 0" - Assert ((Get-FileHashBytes $vcxPath) -eq $origVcxHash) "Faza D (-DryRun, caz valid): oavize.vcx neatins in proiect" - - # --- Test multi-fisier: un .vc2 valid (fundal.scx) + unul stricat (oavize) in aceeasi rulare --- - & $vcx2txt -Source (Join-Path $ProjectRoot 'Ferestre\fundal.scx') -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null - $fundalCacheText = Join-Path $CacheRoot 'Ferestre\fundal.sc2' - $fundalOrigBytes = [IO.File]::ReadAllBytes($fundalCacheText) - $fundalOrigVcxHash = Get-FileHashBytes (Join-Path $ProjectRoot 'Ferestre\fundal.scx') - $fundalText = $script:Enc.GetString($fundalOrigBytes).Replace('Caption = ""', 'Caption = "multi-test"') - [IO.File]::WriteAllBytes($fundalCacheText, $script:Enc.GetBytes($fundalText)) - - $corruptOavizeText = $script:Enc.GetString($origBytes) -replace "(?s)(PROCEDURE do_cauta_altele.*?)\r?\n\s*ENDPROC", '$1' - [IO.File]::WriteAllBytes($cacheText, $script:Enc.GetBytes($corruptOavizeText)) - - $script:TouchedProjectFiles.Add('Ferestre\fundal.scx') | Out-Null - $script:TouchedProjectFiles.Add('Ferestre\fundal.SCT') | Out-Null - - & $txt2vcx -TextFile @($fundalCacheText, $cacheText) -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot - $rcMulti = $LASTEXITCODE - Assert ($rcMulti -eq 1) "Faza D (multi-fisier): exit code agregat = 1 (unul din doua a esuat)" - Assert ((Get-FileHashBytes $vcxPath) -eq $origVcxHash) "Faza D (multi-fisier): oavize.vcx (cel stricat) neatins in proiect" - $fundalVcxHash = Get-FileHashBytes (Join-Path $ProjectRoot 'Ferestre\fundal.scx') - Assert ($fundalVcxHash -ne $fundalOrigVcxHash) "Faza D (multi-fisier): fundal.scx (cel valid) A FOST scris in proiect" - - # cleanup multi-fisier - Push-Location $ProjectRoot - try { git checkout -- 'Ferestre\fundal.scx' 'Ferestre\fundal.SCT' } finally { Pop-Location } - [IO.File]::WriteAllBytes($cacheText, $origBytes) - & $vcx2txt -Source (Join-Path $ProjectRoot 'Clase\oavize.vcx') -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null - & $vcx2txt -Source (Join-Path $ProjectRoot 'Ferestre\fundal.scx') -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null - Remove-Item -Recurse -Force -LiteralPath $work -ErrorAction SilentlyContinue } } finally { - Write-Host "`n--- Cleanup final: git checkout -- pe fisierele atinse + refresh cache ---" - Push-Location $ProjectRoot - try { - $toRevert = $script:TouchedProjectFiles | Sort-Object -Unique - foreach ($f in $toRevert) { - $status = git status --porcelain -- $f - if ($status) { git checkout -- $f 2>$null } - } - } finally { Pop-Location } - & $vcx2txt -Source (Join-Path $ProjectRoot 'Clase\oavize.vcx') -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null - & $vcx2txt -Source (Join-Path $ProjectRoot 'Ferestre\fundal.scx') -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null + Write-Host "`n--- Cleanup final: restore binare din backup + curatare temp ---" + Restore-ProjectBinaries + if ($ownCache -and (Test-Path -LiteralPath $CacheRoot)) { Remove-Item -Recurse -Force -LiteralPath $CacheRoot -ErrorAction SilentlyContinue } + if (Test-Path -LiteralPath $script:BackupRoot) { Remove-Item -Recurse -Force -LiteralPath $script:BackupRoot -ErrorAction SilentlyContinue } } Write-Host "`n=== REZULTAT ===" diff --git a/txt2vcx.ps1 b/txt2vcx.ps1 index 26f0031..789f13c 100644 --- a/txt2vcx.ps1 +++ b/txt2vcx.ps1 @@ -32,7 +32,9 @@ Radacina proiectului tinta (implicit D:\ROA\ROAACNPRO, ca vcx2txt.ps1). .PARAMETER CacheRoot - Radacina cache-ului text (implicit D:\ROA\_vfp_textcache\roacnpro). + Radacina versiunilor text. Implicit egala cu ProjectRoot (flux text in-arbore, + langa binare): maparea cache->proiect devine identitate, iar refresh-ul textului + e in-place. Da o radacina separata pentru fluxul vechi cu cache extern. .PARAMETER AllowComun Obligatoriu pentru a scrie tinte aflate sub COMUN\ (repo partajat intre toate @@ -59,7 +61,7 @@ param( [Parameter(Mandatory)][string[]]$TextFile, [string]$ProjectRoot = 'D:\ROA\ROAACNPRO', - [string]$CacheRoot = 'D:\ROA\_vfp_textcache\roacnpro', + [string]$CacheRoot = $ProjectRoot, [switch]$AllowComun, [switch]$Force, [switch]$NoVerify, @@ -217,6 +219,8 @@ foreach ($tf in $TextFile) { # --- 3. Regenerare in staging --- $stagingDir = Join-Path $stagingRoot $relBinNoExt New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null + # cfg in staging: DontShowErrors suprima MessageBox-ul propriu la write-back esuat (mostenit de verify/refresh) + Set-Content -LiteralPath (Join-Path $stagingDir 'foxbin2prg.cfg') -Value @('DontShowErrors: 1','ShowProgressbar: 0') -Encoding Ascii $stagingText = Join-Path $stagingDir ($binBaseName + $textExt) Copy-Item -LiteralPath $fullTextPath -Destination $stagingText -Force diff --git a/vfp_filemap.ps1 b/vfp_filemap.ps1 index 3e5f8b7..d27a08c 100644 --- a/vfp_filemap.ps1 +++ b/vfp_filemap.ps1 @@ -11,6 +11,7 @@ $script:VfpFileMap = @{ '.vcx' = @('.vct', '.vc2'); '.scx' = @('.sct', '.sc2'); '.frx' = @('.frt', '.fr2') '.lbx' = @('.lbt', '.lb2'); '.mnx' = @('.mnt', '.mn2'); '.dbc' = @('.dct', '.dc2') + '.pjx' = @('.pjt', '.pj2'); '.dbf' = @('.fpt', '.db2') } function Get-VfpBinExtensionForTextExtension {