Files
mfer/internal/cli/pipe_test.go
T
sneak b8c699adee
check / check (push) Waiting to run
Never list index.mf at the top of the tree in gen and freshen (closes #233)
fetch saves the manifest as index.mf at the top of the tree and refuses a
manifest that lists a file there. gen given --output, and freshen with its
manifest outside the base directory, listed such a file, so they wrote
manifests fetch would not take.

Both now leave out each file the manifest would list as index.mf: one at
the top of the tree, or a file given to gen by that name. The rule goes by
that path alone and looks up no file, so a symlink named index.mf never
hides the file it points to. index.mf in a subdirectory is still listed.
The scanner in package mfer gets an option to leave out files by the path
they would be listed by.

Model: opus-5-5
2026-10-15 22:22:04 +00:00

95 lines
3.0 KiB
Go

//go:build unix
//nolint:testpackage // white-box tests exercise unexported internals
package cli
import (
"os"
"path/filepath"
"syscall"
"testing"
"time"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
// testPipe is the name of the named pipe the pipe tests make in a tree.
testPipe = "pipe"
// pipeDeadline is how long a test lets a run go before it opens the
// test's named pipes for writing.
pipeDeadline = 5 * time.Second
)
// makePipe makes a named pipe at path. Opening a named pipe for reading
// blocks until it is opened for writing, so once pipeDeadline has passed,
// unless the test has ended, makePipe opens it for writing and closes it
// again. A run that wrongly reads the pipe then ends, and the test fails on
// what it reports instead of hanging. That lets one read through, so each
// path a wrong run would read gets a pipe of its own.
func makePipe(t *testing.T, path string) {
t.Helper()
require.NoError(t, syscall.Mkfifo(path, 0o600))
timer := time.AfterFunc(pipeDeadline, func() {
f, err := os.OpenFile(path, os.O_WRONLY, 0) //nolint:gosec // test-controlled path
if err == nil {
_ = f.Close()
}
})
t.Cleanup(func() { timer.Stop() })
}
// TestGenerateSkipsPipe runs gen -v on a tree holding a named pipe: the
// manifest lists the regular file only, and gen names the pipe it skipped.
func TestGenerateSkipsPipe(t *testing.T) {
t.Parallel()
dir := t.TempDir()
fs := afero.NewOsFs()
writeTestFile(t, fs, filepath.Join(dir, testFileTxt), "regular")
makePipe(t, filepath.Join(dir, testPipe))
opts := testOpts([]string{testApp, cmdGenerate, "-v", dir}, fs)
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
assert.Equal(t, []string{testFileTxt},
manifestPaths(t, fs, filepath.Join(dir, defaultManifestName)))
assert.Contains(t, testStderr(t, opts), "skipping pipe: not a regular file")
}
// TestFreshenSkipsPipes adds a named pipe to a tree after gen made its
// manifest, and a symlink to another pipe. freshen -v --follow-symlinks
// leaves the manifest listing the regular file only, and names both entries
// it skipped.
func TestFreshenSkipsPipes(t *testing.T) {
t.Parallel()
fs := afero.NewOsFs()
root, manifestPath := setupFreshenDir(t, fs,
map[string]string{testFileTxt: "regular"})
makePipe(t, filepath.Join(root, testPipe))
// The symlink points outside the tree, so no pipe is read twice.
target := filepath.Join(t.TempDir(), testPipe)
makePipe(t, target)
require.NoError(t, os.Symlink(target, filepath.Join(root, testLink)))
opts := testOpts([]string{
testApp, cmdFreshen, "-v", testFlagFollowSymlinks,
testFlagBase, root, manifestPath,
}, fs)
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
assert.Equal(t, []string{testFileTxt}, manifestPaths(t, fs, manifestPath))
stderr := testStderr(t, opts)
assert.Contains(t, stderr, "skipping pipe: not a regular file")
assert.Contains(t, stderr, "skipping link: not a regular file")
}