Skip to content

Commit bf1b5eb

Browse files
committed
Add MultiplexedWriter and CaptureStandardOut
1 parent 076f27f commit bf1b5eb

File tree

6 files changed

+206
-14
lines changed

6 files changed

+206
-14
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ To configure the command a option function will be passed which receives the com
3333
Default option functions:
3434

3535
- `cmd.WithStandardStreams`
36+
- `cmd.WithCustomStdout(...io.Writers)`
37+
- `cmd.WithCustomStderr(...io.Writers)`
3638
- `cmd.WithTimeout(time.Duration)`
3739
- `cmd.WithoutTimeout`
3840
- `cmd.WithWorkingDir(string)`
@@ -56,6 +58,23 @@ c := cmd.NewCommand("pwd", setWorkingDir)
5658
c.Execute()
5759
```
5860

61+
### Testing
62+
63+
You can catch output streams to `stdout` and `stderr` with `cmd.CaptureStandardOut`.
64+
65+
```golang
66+
// caputred is the captured output from all executed source code
67+
// fnResult contains the result of the executed function
68+
captured, fnResult := cmd.CaptureStandardOut(func() interface{} {
69+
c := NewCommand("echo hello", cmd.WithStandardStream)
70+
err := c.Execute()
71+
return err
72+
})
73+
74+
// prints "hello"
75+
fmt.Println(captured)
76+
```
77+
5978
## Development
6079

6180
### Running tests

command.go

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"time"
1111
)
1212

13-
//Command represents a single command which can be executed
13+
// Command represents a single command which can be executed
1414
type Command struct {
1515
Command string
1616
Env []string
@@ -22,8 +22,9 @@ type Command struct {
2222
executed bool
2323
exitCode int
2424
// stderr and stdout retrieve the output after the command was executed
25-
stderr bytes.Buffer
26-
stdout bytes.Buffer
25+
stderr bytes.Buffer
26+
stdout bytes.Buffer
27+
combined bytes.Buffer
2728
}
2829

2930
// NewCommand creates a new command
@@ -68,8 +69,28 @@ func NewCommand(cmd string, options ...func(*Command)) *Command {
6869
// c.Execute()
6970
//
7071
func WithStandardStreams(c *Command) {
71-
c.StdoutWriter = os.Stdout
72-
c.StderrWriter = os.Stderr
72+
c.StdoutWriter = NewMultiplexedWriter(os.Stdout, &c.stdout, &c.combined)
73+
c.StderrWriter = NewMultiplexedWriter(os.Stderr, &c.stdout, &c.combined)
74+
}
75+
76+
// WithCustomStdout allows to add custom writers to stdout
77+
func WithCustomStdout(writers ...io.Writer) func(c *Command) {
78+
return func(c *Command) {
79+
writers = append(writers, &c.stdout, &c.combined)
80+
c.StdoutWriter = NewMultiplexedWriter(writers...)
81+
82+
c.StderrWriter = NewMultiplexedWriter(&c.stderr, &c.combined)
83+
}
84+
}
85+
86+
// WithCustomStderr allows to add custom writers to stderr
87+
func WithCustomStderr(writers ...io.Writer) func(c *Command) {
88+
return func(c *Command) {
89+
writers = append(writers, &c.stderr, &c.combined)
90+
c.StderrWriter = NewMultiplexedWriter(writers...)
91+
92+
c.StdoutWriter = NewMultiplexedWriter(&c.stdout, &c.combined)
93+
}
7394
}
7495

7596
// WithTimeout sets the timeout of the command
@@ -107,18 +128,24 @@ func (c *Command) AddEnv(key string, value string) {
107128
c.Env = append(c.Env, fmt.Sprintf("%s=%s", key, value))
108129
}
109130

110-
//Stdout returns the output to stdout
131+
// Stdout returns the output to stdout
111132
func (c *Command) Stdout() string {
112133
c.isExecuted("Stdout")
113134
return c.stdout.String()
114135
}
115136

116-
//Stderr returns the output to stderr
137+
// Stderr returns the output to stderr
117138
func (c *Command) Stderr() string {
118139
c.isExecuted("Stderr")
119140
return c.stderr.String()
120141
}
121142

143+
// Combined returns the combined output of stderr and stdout according to their timeline
144+
func (c *Command) Combined() string {
145+
c.isExecuted("Combined")
146+
return c.combined.String()
147+
}
148+
122149
//ExitCode returns the exit code of the command
123150
func (c *Command) ExitCode() int {
124151
c.isExecuted("ExitCode")

command_test.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,35 @@ func TestCommand_SetOptions(t *testing.T) {
127127
assertEqualWithLineBreak(t, "test", writer.String())
128128
}
129129

130-
func assertEqualWithLineBreak(t *testing.T, expected string, actual string) {
131-
if runtime.GOOS == "windows" {
132-
expected = expected + "\r\n"
133-
} else {
134-
expected = expected + "\n"
135-
}
130+
func TestWithCustomStderr(t *testing.T) {
131+
writer := bytes.Buffer{}
132+
c := NewCommand(">&2 echo stderr; sleep 0.01; echo stdout;", WithCustomStderr(&writer))
133+
c.Execute()
134+
135+
assertEqualWithLineBreak(t, "stderr", writer.String())
136+
assertEqualWithLineBreak(t, "stdout", c.Stdout())
137+
assertEqualWithLineBreak(t, "stderr", c.Stderr())
138+
assertEqualWithLineBreak(t, "stderr\nstdout", c.Combined())
139+
}
140+
141+
func TestWithCustomStdout(t *testing.T) {
142+
writer := bytes.Buffer{}
143+
c := NewCommand(">&2 echo stderr; sleep 0.01; echo stdout;", WithCustomStdout(&writer))
144+
c.Execute()
136145

137-
assert.Equal(t, expected, actual)
146+
assertEqualWithLineBreak(t, "stdout", writer.String())
147+
assertEqualWithLineBreak(t, "stdout", c.Stdout())
148+
assertEqualWithLineBreak(t, "stderr", c.Stderr())
149+
assertEqualWithLineBreak(t, "stderr\nstdout", c.Combined())
150+
}
151+
152+
func TestWithStandardStreams(t *testing.T) {
153+
out, err := CaptureStandardOutput(func() interface{} {
154+
c := NewCommand(">&2 echo stderr; sleep 0.01; echo stdout;", WithStandardStreams)
155+
err := c.Execute()
156+
return err
157+
})
158+
159+
assertEqualWithLineBreak(t, "stderr\nstdout", out)
160+
assert.Nil(t, err)
138161
}

multiplexed_writer.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package cmd
2+
3+
import (
4+
"io"
5+
)
6+
7+
// NewMultiplexedWriter returns a new multiplexer
8+
func NewMultiplexedWriter(outputs ...io.Writer) *MultiplexedWriter {
9+
return &MultiplexedWriter{Outputs: outputs}
10+
}
11+
12+
// MultiplexedWriter writes to multiple writers at once
13+
type MultiplexedWriter struct {
14+
Outputs []io.Writer
15+
}
16+
17+
// Write writes the given bytes. If one write fails it returns the error
18+
// and bytes of the failed write operation
19+
func (w MultiplexedWriter) Write(p []byte) (n int, err error) {
20+
for _, o := range w.Outputs {
21+
n, err = o.Write(p)
22+
if err != nil {
23+
return 0, nil
24+
}
25+
}
26+
27+
return n, nil
28+
}

multiplexed_writer_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"github.com/stretchr/testify/assert"
6+
"os"
7+
"testing"
8+
)
9+
10+
func TestMultiplexedWriter(t *testing.T) {
11+
writer01 := bytes.Buffer{}
12+
writer02 := bytes.Buffer{}
13+
// Test another io.Writer interface type
14+
r, w, _ := os.Pipe()
15+
16+
writer := NewMultiplexedWriter(&writer01, &writer02, w)
17+
n, err := writer.Write([]byte(`test`))
18+
19+
assert.Nil(t, err)
20+
assert.Equal(t, 4, n)
21+
assert.Equal(t, "test", writer01.String())
22+
assert.Equal(t, "test", writer02.String())
23+
24+
data := make([]byte, 4)
25+
n, err = r.Read(data)
26+
assert.Nil(t, err)
27+
assert.Equal(t, 4, n)
28+
assert.Equal(t, "test", string(data))
29+
}
30+
31+
func TestMultiplexedWriter_SingleWirter(t *testing.T) {
32+
writer01 := bytes.Buffer{}
33+
34+
writer := NewMultiplexedWriter(&writer01)
35+
36+
n, _ := writer.Write([]byte(`another`))
37+
38+
assert.Equal(t, 7, n)
39+
assert.Equal(t, "another", writer01.String())
40+
}

testing_utils.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"github.com/stretchr/testify/assert"
6+
"io"
7+
"log"
8+
"os"
9+
"runtime"
10+
"sync"
11+
"testing"
12+
)
13+
14+
// CaptureStandardOutput allows to capture the output which will be written
15+
// to os.Stdout and os.Stderr.
16+
// It returns the captured output and the return value of the called function
17+
func CaptureStandardOutput(f func() interface{}) (string, interface{}) {
18+
reader, writer, err := os.Pipe()
19+
if err != nil {
20+
panic(err)
21+
}
22+
stdout := os.Stdout
23+
stderr := os.Stderr
24+
defer func() {
25+
os.Stdout = stdout
26+
os.Stderr = stderr
27+
log.SetOutput(os.Stderr)
28+
}()
29+
os.Stdout = writer
30+
os.Stderr = writer
31+
log.SetOutput(writer)
32+
out := make(chan string)
33+
wg := new(sync.WaitGroup)
34+
wg.Add(1)
35+
go func() {
36+
var buf bytes.Buffer
37+
wg.Done()
38+
io.Copy(&buf, reader)
39+
out <- buf.String()
40+
}()
41+
wg.Wait()
42+
result := f()
43+
writer.Close()
44+
return <-out, result
45+
}
46+
47+
func assertEqualWithLineBreak(t *testing.T, expected string, actual string) {
48+
if runtime.GOOS == "windows" {
49+
expected = expected + "\r\n"
50+
} else {
51+
expected = expected + "\n"
52+
}
53+
54+
assert.Equal(t, expected, actual)
55+
}

0 commit comments

Comments
 (0)