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 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 11:04:23 +03:00
parent 16f7d75363
commit a05687a38f
6 changed files with 872 additions and 239 deletions

View File

@@ -20,18 +20,21 @@ It serves two purposes:
## Local additions (ROA-specific, not part of upstream) ## 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`). - **`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.
- **`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). - **`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.
- **`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. - **`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.
- **`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. - **`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`).
- **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. - **`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. - **`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: **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. - 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. - 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. - 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. - **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. - **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.

313
git_sync.ps1 Normal file
View File

@@ -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

223
test_git_sync.ps1 Normal file
View File

@@ -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_<leaf>, 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
}

View File

@@ -1,40 +1,49 @@
<# <#
.SYNOPSIS .SYNOPSIS
Teste standalone (fara Pester) pentru fluxul bin<->text VFP (FoxBin2Prg) si scriptul 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 .DESCRIPTION
Faze: Faze:
- fidelity : roundtrip pur bin->text1->bin->text2 (doar in temp, fara scriere in proiect), - fidelity : esantion din FIECARE tip convertit, auto-descoperit din arborele proiectului.
pentru .vcx/.scx (blocant) si .mnx/.frx (warn-only - read-only in flux). vcx/scx -> roundtrip complet bin->text1->bin->text2 (comparare pe octeti);
- edit : editare reala pe copie de proiect (ROAGEST), via txt2vcx.ps1, cu revert git. 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 - negative : text stricat, fisier gol, guard-uri (staleness, binar lipsa, cale in afara
cache-ului, extensie nesuportata, COMUN, folder-foxbin2prg, -DryRun), cache-ului, extensie nesuportata, COMUN, folder-foxbin2prg, -DryRun),
test multi-fisier (unul valid + unul stricat in aceeasi rulare). test multi-fisier. Textul stricat ruleaza DOAR in cache temp, niciodata in arbore.
- smoke : Faza A (fidelity) pe cate un .vcx mic non-COMUN din ROAIMOB si ROAACNPRO. - smoke : fidelity pe cate un .vcx din proiectele frate (-SmokeProjects).
- all : toate cele de mai sus, in ordine. - all : toate cele de mai sus, in ordine.
Precondita: git status --porcelain trebuie sa fie gol PENTRU FISIERELE TINTA (nu tot Restaurare: orice binar atins de write-back e copiat in backup temp INAINTE si restaurat
repo-ul) inainte de a incepe. La final (in finally), git checkout -- pe orice binar la final (in finally), cu asertie byte-identic fata de original. Cache-ul text implicit e
atins + refresh cache, indiferent de rezultat. 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 .PARAMETER ProjectRoot
Radacina proiectului principal testat (implicit ROAGEST). Radacina proiectului principal testat (implicit ROAGEST).
.PARAMETER CacheRoot .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 .PARAMETER Phase
all | fidelity | edit | negative | smoke all | fidelity | edit | negative | smoke
.PARAMETER SmokeProjects
Lista proiectelor frate pentru faza smoke.
#> #>
[CmdletBinding()] [CmdletBinding()]
param( param(
[string]$ProjectRoot = 'D:\ROA\ROAGEST', [string]$ProjectRoot = 'D:\ROA\ROAGEST',
[string]$CacheRoot = 'D:\ROA\_vfp_textcache\roagest', [string]$CacheRoot = '',
[ValidateSet('all','fidelity','edit','negative','smoke')] [ValidateSet('all','fidelity','edit','negative','smoke')]
[string]$Phase = 'all' [string]$Phase = 'all',
[string[]]$SmokeProjects = @('D:\ROA\ROAIMOB','D:\ROA\ROAACNPRO')
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
@@ -45,7 +54,8 @@ $txt2vcx = 'D:\ROA\UTIL\foxbin2prg\txt2vcx.ps1'
$script:Failures = @() $script:Failures = @()
$script:Enc = [Text.Encoding]::GetEncoding(1252) $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) { function Assert([bool]$cond, [string]$msg) {
if ($cond) { Write-Host " PASS: $msg" -ForegroundColor Green } if ($cond) { Write-Host " PASS: $msg" -ForegroundColor Green }
@@ -76,10 +86,13 @@ function Test-BytesEqual([byte[]]$a, [byte[]]$b) {
} }
function Invoke-Fb2p { function Invoke-Fb2p {
# Watchdog obligatoriu - vezi nota din txt2vcx.ps1: FoxBin2Prg poate afisa un # Watchdog obligatoriu - FoxBin2Prg poate afisa un MessageBox modal nativ VFP la text stricat;
# MessageBox modal nativ VFP (necontrolat de cDontShowErrors) la text stricat; # fara asta, faza negativa ar ramane agatata la infinit intr-o rulare automata.
# fara asta, Faza D (negativ) ar putea ramane agatata la infinit intr-o rulare automata.
param([string]$InputFile, [string]$Recompile = '', [int]$TimeoutSeconds = 60) 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) { if ($Recompile) {
$r = Invoke-FoxBin2PrgSafe -ExePath $exe -ExeArgs @($InputFile,'','','','1','0','1','',$Recompile) -TimeoutSeconds $TimeoutSeconds $r = Invoke-FoxBin2PrgSafe -ExePath $exe -ExeArgs @($InputFile,'','','','1','0','1','',$Recompile) -TimeoutSeconds $TimeoutSeconds
} else { } else {
@@ -94,21 +107,73 @@ function Get-FileHashBytes([string]$path) {
return (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash 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 { function Test-FidelityRoundtrip {
param( param(
[Parameter(Mandatory)][string]$RelPath, # relativ la $ProjRoot, ex Clase\oavize.vcx [Parameter(Mandatory)][string]$RelPath,
[Parameter(Mandatory)][string]$ProjRoot, [Parameter(Mandatory)][string]$ProjRoot,
[bool]$WarnOnly = $false [bool]$WarnOnly = $false
) )
$full = Join-Path $ProjRoot $RelPath $full = Join-Path $ProjRoot $RelPath
if (-not (Test-Path -LiteralPath $full)) { if (-not (Test-Path -LiteralPath $full)) { AssertWarn $false "Faza A: fisier lipsa, sarit: $RelPath"; return }
AssertWarn $false "Faza A: fisier lipsa, sarit: $RelPath"
return
}
$ext = [IO.Path]::GetExtension($full).ToLower() $ext = [IO.Path]::GetExtension($full).ToLower()
$pair = $null $pair = $script:VfpFileMap[$ext]
foreach ($k in $script:VfpFileMap.Keys) { if ($k -eq $ext) { $pair = $script:VfpFileMap[$k]; break } }
if (-not $pair) { AssertWarn $false "Faza A: extensie necunoscuta, sarit: $RelPath"; return } if (-not $pair) { AssertWarn $false "Faza A: extensie necunoscuta, sarit: $RelPath"; return }
$memoExt = $pair[0]; $txtExt = $pair[1] $memoExt = $pair[0]; $txtExt = $pair[1]
$memoFull = [IO.Path]::ChangeExtension($full, $memoExt) $memoFull = [IO.Path]::ChangeExtension($full, $memoExt)
@@ -123,8 +188,7 @@ function Test-FidelityRoundtrip {
$rc1 = Invoke-Fb2p -InputFile $bin1 $rc1 = Invoke-Fb2p -InputFile $bin1
$text1 = Join-Path $work ($baseName + $txtExt) $text1 = Join-Path $work ($baseName + $txtExt)
if ($rc1 -ne 0 -or -not (Test-Path -LiteralPath $text1)) { if ($rc1 -ne 0 -or -not (Test-Path -LiteralPath $text1)) {
AssertWarn (-not $WarnOnly) "Faza A ($RelPath): Bin2Prg #1 a esuat (ErrorLevel=$rc1)" if ($WarnOnly) { AssertWarn $false "Faza A ($RelPath): Bin2Prg #1 a esuat (ErrorLevel=$rc1)"; return }
if ($WarnOnly) { return }
Assert $false "Faza A ($RelPath): Bin2Prg #1 a esuat (ErrorLevel=$rc1)" Assert $false "Faza A ($RelPath): Bin2Prg #1 a esuat (ErrorLevel=$rc1)"
return 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 { function Test-EditRoundtrip {
param( param(
[Parameter(Mandatory)][string]$RelPath, # ex Clase\oavize.vcx [Parameter(Mandatory)][string]$RelPath,
[Parameter(Mandatory)][string]$CaptionOld, [Parameter(Mandatory)][string]$CaptionOld,
[Parameter(Mandatory)][string]$CaptionNew, [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() $ext = [IO.Path]::GetExtension($RelPath).ToLower()
$pair = $script:VfpFileMap[$ext] $pair = $script:VfpFileMap[$ext]
$memoExt = $pair[0]; $txtExt = $pair[1] $txtExt = $pair[1]
$relDir = Split-Path $RelPath -Parent $relDir = Split-Path $RelPath -Parent
$baseName = [IO.Path]::GetFileNameWithoutExtension($RelPath) $base = [IO.Path]::GetFileNameWithoutExtension($RelPath)
# rezolva casing-ul REAL al memo-ului din proiect (poate fi .SCT/.VCT majuscul, vezi fundal.SCT) $relText = if ($relDir) { Join-Path $relDir ($base + $txtExt) } else { $base + $txtExt }
$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)
Write-Host "-- Faza B: $RelPath --" 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 $cacheText = Join-Path $CacheRoot $relText
Assert (Test-Path -LiteralPath $cacheText) "Faza B ($RelPath): cache text regenerat" Assert (Test-Path -LiteralPath $cacheText) "Faza B ($RelPath): cache text regenerat"
$bytes = [IO.File]::ReadAllBytes($cacheText) $text = $script:Enc.GetString([IO.File]::ReadAllBytes($cacheText))
$text = $script:Enc.GetString($bytes)
Assert ($text.Contains($CaptionOld)) "Faza B ($RelPath): linia Caption originala gasita" Assert ($text.Contains($CaptionOld)) "Faza B ($RelPath): linia Caption originala gasita"
$text = $text.Replace($CaptionOld, $CaptionNew) $text = $text.Replace($CaptionOld, $CaptionNew)
$marker = "`t" + $ProcedureMarker + "`r`n" $marker = "`t" + $ProcedureMarker + "`r`n"
$idx = $text.IndexOf($marker) $idx = $text.IndexOf($marker)
Assert ($idx -ge 0) "Faza B ($RelPath): marker '$ProcedureMarker' gasit" Assert ($idx -ge 0) "Faza B ($RelPath): marker '$ProcedureMarker' gasit"
if ($idx -ge 0) {
$insertPos = $idx + $marker.Length $insertPos = $idx + $marker.Length
$text = $text.Substring(0,$insertPos) + "`t`t* test roundtrip`r`n" + $text.Substring($insertPos) $text = $text.Substring(0,$insertPos) + "`t`t* test roundtrip`r`n" + $text.Substring($insertPos)
}
[IO.File]::WriteAllBytes($cacheText, $script:Enc.GetBytes($text)) [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 & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot
$rc = $LASTEXITCODE $rc = $LASTEXITCODE
Assert ($rc -eq 0) "Faza B ($RelPath): txt2vcx.ps1 exit 0" Assert ($rc -eq 0) "Faza B ($RelPath): txt2vcx.ps1 exit 0"
Push-Location $ProjectRoot Assert ((Get-FileHashBytes $full) -ne $origHash) "Faza B ($RelPath): binarul din proiect a fost rescris (hash diferit de original)"
try { $bakStray = Join-Path (Join-Path $ProjectRoot $relDir) ($base + '.bak')
$porcelain = (git status --porcelain -- $RelPath $relMemo) -join "`n" Assert (-not (Test-Path -LiteralPath $bakStray)) "Faza B ($RelPath): fara .bak lasat in proiect"
$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 }
$regenText = $script:Enc.GetString([IO.File]::ReadAllBytes($cacheText)) $regenText = $script:Enc.GetString([IO.File]::ReadAllBytes($cacheText))
Assert ($regenText.Contains($CaptionNew)) "Faza B ($RelPath): textul regenerat contine Caption editat" 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" Assert ($regenText.Contains('* test roundtrip')) "Faza B ($RelPath): textul regenerat contine comentariul adaugat"
# binarul e restaurat din backup in finally-ul global
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
} }
# ============================== MAIN ============================== # ============================== 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 ===" Write-Host "=== test_roundtrip.ps1 -- ProjectRoot=$ProjectRoot CacheRoot=$CacheRoot Phase=$Phase ==="
# Precondita: git status curat pentru fisierele tinta cunoscute dinainte # fixture-uri de editare cunoscute (implicit ROAGEST); se sar automat daca lipsesc in proiect
$knownTargets = @('Clase\oavize.vcx','Clase\oavize.vct','Ferestre\fundal.scx','Ferestre\fundal.SCT') $editFixtures = @(
Push-Location $ProjectRoot @{ Rel='Clase\oavize.vcx'; Old='Lb_titlu_alb_b121.Caption = "Date aviz"'; New='Lb_titlu_alb_b121.Caption = "Atentie-diacritice-ok"'; Marker='PROCEDURE Init' }
try { @{ Rel='Ferestre\fundal.scx'; Old='Caption = ""'; New='Caption = "test"'; Marker='PROCEDURE Show' }
$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 }
try { try {
if ($Phase -eq 'all' -or $Phase -eq 'fidelity') { if ($Phase -eq 'all' -or $Phase -eq 'fidelity') {
Write-Host "`n--- FAZA A: fidelitate (roundtrip pur) ---" Write-Host "`n--- FAZA A: fidelitate (esantion din fiecare tip convertit) ---"
Test-FidelityRoundtrip -RelPath 'Clase\oavize.vcx' -ProjRoot $ProjectRoot # inlocuieste atentie.vcx (gol, fara clase reale - vezi deviere raportata) Invoke-FidelityForProject $ProjectRoot
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
} }
if ($Phase -eq 'all' -or $Phase -eq 'edit') { if ($Phase -eq 'all' -or $Phase -eq 'edit') {
Write-Host "`n--- FAZA B: editare reala (ROAGEST) ---" Write-Host "`n--- FAZA B: editare reala (write-back) ---"
Test-EditRoundtrip -RelPath 'Clase\oavize.vcx' ` foreach ($fx in $editFixtures) {
-CaptionOld 'Lb_titlu_alb_b121.Caption = "Date aviz"' ` Test-EditRoundtrip -RelPath $fx.Rel -CaptionOld $fx.Old -CaptionNew $fx.New -ProcedureMarker $fx.Marker
-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'
} }
if ($Phase -eq 'all' -or $Phase -eq 'smoke') { if ($Phase -eq 'all' -or $Phase -eq 'smoke') {
Write-Host "`n--- FAZA C: smoke pe siblings ---" Write-Host "`n--- FAZA C: smoke pe proiecte frate ---"
if (Test-Path 'D:\ROA\ROAIMOB\Clase\onom_imob.vcx') { foreach ($sp in $SmokeProjects) {
Test-FidelityRoundtrip -RelPath 'Clase\onom_imob.vcx' -ProjRoot 'D:\ROA\ROAIMOB' if (-not (Test-Path -LiteralPath $sp)) { AssertWarn $false "Faza C: proiect lipsa $sp, sarit"; continue }
} else { AssertWarn $false "Faza C: onom_imob.vcx lipsa in ROAIMOB, sarit" } $s = Find-SampleBinary -ProjRoot $sp -Ext '.vcx'
if (Test-Path 'D:\ROA\ROAACNPRO\Clase\ofundal_facturare.vcx') { if ($s) { Test-FidelityRoundtrip -RelPath (Get-RelPath $sp $s.FullName) -ProjRoot $sp }
Test-FidelityRoundtrip -RelPath 'Clase\ofundal_facturare.vcx' -ProjRoot 'D:\ROA\ROAACNPRO' else { AssertWarn $false "Faza C: niciun .vcx in $sp, sarit" }
} else { AssertWarn $false "Faza C: ofundal_facturare.vcx lipsa in ROAACNPRO, sarit" } }
} }
if ($Phase -eq 'all' -or $Phase -eq 'negative') { if ($Phase -eq 'all' -or $Phase -eq 'negative') {
Write-Host "`n--- FAZA D: negativ + guard-uri + multi-fisier ---" 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 $vcxSample = Find-SampleBinary -ProjRoot $ProjectRoot -Ext '.vcx'
$cacheText = Join-Path $CacheRoot 'Clase\oavize.vc2' if (-not $vcxSample) {
$origBytes = [IO.File]::ReadAllBytes($cacheText) AssertWarn $false "Faza D: niciun .vcx in $ProjectRoot, faza sarita"
$origVcxHash = Get-FileHashBytes (Join-Path $ProjectRoot 'Clase\oavize.vcx') } else {
$origVctHash = Get-FileHashBytes (Join-Path $ProjectRoot 'Clase\oavize.vct') $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 & $vcx2txt -Source $vcxFull -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null
$work = Join-Path $env:TEMP ("fb2p_negD_{0}" -f ([Guid]::NewGuid().ToString('N').Substring(0,8))) $cacheText = Join-Path $CacheRoot $vcxRelText
New-Item -ItemType Directory -Force -Path $work | Out-Null Assert (Test-Path -LiteralPath $cacheText) "Faza D: text esantion generat pentru $vcxRel"
$corruptText = Join-Path $work 'oavize.vc2' $origBytes = [IO.File]::ReadAllBytes($cacheText)
# (a) text stricat: adauga o clasa neinchisa -> recompilare esueaza (doar in cache temp)
$text = $script:Enc.GetString($origBytes) $text = $script:Enc.GetString($origBytes)
$text2 = $text -replace "(?s)(PROCEDURE do_cauta_altele.*?)\r?\n\s*ENDPROC", '$1' $brokenSuffix = "`r`nDEFINE CLASS _brokenzzz AS Custom`r`n"
Assert ($text2 -ne $text) "Faza D (a): am reusit sa sterg un ENDPROC din textul de test" [IO.File]::WriteAllBytes($cacheText, $script:Enc.GetBytes($text + $brokenSuffix))
[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 & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot
$rcA = $LASTEXITCODE Assert ($LASTEXITCODE -ne 0) "Faza D (a): txt2vcx exit != 0 pe text stricat (DEFINE CLASS neinchis)"
Assert ($rcA -ne 0) "Faza D (a): txt2vcx.ps1 exit != 0 pe text stricat (ENDPROC lipsa)" Assert ((Get-FileHashBytes $vcxFull) -eq $origVcxHash) "Faza D (a): binarul esantion neatins in proiect"
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) [IO.File]::WriteAllBytes($cacheText, $origBytes)
# (b) fisier text gol (0 bytes) # (b) fisier text gol
[IO.File]::WriteAllBytes($cacheText, [byte[]]@()) [IO.File]::WriteAllBytes($cacheText, [byte[]]@())
& $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot
$rcB = $LASTEXITCODE Assert ($LASTEXITCODE -ne 0) "Faza D (b): txt2vcx exit != 0 pe fisier text gol"
Assert ($rcB -ne 0) "Faza D (b): txt2vcx.ps1 exit != 0 pe fisier text gol" Assert ((Get-FileHashBytes $vcxFull) -eq $origVcxHash) "Faza D (b): binarul esantion neatins in proiect"
Assert ((Get-FileHashBytes (Join-Path $ProjectRoot 'Clase\oavize.vcx')) -eq $origVcxHash) "Faza D (b): oavize.vcx neatins in proiect"
[IO.File]::WriteAllBytes($cacheText, $origBytes) [IO.File]::WriteAllBytes($cacheText, $origBytes)
# --- Test guard-uri --- # (guard) staleness: binar mai nou decat textul -> refuz fara -Force
# staleness: dam mtime-ul binarului in viitor -> refuz fara -Force $origMtime = (Get-Item -LiteralPath $vcxFull).LastWriteTime
$vcxPath = Join-Path $ProjectRoot 'Clase\oavize.vcx' (Get-Item -LiteralPath $vcxFull).LastWriteTime = (Get-Date).AddDays(1)
$origMtime = (Get-Item -LiteralPath $vcxPath).LastWriteTime
(Get-Item -LiteralPath $vcxPath).LastWriteTime = (Get-Date).AddDays(1)
try { try {
[IO.File]::WriteAllBytes($cacheText, $origBytes) # text neschimbat, dar staleness trebuie sa refuze oricum [IO.File]::WriteAllBytes($cacheText, $origBytes)
& $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot
$rcStale = $LASTEXITCODE Assert ($LASTEXITCODE -ne 0) "Faza D (guard staleness): refuz fara -Force cand binarul e mai nou"
Assert ($rcStale -ne 0) "Faza D (guard staleness): refuz fara -Force cand binarul e mai nou decat cache-ul" Assert ((Get-FileHashBytes $vcxFull) -eq $origVcxHash) "Faza D (guard staleness): binar neatins"
Assert ((Get-FileHashBytes $vcxPath) -eq $origVcxHash) "Faza D (guard staleness): oavize.vcx neatins"
} finally { } finally {
(Get-Item -LiteralPath $vcxPath).LastWriteTime = $origMtime (Get-Item -LiteralPath $vcxFull).LastWriteTime = $origMtime
} }
# binar tinta lipsa -> refuz PERMANENT (si cu -Force) # (guard) binar tinta lipsa -> refuz PERMANENT (si cu -Force)
$missingCacheText = Join-Path $CacheRoot 'Clase\_nu_exista_deloc.vc2' $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")) [IO.File]::WriteAllBytes($missingCacheText, $script:Enc.GetBytes("* fake`r`nDEFINE CLASS x AS y`r`nENDDEFINE`r`n"))
try { try {
& $txt2vcx -TextFile $missingCacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force & $txt2vcx -TextFile $missingCacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force
$rcMissing = $LASTEXITCODE Assert ($LASTEXITCODE -ne 0) "Faza D (guard binar lipsa): refuz chiar si cu -Force"
Assert ($rcMissing -ne 0) "Faza D (guard binar lipsa): refuz chiar si cu -Force" } finally { Remove-Item -LiteralPath $missingCacheText -Force -ErrorAction SilentlyContinue }
} finally {
Remove-Item -LiteralPath $missingCacheText -Force -ErrorAction SilentlyContinue
}
# cale text in afara CacheRoot -> eroare dura # (guard) cale text in afara CacheRoot -> refuz
$outsideText = Join-Path $env:TEMP 'oavize_outside.vc2' $outsideText = Join-Path $env:TEMP ('_outside_' + [Guid]::NewGuid().ToString('N').Substring(0,8) + '.vc2')
[IO.File]::WriteAllBytes($outsideText, $origBytes) [IO.File]::WriteAllBytes($outsideText, $origBytes)
try { try {
& $txt2vcx -TextFile $outsideText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot & $txt2vcx -TextFile $outsideText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot
$rcOutside = $LASTEXITCODE Assert ($LASTEXITCODE -ne 0) "Faza D (guard cale in afara CacheRoot): refuz"
Assert ($rcOutside -ne 0) "Faza D (guard cale in afara CacheRoot): refuz"
} finally { Remove-Item -LiteralPath $outsideText -Force -ErrorAction SilentlyContinue } } finally { Remove-Item -LiteralPath $outsideText -Force -ErrorAction SilentlyContinue }
# extensie nesuportata (.mn2) # (guard) extensie nesuportata (.mn2)
$unsupported = Join-Path $CacheRoot 'Clase\_fake.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")) [IO.File]::WriteAllBytes($unsupported, $script:Enc.GetBytes("* fake mn2`r`n"))
try { try {
& $txt2vcx -TextFile $unsupported -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot & $txt2vcx -TextFile $unsupported -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot
$rcUnsup = $LASTEXITCODE Assert ($LASTEXITCODE -ne 0) "Faza D (guard extensie nesuportata .mn2): refuz"
Assert ($rcUnsup -ne 0) "Faza D (guard extensie nesuportata .mn2): refuz"
} finally { Remove-Item -LiteralPath $unsupported -Force -ErrorAction SilentlyContinue } } finally { Remove-Item -LiteralPath $unsupported -Force -ErrorAction SilentlyContinue }
# guard COMUN (cu -DryRun, refuz inainte de staging, zero scrieri) # (guard) COMUN fara -AllowComun -> refuz (-DryRun, zero scrieri)
$comunCandidate = Get-ChildItem -Path (Join-Path $CacheRoot 'comun\clase') -Filter '*.vc2' -ErrorAction SilentlyContinue | Select-Object -First 1 $comunText = Join-Path $CacheRoot 'COMUN\clase\_fake.vc2'
if ($comunCandidate) { New-Item -ItemType Directory -Force -Path (Split-Path $comunText -Parent) | Out-Null
& $txt2vcx -TextFile $comunCandidate.FullName -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -DryRun [IO.File]::WriteAllBytes($comunText, $origBytes)
$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 { try {
& $txt2vcx -TextFile $fb2pFakeText -ProjectRoot 'D:\ROA\UTIL\foxbin2prg' -CacheRoot $fb2pFakeCache -DryRun & $txt2vcx -TextFile $comunText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -DryRun
$rcFb2p = $LASTEXITCODE Assert ($LASTEXITCODE -ne 0) "Faza D (guard COMUN, fara -AllowComun): refuz"
Assert ($rcFb2p -ne 0) "Faza D (guard folder-foxbin2prg): refuz necondiționat" } finally { Remove-Item -Recurse -Force -LiteralPath (Split-Path $comunText -Parent) -ErrorAction SilentlyContinue }
} finally { Remove-Item -Recurse -Force -LiteralPath $fb2pFakeCache -ErrorAction SilentlyContinue }
# -DryRun pe caz valid: nimic modificat in proiect, exit reflecta staging-ul (0 = OK) # (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) [IO.File]::WriteAllBytes($cacheText, $origBytes)
& $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -DryRun & $txt2vcx -TextFile $cacheText -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -DryRun
$rcDry = $LASTEXITCODE Assert ($LASTEXITCODE -eq 0) "Faza D (-DryRun, caz valid): exit 0"
Assert ($rcDry -eq 0) "Faza D (-DryRun, caz valid): exit 0" Assert ((Get-FileHashBytes $vcxFull) -eq $origVcxHash) "Faza D (-DryRun, caz valid): binar neatins in proiect"
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 --- # --- multi-fisier: un scx valid (scris) + vcx stricat (refuzat) in aceeasi rulare ---
& $vcx2txt -Source (Join-Path $ProjectRoot 'Ferestre\fundal.scx') -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null $scxSample = Find-SampleBinary -ProjRoot $ProjectRoot -Ext '.scx'
$fundalCacheText = Join-Path $CacheRoot 'Ferestre\fundal.sc2' if ($scxSample) {
$fundalOrigBytes = [IO.File]::ReadAllBytes($fundalCacheText) $scxRel = Get-RelPath $ProjectRoot $scxSample.FullName
$fundalOrigVcxHash = Get-FileHashBytes (Join-Path $ProjectRoot 'Ferestre\fundal.scx') $scxFull = $scxSample.FullName
$fundalText = $script:Enc.GetString($fundalOrigBytes).Replace('Caption = ""', 'Caption = "multi-test"') $scxDir = Split-Path $scxRel -Parent
[IO.File]::WriteAllBytes($fundalCacheText, $script:Enc.GetBytes($fundalText)) $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
$corruptOavizeText = $script:Enc.GetString($origBytes) -replace "(?s)(PROCEDURE do_cauta_altele.*?)\r?\n\s*ENDPROC", '$1' & $vcx2txt -Source $scxFull -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null
[IO.File]::WriteAllBytes($cacheText, $script:Enc.GetBytes($corruptOavizeText)) $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))
$script:TouchedProjectFiles.Add('Ferestre\fundal.scx') | Out-Null & $txt2vcx -TextFile @($scxCacheText, $cacheText) -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot
$script:TouchedProjectFiles.Add('Ferestre\fundal.SCT') | Out-Null 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"
& $txt2vcx -TextFile @($fundalCacheText, $cacheText) -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot Assert (((Get-Item -LiteralPath $scxFull).LastWriteTime) -ne $scxOrigMtime) "Faza D (multi-fisier): scx valid a fost scris in proiect (mtime schimbat)"
$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) [IO.File]::WriteAllBytes($cacheText, $origBytes)
& $vcx2txt -Source (Join-Path $ProjectRoot 'Clase\oavize.vcx') -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null } else {
& $vcx2txt -Source (Join-Path $ProjectRoot 'Ferestre\fundal.scx') -ProjectRoot $ProjectRoot -CacheRoot $CacheRoot -Force | Out-Null AssertWarn $false "Faza D (multi-fisier): niciun .scx in $ProjectRoot, sarit"
Remove-Item -Recurse -Force -LiteralPath $work -ErrorAction SilentlyContinue }
}
} }
} finally { } finally {
Write-Host "`n--- Cleanup final: git checkout -- pe fisierele atinse + refresh cache ---" Write-Host "`n--- Cleanup final: restore binare din backup + curatare temp ---"
Push-Location $ProjectRoot Restore-ProjectBinaries
try { if ($ownCache -and (Test-Path -LiteralPath $CacheRoot)) { Remove-Item -Recurse -Force -LiteralPath $CacheRoot -ErrorAction SilentlyContinue }
$toRevert = $script:TouchedProjectFiles | Sort-Object -Unique if (Test-Path -LiteralPath $script:BackupRoot) { Remove-Item -Recurse -Force -LiteralPath $script:BackupRoot -ErrorAction SilentlyContinue }
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=== REZULTAT ===" Write-Host "`n=== REZULTAT ==="

View File

@@ -32,7 +32,9 @@
Radacina proiectului tinta (implicit D:\ROA\ROAACNPRO, ca vcx2txt.ps1). Radacina proiectului tinta (implicit D:\ROA\ROAACNPRO, ca vcx2txt.ps1).
.PARAMETER CacheRoot .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 .PARAMETER AllowComun
Obligatoriu pentru a scrie tinte aflate sub COMUN\ (repo partajat intre toate Obligatoriu pentru a scrie tinte aflate sub COMUN\ (repo partajat intre toate
@@ -59,7 +61,7 @@
param( param(
[Parameter(Mandatory)][string[]]$TextFile, [Parameter(Mandatory)][string[]]$TextFile,
[string]$ProjectRoot = 'D:\ROA\ROAACNPRO', [string]$ProjectRoot = 'D:\ROA\ROAACNPRO',
[string]$CacheRoot = 'D:\ROA\_vfp_textcache\roacnpro', [string]$CacheRoot = $ProjectRoot,
[switch]$AllowComun, [switch]$AllowComun,
[switch]$Force, [switch]$Force,
[switch]$NoVerify, [switch]$NoVerify,
@@ -217,6 +219,8 @@ foreach ($tf in $TextFile) {
# --- 3. Regenerare in staging --- # --- 3. Regenerare in staging ---
$stagingDir = Join-Path $stagingRoot $relBinNoExt $stagingDir = Join-Path $stagingRoot $relBinNoExt
New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null 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) $stagingText = Join-Path $stagingDir ($binBaseName + $textExt)
Copy-Item -LiteralPath $fullTextPath -Destination $stagingText -Force Copy-Item -LiteralPath $fullTextPath -Destination $stagingText -Force

View File

@@ -11,6 +11,7 @@
$script:VfpFileMap = @{ $script:VfpFileMap = @{
'.vcx' = @('.vct', '.vc2'); '.scx' = @('.sct', '.sc2'); '.frx' = @('.frt', '.fr2') '.vcx' = @('.vct', '.vc2'); '.scx' = @('.sct', '.sc2'); '.frx' = @('.frt', '.fr2')
'.lbx' = @('.lbt', '.lb2'); '.mnx' = @('.mnt', '.mn2'); '.dbc' = @('.dct', '.dc2') '.lbx' = @('.lbt', '.lb2'); '.mnx' = @('.mnt', '.mn2'); '.dbc' = @('.dct', '.dc2')
'.pjx' = @('.pjt', '.pj2'); '.dbf' = @('.fpt', '.db2')
} }
function Get-VfpBinExtensionForTextExtension { function Get-VfpBinExtensionForTextExtension {