Harden cross-backend run diagnostics
This commit is contained in:
@@ -10,11 +10,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/adapters/local"
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/bundle"
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/config"
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/notify"
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/state"
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/storage"
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/storage/fake"
|
||||
"gitea.maximumdirect.net/eric/distributor/internal/testutil"
|
||||
)
|
||||
|
||||
@@ -38,7 +40,7 @@ func TestRunDryRunPrintsConfigSummary(t *testing.T) {
|
||||
for _, want := range []string{
|
||||
"Configured pipelines: 1",
|
||||
"- pipeline=reports source=local bundles=1 destinations=archive",
|
||||
"bundle=. destination=archive action=publish_new outputs=report.md,summary.txt",
|
||||
"bundle=. destination=archive backend=local action=publish_new outputs=report.md,summary.txt",
|
||||
"Final status: ok planned=1 publish_new=1 replace_older=0 skipped=0 failed=0 dry_run=true",
|
||||
} {
|
||||
if !strings.Contains(output, want) {
|
||||
@@ -292,10 +294,13 @@ func TestRunContinuesAfterDestinationFailure(t *testing.T) {
|
||||
if err == nil || !strings.Contains(err.Error(), "fail_unmanaged") {
|
||||
t.Fatalf("Run() error = %v, want unmanaged failure", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "pipeline reports destination archive-one backend local bundle .") {
|
||||
t.Fatalf("Run() error = %v, want backend context", err)
|
||||
}
|
||||
output := stdout.String()
|
||||
for _, want := range []string{
|
||||
"destination=archive-one action=error",
|
||||
"destination=archive-two action=publish_new",
|
||||
"destination=archive-one backend=local action=error",
|
||||
"destination=archive-two backend=local action=publish_new",
|
||||
"Final status: failed planned=1 publish_new=1 replace_older=0 skipped=0 failed=1 dry_run=false",
|
||||
} {
|
||||
if !strings.Contains(output, want) {
|
||||
@@ -506,6 +511,93 @@ func TestRunFansOutToLocalDestinations(t *testing.T) {
|
||||
assertFile(t, filepath.Join(secondDestination, "daily", "summary.txt"), "Summary\n")
|
||||
}
|
||||
|
||||
func TestRunFansOutWithDifferentPublishPolicies(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
archiveDestination := t.TempDir()
|
||||
htmlDestination := t.TempDir()
|
||||
writeSourceBundle(t, sourceRoot, "", testBundleOptions{})
|
||||
|
||||
err := Run(context.Background(), RunOptions{ConfigPath: writeMixedPolicyFanoutConfig(t, sourceRoot, archiveDestination, htmlDestination)})
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
assertFile(t, filepath.Join(archiveDestination, "report.md"), "# Report\nSunny.\n")
|
||||
assertFile(t, filepath.Join(archiveDestination, "summary.txt"), "Summary\n")
|
||||
if _, err := os.Stat(filepath.Join(archiveDestination, "report.html")); !os.IsNotExist(err) {
|
||||
t.Fatalf("archive report.html stat error = %v, want not exist", err)
|
||||
}
|
||||
assertFileContains(t, filepath.Join(htmlDestination, "report.html"), "<h1>Report</h1>")
|
||||
if _, err := os.Stat(filepath.Join(htmlDestination, "report.md")); !os.IsNotExist(err) {
|
||||
t.Fatalf("html report.md stat error = %v, want not exist", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunExercisesRemoteBackendShapesThroughCommonPath(t *testing.T) {
|
||||
localSourceRoot := t.TempDir()
|
||||
writeSourceBundle(t, localSourceRoot, "", testBundleOptions{})
|
||||
s3Source := fake.New()
|
||||
testutil.WriteFakeSourceBundle(t, s3Source, "", testutil.BundleOptions{})
|
||||
sshSource := fake.New()
|
||||
testutil.WriteFakeSourceBundle(t, sshSource, "", testutil.BundleOptions{})
|
||||
s3Destination := fake.New()
|
||||
sshDestination := fake.New()
|
||||
s3ToLocalDestination := t.TempDir()
|
||||
sshToLocalDestination := t.TempDir()
|
||||
|
||||
cfg := crossBackendConfig(localSourceRoot, s3ToLocalDestination, sshToLocalDestination)
|
||||
if err := config.Validate(cfg); err != nil {
|
||||
t.Fatalf("cross-backend config validation error = %v", err)
|
||||
}
|
||||
provider := fakeBackendFactoryProvider(t, map[string]storage.Backend{
|
||||
"s3:source-bucket": s3Source,
|
||||
"s3:destination-bucket": s3Destination,
|
||||
"ssh:/source": sshSource,
|
||||
"ssh:/destination": sshDestination,
|
||||
})
|
||||
|
||||
var dryRunOutput bytes.Buffer
|
||||
if err := runConfigWithBackendFactory(context.Background(), cfg, RunOptions{DryRun: true, Stdout: &dryRunOutput}, provider); err != nil {
|
||||
t.Fatalf("dry-run error = %v", err)
|
||||
}
|
||||
for _, want := range []string{
|
||||
"pipeline=local-to-s3 source=local",
|
||||
"destination=object-archive backend=s3 action=publish_new",
|
||||
"pipeline=s3-to-local source=s3",
|
||||
"destination=local-archive backend=local action=publish_new",
|
||||
"pipeline=local-to-ssh source=local",
|
||||
"destination=ssh-archive backend=ssh action=publish_new",
|
||||
"pipeline=ssh-to-local source=ssh",
|
||||
"Final status: ok planned=4 publish_new=4 replace_older=0 skipped=0 failed=0 dry_run=true",
|
||||
} {
|
||||
if !strings.Contains(dryRunOutput.String(), want) {
|
||||
t.Fatalf("dry-run output = %q, want substring %q", dryRunOutput.String(), want)
|
||||
}
|
||||
}
|
||||
if hasAny, err := s3Destination.HasAny(context.Background(), ""); err != nil || hasAny {
|
||||
t.Fatalf("s3 destination after dry-run hasAny=%t err=%v, want empty", hasAny, err)
|
||||
}
|
||||
if entries, err := os.ReadDir(s3ToLocalDestination); err != nil || len(entries) != 0 {
|
||||
t.Fatalf("s3-to-local destination entries = %v err=%v, want empty", entries, err)
|
||||
}
|
||||
|
||||
var publishOutput bytes.Buffer
|
||||
if err := runConfigWithBackendFactory(context.Background(), cfg, RunOptions{Stdout: &publishOutput}, provider); err != nil {
|
||||
t.Fatalf("publish error = %v", err)
|
||||
}
|
||||
assertFakeFile(t, s3Destination, "report.md", "# Report\nSunny.\n")
|
||||
assertFakeFile(t, sshDestination, "summary.txt", "Summary\n")
|
||||
assertFile(t, filepath.Join(s3ToLocalDestination, "report.md"), "# Report\nSunny.\n")
|
||||
assertFile(t, filepath.Join(sshToLocalDestination, "summary.txt"), "Summary\n")
|
||||
|
||||
var repeatOutput bytes.Buffer
|
||||
if err := runConfigWithBackendFactory(context.Background(), cfg, RunOptions{Stdout: &repeatOutput}, provider); err != nil {
|
||||
t.Fatalf("repeat error = %v", err)
|
||||
}
|
||||
if got := strings.Count(repeatOutput.String(), "action=skip_same"); got != 4 {
|
||||
t.Fatalf("repeat output = %q, skip_same count = %d, want 4", repeatOutput.String(), got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunDryRunDoesNotWrite(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
@@ -583,6 +675,34 @@ func writeFanoutConfig(t *testing.T, sourceRoot, firstDestination, secondDestina
|
||||
return testutil.WriteFanoutLocalConfig(t, sourceRoot, firstDestination, secondDestination)
|
||||
}
|
||||
|
||||
func writeMixedPolicyFanoutConfig(t *testing.T, sourceRoot, archiveDestination, htmlDestination string) string {
|
||||
t.Helper()
|
||||
return writeConfigFile(t, `
|
||||
pipelines:
|
||||
- id: reports
|
||||
source:
|
||||
backend: local
|
||||
path: `+sourceRoot+`
|
||||
destinations:
|
||||
- id: archive
|
||||
backend: local
|
||||
path: `+archiveDestination+`
|
||||
publish:
|
||||
source: true
|
||||
html: false
|
||||
- id: html
|
||||
backend: local
|
||||
path: `+htmlDestination+`
|
||||
publish:
|
||||
source: false
|
||||
html: true
|
||||
transform:
|
||||
markdown_to_html:
|
||||
enabled: true
|
||||
mode: sidecar
|
||||
`)
|
||||
}
|
||||
|
||||
func writeConfigFile(t *testing.T, body string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "config.yml")
|
||||
@@ -624,6 +744,112 @@ func assertFileContains(t *testing.T, path, want string) {
|
||||
}
|
||||
}
|
||||
|
||||
func assertFakeFile(t *testing.T, backend *fake.Backend, path, want string) {
|
||||
t.Helper()
|
||||
data, err := backend.ReadFile(context.Background(), path)
|
||||
if err != nil {
|
||||
t.Fatalf("read fake file %s: %v", path, err)
|
||||
}
|
||||
if got := string(data); got != want {
|
||||
t.Fatalf("%s = %q, want %q", path, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func crossBackendConfig(localSourceRoot, s3ToLocalDestination, sshToLocalDestination string) config.Config {
|
||||
cfg := config.Config{
|
||||
Pipelines: []config.Pipeline{
|
||||
{
|
||||
ID: "local-to-s3",
|
||||
Source: config.Backend{Backend: config.BackendLocal, Path: localSourceRoot},
|
||||
Destinations: []config.Destination{{
|
||||
ID: "object-archive",
|
||||
Backend: config.BackendS3,
|
||||
Endpoint: "http://s3.test",
|
||||
Bucket: "destination-bucket",
|
||||
}},
|
||||
},
|
||||
{
|
||||
ID: "s3-to-local",
|
||||
Source: config.Backend{
|
||||
Backend: config.BackendS3,
|
||||
Endpoint: "http://s3.test",
|
||||
Bucket: "source-bucket",
|
||||
},
|
||||
Destinations: []config.Destination{{
|
||||
ID: "local-archive",
|
||||
Backend: config.BackendLocal,
|
||||
Path: s3ToLocalDestination,
|
||||
}},
|
||||
},
|
||||
{
|
||||
ID: "local-to-ssh",
|
||||
Source: config.Backend{Backend: config.BackendLocal, Path: localSourceRoot},
|
||||
Destinations: []config.Destination{{
|
||||
ID: "ssh-archive",
|
||||
Backend: config.BackendSSH,
|
||||
Host: "ssh.test",
|
||||
Path: "/destination",
|
||||
}},
|
||||
},
|
||||
{
|
||||
ID: "ssh-to-local",
|
||||
Source: config.Backend{
|
||||
Backend: config.BackendSSH,
|
||||
Host: "ssh.test",
|
||||
Path: "/source",
|
||||
},
|
||||
Destinations: []config.Destination{{
|
||||
ID: "local-archive",
|
||||
Backend: config.BackendLocal,
|
||||
Path: sshToLocalDestination,
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
config.ApplyDefaults(&cfg)
|
||||
return cfg
|
||||
}
|
||||
|
||||
func fakeBackendFactoryProvider(t *testing.T, remoteBackends map[string]storage.Backend) backendFactoryProvider {
|
||||
t.Helper()
|
||||
return func(environment config.Environment) *backendFactory {
|
||||
registry := storage.NewRegistry()
|
||||
if err := registry.Register(config.BackendLocal, func(ctx context.Context, cfg storage.OpenConfig) (storage.Backend, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return local.New(cfg[storagePathKey])
|
||||
}); err != nil {
|
||||
t.Fatalf("register local backend: %v", err)
|
||||
}
|
||||
if err := registry.Register(config.BackendS3, func(ctx context.Context, cfg storage.OpenConfig) (storage.Backend, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
backend := remoteBackends["s3:"+cfg[s3BucketKey]]
|
||||
if backend == nil {
|
||||
return nil, fmt.Errorf("missing fake s3 backend for bucket %s", cfg[s3BucketKey])
|
||||
}
|
||||
return backend, nil
|
||||
}); err != nil {
|
||||
t.Fatalf("register s3 backend: %v", err)
|
||||
}
|
||||
if err := registry.Register(config.BackendSSH, func(ctx context.Context, cfg storage.OpenConfig) (storage.Backend, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
backend := remoteBackends["ssh:"+cfg[storagePathKey]]
|
||||
if backend == nil {
|
||||
return nil, fmt.Errorf("missing fake ssh backend for path %s", cfg[storagePathKey])
|
||||
}
|
||||
return backend, nil
|
||||
}); err != nil {
|
||||
t.Fatalf("register ssh backend: %v", err)
|
||||
}
|
||||
return &backendFactory{registry: registry, environment: environment}
|
||||
}
|
||||
}
|
||||
|
||||
type recordingNotifier struct {
|
||||
events []notify.Event
|
||||
check func()
|
||||
|
||||
Reference in New Issue
Block a user