| //go:build !windows |
| |
| /* |
| Copyright The containerd Authors. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package cio |
| |
| import ( |
| "bytes" |
| "context" |
| "io" |
| "os" |
| "path/filepath" |
| "strings" |
| "syscall" |
| "testing" |
| |
| "github.com/containerd/fifo" |
| "github.com/stretchr/testify/assert" |
| ) |
| |
| func TestOpenFifos(t *testing.T) { |
| scenarios := []*FIFOSet{ |
| { |
| Config: Config{ |
| Stdin: "", |
| Stdout: filepath.Join("This/does/not/exist", "test-stdout"), |
| Stderr: filepath.Join("This/does/not/exist", "test-stderr"), |
| }, |
| }, |
| { |
| Config: Config{ |
| Stdin: filepath.Join("This/does/not/exist", "test-stdin"), |
| Stdout: "", |
| Stderr: filepath.Join("This/does/not/exist", "test-stderr"), |
| }, |
| }, |
| { |
| Config: Config{ |
| Stdin: "", |
| Stdout: "", |
| Stderr: filepath.Join("This/does/not/exist", "test-stderr"), |
| }, |
| }, |
| } |
| for _, scenario := range scenarios { |
| _, err := openFifos(context.Background(), scenario) |
| assert.Error(t, err, scenario) |
| } |
| } |
| |
| // TestOpenFifosWithTerminal tests openFifos should not open stderr if terminal |
| // is set. |
| func TestOpenFifosWithTerminal(t *testing.T) { |
| var ctx, cancel = context.WithCancel(context.Background()) |
| defer cancel() |
| |
| ioFifoDir := t.TempDir() |
| |
| cfg := Config{ |
| Stdout: filepath.Join(ioFifoDir, "test-stdout"), |
| Stderr: filepath.Join(ioFifoDir, "test-stderr"), |
| } |
| |
| // Without terminal, pipes.Stderr should not be nil |
| { |
| p, err := openFifos(ctx, NewFIFOSet(cfg, nil)) |
| if err != nil { |
| t.Fatalf("unexpected error during openFifos: %v", err) |
| } |
| |
| if p.Stderr == nil { |
| t.Fatalf("unexpected empty stderr pipe") |
| } |
| } |
| |
| // With terminal, pipes.Stderr should be nil |
| { |
| cfg.Terminal = true |
| p, err := openFifos(ctx, NewFIFOSet(cfg, nil)) |
| if err != nil { |
| t.Fatalf("unexpected error during openFifos: %v", err) |
| } |
| |
| if p.Stderr != nil { |
| t.Fatalf("unexpected stderr pipe") |
| } |
| } |
| } |
| |
| func assertHasPrefix(t *testing.T, s, prefix string) { |
| t.Helper() |
| if !strings.HasPrefix(s, prefix) { |
| t.Fatalf("expected %s to start with %s", s, prefix) |
| } |
| } |
| |
| func TestNewFIFOSetInDir(t *testing.T) { |
| root := t.TempDir() |
| |
| fifos, err := NewFIFOSetInDir(root, "theid", true) |
| assert.NoError(t, err) |
| |
| dir := filepath.Dir(fifos.Stdin) |
| assertHasPrefix(t, dir, root) |
| expected := &FIFOSet{ |
| Config: Config{ |
| Stdin: filepath.Join(dir, "theid-stdin"), |
| Stdout: filepath.Join(dir, "theid-stdout"), |
| Stderr: filepath.Join(dir, "theid-stderr"), |
| Terminal: true, |
| }, |
| } |
| |
| assert.Equal(t, fifos.Config, expected.Config) |
| |
| files, err := os.ReadDir(root) |
| assert.NoError(t, err) |
| assert.Len(t, files, 1) |
| |
| assert.Nil(t, fifos.Close()) |
| files, err = os.ReadDir(root) |
| assert.NoError(t, err) |
| assert.Len(t, files, 0) |
| } |
| |
| func TestNewAttach(t *testing.T) { |
| testCases := []struct { |
| name string |
| expectedStdin, expectedStdout, expectedStderr string |
| }{ |
| { |
| name: "attach to all streams (stdin, stdout, and stderr)", |
| expectedStdin: "this is the stdin", |
| expectedStdout: "this is the stdout", |
| expectedStderr: "this is the stderr", |
| }, |
| { |
| name: "don't attach to stdin", |
| expectedStdout: "this is the stdout", |
| expectedStderr: "this is the stderr", |
| }, |
| { |
| name: "don't attach to stdout", |
| expectedStdin: "this is the stdin", |
| expectedStderr: "this is the stderr", |
| }, |
| { |
| name: "don't attach to stderr", |
| expectedStdin: "this is the stdin", |
| expectedStdout: "this is the stdout", |
| }, |
| } |
| |
| for _, tc := range testCases { |
| t.Run(tc.name, func(t *testing.T) { |
| var ( |
| stdin = bytes.NewBufferString(tc.expectedStdin) |
| stdout = new(bytes.Buffer) |
| stderr = new(bytes.Buffer) |
| |
| // The variables below have to be of the interface type (i.e., io.Reader/io.Writer) |
| // instead of the concrete type (i.e., *bytes.Buffer) *before* being passed to NewAttach. |
| // Otherwise, in NewAttach, the interface value won't be nil |
| // (it's just that the concrete value inside the interface itself is nil. [1]), |
| // which means that the corresponding FIFO path won't be set to be an empty string, |
| // and that's not what we want. |
| // |
| // [1] https://go.dev/tour/methods/12 |
| stdinArg io.Reader |
| stdoutArg, stderrArg io.Writer |
| ) |
| if tc.expectedStdin != "" { |
| stdinArg = stdin |
| } |
| if tc.expectedStdout != "" { |
| stdoutArg = stdout |
| } |
| if tc.expectedStderr != "" { |
| stderrArg = stderr |
| } |
| |
| attacher := NewAttach(WithStreams(stdinArg, stdoutArg, stderrArg)) |
| |
| fifos, err := NewFIFOSetInDir("", "theid", false) |
| assert.NoError(t, err) |
| |
| attachedFifos, err := attacher(fifos) |
| assert.NoError(t, err) |
| defer attachedFifos.Close() |
| |
| producers := setupFIFOProducers(t, attachedFifos.Config()) |
| initProducers(t, producers, tc.expectedStdout, tc.expectedStderr) |
| |
| var actualStdin []byte |
| if producers.Stdin != nil { |
| actualStdin, err = io.ReadAll(producers.Stdin) |
| assert.NoError(t, err) |
| } |
| |
| attachedFifos.Wait() |
| attachedFifos.Cancel() |
| assert.Nil(t, attachedFifos.Close()) |
| |
| assert.Equal(t, tc.expectedStdout, stdout.String()) |
| assert.Equal(t, tc.expectedStderr, stderr.String()) |
| assert.Equal(t, tc.expectedStdin, string(actualStdin)) |
| }) |
| } |
| } |
| |
| type producers struct { |
| Stdin io.ReadCloser |
| Stdout io.WriteCloser |
| Stderr io.WriteCloser |
| } |
| |
| func setupFIFOProducers(t *testing.T, fifos Config) producers { |
| var ( |
| err error |
| pipes producers |
| ctx = context.Background() |
| ) |
| |
| if fifos.Stdin != "" { |
| pipes.Stdin, err = fifo.OpenFifo(ctx, fifos.Stdin, syscall.O_RDONLY, 0) |
| assert.NoError(t, err) |
| } |
| |
| if fifos.Stdout != "" { |
| pipes.Stdout, err = fifo.OpenFifo(ctx, fifos.Stdout, syscall.O_WRONLY, 0) |
| assert.NoError(t, err) |
| } |
| |
| if fifos.Stderr != "" { |
| pipes.Stderr, err = fifo.OpenFifo(ctx, fifos.Stderr, syscall.O_WRONLY, 0) |
| assert.NoError(t, err) |
| } |
| |
| return pipes |
| } |
| |
| func initProducers(t *testing.T, producers producers, stdout, stderr string) { |
| if producers.Stdout != nil { |
| _, err := producers.Stdout.Write([]byte(stdout)) |
| assert.NoError(t, err) |
| assert.Nil(t, producers.Stdout.Close()) |
| } |
| |
| if producers.Stderr != nil { |
| _, err := producers.Stderr.Write([]byte(stderr)) |
| assert.NoError(t, err) |
| assert.Nil(t, producers.Stderr.Close()) |
| } |
| } |
| |
| func TestLogURIGenerator(t *testing.T) { |
| baseTestLogURIGenerator(t, []LogURIGeneratorTestCase{ |
| { |
| scheme: "fifo", |
| path: "/full/path/pipe.fifo", |
| expected: "fifo:///full/path/pipe.fifo", |
| }, |
| { |
| scheme: "file", |
| path: "/full/path/file.txt", |
| args: map[string]string{ |
| "maxSize": "100MB", |
| }, |
| expected: "file:///full/path/file.txt?maxSize=100MB", |
| }, |
| { |
| scheme: "binary", |
| path: "/full/path/bin", |
| args: map[string]string{ |
| "id": "testing", |
| }, |
| expected: "binary:///full/path/bin?id=testing", |
| }, |
| { |
| scheme: "unknown", |
| path: "nowhere", |
| err: "must be absolute", |
| }, |
| { |
| scheme: "binary", |
| path: "C:\\path\\to\\binary", |
| // NOTE: Windows paths should not be parse-able outside of Windows: |
| err: "must be absolute", |
| }, |
| }) |
| } |