Compare commits

...
2 Commits
Author SHA1 Message Date
sneak ba7d755dc2 Never regenerate mf.pb.go during checks; fail when it is stale (closes #71)
check / check (push) Failing after 2s
The test and format scripts regenerated mfer/mf.pb.go whenever
mfer/mf.proto looked newer by mtime, which a fresh checkout often causes,
so make check could rewrite a committed file and needed protoc. Nothing
regenerates it any more except make generate (script/generate), which
refuses to run unless protoc 33.4 and protoc-gen-go v1.36.11, the
versions that wrote the committed file, are on PATH, and records the
hash of mf.proto in mfer/mf.proto.sha256. A Go test compares that hash
with mf.proto and fails, naming make generate, when they differ. The
mtime rule, the unused protoc-gen-go v1.28.1 install rule, make clean's
deletion of mf.pb.go and the Dockerfile's touch workarounds are removed.

Model: opus-5-5
2026-10-04 10:53:02 +00:00
clawbot 0a9963002c NewChecker takes CheckerOptions instead of positional arguments (closes #78)
check / check (push) Failing after 1s
NewChecker now takes *CheckerOptions (ManifestPath, BasePath, Fs), named
like ScannerOptions. A nil Fs still means the OS filesystem, as before and
as in ScannerOptions; nil options or an empty path return an error naming
the missing path.

Audit of the other exported constructors in mfer: NewManifestFromFile took
a filesystem and a path positionally; it now takes
*ManifestFromFileOptions (Path, Fs) with the same nil and empty rules.
NewBuilder and NewScanner take no arguments, NewScannerWithOptions already
takes options, and NewManifestFromReader takes one reader, which the style
guide exempts; these are unchanged.

Model: opus-5-5
2026-10-04 12:19:31 +02:00
18 changed files with 401 additions and 117 deletions
-6
View File
@@ -8,9 +8,6 @@ RUN go mod download
COPY . .
# Touch .pb.go so make does not try to regenerate via protoc (file is committed)
RUN touch mfer/mf.pb.go
# Go half of fmt-check only: this image has no node, so no prettier. The
# markdown half runs in the mdfmt stage below.
RUN make fmt-check-go
@@ -46,9 +43,6 @@ RUN go mod download
COPY . .
# Touch .pb.go so make does not try to regenerate via protoc (file is committed)
RUN touch mfer/mf.pb.go
RUN make test
# A build context sent as a tar archive, as upaas sends it, keeps its files'
+5 -10
View File
@@ -2,7 +2,6 @@ export DOCKER_BUILDKIT := 1
export PROGRESS_NO_TRUNC := 1
GOPATH := $(shell go env GOPATH)
export PATH := $(PATH):$(GOPATH)/bin
PROTOC_GEN_GO := $(GOPATH)/bin/protoc-gen-go
SOURCEFILES := mfer/*.go mfer/*.proto internal/*/*.go cmd/*/*.go go.mod go.sum
ARCH := $(shell uname -m)
GITREV_BUILD := $(shell bash $(PWD)/bin/gitrev.sh 2>/dev/null || echo unknown)
@@ -13,7 +12,7 @@ GOLDFLAGS += -X main.Version=$(VERSION)
GOLDFLAGS += -X main.Gitrev=$(GITREV_BUILD)
GOFLAGS := -ldflags "$(GOLDFLAGS)"
.PHONY: bootstrap setup docker default run ci test fuzz check lint fmt fmt-check fmt-check-go fmt-check-md hooks fixme
.PHONY: bootstrap setup docker default run ci test fuzz check lint fmt fmt-check fmt-check-go fmt-check-md hooks fixme generate
default: fmt test
@@ -35,9 +34,6 @@ test:
fuzz:
@script/fuzz
$(PROTOC_GEN_GO):
test -e $(PROTOC_GEN_GO) || go install -v google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1
fixme:
@grep -nir fixme . | grep -v Makefile
@@ -58,15 +54,14 @@ fmt-check-md:
hooks:
@script/install-precommit
mfer/mf.pb.go: mfer/mf.proto
cd mfer && go generate .
generate:
@script/generate
bin/mfer: $(SOURCEFILES) mfer/mf.pb.go
protoc --version
bin/mfer: $(SOURCEFILES)
cd cmd/mfer && go build -tags urfave_cli_no_docs -o ../../bin/mfer $(GOFLAGS) .
clean:
rm -rfv mfer/*.pb.go bin/mfer cmd/mfer/mfer *.dockerimage
rm -rfv bin/mfer cmd/mfer/mfer *.dockerimage
fmt:
@script/fmt
+13 -2
View File
@@ -72,8 +72,19 @@ provide:
`script/bootstrap`, then `script/install-precommit`
- `script/projectname` — output the project name (`mfer`); used by other scripts
such as `script/docker`
- `script/test` — run the test suite (`go test`), regenerating the protobuf code
first if it is stale
- `script/test` — run the test suite (`go test`); one test fails when
`mfer/mf.proto` no longer matches the hash `script/generate` recorded
- `script/generate` (`make generate`) — regenerate `mfer/mf.pb.go` from
`mfer/mf.proto` and record the hash of that `mfer/mf.proto` in
`mfer/mf.proto.sha256`; the only thing that regenerates the committed
`mfer/mf.pb.go`. It needs the exact versions that wrote the committed file,
and refuses to run with any other: `protoc` 33.4 (unpack
`protoc-33.4-<platform>.zip` from
[its release](https://github.com/protocolbuffers/protobuf/releases/tag/v33.4)
and put its `bin/protoc` on `PATH`) and `protoc-gen-go` v1.36.11
(`go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11`, which
installs it in `$(go env GOPATH)/bin`; `make generate` adds that directory to
`PATH`)
- `script/fuzz` — fuzz the manifest parser for one minute; run by hand
(`make fuzz`), never by CI, while `script/test` runs its committed seed corpus
as ordinary tests
+5 -1
View File
@@ -298,7 +298,11 @@ func (mfa *CLIApp) checkManifestOperation(ctx *cli.Context) error {
log.Infof("checking manifest %s with base %s", manifestPath, basePath)
// Create checker
chk, err := mfer.NewChecker(manifestPath, basePath, mfa.Fs)
chk, err := mfer.NewChecker(&mfer.CheckerOptions{
ManifestPath: manifestPath,
BasePath: basePath,
Fs: mfa.Fs,
})
if err != nil {
return fmt.Errorf("failed to load manifest: %w", err)
}
+8 -2
View File
@@ -504,7 +504,10 @@ func TestGenerateExcludesDotfilesByDefault(t *testing.T) {
assert.True(t, exists)
// Verify manifest only has 1 file (the non-dotfile)
manifest, err := mfer.NewManifestFromFile(fs, testMF)
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{
Path: testMF,
Fs: fs,
})
require.NoError(t, err)
assert.Len(t, manifest.Files(), 1)
assert.Equal(t, "file1.txt", manifest.Files()[0].GetPath())
@@ -528,7 +531,10 @@ func TestGenerateWithIncludeDotfiles(t *testing.T) {
require.Equal(t, 0, exitCode)
// Verify manifest has 2 files (including dotfile)
manifest, err := mfer.NewManifestFromFile(fs, testMF)
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{
Path: testMF,
Fs: fs,
})
require.NoError(t, err)
assert.Len(t, manifest.Files(), 2)
}
+10 -2
View File
@@ -61,7 +61,11 @@ func unsignedChecker(t *testing.T) *mfer.Checker {
require.NoError(t, s.ToManifest(context.Background(), &buf, nil))
require.NoError(t, afero.WriteFile(fs, "/d/index.mf", buf.Bytes(), 0o644))
chk, err := mfer.NewChecker("/d/index.mf", "/d", fs)
chk, err := mfer.NewChecker(&mfer.CheckerOptions{
ManifestPath: "/d/index.mf",
BasePath: "/d",
Fs: fs,
})
require.NoError(t, err)
require.False(t, chk.IsSigned())
@@ -167,7 +171,11 @@ func signedChecker(t *testing.T) *mfer.Checker {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "/index.mf", buf.Bytes(), 0o644))
chk, err := mfer.NewChecker("/index.mf", "/", fs)
chk, err := mfer.NewChecker(&mfer.CheckerOptions{
ManifestPath: "/index.mf",
BasePath: "/",
Fs: fs,
})
require.NoError(t, err)
require.True(t, chk.IsSigned())
+4 -1
View File
@@ -445,7 +445,10 @@ func (mfa *CLIApp) loadExistingEntries(
log.Infof("loading manifest from %s", manifestPath)
// Load existing manifest
manifest, err := mfer.NewManifestFromFile(mfa.Fs, manifestPath)
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{
Path: manifestPath,
Fs: mfa.Fs,
})
if err != nil {
return nil, fmt.Errorf("failed to load manifest: %w", err)
}
+8 -2
View File
@@ -58,7 +58,10 @@ func TestFreshenUnchanged(t *testing.T) {
setupFreshenDir(t, fs)
// Parse manifest to verify
manifest, err := mfer.NewManifestFromFile(fs, "/testdir/.index.mf")
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{
Path: "/testdir/.index.mf",
Fs: fs,
})
require.NoError(t, err)
assert.Len(t, manifest.Files(), 2)
}
@@ -70,7 +73,10 @@ func TestFreshenWithChanges(t *testing.T) {
setupFreshenDir(t, fs)
// Verify initial manifest has 2 files
manifest, err := mfer.NewManifestFromFile(fs, "/testdir/.index.mf")
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{
Path: "/testdir/.index.mf",
Fs: fs,
})
require.NoError(t, err)
assert.Len(t, manifest.Files(), 2)
+25
View File
@@ -5,10 +5,12 @@ import (
"bytes"
"context"
"fmt"
"path/filepath"
"strings"
"testing"
"time"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -397,6 +399,29 @@ func TestNewManifestFromReaderTruncated(t *testing.T) {
assert.Error(t, err)
}
func TestNewManifestFromFileRequiresPath(t *testing.T) {
t.Parallel()
_, err := NewManifestFromFile(nil)
require.ErrorIs(t, err, errManifestPathEmpty)
_, err = NewManifestFromFile(&ManifestFromFileOptions{Fs: afero.NewMemMapFs()})
require.ErrorIs(t, err, errManifestPathEmpty)
}
func TestNewManifestFromFileNilFsUsesOsFs(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "index.mf")
createTestManifest(t, afero.NewOsFs(), path, map[string][]byte{
testFileName: []byte("hello"),
})
m, err := NewManifestFromFile(&ManifestFromFileOptions{Path: path})
require.NoError(t, err)
assert.Len(t, m.Files(), 1)
}
func TestManifestString(t *testing.T) {
t.Parallel()
+34 -8
View File
@@ -14,7 +14,11 @@ import (
"github.com/spf13/afero"
)
var errNoSigningPubKey = errors.New("manifest has no signing public key")
var (
errNoSigningPubKey = errors.New("manifest has no signing public key")
errManifestPathEmpty = errors.New("manifest path cannot be empty")
errBasePathEmpty = errors.New("base path cannot be empty")
)
// Result represents the outcome of checking a single file.
type Result struct {
@@ -82,20 +86,42 @@ type Checker struct {
signingPubKey []byte
}
// NewChecker creates a new Checker for the given manifest, base path, and filesystem.
// The basePath is the directory relative to which manifest paths are resolved.
// If fs is nil, the real filesystem (OsFs) is used.
func NewChecker(manifestPath string, basePath string, fs afero.Fs) (*Checker, error) {
// CheckerOptions configures a Checker.
type CheckerOptions struct {
// ManifestPath is the manifest file to check against (required).
ManifestPath string
// BasePath is the directory relative to which manifest paths are
// resolved (required).
BasePath string
// Fs is the filesystem to use, defaults to OsFs if nil.
Fs afero.Fs
}
// NewChecker creates a new Checker with the given options. It returns an
// error if opts is nil or either path is empty.
func NewChecker(opts *CheckerOptions) (*Checker, error) {
if opts == nil || opts.ManifestPath == "" {
return nil, errManifestPathEmpty
}
if opts.BasePath == "" {
return nil, errBasePathEmpty
}
fs := opts.Fs
if fs == nil {
fs = afero.NewOsFs()
}
m, err := NewManifestFromFile(fs, manifestPath)
m, err := NewManifestFromFile(&ManifestFromFileOptions{
Path: opts.ManifestPath,
Fs: fs,
})
if err != nil {
return nil, err
}
abs, err := filepath.Abs(basePath)
abs, err := filepath.Abs(opts.BasePath)
if err != nil {
return nil, err
}
@@ -108,7 +134,7 @@ func NewChecker(manifestPath string, basePath string, fs afero.Fs) (*Checker, er
}
// Compute manifest's relative path from basePath for exclusion in FindExtraFiles
absManifest, err := filepath.Abs(manifestPath)
absManifest, err := filepath.Abs(opts.ManifestPath)
if err != nil {
return nil, err
}
+190 -46
View File
@@ -5,6 +5,8 @@ import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"testing"
"time"
@@ -14,9 +16,11 @@ import (
)
const (
testFile1 = "file1.txt"
testFile2 = "file2.txt"
testExistsFile = "exists.txt"
testFile1 = "file1.txt"
testFile2 = "file2.txt"
testExistsFile = "exists.txt"
testManifestPath = "/manifest.mf"
testDataDir = "/data"
)
func TestStatusString(t *testing.T) {
@@ -66,15 +70,13 @@ func createTestManifest(
}
// createFilesOnDisk creates the given files on the filesystem under
// /data.
// testDataDir.
func createFilesOnDisk(t *testing.T, fs afero.Fs, files map[string][]byte) {
t.Helper()
basePath := "/data"
for path, content := range files {
fullPath := basePath + "/" + path
require.NoError(t, fs.MkdirAll(basePath, 0o755))
fullPath := testDataDir + "/" + path
require.NoError(t, fs.MkdirAll(testDataDir, 0o755))
require.NoError(t, afero.WriteFile(fs, fullPath, content, 0o644))
}
}
@@ -90,9 +92,13 @@ func TestNewChecker(t *testing.T) {
testFile1: []byte("hello"),
testFile2: []byte("world"),
}
createTestManifest(t, fs, "/manifest.mf", files)
createTestManifest(t, fs, testManifestPath, files)
chk, err := NewChecker("/manifest.mf", "/", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: testManifestPath,
BasePath: "/",
Fs: fs,
})
require.NoError(t, err)
assert.NotNil(t, chk)
assert.Equal(t, FileCount(2), chk.FileCount())
@@ -102,7 +108,11 @@ func TestNewChecker(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs()
_, err := NewChecker("/nonexistent.mf", "/", fs)
_, err := NewChecker(&CheckerOptions{
ManifestPath: "/nonexistent.mf",
BasePath: "/",
Fs: fs,
})
assert.Error(t, err)
})
@@ -111,11 +121,73 @@ func TestNewChecker(t *testing.T) {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "/bad.mf", []byte("not a manifest"), 0o644))
_, err := NewChecker("/bad.mf", "/", fs)
_, 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()
@@ -125,9 +197,13 @@ func TestCheckerFileCountAndTotalBytes(t *testing.T) {
"medium.txt": []byte("hello world"),
"large.txt": bytes.Repeat([]byte("x"), 1000),
}
createTestManifest(t, fs, "/manifest.mf", files)
createTestManifest(t, fs, testManifestPath, files)
chk, err := NewChecker("/manifest.mf", "/", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: testManifestPath,
BasePath: "/",
Fs: fs,
})
require.NoError(t, err)
assert.Equal(t, FileCount(3), chk.FileCount())
@@ -142,10 +218,14 @@ func TestCheckAllFilesOK(t *testing.T) {
testFile1: []byte("content one"),
testFile2: []byte("content two"),
}
createTestManifest(t, fs, "/manifest.mf", files)
createTestManifest(t, fs, testManifestPath, files)
createFilesOnDisk(t, fs, files)
chk, err := NewChecker("/manifest.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: testManifestPath,
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
results := make(chan Result, 10)
@@ -172,13 +252,17 @@ func TestCheckMissingFile(t *testing.T) {
testExistsFile: []byte("I exist"),
"missing.txt": []byte("I don't exist on disk"),
}
createTestManifest(t, fs, "/manifest.mf", files)
createTestManifest(t, fs, testManifestPath, files)
// Only create one file
createFilesOnDisk(t, fs, map[string][]byte{
testExistsFile: []byte("I exist"),
})
chk, err := NewChecker("/manifest.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: testManifestPath,
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
results := make(chan Result, 10)
@@ -211,13 +295,17 @@ func TestCheckSizeMismatch(t *testing.T) {
files := map[string][]byte{
testFileName: []byte("original content"),
}
createTestManifest(t, fs, "/manifest.mf", files)
createTestManifest(t, fs, testManifestPath, files)
// Create file with different size
createFilesOnDisk(t, fs, map[string][]byte{
testFileName: []byte("short"),
})
chk, err := NewChecker("/manifest.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: testManifestPath,
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
results := make(chan Result, 10)
@@ -237,7 +325,7 @@ func TestCheckHashMismatch(t *testing.T) {
files := map[string][]byte{
testFileName: originalContent,
}
createTestManifest(t, fs, "/manifest.mf", files)
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")
@@ -245,7 +333,11 @@ func TestCheckHashMismatch(t *testing.T) {
testFileName: differentContent,
})
chk, err := NewChecker("/manifest.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: testManifestPath,
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
results := make(chan Result, 10)
@@ -265,10 +357,14 @@ func TestCheckWithProgress(t *testing.T) {
testFile1: bytes.Repeat([]byte("a"), 100),
testFile2: bytes.Repeat([]byte("b"), 200),
}
createTestManifest(t, fs, "/manifest.mf", files)
createTestManifest(t, fs, testManifestPath, files)
createFilesOnDisk(t, fs, files)
chk, err := NewChecker("/manifest.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: testManifestPath,
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
results := make(chan Result, 10)
@@ -305,10 +401,14 @@ func TestCheckContextCancellation(t *testing.T) {
files[string(rune('a'+i%26))+".txt"] = bytes.Repeat([]byte("x"), 1000)
}
createTestManifest(t, fs, "/manifest.mf", files)
createTestManifest(t, fs, testManifestPath, files)
createFilesOnDisk(t, fs, files)
chk, err := NewChecker("/manifest.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: testManifestPath,
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
@@ -327,7 +427,7 @@ func TestFindExtraFiles(t *testing.T) {
manifestFiles := map[string][]byte{
testFile1: []byte("in manifest"),
}
createTestManifest(t, fs, "/manifest.mf", manifestFiles)
createTestManifest(t, fs, testManifestPath, manifestFiles)
// Disk has file1 and file2
createFilesOnDisk(t, fs, map[string][]byte{
@@ -335,7 +435,11 @@ func TestFindExtraFiles(t *testing.T) {
testFile2: []byte("extra file"),
})
chk, err := NewChecker("/manifest.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: testManifestPath,
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
results := make(chan Result, 10)
@@ -368,10 +472,14 @@ func TestFindExtraFilesSkipsManifestAndDotfiles(t *testing.T) {
require.NoError(t, afero.WriteFile(fs, "/data/.hidden", []byte("hidden"), 0o644))
require.NoError(t, afero.WriteFile(fs, "/data/.config/settings", []byte("cfg"), 0o644))
// Create a real extra file
require.NoError(t, fs.MkdirAll("/data", 0o755))
require.NoError(t, fs.MkdirAll(testDataDir, 0o755))
require.NoError(t, afero.WriteFile(fs, "/data/extra.txt", []byte("extra"), 0o644))
chk, err := NewChecker("/data/.index.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: "/data/.index.mf",
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
results := make(chan Result, 10)
@@ -400,10 +508,14 @@ func TestFindExtraFilesContextCancellation(t *testing.T) {
fs := afero.NewMemMapFs()
files := map[string][]byte{testFileName: []byte("data")}
createTestManifest(t, fs, "/manifest.mf", files)
createTestManifest(t, fs, testManifestPath, files)
createFilesOnDisk(t, fs, files)
chk, err := NewChecker("/manifest.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: testManifestPath,
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
@@ -419,10 +531,14 @@ func TestCheckNilChannels(t *testing.T) {
fs := afero.NewMemMapFs()
files := map[string][]byte{testFileName: []byte("data")}
createTestManifest(t, fs, "/manifest.mf", files)
createTestManifest(t, fs, testManifestPath, files)
createFilesOnDisk(t, fs, files)
chk, err := NewChecker("/manifest.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: testManifestPath,
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
// Should not panic with nil channels
@@ -435,10 +551,14 @@ func TestFindExtraFilesNilChannel(t *testing.T) {
fs := afero.NewMemMapFs()
files := map[string][]byte{testFileName: []byte("data")}
createTestManifest(t, fs, "/manifest.mf", files)
createTestManifest(t, fs, testManifestPath, files)
createFilesOnDisk(t, fs, files)
chk, err := NewChecker("/manifest.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: testManifestPath,
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
// Should not panic with nil channel
@@ -455,7 +575,7 @@ func TestCheckSubdirectories(t *testing.T) {
"dir1/dir2/file2.txt": []byte("content2"),
"dir1/dir2/dir3/deep.txt": []byte("deep content"),
}
createTestManifest(t, fs, "/manifest.mf", files)
createTestManifest(t, fs, testManifestPath, files)
// Create files with full directory structure
for path, content := range files {
@@ -465,7 +585,11 @@ func TestCheckSubdirectories(t *testing.T) {
require.NoError(t, afero.WriteFile(fs, fullPath, content, 0o644))
}
chk, err := NewChecker("/manifest.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: testManifestPath,
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
results := make(chan Result, 10)
@@ -493,13 +617,17 @@ func TestCheckMissingFileDetectedWithoutFallback(t *testing.T) {
testExistsFile: []byte("here"),
"missing.txt": []byte("not on disk"),
}
createTestManifest(t, fs, "/manifest.mf", files)
createTestManifest(t, fs, testManifestPath, files)
// Only create one file on disk
createFilesOnDisk(t, fs, map[string][]byte{
testExistsFile: []byte("here"),
})
chk, err := NewChecker("/manifest.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: testManifestPath,
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
results := make(chan Result, 10)
@@ -537,7 +665,11 @@ func TestFindExtraFilesSkipsDotfiles(t *testing.T) {
require.NoError(t,
afero.WriteFile(fs, "/data/.git/config", []byte("git config"), 0o644))
chk, err := NewChecker("/data/.index.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: "/data/.index.mf",
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
results := make(chan Result, 10)
@@ -566,7 +698,11 @@ func TestFindExtraFilesSkipsManifestFile(t *testing.T) {
createTestManifest(t, fs, "/data/index.mf", files)
createFilesOnDisk(t, fs, files)
chk, err := NewChecker("/data/index.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: "/data/index.mf",
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
results := make(chan Result, 10)
@@ -587,9 +723,13 @@ func TestCheckEmptyManifest(t *testing.T) {
fs := afero.NewMemMapFs()
// Create manifest with no files
createTestManifest(t, fs, "/manifest.mf", map[string][]byte{})
createTestManifest(t, fs, testManifestPath, map[string][]byte{})
chk, err := NewChecker("/manifest.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: testManifestPath,
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
assert.Equal(t, FileCount(0), chk.FileCount())
@@ -621,10 +761,14 @@ func TestCheckProgressRateLimited(t *testing.T) {
files[name] = []byte("content")
}
createTestManifest(t, fs, "/manifest.mf", files)
createTestManifest(t, fs, testManifestPath, files)
createFilesOnDisk(t, fs, files)
chk, err := NewChecker("/manifest.mf", "/data", fs)
chk, err := NewChecker(&CheckerOptions{
ManifestPath: testManifestPath,
BasePath: testDataDir,
Fs: fs,
})
require.NoError(t, err)
results := make(chan Result, 200)
+17 -4
View File
@@ -352,16 +352,29 @@ func NewManifestFromReader(input io.Reader) (*manifest, error) {
return m, nil
}
// NewManifestFromFile reads a manifest from a file path using the given filesystem.
// If fs is nil, the real filesystem (OsFs) is used.
// ManifestFromFileOptions configures NewManifestFromFile.
type ManifestFromFileOptions struct {
// Path is the manifest file to read (required).
Path string
// Fs is the filesystem to use, defaults to OsFs if nil.
Fs afero.Fs
}
// NewManifestFromFile reads a manifest from a file. It returns an error if
// opts is nil or its path is empty.
//
//nolint:revive // unexported-return: exporting manifest is owner question 13
func NewManifestFromFile(fs afero.Fs, path string) (*manifest, error) {
func NewManifestFromFile(opts *ManifestFromFileOptions) (*manifest, error) {
if opts == nil || opts.Path == "" {
return nil, errManifestPathEmpty
}
fs := opts.Fs
if fs == nil {
fs = afero.NewOsFs()
}
f, err := fs.Open(path)
f, err := fs.Open(opts.Path)
if err != nil {
return nil, err
}
+1
View File
@@ -0,0 +1 @@
103901c42b94396aa7ae128fd503ef693a4b7a03b2169481f25fda3d2c254e00 mf.proto
+29
View File
@@ -0,0 +1,29 @@
package mfer_test
import (
"crypto/sha256"
"encoding/hex"
"os"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
// mf.pb.go is generated from mf.proto and committed. `make generate`
// records the hash of the mf.proto it generated from in mf.proto.sha256.
func TestGeneratedCodeMatchesProto(t *testing.T) {
t.Parallel()
proto, err := os.ReadFile("mf.proto")
require.NoError(t, err)
recorded, err := os.ReadFile("mf.proto.sha256")
require.NoError(t, err)
recordedHash, _, _ := strings.Cut(string(recorded), " ")
sum := sha256.Sum256(proto)
require.Equal(t, recordedHash, hex.EncodeToString(sum[:]),
"mfer/mf.proto has changed since mfer/mf.pb.go was generated "+
"from it: run `make generate` and commit the result")
}
-11
View File
@@ -5,19 +5,8 @@ set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
# Regenerate mfer/mf.pb.go from mfer/mf.proto if it is missing or stale
# (mirrors the old Makefile prerequisite; the generated file is
# committed, so this is normally a no-op).
ensure_pb() {
if [ ! -f mfer/mf.pb.go ] ||
[ -n "$(find mfer/mf.proto -newer mfer/mf.pb.go 2>/dev/null)" ]; then
(cd mfer && go generate .)
fi
}
main() {
cd "$ROOT"
ensure_pb
gofumpt -l -w mfer internal cmd
# Markdown and JSON, over the same file set script/fmt-check verifies.
"$SCRIPT_DIR/prettier" --write
-11
View File
@@ -6,19 +6,8 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Regenerate mfer/mf.pb.go from mfer/mf.proto if it is missing or stale
# (mirrors the old Makefile prerequisite; the generated file is
# committed, so this is normally a no-op).
ensure_pb() {
if [ ! -f mfer/mf.pb.go ] ||
[ -n "$(find mfer/mf.proto -newer mfer/mf.pb.go 2>/dev/null)" ]; then
(cd mfer && go generate .)
fi
}
main() {
cd "$ROOT"
ensure_pb
if [ -n "$(gofmt -l .)" ]; then
echo "gofmt: files need formatting:" >&2
gofmt -l . >&2
+52
View File
@@ -0,0 +1,52 @@
#!/bin/sh
# script/generate: regenerate mfer/mf.pb.go from mfer/mf.proto, and record
# the hash of that mf.proto in mfer/mf.proto.sha256. Nothing else
# regenerates mf.pb.go: it is committed, so building and checking need no
# protoc. A test fails while mf.proto no longer matches the recorded hash.
#
# Needs exactly the protoc and protoc-gen-go versions named in the header of
# the committed mf.pb.go (README.md says how to install them). Another
# version writes a different mf.pb.go, so the script refuses to run.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# protoc 33.4 names itself v6.33.4 in the mf.pb.go header.
PROTOC_VERSION="33.4"
PROTOC_GEN_GO_VERSION="v1.36.11"
# require_version <command> <its exact --version output>
require_version() {
actual="$("$1" --version 2>/dev/null || true)"
if [ "$actual" != "$2" ]; then
echo "generate: needs $2 on PATH, found: ${actual:-none}" >&2
echo " README.md says how to install it." >&2
exit 1
fi
}
# sha256 <file>: print "<hash> <file>", with sha256sum, or with shasum
# where there is no sha256sum.
sha256() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1"
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$1"
else
echo "generate: needs sha256sum or shasum on PATH" >&2
exit 1
fi
}
main() {
cd "$ROOT/mfer"
require_version protoc "libprotoc $PROTOC_VERSION"
require_version protoc-gen-go "protoc-gen-go $PROTOC_GEN_GO_VERSION"
# Hashed before regenerating, so a missing hash tool stops the script
# before it changes anything. Regenerating leaves mf.proto as it is.
proto_hash="$(sha256 mf.proto)"
go generate .
echo "$proto_hash" >mf.proto.sha256
}
main "$@"
-11
View File
@@ -4,19 +4,8 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Regenerate mfer/mf.pb.go from mfer/mf.proto if it is missing or stale
# (mirrors the old Makefile prerequisite; the generated file is
# committed, so this is normally a no-op).
ensure_pb() {
if [ ! -f mfer/mf.pb.go ] ||
[ -n "$(find mfer/mf.proto -newer mfer/mf.pb.go 2>/dev/null)" ]; then
(cd mfer && go generate .)
fi
}
main() {
cd "$ROOT"
ensure_pb
go test -timeout 30s -race -cover ./... ||
{
echo "--- Rerunning with -v for details ---"