Files
distributor/internal/cli/prune_test.go

214 lines
7.5 KiB
Go

package cli
import (
"bytes"
"context"
"os"
"path/filepath"
"strings"
"testing"
"gitea.maximumdirect.net/eric/distributor/internal/state"
"gitea.maximumdirect.net/eric/distributor/internal/storage"
"gitea.maximumdirect.net/eric/distributor/internal/testutil"
)
func TestExecutePruneRejectsInvalidFlags(t *testing.T) {
tests := []struct {
name string
args []string
wantStderr string
}{
{
name: "missing config",
args: []string{"prune", "--pipeline", "reports", "--destination", "archive", "--dry-run"},
wantStderr: "requires --config",
},
{
name: "missing pipeline",
args: []string{"prune", "--config", "config.yml", "--destination", "archive", "--dry-run"},
wantStderr: "requires --pipeline",
},
{
name: "missing destination",
args: []string{"prune", "--config", "config.yml", "--pipeline", "reports", "--dry-run"},
wantStderr: "requires --destination",
},
{
name: "missing mode",
args: []string{"prune", "--config", "config.yml", "--pipeline", "reports", "--destination", "archive"},
wantStderr: "requires exactly one of --dry-run or --apply",
},
{
name: "conflicting modes",
args: []string{"prune", "--config", "config.yml", "--pipeline", "reports", "--destination", "archive", "--dry-run", "--apply"},
wantStderr: "requires exactly one of --dry-run or --apply",
},
{
name: "invalid format",
args: []string{"prune", "--config", "config.yml", "--pipeline", "reports", "--destination", "archive", "--dry-run", "--format", "xml"},
wantStderr: "format must be text or json",
},
{
name: "positional",
args: []string{"prune", "--config", "config.yml", "--pipeline", "reports", "--destination", "archive", "--dry-run", "extra"},
wantStderr: "does not accept positional arguments",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), tt.args, &stdout, &stderr)
if code != exitUsage {
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitUsage, stderr.String())
}
if stdout.Len() != 0 {
t.Fatalf("stdout = %q, want empty", stdout.String())
}
if !strings.Contains(stderr.String(), tt.wantStderr) {
t.Fatalf("stderr = %q, want substring %q", stderr.String(), tt.wantStderr)
}
})
}
}
func TestExecutePruneDryRunReportsWithoutWriting(t *testing.T) {
t.Skip("catalog prune execution is covered by the catalog maintenance work")
destinationRoot, configPath := writePruneLocalFixture(t)
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), []string{
"prune",
"--config", configPath,
"--pipeline", "reports",
"--destination", "archive",
"--dry-run",
}, &stdout, &stderr)
if code != exitOK {
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
}
if output := stdout.String(); !strings.Contains(output, "status=would_change") || !strings.Contains(output, "planned=2") || !strings.Contains(output, "deleted=0") {
t.Fatalf("stdout = %q, want dry-run prune summary", output)
}
assertLocalFile(t, filepath.Join(destinationRoot, "report.md"), "# Report\nSunny.\n")
assertLocalFile(t, filepath.Join(destinationRoot, "summary.txt"), "Summary\n")
assertLocalFile(t, filepath.Join(destinationRoot, "extra.txt"), "unmanaged")
destinationState := testutil.ReadDestinationState(t, filepath.Join(destinationRoot, storage.StateFileName))
if got := strings.Join(state.ManagedOutputPaths(destinationState), ","); got != "report.md,summary.txt" {
t.Fatalf("state outputs = %q, want original outputs", got)
}
if stderr.Len() != 0 {
t.Fatalf("stderr = %q, want empty", stderr.String())
}
}
func TestExecutePruneJSONReport(t *testing.T) {
t.Skip("catalog prune execution is covered by the catalog maintenance work")
_, configPath := writePruneLocalFixture(t)
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), []string{
"prune",
"--config", configPath,
"--pipeline", "reports",
"--destination", "archive",
"--dry-run",
"--format", "json",
}, &stdout, &stderr)
if code != exitOK {
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
}
envelope := decodeEnvelope(t, &stdout)
if envelope["command"] != "prune" || envelope["ok"] != true {
t.Fatalf("envelope = %#v, want prune ok", envelope)
}
result := envelopeResult(t, envelope)
if result["would_change"] != true || result["state_changed"] != false || result["dry_run"] != true {
t.Fatalf("result = %#v, want dry-run pending change", result)
}
planned, ok := result["planned_outputs"].([]any)
if !ok || len(planned) != 2 {
t.Fatalf("planned outputs = %#v, want two", result["planned_outputs"])
}
if stderr.Len() != 0 {
t.Fatalf("stderr = %q, want empty", stderr.String())
}
}
func TestExecutePruneApplyDeletesManagedOutputs(t *testing.T) {
t.Skip("catalog prune execution is covered by the catalog maintenance work")
destinationRoot, configPath := writePruneLocalFixture(t)
var stdout, stderr bytes.Buffer
code := Execute(context.Background(), []string{
"prune",
"--config", configPath,
"--pipeline", "reports",
"--destination", "archive",
"--apply",
}, &stdout, &stderr)
if code != exitOK {
t.Fatalf("exit code = %d, want %d; stderr = %q", code, exitOK, stderr.String())
}
if output := stdout.String(); !strings.Contains(output, "status=changed") || !strings.Contains(output, "deleted=2") {
t.Fatalf("stdout = %q, want applied prune summary", output)
}
if _, err := os.Stat(filepath.Join(destinationRoot, "report.md")); !os.IsNotExist(err) {
t.Fatalf("report.md stat error = %v, want not exist", err)
}
if _, err := os.Stat(filepath.Join(destinationRoot, "summary.txt")); !os.IsNotExist(err) {
t.Fatalf("summary.txt stat error = %v, want not exist", err)
}
assertLocalFile(t, filepath.Join(destinationRoot, "extra.txt"), "unmanaged")
if _, err := os.Stat(filepath.Join(destinationRoot, storage.StateFileName)); err != nil {
t.Fatalf("state file stat error = %v", err)
}
destinationState := testutil.ReadDestinationState(t, filepath.Join(destinationRoot, storage.StateFileName))
if got := state.ManagedOutputPaths(destinationState); len(got) != 0 {
t.Fatalf("state outputs = %#v, want none", got)
}
if stderr.Len() != 0 {
t.Fatalf("stderr = %q, want empty", stderr.String())
}
}
func writePruneLocalFixture(t *testing.T) (string, string) {
t.Helper()
sourceRoot := t.TempDir()
destinationRoot := t.TempDir()
manifest := testutil.WriteSourceBundle(t, sourceRoot, "", testutil.BundleOptions{})
testutil.WriteDestinationState(t, destinationRoot, "", manifest, testutil.DestinationStateOptions{})
for _, file := range testutil.DefaultSourceFiles() {
path := filepath.Join(destinationRoot, filepath.FromSlash(file.Path))
if err := os.WriteFile(path, []byte(file.Data), 0o600); err != nil {
t.Fatalf("write destination output: %v", err)
}
}
if err := os.WriteFile(filepath.Join(destinationRoot, "extra.txt"), []byte("unmanaged"), 0o600); err != nil {
t.Fatalf("write unmanaged output: %v", err)
}
configPath := filepath.Join(t.TempDir(), "config.yml")
config := `
pipelines:
- id: reports
source:
backend: local
path: ` + sourceRoot + `
destinations:
- id: archive
backend: local
path: ` + destinationRoot + `
retention:
prune:
enabled: true
older_than: 1h
`
if err := os.WriteFile(configPath, []byte(strings.TrimSpace(config)+"\n"), 0o600); err != nil {
t.Fatalf("write prune config: %v", err)
}
return destinationRoot, configPath
}