diff --git a/README.md b/README.md index 9805faa..5995057 100644 --- a/README.md +++ b/README.md @@ -304,6 +304,9 @@ are now tracked only in the [issues](https://git.eeqj.de/sneak/mfer/issues). `--no-extra-files` each one is a failure instead. A named pipe, socket or device, a symlink to a directory and a broken symlink are never reported, since gen cannot list them + - never reports `index.mf` at the top of the base directory as a file the + manifest does not list, even when given another manifest or a URL, since + gen and fetch write the manifest there - `mfer freshen` / `mfer freshen .` - rewrites `index.mf` to list the files now under the directory holding it, or under the one `--base` names, hashing only the files that are new or diff --git a/internal/cli/check.go b/internal/cli/check.go index c848307..9e6aeab 100644 --- a/internal/cli/check.go +++ b/internal/cli/check.go @@ -195,7 +195,10 @@ func countCheckFailures( // findExtraFiles reports files present on disk but absent from the // manifest, and anything the search cannot read: each is a failure, logged -// at error level, under --no-extra-files, otherwise a warning. +// at error level, under --no-extra-files, otherwise a warning. index.mf at +// the top of the base directory is never reported as absent from the +// manifest, since gen and fetch write the manifest there, even when check +// is given another manifest or a URL. func findExtraFiles( ctx context.Context, cmd *cli.Command, chk *mfer.Checker, failures *int64, ) error { @@ -204,6 +207,10 @@ func findExtraFiles( go func() { for result := range extraResults { + if result.Status == mfer.StatusExtra && result.Path == defaultManifestName { + continue + } + if cmd.Bool("no-extra-files") { *failures++ diff --git a/internal/cli/entry_test.go b/internal/cli/entry_test.go index 768087e..6ae6933 100644 --- a/internal/cli/entry_test.go +++ b/internal/cli/entry_test.go @@ -1190,6 +1190,91 @@ func TestCheckURLManifestWithoutTempDirectory(t *testing.T) { assert.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts)) } +// TestCheckNeverReportsIndexMfAtTopOfBase checks a tree holding index.mf at +// its top, where gen and fetch write the manifest, against a manifest that +// does not list it, given by URL and by a path outside the tree: the check +// passes under --no-extra-files. index.mf in a subdirectory, and the temp +// file fetch saves index.mf through, are still reported. +func TestCheckNeverReportsIndexMfAtTopOfBase(t *testing.T) { + t.Parallel() + + files := map[string][]byte{testFileTxt: []byte("hello")} + manifest := manifestOf(t, files) + + server := httptest.NewServer(fetchTestHandler(manifest, files)) + t.Cleanup(server.Close) + + manifestOutsideTree := filepath.Join(t.TempDir(), "other.mf") + require.NoError(t, os.WriteFile(manifestOutsideTree, manifest, 0o600)) + + for name, manifestArg := range map[string]string{ + "manifest by URL": server.URL + "/" + defaultManifestName, + "manifest by path": manifestOutsideTree, + } { + t.Run(name, func(t *testing.T) { + t.Parallel() + + base := t.TempDir() + require.NoError(t, os.WriteFile( + filepath.Join(base, testFileTxt), files[testFileTxt], 0o600)) + require.NoError(t, os.WriteFile( + filepath.Join(base, defaultManifestName), manifest, 0o600)) + + check := func() (int, string) { + opts := testOpts([]string{ + testApp, cmdCheck, testFlagNoExtra, testFlagBase, base, manifestArg, + }, afero.NewOsFs()) + + return runCLI(opts), testStderr(t, opts) + } + + exitCode, stderr := check() + assert.Equal(t, 0, exitCode, "stderr: %s", stderr) + + reported := []string{ + testSubdir + "/" + defaultManifestName, + tempPathFor(defaultManifestName), + } + + require.NoError(t, os.Mkdir(filepath.Join(base, testSubdir), 0o750)) + + for _, path := range reported { + require.NoError(t, + os.WriteFile(filepath.Join(base, path), manifest, 0o600)) + } + + exitCode, stderr = check() + assert.Equal(t, 1, exitCode, "stderr: %s", stderr) + + for _, path := range reported { + assert.Contains(t, stderr, "EXTRA: "+path) + } + }) + } +} + +// TestCheckChecksListedIndexMf checks a tree against a manifest that lists +// index.mf at the top of the tree, where that file has changed since: check +// reports it like any other changed file. +func TestCheckChecksListedIndexMf(t *testing.T) { + t.Parallel() + + manifestPath := filepath.Join(t.TempDir(), "other.mf") + require.NoError(t, os.WriteFile(manifestPath, manifestOf(t, map[string][]byte{ + defaultManifestName: []byte("listed"), + }), 0o600)) + + base := t.TempDir() + require.NoError(t, os.WriteFile( + filepath.Join(base, defaultManifestName), []byte("edited"), 0o600)) + + opts := testOpts([]string{ + testApp, cmdCheck, testFlagBase, base, manifestPath, + }, afero.NewOsFs()) + assert.Equal(t, 1, runCLI(opts), "stderr: %s", testStderr(t, opts)) + assert.Contains(t, testStderr(t, opts), "HASH_MISMATCH: "+defaultManifestName) +} + // unlistableDirFs is a filesystem on which one directory cannot be listed. type unlistableDirFs struct { afero.Fs