//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") }