v0.6.0: strengthen backup integrity
This commit is contained in:
+270
-1
@@ -3815,7 +3815,7 @@ func TestReportDiffVerifyRetryFailedAndConfig(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.Close()
|
||||
if err := os.WriteFile(filepath.Join(dir, "file.jpg"), []byte("x"), 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(dir, "file.jpg"), []byte("0123456789"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
appendFailure(dir, pendingAsset{asset: photos.Asset{ID: "bad", Filename: "bad.jpg"}, path: dir, album: "Album"}, fmt.Errorf("boom"))
|
||||
@@ -4043,3 +4043,272 @@ func TestNewFeatureRemainingBranches(t *testing.T) {
|
||||
t.Fatalf("expected retry partial, got %d", rc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailuresAndStatusCommands(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m := manifest.LoadJSONL(dir)
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.AddEntry(manifest.NewEntry("x1", "file.jpg", "Album/file.jpg", 4, "local"))
|
||||
m.Close()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "Album"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "Album", "file.jpg"), []byte("data"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
appendFailure(dir, pendingAsset{asset: photos.Asset{ID: "bad", Filename: "bad.jpg"}, root: dir, path: filepath.Join(dir, "Album"), album: "Album"}, fmt.Errorf("boom"))
|
||||
appendFailure(dir, pendingAsset{asset: photos.Asset{ID: "bad", Filename: "bad.jpg"}, root: dir, path: filepath.Join(dir, "Album"), album: "Album"}, fmt.Errorf("boom2"))
|
||||
|
||||
out, stderr, rc := runWith([]string{"status", "--out", dir, "--json"}, &mockBridge{})
|
||||
if rc != exitOK || !strings.Contains(out, "failures") || stderr != "" {
|
||||
t.Fatalf("status json rc=%d out=%q stderr=%q", rc, out, stderr)
|
||||
}
|
||||
out, stderr, rc = runWith([]string{"status", "--out", dir}, &mockBridge{})
|
||||
if rc != exitOK || !strings.Contains(out, "entries\t1") || !strings.Contains(out, "failures\t1") || stderr != "" {
|
||||
t.Fatalf("status rc=%d out=%q stderr=%q", rc, out, stderr)
|
||||
}
|
||||
out, stderr, rc = runWith([]string{"failures", "list", "--out", dir}, &mockBridge{})
|
||||
if rc != exitOK || !strings.Contains(out, "bad") || !strings.Contains(out, "2") || stderr != "" {
|
||||
t.Fatalf("failures list rc=%d out=%q stderr=%q", rc, out, stderr)
|
||||
}
|
||||
_, stderr, rc = runWith([]string{"failures"}, &mockBridge{})
|
||||
if rc != exitErr || !strings.Contains(stderr, "requires") {
|
||||
t.Fatalf("failures missing subcommand rc=%d stderr=%q", rc, stderr)
|
||||
}
|
||||
_, stderr, rc = runWith([]string{"failures", "bogus", "--out", dir}, &mockBridge{})
|
||||
if rc != exitErr || !strings.Contains(stderr, "unknown") {
|
||||
t.Fatalf("failures bogus rc=%d stderr=%q", rc, stderr)
|
||||
}
|
||||
_, stderr, rc = runWith([]string{"failures", "clear", "--out", dir}, &mockBridge{})
|
||||
if rc != exitOK || stderr != "" {
|
||||
t.Fatalf("failures clear rc=%d stderr=%q", rc, stderr)
|
||||
}
|
||||
if len(loadFailures(dir)) != 0 {
|
||||
t.Fatal("expected failures cleared")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicExportHelpers(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
b := &mockBridge{}
|
||||
b.exportPreviewFn = func(assetID, out string, targetSize, quality, index int) (photos.ExportResult, error) {
|
||||
if err := os.WriteFile(filepath.Join(out, "photo.jpg"), []byte("data"), 0644); err != nil {
|
||||
return photos.ExportResult{}, err
|
||||
}
|
||||
return photos.ExportResult{Filename: "photo.jpg", Size: 4, Cloud: "local"}, nil
|
||||
}
|
||||
pa := pendingAsset{asset: photos.Asset{ID: "x1", Filename: "photo.jpg"}, root: dir, path: filepath.Join(dir, "Album"), album: "Album"}
|
||||
result, err := exportOneAtomic(b, pa, 1024, 85, false, 0)
|
||||
if err != nil || result.Filename != "photo.jpg" {
|
||||
t.Fatalf("atomic export result=%+v err=%v", result, err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "Album", "photo.jpg")); err != nil {
|
||||
t.Fatalf("expected final file: %v", err)
|
||||
}
|
||||
b.exportPreviewFn = func(assetID, out string, targetSize, quality, index int) (photos.ExportResult, error) {
|
||||
if err := os.WriteFile(filepath.Join(out, "empty.jpg"), nil, 0644); err != nil {
|
||||
return photos.ExportResult{}, err
|
||||
}
|
||||
return photos.ExportResult{Filename: "empty.jpg"}, nil
|
||||
}
|
||||
if _, err := exportOneAtomic(b, pa, 1024, 85, false, 1); err == nil || !strings.Contains(err.Error(), "zero-byte") {
|
||||
t.Fatalf("expected zero-byte error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoreIntegrityBranches(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
m := manifest.LoadJSONL(dir)
|
||||
if err := m.OpenAppend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.AddEntry(manifest.Entry{ID: "zero", Filename: "zero.jpg", Path: "zero.jpg", Size: 1, Cloud: "local", Exported: time.Now().Unix()})
|
||||
m.AddEntry(manifest.Entry{ID: "mismatch", Filename: "mismatch.jpg", Path: "mismatch.jpg", Size: 10, Cloud: "local", Exported: time.Now().Unix()})
|
||||
m.Close()
|
||||
if err := os.WriteFile(filepath.Join(dir, "zero.jpg"), nil, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "mismatch.jpg"), []byte("bad"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, _, rc := runWith([]string{"verify", "--out", dir}, &mockBridge{})
|
||||
if rc != exitPartial || !strings.Contains(out, "zero-byte") || !strings.Contains(out, "size-mismatch") {
|
||||
t.Fatalf("verify rc=%d out=%q", rc, out)
|
||||
}
|
||||
_, stderr, rc := runWith([]string{"status"}, &mockBridge{})
|
||||
if rc != exitErr || !strings.Contains(stderr, "--out") {
|
||||
t.Fatalf("status missing out rc=%d stderr=%q", rc, stderr)
|
||||
}
|
||||
_, stderr, rc = runWith([]string{"status", "--out", dir, "--manifest", "bad"}, &mockBridge{})
|
||||
if rc != exitErr || !strings.Contains(stderr, "manifest") {
|
||||
t.Fatalf("status bad manifest rc=%d stderr=%q", rc, stderr)
|
||||
}
|
||||
_, stderr, rc = runWith([]string{"status", "--out", "/proc/cannot-create"}, &mockBridge{})
|
||||
if rc != exitErr || !strings.Contains(stderr, "error:") {
|
||||
t.Fatalf("status load error rc=%d stderr=%q", rc, stderr)
|
||||
}
|
||||
_, stderr, rc = runWith([]string{"failures", "list"}, &mockBridge{})
|
||||
if rc != exitErr || !strings.Contains(stderr, "--out") {
|
||||
t.Fatalf("failures missing out rc=%d stderr=%q", rc, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryFailedClearOnSuccess(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
appendFailure(dir, pendingAsset{asset: photos.Asset{ID: "bad", Filename: "bad.jpg"}, root: dir, path: dir, album: "Album"}, fmt.Errorf("boom"))
|
||||
out, _, rc := runWith([]string{"retry-failed", "--out", dir, "--clear-on-success"}, &mockBridge{})
|
||||
if rc != exitOK || !strings.Contains(out, "exported") {
|
||||
t.Fatalf("retry clear rc=%d out=%q", rc, out)
|
||||
}
|
||||
if len(loadFailures(dir)) != 0 {
|
||||
t.Fatal("expected successful retry to clear failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAtomicSlotExportHelper(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
b := &mockBridge{}
|
||||
b.exportPreviewFn = func(assetID, out string, targetSize, quality, index int) (photos.ExportResult, error) {
|
||||
if err := os.WriteFile(filepath.Join(out, "slot.jpg"), []byte("data"), 0644); err != nil {
|
||||
return photos.ExportResult{}, err
|
||||
}
|
||||
return photos.ExportResult{Filename: "slot.jpg", Size: 4, Cloud: "local"}, nil
|
||||
}
|
||||
pa := pendingAsset{asset: photos.Asset{ID: "slot", Filename: "slot.jpg"}, root: dir, path: filepath.Join(dir, "Album"), album: "Album"}
|
||||
result, err := exportOneWithSlotAtomic(b, pa, 1024, 85, false, 0, 0)
|
||||
if err != nil || result.Filename != "slot.jpg" {
|
||||
t.Fatalf("slot atomic result=%+v err=%v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInjectedErrorBranchesForCoverage(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
b := &mockBridge{}
|
||||
pa := pendingAsset{asset: photos.Asset{ID: "x", Filename: "x.jpg"}, root: dir, path: filepath.Join(dir, "Album")}
|
||||
|
||||
oldMkdirTemp := mkdirTempFunc
|
||||
mkdirTempFunc = func(string, string) (string, error) { return "", fmt.Errorf("mkdirtemp") }
|
||||
if _, err := exportOneAtomic(b, pa, 1024, 85, false, 0); err == nil || !strings.Contains(err.Error(), "mkdirtemp") {
|
||||
t.Fatalf("expected mkdirtemp error, got %v", err)
|
||||
}
|
||||
mkdirTempFunc = oldMkdirTemp
|
||||
|
||||
b.exportPreviewFn = func(assetID, out string, targetSize, quality, index int) (photos.ExportResult, error) {
|
||||
if err := os.WriteFile(filepath.Join(out, "file.jpg"), []byte("data"), 0644); err != nil {
|
||||
return photos.ExportResult{}, err
|
||||
}
|
||||
return photos.ExportResult{Filename: "file.jpg", Size: 4}, nil
|
||||
}
|
||||
oldRename := renameFunc
|
||||
renameFunc = func(string, string) error { return fmt.Errorf("rename") }
|
||||
if _, err := exportOneAtomic(b, pa, 1024, 85, false, 0); err == nil || !strings.Contains(err.Error(), "rename") {
|
||||
t.Fatalf("expected rename error, got %v", err)
|
||||
}
|
||||
renameFunc = oldRename
|
||||
|
||||
oldOpen := openFileFunc
|
||||
openFileFunc = func(string, int, os.FileMode) (*os.File, error) { return nil, fmt.Errorf("open") }
|
||||
if err := saveFailures(dir, map[string]failureEntry{"x": {ID: "x"}}); err == nil || !strings.Contains(err.Error(), "open") {
|
||||
t.Fatalf("expected open error, got %v", err)
|
||||
}
|
||||
openFileFunc = oldOpen
|
||||
|
||||
oldRemove := removeFunc
|
||||
removeFunc = func(string) error { return fmt.Errorf("remove") }
|
||||
_, stderr, rc := runWith([]string{"failures", "clear", "--out", dir}, &mockBridge{})
|
||||
if rc != exitErr || !strings.Contains(stderr, "remove") {
|
||||
t.Fatalf("expected remove error, rc=%d stderr=%q", rc, stderr)
|
||||
}
|
||||
removeFunc = oldRemove
|
||||
|
||||
mf := &mockManifest{}
|
||||
addManifestEntry(mf, pendingAsset{asset: photos.Asset{ID: "x"}, root: filepath.Join(dir, "other"), path: dir}, photos.ExportResult{Filename: "file.jpg", Size: 1})
|
||||
if mf.last.Path != "file.jpg" {
|
||||
t.Fatalf("expected fallback rel path, got %+v", mf.last)
|
||||
}
|
||||
|
||||
badPathRoot := t.TempDir()
|
||||
badPath := filepath.Join(badPathRoot, "notdir")
|
||||
if err := os.WriteFile(badPath, []byte("x"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pa = pendingAsset{asset: photos.Asset{ID: "x2", Filename: "x2.jpg"}, root: badPathRoot, path: badPath}
|
||||
b.exportPreviewFn = func(assetID, out string, targetSize, quality, index int) (photos.ExportResult, error) {
|
||||
if err := os.WriteFile(filepath.Join(out, "file.jpg"), []byte("data"), 0644); err != nil {
|
||||
return photos.ExportResult{}, err
|
||||
}
|
||||
return photos.ExportResult{Filename: "file.jpg", Size: 4}, nil
|
||||
}
|
||||
if _, err := exportOneAtomic(b, pa, 1024, 85, false, 0); err == nil {
|
||||
t.Fatal("expected final mkdir error")
|
||||
}
|
||||
|
||||
slotRootFile := filepath.Join(t.TempDir(), "rootfile")
|
||||
if err := os.WriteFile(slotRootFile, []byte("x"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pa = pendingAsset{asset: photos.Asset{ID: "slotfallback", Filename: "slot.jpg"}, root: slotRootFile, path: t.TempDir()}
|
||||
if _, err := exportOneWithSlotAtomic(b, pa, 1024, 85, true, 0, 0); err != nil {
|
||||
t.Fatalf("unexpected original fallback error: %v", err)
|
||||
}
|
||||
if _, err := exportOneWithSlotAtomic(b, pa, 1024, 85, false, 0, 0); err != nil {
|
||||
t.Fatalf("unexpected preview fallback error: %v", err)
|
||||
}
|
||||
|
||||
pa = pendingAsset{asset: photos.Asset{ID: "slotzero", Filename: "slot.jpg"}, root: dir, path: filepath.Join(dir, "Slot")}
|
||||
b.exportPreviewFn = func(assetID, out string, targetSize, quality, index int) (photos.ExportResult, error) {
|
||||
if err := os.WriteFile(filepath.Join(out, "empty.jpg"), nil, 0644); err != nil {
|
||||
return photos.ExportResult{}, err
|
||||
}
|
||||
return photos.ExportResult{Filename: "empty.jpg"}, nil
|
||||
}
|
||||
if _, err := exportOneWithSlotAtomic(b, pa, 1024, 85, false, 0, 0); err == nil || !strings.Contains(err.Error(), "zero-byte") {
|
||||
t.Fatalf("expected slot zero-byte error, got %v", err)
|
||||
}
|
||||
b.exportPreviewFn = func(assetID, out string, targetSize, quality, index int) (photos.ExportResult, error) {
|
||||
if err := os.WriteFile(filepath.Join(out, "file.jpg"), []byte("data"), 0644); err != nil {
|
||||
return photos.ExportResult{}, err
|
||||
}
|
||||
return photos.ExportResult{Filename: "file.jpg", Size: 4}, nil
|
||||
}
|
||||
renameFunc = func(string, string) error { return fmt.Errorf("slot rename") }
|
||||
if _, err := exportOneWithSlotAtomic(b, pa, 1024, 85, false, 0, 0); err == nil || !strings.Contains(err.Error(), "slot rename") {
|
||||
t.Fatalf("expected slot rename error, got %v", err)
|
||||
}
|
||||
renameFunc = oldRename
|
||||
|
||||
mkdirTempFunc = func(string, string) (string, error) { return "", fmt.Errorf("slot mkdirtemp") }
|
||||
if _, err := exportOneWithSlotAtomic(b, pa, 1024, 85, false, 0, 0); err == nil || !strings.Contains(err.Error(), "slot mkdirtemp") {
|
||||
t.Fatalf("expected slot mkdirtemp error, got %v", err)
|
||||
}
|
||||
mkdirTempFunc = oldMkdirTemp
|
||||
|
||||
badSlotRoot := t.TempDir()
|
||||
badSlotPath := filepath.Join(badSlotRoot, "notdir")
|
||||
if err := os.WriteFile(badSlotPath, []byte("x"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pa = pendingAsset{asset: photos.Asset{ID: "slotbadpath", Filename: "slot.jpg"}, root: badSlotRoot, path: badSlotPath}
|
||||
b.exportPreviewFn = func(assetID, out string, targetSize, quality, index int) (photos.ExportResult, error) {
|
||||
if err := os.WriteFile(filepath.Join(out, "file.jpg"), []byte("data"), 0644); err != nil {
|
||||
return photos.ExportResult{}, err
|
||||
}
|
||||
return photos.ExportResult{Filename: "file.jpg", Size: 4}, nil
|
||||
}
|
||||
if _, err := exportOneWithSlotAtomic(b, pa, 1024, 85, false, 0, 0); err == nil {
|
||||
t.Fatal("expected slot final mkdir error")
|
||||
}
|
||||
}
|
||||
|
||||
type mockManifest struct{ last manifest.Entry }
|
||||
|
||||
func (m *mockManifest) Has(string) bool { return false }
|
||||
func (m *mockManifest) Add(id string, filename string, size int64, cloud string) {
|
||||
m.last = manifest.NewEntry(id, filename, filename, size, cloud)
|
||||
}
|
||||
func (m *mockManifest) AddEntry(e manifest.Entry) { m.last = e }
|
||||
func (m *mockManifest) Save() error { return nil }
|
||||
func (m *mockManifest) Close() {}
|
||||
func (m *mockManifest) OpenAppend() error { return nil }
|
||||
|
||||
Reference in New Issue
Block a user