tests - more test cases for internal/repository/check.go (#21830)

Co-authored-by: Michael Eischer <michael.eischer@fau.de>
This commit is contained in:
Winfried Plappert
2026-06-13 16:35:48 +01:00
committed by GitHub
parent 9f9a3472aa
commit 0938f52f38
+225
View File
@@ -0,0 +1,225 @@
package repository
import (
"bufio"
"bytes"
"context"
"io"
"path/filepath"
"strings"
"testing"
"github.com/klauspost/compress/zstd"
"github.com/restic/restic/internal/archiver"
"github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/repository/pack"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
)
var checkerTestData = filepath.Join("..", "checker", "testdata", "checker-test-repo.tar.gz")
func testWrapCheckPack(ctx context.Context, t *testing.T, repo *Repository,
packID restic.ID, blobs []restic.Blob, size int64,
) error {
t.Helper()
bufRd := bufio.NewReaderSize(nil, maxStreamBufferSize)
dec, err := zstd.NewReader(nil)
rtest.OK(t, err)
return checkPack(ctx, repo, packID, blobs, size, bufRd, dec)
}
// TestGapInBlobs creates a gap in the blob list by omitting the first entry before passing it to checkPack
func TestGapInBlobs(t *testing.T) {
repo, _, cleanup := TestFromFixture(t, checkerTestData)
defer cleanup()
err := repo.LoadIndex(context.TODO(), nil)
rtest.OK(t, err)
repoPacks, err := pack.Size(context.TODO(), repo, false)
rtest.OK(t, err)
packID := restic.TestParseID("19a731a515618ec8b75fc0ff3b887d8feb83aef1001c9899f6702761142ed068")
_, ok := repoPacks[packID]
rtest.Assert(t, ok, "expected pack 19a731a515618ec8b75fc0ff3b887d8feb83aef1001c9899f6702761142ed068")
blobs := []restic.Blob{}
pb := <-repo.ListPacksFromIndex(context.TODO(), restic.NewIDSet(packID))
blobs = append(blobs, pb.Blobs...)
// assertion for clarity, actually can't fail as the packfile content is fixed
rtest.Assert(t, len(blobs) >= 2, "need at least 2 blobs in packfile 19a731a51")
blobs = blobs[1:]
err = testWrapCheckPack(context.TODO(), t, repo, packID, blobs, repoPacks[packID])
var packErr *ErrPackData
rtest.Assert(t, errors.As(err, &packErr), "expected ErrPackData, got: %T %v", err, err)
rtest.Equals(t, packID, packErr.PackID)
errText := err.Error()
rtest.Assert(t, strings.Contains(errText, "gaps") || strings.Contains(errText, "overlapping"),
"expected gap/overlap error in: %v", errText)
rtest.Assert(t, strings.Contains(errText, "pack header size does not match"),
"expected header size mismatch error in: %v", errText)
}
// helper functions for backend error fails
// collectErrors collects errors from checker methods
func collectErrors(ctx context.Context, f func(context.Context, chan<- error)) (errs []error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
errChan := make(chan error)
go f(ctx, errChan)
for err := range errChan {
errs = append(errs, err)
}
return errs
}
// runReadPacks calls ReadPacks which loads data from specified packs and checks the integrity
func runReadPacks(chkr *Checker) []error {
return collectErrors(context.TODO(),
func(ctx context.Context, errCh chan<- error) {
chkr.ReadPacks(ctx, func(packs map[restic.ID]int64) map[restic.ID]int64 {
return packs
}, nil, errCh)
})
}
// lastByteFlipBackend flips the last byte of every pack file on read,
// causing the SHA-256 hash computed by checkPackInner to differ from the
// content-addressed pack ID stored in the filename.
type lastByteFlipBackend struct {
backend.Backend
}
func (b *lastByteFlipBackend) Load(ctx context.Context, h backend.Handle, length int, offset int64, consumer func(rd io.Reader) error) error {
if h.Type != restic.PackFile {
return b.Backend.Load(ctx, h, length, offset, consumer)
}
return b.Backend.Load(ctx, h, length, offset, func(rd io.Reader) error {
buf, err := io.ReadAll(rd)
if err != nil {
return err
}
if len(buf) > 0 {
buf[len(buf)-1] ^= 0xff
}
return consumer(bytes.NewReader(buf))
})
}
// alwaysFailBackend returns a hard, non-partial error for every pack file
// load, simulating a complete download failure (e.g. network unreachable).
type alwaysFailBackend struct {
backend.Backend
}
func (b *alwaysFailBackend) Load(ctx context.Context, h backend.Handle, length int, offset int64, consumer func(rd io.Reader) error) error {
if h.Type == restic.PackFile {
return errors.New("simulated total download failure")
}
return b.Backend.Load(ctx, h, length, offset, consumer)
}
// truncatingBackend returns only the first 8 bytes of every pack file,
// guaranteeing io.ErrUnexpectedEOF inside checkPackInner (a partial read).
type truncatingBackend struct {
backend.Backend
}
func (b *truncatingBackend) Load(ctx context.Context, h backend.Handle, length int, offset int64, consumer func(rd io.Reader) error) error {
if h.Type != restic.PackFile {
return b.Backend.Load(ctx, h, length, offset, consumer)
}
return b.Backend.Load(ctx, h, length, offset, func(rd io.Reader) error {
buf := make([]byte, 8)
n, _ := io.ReadFull(rd, buf)
return consumer(bytes.NewReader(buf[:n]))
})
}
// setupChecker creates a repository with one snapshot, then re-opens the
// same backend through the given wrapper for use by the checker.
func setupChecker(t *testing.T, wrap func(backend.Backend) backend.Backend) *Checker {
t.Helper()
// Write a snapshot into a fresh in-memory repository.
repo, be := TestRepositoryWithBackend(t, nil, 0, Options{})
_ = archiver.TestSnapshot(t, repo, ".", nil)
// Re-open the same backend (now containing real pack files) through
// the corruption wrapper so the checker reads corrupted data.
checkRepo := TestOpenBackend(t, wrap(be))
chkr := NewChecker(checkRepo)
// make sure the index is loaded
err := checkRepo.LoadIndex(context.TODO(), nil)
rtest.OK(t, err)
return chkr
}
// TestCheckPackHashMismatch verifies that checkPackInner detects when the
// bytes stored in the backend don't hash to the pack's content-addressed ID.
// Covers the `!hash.Equal(id)` branch → ErrPackData "unexpected pack id".
func TestCheckPackHashMismatch(t *testing.T) {
chkr := setupChecker(t, func(be backend.Backend) backend.Backend {
return &lastByteFlipBackend{Backend: be}
})
found := false
dataErrs := runReadPacks(chkr)
for _, err := range dataErrs {
if strings.Contains(err.Error(), "unexpected pack id") {
found = true
}
}
rtest.Assert(t, found, "expected 'unexpected pack id' error, got: %v", dataErrs)
}
// TestCheckPackDownloadError verifies that a complete (non-partial) backend
// load failure is returned as a plain "download error" and NOT as ErrPackData.
func TestCheckPackDownloadError(t *testing.T) {
chkr := setupChecker(t, func(be backend.Backend) backend.Backend {
return &alwaysFailBackend{Backend: be}
})
dataErrs := runReadPacks(chkr)
rtest.Assert(t, len(dataErrs) > 0, "expected download errors, got none")
for _, err := range dataErrs {
var packErr *ErrPackData
rtest.Assert(t, !errors.As(err, &packErr),
"complete download failure must NOT produce ErrPackData, got: %v", err)
rtest.Assert(t, strings.Contains(err.Error(), "download error"),
"expected 'download error' in message, got: %v", err)
}
}
// TestCheckPackPartialDownloadError verifies that a partial read (truncated
// response) is returned as ErrPackData, so the check command can suggest
// `restic repair packs` for the affected pack.
// Covers the partialReadError branch of the be.Load error path.
func TestCheckPackPartialDownloadError(t *testing.T) {
chkr := setupChecker(t, func(be backend.Backend) backend.Backend {
return &truncatingBackend{Backend: be}
})
dataErrs := runReadPacks(chkr)
rtest.Assert(t, len(dataErrs) > 0, "expected errors from truncated reads, got none")
for _, err := range dataErrs {
var packErr *ErrPackData
rtest.Assert(t, errors.As(err, &packErr),
"partial read must produce ErrPackData, got: %T %v", err, err)
}
}