Some checks failed
check / check (push) Has been cancelled
Adopts golangci-lint v2.12.2 and the canonical .golangci.yml (default: all), and fixes all resulting findings across the tree. Two intended behavior changes: absent MFFilePath.Mtime is handled explicitly in freshen, list and export rather than dereferenced (main panicked); gpg positional key IDs now follow an explicit -- end-of-options marker. All twelve reworded user-visible error messages restored to byte-identical parity with main and pinned by tests.
198 lines
5.3 KiB
Go
198 lines
5.3 KiB
Go
//nolint:testpackage // white-box tests exercise unexported internals
|
|
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/mfer/mfer"
|
|
)
|
|
|
|
// stubFileInfo is a minimal fs.FileInfo for exercising recordEntry
|
|
// without touching a filesystem.
|
|
type stubFileInfo struct {
|
|
size int64
|
|
mtime time.Time
|
|
}
|
|
|
|
func (s stubFileInfo) Name() string { return "stub" }
|
|
func (s stubFileInfo) Size() int64 { return s.size }
|
|
func (s stubFileInfo) Mode() os.FileMode { return 0 }
|
|
func (s stubFileInfo) ModTime() time.Time { return s.mtime }
|
|
func (s stubFileInfo) IsDir() bool { return false }
|
|
func (s stubFileInfo) Sys() any { return nil }
|
|
|
|
// setupFreshenDir populates /testdir with two files, scans it, and
|
|
// writes the resulting manifest to /testdir/.index.mf.
|
|
func setupFreshenDir(t *testing.T, fs afero.Fs) {
|
|
t.Helper()
|
|
|
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
writeTestFile(t, fs, testFile1, "content1")
|
|
writeTestFile(t, fs, "/testdir/file2.txt", "content2")
|
|
|
|
// Generate initial manifest
|
|
opts := &mfer.ScannerOptions{Fs: fs}
|
|
s := mfer.NewScannerWithOptions(opts)
|
|
require.NoError(t, s.EnumeratePath(testDir, nil))
|
|
|
|
var manifestBuf bytes.Buffer
|
|
|
|
require.NoError(t, s.ToManifest(context.Background(), &manifestBuf, nil))
|
|
|
|
// Write manifest to filesystem
|
|
require.NoError(t,
|
|
afero.WriteFile(fs, "/testdir/.index.mf", manifestBuf.Bytes(), 0o644))
|
|
}
|
|
|
|
func TestFreshenUnchanged(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
setupFreshenDir(t, fs)
|
|
|
|
// Parse manifest to verify
|
|
manifest, err := mfer.NewManifestFromFile(fs, "/testdir/.index.mf")
|
|
require.NoError(t, err)
|
|
assert.Len(t, manifest.Files(), 2)
|
|
}
|
|
|
|
func TestFreshenWithChanges(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs := afero.NewMemMapFs()
|
|
setupFreshenDir(t, fs)
|
|
|
|
// Verify initial manifest has 2 files
|
|
manifest, err := mfer.NewManifestFromFile(fs, "/testdir/.index.mf")
|
|
require.NoError(t, err)
|
|
assert.Len(t, manifest.Files(), 2)
|
|
|
|
// Add a new file
|
|
writeTestFile(t, fs, "/testdir/file3.txt", "content3")
|
|
|
|
// Modify file2 (change content and size)
|
|
writeTestFile(t, fs, "/testdir/file2.txt", "modified content2")
|
|
|
|
// Remove file1
|
|
require.NoError(t, fs.Remove(testFile1))
|
|
|
|
// Note: The freshen operation would need to be run here
|
|
// For now, we just verify the test setup is correct
|
|
exists, _ := afero.Exists(fs, testFile1)
|
|
assert.False(t, exists)
|
|
|
|
exists, _ = afero.Exists(fs, "/testdir/file3.txt")
|
|
assert.True(t, exists)
|
|
|
|
content, _ := afero.ReadFile(fs, "/testdir/file2.txt")
|
|
assert.Equal(t, "modified content2", string(content))
|
|
}
|
|
|
|
// TestFreshenRecordEntryMtimePresence pins the behavior of recordEntry
|
|
// with respect to MFFilePath.Mtime, which is a message pointer with
|
|
// proto3 field presence and may legitimately be absent.
|
|
//
|
|
// An absent mtime must never be read as time.Unix(0, 0): that value
|
|
// never equals a real modification time, so every entry would be
|
|
// classified as changed, re-hashed, and the manifest rewritten
|
|
// unconditionally - the exact inverse of what freshen is for, and
|
|
// silent. An entry with no mtime is therefore "changed" because it
|
|
// cannot be compared, not because it looks like it dates from 1970.
|
|
func TestFreshenRecordEntryMtimePresence(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const relPath = "file1.txt"
|
|
|
|
mtime := time.Unix(1_700_000_000, 0)
|
|
info := stubFileInfo{size: 8, mtime: mtime}
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
entry *mfer.MFFilePath
|
|
needsHash bool
|
|
changed int64
|
|
unchanged int64
|
|
}{
|
|
{
|
|
name: "matching mtime and size is unchanged",
|
|
entry: &mfer.MFFilePath{
|
|
Path: relPath,
|
|
Size: 8,
|
|
Mtime: &mfer.Timestamp{Seconds: mtime.Unix()},
|
|
},
|
|
needsHash: false,
|
|
changed: 0,
|
|
unchanged: 1,
|
|
},
|
|
{
|
|
name: "absent mtime is changed, not epoch",
|
|
entry: &mfer.MFFilePath{
|
|
Path: relPath,
|
|
Size: 8,
|
|
Mtime: nil,
|
|
},
|
|
needsHash: true,
|
|
changed: 1,
|
|
unchanged: 0,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := &freshenScanner{
|
|
existingByPath: map[string]*mfer.MFFilePath{relPath: tc.entry},
|
|
}
|
|
s.recordEntry(relPath, info)
|
|
|
|
require.Len(t, s.entries, 1)
|
|
assert.Equal(t, tc.needsHash, s.entries[0].needsHash)
|
|
assert.Equal(t, tc.changed, s.changed)
|
|
assert.Equal(t, tc.unchanged, s.unchanged)
|
|
assert.Zero(t, s.added)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFreshenAddExistingRejectsMissingMtime pins that an entry with no
|
|
// mtime is never carried forward into a rebuilt manifest with a
|
|
// fabricated epoch timestamp.
|
|
func TestFreshenAddExistingRejectsMissingMtime(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
b := mfer.NewBuilder()
|
|
entry := &mfer.MFFilePath{
|
|
Path: "file1.txt",
|
|
Size: 8,
|
|
Mtime: nil,
|
|
Hashes: []*mfer.MFFileChecksum{
|
|
{MultiHash: []byte{0x12, 0x20}},
|
|
},
|
|
}
|
|
|
|
err := addExistingToBuilder(b, entry)
|
|
require.ErrorIs(t, err, errEntryMissingMtime)
|
|
assert.Contains(t, err.Error(), "file1.txt")
|
|
}
|
|
|
|
// TestEntryMtime pins the presence semantics the callers depend on.
|
|
func TestEntryMtime(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got, ok := entryMtime(&mfer.MFFilePath{Mtime: nil})
|
|
assert.False(t, ok)
|
|
assert.True(t, got.IsZero())
|
|
|
|
got, ok = entryMtime(&mfer.MFFilePath{
|
|
Mtime: &mfer.Timestamp{Seconds: 1_700_000_000, Nanos: 500},
|
|
})
|
|
assert.True(t, ok)
|
|
assert.Equal(t, time.Unix(1_700_000_000, 500), got)
|
|
}
|