538 lines
19 KiB
Go
538 lines
19 KiB
Go
package publish
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestLoadEvidenceValidatesKnownByteFieldsAsBase64Strings(t *testing.T) {
|
|
for _, test := range []struct{ name, value, want, failure string }{
|
|
{"valid", `"djEuMi4z"`, "v1.2.3", ""},
|
|
{"empty", `""`, "", ""},
|
|
{"malformed base64", `"not base64!"`, "", "base64"},
|
|
{"numeric array", `[118,49,46,50,46,51]`, "", "type"},
|
|
{"object", `{}`, "", "type"},
|
|
{"boolean", `true`, "", "type"},
|
|
{"number", `123`, "", "type"},
|
|
{"null", `null`, "", "schema"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "record.json")
|
|
contents := `{"format_version":2,"evidence":{},"evidence_bytes":{"tag":` + test.value + `}}`
|
|
if err := os.WriteFile(path, []byte(contents), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, err := LoadEvidence(path)
|
|
if test.failure == "" {
|
|
if err != nil || loaded.Tag != test.want {
|
|
t.Fatalf("byte string = %#v, %v", loaded, err)
|
|
}
|
|
return
|
|
}
|
|
var typed *RecoveryError
|
|
if !errors.As(err, &typed) || typed.Kind != "record_format" || typed.Unwrap() == nil {
|
|
t.Fatalf("byte format failure = %v", err)
|
|
}
|
|
switch test.failure {
|
|
case "type":
|
|
var cause *json.UnmarshalTypeError
|
|
if !errors.As(err, &cause) {
|
|
t.Fatalf("lost JSON type cause: %v", err)
|
|
}
|
|
case "base64":
|
|
var cause base64.CorruptInputError
|
|
if !errors.As(err, &cause) {
|
|
t.Fatalf("lost base64 cause: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRecordsRoundTripNonUTF8Evidence(t *testing.T) {
|
|
repo := recordRepo(t)
|
|
raw := "path-\xff"
|
|
evidence := Evidence{RepositoryDir: raw, ModuleDir: raw, ModulePath: raw, Version: raw, Tag: raw, CandidateCommit: raw, TagObject: raw, Endpoint: raw, Remote: raw, RecordedAt: raw}
|
|
path, err := SaveEvidence(t.Context(), repo, t.TempDir(), evidence)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, err := LoadEvidence(path)
|
|
if err != nil || loaded != evidence {
|
|
t.Fatalf("lossy evidence: %#v, %v", loaded, err)
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var envelope recordEnvelope
|
|
if err := json.Unmarshal(data, &envelope); err != nil || envelope.FormatVersion != recordByteFormatVersion {
|
|
t.Fatalf("byte record envelope: %#v, %v", envelope, err)
|
|
}
|
|
// Unknown fields remain ignorable; known byte fields retain real codec errors.
|
|
for _, test := range []struct {
|
|
field string
|
|
wantError bool
|
|
}{{"endpoint", true}, {"future_field", false}} {
|
|
envelope.EvidenceBytes = json.RawMessage(`{"` + test.field + `":"not base64!"}`)
|
|
data, err := json.Marshal(envelope)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, data, 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = LoadEvidence(path)
|
|
if (err != nil) != test.wantError {
|
|
t.Fatalf("byte field %s error=%v", test.field, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRecordLoadedObservationKeepsNonUTF8EndpointIdentity(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("raw pathname bytes are a POSIX behavior")
|
|
}
|
|
repo := recordRepo(t)
|
|
base := t.TempDir()
|
|
original := filepath.Join(base, "remote-\xff.git")
|
|
decoy := filepath.Join(base, "remote-\uFFFD.git")
|
|
for _, remote := range []string{original, decoy} {
|
|
recordGit(t, repo, "init", "--bare", remote)
|
|
}
|
|
recordGit(t, repo, "tag", "-a", "v1.2.3", "-m", "Release")
|
|
recordGit(t, repo, "push", decoy, "refs/tags/v1.2.3")
|
|
evidence := Evidence{RepositoryDir: repo, ModuleDir: repo, Endpoint: original, Tag: "v1.2.3", TagObject: strings.TrimSpace(recordGit(t, repo, "rev-parse", "v1.2.3")), CandidateCommit: strings.TrimSpace(recordGit(t, repo, "rev-parse", "HEAD"))}
|
|
path, err := SaveEvidence(t.Context(), repo, "", evidence)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, err := LoadEvidence(path)
|
|
if err != nil || loaded.Endpoint != original {
|
|
t.Fatalf("endpoint changed: %q, %v", loaded.Endpoint, err)
|
|
}
|
|
for _, selected := range []Evidence{evidence, loaded} {
|
|
observed, err := Reconcile(t.Context(), selected)
|
|
if err != nil || observed.State != NotPublished {
|
|
t.Fatalf("original endpoint observation = %#v, %v", observed, err)
|
|
}
|
|
}
|
|
loaded.Endpoint = decoy
|
|
observed, err := Reconcile(t.Context(), loaded)
|
|
if err != nil || observed.State != Published {
|
|
t.Fatalf("decoy does not distinguish identity: %#v, %v", observed, err)
|
|
}
|
|
}
|
|
|
|
func recordGit(t *testing.T, dir string, args ...string) string {
|
|
t.Helper()
|
|
cmd := exec.Command("git", append([]string{"-C", dir}, args...)...)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("git %v: %v: %s", args, err, out)
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
func recordRepo(t *testing.T) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
recordGit(t, dir, "init", "-q")
|
|
recordGit(t, dir, "config", "user.name", "Test")
|
|
recordGit(t, dir, "config", "user.email", "test@example.invalid")
|
|
if err := os.WriteFile(filepath.Join(dir, "file"), []byte("x"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
recordGit(t, dir, "add", "file")
|
|
recordGit(t, dir, "commit", "-qm", "init")
|
|
return dir
|
|
}
|
|
|
|
func TestLoadEvidenceAcceptsPartialAndRejectsMalformedRecords(t *testing.T) {
|
|
dir := t.TempDir()
|
|
partial := filepath.Join(dir, "partial.json")
|
|
if err := os.WriteFile(partial, []byte(`{"format_version":1,"evidence":{}}`), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := LoadEvidence(partial)
|
|
if err != nil || got != (Evidence{}) {
|
|
t.Fatalf("partial record = %#v, %v", got, err)
|
|
}
|
|
for name, data := range map[string]string{"bad-json": "{", "unsupported": `{"format_version":3,"evidence":{}}`, "type": `{"format_version":1,"evidence":{"tag":false}}`, "trailing": `{"format_version":1,"evidence":{}} {}`} {
|
|
t.Run(name, func(t *testing.T) {
|
|
path := filepath.Join(dir, name)
|
|
if err := os.WriteFile(path, []byte(data), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := LoadEvidence(path)
|
|
var recovery *RecoveryError
|
|
if !errors.As(err, &recovery) || recovery.Kind != "record_format" {
|
|
t.Fatalf("LoadEvidence() error=%v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadEvidenceRequiresOneDocumentAndPreservesSyntaxCause(t *testing.T) {
|
|
const valid = `{"format_version":1,"evidence":{"tag":"v1.2.3","future":{}},"future":true}`
|
|
for _, test := range []struct {
|
|
name, data string
|
|
valid bool
|
|
}{
|
|
{"single", valid, true}, {"whitespace", valid + "\n \t\r", true},
|
|
{"first incomplete", "{", false}, {"first syntax", "x", false},
|
|
{"second object", valid + ` {"unused":[1,2,3]}`, false},
|
|
{"incomplete suffix", valid + " {", false}, {"invalid suffix", valid + " x", false},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "record.json")
|
|
if err := os.WriteFile(path, []byte(test.data), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, err := LoadEvidence(path)
|
|
if test.valid {
|
|
if err != nil || loaded.Tag != "v1.2.3" {
|
|
t.Fatalf("valid record = %#v, %v", loaded, err)
|
|
}
|
|
return
|
|
}
|
|
var recovery *RecoveryError
|
|
var syntax *json.SyntaxError
|
|
if !errors.As(err, &recovery) || recovery.Kind != "record_format" || !errors.As(err, &syntax) {
|
|
t.Fatalf("lost format/parser cause: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadEvidenceReadFailureHasTypedError(t *testing.T) {
|
|
_, err := LoadEvidence(filepath.Join(t.TempDir(), "missing.json"))
|
|
var recovery *RecoveryError
|
|
if !errors.As(err, &recovery) || recovery.Kind != "record_read" || !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("LoadEvidence() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSaveEvidenceUsesPrivateUniqueRecordsAndLinkedWorktreeMetadata(t *testing.T) {
|
|
repo := recordRepo(t)
|
|
worktree := filepath.Join(t.TempDir(), "linked")
|
|
recordGit(t, repo, "worktree", "add", "--detach", worktree)
|
|
evidence := Evidence{Tag: "v1.2.3", Endpoint: "https://user:secret@example.invalid/repo.git"}
|
|
first, err := SaveEvidence(t.Context(), worktree, "", evidence)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := SaveEvidence(t.Context(), worktree, "", evidence)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first == second {
|
|
t.Fatalf("record paths collided: %q", first)
|
|
}
|
|
common := recordGit(t, repo, "rev-parse", "--path-format=absolute", "--git-common-dir")
|
|
wantDir := filepath.Join(strings.TrimSpace(common), "releasekit", "publication-attempts")
|
|
if filepath.Dir(first) != wantDir {
|
|
t.Fatalf("record dir=%q want %q", filepath.Dir(first), wantDir)
|
|
}
|
|
dirInfo, err := os.Stat(wantDir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if dirInfo.Mode().Perm() != 0700 {
|
|
t.Fatalf("record directory permissions=%o", dirInfo.Mode().Perm())
|
|
}
|
|
info, err := os.Stat(first)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.Mode().Perm() != 0600 {
|
|
t.Fatalf("record permissions=%o", info.Mode().Perm())
|
|
}
|
|
loaded, err := LoadEvidence(first)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if loaded.Endpoint != evidence.Endpoint || loaded.RecordedAt == "" {
|
|
t.Fatalf("loaded evidence=%#v", loaded)
|
|
}
|
|
var envelope map[string]json.RawMessage
|
|
data, err := os.ReadFile(first)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := json.Unmarshal(data, &envelope); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(data), "user:secret@example.invalid") {
|
|
t.Fatal("private record did not retain the exact endpoint")
|
|
}
|
|
}
|
|
|
|
func TestSaveEvidenceResolvesRelativeAndIgnoredOverrides(t *testing.T) {
|
|
repo := recordRepo(t)
|
|
if err := os.WriteFile(filepath.Join(repo, ".gitignore"), []byte("saved-records/\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := SaveEvidence(t.Context(), repo, "saved-records", Evidence{Tag: "v1.2.3"}); err != nil {
|
|
t.Fatalf("ignored override: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(repo, "saved-records")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
external := filepath.Join(t.TempDir(), "external-store")
|
|
path, err := SaveEvidence(t.Context(), repo, external, Evidence{Tag: "v2.0.0"})
|
|
if err != nil || !strings.HasPrefix(path, external+string(filepath.Separator)) {
|
|
t.Fatalf("absolute external override path=%q error=%v", path, err)
|
|
}
|
|
}
|
|
|
|
func TestSaveEvidenceAllowsExplicitGitMetadataStores(t *testing.T) {
|
|
for _, layout := range []string{"ordinary", "linked", "separate"} {
|
|
t.Run(layout, func(t *testing.T) {
|
|
repo := recordRepo(t)
|
|
switch layout {
|
|
case "linked":
|
|
linked := filepath.Join(t.TempDir(), "linked")
|
|
recordGit(t, repo, "worktree", "add", "--detach", linked)
|
|
repo = linked
|
|
case "separate":
|
|
recordGit(t, repo, "init", "--separate-git-dir", filepath.Join(t.TempDir(), "metadata"))
|
|
}
|
|
defaultPath, err := SaveEvidence(t.Context(), repo, "", Evidence{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
privateDir := strings.TrimSuffix(recordGit(t, repo, "rev-parse", "--absolute-git-dir"), "\n")
|
|
for _, directory := range []string{filepath.Dir(defaultPath), filepath.Join(privateDir, "other-private-records")} {
|
|
path, err := SaveEvidence(t.Context(), repo, directory, Evidence{Tag: "v1.2.3"})
|
|
if err != nil || filepath.Dir(path) != directory {
|
|
t.Fatalf("explicit metadata store %q = %q, %v", directory, path, err)
|
|
}
|
|
}
|
|
if status := recordGit(t, repo, "status", "--porcelain"); status != "" {
|
|
t.Fatalf("metadata records appeared in candidate: %s", status)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRecordStoragePreservesWhitespaceInRepositoryAndMetadataPaths(t *testing.T) {
|
|
for _, separate := range []bool{false, true} {
|
|
t.Run(fmt.Sprintf("separate=%t", separate), func(t *testing.T) {
|
|
original := recordRepo(t)
|
|
repo := original + " "
|
|
if err := os.Rename(original, repo); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// recordRepo's temporary-directory cleanup owns the old spelling.
|
|
t.Cleanup(func() { _ = os.RemoveAll(repo) })
|
|
if separate {
|
|
recordGit(t, repo, "init", "--separate-git-dir", filepath.Join(t.TempDir(), "metadata "))
|
|
}
|
|
if err := os.WriteFile(filepath.Join(repo, ".gitignore"), []byte("saved/\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
recordGit(t, repo, "add", ".gitignore")
|
|
recordGit(t, repo, "commit", "-qm", "Ignore saved records")
|
|
for _, override := range []string{"", "saved"} {
|
|
path, err := SaveEvidence(t.Context(), repo, override, Evidence{RepositoryDir: repo, Tag: "v1.2.3"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listed, err := ListAttempts(t.Context(), repo, override)
|
|
if err != nil || len(listed.Attempts) != 1 || listed.Attempts[0].RecordPath != path || listed.Attempts[0].Evidence.RepositoryDir != repo {
|
|
t.Fatalf("read/write disagreement for %q: %#v, %v", override, listed, err)
|
|
}
|
|
if override != "" && filepath.Dir(path) != filepath.Join(repo, override) {
|
|
t.Fatalf("changed repository path: %q", path)
|
|
}
|
|
}
|
|
if _, err := SaveEvidence(t.Context(), repo, filepath.Join(repo, "unignored"), Evidence{}); err == nil {
|
|
t.Fatal("absolute unignored store was misclassified as external")
|
|
}
|
|
if status := recordGit(t, repo, "status", "--porcelain"); status != "" {
|
|
t.Fatalf("recording changed candidate: %s", status)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSaveEvidenceRejectsUnignoredOverrideInsideWorktree(t *testing.T) {
|
|
repo := recordRepo(t)
|
|
_, err := SaveEvidence(t.Context(), repo, "records", Evidence{Tag: "v1.2.3"})
|
|
var recovery *RecoveryError
|
|
if !errors.As(err, &recovery) || recovery.Kind != "record_write" {
|
|
t.Fatalf("SaveEvidence() error=%v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(repo, "records")); err != nil {
|
|
t.Fatalf("expected parent directory to exist without record: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(repo, "records", "attempt")); !os.IsNotExist(err) {
|
|
t.Fatalf("unexpected record: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSaveEvidenceChecksEachActualFilename(t *testing.T) {
|
|
repo := recordRepo(t)
|
|
if err := os.WriteFile(filepath.Join(repo, ".gitignore"), []byte("selective/attempt-[0-7]*.json\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
recordGit(t, repo, "add", ".gitignore")
|
|
recordGit(t, repo, "commit", "-qm", "Selective ignore")
|
|
dir := filepath.Join(repo, "selective")
|
|
next := func(name string) func(string) (string, error) {
|
|
return func(directory string) (string, error) { return filepath.Join(directory, name), nil }
|
|
}
|
|
path, err := saveEvidenceNamed(t.Context(), repo, repo, dir, dir, Evidence{Tag: "v1.0.0"}, next("attempt-0.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
recordGit(t, repo, "check-ignore", "--", path)
|
|
// A collision must validate the replacement name as well.
|
|
calls := 0
|
|
_, err = saveEvidenceNamed(t.Context(), repo, repo, dir, dir, Evidence{Tag: "v2.0.0"}, func(directory string) (string, error) {
|
|
calls++
|
|
name := "attempt-0.json"
|
|
if calls > 1 {
|
|
name = "attempt-c.json"
|
|
}
|
|
return filepath.Join(directory, name), nil
|
|
})
|
|
if err == nil || calls != 2 {
|
|
t.Fatalf("unignored replacement: calls=%d error=%v", calls, err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(dir, "attempt-c.json")); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("unignored file was created: %v", err)
|
|
}
|
|
if status := recordGit(t, repo, "status", "--porcelain"); status != "" {
|
|
t.Fatalf("recording dirtied candidate: %s", status)
|
|
}
|
|
loaded, err := LoadEvidence(path)
|
|
if err != nil || loaded.Tag != "v1.0.0" {
|
|
t.Fatalf("collision changed original record: %#v, %v", loaded, err)
|
|
}
|
|
}
|
|
|
|
func TestListAttemptsReturnsReadableRecordsAndDiagnosticsInPathOrder(t *testing.T) {
|
|
repo := recordRepo(t)
|
|
directory := filepath.Join(t.TempDir(), "attempts")
|
|
if err := os.Mkdir(directory, 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, _ := SaveEvidence(t.Context(), repo, directory, Evidence{Tag: "v2.0.0"})
|
|
first, _ := SaveEvidence(t.Context(), repo, directory, Evidence{Tag: "v1.0.0"})
|
|
if err := os.WriteFile(filepath.Join(directory, "broken.json"), []byte("{"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listed, err := ListAttempts(t.Context(), repo, directory)
|
|
if err != nil || len(listed.Attempts) != 2 || len(listed.Diagnostics) != 1 {
|
|
t.Fatalf("list=%#v error=%v", listed, err)
|
|
}
|
|
wantPaths := []string{first, second}
|
|
sort.Strings(wantPaths)
|
|
if listed.Attempts[0].RecordPath != wantPaths[0] || listed.Attempts[1].RecordPath != wantPaths[1] {
|
|
t.Fatalf("attempt order=%v", listed.Attempts)
|
|
}
|
|
if listed.Diagnostics[0].Kind != "record_format" || listed.Diagnostics[0].RecordPath == "" {
|
|
t.Fatalf("diagnostic=%#v", listed.Diagnostics[0])
|
|
}
|
|
}
|
|
|
|
func TestListAttemptsFiltersMixedDirectoryAcrossBatches(t *testing.T) {
|
|
directory := t.TempDir()
|
|
for i := range 400 {
|
|
if err := os.WriteFile(filepath.Join(directory, fmt.Sprintf("unrelated-%04d.txt", i)), []byte("not a record"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := os.Mkdir(filepath.Join(directory, "directory.json"), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
names := []string{"z.json", "b.json", "a.json"}
|
|
for _, name := range names {
|
|
if err := os.WriteFile(filepath.Join(directory, name), []byte(`{"format_version":1,"evidence":{"tag":"v1.2.3"}}`), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
listed, err := ListAttempts(t.Context(), "", directory)
|
|
if err != nil || len(listed.Diagnostics) != 0 || len(listed.Attempts) != len(names) {
|
|
t.Fatalf("filtered result = %#v, %v", listed, err)
|
|
}
|
|
sort.Strings(names)
|
|
for i, name := range names {
|
|
if listed.Attempts[i].RecordPath != filepath.Join(directory, name) {
|
|
t.Fatalf("incorrect path order: %#v", listed.Attempts)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListAttemptsRetainsEntriesWithEnumerationFailure(t *testing.T) {
|
|
directory := t.TempDir()
|
|
for name, contents := range map[string]string{
|
|
"z.json": `{"format_version":1,"evidence":{"tag":"v2.0.0"}}`,
|
|
"a.json": `{"format_version":1,"evidence":{"tag":"v1.0.0"}}`,
|
|
"broken.json": "{", "unrelated.txt": "ignored",
|
|
} {
|
|
if err := os.WriteFile(filepath.Join(directory, name), []byte(contents), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
entries, err := os.ReadDir(directory)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
failure := errors.New("controlled directory read failure")
|
|
for _, empty := range []bool{false, true} {
|
|
calls := 0
|
|
listed, err := listAttempts(t.Context(), "", directory, func(ctx context.Context, dir string) ([]string, error) {
|
|
return readRecordPaths(ctx, dir, func(int) ([]os.DirEntry, error) {
|
|
calls++
|
|
if empty {
|
|
return nil, failure
|
|
}
|
|
return entries, failure
|
|
})
|
|
})
|
|
var recovery *RecoveryError
|
|
if !errors.As(err, &recovery) || recovery.Kind != "storage_enumeration" || !errors.Is(err, failure) || calls != 1 {
|
|
t.Fatalf("enumeration failure lost: %#v, %v; reads=%d", listed, err, calls)
|
|
}
|
|
if empty {
|
|
if len(listed.Attempts) != 0 || len(listed.Diagnostics) != 0 {
|
|
t.Fatalf("empty read: %#v", listed)
|
|
}
|
|
} else if len(listed.Attempts) != 2 || len(listed.Diagnostics) != 1 || filepath.Base(listed.Attempts[0].RecordPath) != "a.json" || filepath.Base(listed.Attempts[1].RecordPath) != "z.json" || listed.Diagnostics[0].Kind != "record_format" {
|
|
t.Fatalf("partial read dropped/reordered records: %#v", listed)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListAttemptsDistinguishesEmptyMissingAndCanceledStorage(t *testing.T) {
|
|
empty := t.TempDir()
|
|
listed, err := ListAttempts(t.Context(), "", empty)
|
|
if err != nil || len(listed.Attempts) != 0 || len(listed.Diagnostics) != 0 {
|
|
t.Fatalf("empty list=%#v error=%v", listed, err)
|
|
}
|
|
_, err = ListAttempts(t.Context(), "", filepath.Join(t.TempDir(), "missing"))
|
|
var recovery *RecoveryError
|
|
if !errors.As(err, &recovery) || recovery.Kind != "storage_enumeration" {
|
|
t.Fatalf("missing storage error=%v", err)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
_, err = ListAttempts(ctx, "", empty)
|
|
if !errors.As(err, &recovery) || recovery.Kind != "canceled" || !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("canceled error=%v", err)
|
|
}
|
|
}
|