From 9c80e7179e7f69351e7759672e479e79ac498fc2 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 31 May 2026 03:35:46 +0000 Subject: [PATCH] Add shared test fixture helpers --- internal/publish/execute_test.go | 31 +-- internal/testutil/fixtures.go | 256 +++++++++++++++++++ internal/transform/markdown/markdown_test.go | 27 +- 3 files changed, 267 insertions(+), 47 deletions(-) create mode 100644 internal/testutil/fixtures.go diff --git a/internal/publish/execute_test.go b/internal/publish/execute_test.go index cd3836b..3cf1458 100644 --- a/internal/publish/execute_test.go +++ b/internal/publish/execute_test.go @@ -5,18 +5,17 @@ import ( "fmt" "io" "testing" - "time" - "gitea.maximumdirect.net/eric/distributor/internal/bundle" "gitea.maximumdirect.net/eric/distributor/internal/config" "gitea.maximumdirect.net/eric/distributor/internal/storage" "gitea.maximumdirect.net/eric/distributor/internal/storage/fake" + "gitea.maximumdirect.net/eric/distributor/internal/testutil" ) func TestExecuteCleansUpAfterWriteFailure(t *testing.T) { sourceBackend := fake.New() destinationBackend := &failingBackend{Backend: fake.New(), failPath: "summary.txt"} - sourceBundle := writeFakeSourceBundle(t, sourceBackend) + sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "", testutil.BundleOptions{}) req := Request{ PipelineID: "reports", DestinationID: "archive", @@ -63,29 +62,3 @@ func (b *failingBackend) WriteFrom(ctx context.Context, path string, r io.Reader } return b.Backend.WriteFrom(ctx, path, r, opts) } - -func writeFakeSourceBundle(t *testing.T, backend *fake.Backend) bundle.Bundle { - t.Helper() - files := []struct { - path string - data string - }{ - {path: "report.md", data: "# Report\nSunny.\n"}, - {path: "summary.txt", data: "Summary\n"}, - } - manifestFiles := make([]bundle.ManifestFile, 0, len(files)) - for _, file := range files { - if _, err := backend.WriteFile(context.Background(), file.path, []byte(file.data), storage.WriteOptions{}); err != nil { - t.Fatalf("WriteFile() error = %v", err) - } - manifestFiles = append(manifestFiles, bundle.ManifestFile{Path: file.path, SHA256: bundle.FileDigest([]byte(file.data)), Size: int64(len(file.data))}) - } - manifest := bundle.Manifest{ - SchemaVersion: 1, - ID: "weather.daily.brentwood.2026-05-30", - Created: time.Date(2026, 5, 30, 11, 10, 0, 0, time.UTC), - Files: manifestFiles, - } - manifest.Digest = bundle.BundleDigest(manifest.Files) - return bundle.Bundle{Manifest: manifest} -} diff --git a/internal/testutil/fixtures.go b/internal/testutil/fixtures.go new file mode 100644 index 0000000..20e4ca2 --- /dev/null +++ b/internal/testutil/fixtures.go @@ -0,0 +1,256 @@ +package testutil + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "gitea.maximumdirect.net/eric/distributor/internal/bundle" + "gitea.maximumdirect.net/eric/distributor/internal/state" + "gitea.maximumdirect.net/eric/distributor/internal/storage" + "gitea.maximumdirect.net/eric/distributor/internal/storage/fake" +) + +const DefaultBundleID = "weather.daily.brentwood.2026-05-30" + +var DefaultCreated = time.Date(2026, 5, 30, 11, 10, 0, 0, time.UTC) + +type SourceFile struct { + Path string + Data string +} + +type BundleOptions struct { + ID string + Created time.Time + Files []SourceFile + ExtraFiles []SourceFile +} + +type DestinationStateOptions struct { + PipelineID string + DestinationID string + DistributorVersion string + PublishedAt time.Time +} + +func DefaultSourceFiles() []SourceFile { + return []SourceFile{ + {Path: "report.md", Data: "# Report\nSunny.\n"}, + {Path: "summary.txt", Data: "Summary\n"}, + } +} + +func ValidManifest(opts BundleOptions) bundle.Manifest { + files := sourceFiles(opts) + manifestFiles := make([]bundle.ManifestFile, 0, len(files)) + for _, file := range files { + data := []byte(file.Data) + manifestFiles = append(manifestFiles, bundle.ManifestFile{ + Path: file.Path, + SHA256: bundle.FileDigest(data), + Size: int64(len(data)), + }) + } + manifest := bundle.Manifest{ + SchemaVersion: 1, + ID: defaultBundleID(opts.ID), + Created: defaultCreated(opts.Created), + Files: manifestFiles, + } + manifest.Digest = bundle.BundleDigest(manifest.Files) + return manifest +} + +func WriteSourceBundle(t testing.TB, root, relative string, opts BundleOptions) bundle.Manifest { + t.Helper() + bundleRoot := filepath.Join(root, filepath.FromSlash(relative)) + if err := os.MkdirAll(bundleRoot, 0o755); err != nil { + t.Fatalf("mkdir source bundle: %v", err) + } + manifest := ValidManifest(opts) + for _, file := range sourceFiles(opts) { + path := filepath.Join(bundleRoot, filepath.FromSlash(file.Path)) + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("mkdir source file parent: %v", err) + } + if err := os.WriteFile(path, []byte(file.Data), 0o600); err != nil { + t.Fatalf("write source file: %v", err) + } + } + writeJSONFile(t, filepath.Join(bundleRoot, bundle.ManifestName), manifest) + return manifest +} + +func WriteFakeSourceBundle(t testing.TB, backend *fake.Backend, relative string, opts BundleOptions) bundle.Bundle { + t.Helper() + manifest := ValidManifest(opts) + for _, file := range sourceFiles(opts) { + path := joinStoragePath(t, relative, file.Path) + if _, err := backend.WriteFile(context.Background(), path, []byte(file.Data), storage.WriteOptions{}); err != nil { + t.Fatalf("write fake source file: %v", err) + } + } + data, err := json.MarshalIndent(manifest, "", " ") + if err != nil { + t.Fatalf("marshal manifest: %v", err) + } + manifestPath := joinStoragePath(t, relative, bundle.ManifestName) + if _, err := backend.WriteFile(context.Background(), manifestPath, append(data, '\n'), storage.WriteOptions{}); err != nil { + t.Fatalf("write fake manifest: %v", err) + } + return bundle.Bundle{RootRelativePath: relative, Manifest: manifest} +} + +func WriteMinimalLocalConfig(t testing.TB, sourceRoot, destinationRoot string) string { + t.Helper() + return writeConfigFile(t, ` +pipelines: + - id: reports + source: + backend: local + path: `+sourceRoot+` + destinations: + - id: archive + backend: local + path: `+destinationRoot+` +`) +} + +func WriteFanoutLocalConfig(t testing.TB, sourceRoot, firstDestinationRoot, secondDestinationRoot string) string { + t.Helper() + return writeConfigFile(t, ` +pipelines: + - id: reports + source: + backend: local + path: `+sourceRoot+` + destinations: + - id: archive-one + backend: local + path: `+firstDestinationRoot+` + - id: archive-two + backend: local + path: `+secondDestinationRoot+` +`) +} + +func WriteDestinationState(t testing.TB, root, relative string, manifest bundle.Manifest, opts DestinationStateOptions) state.DistributorState { + t.Helper() + bundleRoot := filepath.Join(root, filepath.FromSlash(relative)) + if err := os.MkdirAll(bundleRoot, 0o755); err != nil { + t.Fatalf("mkdir destination bundle: %v", err) + } + destinationState := DestinationState(manifest, opts) + writeJSONFile(t, filepath.Join(bundleRoot, storage.StateFileName), destinationState) + return destinationState +} + +func DestinationState(manifest bundle.Manifest, opts DestinationStateOptions) state.DistributorState { + return state.DistributorState{ + SchemaVersion: state.SchemaVersion, + DistributorVersion: opts.DistributorVersion, + PipelineID: defaultString(opts.PipelineID, "reports"), + DestinationID: defaultString(opts.DestinationID, "archive"), + PublishedAt: defaultPublishedAt(opts.PublishedAt), + Source: state.SourceState{Manifest: manifest}, + Outputs: sourceOutputs(manifest), + } +} + +func ReadDestinationState(t testing.TB, path string) state.DistributorState { + t.Helper() + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read destination state: %v", err) + } + destinationState, err := state.Parse(data) + if err != nil { + t.Fatalf("parse destination state: %v", err) + } + return destinationState +} + +func sourceFiles(opts BundleOptions) []SourceFile { + files := opts.Files + if files == nil { + files = DefaultSourceFiles() + } else { + files = append([]SourceFile(nil), files...) + } + files = append(files, opts.ExtraFiles...) + return files +} + +func sourceOutputs(manifest bundle.Manifest) []state.OutputFile { + outputs := make([]state.OutputFile, 0, len(manifest.Files)) + for _, file := range manifest.Files { + outputs = append(outputs, state.OutputFile{ + Path: file.Path, + Kind: state.OutputKindSource, + SourcePath: file.Path, + SHA256: file.SHA256, + Size: file.Size, + }) + } + return outputs +} + +func writeConfigFile(t testing.TB, body string) string { + t.Helper() + path := filepath.Join(t.TempDir(), "config.yml") + if err := os.WriteFile(path, []byte(strings.TrimSpace(body)+"\n"), 0o600); err != nil { + t.Fatalf("write config: %v", err) + } + return path +} + +func writeJSONFile(t testing.TB, path string, value any) { + t.Helper() + data, err := json.MarshalIndent(value, "", " ") + if err != nil { + t.Fatalf("marshal json: %v", err) + } + data = append(data, '\n') + if err := os.WriteFile(path, data, 0o600); err != nil { + t.Fatalf("write json file: %v", err) + } +} + +func joinStoragePath(t testing.TB, root, path string) string { + t.Helper() + joined, err := storage.Join(root, path) + if err != nil { + t.Fatalf("join storage path: %v", err) + } + return joined +} + +func defaultBundleID(value string) string { + return defaultString(value, DefaultBundleID) +} + +func defaultString(value, fallback string) string { + if value != "" { + return value + } + return fallback +} + +func defaultCreated(value time.Time) time.Time { + if value.IsZero() { + return DefaultCreated + } + return value +} + +func defaultPublishedAt(value time.Time) time.Time { + if value.IsZero() { + return time.Date(2026, 5, 30, 11, 12, 0, 0, time.UTC) + } + return value +} diff --git a/internal/transform/markdown/markdown_test.go b/internal/transform/markdown/markdown_test.go index 9dce626..55eed36 100644 --- a/internal/transform/markdown/markdown_test.go +++ b/internal/transform/markdown/markdown_test.go @@ -9,6 +9,7 @@ import ( "gitea.maximumdirect.net/eric/distributor/internal/bundle" "gitea.maximumdirect.net/eric/distributor/internal/storage" "gitea.maximumdirect.net/eric/distributor/internal/storage/fake" + "gitea.maximumdirect.net/eric/distributor/internal/testutil" "gitea.maximumdirect.net/eric/distributor/internal/transform" ) @@ -92,22 +93,12 @@ func TestGenerateDeterministicOutput(t *testing.T) { func markdownFixture(t *testing.T, markdown string) (*fake.Backend, bundle.Bundle) { t.Helper() backend := fake.New() - if _, err := backend.WriteFile(context.Background(), "report.md", []byte(markdown), storage.WriteOptions{}); err != nil { - t.Fatalf("WriteFile() error = %v", err) - } - if _, err := backend.WriteFile(context.Background(), "summary.txt", []byte("Summary\n"), storage.WriteOptions{}); err != nil { - t.Fatalf("WriteFile() error = %v", err) - } - files := []bundle.ManifestFile{ - {Path: "report.md", SHA256: bundle.FileDigest([]byte(markdown)), Size: int64(len(markdown))}, - {Path: "summary.txt", SHA256: bundle.FileDigest([]byte("Summary\n")), Size: 8}, - } - manifest := bundle.Manifest{ - SchemaVersion: 1, - ID: "bundle", - Created: time.Date(2026, 5, 30, 11, 10, 0, 0, time.UTC), - Files: files, - } - manifest.Digest = bundle.BundleDigest(manifest.Files) - return backend, bundle.Bundle{Manifest: manifest} + sourceBundle := testutil.WriteFakeSourceBundle(t, backend, "", testutil.BundleOptions{ + ID: "bundle", + Files: []testutil.SourceFile{ + {Path: "report.md", Data: markdown}, + {Path: "summary.txt", Data: "Summary\n"}, + }, + }) + return backend, sourceBundle }