check / check (push) Successful in 2m12s
filepath.Walk hands its callback the error for a path it could not read, such as a directory it cannot list. Both callbacks, in walkAndProcess and countFiles, returned that error before they checked the exclusions, so a directory that --exclude or --exclude-dotfiles excluded still failed the run when it could not be listed. Given an error, each callback now checks the exclusions first and skips an excluded path. The new test excludes a directory that cannot be listed and runs sum add and check over its parent. Model: opus-5-5
379 lines
8.2 KiB
Go
379 lines
8.2 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pkg/xattr"
|
|
)
|
|
|
|
const (
|
|
dirPerm = 0o755
|
|
filePerm = 0o644
|
|
noPerm = 0o000
|
|
|
|
// futureSkew advances a file's mtime far enough to be unambiguously
|
|
// newer than a previously recorded sumtime.
|
|
futureSkew = 2 * time.Second
|
|
)
|
|
|
|
// skipIfNoXattr skips the test unless the filesystem under dir supports the
|
|
// extended-attribute namespace the program actually uses. Probing with the
|
|
// real checksumKey (rather than a user.* key) matters on Linux, where regular
|
|
// files only accept xattrs in the user.* namespace and the program's
|
|
// berlin.sneak.* keys yield "operation not supported".
|
|
func skipIfNoXattr(t *testing.T, dir string) {
|
|
t.Helper()
|
|
|
|
probe := filepath.Join(dir, "xattr-probe")
|
|
|
|
err := os.WriteFile(probe, []byte("probe"), filePerm)
|
|
if err != nil {
|
|
t.Fatalf("write probe file: %v", err)
|
|
}
|
|
|
|
defer func() { _ = os.Remove(probe) }()
|
|
|
|
err = xattr.Set(probe, checksumKey, []byte("1"))
|
|
if err != nil {
|
|
t.Skipf("skipping: xattr namespace %q not supported: %v", checksumKey, err)
|
|
|
|
return
|
|
}
|
|
|
|
_ = xattr.Remove(probe, checksumKey)
|
|
}
|
|
|
|
func writeFile(t *testing.T, root, name, content string) string {
|
|
t.Helper()
|
|
|
|
p := filepath.Join(root, name)
|
|
|
|
err := os.MkdirAll(filepath.Dir(p), dirPerm)
|
|
if err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
|
|
err = os.WriteFile(p, []byte(content), filePerm)
|
|
if err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
return p
|
|
}
|
|
|
|
func newTestStats() *Stats {
|
|
return &Stats{StartTime: time.Now()}
|
|
}
|
|
|
|
func TestSumAddAndUpdate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
opts := &options{}
|
|
dir := t.TempDir()
|
|
skipIfNoXattr(t, dir)
|
|
|
|
f := writeFile(t, dir, "a.txt", "hello")
|
|
|
|
err := processSumAdd(opts, dir, newTestStats(), nil)
|
|
if err != nil {
|
|
t.Fatalf("add: %v", err)
|
|
}
|
|
|
|
_, err = xattr.Get(f, checksumKey)
|
|
if err != nil {
|
|
t.Fatalf("checksum missing: %v", err)
|
|
}
|
|
|
|
tsb, _ := xattr.Get(f, sumTimeKey)
|
|
origTime, _ := time.Parse(time.RFC3339Nano, string(tsb))
|
|
|
|
err = os.WriteFile(f, []byte(strings.ToUpper("hello")), filePerm)
|
|
if err != nil {
|
|
t.Fatalf("rewrite: %v", err)
|
|
}
|
|
|
|
now := time.Now().Add(futureSkew)
|
|
|
|
err = os.Chtimes(f, now, now)
|
|
if err != nil {
|
|
t.Fatalf("chtimes: %v", err)
|
|
}
|
|
|
|
err = processSumUpdate(opts, dir, newTestStats(), nil)
|
|
if err != nil {
|
|
t.Fatalf("update: %v", err)
|
|
}
|
|
|
|
tsb2, _ := xattr.Get(f, sumTimeKey)
|
|
|
|
newTime, _ := time.Parse(time.RFC3339Nano, string(tsb2))
|
|
if !newTime.After(origTime) {
|
|
t.Fatalf("sumtime not updated")
|
|
}
|
|
}
|
|
|
|
func TestProcessCheckIntegration(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
opts := &options{}
|
|
dir := t.TempDir()
|
|
skipIfNoXattr(t, dir)
|
|
|
|
writeFile(t, dir, "b.txt", "world")
|
|
|
|
err := processSumAdd(opts, dir, newTestStats(), nil)
|
|
if err != nil {
|
|
t.Fatalf("add: %v", err)
|
|
}
|
|
|
|
err = processCheck(opts, dir, false, newTestStats(), nil)
|
|
if err != nil {
|
|
t.Fatalf("check ok: %v", err)
|
|
}
|
|
|
|
f := filepath.Join(dir, "b.txt")
|
|
|
|
err = os.WriteFile(f, []byte("corrupt"), filePerm)
|
|
if err != nil {
|
|
t.Fatalf("corrupt: %v", err)
|
|
}
|
|
|
|
err = processCheck(opts, dir, false, newTestStats(), nil)
|
|
if err == nil {
|
|
t.Fatalf("expected mismatch error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestClearRemovesAttrs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
opts := &options{}
|
|
dir := t.TempDir()
|
|
skipIfNoXattr(t, dir)
|
|
|
|
f := writeFile(t, dir, "c.txt", "data")
|
|
|
|
err := processSumAdd(opts, dir, newTestStats(), nil)
|
|
if err != nil {
|
|
t.Fatalf("add: %v", err)
|
|
}
|
|
|
|
err = processClear(opts, dir, newTestStats(), nil)
|
|
if err != nil {
|
|
t.Fatalf("clear: %v", err)
|
|
}
|
|
|
|
_, err = xattr.Get(f, checksumKey)
|
|
if err == nil {
|
|
t.Fatalf("checksum still present after clear")
|
|
}
|
|
|
|
_, err = xattr.Get(f, sumTimeKey)
|
|
if err == nil {
|
|
t.Fatalf("sumtime still present after clear")
|
|
}
|
|
}
|
|
|
|
func TestExcludeDotfilesAndPatterns(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
opts := &options{
|
|
excludeDotfiles: true,
|
|
excludePatterns: []string{"*.me"},
|
|
}
|
|
dir := t.TempDir()
|
|
skipIfNoXattr(t, dir)
|
|
|
|
hidden := writeFile(t, dir, ".hidden", "dot")
|
|
keep := writeFile(t, dir, "keep.txt", "keep")
|
|
skip := writeFile(t, dir, "skip.me", "skip")
|
|
|
|
err := processSumAdd(opts, dir, newTestStats(), nil)
|
|
if err != nil {
|
|
t.Fatalf("add with excludes: %v", err)
|
|
}
|
|
|
|
_, err = xattr.Get(keep, checksumKey)
|
|
if err != nil {
|
|
t.Fatalf("expected xattr on keep.txt: %v", err)
|
|
}
|
|
|
|
_, err = xattr.Get(hidden, checksumKey)
|
|
if err == nil {
|
|
t.Fatalf(".hidden should have been excluded")
|
|
}
|
|
|
|
_, err = xattr.Get(skip, checksumKey)
|
|
if err == nil {
|
|
t.Fatalf("skip.me should have been excluded")
|
|
}
|
|
}
|
|
|
|
func TestExcludeUnreadableDir(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
opts := &options{excludePatterns: []string{"locked"}}
|
|
dir := t.TempDir()
|
|
skipIfNoXattr(t, dir)
|
|
|
|
keep := writeFile(t, dir, "keep.txt", "keep")
|
|
hidden := writeFile(t, dir, "locked/a.txt", "hidden")
|
|
|
|
// The excluded directory cannot be listed.
|
|
sub := filepath.Join(dir, "locked")
|
|
|
|
err := os.Chmod(sub, noPerm)
|
|
if err != nil {
|
|
t.Fatalf("chmod dir: %v", err)
|
|
}
|
|
|
|
defer func() { _ = os.Chmod(sub, dirPerm) }()
|
|
|
|
err = processSumAdd(opts, dir, newTestStats(), nil)
|
|
if err != nil {
|
|
t.Fatalf("add: %v", err)
|
|
}
|
|
|
|
_, err = xattr.Get(keep, checksumKey)
|
|
if err != nil {
|
|
t.Fatalf("expected xattr on keep.txt: %v", err)
|
|
}
|
|
|
|
// Without --quiet, runCheck counts the files for the progress bar
|
|
// before it checks any, so this also covers the count.
|
|
err = runCheck(opts, []string{dir}, false)
|
|
if err != nil {
|
|
t.Fatalf("check: %v", err)
|
|
}
|
|
|
|
err = os.Chmod(sub, dirPerm)
|
|
if err != nil {
|
|
t.Fatalf("chmod dir back: %v", err)
|
|
}
|
|
|
|
_, err = xattr.Get(hidden, checksumKey)
|
|
if err == nil {
|
|
t.Fatalf("locked/a.txt should have been excluded")
|
|
}
|
|
}
|
|
|
|
func TestSkipBrokenSymlink(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
opts := &options{}
|
|
dir := t.TempDir()
|
|
skipIfNoXattr(t, dir)
|
|
|
|
// Create a dangling symlink.
|
|
link := filepath.Join(dir, "dangling.lnk")
|
|
|
|
err := os.Symlink(filepath.Join(dir, "nonexistent.txt"), link)
|
|
if err != nil {
|
|
t.Fatalf("symlink: %v", err)
|
|
}
|
|
|
|
// Should not error and should not create xattrs on link.
|
|
err = processSumAdd(opts, dir, newTestStats(), nil)
|
|
if err != nil {
|
|
t.Fatalf("processSumAdd with symlink: %v", err)
|
|
}
|
|
|
|
_, err = xattr.Get(link, checksumKey)
|
|
if err == nil {
|
|
t.Fatalf("symlink should not have xattr")
|
|
}
|
|
}
|
|
|
|
func TestPermissionErrors(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
opts := &options{}
|
|
dir := t.TempDir()
|
|
skipIfNoXattr(t, dir)
|
|
|
|
secret := writeFile(t, dir, "secret.txt", "data")
|
|
|
|
err := os.Chmod(secret, noPerm)
|
|
if err != nil {
|
|
t.Fatalf("chmod: %v", err)
|
|
}
|
|
|
|
defer func() { _ = os.Chmod(secret, filePerm) }()
|
|
|
|
err = processSumAdd(opts, dir, newTestStats(), nil)
|
|
if err == nil {
|
|
t.Fatalf("expected permission error, got nil")
|
|
}
|
|
|
|
err = processSumUpdate(opts, dir, newTestStats(), nil)
|
|
if err == nil {
|
|
t.Fatalf("expected permission error on update, got nil")
|
|
}
|
|
|
|
err = processCheck(opts, dir, false, newTestStats(), nil)
|
|
if err == nil {
|
|
t.Fatalf("expected permission error on check, got nil")
|
|
}
|
|
}
|
|
|
|
func TestCheckContinuePastUnreadable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
opts := &options{}
|
|
dir := t.TempDir()
|
|
skipIfNoXattr(t, dir)
|
|
|
|
writeFile(t, dir, "a.txt", "one")
|
|
secret := writeFile(t, dir, "b.txt", "two")
|
|
writeFile(t, dir, "c/d.txt", "three")
|
|
writeFile(t, dir, "e.txt", "four")
|
|
|
|
err := processSumAdd(opts, dir, newTestStats(), nil)
|
|
if err != nil {
|
|
t.Fatalf("add: %v", err)
|
|
}
|
|
|
|
// An unreadable file and an unlistable directory sit between the
|
|
// readable files a.txt and e.txt.
|
|
sub := filepath.Join(dir, "c")
|
|
|
|
err = os.Chmod(secret, noPerm)
|
|
if err != nil {
|
|
t.Fatalf("chmod file: %v", err)
|
|
}
|
|
|
|
defer func() { _ = os.Chmod(secret, filePerm) }()
|
|
|
|
err = os.Chmod(sub, noPerm)
|
|
if err != nil {
|
|
t.Fatalf("chmod dir: %v", err)
|
|
}
|
|
|
|
defer func() { _ = os.Chmod(sub, dirPerm) }()
|
|
|
|
stats := newTestStats()
|
|
|
|
err = processCheck(opts, dir, true, stats, nil)
|
|
if !errors.Is(err, errVerification) {
|
|
t.Fatalf("expected verification error, got %v", err)
|
|
}
|
|
|
|
if stats.FilesProcessed != 2 || stats.FilesFailed != 2 {
|
|
t.Fatalf("expected 2 verified and 2 failed, got %d and %d",
|
|
stats.FilesProcessed, stats.FilesFailed)
|
|
}
|
|
|
|
// Without --quiet, runCheck counts the files for the progress bar
|
|
// before it checks any, so the count reaches the unlistable directory
|
|
// first.
|
|
err = runCheck(opts, []string{dir}, true)
|
|
if !errors.Is(err, errVerification) {
|
|
t.Fatalf("expected verification error from runCheck, got %v", err)
|
|
}
|
|
}
|