Files
mfer/internal/cli/pipe_test.go
T
clawbot e633d3a31b
check / check (push) Waiting to run
Read only regular files in gen, freshen and check (closes #189)
Opening a named pipe that has no writer blocks, so gen and freshen hung
on a tree holding one, and check hung on a listed path that is a pipe on
disk. gen and freshen now list regular files only (with
--follow-symlinks, what a symlink points to must be one) and name what
they skip with -v. check reports a listed path that is not a regular
file as an error without opening it, and its search for extra files
leaves out pipes, sockets and devices.

Judgement call: the pipe tests build on unix only, where syscall.Mkfifo
exists.
Unverified: sockets and devices are not tested; the tests use pipes.

Model: opus-5-5
2026-10-13 16:39:09 +02: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", "--follow-symlinks",
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")
}