Never report index.mf at the top of the base as unlisted in check (closes #229)
check / check (push) Waiting to run
check / check (push) Waiting to run
gen and fetch write the manifest as index.mf at the top of the tree. check left out only the manifest file it was given, so given a URL, or a manifest stored elsewhere, it reported that index.mf as not in the manifest, and --no-extra-files failed a tree fetch had just verified. check now drops that one report for index.mf at the top of the base directory. A manifest that lists index.mf still has it checked, and index.mf in a subdirectory or fetch's leftover .index.mf.tmp are still reported. The change is in package cli, which owns the default manifest name; the Checker in package mfer is unchanged. Model: opus-5-5
This commit is contained in:
@@ -301,6 +301,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
|
`--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,
|
device, a symlink to a directory and a broken symlink are never reported,
|
||||||
since gen cannot list them
|
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 .`
|
- `mfer freshen` / `mfer freshen .`
|
||||||
- rewrites `index.mf` to list the files now under the directory holding it,
|
- 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
|
or under the one `--base` names, hashing only the files that are new or
|
||||||
|
|||||||
@@ -195,7 +195,10 @@ func countCheckFailures(
|
|||||||
|
|
||||||
// findExtraFiles reports files present on disk but absent from the
|
// findExtraFiles reports files present on disk but absent from the
|
||||||
// manifest, and anything the search cannot read: each is a failure, logged
|
// 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, even when check is given another manifest or a URL, since gen
|
||||||
|
// and fetch write the manifest there.
|
||||||
func findExtraFiles(
|
func findExtraFiles(
|
||||||
ctx context.Context, cmd *cli.Command, chk *mfer.Checker, failures *int64,
|
ctx context.Context, cmd *cli.Command, chk *mfer.Checker, failures *int64,
|
||||||
) error {
|
) error {
|
||||||
@@ -204,6 +207,10 @@ func findExtraFiles(
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for result := range extraResults {
|
for result := range extraResults {
|
||||||
|
if result.Status == mfer.StatusExtra && result.Path == defaultManifestName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if cmd.Bool("no-extra-files") {
|
if cmd.Bool("no-extra-files") {
|
||||||
*failures++
|
*failures++
|
||||||
|
|
||||||
|
|||||||
@@ -1190,6 +1190,99 @@ func TestCheckURLManifestWithoutTempDirectory(t *testing.T) {
|
|||||||
assert.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
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: check never
|
||||||
|
// reports it, and passes with and without --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(flags ...string) (int, string) {
|
||||||
|
opts := testOpts(slices.Concat(
|
||||||
|
[]string{testApp, cmdCheck}, flags,
|
||||||
|
[]string{testFlagBase, base, manifestArg},
|
||||||
|
), afero.NewOsFs())
|
||||||
|
|
||||||
|
return runCLI(opts), testStderr(t, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
exitCode, stderr := check()
|
||||||
|
assert.Equal(t, 0, exitCode, "stderr: %s", stderr)
|
||||||
|
assert.NotContains(t, stderr, "EXTRA: "+defaultManifestName)
|
||||||
|
|
||||||
|
exitCode, stderr = check(testFlagNoExtra)
|
||||||
|
assert.Equal(t, 0, exitCode, "stderr: %s", stderr)
|
||||||
|
assert.NotContains(t, stderr, "EXTRA: "+defaultManifestName)
|
||||||
|
|
||||||
|
reported := []string{
|
||||||
|
testSubdirManifest,
|
||||||
|
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(testFlagNoExtra)
|
||||||
|
assert.Equal(t, 1, exitCode, "stderr: %s", stderr)
|
||||||
|
assert.NotContains(t, stderr, "EXTRA: "+defaultManifestName)
|
||||||
|
|
||||||
|
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.
|
// unlistableDirFs is a filesystem on which one directory cannot be listed.
|
||||||
type unlistableDirFs struct {
|
type unlistableDirFs struct {
|
||||||
afero.Fs
|
afero.Fs
|
||||||
@@ -1238,6 +1331,60 @@ func TestCheckWithUnlistableDirectory(t *testing.T) {
|
|||||||
assert.Contains(t, testStderr(t, opts), "unlisted.txt")
|
assert.Contains(t, testStderr(t, opts), "unlisted.txt")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCheckReportsUnreadableIndexMFAtTopOfBase has a directory named
|
||||||
|
// index.mf at the top of the base that cannot be listed. check leaves out
|
||||||
|
// only the report that index.mf is not in the manifest, so it reports the
|
||||||
|
// error and fails under --no-extra-files.
|
||||||
|
func TestCheckReportsUnreadableIndexMFAtTopOfBase(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
unreadable := filepath.Join(testDir, defaultManifestName)
|
||||||
|
|
||||||
|
mem := afero.NewMemMapFs()
|
||||||
|
require.NoError(t, mem.MkdirAll(unreadable, 0o755))
|
||||||
|
writeTestFile(t, mem, testFile1, "hello")
|
||||||
|
|
||||||
|
opts := testOpts([]string{
|
||||||
|
testApp, cmdGenerate, "-q", "-o", testManifest, testDir,
|
||||||
|
}, mem)
|
||||||
|
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
||||||
|
|
||||||
|
fs := unlistableDirFs{Fs: mem, dir: unreadable}
|
||||||
|
|
||||||
|
opts = testOpts([]string{
|
||||||
|
testApp, cmdCheck, testFlagNoExtra, testFlagBase, testDir, testManifest,
|
||||||
|
}, fs)
|
||||||
|
assert.Equal(t, 1, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
||||||
|
assert.Contains(t, testStderr(t, opts), "ERROR: "+defaultManifestName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCheckReportsUpperCaseIndexMFAtTopOfBase checks a tree holding
|
||||||
|
// INDEX.MF at its top against a manifest that does not list it. Only the
|
||||||
|
// name index.mf, in exactly that letter case, is left out, so check reports
|
||||||
|
// INDEX.MF. The tree is in memory, where the two names are two files
|
||||||
|
// whatever the letter case rules of the host's filesystem.
|
||||||
|
func TestCheckReportsUpperCaseIndexMFAtTopOfBase(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
upperCase := strings.ToUpper(defaultManifestName)
|
||||||
|
|
||||||
|
fs := afero.NewMemMapFs()
|
||||||
|
writeTestFile(t, fs, testFile1, "hello")
|
||||||
|
|
||||||
|
opts := testOpts([]string{
|
||||||
|
testApp, cmdGenerate, "-q", "-o", testManifest, testDir,
|
||||||
|
}, fs)
|
||||||
|
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
||||||
|
|
||||||
|
writeTestFile(t, fs, filepath.Join(testDir, upperCase), "not listed")
|
||||||
|
|
||||||
|
opts = testOpts([]string{
|
||||||
|
testApp, cmdCheck, testFlagNoExtra, testFlagBase, testDir, testManifest,
|
||||||
|
}, fs)
|
||||||
|
assert.Equal(t, 1, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
||||||
|
assert.Contains(t, testStderr(t, opts), "EXTRA: "+upperCase)
|
||||||
|
}
|
||||||
|
|
||||||
func TestGenerateAtomicWriteNoTempFileOnSuccess(t *testing.T) {
|
func TestGenerateAtomicWriteNoTempFileOnSuccess(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user