1 Commits
Author SHA1 Message Date
sneak ea35293a1f Hold runMu in internal/cli tests that log outside a CLI run (closes #210)
check / check (push) Waiting to run
A CLI run under runMu points the process-global logger at its own
buffers, so a line another test logged outside runMu could land in that
run's output. TestCheckQuietShowsFailedFiles caught one: a "loaded
manifest" line in the stderr of a passing check -q run.

The test helpers that scan or load a manifest now hold runMu
themselves, and so do the remaining direct loads and scans, the
recordEntry call and the retrying download. Two dotfile tests use
manifestPaths instead of loading the manifest themselves.
TestRootVerbosityFlags ran on its own only to avoid those lines, so it
now runs in parallel.

Judgement call: a downloadFile call that gets its answer on the first
request does not log, so those calls are left as they are.

Model: opus-5-5
2026-10-13 21:12:43 +00:00
5 changed files with 87 additions and 33 deletions
+24 -21
View File
@@ -58,9 +58,10 @@ func runCLI(opts *RunOptions) int {
// runCLIWithLevel is runCLI that also returns the log level the run left // runCLIWithLevel is runCLI that also returns the log level the run left
// set, read while runMu still keeps other runs from changing it. Each run // set, read while runMu still keeps other runs from changing it. Each run
// starts at the default level, as a new process does. Before releasing the // starts at the default level, as a new process does. Before releasing the
// lock it points the process-global logger at io.Discard: other tests log // lock it points the process-global logger at io.Discard: test helpers also
// outside the lock (manifest loads, scans), and those lines must not land in // log under runMu outside any run (manifest loads, scans), and those lines
// this run's buffers once it has returned and its test is reading them. // must not land in this run's buffers once it has returned and its test is
// reading them.
func runCLIWithLevel(opts *RunOptions) (int, log.Level) { func runCLIWithLevel(opts *RunOptions) (int, log.Level) {
runMu.Lock() runMu.Lock()
defer runMu.Unlock() defer runMu.Unlock()
@@ -206,13 +207,13 @@ func TestVersionFlagAndCommandMatch(t *testing.T) {
// TestRootVerbosityFlags asserts that -q and -v given before any subcommand // TestRootVerbosityFlags asserts that -q and -v given before any subcommand
// name take effect: in the root itself, and in the fetch and export // name take effect: in the root itself, and in the fetch and export
// subcommands (issue #64). It does not run in parallel: other tests log // subcommands (issue #64).
// outside runMu (manifest loads, scans), and a line logged while one of these
// runs is in progress lands in its output.
//
//nolint:paralleltest // see above
func TestRootVerbosityFlags(t *testing.T) { func TestRootVerbosityFlags(t *testing.T) {
t.Parallel()
t.Run("quiet with no subcommand hides the banner", func(t *testing.T) { t.Run("quiet with no subcommand hides the banner", func(t *testing.T) {
t.Parallel()
loud := testOpts([]string{testApp}, afero.NewMemMapFs()) loud := testOpts([]string{testApp}, afero.NewMemMapFs())
require.Equal(t, 0, runCLI(loud)) require.Equal(t, 0, runCLI(loud))
assert.Contains(t, testStdout(t, loud), banner) assert.Contains(t, testStdout(t, loud), banner)
@@ -224,6 +225,8 @@ func TestRootVerbosityFlags(t *testing.T) {
}) })
t.Run("quiet before fetch hides the banner", func(t *testing.T) { t.Run("quiet before fetch hides the banner", func(t *testing.T) {
t.Parallel()
opts := testOpts([]string{testApp, "-q", cmdFetch}, afero.NewMemMapFs()) opts := testOpts([]string{testApp, "-q", cmdFetch}, afero.NewMemMapFs())
assert.Equal(t, 1, runCLI(opts)) assert.Equal(t, 1, runCLI(opts))
@@ -232,6 +235,8 @@ func TestRootVerbosityFlags(t *testing.T) {
}) })
t.Run("verbose twice before fetch enables debug logging", func(t *testing.T) { t.Run("verbose twice before fetch enables debug logging", func(t *testing.T) {
t.Parallel()
opts := testOpts([]string{testApp, "-v", "-v", cmdFetch}, afero.NewMemMapFs()) opts := testOpts([]string{testApp, "-v", "-v", cmdFetch}, afero.NewMemMapFs())
assert.Equal(t, 1, runCLI(opts)) assert.Equal(t, 1, runCLI(opts))
@@ -239,6 +244,8 @@ func TestRootVerbosityFlags(t *testing.T) {
}) })
t.Run("quiet before export hides the manifest summary", func(t *testing.T) { t.Run("quiet before export hides the manifest summary", func(t *testing.T) {
t.Parallel()
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
manifest := buildTestManifest(t, map[string][]byte{"a.txt": []byte("a")}) manifest := buildTestManifest(t, map[string][]byte{"a.txt": []byte("a")})
require.NoError(t, afero.WriteFile(fs, testManifest, manifest, 0o644)) require.NoError(t, afero.WriteFile(fs, testManifest, manifest, 0o644))
@@ -771,13 +778,9 @@ func TestGenerateExcludesDotfilesByDefault(t *testing.T) {
assert.True(t, exists) assert.True(t, exists)
// Verify manifest only has 1 file (the non-dotfile) // Verify manifest only has 1 file (the non-dotfile)
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{ paths := manifestPaths(t, fs, testMF)
Path: testMF, require.Len(t, paths, 1)
Fs: fs, assert.Equal(t, "file1.txt", paths[0])
})
require.NoError(t, err)
assert.Len(t, manifest.Files(), 1)
assert.Equal(t, "file1.txt", manifest.Files()[0].GetPath())
} }
func TestGenerateWithIncludeDotfiles(t *testing.T) { func TestGenerateWithIncludeDotfiles(t *testing.T) {
@@ -798,12 +801,7 @@ func TestGenerateWithIncludeDotfiles(t *testing.T) {
require.Equal(t, 0, exitCode) require.Equal(t, 0, exitCode)
// Verify manifest has 2 files (including dotfile) // Verify manifest has 2 files (including dotfile)
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{ assert.Len(t, manifestPaths(t, fs, testMF), 2)
Path: testMF,
Fs: fs,
})
require.NoError(t, err)
assert.Len(t, manifest.Files(), 2)
} }
func TestMultipleInputPaths(t *testing.T) { func TestMultipleInputPaths(t *testing.T) {
@@ -1224,6 +1222,11 @@ func TestGenerateFailsWithoutForceWhenOutputExists(t *testing.T) {
func manifestPaths(t *testing.T, fs afero.Fs, path string) []string { func manifestPaths(t *testing.T, fs afero.Fs, path string) []string {
t.Helper() t.Helper()
// Loading a manifest logs to the process-global logger, so serialize
// with the CLI runs.
runMu.Lock()
defer runMu.Unlock()
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{ manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{
Path: path, Path: path,
Fs: fs, Fs: fs,
+10
View File
@@ -54,6 +54,11 @@ func runLocked(fn func() error) error {
func unsignedChecker(t *testing.T) *mfer.Checker { func unsignedChecker(t *testing.T) *mfer.Checker {
t.Helper() t.Helper()
// The scan and the manifest load log to the process-global logger, so
// serialize with the CLI runs.
runMu.Lock()
defer runMu.Unlock()
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
require.NoError(t, fs.MkdirAll("/d", 0o755)) require.NoError(t, fs.MkdirAll("/d", 0o755))
require.NoError(t, afero.WriteFile(fs, "/d/f.txt", []byte("hi"), 0o644)) require.NoError(t, afero.WriteFile(fs, "/d/f.txt", []byte("hi"), 0o644))
@@ -183,6 +188,11 @@ func signedManifest(t *testing.T, files map[string][]byte) []byte {
func signedChecker(t *testing.T, manifest []byte) *mfer.Checker { func signedChecker(t *testing.T, manifest []byte) *mfer.Checker {
t.Helper() t.Helper()
// Loading a manifest logs to the process-global logger, so serialize
// with the CLI runs.
runMu.Lock()
defer runMu.Unlock()
fs := afero.NewMemMapFs() fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "/index.mf", manifest, 0o644)) require.NoError(t, afero.WriteFile(fs, "/index.mf", manifest, 0o644))
+5
View File
@@ -20,6 +20,11 @@ const testCmdExport = "export"
func buildTestManifest(t *testing.T, files map[string][]byte) []byte { func buildTestManifest(t *testing.T, files map[string][]byte) []byte {
t.Helper() t.Helper()
// The scan logs to the process-global logger, so serialize with the
// CLI runs.
runMu.Lock()
defer runMu.Unlock()
sourceFs := afero.NewMemMapFs() sourceFs := afero.NewMemMapFs()
for path, content := range files { for path, content := range files {
require.NoError(t, sourceFs.MkdirAll("/", 0o755)) require.NoError(t, sourceFs.MkdirAll("/", 0o755))
+39 -12
View File
@@ -171,6 +171,11 @@ func TestResolveManifestURL(t *testing.T) {
func scanToManifest(t *testing.T, sourceFs afero.Fs) []byte { func scanToManifest(t *testing.T, sourceFs afero.Fs) []byte {
t.Helper() t.Helper()
// The scan logs to the process-global logger, so serialize with the
// CLI runs.
runMu.Lock()
defer runMu.Unlock()
s := mfer.NewScannerWithOptions(&mfer.ScannerOptions{Fs: sourceFs}) s := mfer.NewScannerWithOptions(&mfer.ScannerOptions{Fs: sourceFs})
require.NoError(t, s.EnumerateFS(sourceFs, "/", nil)) require.NoError(t, s.EnumerateFS(sourceFs, "/", nil))
@@ -314,8 +319,11 @@ func TestFetchFromHTTP(t *testing.T) {
// Change to a fresh destination directory for the test // Change to a fresh destination directory for the test
destDir := chdirTemp(t) destDir := chdirTemp(t)
// Parse the manifest to get file entries // Parse the manifest to get file entries. Loading a manifest logs to
// the process-global logger.
runMu.Lock()
manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestData)) manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestData))
runMu.Unlock()
require.NoError(t, err) require.NoError(t, err)
files := manifest.Files() files := manifest.Files()
@@ -365,7 +373,10 @@ func TestFetchHashMismatch(t *testing.T) {
// Generate and parse manifest // Generate and parse manifest
manifestData := scanToManifest(t, sourceFs) manifestData := scanToManifest(t, sourceFs)
// Loading a manifest logs to the process-global logger.
runMu.Lock()
manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestData)) manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestData))
runMu.Unlock()
require.NoError(t, err) require.NoError(t, err)
files := manifest.Files() files := manifest.Files()
@@ -411,7 +422,10 @@ func TestFetchSizeMismatch(t *testing.T) {
// Generate and parse manifest // Generate and parse manifest
manifestData := scanToManifest(t, sourceFs) manifestData := scanToManifest(t, sourceFs)
// Loading a manifest logs to the process-global logger.
runMu.Lock()
manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestData)) manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestData))
runMu.Unlock()
require.NoError(t, err) require.NoError(t, err)
files := manifest.Files() files := manifest.Files()
@@ -523,7 +537,10 @@ func TestFetchProgress(t *testing.T) {
// Generate and parse manifest // Generate and parse manifest
manifestData := scanToManifest(t, sourceFs) manifestData := scanToManifest(t, sourceFs)
// Loading a manifest logs to the process-global logger.
runMu.Lock()
manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestData)) manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestData))
runMu.Unlock()
require.NoError(t, err) require.NoError(t, err)
files := manifest.Files() files := manifest.Files()
@@ -897,9 +914,12 @@ func TestGetStopsWaitingWhenCanceled(t *testing.T) {
//nolint:paralleltest // changes the process-global working directory //nolint:paralleltest // changes the process-global working directory
func TestDownloadFileRetriesToSuccess(t *testing.T) { func TestDownloadFileRetriesToSuccess(t *testing.T) {
content := []byte("the whole file, every byte of it") content := []byte("the whole file, every byte of it")
manifestData := manifestOf(t, map[string][]byte{testFileTxt: content})
manifest, err := mfer.NewManifestFromReader(bytes.NewReader( // Loading a manifest logs to the process-global logger.
manifestOf(t, map[string][]byte{testFileTxt: content}))) runMu.Lock()
manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestData))
runMu.Unlock()
require.NoError(t, err) require.NoError(t, err)
var requests atomic.Int32 var requests atomic.Int32
@@ -922,8 +942,11 @@ func TestDownloadFileRetriesToSuccess(t *testing.T) {
chdirTemp(t) chdirTemp(t)
err = downloadFile(context.Background(), testClient(), // downloadFile logs each retry to the process-global logger.
server.URL+"/"+testFileTxt, ".", testFileTxt, manifest.Files()[0], nil) err = runLocked(func() error {
return downloadFile(context.Background(), testClient(),
server.URL+"/"+testFileTxt, ".", testFileTxt, manifest.Files()[0], nil)
})
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, int32(3), requests.Load()) assert.Equal(t, int32(3), requests.Load())
@@ -1404,14 +1427,18 @@ func TestFetchSetsRecordedMode(t *testing.T) {
require.NoError(t, afero.WriteFile(sourceFs, "/"+p, content, modes[p])) require.NoError(t, afero.WriteFile(sourceFs, "/"+p, content, modes[p]))
} }
scanner := mfer.NewScannerWithOptions(&mfer.ScannerOptions{
Fs: sourceFs,
IncludePermissions: true,
})
require.NoError(t, scanner.EnumerateFS(sourceFs, "/", nil))
var manifest bytes.Buffer var manifest bytes.Buffer
require.NoError(t, scanner.ToManifest(context.Background(), &manifest, nil))
// The scan logs to the process-global logger.
require.NoError(t, runLocked(func() error {
scanner := mfer.NewScannerWithOptions(&mfer.ScannerOptions{
Fs: sourceFs,
IncludePermissions: true,
})
require.NoError(t, scanner.EnumerateFS(sourceFs, "/", nil))
return scanner.ToManifest(context.Background(), &manifest, nil)
}))
tree := fetchTestHandler(manifest.Bytes(), files) tree := fetchTestHandler(manifest.Bytes(), files)
+9
View File
@@ -70,6 +70,11 @@ func runFreshen(t *testing.T, fs afero.Fs, root, manifestPath string) {
func manifestFiles(t *testing.T, fs afero.Fs, path string) []*mfer.MFFilePath { func manifestFiles(t *testing.T, fs afero.Fs, path string) []*mfer.MFFilePath {
t.Helper() t.Helper()
// Loading a manifest logs to the process-global logger, so serialize
// with the CLI runs.
runMu.Lock()
defer runMu.Unlock()
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{ manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{
Path: path, Path: path,
Fs: fs, Fs: fs,
@@ -436,7 +441,11 @@ func TestFreshenRecordEntryMtimePresence(t *testing.T) {
s := &freshenScanner{ s := &freshenScanner{
existingByPath: map[string]*mfer.MFFilePath{relPath: tc.entry}, existingByPath: map[string]*mfer.MFFilePath{relPath: tc.entry},
} }
// recordEntry logs to the process-global logger.
runMu.Lock()
s.recordEntry(relPath, info) s.recordEntry(relPath, info)
runMu.Unlock()
require.Len(t, s.entries, 1) require.Len(t, s.entries, 1)
assert.Equal(t, tc.needsHash, s.entries[0].needsHash) assert.Equal(t, tc.needsHash, s.entries[0].needsHash)