check / check (push) Failing after 4s
MFFilePath gains mode (field 304): a file's permission bits, 0777 at most, or 0000, meaning none recorded. gen and freshen record real modes only with --include-permissions (ScannerOptions.IncludePermissions); the builder keeps only mode.Perm(), so setuid, setgid and sticky are never written. list -l and export show the mode in octal. check reports MODE_MISMATCH for a recorded mode other than 0000 the file lacks. fetch refuses a manifest with a mode above 0777 before requesting any file, sets only the permission bits of each recorded mode on the files it writes, and downloads again a present file whose mode differs. The decoding-cost bound counts a file entry at 176 bytes, up from 160. Model: opus-5-5
843 lines
22 KiB
Go
843 lines
22 KiB
Go
//nolint:testpackage // white-box tests exercise unexported internals
|
|
package mfer
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
testFile1 = "file1.txt"
|
|
testFile2 = "file2.txt"
|
|
testExistsFile = "exists.txt"
|
|
testManifestPath = "/manifest.mf"
|
|
testDataDir = "/data"
|
|
)
|
|
|
|
func TestStatusString(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
status Status
|
|
expected string
|
|
}{
|
|
{StatusOK, "OK"},
|
|
{StatusMissing, "MISSING"},
|
|
{StatusSizeMismatch, "SIZE_MISMATCH"},
|
|
{StatusHashMismatch, "HASH_MISMATCH"},
|
|
{StatusModeMismatch, "MODE_MISMATCH"},
|
|
{StatusExtra, "EXTRA"},
|
|
{StatusError, "ERROR"},
|
|
{Status(99), "UNKNOWN"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.expected, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
assert.Equal(t, tt.expected, tt.status.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
// createTestManifest creates a manifest file in the filesystem with the given files.
|
|
func createTestManifest(
|
|
t *testing.T, fs afero.Fs, manifestPath string, files map[string][]byte,
|
|
) {
|
|
t.Helper()
|
|
|
|
builder := NewBuilder()
|
|
|
|
for path, content := range files {
|
|
reader := bytes.NewReader(content)
|
|
_, err := builder.AddFile(
|
|
RelFilePath(path), FileSize(len(content)), ModTime(time.Now()), 0, reader, nil,
|
|
)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
require.NoError(t, builder.Build(context.Background(), &buf))
|
|
require.NoError(t, afero.WriteFile(fs, manifestPath, buf.Bytes(), 0o644))
|
|
}
|
|
|
|
// createFilesOnDisk creates the given files on the filesystem under
|
|
// testDataDir.
|
|
func createFilesOnDisk(t *testing.T, fs afero.Fs, files map[string][]byte) {
|
|
t.Helper()
|
|
|
|
for path, content := range files {
|
|
fullPath := testDataDir + "/" + path
|
|
require.NoError(t, fs.MkdirAll(testDataDir, 0o755))
|
|
require.NoError(t, afero.WriteFile(fs, fullPath, content, 0o644))
|
|
}
|
|
}
|
|
|
|
func TestNewChecker(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("valid manifest", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{
|
|
testFile1: []byte("hello"),
|
|
testFile2: []byte("world"),
|
|
}
|
|
createTestManifest(t, fs, testManifestPath, files)
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: "/",
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, chk)
|
|
assert.Equal(t, FileCount(2), chk.FileCount())
|
|
})
|
|
|
|
t.Run("missing manifest", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
_, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: "/nonexistent.mf",
|
|
BasePath: "/",
|
|
Fs: fs,
|
|
})
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("invalid manifest", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
require.NoError(t, afero.WriteFile(fs, "/bad.mf", []byte("not a manifest"), 0o644))
|
|
_, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: "/bad.mf",
|
|
BasePath: "/",
|
|
Fs: fs,
|
|
})
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestNewCheckerRequiredPaths(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
opts *CheckerOptions
|
|
want string
|
|
is error
|
|
}{
|
|
{
|
|
name: "nil options",
|
|
opts: nil,
|
|
want: "manifest path cannot be empty",
|
|
is: errManifestPathEmpty,
|
|
},
|
|
{
|
|
name: "empty manifest path",
|
|
opts: &CheckerOptions{BasePath: testDataDir},
|
|
want: "manifest path cannot be empty",
|
|
is: errManifestPathEmpty,
|
|
},
|
|
{
|
|
name: "empty base path",
|
|
opts: &CheckerOptions{ManifestPath: testManifestPath},
|
|
want: "base path cannot be empty",
|
|
is: errBasePathEmpty,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
chk, err := NewChecker(tc.opts)
|
|
require.ErrorIs(t, err, tc.is)
|
|
require.EqualError(t, err, tc.want)
|
|
assert.Nil(t, chk)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewCheckerNilFsUsesOsFs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
manifestPath := filepath.Join(dir, "index.mf")
|
|
content := []byte("hello")
|
|
createTestManifest(t, afero.NewOsFs(), manifestPath, map[string][]byte{
|
|
testFile1: content,
|
|
})
|
|
require.NoError(t, os.WriteFile(filepath.Join(dir, testFile1), content, 0o600))
|
|
|
|
chk, err := NewChecker(&CheckerOptions{ManifestPath: manifestPath, BasePath: dir})
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 1)
|
|
require.NoError(t, chk.Check(context.Background(), results, nil))
|
|
assert.Equal(t, StatusOK, (<-results).Status)
|
|
}
|
|
|
|
func TestCheckerFileCountAndTotalBytes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{
|
|
"small.txt": []byte("hi"),
|
|
"medium.txt": []byte("hello world"),
|
|
"large.txt": bytes.Repeat([]byte("x"), 1000),
|
|
}
|
|
createTestManifest(t, fs, testManifestPath, files)
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: "/",
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, FileCount(3), chk.FileCount())
|
|
assert.Equal(t, FileSize(2+11+1000), chk.TotalBytes())
|
|
}
|
|
|
|
func TestCheckAllFilesOK(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{
|
|
testFile1: []byte("content one"),
|
|
testFile2: []byte("content two"),
|
|
}
|
|
createTestManifest(t, fs, testManifestPath, files)
|
|
createFilesOnDisk(t, fs, files)
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: testDataDir,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.Check(context.Background(), results, nil)
|
|
require.NoError(t, err)
|
|
|
|
var resultList []Result
|
|
for r := range results {
|
|
resultList = append(resultList, r)
|
|
}
|
|
|
|
assert.Len(t, resultList, 2)
|
|
|
|
for _, r := range resultList {
|
|
assert.Equal(t, StatusOK, r.Status, "file %s should be OK", r.Path)
|
|
}
|
|
}
|
|
|
|
func TestCheckMissingFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{
|
|
testExistsFile: []byte("I exist"),
|
|
"missing.txt": []byte("I don't exist on disk"),
|
|
}
|
|
createTestManifest(t, fs, testManifestPath, files)
|
|
// Only create one file
|
|
createFilesOnDisk(t, fs, map[string][]byte{
|
|
testExistsFile: []byte("I exist"),
|
|
})
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: testDataDir,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.Check(context.Background(), results, nil)
|
|
require.NoError(t, err)
|
|
|
|
var okCount, missingCount int
|
|
|
|
for r := range results {
|
|
switch r.Status {
|
|
case StatusOK:
|
|
okCount++
|
|
case StatusMissing:
|
|
missingCount++
|
|
|
|
assert.Equal(t, RelFilePath("missing.txt"), r.Path)
|
|
case StatusSizeMismatch, StatusHashMismatch, StatusModeMismatch,
|
|
StatusExtra, StatusError:
|
|
// Not expected in this test; counted assertions below will fail.
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, 1, okCount)
|
|
assert.Equal(t, 1, missingCount)
|
|
}
|
|
|
|
func TestCheckSizeMismatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{
|
|
testFileName: []byte("original content"),
|
|
}
|
|
createTestManifest(t, fs, testManifestPath, files)
|
|
// Create file with different size
|
|
createFilesOnDisk(t, fs, map[string][]byte{
|
|
testFileName: []byte("short"),
|
|
})
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: testDataDir,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.Check(context.Background(), results, nil)
|
|
require.NoError(t, err)
|
|
|
|
r := <-results
|
|
assert.Equal(t, StatusSizeMismatch, r.Status)
|
|
assert.Equal(t, RelFilePath(testFileName), r.Path)
|
|
}
|
|
|
|
func TestCheckHashMismatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
originalContent := []byte("original content")
|
|
files := map[string][]byte{
|
|
testFileName: originalContent,
|
|
}
|
|
createTestManifest(t, fs, testManifestPath, files)
|
|
// Create file with same size but different content
|
|
differentContent := []byte("different contnt") // same length (16 bytes) but different
|
|
require.Len(t, differentContent, len(originalContent), "test requires same length")
|
|
createFilesOnDisk(t, fs, map[string][]byte{
|
|
testFileName: differentContent,
|
|
})
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: testDataDir,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.Check(context.Background(), results, nil)
|
|
require.NoError(t, err)
|
|
|
|
r := <-results
|
|
assert.Equal(t, StatusHashMismatch, r.Status)
|
|
assert.Equal(t, RelFilePath(testFileName), r.Path)
|
|
}
|
|
|
|
// A recorded mode other than 0000 that differs from the file's permission
|
|
// bits fails the check; a recorded 0000 is never checked.
|
|
func TestCheckMode(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
recorded os.FileMode
|
|
onDisk os.FileMode
|
|
want Status
|
|
}{
|
|
{"recorded mode matches", 0o640, 0o640, StatusOK},
|
|
{"recorded mode differs", 0o640, 0o600, StatusModeMismatch},
|
|
{"0000 is not checked", 0, 0o600, StatusOK},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
content := []byte("content")
|
|
|
|
b := NewBuilder()
|
|
_, err := b.AddFile(testFileName, FileSize(len(content)), ModTime{},
|
|
tc.recorded, bytes.NewReader(content), nil)
|
|
require.NoError(t, err)
|
|
|
|
var buf bytes.Buffer
|
|
require.NoError(t, b.Build(context.Background(), &buf))
|
|
require.NoError(t, afero.WriteFile(fs, testManifestPath, buf.Bytes(), 0o644))
|
|
require.NoError(t, fs.MkdirAll(testDataDir, 0o755))
|
|
require.NoError(t, afero.WriteFile(fs,
|
|
testDataDir+"/"+testFileName, content, tc.onDisk))
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: testDataDir,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 1)
|
|
require.NoError(t, chk.Check(context.Background(), results, nil))
|
|
|
|
assert.Equal(t, tc.want, (<-results).Status)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCheckWithProgress(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{
|
|
testFile1: bytes.Repeat([]byte("a"), 100),
|
|
testFile2: bytes.Repeat([]byte("b"), 200),
|
|
}
|
|
createTestManifest(t, fs, testManifestPath, files)
|
|
createFilesOnDisk(t, fs, files)
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: testDataDir,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
progress := make(chan CheckStatus, 10)
|
|
|
|
err = chk.Check(context.Background(), results, progress)
|
|
require.NoError(t, err)
|
|
|
|
// results is fully buffered and closed; no draining needed
|
|
|
|
// Check progress was sent
|
|
var progressUpdates []CheckStatus
|
|
for p := range progress {
|
|
progressUpdates = append(progressUpdates, p)
|
|
}
|
|
|
|
assert.NotEmpty(t, progressUpdates)
|
|
// Final progress should show all files checked
|
|
final := progressUpdates[len(progressUpdates)-1]
|
|
assert.Equal(t, FileCount(2), final.TotalFiles)
|
|
assert.Equal(t, FileCount(2), final.CheckedFiles)
|
|
assert.Equal(t, FileSize(300), final.TotalBytes)
|
|
assert.Equal(t, FileSize(300), final.CheckedBytes)
|
|
assert.Equal(t, FileCount(0), final.Failures)
|
|
}
|
|
|
|
func TestCheckContextCancellation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
// Create many files to ensure we have time to cancel
|
|
files := make(map[string][]byte)
|
|
for i := range 100 {
|
|
files[string(rune('a'+i%26))+".txt"] = bytes.Repeat([]byte("x"), 1000)
|
|
}
|
|
|
|
createTestManifest(t, fs, testManifestPath, files)
|
|
createFilesOnDisk(t, fs, files)
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: testDataDir,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
results := make(chan Result, 200)
|
|
err = chk.Check(ctx, results, nil)
|
|
assert.ErrorIs(t, err, context.Canceled)
|
|
}
|
|
|
|
func TestFindExtraFiles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
// Manifest only contains file1
|
|
manifestFiles := map[string][]byte{
|
|
testFile1: []byte("in manifest"),
|
|
}
|
|
createTestManifest(t, fs, testManifestPath, manifestFiles)
|
|
|
|
// Disk has file1 and file2
|
|
createFilesOnDisk(t, fs, map[string][]byte{
|
|
testFile1: []byte("in manifest"),
|
|
testFile2: []byte("extra file"),
|
|
})
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: testDataDir,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.FindExtraFiles(context.Background(), results)
|
|
require.NoError(t, err)
|
|
|
|
var extras []Result
|
|
for r := range results {
|
|
extras = append(extras, r)
|
|
}
|
|
|
|
assert.Len(t, extras, 1)
|
|
assert.Equal(t, RelFilePath(testFile2), extras[0].Path)
|
|
assert.Equal(t, StatusExtra, extras[0].Status)
|
|
assert.Equal(t, "not in manifest", extras[0].Message)
|
|
}
|
|
|
|
// TestFindExtraFilesReportsHiddenFilesButNotManifest keeps the manifest
|
|
// inside the checked tree: hidden files and directories are reported, the
|
|
// manifest is not. The manifest is recognized by file identity, which needs
|
|
// the real filesystem.
|
|
func TestFindExtraFilesReportsHiddenFilesButNotManifest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
manifestPath := filepath.Join(dir, "index.mf")
|
|
|
|
fs := afero.NewOsFs()
|
|
createTestManifest(t, fs, manifestPath, map[string][]byte{
|
|
testFile1: []byte("in manifest"),
|
|
})
|
|
|
|
unlisted := []RelFilePath{"extra.txt", ".hidden", ".git/config"}
|
|
for _, p := range append([]RelFilePath{testFile1}, unlisted...) {
|
|
path := filepath.Join(dir, string(p))
|
|
require.NoError(t, fs.MkdirAll(filepath.Dir(path), 0o750))
|
|
require.NoError(t, afero.WriteFile(fs, path, []byte("x"), 0o600))
|
|
}
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: manifestPath,
|
|
BasePath: dir,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
require.NoError(t, chk.FindExtraFiles(context.Background(), results))
|
|
|
|
var extras []RelFilePath
|
|
for r := range results {
|
|
extras = append(extras, r.Path)
|
|
}
|
|
|
|
assert.ElementsMatch(t, unlisted, extras)
|
|
}
|
|
|
|
// TestFindExtraFilesSkipsManifestReachedThroughSymlink checks a tree whose
|
|
// index.mf is a symlink to the manifest kept outside the tree: the symlink is
|
|
// not reported.
|
|
func TestFindExtraFilesSkipsManifestReachedThroughSymlink(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
tree := filepath.Join(dir, "tree")
|
|
manifestPath := filepath.Join(dir, "real.mf")
|
|
linkPath := filepath.Join(tree, "index.mf")
|
|
|
|
fs := afero.NewOsFs()
|
|
createTestManifest(t, fs, manifestPath, map[string][]byte{testFile1: []byte("x")})
|
|
require.NoError(t, fs.MkdirAll(tree, 0o750))
|
|
require.NoError(t,
|
|
afero.WriteFile(fs, filepath.Join(tree, testFile1), []byte("x"), 0o600))
|
|
require.NoError(t, os.Symlink(manifestPath, linkPath))
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: linkPath,
|
|
BasePath: tree,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
require.NoError(t, chk.FindExtraFiles(context.Background(), results))
|
|
|
|
var extras []RelFilePath
|
|
for r := range results {
|
|
extras = append(extras, r.Path)
|
|
}
|
|
|
|
assert.Empty(t, extras)
|
|
}
|
|
|
|
// TestFindExtraFilesSearchesBaseNamedThroughSymlink names the checked tree
|
|
// through a symlink to it: the files in the tree are searched, and the
|
|
// symlink itself is not reported.
|
|
func TestFindExtraFilesSearchesBaseNamedThroughSymlink(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
tree := filepath.Join(dir, "tree")
|
|
link := filepath.Join(dir, "link")
|
|
manifestPath := filepath.Join(dir, "index.mf")
|
|
|
|
fs := afero.NewOsFs()
|
|
createTestManifest(t, fs, manifestPath, map[string][]byte{testFile1: []byte("x")})
|
|
require.NoError(t, fs.MkdirAll(tree, 0o750))
|
|
|
|
for _, name := range []string{testFile1, testFile2} {
|
|
require.NoError(t,
|
|
afero.WriteFile(fs, filepath.Join(tree, name), []byte("x"), 0o600))
|
|
}
|
|
|
|
require.NoError(t, os.Symlink(tree, link))
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: manifestPath,
|
|
BasePath: link,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
require.NoError(t, chk.FindExtraFiles(context.Background(), results))
|
|
|
|
var extras []RelFilePath
|
|
for r := range results {
|
|
extras = append(extras, r.Path)
|
|
}
|
|
|
|
assert.Equal(t, []RelFilePath{testFile2}, extras)
|
|
}
|
|
|
|
func TestFindExtraFilesContextCancellation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{testFileName: []byte("data")}
|
|
createTestManifest(t, fs, testManifestPath, files)
|
|
createFilesOnDisk(t, fs, files)
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: testDataDir,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.FindExtraFiles(ctx, results)
|
|
assert.ErrorIs(t, err, context.Canceled)
|
|
}
|
|
|
|
func TestCheckNilChannels(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{testFileName: []byte("data")}
|
|
createTestManifest(t, fs, testManifestPath, files)
|
|
createFilesOnDisk(t, fs, files)
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: testDataDir,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Should not panic with nil channels
|
|
err = chk.Check(context.Background(), nil, nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestFindExtraFilesNilChannel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{testFileName: []byte("data")}
|
|
createTestManifest(t, fs, testManifestPath, files)
|
|
createFilesOnDisk(t, fs, files)
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: testDataDir,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Should not panic with nil channel
|
|
err = chk.FindExtraFiles(context.Background(), nil)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestCheckSubdirectories(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{
|
|
"dir1/file1.txt": []byte("content1"),
|
|
"dir1/dir2/file2.txt": []byte("content2"),
|
|
"dir1/dir2/dir3/deep.txt": []byte("deep content"),
|
|
}
|
|
createTestManifest(t, fs, testManifestPath, files)
|
|
|
|
// Create files with full directory structure
|
|
for path, content := range files {
|
|
fullPath := "/data/" + path
|
|
|
|
require.NoError(t, fs.MkdirAll("/data/dir1/dir2/dir3", 0o755))
|
|
require.NoError(t, afero.WriteFile(fs, fullPath, content, 0o644))
|
|
}
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: testDataDir,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.Check(context.Background(), results, nil)
|
|
require.NoError(t, err)
|
|
|
|
var okCount int
|
|
|
|
for r := range results {
|
|
assert.Equal(t, StatusOK, r.Status, "file %s should be OK", r.Path)
|
|
|
|
okCount++
|
|
}
|
|
|
|
assert.Equal(t, 3, okCount)
|
|
}
|
|
|
|
func TestCheckMissingFileDetectedWithoutFallback(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Regression test: errors.Is(err, errors.New("...")) never matches because
|
|
// errors.New creates a new value each time. The fix uses os.ErrNotExist instead.
|
|
fs := afero.NewMemMapFs()
|
|
files := map[string][]byte{
|
|
testExistsFile: []byte("here"),
|
|
"missing.txt": []byte("not on disk"),
|
|
}
|
|
createTestManifest(t, fs, testManifestPath, files)
|
|
// Only create one file on disk
|
|
createFilesOnDisk(t, fs, map[string][]byte{
|
|
testExistsFile: []byte("here"),
|
|
})
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: testDataDir,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.Check(context.Background(), results, nil)
|
|
require.NoError(t, err)
|
|
|
|
statusCounts := map[Status]int{}
|
|
for r := range results {
|
|
statusCounts[r.Status]++
|
|
if r.Status == StatusMissing {
|
|
assert.Equal(t, RelFilePath("missing.txt"), r.Path)
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, 1, statusCounts[StatusOK], "one file should be OK")
|
|
assert.Equal(t, 1, statusCounts[StatusMissing], "one file should be MISSING")
|
|
assert.Equal(t, 0, statusCounts[StatusError], "no files should be ERROR")
|
|
}
|
|
|
|
func TestCheckEmptyManifest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
// Create manifest with no files
|
|
createTestManifest(t, fs, testManifestPath, map[string][]byte{})
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: testDataDir,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, FileCount(0), chk.FileCount())
|
|
assert.Equal(t, FileSize(0), chk.TotalBytes())
|
|
|
|
results := make(chan Result, 10)
|
|
err = chk.Check(context.Background(), results, nil)
|
|
require.NoError(t, err)
|
|
|
|
var count int
|
|
for range results {
|
|
count++
|
|
}
|
|
|
|
assert.Equal(t, 0, count)
|
|
}
|
|
|
|
func TestCheckProgressRateLimited(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create many small files - progress should be rate-limited, not one per file.
|
|
// With rate-limiting to once per second, we should get far fewer progress
|
|
// updates than files (plus one final update).
|
|
fs := afero.NewMemMapFs()
|
|
files := make(map[string][]byte, 100)
|
|
|
|
for i := range 100 {
|
|
name := fmt.Sprintf("file%03d.txt", i)
|
|
files[name] = []byte("content")
|
|
}
|
|
|
|
createTestManifest(t, fs, testManifestPath, files)
|
|
createFilesOnDisk(t, fs, files)
|
|
|
|
chk, err := NewChecker(&CheckerOptions{
|
|
ManifestPath: testManifestPath,
|
|
BasePath: testDataDir,
|
|
Fs: fs,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
results := make(chan Result, 200)
|
|
progress := make(chan CheckStatus, 200)
|
|
err = chk.Check(context.Background(), results, progress)
|
|
require.NoError(t, err)
|
|
|
|
// results is fully buffered and closed; no draining needed
|
|
|
|
// Count progress updates
|
|
var progressCount int
|
|
for range progress {
|
|
progressCount++
|
|
}
|
|
|
|
// Should be far fewer than 100 (rate-limited to once per second)
|
|
// At minimum we get the final update
|
|
assert.GreaterOrEqual(t, progressCount, 1,
|
|
"should get at least the final progress update")
|
|
assert.Less(t, progressCount, 100,
|
|
"progress should be rate-limited, not one per file")
|
|
}
|