Merge pull request #18 from entireio/changes · Entire
Merge pull request #18 from entireio/changes
b83d31d→main·
pjbgf·2mo ago·6 files·+772 added/-0 removed
Expand on integration tests
Changes
6
internal
planner
- Mplanner_test.go+159
syncer
- Mintegration_test.go+445
- Msyncer.go+5
- Msyncer_test.go+64
validation
- Mvalidation.go+57
- Mvalidation_test.go+42
425 unmodified lines
// TestBuildPlansPrunePreservesUnrelatedBranchesUnderFilter is a regression
// guard for the prune-scoping rule in planner.go: --prune deletes orphan
target branches only when the user has not narrowed the source ref set
// with --branch or --map. With either filter present, branches that exist
// only on the target are out of scope and must be preserved.
func TestBuildPlansPrunePreservesUnrelatedBranchesUnderFilter(t *testing.T) {
t.Parallel()
mainHash := plumbing.NewHash("1111111111111111111111111111111111111111")
releaseHash := plumbing.NewHash("2222222222222222222222222222222222222222")
mainRef := plumbing.NewBranchReferenceName("main")
stableRef := plumbing.NewBranchReferenceName("stable")
releaseRef := plumbing.NewBranchReferenceName("release")
sourceRefs := map[plumbing.ReferenceName]plumbing.Hash{
mainRef: mainHash,
}
tests := []struct {
name string
cfg PlanConfig
targetRefs map[plumbing.ReferenceName]plumbing.Hash
wantManaged plumbing.ReferenceName
preservedRef plumbing.ReferenceName
}{
{
name: "branch filter --branch main --prune",
cfg: PlanConfig{
Branches: []string{"main"},
Prune: true,
},
targetRefs: map[plumbing.ReferenceName]plumbing.Hash{
mainRef: mainHash,
releaseRef: releaseHash,
},
wantManaged: mainRef,
preservedRef: releaseRef,
},
{
name: "rename mapping --map main:stable --prune",
cfg: PlanConfig{
Mappings: []RefMapping{{Source: "main", Target: "stable"}},
Prune: true,
},
targetRefs: map[plumbing.ReferenceName]plumbing.Hash{
stableRef: mainHash,
releaseRef: releaseHash,
},
wantManaged: stableRef,
preservedRef: releaseRef,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
desired, managed, err := BuildDesiredRefs(sourceRefs, tt.cfg)
if err != nil {
t.Fatalf("BuildDesiredRefs: %v", err)
}
plans, err := BuildPlans(nil, desired, tt.targetRefs, managed, tt.cfg)
if err != nil {
t.Fatalf("BuildPlans: %v", err)
}
for _, p := range plans {
if p.TargetRef == tt.preservedRef {
t.Fatalf("unrelated target branch %s emitted plan %+v; --prune must preserve it under filtered scope", tt.preservedRef, p)
}
if p.Action == ActionDelete && p.TargetRef != tt.preservedRef {
t.Fatalf("unexpected delete plan for %s: %+v", p.TargetRef, p)
}
}
if _, ok := managed[tt.preservedRef]; ok {
t.Fatalf("managed map leaked unrelated target ref %s under filtered prune scope", tt.preservedRef)
}
if _, ok := managed[tt.wantManaged]; !ok {
t.Fatalf("expected managed scope to include %s, got %+v", tt.wantManaged, managed)
}
})
}
// More tests follow...
// TestRun_IntegrationIncrementalPushFailureRecoversOnRetry covers the
// failure-and-retry contract for the incremental relay path. When the
// receive-pack rejects the push (here, a one-shot "ng" status), the run
// must surface an error and leave the target unchanged — receive-pack
// only commits refs that the server itself reports as ok. A retry against
// the same source/target must then drive the incremental relay to
// completion, leaving the target at the new source head.
func TestRun_IntegrationIncrementalPushFailureRecoversOnRetry(t *testing.T) {
sourceRepo, sourceFS := newSourceRepo(t)
makeCommits(t, sourceRepo, sourceFS, 2)
targetRepo, _ := newSourceRepo(t)
sourceServer := newSmartHTTPRepoServer(t, sourceRepo)
targetServer := newSmartHTTPRepoServer(t, targetRepo)
targetServer.receivePackThinCap = true
defer sourceServer.Close()
defer targetServer.Close()
cfg := Config{
Source: Endpoint{URL: sourceServer.RepoURL()},
Target: Endpoint{URL: targetServer.RepoURL()},
}
if _, err := Run(context.Background(), cfg); err != nil {
t.Fatalf("seed sync: %v", err)
}
branchRef := plumbing.NewBranchReferenceName(testBranch)
preRetryHead, err := targetRepo.Reference(branchRef, true)
if err != nil {
t.Fatalf("target head after seed: %v", err)
}
// Advance source so the next sync produces a fast-forward update plan,
// the only branch shape that takes the incremental relay path.
makeCommits(t, sourceRepo, sourceFS, 1)
sourceHead, err := sourceRepo.Reference(branchRef, true)
if err != nil {
t.Fatalf("source head: %v", err)
}
var pushAttempts int
targetServer.receivePackHook = func(req *packp.UpdateRequests, _ bool) *packp.ReportStatus {
pushAttempts++
if pushAttempts > 1 {
return nil
}
report := packp.NewReportStatus()
report.UnpackStatus = "ok"
for _, cmd := range req.Commands {
report.CommandStatuses = append(report.CommandStatuses, &packp.CommandStatus{
ReferenceName: cmd.Name,
Status: "ng simulated incremental push failure",
})
}
return report
}
if _, err := Run(context.Background(), cfg); err == nil {
t.Fatal("expected first incremental sync to fail under injected push rejection")
}
afterFail, err := targetRepo.Reference(branchRef, true)
if err != nil {
t.Fatalf("target head after failed sync: %v", err)
}
if afterFail.Hash() != preRetryHead.Hash() {
t.Fatalf("target advanced despite rejected push: pre=%s post=%s", preRetryHead.Hash(), afterFail.Hash())
}
result, err := Run(context.Background(), cfg)
if err != nil {
t.Fatalf("retry after incremental failure: %v", err)
}
if !result.Relay || result.RelayMode != relayModeIncremental {
t.Fatalf("expected incremental relay on retry, got %+v", result)
}
if result.Pushed != 1 {
t.Fatalf("expected exactly one pushed ref on retry, got %+v", result)
}
if pushAttempts != 2 {
t.Fatalf("expected exactly two receive-pack attempts (fail + retry), got %d", pushAttempts)
}
finalHead, err := targetRepo.Reference(branchRef, true)
if err != nil {
t.Fatalf("target head after retry: %v", err)
}
if finalHead.Hash() != sourceHead.Hash() {
t.Fatalf("target head not at source after retry: target=%s source=%s", finalHead.Hash(), sourceHead.Hash())
}
}
// More tests follow...