Adauga write-back text->binar VFP: txt2vcx.ps1 + vfp_filemap.ps1 + test_roundtrip.ps1
Fluxul complet bin->text->edit->bin pentru .vcx/.scx (faza 1; .mnx/.frx raman read-only): regenerare+compilare in staging unic per rulare, guard-uri (COMUN cu -AllowComun, staleness, binar lipsa, folder foxbin2prg), fidelity check pe octeti inainte de orice copiere, copiere .vct->.vcx cu restaurare la esec partial, refresh cache. Harta de extensii extrasa in vfp_filemap.ps1, dot-sourced si de vcx2txt.ps1 (comportament identic). Invoke-FoxBin2PrgSafe: wrapper cu timeout+taskkill pentru toate invocarile exe-ului - la text stricat Prg2Bin intoarce ErrorLevel 0 dar binarul corupt poate ridica MessageBox modal VFP nesuprimabil la reconversie. test_roundtrip.ps1: faze fidelitate/editare/smoke siblings/negativ+guard-uri, 41 PASS pe ROAGEST + smoke ROAIMOB/ROAACNPRO. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
32
CLAUDE.md
32
CLAUDE.md
@@ -21,10 +21,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.
|
||||
- **`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 generated text cache is **read-only, for searching only**. Code changes to a VFP project are made in the VFP IDE on the binary files (`.vcx/.scx/...`), never by editing the cached text.
|
||||
**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.
|
||||
- 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.
|
||||
- **Corrupt text can trigger a modal VFP dialog, not just a nonzero exit code**: if `.vc2`/`.sc2` text is malformed, `Prg2Bin` can return `ErrorLevel 0` while producing a corrupt binary — the real error only surfaces on a subsequent bin→text pass, sometimes as a **native VFP MessageBox that blocks headlessly** (not suppressible via parameters/`.cfg`). Any automated invocation of `FoxBin2Prg.EXE` must go through the `Invoke-FoxBin2PrgSafe` wrapper in `vfp_filemap.ps1` (default 60s timeout + `taskkill` on hang) rather than calling the exe directly — and treat the **fidelity check** (bin→text→compare), not the exe's `ErrorLevel`, as the real backstop against corrupt/malformed text.
|
||||
|
||||
```powershell
|
||||
# Populate/refresh a project's text cache (incremental)
|
||||
@@ -37,6 +48,25 @@ It serves two purposes:
|
||||
|
||||
Then search the cache with Grep (e.g. `PROCEDURE do_salvare`), citing `file:line` from the cache.
|
||||
|
||||
Write back an edited `.vc2`/`.sc2` (after reviewing the diff on text):
|
||||
|
||||
```powershell
|
||||
& 'D:\ROA\UTIL\foxbin2prg\txt2vcx.ps1' -TextFile 'D:\ROA\_vfp_textcache\roagest\Clase\oavize.vc2' -ProjectRoot 'D:\ROA\ROAGEST' -CacheRoot 'D:\ROA\_vfp_textcache\roagest'
|
||||
|
||||
# Preview only, no files touched in the project: add -DryRun
|
||||
# Target under COMUN\: add -AllowComun (needs approval)
|
||||
# Skip the staleness check (IDE binary is known up to date some other way): -Force
|
||||
# Skip the fidelity re-check after regen (not recommended): -NoVerify
|
||||
```
|
||||
|
||||
Run the regression/fidelity test suite (no Pester, standalone):
|
||||
|
||||
```powershell
|
||||
& 'D:\ROA\UTIL\foxbin2prg\test_roundtrip.ps1' -ProjectRoot 'D:\ROA\ROAGEST' -CacheRoot 'D:\ROA\_vfp_textcache\roagest' -Phase all
|
||||
|
||||
# Run a single phase: -Phase fidelity|edit|negative|smoke
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
All FoxBin2Prg commands run inside VFP 9 (or via the compiled `FoxBin2Prg.EXE`).
|
||||
|
||||
@@ -69,6 +69,31 @@ fișier:linie din cache, dar reține: editarea efectivă se face în IDE-ul VFP
|
||||
|
||||
---
|
||||
|
||||
## Write-back: editare directă pe text (`.vcx`/`.scx`)
|
||||
|
||||
Pe lângă căutare, cache-ul text poate fi acum și **sursă de editare** pentru `.vcx`/`.scx` (nu și
|
||||
pentru `.mnx`/`.frx`, vezi mai jos), folosind scriptul frate `txt2vcx.ps1`:
|
||||
|
||||
```
|
||||
& 'D:\ROA\UTIL\foxbin2prg\txt2vcx.ps1' -TextFile 'D:\ROA\_vfp_textcache\<<NUME_PROIECT>>\Clase\lib.vc2' -ProjectRoot '<<RADACINA_PROIECT>>' -CacheRoot 'D:\ROA\_vfp_textcache\<<NUME_PROIECT>>'
|
||||
```
|
||||
|
||||
Fluxul complet: (1) refresh cache cu `vcx2txt.ps1` (obligatoriu după orice sesiune de IDE, altfel
|
||||
verificarea de staleness refuză scrierea); (2) editează `.vc2`/`.sc2` direct — formatul e
|
||||
**position-sensitive** (nu reformata/reflow) și proprietățile sunt **alfabetizate** (o proprietate
|
||||
nouă se inserează la locul ei alfabetic); (3) revizuiește diff-ul pe text ca pe orice altă
|
||||
modificare de cod; (4) rulează `txt2vcx.ps1` — regenerează și compilează binarul într-un folder de
|
||||
staging, verifică fidelitatea față de textul editat, și abia apoi copiază binarul în proiect (dacă
|
||||
verificarea eșuează, proiectul rămâne neatins).
|
||||
|
||||
Limite: `.mnx`/`.frx` rămân **doar pentru citire** (meniurile tot au nevoie de GENMENU în IDE;
|
||||
formatul de raport e prea fragil pentru round-trip). Ținte sub `COMUN\` necesită flag-ul
|
||||
`-AllowComun` **și** aprobare explicită — o modificare acolo afectează toate aplicațiile ROA.
|
||||
Fără backup-uri `.bak` lângă binarele din proiect — git e mecanismul de restore. Detalii complete
|
||||
(guard-uri, ordinea pașilor, teste): `D:\ROA\UTIL\foxbin2prg\CLAUDE.md`.
|
||||
|
||||
---
|
||||
|
||||
## Note pentru adaptare
|
||||
|
||||
- `<<NUME_PROIECT>>` îl poți pune orice etichetă scurtă (ex. `roagest`), doar ca numele
|
||||
|
||||
418
test_roundtrip.ps1
Normal file
418
test_roundtrip.ps1
Normal file
@@ -0,0 +1,418 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Teste standalone (fara Pester) pentru fluxul bin<->text VFP (FoxBin2Prg) si scriptul
|
||||
de write-back txt2vcx.ps1. Implicit ruleaza pe 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
Exit 1 daca orice Assert a esuat, CU EXCEPTIA celor marcate warn-only (mnx/frx in Faza A).
|
||||
|
||||
.PARAMETER ProjectRoot
|
||||
Radacina proiectului principal testat (implicit ROAGEST).
|
||||
|
||||
.PARAMETER CacheRoot
|
||||
Cache-ul text al proiectului principal.
|
||||
|
||||
.PARAMETER Phase
|
||||
all | fidelity | edit | negative | smoke
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ProjectRoot = 'D:\ROA\ROAGEST',
|
||||
[string]$CacheRoot = 'D:\ROA\_vfp_textcache\roagest',
|
||||
[ValidateSet('all','fidelity','edit','negative','smoke')]
|
||||
[string]$Phase = 'all'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$exe = 'D:\ROA\UTIL\foxbin2prg\FoxBin2Prg.EXE'
|
||||
$vcx2txt = 'D:\ROA\UTIL\foxbin2prg\vcx2txt.ps1'
|
||||
$txt2vcx = 'D:\ROA\UTIL\foxbin2prg\txt2vcx.ps1'
|
||||
. (Join-Path $PSScriptRoot 'vfp_filemap.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 --
|
||||
|
||||
function Assert([bool]$cond, [string]$msg) {
|
||||
if ($cond) { Write-Host " PASS: $msg" -ForegroundColor Green }
|
||||
else { Write-Host " FAIL: $msg" -ForegroundColor Red; $script:Failures += $msg }
|
||||
}
|
||||
|
||||
function AssertWarn([bool]$cond, [string]$msg) {
|
||||
if ($cond) { Write-Host " PASS(warn-only): $msg" -ForegroundColor Green }
|
||||
else { Write-Warning " WARN (non-blocant): $msg" }
|
||||
}
|
||||
|
||||
function Normalize-Crlf([byte[]]$bytes) {
|
||||
$out = New-Object System.Collections.Generic.List[byte]
|
||||
for ($i = 0; $i -lt $bytes.Length; $i++) {
|
||||
if ($bytes[$i] -eq 0x0D) {
|
||||
if ($i + 1 -lt $bytes.Length -and $bytes[$i+1] -eq 0x0A) { continue }
|
||||
$out.Add(0x0A) | Out-Null
|
||||
} else { $out.Add($bytes[$i]) | Out-Null }
|
||||
}
|
||||
return $out.ToArray()
|
||||
}
|
||||
|
||||
function Test-BytesEqual([byte[]]$a, [byte[]]$b) {
|
||||
$na = Normalize-Crlf $a; $nb = Normalize-Crlf $b
|
||||
if ($na.Length -ne $nb.Length) { return $false }
|
||||
for ($i = 0; $i -lt $na.Length; $i++) { if ($na[$i] -ne $nb[$i]) { return $false } }
|
||||
return $true
|
||||
}
|
||||
|
||||
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.
|
||||
param([string]$InputFile, [string]$Recompile = '', [int]$TimeoutSeconds = 60)
|
||||
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
|
||||
}
|
||||
if ($r.TimedOut) { Write-Warning "Invoke-Fb2p: timeout pe '$InputFile' - tratat ca ESEC." }
|
||||
return $r.ExitCode
|
||||
}
|
||||
|
||||
function Get-FileHashBytes([string]$path) {
|
||||
if (-not (Test-Path -LiteralPath $path)) { return $null }
|
||||
return (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash
|
||||
}
|
||||
|
||||
# --- Faza A: fidelitate pura (bin->text1->bin->text2, doar in temp) ---
|
||||
function Test-FidelityRoundtrip {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$RelPath, # relativ la $ProjRoot, ex Clase\oavize.vcx
|
||||
[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 = $null
|
||||
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 }
|
||||
$memoExt = $pair[0]; $txtExt = $pair[1]
|
||||
$memoFull = [IO.Path]::ChangeExtension($full, $memoExt)
|
||||
|
||||
$work = Join-Path $env:TEMP ("fb2p_fidelity_{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); $memo1 = Join-Path $work ($baseName + $memoExt)
|
||||
Copy-Item -LiteralPath $full -Destination $bin1 -Force
|
||||
if (Test-Path -LiteralPath $memoFull) { Copy-Item -LiteralPath $memoFull -Destination $memo1 -Force }
|
||||
$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 }
|
||||
Assert $false "Faza A ($RelPath): Bin2Prg #1 a esuat (ErrorLevel=$rc1)"
|
||||
return
|
||||
}
|
||||
|
||||
$stage2 = Join-Path $work 'stage2'
|
||||
New-Item -ItemType Directory -Force -Path $stage2 | Out-Null
|
||||
$text1copy = Join-Path $stage2 ($baseName + $txtExt)
|
||||
Copy-Item -LiteralPath $text1 -Destination $text1copy -Force
|
||||
$rc2 = Invoke-Fb2p -InputFile $text1copy -Recompile $ProjRoot
|
||||
$bin2 = Join-Path $stage2 ($baseName + $ext); $memo2 = Join-Path $stage2 ($baseName + $memoExt)
|
||||
if ($rc2 -ne 0 -or -not (Test-Path -LiteralPath $bin2) -or -not (Test-Path -LiteralPath $memo2)) {
|
||||
$ok = $false
|
||||
} else {
|
||||
$rc3 = Invoke-Fb2p -InputFile $bin2
|
||||
$text2 = Join-Path $stage2 ($baseName + $txtExt)
|
||||
$ok = ($rc3 -eq 0) -and (Test-Path -LiteralPath $text2) -and (Test-BytesEqual ([IO.File]::ReadAllBytes($text1)) ([IO.File]::ReadAllBytes($text2)))
|
||||
}
|
||||
if ($WarnOnly) { AssertWarn $ok "Faza A ($RelPath): text1 == text2 dupa roundtrip complet" }
|
||||
else { Assert $ok "Faza A ($RelPath): text1 == text2 dupa roundtrip complet" }
|
||||
} finally {
|
||||
Remove-Item -Recurse -Force -LiteralPath $work -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# --- Faza B: editare reala prin txt2vcx.ps1, cu revert git ---
|
||||
function Test-EditRoundtrip {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$RelPath, # ex Clase\oavize.vcx
|
||||
[Parameter(Mandatory)][string]$CaptionOld,
|
||||
[Parameter(Mandatory)][string]$CaptionNew,
|
||||
[Parameter(Mandatory)][string]$ProcedureMarker # ex "PROCEDURE Init"
|
||||
)
|
||||
$ext = [IO.Path]::GetExtension($RelPath).ToLower()
|
||||
$pair = $script:VfpFileMap[$ext]
|
||||
$memoExt = $pair[0]; $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)
|
||||
|
||||
Write-Host "-- Faza B: $RelPath --"
|
||||
& $vcx2txt -Source (Join-Path $ProjectRoot $RelPath) -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)
|
||||
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)
|
||||
[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 }
|
||||
|
||||
$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
|
||||
}
|
||||
|
||||
# ============================== MAIN ==============================
|
||||
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 }
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
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" }
|
||||
}
|
||||
|
||||
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')
|
||||
|
||||
# (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)
|
||||
|
||||
# (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
|
||||
& $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
|
||||
}
|
||||
|
||||
# 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=== REZULTAT ==="
|
||||
if ($script:Failures.Count -eq 0) {
|
||||
Write-Host "TOATE testele au trecut (Phase=$Phase)." -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
|
||||
}
|
||||
316
txt2vcx.ps1
Normal file
316
txt2vcx.ps1
Normal file
@@ -0,0 +1,316 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Scrie inapoi in proiect binarele VFP (.vcx+.vct / .scx+.sct) regenerate din textul editat
|
||||
in cache (.vc2/.sc2). Fratele "invers" al vcx2txt.ps1.
|
||||
|
||||
.DESCRIPTION
|
||||
Flux complet text->bin, cu verificari de siguranta inainte de a atinge proiectul:
|
||||
1. Mapare cache -> proiect (calea text trebuie sa fie sub -CacheRoot).
|
||||
2. Guard-uri: extensie suportata (.vc2/.sc2 - faza 1), tinta nu e sub folderul
|
||||
foxbin2prg, tinta sub COMUN\ necesita -AllowComun, staleness (binarul din
|
||||
proiect mai nou decat textul din cache => refuz fara -Force), binar tinta
|
||||
lipsa => refuz PERMANENT (nici -Force nu trece peste asta).
|
||||
3. Regenerare (Prg2Bin + compilare) intr-un folder de staging unic per rulare.
|
||||
4. Fidelity check IN STAGING (bin -> text, comparat pe octeti cu textul editat,
|
||||
normalizat CRLF) inainte de orice copiere in proiect.
|
||||
5. Copiere in proiect (.vct intai, apoi .vcx), cu backup temporar (nu in proiect)
|
||||
si acces exclusiv verificat inainte.
|
||||
6. Refresh cache: textul canonic de la fidelity check se scrie peste .vc2-ul din
|
||||
cache, cu LastWriteTime >= mtime-ul noilor binare (evita staleness fals la
|
||||
urmatoarea rulare).
|
||||
7. Sumar OK/ESEC per fisier; proceseaza toate fisierele; exit 1 daca vreunul a esuat.
|
||||
|
||||
Encoding: fisierele .vc2/.sc2 sunt Windows-1252 (VFP CODEPAGE=1252). Orice citire/
|
||||
scriere foloseste explicit acest encoding; comparatiile de fidelitate se fac PE
|
||||
OCTETI (ReadAllBytes), normalizate CRLF -> nu pe stringuri cu encoding implicit.
|
||||
|
||||
.PARAMETER TextFile
|
||||
Unul sau mai multe fisiere .vc2/.sc2 (absolute sau relative la locatia curenta),
|
||||
care trebuie sa fie sub -CacheRoot.
|
||||
|
||||
.PARAMETER ProjectRoot
|
||||
Radacina proiectului tinta (implicit D:\ROA\ROAACNPRO, ca vcx2txt.ps1).
|
||||
|
||||
.PARAMETER CacheRoot
|
||||
Radacina cache-ului text (implicit D:\ROA\_vfp_textcache\roacnpro).
|
||||
|
||||
.PARAMETER AllowComun
|
||||
Obligatoriu pentru a scrie tinte aflate sub COMUN\ (repo partajat intre toate
|
||||
aplicatiile ROA - blast radius mare).
|
||||
|
||||
.PARAMETER Force
|
||||
Sare peste verificarea de staleness (binar mai nou decat textul din cache).
|
||||
NU acopera cazul "binar tinta lipsa" - acela e refuz permanent.
|
||||
|
||||
.PARAMETER NoVerify
|
||||
Sare peste fidelity check (bin->text->compara). Cache-ul tot se reimprospateaza
|
||||
(ruleaza bin->text pe binarul nou doar pentru refresh).
|
||||
|
||||
.PARAMETER DryRun
|
||||
Ruleaza maparea, guard-urile si regenerarea/compilarea/fidelity check-ul IN STAGING,
|
||||
dar nu copiaza nimic in proiect si nu atinge cache-ul.
|
||||
|
||||
.EXAMPLE
|
||||
powershell -ExecutionPolicy Bypass -File D:\ROA\UTIL\foxbin2prg\txt2vcx.ps1 `
|
||||
-TextFile 'D:\ROA\_vfp_textcache\roagest\Clase\atentie.vc2' `
|
||||
-ProjectRoot 'D:\ROA\ROAGEST' -CacheRoot 'D:\ROA\_vfp_textcache\roagest'
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string[]]$TextFile,
|
||||
[string]$ProjectRoot = 'D:\ROA\ROAACNPRO',
|
||||
[string]$CacheRoot = 'D:\ROA\_vfp_textcache\roacnpro',
|
||||
[switch]$AllowComun,
|
||||
[switch]$Force,
|
||||
[switch]$NoVerify,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$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)."
|
||||
}
|
||||
. (Join-Path $PSScriptRoot 'vfp_filemap.ps1')
|
||||
|
||||
$script:Enc = [Text.Encoding]::GetEncoding(1252)
|
||||
$script:FoxBin2PrgFolder = 'D:\ROA\UTIL\foxbin2prg'
|
||||
|
||||
function Normalize-Crlf([byte[]]$bytes) {
|
||||
# normalizeaza CRLF -> LF, pe octeti, ca sa compare fidelity-ul indiferent de terminatorul de linie
|
||||
$out = New-Object System.Collections.Generic.List[byte]
|
||||
for ($i = 0; $i -lt $bytes.Length; $i++) {
|
||||
if ($bytes[$i] -eq 0x0D) {
|
||||
if ($i + 1 -lt $bytes.Length -and $bytes[$i+1] -eq 0x0A) { continue } # skip CR of CRLF
|
||||
$out.Add(0x0A) | Out-Null # CR izolat -> LF
|
||||
} else {
|
||||
$out.Add($bytes[$i]) | Out-Null
|
||||
}
|
||||
}
|
||||
return $out.ToArray()
|
||||
}
|
||||
|
||||
function Test-BytesEqual([byte[]]$a, [byte[]]$b) {
|
||||
$na = Normalize-Crlf $a
|
||||
$nb = Normalize-Crlf $b
|
||||
if ($na.Length -ne $nb.Length) { return $false }
|
||||
for ($i = 0; $i -lt $na.Length; $i++) { if ($na[$i] -ne $nb[$i]) { return $false } }
|
||||
return $true
|
||||
}
|
||||
|
||||
function Test-ExclusiveAccess([string]$path) {
|
||||
try {
|
||||
$fs = [IO.File]::Open($path, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None)
|
||||
$fs.Close()
|
||||
return $true
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-FoxBin2Prg {
|
||||
# Watchdog obligatoriu: FoxBin2Prg.EXE poate afisa un MessageBox MODAL nativ al
|
||||
# motorului VFP (nu al propriului error-handler) cand recompilarea unui text stricat
|
||||
# esueaza - cDontShowErrors='1' NU suprima acest dialog. Fara watchdog, o rulare
|
||||
# automata ar ramane agatata la infinit. Orice timeout => tratat ca ESEC (ExitCode -1).
|
||||
param([string]$InputFile, [string]$Recompile = '', [int]$TimeoutSeconds = 60)
|
||||
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
|
||||
}
|
||||
if ($r.TimedOut) { Write-Warning "Invoke-FoxBin2Prg: timeout pe '$InputFile' - tratat ca ESEC." }
|
||||
return $r.ExitCode
|
||||
}
|
||||
|
||||
function Write-Summary($results) {
|
||||
Write-Host ''
|
||||
Write-Host '--- Sumar txt2vcx ---'
|
||||
$fail = 0
|
||||
foreach ($r in $results) {
|
||||
if ($r.Ok) { Write-Host "OK $($r.Text)" }
|
||||
else { Write-Host "ESEC $($r.Text) -- $($r.Reason)"; $fail++ }
|
||||
}
|
||||
Write-Host ''
|
||||
if ($fail -gt 0) {
|
||||
Write-Host "$fail/$($results.Count) fisier(e) au esuat."
|
||||
Write-Host 'Ruleaza git diff --stat in proiect pentru a revizui ce s-a schimbat.'
|
||||
} else {
|
||||
Write-Host "Toate cele $($results.Count) fisier(e) au fost scrise cu succes."
|
||||
Write-Host 'Ruleaza git diff --stat in proiect pentru a revizui ce s-a schimbat.'
|
||||
}
|
||||
return $fail
|
||||
}
|
||||
|
||||
$results = @()
|
||||
$runStamp = "{0}-{1}" -f $PID, (Get-Date -Format 'yyyyMMddHHmmssfff')
|
||||
$stagingRoot = Join-Path $env:TEMP ("txt2bin_" + (Split-Path $ProjectRoot -Leaf) + "\$runStamp")
|
||||
|
||||
foreach ($tf in $TextFile) {
|
||||
$reason = $null
|
||||
$ok = $false
|
||||
$stagingDir = $null
|
||||
try {
|
||||
# --- resolve full path fara sa ceara existenta anterioara garantata pe alte disk-uri ---
|
||||
$fullTextPath = if ([IO.Path]::IsPathRooted($tf)) { $tf } else { Join-Path (Get-Location) $tf }
|
||||
if (-not (Test-Path -LiteralPath $fullTextPath -PathType Leaf)) {
|
||||
throw "Fisierul text nu exista: $fullTextPath"
|
||||
}
|
||||
$fullTextPath = (Get-Item -LiteralPath $fullTextPath).FullName
|
||||
|
||||
# --- 1. Mapare cache -> proiect ---
|
||||
$cacheRootFull = (Get-Item -LiteralPath $CacheRoot).FullName
|
||||
if (-not $fullTextPath.ToLower().StartsWith($cacheRootFull.ToLower() + '\')) {
|
||||
throw "Calea text trebuie sa fie sub CacheRoot ($cacheRootFull): $fullTextPath"
|
||||
}
|
||||
$relText = $fullTextPath.Substring($cacheRootFull.Length).TrimStart('\')
|
||||
$textExt = [IO.Path]::GetExtension($fullTextPath).ToLower()
|
||||
|
||||
# --- 2a. Guard extensie (doar .vc2/.sc2 in faza 1) ---
|
||||
$pair = Get-VfpBinExtensionForTextExtension -TextExtension $textExt
|
||||
if (-not $pair -or ($pair[0] -ne '.vcx' -and $pair[0] -ne '.scx')) {
|
||||
throw "Extensie nesuportata in faza 2 (doar .vc2/.sc2): $textExt"
|
||||
}
|
||||
$binExt = $pair[0]
|
||||
$memoExt = $pair[1]
|
||||
|
||||
$relBinNoExt = [IO.Path]::ChangeExtension($relText, $null).TrimEnd('.')
|
||||
$relBin = [IO.Path]::ChangeExtension($relText, $binExt)
|
||||
$binDir = Split-Path (Join-Path $ProjectRoot $relBin) -Parent
|
||||
$binBaseName = [IO.Path]::GetFileNameWithoutExtension($relBin)
|
||||
|
||||
# --- 2b. Guard: refuz necondiționat sub folderul foxbin2prg ---
|
||||
$projectRootFull = if (Test-Path -LiteralPath $ProjectRoot) { (Get-Item -LiteralPath $ProjectRoot).FullName } else { $ProjectRoot }
|
||||
$prospectiveBinFull = Join-Path $projectRootFull $relBin
|
||||
if ($prospectiveBinFull.ToLower().StartsWith($script:FoxBin2PrgFolder.ToLower() + '\')) {
|
||||
throw "Refuz: tinta sub folderul foxbin2prg ($script:FoxBin2PrgFolder) - nu e permis sa scrii aici."
|
||||
}
|
||||
|
||||
# --- 2c. Guard COMUN ---
|
||||
if ($relBin -match '(?i)^COMUN\\' -and -not $AllowComun) {
|
||||
throw "Tinta e sub COMUN\ (repo partajat) - ruleaza cu -AllowComun daca esti sigur."
|
||||
}
|
||||
|
||||
# --- Rezolva numele real (casing) al binarului si memo-ului din director ---
|
||||
if (-not (Test-Path -LiteralPath $binDir -PathType Container)) {
|
||||
throw "Director tinta lipsa: $binDir"
|
||||
}
|
||||
$binEntry = Get-ChildItem -LiteralPath $binDir -Filter ($binBaseName + $binExt) -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
$memoEntry = Get-ChildItem -LiteralPath $binDir -Filter ($binBaseName + $memoExt) -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
|
||||
# --- 2d. Guard: binar tinta lipsa -> refuz PERMANENT, -Force nu trece peste asta ---
|
||||
if (-not $binEntry -or -not $memoEntry) {
|
||||
throw "Binarul/memo-ul tinta nu exista in proiect ($binBaseName$binExt / $binBaseName$memoExt) - fisier nou, trebuie creat in IDE (SET CLASSLIB/.pjx) inainte de write-back."
|
||||
}
|
||||
$binFull = $binEntry.FullName
|
||||
$memoFull = $memoEntry.FullName
|
||||
|
||||
# --- 2e. Guard staleness ---
|
||||
$binMtime = $binEntry.LastWriteTime
|
||||
$memoMtime = $memoEntry.LastWriteTime
|
||||
$newestBinMtime = if ($binMtime -gt $memoMtime) { $binMtime } else { $memoMtime }
|
||||
$textMtime = (Get-Item -LiteralPath $fullTextPath).LastWriteTime
|
||||
if ($newestBinMtime -gt $textMtime -and -not $Force) {
|
||||
throw "Staleness: binarul/memo-ul din proiect ($newestBinMtime) e mai nou decat textul din cache ($textMtime) - probabil modificat in IDE. Ruleaza vcx2txt.ps1 -Force intai, sau -Force aici daca esti sigur ca vrei sa suprascrii."
|
||||
}
|
||||
|
||||
# --- 3. Regenerare in staging ---
|
||||
$stagingDir = Join-Path $stagingRoot $relBinNoExt
|
||||
New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null
|
||||
$stagingText = Join-Path $stagingDir ($binBaseName + $textExt)
|
||||
Copy-Item -LiteralPath $fullTextPath -Destination $stagingText -Force
|
||||
|
||||
$rc = Invoke-FoxBin2Prg -InputFile $stagingText -Recompile $ProjectRoot
|
||||
$stagingBin = Join-Path $stagingDir ($binBaseName + $binExt)
|
||||
$stagingMemo = Join-Path $stagingDir ($binBaseName + $memoExt)
|
||||
if ($rc -ne 0 -or -not (Test-Path -LiteralPath $stagingBin) -or -not (Test-Path -LiteralPath $stagingMemo)) {
|
||||
throw "Prg2Bin/compilare esuata (ErrorLevel=$rc) sau lipsesc $binExt/$memoExt in staging."
|
||||
}
|
||||
|
||||
# --- 4. Fidelity check IN STAGING ---
|
||||
$canonicalText = $null
|
||||
if (-not $NoVerify) {
|
||||
$verifyDir = Join-Path $stagingDir 'verify'
|
||||
New-Item -ItemType Directory -Force -Path $verifyDir | Out-Null
|
||||
$verifyBin = Join-Path $verifyDir ($binBaseName + $binExt)
|
||||
$verifyMemo = Join-Path $verifyDir ($binBaseName + $memoExt)
|
||||
Copy-Item -LiteralPath $stagingBin -Destination $verifyBin -Force
|
||||
Copy-Item -LiteralPath $stagingMemo -Destination $verifyMemo -Force
|
||||
$rc2 = Invoke-FoxBin2Prg -InputFile $verifyBin
|
||||
$verifyText = Join-Path $verifyDir ($binBaseName + $textExt)
|
||||
if ($rc2 -ne 0 -or -not (Test-Path -LiteralPath $verifyText)) {
|
||||
throw "Fidelity check: Bin2Prg pe binarul regenerat a esuat (ErrorLevel=$rc2) - probabil text stricat (ex. lipsa ENDPROC)."
|
||||
}
|
||||
$editedBytes = [IO.File]::ReadAllBytes($stagingText)
|
||||
$verifyBytes = [IO.File]::ReadAllBytes($verifyText)
|
||||
if (-not (Test-BytesEqual $editedBytes $verifyBytes)) {
|
||||
throw "Fidelity check: textul regenerat difera de textul editat (posibil proprietate nealfabetizata sau linie reformatata)."
|
||||
}
|
||||
$canonicalText = $verifyText
|
||||
}
|
||||
|
||||
if ($DryRun) {
|
||||
$ok = $true
|
||||
$reason = '(dry-run: regenerare+fidelity OK in staging, nimic copiat in proiect)'
|
||||
} else {
|
||||
# --- 5. Copiere in proiect ---
|
||||
if (-not (Test-ExclusiveAccess $binFull) -or -not (Test-ExclusiveAccess $memoFull)) {
|
||||
throw "Nu pot obtine acces exclusiv la $binBaseName$binExt/$memoExt - inchide IDE-ul VFP si reincearca."
|
||||
}
|
||||
$backupDir = Join-Path $stagingDir 'backup_proiect'
|
||||
New-Item -ItemType Directory -Force -Path $backupDir | Out-Null
|
||||
$backupBin = Join-Path $backupDir ($binBaseName + $binExt)
|
||||
$backupMemo = Join-Path $backupDir ($binBaseName + $memoExt)
|
||||
Copy-Item -LiteralPath $binFull -Destination $backupBin -Force
|
||||
Copy-Item -LiteralPath $memoFull -Destination $backupMemo -Force
|
||||
|
||||
Copy-Item -LiteralPath $stagingMemo -Destination $memoFull -Force
|
||||
try {
|
||||
Copy-Item -LiteralPath $stagingBin -Destination $binFull -Force
|
||||
} catch {
|
||||
# restaureaza memo-ul vechi din backup, ca sa nu ramana o pereche rupta
|
||||
Copy-Item -LiteralPath $backupMemo -Destination $memoFull -Force
|
||||
throw "Copierea $binExt a esuat, memo-ul a fost restaurat din backup: $_"
|
||||
}
|
||||
|
||||
# --- 6. Refresh cache ---
|
||||
if (-not $NoVerify -and $canonicalText) {
|
||||
Copy-Item -LiteralPath $canonicalText -Destination $fullTextPath -Force
|
||||
} else {
|
||||
# fara fidelity text disponibil (NoVerify) - ruleaza bin->text pe binarul nou doar pentru refresh
|
||||
$refreshDir = Join-Path $stagingDir 'refresh'
|
||||
New-Item -ItemType Directory -Force -Path $refreshDir | Out-Null
|
||||
$refreshBin = Join-Path $refreshDir ($binBaseName + $binExt)
|
||||
$refreshMemo = Join-Path $refreshDir ($binBaseName + $memoExt)
|
||||
Copy-Item -LiteralPath $binFull -Destination $refreshBin -Force
|
||||
Copy-Item -LiteralPath $memoFull -Destination $refreshMemo -Force
|
||||
Invoke-FoxBin2Prg -InputFile $refreshBin | Out-Null
|
||||
$refreshText = Join-Path $refreshDir ($binBaseName + $textExt)
|
||||
if (Test-Path -LiteralPath $refreshText) {
|
||||
Copy-Item -LiteralPath $refreshText -Destination $fullTextPath -Force
|
||||
}
|
||||
}
|
||||
$newBinMtime = (Get-Item -LiteralPath $binFull).LastWriteTime
|
||||
$newMemoMtime = (Get-Item -LiteralPath $memoFull).LastWriteTime
|
||||
$newestMtime = if ($newBinMtime -gt $newMemoMtime) { $newBinMtime } else { $newMemoMtime }
|
||||
(Get-Item -LiteralPath $fullTextPath).LastWriteTime = $newestMtime.AddSeconds(1)
|
||||
|
||||
$ok = $true
|
||||
}
|
||||
|
||||
# curatare staging la succes
|
||||
if ($ok -and -not $DryRun) {
|
||||
Remove-Item -Recurse -Force -LiteralPath $stagingDir -ErrorAction SilentlyContinue
|
||||
} elseif ($ok -and $DryRun) {
|
||||
Remove-Item -Recurse -Force -LiteralPath $stagingDir -ErrorAction SilentlyContinue
|
||||
}
|
||||
} catch {
|
||||
$ok = $false
|
||||
$reason = $_.Exception.Message
|
||||
if ($stagingDir) { Write-Warning "Staging pastrat pentru diagnostic: $stagingDir" }
|
||||
}
|
||||
$results += [PSCustomObject]@{ Text = $tf; Ok = $ok; Reason = $reason }
|
||||
}
|
||||
|
||||
$failCount = Write-Summary $results
|
||||
exit ([int]($failCount -gt 0))
|
||||
@@ -66,11 +66,9 @@ if (-not (Test-Path $exe)) {
|
||||
throw "Nu gasesc $exe. Compileaza intai FoxBin2Prg (deschide foxbin2prg.pj2 in VFP si Build)."
|
||||
}
|
||||
|
||||
# binar -> (memo, extensie text)
|
||||
$map = @{
|
||||
'.vcx' = @('.vct', '.vc2'); '.scx' = @('.sct', '.sc2'); '.frx' = @('.frt', '.fr2')
|
||||
'.lbx' = @('.lbt', '.lb2'); '.mnx' = @('.mnt', '.mn2'); '.dbc' = @('.dct', '.dc2')
|
||||
}
|
||||
# binar -> (memo, extensie text) -- harta comuna, partajata cu txt2vcx.ps1
|
||||
. (Join-Path $PSScriptRoot 'vfp_filemap.ps1')
|
||||
$map = $script:VfpFileMap
|
||||
|
||||
function Convert-One($fullPath) {
|
||||
$f = Get-Item -LiteralPath $fullPath
|
||||
|
||||
80
vfp_filemap.ps1
Normal file
80
vfp_filemap.ps1
Normal file
@@ -0,0 +1,80 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Harta comuna bin<->memo<->text pentru conversiile VFP (FoxBin2Prg), dot-sourced de
|
||||
vcx2txt.ps1 (Bin2Prg) si txt2vcx.ps1 (Prg2Bin), ca sa nu existe doua copii ale hartii.
|
||||
|
||||
.DESCRIPTION
|
||||
Defineste $script:VfpFileMap: extensie-binar (lowercase, cu punct) -> @(extensie-memo, extensie-text).
|
||||
Si o functie inversa Get-VfpBinExtensionForTextExtension pentru txt2vcx.ps1 (text -> binar).
|
||||
#>
|
||||
|
||||
$script:VfpFileMap = @{
|
||||
'.vcx' = @('.vct', '.vc2'); '.scx' = @('.sct', '.sc2'); '.frx' = @('.frt', '.fr2')
|
||||
'.lbx' = @('.lbt', '.lb2'); '.mnx' = @('.mnt', '.mn2'); '.dbc' = @('.dct', '.dc2')
|
||||
}
|
||||
|
||||
function Get-VfpBinExtensionForTextExtension {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Data o extensie text (.vc2, .sc2, ...), intoarce @(extensie-binar, extensie-memo), sau $null daca nu e cunoscuta.
|
||||
#>
|
||||
param([Parameter(Mandatory)][string]$TextExtension)
|
||||
$t = $TextExtension.ToLower()
|
||||
foreach ($binExt in $script:VfpFileMap.Keys) {
|
||||
if ($script:VfpFileMap[$binExt][1] -eq $t) {
|
||||
return @($binExt, $script:VfpFileMap[$binExt][0])
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Invoke-FoxBin2PrgSafe {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Ruleaza FoxBin2Prg.EXE cu un watchdog de timp, ca sa nu ramana blocat la infinit.
|
||||
|
||||
.DESCRIPTION
|
||||
FoxBin2Prg.EXE poate afisa un MessageBox MODAL nativ al motorului VFP (nu al
|
||||
propriului sau error-handler) cand recompilarea textului esueaza (ex. text
|
||||
stricat, ENDPROC lipsa) - vazut concret: "Error 1098, Procedure not closed...".
|
||||
Parametrul cDontShowErrors='1' NU suprima acest dialog (acela controleaza doar
|
||||
raportarea de erori proprie a FoxBin2Prg, nu eroarea nativa de compilare VFP).
|
||||
Fara acest watchdog, un rulaj automat/CI ar ramane agatat la infinit asteptand
|
||||
un om sa apese OK. Orice timeout se trateaza ca ESEC pentru fisierul respectiv.
|
||||
|
||||
.PARAMETER ExePath
|
||||
Calea catre FoxBin2Prg.EXE.
|
||||
|
||||
.PARAMETER ExeArgs
|
||||
Lista de argumente pozitionale, in ordinea folosita de FoxBin2Prg (inclusiv
|
||||
string-urile goale intre ele, ca in apelul cu "&").
|
||||
|
||||
.PARAMETER TimeoutSeconds
|
||||
Cat asteapta procesul sa termine normal inainte sa-l omoare (implicit 60s -
|
||||
conversiile uzuale dureaza sub o secunda; un timeout mare inseamna aproape
|
||||
sigur un dialog modal blocat).
|
||||
|
||||
.OUTPUTS
|
||||
PSCustomObject cu TimedOut (bool) si ExitCode (int; -1 daca a fost omorat la timeout).
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$ExePath,
|
||||
[Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][string[]]$ExeArgs,
|
||||
[int]$TimeoutSeconds = 60
|
||||
)
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $ExePath
|
||||
$psi.Arguments = ($ExeArgs | ForEach-Object { '"' + ($_ -replace '"', '\"') + '"' }) -join ' '
|
||||
$psi.UseShellExecute = $false
|
||||
|
||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||
$finished = $proc.WaitForExit($TimeoutSeconds * 1000)
|
||||
if (-not $finished) {
|
||||
Write-Warning "Invoke-FoxBin2PrgSafe: TIMEOUT dupa $TimeoutSeconds s (probabil MessageBox modal blocat) - omor procesul (PID $($proc.Id)) si eventualele lui ferestre/copii."
|
||||
try { & taskkill /PID $proc.Id /T /F 2>&1 | Out-Null } catch {}
|
||||
try { if (-not $proc.HasExited) { $proc.Kill() } } catch {}
|
||||
Start-Sleep -Milliseconds 300
|
||||
return [PSCustomObject]@{ TimedOut = $true; ExitCode = -1 }
|
||||
}
|
||||
return [PSCustomObject]@{ TimedOut = $false; ExitCode = $proc.ExitCode }
|
||||
}
|
||||
Reference in New Issue
Block a user