test(s3/lifecycle): pin Pipeline.Run dependency + shard validation (Phase 15 slice) (#9402)

* test(s3/lifecycle): pin Pipeline.Run dependency + shard validation

Pre-existing TestPipelineRunRequiresDependencies only checked that an
empty Pipeline errors; it didn't pin which specific dependency must be
present. A refactor that makes one nilable accidentally would slip
through.

8 new tests pin every validation branch in Pipeline.Run: missing
Engine / Persister / Client / FilerClient each error with "missing
required dependency"; missing BucketsPath errors with its own
distinct message so operators can spot the missing wiring; ShardID =
-1 / ShardCount errors with a range message (covers the half-open
[0, ShardCount) boundary so a < to <= refactor can't introduce a
one-past-the-end shard); and a multi-shard config with one
out-of-range entry refuses the whole run rather than silently
disabling the rest.

* test(s3/lifecycle): refactor Pipeline.Run validation tests as table-driven

Per gemini review on #9402: collapse the eight per-branch tests into
TestPipelineRunValidation with a slice of (name, mutate, wantErr)
cases. Same coverage, ~30 fewer lines, idiomatic Go pattern that
makes adding a new validation case trivial.
This commit is contained in:
Chris Lu 2026-05-09 20:34:51 -07:00 committed by GitHub
parent cb6e498e0b
commit 1350e681c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -2,6 +2,7 @@ package dispatcher
import (
"context"
"strings"
"testing"
"time"
@ -82,3 +83,59 @@ func TestPipelineRunRequiresDependencies(t *testing.T) {
t.Fatal("expected error for empty Pipeline")
}
}
// stubFilerClient satisfies filer_pb.SeaweedFilerClient just enough to
// pass the nil-check in Pipeline.Run; methods would panic if called,
// but the validation tests below all return before any RPC.
type stubFilerClient struct {
filer_pb.SeaweedFilerClient
}
// fullPipeline assembles a Pipeline whose dependencies all pass the
// nil-check, so individual tests can knock out one piece at a time
// to exercise specific validation branches.
func fullPipeline() *Pipeline {
return &Pipeline{
Engine: engine.New(),
Persister: reader.NewInMemoryPersister(),
Client: &fakeClient{},
FilerClient: &stubFilerClient{},
BucketsPath: "/buckets",
ShardID: 0,
}
}
func TestPipelineRunValidation(t *testing.T) {
// Each case mutates one piece of a fullPipeline() and asserts the
// expected error fragment. Per-dependency cases pin that the nil
// check exercises every required field individually; the Buckets-
// Path case asserts the distinct error message; the shard cases
// pin the half-open [0, ShardCount) range and that any one bad
// entry refuses the whole multi-shard run.
cases := []struct {
name string
mutate func(*Pipeline)
wantErr string
}{
{"missing Engine", func(p *Pipeline) { p.Engine = nil }, "missing required dependency"},
{"missing Persister", func(p *Pipeline) { p.Persister = nil }, "missing required dependency"},
{"missing Client", func(p *Pipeline) { p.Client = nil }, "missing required dependency"},
{"missing FilerClient", func(p *Pipeline) { p.FilerClient = nil }, "missing required dependency"},
{"missing BucketsPath", func(p *Pipeline) { p.BucketsPath = "" }, "BucketsPath required"},
{"negative ShardID", func(p *Pipeline) { p.ShardID = -1 }, "out of"},
{"ShardID at boundary", func(p *Pipeline) { p.ShardID = s3lifecycle.ShardCount }, "out of"},
{"multi-shard out of range", func(p *Pipeline) {
p.Shards = []int{0, 1, s3lifecycle.ShardCount + 1}
}, "out of"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
p := fullPipeline()
tc.mutate(p)
err := p.Run(context.Background())
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("expected error containing %q, got %v", tc.wantErr, err)
}
})
}
}