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

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