Add explicit force replacement workflow
This commit is contained in:
@@ -41,7 +41,7 @@ func TestRunDryRunPrintsConfigSummary(t *testing.T) {
|
||||
"Configured pipelines: 1",
|
||||
"- pipeline=reports source=local bundles=1 destinations=archive",
|
||||
"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",
|
||||
"Final status: ok planned=1 publish_new=1 replace_older=0 force_replace=0 skipped=0 failed=0 dry_run=true",
|
||||
} {
|
||||
if !strings.Contains(output, want) {
|
||||
t.Fatalf("Run() output = %q, want substring %q", output, want)
|
||||
@@ -301,7 +301,7 @@ func TestRunContinuesAfterDestinationFailure(t *testing.T) {
|
||||
for _, want := range []string{
|
||||
"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",
|
||||
"Final status: failed planned=1 publish_new=1 replace_older=0 force_replace=0 skipped=0 failed=1 dry_run=false",
|
||||
} {
|
||||
if !strings.Contains(output, want) {
|
||||
t.Fatalf("stdout = %q, want substring %q", output, want)
|
||||
@@ -497,6 +497,32 @@ func TestRunFailsOnUnmanagedDestination(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunForceReplacesUnmanagedDestination(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
writeSourceBundle(t, sourceRoot, "", testBundleOptions{})
|
||||
if err := os.WriteFile(filepath.Join(destinationRoot, "unmanaged.txt"), []byte("old"), 0o600); err != nil {
|
||||
t.Fatalf("write unmanaged file: %v", err)
|
||||
}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
err := Run(context.Background(), RunOptions{
|
||||
ConfigPath: writeLocalConfig(t, sourceRoot, destinationRoot),
|
||||
Force: true,
|
||||
Stdout: &stdout,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "action=force_replace") {
|
||||
t.Fatalf("stdout = %q, want force_replace", stdout.String())
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(destinationRoot, "unmanaged.txt")); !os.IsNotExist(err) {
|
||||
t.Fatalf("unmanaged file stat error = %v, want not exist", err)
|
||||
}
|
||||
assertFile(t, filepath.Join(destinationRoot, "report.md"), "# Report\nSunny.\n")
|
||||
}
|
||||
|
||||
func TestRunFansOutToLocalDestinations(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
firstDestination := t.TempDir()
|
||||
@@ -567,7 +593,7 @@ func TestRunExercisesRemoteBackendShapesThroughCommonPath(t *testing.T) {
|
||||
"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",
|
||||
"Final status: ok planned=4 publish_new=4 replace_older=0 force_replace=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)
|
||||
@@ -598,6 +624,50 @@ func TestRunExercisesRemoteBackendShapesThroughCommonPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunForceReplacementStaysWithinRemoteBundlePaths(t *testing.T) {
|
||||
localSourceRoot := t.TempDir()
|
||||
writeSourceBundle(t, localSourceRoot, "bundle", testBundleOptions{})
|
||||
s3Destination := fake.New()
|
||||
sshDestination := fake.New()
|
||||
mustWriteFake(t, s3Destination, "bundle/old.txt", "old")
|
||||
mustWriteFake(t, s3Destination, "bundle-sibling/keep.txt", "keep")
|
||||
mustWriteFake(t, sshDestination, "bundle/old.txt", "old")
|
||||
mustWriteFake(t, sshDestination, "bundle-sibling/keep.txt", "keep")
|
||||
cfg := config.Config{Pipelines: []config.Pipeline{{
|
||||
ID: "reports",
|
||||
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: "ssh-archive",
|
||||
Backend: config.BackendSSH,
|
||||
Host: "ssh.test",
|
||||
Path: "/destination",
|
||||
},
|
||||
},
|
||||
}}}
|
||||
config.ApplyDefaults(&cfg)
|
||||
provider := fakeBackendFactoryProvider(t, map[string]storage.Backend{
|
||||
"s3:destination-bucket": s3Destination,
|
||||
"ssh:/destination": sshDestination,
|
||||
})
|
||||
|
||||
if err := runConfigWithBackendFactory(context.Background(), cfg, RunOptions{Force: true}, provider); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
assertFakeFile(t, s3Destination, "bundle/report.md", "# Report\nSunny.\n")
|
||||
assertFakeMissing(t, s3Destination, "bundle/old.txt")
|
||||
assertFakeFile(t, s3Destination, "bundle-sibling/keep.txt", "keep")
|
||||
assertFakeFile(t, sshDestination, "bundle/report.md", "# Report\nSunny.\n")
|
||||
assertFakeMissing(t, sshDestination, "bundle/old.txt")
|
||||
assertFakeFile(t, sshDestination, "bundle-sibling/keep.txt", "keep")
|
||||
}
|
||||
|
||||
func TestRunDryRunDoesNotWrite(t *testing.T) {
|
||||
sourceRoot := t.TempDir()
|
||||
destinationRoot := t.TempDir()
|
||||
@@ -755,6 +825,20 @@ func assertFakeFile(t *testing.T, backend *fake.Backend, path, want string) {
|
||||
}
|
||||
}
|
||||
|
||||
func assertFakeMissing(t *testing.T, backend *fake.Backend, path string) {
|
||||
t.Helper()
|
||||
if _, err := backend.Stat(context.Background(), path); !storage.IsNotFound(err) {
|
||||
t.Fatalf("fake file %s stat error = %v, want not found", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustWriteFake(t *testing.T, backend *fake.Backend, path, data string) {
|
||||
t.Helper()
|
||||
if _, err := backend.WriteFile(context.Background(), path, []byte(data), storage.WriteOptions{}); err != nil {
|
||||
t.Fatalf("write fake file %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func crossBackendConfig(localSourceRoot, s3ToLocalDestination, sshToLocalDestination string) config.Config {
|
||||
cfg := config.Config{
|
||||
Pipelines: []config.Pipeline{
|
||||
|
||||
Reference in New Issue
Block a user