Add unit tests for checker/scanner and validate input paths
- Add comprehensive unit tests for internal/checker (88.5% coverage) - Status string representations - NewChecker validation - Check operation (OK, missing, size/hash mismatch) - Progress reporting and context cancellation - FindExtraFiles functionality - Add comprehensive unit tests for internal/scanner (80.1% coverage) - Constructors and options - File/path enumeration - Dotfile exclusion/inclusion - ToManifest with progress and cancellation - Non-blocking status channel sends - Validate input paths before scanning in generate command - Fail fast with clear error if paths don't exist - Prevents confusing errors deep in enumeration
This commit is contained in:
parent
07db5d434f
commit
d3776d7d7c
405
internal/checker/checker_test.go
Normal file
405
internal/checker/checker_test.go
Normal file
@ -0,0 +1,405 @@
|
|||||||
|
package checker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"sneak.berlin/go/mfer/mfer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStatusString(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
status Status
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{StatusOK, "OK"},
|
||||||
|
{StatusMissing, "MISSING"},
|
||||||
|
{StatusSizeMismatch, "SIZE_MISMATCH"},
|
||||||
|
{StatusHashMismatch, "HASH_MISMATCH"},
|
||||||
|
{StatusExtra, "EXTRA"},
|
||||||
|
{StatusError, "ERROR"},
|
||||||
|
{Status(99), "UNKNOWN"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.expected, func(t *testing.T) {
|
||||||
|
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 := mfer.NewBuilder()
|
||||||
|
for path, content := range files {
|
||||||
|
reader := bytes.NewReader(content)
|
||||||
|
_, err := builder.AddFile(path, int64(len(content)), time.Now(), reader, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
require.NoError(t, builder.Build(&buf))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, manifestPath, buf.Bytes(), 0644))
|
||||||
|
}
|
||||||
|
|
||||||
|
// createFilesOnDisk creates the given files on the filesystem.
|
||||||
|
func createFilesOnDisk(t *testing.T, fs afero.Fs, basePath string, files map[string][]byte) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for path, content := range files {
|
||||||
|
fullPath := basePath + "/" + path
|
||||||
|
require.NoError(t, fs.MkdirAll(basePath, 0755))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, fullPath, content, 0644))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewChecker(t *testing.T) {
|
||||||
|
t.Run("valid manifest", func(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
files := map[string][]byte{
|
||||||
|
"file1.txt": []byte("hello"),
|
||||||
|
"file2.txt": []byte("world"),
|
||||||
|
}
|
||||||
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
|
|
||||||
|
chk, err := NewChecker("/manifest.mf", "/", fs)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotNil(t, chk)
|
||||||
|
assert.Equal(t, int64(2), chk.FileCount())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing manifest", func(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
_, err := NewChecker("/nonexistent.mf", "/", fs)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid manifest", func(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/bad.mf", []byte("not a manifest"), 0644))
|
||||||
|
_, err := NewChecker("/bad.mf", "/", fs)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckerFileCountAndTotalBytes(t *testing.T) {
|
||||||
|
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, "/manifest.mf", files)
|
||||||
|
|
||||||
|
chk, err := NewChecker("/manifest.mf", "/", fs)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(3), chk.FileCount())
|
||||||
|
assert.Equal(t, int64(2+11+1000), chk.TotalBytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckAllFilesOK(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
files := map[string][]byte{
|
||||||
|
"file1.txt": []byte("content one"),
|
||||||
|
"file2.txt": []byte("content two"),
|
||||||
|
}
|
||||||
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
|
createFilesOnDisk(t, fs, "/data", files)
|
||||||
|
|
||||||
|
chk, err := NewChecker("/manifest.mf", "/data", 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) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
files := map[string][]byte{
|
||||||
|
"exists.txt": []byte("I exist"),
|
||||||
|
"missing.txt": []byte("I don't exist on disk"),
|
||||||
|
}
|
||||||
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
|
// Only create one file
|
||||||
|
createFilesOnDisk(t, fs, "/data", map[string][]byte{
|
||||||
|
"exists.txt": []byte("I exist"),
|
||||||
|
})
|
||||||
|
|
||||||
|
chk, err := NewChecker("/manifest.mf", "/data", 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, "missing.txt", r.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, 1, okCount)
|
||||||
|
assert.Equal(t, 1, missingCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckSizeMismatch(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
files := map[string][]byte{
|
||||||
|
"file.txt": []byte("original content"),
|
||||||
|
}
|
||||||
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
|
// Create file with different size
|
||||||
|
createFilesOnDisk(t, fs, "/data", map[string][]byte{
|
||||||
|
"file.txt": []byte("short"),
|
||||||
|
})
|
||||||
|
|
||||||
|
chk, err := NewChecker("/manifest.mf", "/data", 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, "file.txt", r.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckHashMismatch(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
originalContent := []byte("original content")
|
||||||
|
files := map[string][]byte{
|
||||||
|
"file.txt": originalContent,
|
||||||
|
}
|
||||||
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
|
// Create file with same size but different content
|
||||||
|
differentContent := []byte("different contnt") // same length (16 bytes) but different
|
||||||
|
require.Equal(t, len(originalContent), len(differentContent), "test requires same length")
|
||||||
|
createFilesOnDisk(t, fs, "/data", map[string][]byte{
|
||||||
|
"file.txt": differentContent,
|
||||||
|
})
|
||||||
|
|
||||||
|
chk, err := NewChecker("/manifest.mf", "/data", 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, "file.txt", r.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckWithProgress(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
files := map[string][]byte{
|
||||||
|
"file1.txt": bytes.Repeat([]byte("a"), 100),
|
||||||
|
"file2.txt": bytes.Repeat([]byte("b"), 200),
|
||||||
|
}
|
||||||
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
|
createFilesOnDisk(t, fs, "/data", files)
|
||||||
|
|
||||||
|
chk, err := NewChecker("/manifest.mf", "/data", 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)
|
||||||
|
|
||||||
|
// Drain results
|
||||||
|
for range results {
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, int64(2), final.TotalFiles)
|
||||||
|
assert.Equal(t, int64(2), final.CheckedFiles)
|
||||||
|
assert.Equal(t, int64(300), final.TotalBytes)
|
||||||
|
assert.Equal(t, int64(300), final.CheckedBytes)
|
||||||
|
assert.Equal(t, int64(0), final.Failures)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckContextCancellation(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
// Create many files to ensure we have time to cancel
|
||||||
|
files := make(map[string][]byte)
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
files[string(rune('a'+i%26))+".txt"] = bytes.Repeat([]byte("x"), 1000)
|
||||||
|
}
|
||||||
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
|
createFilesOnDisk(t, fs, "/data", files)
|
||||||
|
|
||||||
|
chk, err := NewChecker("/manifest.mf", "/data", 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) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
// Manifest only contains file1
|
||||||
|
manifestFiles := map[string][]byte{
|
||||||
|
"file1.txt": []byte("in manifest"),
|
||||||
|
}
|
||||||
|
createTestManifest(t, fs, "/manifest.mf", manifestFiles)
|
||||||
|
|
||||||
|
// Disk has file1 and file2
|
||||||
|
createFilesOnDisk(t, fs, "/data", map[string][]byte{
|
||||||
|
"file1.txt": []byte("in manifest"),
|
||||||
|
"file2.txt": []byte("extra file"),
|
||||||
|
})
|
||||||
|
|
||||||
|
chk, err := NewChecker("/manifest.mf", "/data", 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, "file2.txt", extras[0].Path)
|
||||||
|
assert.Equal(t, StatusExtra, extras[0].Status)
|
||||||
|
assert.Equal(t, "not in manifest", extras[0].Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFindExtraFilesContextCancellation(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
files := map[string][]byte{"file.txt": []byte("data")}
|
||||||
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
|
createFilesOnDisk(t, fs, "/data", files)
|
||||||
|
|
||||||
|
chk, err := NewChecker("/manifest.mf", "/data", 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) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
files := map[string][]byte{"file.txt": []byte("data")}
|
||||||
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
|
createFilesOnDisk(t, fs, "/data", files)
|
||||||
|
|
||||||
|
chk, err := NewChecker("/manifest.mf", "/data", 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) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
files := map[string][]byte{"file.txt": []byte("data")}
|
||||||
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
|
createFilesOnDisk(t, fs, "/data", files)
|
||||||
|
|
||||||
|
chk, err := NewChecker("/manifest.mf", "/data", 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) {
|
||||||
|
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, "/manifest.mf", files)
|
||||||
|
|
||||||
|
// Create files with full directory structure
|
||||||
|
for path, content := range files {
|
||||||
|
fullPath := "/data/" + path
|
||||||
|
require.NoError(t, fs.MkdirAll("/data/dir1/dir2/dir3", 0755))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, fullPath, content, 0644))
|
||||||
|
}
|
||||||
|
|
||||||
|
chk, err := NewChecker("/manifest.mf", "/data", 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 TestCheckEmptyManifest(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
// Create manifest with no files
|
||||||
|
createTestManifest(t, fs, "/manifest.mf", map[string][]byte{})
|
||||||
|
|
||||||
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(0), chk.FileCount())
|
||||||
|
assert.Equal(t, int64(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)
|
||||||
|
}
|
||||||
@ -485,6 +485,42 @@ func TestGenerateAtomicWriteCleansUpOnError(t *testing.T) {
|
|||||||
assert.False(t, tmpExists, "temp file should be cleaned up after failed generation")
|
assert.False(t, tmpExists, "temp file should be cleaned up after failed generation")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGenerateValidatesInputPaths(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
|
||||||
|
// Create one valid directory
|
||||||
|
require.NoError(t, fs.MkdirAll("/validdir", 0755))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/validdir/file.txt", []byte("content"), 0644))
|
||||||
|
|
||||||
|
t.Run("nonexistent path fails fast", func(t *testing.T) {
|
||||||
|
opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/output.mf", "/nonexistent"}, fs)
|
||||||
|
exitCode := RunWithOptions(opts)
|
||||||
|
assert.Equal(t, 1, exitCode)
|
||||||
|
stderr := opts.Stderr.(*bytes.Buffer).String()
|
||||||
|
assert.Contains(t, stderr, "path does not exist")
|
||||||
|
assert.Contains(t, stderr, "/nonexistent")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("mix of valid and invalid paths fails fast", func(t *testing.T) {
|
||||||
|
opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/output.mf", "/validdir", "/alsononexistent"}, fs)
|
||||||
|
exitCode := RunWithOptions(opts)
|
||||||
|
assert.Equal(t, 1, exitCode)
|
||||||
|
stderr := opts.Stderr.(*bytes.Buffer).String()
|
||||||
|
assert.Contains(t, stderr, "path does not exist")
|
||||||
|
assert.Contains(t, stderr, "/alsononexistent")
|
||||||
|
|
||||||
|
// Output file should not have been created
|
||||||
|
exists, _ := afero.Exists(fs, "/output.mf")
|
||||||
|
assert.False(t, exists, "output file should not exist when path validation fails")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("valid paths succeed", func(t *testing.T) {
|
||||||
|
opts := testOpts([]string{"mfer", "generate", "-q", "-o", "/output.mf", "/validdir"}, fs)
|
||||||
|
exitCode := RunWithOptions(opts)
|
||||||
|
assert.Equal(t, 0, exitCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestCheckDetectsManifestCorruption(t *testing.T) {
|
func TestCheckDetectsManifestCorruption(t *testing.T) {
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
rng := rand.New(rand.NewSource(42))
|
rng := rand.New(rand.NewSource(42))
|
||||||
|
|||||||
@ -54,13 +54,18 @@ func (mfa *CLIApp) generateManifestOperation(ctx *cli.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Collect all paths first
|
// Collect and validate all paths first
|
||||||
paths := make([]string, 0, args.Len())
|
paths := make([]string, 0, args.Len())
|
||||||
for i := 0; i < args.Len(); i++ {
|
for i := 0; i < args.Len(); i++ {
|
||||||
ap, err := filepath.Abs(args.Get(i))
|
inputPath := args.Get(i)
|
||||||
|
ap, err := filepath.Abs(inputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Validate path exists before adding to list
|
||||||
|
if exists, _ := afero.Exists(mfa.Fs, ap); !exists {
|
||||||
|
return fmt.Errorf("path does not exist: %s", inputPath)
|
||||||
|
}
|
||||||
log.Debugf("enumerating path: %s", ap)
|
log.Debugf("enumerating path: %s", ap)
|
||||||
paths = append(paths, ap)
|
paths = append(paths, ap)
|
||||||
}
|
}
|
||||||
|
|||||||
362
internal/scanner/scanner_test.go
Normal file
362
internal/scanner/scanner_test.go
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
package scanner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
s := New()
|
||||||
|
assert.NotNil(t, s)
|
||||||
|
assert.Equal(t, int64(0), s.FileCount())
|
||||||
|
assert.Equal(t, int64(0), s.TotalBytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewWithOptions(t *testing.T) {
|
||||||
|
t.Run("nil options", func(t *testing.T) {
|
||||||
|
s := NewWithOptions(nil)
|
||||||
|
assert.NotNil(t, s)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("with options", func(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
opts := &Options{
|
||||||
|
IncludeDotfiles: true,
|
||||||
|
FollowSymLinks: true,
|
||||||
|
Fs: fs,
|
||||||
|
}
|
||||||
|
s := NewWithOptions(opts)
|
||||||
|
assert.NotNil(t, s)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnumerateFile(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/test.txt", []byte("hello world"), 0644))
|
||||||
|
|
||||||
|
s := NewWithOptions(&Options{Fs: fs})
|
||||||
|
err := s.EnumerateFile("/test.txt")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1), s.FileCount())
|
||||||
|
assert.Equal(t, int64(11), s.TotalBytes())
|
||||||
|
|
||||||
|
files := s.Files()
|
||||||
|
require.Len(t, files, 1)
|
||||||
|
assert.Equal(t, "test.txt", files[0].Path)
|
||||||
|
assert.Equal(t, int64(11), files[0].Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnumerateFileMissing(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
s := NewWithOptions(&Options{Fs: fs})
|
||||||
|
err := s.EnumerateFile("/nonexistent.txt")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnumeratePath(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
require.NoError(t, fs.MkdirAll("/testdir/subdir", 0755))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("one"), 0644))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/testdir/file2.txt", []byte("two"), 0644))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/testdir/subdir/file3.txt", []byte("three"), 0644))
|
||||||
|
|
||||||
|
s := NewWithOptions(&Options{Fs: fs})
|
||||||
|
err := s.EnumeratePath("/testdir", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(3), s.FileCount())
|
||||||
|
assert.Equal(t, int64(3+3+5), s.TotalBytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnumeratePathWithProgress(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
require.NoError(t, fs.MkdirAll("/testdir", 0755))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("one"), 0644))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/testdir/file2.txt", []byte("two"), 0644))
|
||||||
|
|
||||||
|
s := NewWithOptions(&Options{Fs: fs})
|
||||||
|
progress := make(chan EnumerateStatus, 10)
|
||||||
|
|
||||||
|
err := s.EnumeratePath("/testdir", progress)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var updates []EnumerateStatus
|
||||||
|
for p := range progress {
|
||||||
|
updates = append(updates, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NotEmpty(t, updates)
|
||||||
|
// Final update should show all files
|
||||||
|
final := updates[len(updates)-1]
|
||||||
|
assert.Equal(t, int64(2), final.FilesFound)
|
||||||
|
assert.Equal(t, int64(6), final.BytesFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnumeratePaths(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
require.NoError(t, fs.MkdirAll("/dir1", 0755))
|
||||||
|
require.NoError(t, fs.MkdirAll("/dir2", 0755))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/dir1/a.txt", []byte("aaa"), 0644))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/dir2/b.txt", []byte("bbb"), 0644))
|
||||||
|
|
||||||
|
s := NewWithOptions(&Options{Fs: fs})
|
||||||
|
err := s.EnumeratePaths(nil, "/dir1", "/dir2")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(2), s.FileCount())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExcludeDotfiles(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
require.NoError(t, fs.MkdirAll("/testdir/.hidden", 0755))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/testdir/visible.txt", []byte("visible"), 0644))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/testdir/.hidden.txt", []byte("hidden"), 0644))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/testdir/.hidden/inside.txt", []byte("inside"), 0644))
|
||||||
|
|
||||||
|
t.Run("exclude by default", func(t *testing.T) {
|
||||||
|
s := NewWithOptions(&Options{Fs: fs, IncludeDotfiles: false})
|
||||||
|
err := s.EnumeratePath("/testdir", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1), s.FileCount())
|
||||||
|
files := s.Files()
|
||||||
|
assert.Equal(t, "visible.txt", files[0].Path)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("include when enabled", func(t *testing.T) {
|
||||||
|
s := NewWithOptions(&Options{Fs: fs, IncludeDotfiles: true})
|
||||||
|
err := s.EnumeratePath("/testdir", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(3), s.FileCount())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPathIsHidden(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
path string
|
||||||
|
hidden bool
|
||||||
|
}{
|
||||||
|
{"file.txt", false},
|
||||||
|
{".hidden", true},
|
||||||
|
{"dir/file.txt", false},
|
||||||
|
{"dir/.hidden", true},
|
||||||
|
{".dir/file.txt", true},
|
||||||
|
{"/absolute/path", false},
|
||||||
|
{"/absolute/.hidden", true},
|
||||||
|
{"./relative", false}, // path.Clean removes leading ./
|
||||||
|
{"a/b/c/.d/e", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.path, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.hidden, pathIsHidden(tt.path), "pathIsHidden(%q)", tt.path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToManifest(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
require.NoError(t, fs.MkdirAll("/testdir", 0755))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("content one"), 0644))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/testdir/file2.txt", []byte("content two"), 0644))
|
||||||
|
|
||||||
|
s := NewWithOptions(&Options{Fs: fs})
|
||||||
|
err := s.EnumeratePath("/testdir", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = s.ToManifest(context.Background(), &buf, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Manifest should have magic bytes
|
||||||
|
assert.True(t, buf.Len() > 0)
|
||||||
|
assert.Equal(t, "ZNAVSRFG", string(buf.Bytes()[:8]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToManifestWithProgress(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
require.NoError(t, fs.MkdirAll("/testdir", 0755))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/testdir/file.txt", bytes.Repeat([]byte("x"), 1000), 0644))
|
||||||
|
|
||||||
|
s := NewWithOptions(&Options{Fs: fs})
|
||||||
|
err := s.EnumeratePath("/testdir", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
progress := make(chan ScanStatus, 10)
|
||||||
|
|
||||||
|
err = s.ToManifest(context.Background(), &buf, progress)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var updates []ScanStatus
|
||||||
|
for p := range progress {
|
||||||
|
updates = append(updates, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NotEmpty(t, updates)
|
||||||
|
// Final update should show completion
|
||||||
|
final := updates[len(updates)-1]
|
||||||
|
assert.Equal(t, int64(1), final.TotalFiles)
|
||||||
|
assert.Equal(t, int64(1), final.ScannedFiles)
|
||||||
|
assert.Equal(t, int64(1000), final.TotalBytes)
|
||||||
|
assert.Equal(t, int64(1000), final.ScannedBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToManifestContextCancellation(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
require.NoError(t, fs.MkdirAll("/testdir", 0755))
|
||||||
|
// Create many files to ensure we have time to cancel
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
name := string(rune('a'+i%26)) + string(rune('0'+i/26)) + ".txt"
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/testdir/"+name, bytes.Repeat([]byte("x"), 100), 0644))
|
||||||
|
}
|
||||||
|
|
||||||
|
s := NewWithOptions(&Options{Fs: fs})
|
||||||
|
err := s.EnumeratePath("/testdir", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel() // Cancel immediately
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = s.ToManifest(ctx, &buf, nil)
|
||||||
|
assert.ErrorIs(t, err, context.Canceled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToManifestEmptyScanner(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
s := NewWithOptions(&Options{Fs: fs})
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := s.ToManifest(context.Background(), &buf, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Should still produce a valid manifest
|
||||||
|
assert.True(t, buf.Len() > 0)
|
||||||
|
assert.Equal(t, "ZNAVSRFG", string(buf.Bytes()[:8]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilesCopiesSlice(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/test.txt", []byte("hello"), 0644))
|
||||||
|
|
||||||
|
s := NewWithOptions(&Options{Fs: fs})
|
||||||
|
require.NoError(t, s.EnumerateFile("/test.txt"))
|
||||||
|
|
||||||
|
files1 := s.Files()
|
||||||
|
files2 := s.Files()
|
||||||
|
|
||||||
|
// Should be different slices
|
||||||
|
assert.NotSame(t, &files1[0], &files2[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnumerateFS(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
require.NoError(t, fs.MkdirAll("/testdir/sub", 0755))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/testdir/file.txt", []byte("hello"), 0644))
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/testdir/sub/nested.txt", []byte("world"), 0644))
|
||||||
|
|
||||||
|
// Create a basepath filesystem
|
||||||
|
baseFs := afero.NewBasePathFs(fs, "/testdir")
|
||||||
|
|
||||||
|
s := NewWithOptions(&Options{Fs: fs})
|
||||||
|
err := s.EnumerateFS(baseFs, "/testdir", nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, int64(2), s.FileCount())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendEnumerateStatusNonBlocking(t *testing.T) {
|
||||||
|
// Channel with no buffer - send should not block
|
||||||
|
ch := make(chan EnumerateStatus)
|
||||||
|
|
||||||
|
// This should not block
|
||||||
|
done := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
sendEnumerateStatus(ch, EnumerateStatus{FilesFound: 1})
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
// Success - did not block
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Fatal("sendEnumerateStatus blocked on full channel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendScanStatusNonBlocking(t *testing.T) {
|
||||||
|
// Channel with no buffer - send should not block
|
||||||
|
ch := make(chan ScanStatus)
|
||||||
|
|
||||||
|
done := make(chan bool)
|
||||||
|
go func() {
|
||||||
|
sendScanStatus(ch, ScanStatus{ScannedFiles: 1})
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
// Success - did not block
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
t.Fatal("sendScanStatus blocked on full channel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendStatusNilChannel(t *testing.T) {
|
||||||
|
// Should not panic with nil channel
|
||||||
|
sendEnumerateStatus(nil, EnumerateStatus{})
|
||||||
|
sendScanStatus(nil, ScanStatus{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileEntryFields(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
now := time.Now().Truncate(time.Second)
|
||||||
|
require.NoError(t, afero.WriteFile(fs, "/test.txt", []byte("content"), 0644))
|
||||||
|
require.NoError(t, fs.Chtimes("/test.txt", now, now))
|
||||||
|
|
||||||
|
s := NewWithOptions(&Options{Fs: fs})
|
||||||
|
require.NoError(t, s.EnumerateFile("/test.txt"))
|
||||||
|
|
||||||
|
files := s.Files()
|
||||||
|
require.Len(t, files, 1)
|
||||||
|
|
||||||
|
entry := files[0]
|
||||||
|
assert.Equal(t, "test.txt", entry.Path)
|
||||||
|
assert.Contains(t, entry.AbsPath, "test.txt")
|
||||||
|
assert.Equal(t, int64(7), entry.Size)
|
||||||
|
// Mtime should be set (within a second of now)
|
||||||
|
assert.WithinDuration(t, now, entry.Mtime, 2*time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLargeFileEnumeration(t *testing.T) {
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
require.NoError(t, fs.MkdirAll("/testdir", 0755))
|
||||||
|
|
||||||
|
// Create 100 files
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
name := "/testdir/" + string(rune('a'+i%26)) + string(rune('0'+i/26%10)) + ".txt"
|
||||||
|
require.NoError(t, afero.WriteFile(fs, name, []byte("data"), 0644))
|
||||||
|
}
|
||||||
|
|
||||||
|
s := NewWithOptions(&Options{Fs: fs})
|
||||||
|
progress := make(chan EnumerateStatus, 200)
|
||||||
|
|
||||||
|
err := s.EnumeratePath("/testdir", progress)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Drain channel
|
||||||
|
for range progress {
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, int64(100), s.FileCount())
|
||||||
|
assert.Equal(t, int64(400), s.TotalBytes()) // 100 * 4 bytes
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user