Files
foxbin2prg/test_roa_sync.ps1
Marius Mutu 51cab198c4 roa_sync: reconciliere cu origin + push gardat doar commit-uri de sync
Dupa commit-ul local de sync: fetch origin, apoi ff-only (doar in urma) sau
rebase --empty=drop (divergenta - sync-urile cu continut deja identic pe
origin devin goale si sunt aruncate automat, fara interventie manuala);
conflict real -> rebase --abort + exit nenul. Push origin main DOAR cand
toate commit-urile ahead sunt de forma sync SVN[ r<rev>], cu maxim 3
reincercari la push respins (push simultan de pe alta masina). Switch nou
-GitOnly (sare svn update + regenerarea textelor). Harness nou
test_roa_sync.ps1: 8 scenarii pe fixture-uri git in TEMP, inclusiv
reproducerea incidentului din 2026-07-27 (local byte-identic cu origin).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvccPZXr4A9ihcTtExNoJT
2026-07-27 11:52:23 +03:00

240 lines
12 KiB
PowerShell

<#
.SYNOPSIS
Teste standalone (fara Pester) pentru reconcilierea cu origin din roa_sync.ps1, pe
fixture-uri git construite in TEMP (origin bare + doua clone "masini"). Ruleaza
roa_sync.ps1 -GitOnly (fara SVN, fara FoxBin2Prg), deci nu atinge niciun proiect real.
Scenarii:
(A) scenariul din 2026-07-27: modificari locale necomise + fisier netracked, toate
byte-identice cu ce a impins deja alta masina -> reconciliere automata (sync-ul
local devine gol la rebase si e aruncat), fara interventie manuala, exit 0;
(B) behind + continut local NOU -> rebase pastreaza sync-ul si il impinge pe origin;
(C) doar behind, fara modificari locale -> fast-forward la origin/main;
(D) conflict real (acelasi fisier, continut diferit) -> rebase --abort, exit nenul,
repo lasat curat (fara rebase in curs), commit-ul local pastrat;
(E) garda de push: commit non-sync ahead de origin -> push blocat + avertisment;
(F) fetch esuat (origin inaccesibil) -> comportamentul vechi (doar commit local), exit 0;
(G) push respins o data (hook pre-receive, simuleaza push simultan) -> retry reuseste;
(H) repo fara remote origin -> doar commit local, exit 0.
#>
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
$script:RoaSync = 'D:\ROA\UTIL\foxbin2prg\roa_sync.ps1'
$script:Failures = @()
function Assert([bool]$cond, [string]$msg) {
if ($cond) { Write-Host " PASS: $msg" -ForegroundColor Green }
else { Write-Host " FAIL: $msg" -ForegroundColor Red; $script:Failures += $msg }
}
# --- git in fixture: EAP Continue local (git scrie pe stderr; sub Stop 2>$null ar arunca) ---
function G {
param([Parameter(Mandatory)][string]$Repo,
[Parameter(Mandatory, ValueFromRemainingArguments)][string[]]$GitArgs)
$prev = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
$out = (& git -C $Repo @GitArgs 2>$null | Out-String).Trim()
$code = $LASTEXITCODE
$ErrorActionPreference = $prev
if ($code -ne 0) { throw "git -C $Repo $($GitArgs -join ' ') a esuat (exit $code)" }
return $out
}
# scriere fisier cu octeti determinati (acelasi continut => aceiasi octeti pe ambele "masini")
function WF([string]$path, [string]$content) { [IO.File]::WriteAllText($path, $content) }
function Set-RepoConfig([string]$repo) {
$null = G $repo config user.name 'Test ROA'
$null = G $repo config user.email 'test@roa.local'
$null = G $repo config commit.gpgsign false
$null = G $repo config core.autocrlf false
}
# --- lume de test: origin bare + masinaA (a impins deja) + masinaB (fixture-ul testat) ---
function New-World([string]$tag) {
$root = Join-Path $env:TEMP ("rs_test_{0}_{1}_{2}" -f $tag, $PID, [Guid]::NewGuid().ToString('N').Substring(0,8))
New-Item -ItemType Directory -Force -Path $root | Out-Null
$bare = Join-Path $root 'origin.git'
$null = G $root init --bare origin.git
$null = G $bare symbolic-ref HEAD refs/heads/main
# -c la clone seteaza configul in noul repo INAINTE de checkout: cu autocrlf global true,
# checkout-ul ar scrie CRLF si un add -A ulterior ar vedea fals toate fisierele modificate
$null = G $root clone -c core.autocrlf=false origin.git masinaA
$A = Join-Path $root 'masinaA'
Set-RepoConfig $A
$null = G $A checkout -b main
WF (Join-Path $A 'a.prg') "linie 1`nv1`n"
WF (Join-Path $A 'b.prg') "b v1`n"
$null = G $A add -A
$null = G $A commit -m 'initial'
$null = G $A push -u origin main
$null = G $root clone -c core.autocrlf=false origin.git masinaB
$B = Join-Path $root 'masinaB'
Set-RepoConfig $B
return [PSCustomObject]@{ Root=$root; Bare=$bare; A=$A; B=$B }
}
function Remove-World($w) {
Remove-Item -Recurse -Force -LiteralPath $w.Root -ErrorAction SilentlyContinue
}
function Invoke-RoaSync([string]$Fixture) {
$prev = $ErrorActionPreference; $ErrorActionPreference = 'Continue'
$raw = & powershell -ExecutionPolicy Bypass -NoProfile -File $script:RoaSync -ProjectRoot $Fixture -GitOnly 2>&1 | Out-String
$exit = $LASTEXITCODE
$ErrorActionPreference = $prev
return [PSCustomObject]@{ Exit=$exit; Raw=$raw }
}
# ============================== SCENARIILE ==============================
function Test-ContinutIdentic {
Write-Host "`n--- (A) scenariul 2026-07-27: local byte-identic cu origin (modificat + netracked) ---"
$w = New-World 'ident'
try {
# masina A a facut svn update + sync + push inaintea noastra
WF (Join-Path $w.A 'a.prg') "linie 1`nv2`n"
WF (Join-Path $w.A 'wincrypt.h') "#define W 1`n"
$null = G $w.A add -A; $null = G $w.A commit -m 'sync SVN r100'; $null = G $w.A push origin main
# masina B: acelasi svn update + regenerare => aceleasi continuturi, necomise/netracked
WF (Join-Path $w.B 'a.prg') "linie 1`nv2`n"
WF (Join-Path $w.B 'wincrypt.h') "#define W 1`n"
$r = Invoke-RoaSync $w.B
Assert ($r.Exit -eq 0) "A: exit 0, fara interventie manuala (exit=$($r.Exit))"
Assert ((G $w.B rev-parse main) -eq (G $w.Bare rev-parse main)) "A: main local == origin/main dupa reconciliere"
Assert ((G $w.B log -1 --format=%s) -eq 'sync SVN r100') "A: sync-ul local gol a fost aruncat (tip = commit-ul masinii A)"
Assert ([string]::IsNullOrWhiteSpace((G $w.B status --porcelain))) "A: working tree curat"
} finally { Remove-World $w }
}
function Test-ContinutNou {
Write-Host "`n--- (B) behind + continut local nou: rebase pastreaza sync-ul si il impinge ---"
$w = New-World 'nou'
try {
WF (Join-Path $w.A 'a.prg') "linie 1`nv2`n"
$null = G $w.A add -A; $null = G $w.A commit -m 'sync SVN r100'; $null = G $w.A push origin main
# masina B: aceeasi modificare (identica) + un fisier NOU pe care origin nu il are
WF (Join-Path $w.B 'a.prg') "linie 1`nv2`n"
WF (Join-Path $w.B 'c.prg') "continut nou local`n"
$r = Invoke-RoaSync $w.B
Assert ($r.Exit -eq 0) "B: exit 0 (exit=$($r.Exit))"
Assert ((G $w.B log -1 --format=%s) -eq 'sync SVN') "B: sync-ul local (continut nou) pastrat dupa rebase"
Assert ((G $w.B rev-parse main) -eq (G $w.Bare rev-parse main)) "B: push automat - origin/main actualizat"
Assert ((G $w.Bare ls-tree --name-only main) -match 'c\.prg') "B: c.prg a ajuns pe origin"
} finally { Remove-World $w }
}
function Test-DoarBehind {
Write-Host "`n--- (C) doar behind, fara modificari locale: fast-forward ---"
$w = New-World 'ff'
try {
WF (Join-Path $w.A 'b.prg') "b v2`n"
$null = G $w.A add -A; $null = G $w.A commit -m 'sync SVN r101'; $null = G $w.A push origin main
$r = Invoke-RoaSync $w.B
Assert ($r.Exit -eq 0) "C: exit 0 (exit=$($r.Exit))"
Assert ($r.Raw -match 'fast-forward') "C: reconciliere prin fast-forward"
Assert ((G $w.B rev-parse main) -eq (G $w.Bare rev-parse main)) "C: main local == origin/main"
} finally { Remove-World $w }
}
function Test-ConflictReal {
Write-Host "`n--- (D) conflict real: rebase abortat, exit nenul, repo curat ---"
$w = New-World 'conf'
try {
WF (Join-Path $w.A 'a.prg') "linie 1`nvarianta A`n"
$null = G $w.A add -A; $null = G $w.A commit -m 'sync SVN r102'; $null = G $w.A push origin main
WF (Join-Path $w.B 'a.prg') "linie 1`nvarianta B`n"
$r = Invoke-RoaSync $w.B
Assert ($r.Exit -ne 0) "D: exit nenul la conflict real (exit=$($r.Exit))"
Assert ($r.Raw -match 'CONFLICT') "D: conflictul e raportat explicit"
$inRebase = (Test-Path (Join-Path $w.B '.git\rebase-merge')) -or (Test-Path (Join-Path $w.B '.git\rebase-apply'))
Assert (-not $inRebase) "D: rebase anulat curat (fara rebase in curs)"
Assert ((G $w.B log -1 --format=%s) -eq 'sync SVN') "D: commit-ul local de sync pastrat"
Assert ([string]::IsNullOrWhiteSpace((G $w.B status --porcelain))) "D: working tree curat dupa abort"
Assert ((G $w.Bare log -1 --format=%s) -eq 'sync SVN r102') "D: origin neatins"
} finally { Remove-World $w }
}
function Test-GardaNonSync {
Write-Host "`n--- (E) garda de push: commit non-sync ahead blocheaza push-ul automat ---"
$w = New-World 'guard'
try {
$init = G $w.Bare rev-parse main
# commit local non-sync (lucrare in curs, nepushed) + modificari de sync necomise
WF (Join-Path $w.B 'd.prg') "lucru manual`n"
$null = G $w.B add -A; $null = G $w.B commit -m 'lucrare manuala neaprobata'
WF (Join-Path $w.B 'b.prg') "b v2`n"
$r = Invoke-RoaSync $w.B
Assert ($r.Exit -eq 0) "E: exit 0 (exit=$($r.Exit))"
Assert ($r.Raw -match 'non-sync') "E: avertisment despre commit-uri non-sync"
Assert ((G $w.Bare rev-parse main) -eq $init) "E: origin neatins (fara push)"
Assert ((G $w.B log -1 --format=%s) -eq 'sync SVN') "E: sync-ul local totusi comis"
} finally { Remove-World $w }
}
function Test-Offline {
Write-Host "`n--- (F) fetch esuat (origin inaccesibil): doar commit local, exit 0 ---"
$w = New-World 'off'
try {
$null = G $w.B remote set-url origin (Join-Path $w.Root 'inexistent.git')
WF (Join-Path $w.B 'b.prg') "b v3`n"
$r = Invoke-RoaSync $w.B
Assert ($r.Exit -eq 0) "F: exit 0 (exit=$($r.Exit))"
Assert ($r.Raw -match 'fetch origin a esuat') "F: avertisment de fetch esuat"
Assert ((G $w.B log -1 --format=%s) -eq 'sync SVN') "F: commit-ul local de sync facut totusi"
} finally { Remove-World $w }
}
function Test-PushRetry {
Write-Host "`n--- (G) push respins o data (simulare push simultan): retry reuseste ---"
$w = New-World 'retry'
try {
# hook pre-receive: respinge cat timp exista flagul reject_once, apoi accepta
$hook = "#!/bin/sh`nif [ -f reject_once ]; then`n rm -f reject_once`n exit 1`nfi`nexit 0`n"
[IO.File]::WriteAllText((Join-Path $w.Bare 'hooks\pre-receive'), $hook)
WF (Join-Path $w.Bare 'reject_once') "1"
WF (Join-Path $w.B 'b.prg') "b v4`n"
$r = Invoke-RoaSync $w.B
Assert ($r.Exit -eq 0) "G: exit 0 (exit=$($r.Exit))"
Assert ($r.Raw -match 'push respins') "G: prima incercare respinsa si raportata"
Assert ((G $w.Bare rev-parse main) -eq (G $w.B rev-parse main)) "G: push reusit la reincercare"
} finally { Remove-World $w }
}
function Test-FaraOrigin {
Write-Host "`n--- (H) repo fara remote origin: doar commit local ---"
$w = New-World 'noorig'
try {
$null = G $w.B remote remove origin
WF (Join-Path $w.B 'b.prg') "b v5`n"
$r = Invoke-RoaSync $w.B
Assert ($r.Exit -eq 0) "H: exit 0 (exit=$($r.Exit))"
Assert ($r.Raw -match 'fara remote origin') "H: lipsa origin raportata"
Assert ((G $w.B log -1 --format=%s) -eq 'sync SVN') "H: commit-ul local de sync facut"
} finally { Remove-World $w }
}
# ============================== MAIN ==============================
if (-not (Test-Path -LiteralPath $script:RoaSync)) { throw "Nu gasesc roa_sync.ps1: $script:RoaSync" }
Write-Host "=== test_roa_sync.ps1 ==="
Test-ContinutIdentic
Test-ContinutNou
Test-DoarBehind
Test-ConflictReal
Test-GardaNonSync
Test-Offline
Test-PushRetry
Test-FaraOrigin
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
}