1168 lines
38 KiB
Go
1168 lines
38 KiB
Go
package comparison
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/testutil"
|
|
)
|
|
|
|
func TestPlanDestination(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
absent := filepath.Join(workingDirectory, "absent")
|
|
plan, err := PlanDestination(workingDirectory, absent, false)
|
|
if err != nil {
|
|
t.Fatalf("PlanDestination() error = %v", err)
|
|
}
|
|
if plan.Target != absent || plan.state != destinationAbsent {
|
|
t.Fatalf("PlanDestination() = %#v, want absent target", plan)
|
|
}
|
|
nested := filepath.Join(workingDirectory, "missing-parent", "comparison")
|
|
if _, err := PlanDestination(workingDirectory, nested, false); err != nil {
|
|
t.Fatalf("PlanDestination(nested) error = %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Dir(nested)); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("read-only plan created parent: stat error = %v", err)
|
|
}
|
|
|
|
empty := filepath.Join(workingDirectory, "empty")
|
|
if err := os.Mkdir(empty, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, err = PlanDestination(workingDirectory, empty, false)
|
|
if err != nil || plan.state != destinationEmpty {
|
|
t.Fatalf("PlanDestination(empty) = %#v, %v", plan, err)
|
|
}
|
|
|
|
file := filepath.Join(workingDirectory, "file")
|
|
if err := os.WriteFile(file, []byte("not a directory"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertDestinationErrorKind(t, workingDirectory, file, false, DestinationNotDirectory)
|
|
assertDestinationErrorKind(t, workingDirectory, workingDirectory, false, DestinationWorkingDirectory)
|
|
assertDestinationErrorKind(t, workingDirectory, string(os.PathSeparator), false, DestinationFilesystemRoot)
|
|
assertDestinationErrorKind(t, workingDirectory, "relative", false, DestinationInvalidPath)
|
|
|
|
t.Run("symbolic links", func(t *testing.T) {
|
|
link := filepath.Join(workingDirectory, "link")
|
|
testutil.RequireSymlink(t, empty, link)
|
|
assertDestinationErrorKind(t, workingDirectory, link, false, DestinationSymlink)
|
|
|
|
dangling := filepath.Join(workingDirectory, "dangling")
|
|
testutil.RequireSymlink(t, filepath.Join(workingDirectory, "missing"), dangling)
|
|
assertDestinationErrorKind(t, workingDirectory, dangling, false, DestinationSymlink)
|
|
assertDestinationErrorKind(t, workingDirectory, filepath.Join(dangling, "child"), false, DestinationInspection)
|
|
})
|
|
|
|
nonempty := filepath.Join(workingDirectory, "nonempty")
|
|
if err := os.Mkdir(nonempty, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(nonempty, "extra"), []byte("x"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertDestinationErrorKind(t, workingDirectory, nonempty, false, DestinationNotEmpty)
|
|
assertDestinationErrorKind(t, workingDirectory, nonempty, true, DestinationUnrecognized)
|
|
}
|
|
|
|
func TestPlanDestinationRejectsUnreadableDirectory(t *testing.T) {
|
|
if os.Geteuid() == 0 {
|
|
t.Skip("root can inspect directories regardless of mode")
|
|
}
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "unreadable")
|
|
if err := os.Mkdir(target, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chmod(target, 0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = os.Chmod(target, 0o700) })
|
|
assertDestinationErrorKind(t, workingDirectory, target, false, DestinationInspection)
|
|
}
|
|
|
|
func TestRecognizeBundleRejectsUnsafeAndMalformedContents(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mutate func(t *testing.T, directory string)
|
|
}{
|
|
{name: "extra file", mutate: func(t *testing.T, directory string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(filepath.Join(directory, "extra.txt"), []byte("extra"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}},
|
|
{name: "subdirectory", mutate: func(t *testing.T, directory string) {
|
|
t.Helper()
|
|
if err := os.Mkdir(filepath.Join(directory, "nested"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}},
|
|
{name: "missing report", mutate: func(t *testing.T, directory string) {
|
|
t.Helper()
|
|
if err := os.Remove(filepath.Join(directory, "01-weather-light.md")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}},
|
|
{name: "symlink report", mutate: func(t *testing.T, directory string) {
|
|
t.Helper()
|
|
path := filepath.Join(directory, "01-weather-light.md")
|
|
if err := os.Remove(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
testutil.RequireSymlink(t, filepath.Join(directory, DataPackageFilename), path)
|
|
}},
|
|
{name: "digest mismatch", mutate: func(t *testing.T, directory string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(filepath.Join(directory, DataPackageFilename), []byte("altered"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}},
|
|
{name: "unknown manifest field", mutate: func(t *testing.T, directory string) {
|
|
t.Helper()
|
|
path := filepath.Join(directory, ManifestFilename)
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data = append(data[:len(data)-2], []byte(",\n \"unknown\": true\n}\n")...)
|
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}},
|
|
{name: "duplicate manifest field", mutate: func(t *testing.T, directory string) {
|
|
t.Helper()
|
|
appendManifestField(t, directory, `"schemaVersion": "weatherreporter.comparison.v1"`)
|
|
}},
|
|
{name: "case variant manifest field", mutate: func(t *testing.T, directory string) {
|
|
t.Helper()
|
|
appendManifestField(t, directory, `"SchemaVersion": "weatherreporter.comparison.v1"`)
|
|
}},
|
|
{name: "traversal report path", mutate: func(t *testing.T, directory string) {
|
|
t.Helper()
|
|
path := filepath.Join(directory, ManifestFilename)
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var manifest Manifest
|
|
if err := json.Unmarshal(data, &manifest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manifest.Results[0].ReportPath = "../outside.md"
|
|
data, err = json.Marshal(manifest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}},
|
|
{name: "noncanonical report path", mutate: func(t *testing.T, directory string) {
|
|
t.Helper()
|
|
path := filepath.Join(directory, ManifestFilename)
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var manifest Manifest
|
|
if err := json.Unmarshal(data, &manifest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Rename(filepath.Join(directory, manifest.Results[0].ReportPath), filepath.Join(directory, "arbitrary.md")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manifest.Results[0].ReportPath = "arbitrary.md"
|
|
data, err = json.Marshal(manifest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
directory := publishTestBundle(t, testBundle())
|
|
test.mutate(t, directory)
|
|
if _, err := RecognizeBundle(directory); !errors.Is(err, ErrUnrecognizedBundle) {
|
|
t.Fatalf("RecognizeBundle() error = %v, want ErrUnrecognizedBundle", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func appendManifestField(t *testing.T, directory, field string) {
|
|
t.Helper()
|
|
path := filepath.Join(directory, ManifestFilename)
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data = append(data[:len(data)-2], []byte(",\n "+field+"\n}\n")...)
|
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestPlanDestinationAcceptsRecognizedReplacement(t *testing.T) {
|
|
directory := publishTestBundle(t, testBundle())
|
|
plan, err := PlanDestination(filepath.Dir(directory), directory, true)
|
|
if err != nil {
|
|
t.Fatalf("PlanDestination() error = %v", err)
|
|
}
|
|
if plan.state != destinationBundle {
|
|
t.Fatal("PlanDestination() did not record recognized existing destination")
|
|
}
|
|
}
|
|
|
|
func TestRecognizeBundleReadsOnlyRequiredArtifactContents(t *testing.T) {
|
|
for _, reportCount := range []int{2, 12} {
|
|
t.Run(fmt.Sprintf("%d reports", reportCount), func(t *testing.T) {
|
|
bundle := testBundleWithReports(t, reportCount)
|
|
directory := publishTestBundle(t, bundle)
|
|
bytesRead := make(map[string]int)
|
|
opens := make(map[string]int)
|
|
operations := defaultRecognitionOperations
|
|
operations.open = func(path string) (io.ReadCloser, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
name := filepath.Base(path)
|
|
opens[name]++
|
|
return &countingReadCloser{ReadCloser: file, count: func(n int) {
|
|
bytesRead[name] += n
|
|
}}, nil
|
|
}
|
|
|
|
if _, err := recognizeBundle(directory, operations); err != nil {
|
|
t.Fatalf("recognizeBundle() error = %v", err)
|
|
}
|
|
for _, name := range []string{ManifestFilename, DataPackageFilename} {
|
|
data := readFile(t, filepath.Join(directory, name))
|
|
if opens[name] != 1 || bytesRead[name] != len(data) {
|
|
t.Fatalf("%s opens/bytes = %d/%d, want 1/%d", name, opens[name], bytesRead[name], len(data))
|
|
}
|
|
}
|
|
for _, report := range bundle.Reports {
|
|
if opens[report.Path] != 1 || bytesRead[report.Path] != 0 {
|
|
t.Fatalf("report %s opens/bytes = %d/%d, want 1/0", report.Path, opens[report.Path], bytesRead[report.Path])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReplacementUsesEarlyAndFinalRecognition(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "comparison-daily")
|
|
bundle := testBundleWithReports(t, 12)
|
|
initialPlan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Publish(context.Background(), initialPlan, bundle); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
recognitions := 0
|
|
recognize := func(directory string) (Manifest, error) {
|
|
recognitions++
|
|
return RecognizeBundle(directory)
|
|
}
|
|
plan, err := planDestination(workingDirectory, target, true, recognize)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := publish(context.Background(), plan, bundle, publishOperations{
|
|
rename: os.Rename, removeAll: os.RemoveAll, recognize: recognize,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if recognitions != 2 {
|
|
t.Fatalf("full bundle recognitions = %d, want early and final authorization", recognitions)
|
|
}
|
|
}
|
|
|
|
func TestPublishReauthorizesMovedDestination(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
replace bool
|
|
requiresSymlink bool
|
|
prepare func(t *testing.T, workingDirectory, target string)
|
|
mutate func(t *testing.T, target string)
|
|
verify func(t *testing.T, target string)
|
|
}{
|
|
{
|
|
name: "regular file after absent preflight",
|
|
mutate: func(t *testing.T, target string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(target, []byte("unrelated file"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
verify: func(t *testing.T, target string) {
|
|
t.Helper()
|
|
if data := readFile(t, target); string(data) != "unrelated file" {
|
|
t.Fatalf("unrelated file data = %q", data)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "nonempty directory after empty preflight",
|
|
prepare: func(t *testing.T, _, target string) {
|
|
t.Helper()
|
|
if err := os.Mkdir(target, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
mutate: writeUnrecognizedDirectory,
|
|
verify: func(t *testing.T, target string) {
|
|
t.Helper()
|
|
if data := readFile(t, filepath.Join(target, "unrelated")); string(data) != "unrelated" {
|
|
t.Fatalf("unrelated data = %q", data)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "regular file after empty preflight",
|
|
prepare: func(t *testing.T, _, target string) {
|
|
t.Helper()
|
|
if err := os.Mkdir(target, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
mutate: func(t *testing.T, target string) {
|
|
t.Helper()
|
|
if err := os.WriteFile(target, []byte("unrelated file"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
verify: func(t *testing.T, target string) {
|
|
t.Helper()
|
|
if data := readFile(t, target); string(data) != "unrelated file" {
|
|
t.Fatalf("unrelated file data = %q", data)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "symlink after empty preflight",
|
|
requiresSymlink: true,
|
|
prepare: func(t *testing.T, _, target string) {
|
|
t.Helper()
|
|
if err := os.Mkdir(target, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
mutate: func(t *testing.T, target string) {
|
|
t.Helper()
|
|
testutil.RequireSymlink(t, "unrelated-target", target)
|
|
},
|
|
verify: func(t *testing.T, target string) {
|
|
t.Helper()
|
|
info, err := os.Lstat(target)
|
|
if err != nil || info.Mode()&os.ModeSymlink == 0 {
|
|
t.Fatalf("symlink stat = %v, %v", info, err)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "unrecognized directory after bundle preflight",
|
|
replace: true,
|
|
prepare: func(t *testing.T, workingDirectory, target string) {
|
|
t.Helper()
|
|
plan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Publish(context.Background(), plan, testBundle()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
mutate: writeUnrecognizedDirectory,
|
|
verify: func(t *testing.T, target string) {
|
|
t.Helper()
|
|
if data := readFile(t, filepath.Join(target, "unrelated")); string(data) != "unrelated" {
|
|
t.Fatalf("unrelated data = %q", data)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if test.requiresSymlink && runtime.GOOS == "windows" {
|
|
t.Skip("symlink replacement coverage requires Unix symlink semantics")
|
|
}
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "comparison-daily")
|
|
if test.prepare != nil {
|
|
test.prepare(t, workingDirectory, target)
|
|
}
|
|
plan, err := PlanDestination(workingDirectory, target, test.replace)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = publish(context.Background(), plan, testBundle(), publishOperations{
|
|
rename: os.Rename,
|
|
beforeCommit: func() {
|
|
if err := os.RemoveAll(target); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
test.mutate(t, target)
|
|
},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("publish() succeeded despite unauthorized replacement")
|
|
}
|
|
test.verify(t, target)
|
|
assertOnlyDestinationEntry(t, workingDirectory, filepath.Base(target))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPublishRetainsUnauthorizedMovedDestinationWhenRestoreFails(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "comparison-daily")
|
|
if err := os.Mkdir(target, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var backupPath string
|
|
calls := 0
|
|
_, err = publish(context.Background(), plan, testBundle(), publishOperations{
|
|
rename: func(oldPath, newPath string) error {
|
|
calls++
|
|
if calls == 1 {
|
|
backupPath = newPath
|
|
}
|
|
if calls == 2 {
|
|
return errors.New("restore failed")
|
|
}
|
|
return os.Rename(oldPath, newPath)
|
|
},
|
|
beforeCommit: func() {
|
|
if err := os.RemoveAll(target); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(target, []byte("unrelated file"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), backupPath) {
|
|
t.Fatalf("publish() error = %v, want retained backup path %q", err, backupPath)
|
|
}
|
|
if data := readFile(t, backupPath); string(data) != "unrelated file" {
|
|
t.Fatalf("retained backup data = %q", data)
|
|
}
|
|
if _, err := os.Lstat(target); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("target stat error = %v, want not exist", err)
|
|
}
|
|
assertOnlyDestinationEntry(t, workingDirectory, filepath.Base(backupPath))
|
|
}
|
|
|
|
func TestPublishRetainsMovedDestinationWhenTargetReappears(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "comparison-daily")
|
|
if err := os.Mkdir(target, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var backupPath string
|
|
_, err = publish(context.Background(), plan, testBundle(), publishOperations{
|
|
rename: func(oldPath, newPath string) error {
|
|
if backupPath != "" {
|
|
return os.Rename(oldPath, newPath)
|
|
}
|
|
backupPath = newPath
|
|
if err := os.Rename(oldPath, newPath); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(filepath.Join(backupPath, "unrelated"), []byte("unrelated"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(target, []byte("reappeared"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return nil
|
|
},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "reappeared") || !strings.Contains(err.Error(), backupPath) {
|
|
t.Fatalf("publish() error = %v, want reappeared target and retained backup path %q", err, backupPath)
|
|
}
|
|
if data := readFile(t, target); string(data) != "reappeared" {
|
|
t.Fatalf("reappeared target data = %q", data)
|
|
}
|
|
if data := readFile(t, filepath.Join(backupPath, "unrelated")); string(data) != "unrelated" {
|
|
t.Fatalf("retained backup data = %q", data)
|
|
}
|
|
assertDestinationEntries(t, workingDirectory, filepath.Base(target), filepath.Base(backupPath))
|
|
}
|
|
|
|
func TestPublishWritesAndReplacesBundle(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "comparison-daily")
|
|
plan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Publish(context.Background(), plan, testBundle()); err != nil {
|
|
t.Fatalf("Publish() error = %v", err)
|
|
}
|
|
if _, err := RecognizeBundle(target); err != nil {
|
|
t.Fatalf("RecognizeBundle() error = %v", err)
|
|
}
|
|
assertMode(t, target, 0o700)
|
|
for _, name := range []string{ManifestFilename, DataPackageFilename, "01-weather-light.md"} {
|
|
assertMode(t, filepath.Join(target, name), 0o600)
|
|
}
|
|
|
|
next := testBundle()
|
|
next.Manifest.Results[0].ProfileID = "weather-balanced"
|
|
next.Manifest.Results[0].ReportPath = "01-weather-balanced.md"
|
|
next.Reports[0].Path = "01-weather-balanced.md"
|
|
next.Reports[0].Markdown = []byte("# Replacement\n")
|
|
plan, err = PlanDestination(workingDirectory, target, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Publish(context.Background(), plan, next); err != nil {
|
|
t.Fatalf("Publish(replace) error = %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(target, "01-weather-light.md")); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("stale report stat error = %v, want not exist", err)
|
|
}
|
|
if data, err := os.ReadFile(filepath.Join(target, "01-weather-balanced.md")); err != nil || string(data) != "# Replacement\n" {
|
|
t.Fatalf("replacement report = %q, %v", data, err)
|
|
}
|
|
}
|
|
|
|
func TestPublishReplacesEmptyDirectory(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "comparison-daily")
|
|
if err := os.Mkdir(target, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Publish(context.Background(), plan, testBundle()); err != nil {
|
|
t.Fatalf("Publish() error = %v", err)
|
|
}
|
|
if _, err := RecognizeBundle(target); err != nil {
|
|
t.Fatalf("RecognizeBundle() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestComparisonDestinationPreflightsTransactionSiblingNames(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
missingParent := filepath.Join(workingDirectory, "missing")
|
|
target := filepath.Join(missingParent, strings.Repeat("a", maxComparisonComponentBytes+1))
|
|
if _, err := PlanDestination(workingDirectory, target, false); err == nil {
|
|
t.Fatal("PlanDestination() accepted an overlong destination name")
|
|
}
|
|
if _, err := os.Lstat(missingParent); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("missing parent stat error = %v, want not exist", err)
|
|
}
|
|
}
|
|
|
|
func TestPublishSupportsLongestDestinationComponent(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
parent := filepath.Join(workingDirectory, "nested")
|
|
target := filepath.Join(parent, strings.Repeat("a", maxComparisonComponentBytes))
|
|
|
|
plan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatalf("PlanDestination() error = %v", err)
|
|
}
|
|
if _, err := Publish(context.Background(), plan, testBundle()); err != nil {
|
|
t.Fatalf("Publish() error = %v", err)
|
|
}
|
|
if _, err := RecognizeBundle(target); err != nil {
|
|
t.Fatalf("RecognizeBundle() error = %v", err)
|
|
}
|
|
|
|
next := testBundle()
|
|
next.Reports[0].Markdown = []byte("# Replacement\n")
|
|
plan, err = PlanDestination(workingDirectory, target, true)
|
|
if err != nil {
|
|
t.Fatalf("PlanDestination(replace) error = %v", err)
|
|
}
|
|
if _, err := Publish(context.Background(), plan, next); err != nil {
|
|
t.Fatalf("Publish(replace) error = %v", err)
|
|
}
|
|
if data := readFile(t, filepath.Join(target, "01-weather-light.md")); string(data) != "# Replacement\n" {
|
|
t.Fatalf("replacement report = %q", data)
|
|
}
|
|
assertOnlyDestinationEntry(t, parent, filepath.Base(target))
|
|
}
|
|
|
|
func TestPublishReportsCommittedBundleWhenBackupCleanupFails(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "comparison-daily")
|
|
initialPlan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Publish(context.Background(), initialPlan, testBundle()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
previous := directorySnapshot(t, target)
|
|
|
|
next := testBundle()
|
|
next.Reports[0].Markdown = []byte("# Next\n")
|
|
plan, err := PlanDestination(workingDirectory, target, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cleanupCause := errors.New("backup removal failed")
|
|
var backupPath string
|
|
result, err := publish(context.Background(), plan, next, publishOperations{
|
|
rename: os.Rename,
|
|
removeAll: func(path string) error {
|
|
backupPath = path
|
|
return cleanupCause
|
|
},
|
|
})
|
|
var cleanupErr *PublicationCleanupError
|
|
if !result.Committed || result.RecoveryState != BackupRecoveryComplete || result.RecoveryPath != backupPath || !filepath.IsAbs(backupPath) || !errors.As(err, &cleanupErr) || cleanupErr.RecoveryState != BackupRecoveryComplete || cleanupErr.RecoveryPath != backupPath || !errors.Is(err, cleanupCause) {
|
|
t.Fatalf("publish() result/error = %#v/%v", result, err)
|
|
}
|
|
if _, err := RecognizeBundle(target); err != nil {
|
|
t.Fatalf("new bundle recognition error = %v", err)
|
|
}
|
|
if retained := directorySnapshot(t, backupPath); !equalSnapshots(previous, retained) {
|
|
t.Fatalf("retained backup = %#v, want %#v", retained, previous)
|
|
}
|
|
manifestData, err := os.ReadFile(filepath.Join(target, ManifestFilename))
|
|
if err != nil || strings.Contains(string(manifestData), backupPath) {
|
|
t.Fatalf("manifest/backup path = %q/%q, error = %v", manifestData, backupPath, err)
|
|
}
|
|
}
|
|
|
|
func TestPublishReportsPartialRecoveryAfterBackupCleanupFailure(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "comparison-daily")
|
|
initialPlan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Publish(context.Background(), initialPlan, testBundle()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, err := PlanDestination(workingDirectory, target, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cleanupCause := errors.New("backup removal failed")
|
|
var backupPath string
|
|
result, err := publish(context.Background(), plan, testBundle(), publishOperations{
|
|
rename: os.Rename,
|
|
removeAll: func(path string) error {
|
|
backupPath = path
|
|
if err := os.Remove(filepath.Join(path, ManifestFilename)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return cleanupCause
|
|
},
|
|
})
|
|
var cleanupErr *PublicationCleanupError
|
|
if !result.Committed || result.RecoveryState != BackupRecoveryPartial || result.RecoveryPath != backupPath || !errors.As(err, &cleanupErr) || cleanupErr.RecoveryState != BackupRecoveryPartial || cleanupErr.RecoveryPath != backupPath || !errors.Is(err, cleanupCause) {
|
|
t.Fatalf("publish() result/error = %#v/%v", result, err)
|
|
}
|
|
if _, err := RecognizeBundle(target); err != nil {
|
|
t.Fatalf("new bundle recognition error = %v", err)
|
|
}
|
|
if _, err := RecognizeBundle(backupPath); err == nil {
|
|
t.Fatal("partially removed backup was recognized")
|
|
}
|
|
}
|
|
|
|
func TestPublishReportsAbsentRecoveryAfterBackupCleanupFailure(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "comparison-daily")
|
|
initialPlan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Publish(context.Background(), initialPlan, testBundle()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, err := PlanDestination(workingDirectory, target, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cleanupCause := errors.New("backup removal failed")
|
|
var backupPath string
|
|
result, err := publish(context.Background(), plan, testBundle(), publishOperations{
|
|
rename: os.Rename,
|
|
removeAll: func(path string) error {
|
|
backupPath = path
|
|
if err := os.RemoveAll(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return cleanupCause
|
|
},
|
|
})
|
|
var cleanupErr *PublicationCleanupError
|
|
if !result.Committed || result.RecoveryState != BackupRecoveryAbsent || result.RecoveryPath != "" || !errors.As(err, &cleanupErr) || cleanupErr.RecoveryState != BackupRecoveryAbsent || cleanupErr.RecoveryPath != "" || !errors.Is(err, cleanupCause) {
|
|
t.Fatalf("publish() result/error = %#v/%v", result, err)
|
|
}
|
|
if _, err := RecognizeBundle(target); err != nil {
|
|
t.Fatalf("new bundle recognition error = %v", err)
|
|
}
|
|
if _, err := os.Lstat(backupPath); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("backup stat error = %v, want not exist", err)
|
|
}
|
|
}
|
|
|
|
func TestPublishRestoresExistingBundleAfterReplacementFailure(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "comparison-daily")
|
|
initial := testBundle()
|
|
initialPlan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Publish(context.Background(), initialPlan, initial); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := directorySnapshot(t, target)
|
|
|
|
next := testBundle()
|
|
next.Reports[0].Markdown = []byte("# New\n")
|
|
plan, err := PlanDestination(workingDirectory, target, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
calls := 0
|
|
publication, err := publish(context.Background(), plan, next, publishOperations{rename: func(oldPath, newPath string) error {
|
|
calls++
|
|
if calls == 2 {
|
|
return errors.New("replace failed")
|
|
}
|
|
return os.Rename(oldPath, newPath)
|
|
}})
|
|
if publication.Committed || err == nil {
|
|
t.Fatal("publish() succeeded despite replacement failure")
|
|
}
|
|
after := directorySnapshot(t, target)
|
|
if !equalSnapshots(before, after) {
|
|
t.Fatal("failed publication changed the prior bundle")
|
|
}
|
|
entries, err := os.ReadDir(workingDirectory)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(entries) != 1 || entries[0].Name() != filepath.Base(target) {
|
|
t.Fatalf("failed publication left sibling artifacts: %#v", entries)
|
|
}
|
|
}
|
|
|
|
func TestPublishRetainsBackupWhenRestorationFails(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "comparison-daily")
|
|
initialPlan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Publish(context.Background(), initialPlan, testBundle()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, err := PlanDestination(workingDirectory, target, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var backupPath string
|
|
calls := 0
|
|
_, err = publish(context.Background(), plan, testBundle(), publishOperations{rename: func(oldPath, newPath string) error {
|
|
calls++
|
|
if calls == 1 {
|
|
backupPath = newPath
|
|
}
|
|
if calls == 2 || calls == 3 {
|
|
return errors.New("rename failed")
|
|
}
|
|
return os.Rename(oldPath, newPath)
|
|
}})
|
|
if err == nil || !strings.Contains(err.Error(), backupPath) {
|
|
t.Fatalf("publish() error = %v, want retained backup path %q", err, backupPath)
|
|
}
|
|
if info, statErr := os.Stat(backupPath); statErr != nil || !info.IsDir() {
|
|
t.Fatalf("backup stat = %v, %v, want retained directory", info, statErr)
|
|
}
|
|
}
|
|
|
|
func TestPublishCancellationBeforeCommitLeavesNoDestination(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "comparison-daily")
|
|
plan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
if publication, err := publish(ctx, plan, testBundle(), publishOperations{rename: os.Rename, beforeCommit: cancel}); publication.Committed || !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("Publish() error = %v, want context cancellation", err)
|
|
}
|
|
if _, err := os.Lstat(target); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("destination stat error = %v, want not exist", err)
|
|
}
|
|
}
|
|
|
|
func TestPublishRestoresPriorDestinationWhenCanceledAfterBackup(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
prepare func(t *testing.T, workingDirectory, target string)
|
|
}{
|
|
{
|
|
name: "empty directory",
|
|
prepare: func(t *testing.T, _, target string) {
|
|
t.Helper()
|
|
if err := os.Mkdir(target, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "recognized bundle",
|
|
prepare: func(t *testing.T, workingDirectory, target string) {
|
|
t.Helper()
|
|
plan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Publish(context.Background(), plan, testBundle()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "comparison-daily")
|
|
test.prepare(t, workingDirectory, target)
|
|
before := directorySnapshot(t, target)
|
|
|
|
plan, err := PlanDestination(workingDirectory, target, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
next := testBundle()
|
|
next.Reports[0].Markdown = []byte("# Replacement\n")
|
|
publication, err := publish(ctx, plan, next, publishOperations{rename: func(oldPath, newPath string) error {
|
|
if err := os.Rename(oldPath, newPath); err != nil {
|
|
return err
|
|
}
|
|
if oldPath == target {
|
|
cancel()
|
|
}
|
|
return nil
|
|
}})
|
|
if publication.Committed || !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("publish() result/error = %#v/%v, want canceled uncommitted publication", publication, err)
|
|
}
|
|
if after := directorySnapshot(t, target); !equalSnapshots(before, after) {
|
|
t.Fatalf("restored destination = %#v, want %#v", after, before)
|
|
}
|
|
assertOnlyDestinationEntry(t, workingDirectory, filepath.Base(target))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPublishRetainsBackupWhenCancellationRestorationFails(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "comparison-daily")
|
|
initialPlan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Publish(context.Background(), initialPlan, testBundle()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, err := PlanDestination(workingDirectory, target, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
restoreCause := errors.New("restore failed")
|
|
var backupPath string
|
|
calls := 0
|
|
publication, err := publish(ctx, plan, testBundle(), publishOperations{rename: func(oldPath, newPath string) error {
|
|
calls++
|
|
if calls == 2 {
|
|
return restoreCause
|
|
}
|
|
if err := os.Rename(oldPath, newPath); err != nil {
|
|
return err
|
|
}
|
|
if calls == 1 {
|
|
backupPath = newPath
|
|
cancel()
|
|
}
|
|
return nil
|
|
}})
|
|
if publication.Committed || !errors.Is(err, context.Canceled) || !errors.Is(err, restoreCause) {
|
|
t.Fatalf("publish() result/error = %#v/%v, want cancellation and restoration failure", publication, err)
|
|
}
|
|
if info, statErr := os.Stat(backupPath); statErr != nil || !info.IsDir() {
|
|
t.Fatalf("backup stat = %v, %v, want retained directory", info, statErr)
|
|
}
|
|
if _, statErr := os.Lstat(target); !errors.Is(statErr, os.ErrNotExist) {
|
|
t.Fatalf("destination stat error = %v, want not exist", statErr)
|
|
}
|
|
assertOnlyDestinationEntry(t, workingDirectory, filepath.Base(backupPath))
|
|
}
|
|
|
|
func TestPublishReturnsCommittedBundleWhenCanceledAfterInstall(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "comparison-daily")
|
|
initialPlan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Publish(context.Background(), initialPlan, testBundle()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
next := testBundle()
|
|
next.Reports[0].Markdown = []byte("# Next\n")
|
|
plan, err := PlanDestination(workingDirectory, target, true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
publication, err := publish(ctx, plan, next, publishOperations{rename: func(oldPath, newPath string) error {
|
|
if err := os.Rename(oldPath, newPath); err != nil {
|
|
return err
|
|
}
|
|
if newPath == target && oldPath != target {
|
|
cancel()
|
|
}
|
|
return nil
|
|
}})
|
|
if !publication.Committed || err != nil {
|
|
t.Fatalf("publish() result/error = %#v/%v, want committed bundle", publication, err)
|
|
}
|
|
if err := ctx.Err(); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("context error = %v, want cancellation", err)
|
|
}
|
|
if got := string(readFile(t, filepath.Join(target, next.Reports[0].Path))); got != string(next.Reports[0].Markdown) {
|
|
t.Fatalf("published report = %q, want %q", got, next.Reports[0].Markdown)
|
|
}
|
|
if _, err := RecognizeBundle(target); err != nil {
|
|
t.Fatalf("committed bundle recognition error = %v", err)
|
|
}
|
|
assertOnlyDestinationEntry(t, workingDirectory, filepath.Base(target))
|
|
}
|
|
|
|
func assertDestinationErrorKind(t *testing.T, workingDirectory, target string, replace bool, want DestinationErrorKind) {
|
|
t.Helper()
|
|
_, err := PlanDestination(workingDirectory, target, replace)
|
|
var destinationError *DestinationError
|
|
if !errors.As(err, &destinationError) || destinationError.Kind != want {
|
|
t.Fatalf("PlanDestination(%q) error = %v, want destination error %q", target, err, want)
|
|
}
|
|
}
|
|
|
|
func publishTestBundle(t *testing.T, bundle LogicalBundle) string {
|
|
t.Helper()
|
|
workingDirectory := t.TempDir()
|
|
target := filepath.Join(workingDirectory, "comparison-daily")
|
|
plan, err := PlanDestination(workingDirectory, target, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := Publish(context.Background(), plan, bundle); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return target
|
|
}
|
|
|
|
func writeUnrecognizedDirectory(t *testing.T, target string) {
|
|
t.Helper()
|
|
if err := os.Mkdir(target, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(target, "unrelated"), []byte("unrelated"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func readFile(t *testing.T, path string) []byte {
|
|
t.Helper()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func assertOnlyDestinationEntry(t *testing.T, directory, want string) {
|
|
t.Helper()
|
|
assertDestinationEntries(t, directory, want)
|
|
}
|
|
|
|
func assertDestinationEntries(t *testing.T, directory string, wants ...string) {
|
|
t.Helper()
|
|
entries, err := os.ReadDir(directory)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(entries) != len(wants) {
|
|
t.Fatalf("directory entries = %#v, want %#v", entries, wants)
|
|
}
|
|
for _, want := range wants {
|
|
found := false
|
|
for _, entry := range entries {
|
|
if entry.Name() == want {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatalf("directory entries = %#v, missing %q", entries, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testBundle() LogicalBundle {
|
|
dataPackage := []byte("report: daily\n")
|
|
manifest := validManifest()
|
|
manifest.DataPackage.SHA256 = SHA256(dataPackage)
|
|
return LogicalBundle{
|
|
Manifest: manifest,
|
|
DataPackage: dataPackage,
|
|
Reports: []BundleReport{{
|
|
Position: 1,
|
|
Path: "01-weather-light.md",
|
|
Markdown: []byte("# Daily\n"),
|
|
}},
|
|
}
|
|
}
|
|
|
|
func testBundleWithReports(t *testing.T, count int) LogicalBundle {
|
|
t.Helper()
|
|
dataPackage := []byte("report: daily\n")
|
|
manifest := validManifest()
|
|
manifest.Total, manifest.Succeeded, manifest.Failed = count, count, 0
|
|
manifest.Results = make([]Result, count)
|
|
reports := make([]BundleReport, count)
|
|
for i := range manifest.Results {
|
|
position := i + 1
|
|
profileID := fmt.Sprintf("weather-%d", position)
|
|
path, err := ReportFilename(position, count, profileID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
manifest.Results[i] = Result{
|
|
Position: position, ProfileID: profileID, ModelName: "gpt-5-mini",
|
|
Status: StatusSucceeded, ValidationStatus: "passed", ReportPath: path,
|
|
}
|
|
reports[i] = BundleReport{Position: position, Path: path, Markdown: []byte("# Daily\n")}
|
|
}
|
|
manifest.DataPackage.SHA256 = SHA256(dataPackage)
|
|
return LogicalBundle{Manifest: manifest, DataPackage: dataPackage, Reports: reports}
|
|
}
|
|
|
|
type countingReadCloser struct {
|
|
io.ReadCloser
|
|
count func(int)
|
|
}
|
|
|
|
func (reader *countingReadCloser) Read(buffer []byte) (int, error) {
|
|
n, err := reader.ReadCloser.Read(buffer)
|
|
reader.count(n)
|
|
return n, err
|
|
}
|
|
|
|
func assertMode(t *testing.T, path string, want os.FileMode) {
|
|
t.Helper()
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := info.Mode().Perm(); got != want {
|
|
t.Fatalf("mode for %q = %#o, want %#o", path, got, want)
|
|
}
|
|
}
|
|
|
|
func directorySnapshot(t *testing.T, directory string) map[string]string {
|
|
t.Helper()
|
|
entries, err := os.ReadDir(directory)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
snapshot := make(map[string]string, len(entries))
|
|
for _, entry := range entries {
|
|
data, err := os.ReadFile(filepath.Join(directory, entry.Name()))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
snapshot[entry.Name()] = string(data)
|
|
}
|
|
return snapshot
|
|
}
|
|
|
|
func equalSnapshots(left, right map[string]string) bool {
|
|
if len(left) != len(right) {
|
|
return false
|
|
}
|
|
for name, value := range left {
|
|
if right[name] != value {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestRecognizeBundleRejectsTrailingJSONValue(t *testing.T) {
|
|
directory := publishTestBundle(t, testBundle())
|
|
path := filepath.Join(directory, ManifestFilename)
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, append(data, []byte("{}")...), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := RecognizeBundle(directory); !errors.Is(err, ErrUnrecognizedBundle) {
|
|
t.Fatalf("RecognizeBundle() error = %v, want ErrUnrecognizedBundle", err)
|
|
}
|
|
}
|
|
|
|
func TestPlanDestinationRejectsWhitespaceTarget(t *testing.T) {
|
|
workingDirectory := t.TempDir()
|
|
assertDestinationErrorKind(t, workingDirectory, " \t", false, DestinationInvalidPath)
|
|
}
|
|
|
|
func TestRecognizeBundleDoesNotAcceptNonRegularManifest(t *testing.T) {
|
|
directory := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(directory, ManifestFilename), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := RecognizeBundle(directory); !strings.Contains(err.Error(), "regular file") {
|
|
t.Fatalf("RecognizeBundle() error = %v, want regular file rejection", err)
|
|
}
|
|
}
|