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:
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
|
||||
}
|
||||
Reference in New Issue
Block a user