Browse code

Refacator pkg/streamformatter

StreamFormatter suffered was two distinct structs mixed into a single struct
without any overlap.

Signed-off-by: Daniel Nephin <dnephin@docker.com>

Daniel Nephin authored on 2017/05/02 03:54:56
Showing 15 changed files
... ...
@@ -138,7 +138,6 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
138 138
 
139 139
 	output := ioutils.NewWriteFlusher(w)
140 140
 	defer output.Close()
141
-	sf := streamformatter.NewJSONStreamFormatter()
142 141
 	errf := func(err error) error {
143 142
 		if httputils.BoolValue(r, "q") && notVerboseBuffer.Len() > 0 {
144 143
 			output.Write(notVerboseBuffer.Bytes())
... ...
@@ -148,7 +147,7 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
148 148
 		if !output.Flushed() {
149 149
 			return err
150 150
 		}
151
-		_, err = w.Write(sf.FormatError(err))
151
+		_, err = w.Write(streamformatter.FormatError(err))
152 152
 		if err != nil {
153 153
 			logrus.Warnf("could not write error response: %v", err)
154 154
 		}
... ...
@@ -166,25 +165,22 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
166 166
 			errors.New("squash is only supported with experimental mode"))
167 167
 	}
168 168
 
169
+	out := io.Writer(output)
170
+	if buildOptions.SuppressOutput {
171
+		out = notVerboseBuffer
172
+	}
173
+
169 174
 	// Currently, only used if context is from a remote url.
170 175
 	// Look at code in DetectContextFromRemoteURL for more information.
171 176
 	createProgressReader := func(in io.ReadCloser) io.ReadCloser {
172
-		progressOutput := sf.NewProgressOutput(output, true)
173
-		if buildOptions.SuppressOutput {
174
-			progressOutput = sf.NewProgressOutput(notVerboseBuffer, true)
175
-		}
177
+		progressOutput := streamformatter.NewJSONProgressOutput(out, true)
176 178
 		return progress.NewProgressReader(in, progressOutput, r.ContentLength, "Downloading context", buildOptions.RemoteContext)
177 179
 	}
178 180
 
179
-	out := io.Writer(output)
180
-	if buildOptions.SuppressOutput {
181
-		out = notVerboseBuffer
182
-	}
183
-
184 181
 	imgID, err := br.backend.Build(ctx, backend.BuildConfig{
185 182
 		Source:         r.Body,
186 183
 		Options:        buildOptions,
187
-		ProgressWriter: buildProgressWriter(out, sf, createProgressReader),
184
+		ProgressWriter: buildProgressWriter(out, createProgressReader),
188 185
 	})
189 186
 	if err != nil {
190 187
 		return errf(err)
... ...
@@ -193,8 +189,7 @@ func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *
193 193
 	// Everything worked so if -q was provided the output from the daemon
194 194
 	// should be just the image ID and we'll print that to stdout.
195 195
 	if buildOptions.SuppressOutput {
196
-		stdout := &streamformatter.StdoutFormatter{Writer: output, StreamFormatter: sf}
197
-		fmt.Fprintln(stdout, imgID)
196
+		fmt.Fprintln(streamformatter.NewStdoutWriter(output), imgID)
198 197
 	}
199 198
 	return nil
200 199
 }
... ...
@@ -226,15 +221,13 @@ func (s *syncWriter) Write(b []byte) (count int, err error) {
226 226
 	return
227 227
 }
228 228
 
229
-func buildProgressWriter(out io.Writer, sf *streamformatter.StreamFormatter, createProgressReader func(io.ReadCloser) io.ReadCloser) backend.ProgressWriter {
229
+func buildProgressWriter(out io.Writer, createProgressReader func(io.ReadCloser) io.ReadCloser) backend.ProgressWriter {
230 230
 	out = &syncWriter{w: out}
231
-	stdout := &streamformatter.StdoutFormatter{Writer: out, StreamFormatter: sf}
232
-	stderr := &streamformatter.StderrFormatter{Writer: out, StreamFormatter: sf}
233 231
 
234 232
 	return backend.ProgressWriter{
235 233
 		Output:             out,
236
-		StdoutFormatter:    stdout,
237
-		StderrFormatter:    stderr,
234
+		StdoutFormatter:    streamformatter.NewStdoutWriter(out),
235
+		StderrFormatter:    streamformatter.NewStderrWriter(out),
238 236
 		ProgressReaderFunc: createProgressReader,
239 237
 	}
240 238
 }
... ...
@@ -118,8 +118,7 @@ func (s *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWrite
118 118
 		if !output.Flushed() {
119 119
 			return err
120 120
 		}
121
-		sf := streamformatter.NewJSONStreamFormatter()
122
-		output.Write(sf.FormatError(err))
121
+		output.Write(streamformatter.FormatError(err))
123 122
 	}
124 123
 
125 124
 	return nil
... ...
@@ -164,8 +163,7 @@ func (s *imageRouter) postImagesPush(ctx context.Context, w http.ResponseWriter,
164 164
 		if !output.Flushed() {
165 165
 			return err
166 166
 		}
167
-		sf := streamformatter.NewJSONStreamFormatter()
168
-		output.Write(sf.FormatError(err))
167
+		output.Write(streamformatter.FormatError(err))
169 168
 	}
170 169
 	return nil
171 170
 }
... ...
@@ -190,8 +188,7 @@ func (s *imageRouter) getImagesGet(ctx context.Context, w http.ResponseWriter, r
190 190
 		if !output.Flushed() {
191 191
 			return err
192 192
 		}
193
-		sf := streamformatter.NewJSONStreamFormatter()
194
-		output.Write(sf.FormatError(err))
193
+		output.Write(streamformatter.FormatError(err))
195 194
 	}
196 195
 	return nil
197 196
 }
... ...
@@ -207,7 +204,7 @@ func (s *imageRouter) postImagesLoad(ctx context.Context, w http.ResponseWriter,
207 207
 	output := ioutils.NewWriteFlusher(w)
208 208
 	defer output.Close()
209 209
 	if err := s.backend.LoadImage(r.Body, output, quiet); err != nil {
210
-		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
210
+		output.Write(streamformatter.FormatError(err))
211 211
 	}
212 212
 	return nil
213 213
 }
... ...
@@ -121,7 +121,7 @@ func (pr *pluginRouter) upgradePlugin(ctx context.Context, w http.ResponseWriter
121 121
 		if !output.Flushed() {
122 122
 			return err
123 123
 		}
124
-		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
124
+		output.Write(streamformatter.FormatError(err))
125 125
 	}
126 126
 
127 127
 	return nil
... ...
@@ -160,7 +160,7 @@ func (pr *pluginRouter) pullPlugin(ctx context.Context, w http.ResponseWriter, r
160 160
 		if !output.Flushed() {
161 161
 			return err
162 162
 		}
163
-		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
163
+		output.Write(streamformatter.FormatError(err))
164 164
 	}
165 165
 
166 166
 	return nil
... ...
@@ -268,7 +268,7 @@ func (pr *pluginRouter) pushPlugin(ctx context.Context, w http.ResponseWriter, r
268 268
 		if !output.Flushed() {
269 269
 			return err
270 270
 		}
271
-		output.Write(streamformatter.NewJSONStreamFormatter().FormatError(err))
271
+		output.Write(streamformatter.FormatError(err))
272 272
 	}
273 273
 	return nil
274 274
 }
... ...
@@ -4,14 +4,13 @@ import (
4 4
 	"io"
5 5
 
6 6
 	"github.com/docker/docker/api/types"
7
-	"github.com/docker/docker/pkg/streamformatter"
8 7
 )
9 8
 
10 9
 // ProgressWriter is a data object to transport progress streams to the client
11 10
 type ProgressWriter struct {
12 11
 	Output             io.Writer
13
-	StdoutFormatter    *streamformatter.StdoutFormatter
14
-	StderrFormatter    *streamformatter.StderrFormatter
12
+	StdoutFormatter    io.Writer
13
+	StderrFormatter    io.Writer
15 14
 	ProgressReaderFunc func(io.ReadCloser) io.ReadCloser
16 15
 }
17 16
 
... ...
@@ -275,8 +275,7 @@ func (b *Builder) download(srcURL string) (remote builder.Source, p string, err
275 275
 		return
276 276
 	}
277 277
 
278
-	stdoutFormatter := b.Stdout.(*streamformatter.StdoutFormatter)
279
-	progressOutput := stdoutFormatter.StreamFormatter.NewProgressOutput(stdoutFormatter.Writer, true)
278
+	progressOutput := streamformatter.NewJSONProgressOutput(b.Output, true)
280 279
 	progressReader := progress.NewProgressReader(resp.Body, progressOutput, resp.ContentLength, "", "Downloading")
281 280
 	// Download and dump result to tmp file
282 281
 	// TODO: add filehash directly
... ...
@@ -269,7 +269,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
269 269
 	}
270 270
 
271 271
 	// Setup an upload progress bar
272
-	progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true)
272
+	progressOutput := streamformatter.NewProgressOutput(progBuff)
273 273
 	if !dockerCli.Out().IsTerminal() {
274 274
 		progressOutput = &lastProgressOutput{output: progressOutput}
275 275
 	}
... ...
@@ -154,7 +154,7 @@ func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.Read
154 154
 	if err != nil {
155 155
 		return nil, "", errors.Errorf("unable to download remote context %s: %v", remoteURL, err)
156 156
 	}
157
-	progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(out, true)
157
+	progressOutput := streamformatter.NewProgressOutput(out)
158 158
 
159 159
 	// Pass the response body through a progress reader.
160 160
 	progReader := progress.NewProgressReader(response.Body, progressOutput, response.ContentLength, "", fmt.Sprintf("Downloading build context from remote url: %s", remoteURL))
... ...
@@ -62,7 +62,7 @@ func stateToProgress(state swarm.TaskState, rollback bool) int64 {
62 62
 func ServiceProgress(ctx context.Context, client client.APIClient, serviceID string, progressWriter io.WriteCloser) error {
63 63
 	defer progressWriter.Close()
64 64
 
65
-	progressOut := streamformatter.NewJSONStreamFormatter().NewProgressOutput(progressWriter, false)
65
+	progressOut := streamformatter.NewJSONProgressOutput(progressWriter, false)
66 66
 
67 67
 	sigint := make(chan os.Signal, 1)
68 68
 	signal.Notify(sigint, os.Interrupt)
... ...
@@ -28,7 +28,6 @@ import (
28 28
 // the repo and tag arguments, respectively.
29 29
 func (daemon *Daemon) ImportImage(src string, repository, tag string, msg string, inConfig io.ReadCloser, outStream io.Writer, changes []string) error {
30 30
 	var (
31
-		sf     = streamformatter.NewJSONStreamFormatter()
32 31
 		rc     io.ReadCloser
33 32
 		resp   *http.Response
34 33
 		newRef reference.Named
... ...
@@ -72,8 +71,8 @@ func (daemon *Daemon) ImportImage(src string, repository, tag string, msg string
72 72
 		if err != nil {
73 73
 			return err
74 74
 		}
75
-		outStream.Write(sf.FormatStatus("", "Downloading from %s", u))
76
-		progressOutput := sf.NewProgressOutput(outStream, true)
75
+		outStream.Write(streamformatter.FormatStatus("", "Downloading from %s", u))
76
+		progressOutput := streamformatter.NewJSONProgressOutput(outStream, true)
77 77
 		rc = progress.NewProgressReader(resp.Body, progressOutput, resp.ContentLength, "", "Importing")
78 78
 	}
79 79
 
... ...
@@ -129,6 +128,6 @@ func (daemon *Daemon) ImportImage(src string, repository, tag string, msg string
129 129
 	}
130 130
 
131 131
 	daemon.LogImageEvent(id.String(), id.String(), "import")
132
-	outStream.Write(sf.FormatStatus("", id.String()))
132
+	outStream.Write(streamformatter.FormatStatus("", id.String()))
133 133
 	return nil
134 134
 }
... ...
@@ -14,7 +14,7 @@ import (
14 14
 // WriteDistributionProgress is a helper for writing progress from chan to JSON
15 15
 // stream with an optional cancel function.
16 16
 func WriteDistributionProgress(cancelFunc func(), outStream io.Writer, progressChan <-chan progress.Progress) {
17
-	progressOutput := streamformatter.NewJSONStreamFormatter().NewProgressOutput(outStream, false)
17
+	progressOutput := streamformatter.NewJSONProgressOutput(outStream, false)
18 18
 	operationCancelled := false
19 19
 
20 20
 	for prog := range progressChan {
... ...
@@ -26,14 +26,11 @@ import (
26 26
 )
27 27
 
28 28
 func (l *tarexporter) Load(inTar io.ReadCloser, outStream io.Writer, quiet bool) error {
29
-	var (
30
-		sf             = streamformatter.NewJSONStreamFormatter()
31
-		progressOutput progress.Output
32
-	)
29
+	var progressOutput progress.Output
33 30
 	if !quiet {
34
-		progressOutput = sf.NewProgressOutput(outStream, false)
31
+		progressOutput = streamformatter.NewJSONProgressOutput(outStream, false)
35 32
 	}
36
-	outStream = &streamformatter.StdoutFormatter{Writer: outStream, StreamFormatter: streamformatter.NewJSONStreamFormatter()}
33
+	outStream = streamformatter.NewStdoutWriter(outStream)
37 34
 
38 35
 	tmpDir, err := ioutil.TempDir("", "docker-import-")
39 36
 	if err != nil {
... ...
@@ -10,91 +10,76 @@ import (
10 10
 	"github.com/docker/docker/pkg/progress"
11 11
 )
12 12
 
13
-// StreamFormatter formats a stream, optionally using JSON.
14
-type StreamFormatter struct {
15
-	json bool
16
-}
17
-
18
-// NewStreamFormatter returns a simple StreamFormatter
19
-func NewStreamFormatter() *StreamFormatter {
20
-	return &StreamFormatter{}
21
-}
22
-
23
-// NewJSONStreamFormatter returns a StreamFormatter configured to stream json
24
-func NewJSONStreamFormatter() *StreamFormatter {
25
-	return &StreamFormatter{true}
26
-}
27
-
28 13
 const streamNewline = "\r\n"
29 14
 
30
-var streamNewlineBytes = []byte(streamNewline)
15
+type jsonProgressFormatter struct{}
31 16
 
32
-// FormatStream formats the specified stream.
33
-func (sf *StreamFormatter) FormatStream(str string) []byte {
34
-	if sf.json {
35
-		b, err := json.Marshal(&jsonmessage.JSONMessage{Stream: str})
36
-		if err != nil {
37
-			return sf.FormatError(err)
38
-		}
39
-		return append(b, streamNewlineBytes...)
40
-	}
41
-	return []byte(str + "\r")
17
+func appendNewline(source []byte) []byte {
18
+	return append(source, []byte(streamNewline)...)
42 19
 }
43 20
 
44 21
 // FormatStatus formats the specified objects according to the specified format (and id).
45
-func (sf *StreamFormatter) FormatStatus(id, format string, a ...interface{}) []byte {
22
+func FormatStatus(id, format string, a ...interface{}) []byte {
46 23
 	str := fmt.Sprintf(format, a...)
47
-	if sf.json {
48
-		b, err := json.Marshal(&jsonmessage.JSONMessage{ID: id, Status: str})
49
-		if err != nil {
50
-			return sf.FormatError(err)
51
-		}
52
-		return append(b, streamNewlineBytes...)
24
+	b, err := json.Marshal(&jsonmessage.JSONMessage{ID: id, Status: str})
25
+	if err != nil {
26
+		return FormatError(err)
53 27
 	}
54
-	return []byte(str + streamNewline)
28
+	return appendNewline(b)
55 29
 }
56 30
 
57
-// FormatError formats the specified error.
58
-func (sf *StreamFormatter) FormatError(err error) []byte {
59
-	if sf.json {
60
-		jsonError, ok := err.(*jsonmessage.JSONError)
61
-		if !ok {
62
-			jsonError = &jsonmessage.JSONError{Message: err.Error()}
63
-		}
64
-		if b, err := json.Marshal(&jsonmessage.JSONMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil {
65
-			return append(b, streamNewlineBytes...)
66
-		}
67
-		return []byte("{\"error\":\"format error\"}" + streamNewline)
31
+// FormatError formats the error as a JSON object
32
+func FormatError(err error) []byte {
33
+	jsonError, ok := err.(*jsonmessage.JSONError)
34
+	if !ok {
35
+		jsonError = &jsonmessage.JSONError{Message: err.Error()}
68 36
 	}
69
-	return []byte("Error: " + err.Error() + streamNewline)
37
+	if b, err := json.Marshal(&jsonmessage.JSONMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil {
38
+		return appendNewline(b)
39
+	}
40
+	return []byte(`{"error":"format error"}` + streamNewline)
41
+}
42
+
43
+func (sf *jsonProgressFormatter) formatStatus(id, format string, a ...interface{}) []byte {
44
+	return FormatStatus(id, format, a...)
70 45
 }
71 46
 
72
-// FormatProgress formats the progress information for a specified action.
73
-func (sf *StreamFormatter) FormatProgress(id, action string, progress *jsonmessage.JSONProgress, aux interface{}) []byte {
47
+// formatProgress formats the progress information for a specified action.
48
+func (sf *jsonProgressFormatter) formatProgress(id, action string, progress *jsonmessage.JSONProgress, aux interface{}) []byte {
74 49
 	if progress == nil {
75 50
 		progress = &jsonmessage.JSONProgress{}
76 51
 	}
77
-	if sf.json {
78
-		var auxJSON *json.RawMessage
79
-		if aux != nil {
80
-			auxJSONBytes, err := json.Marshal(aux)
81
-			if err != nil {
82
-				return nil
83
-			}
84
-			auxJSON = new(json.RawMessage)
85
-			*auxJSON = auxJSONBytes
86
-		}
87
-		b, err := json.Marshal(&jsonmessage.JSONMessage{
88
-			Status:          action,
89
-			ProgressMessage: progress.String(),
90
-			Progress:        progress,
91
-			ID:              id,
92
-			Aux:             auxJSON,
93
-		})
52
+	var auxJSON *json.RawMessage
53
+	if aux != nil {
54
+		auxJSONBytes, err := json.Marshal(aux)
94 55
 		if err != nil {
95 56
 			return nil
96 57
 		}
97
-		return append(b, streamNewlineBytes...)
58
+		auxJSON = new(json.RawMessage)
59
+		*auxJSON = auxJSONBytes
60
+	}
61
+	b, err := json.Marshal(&jsonmessage.JSONMessage{
62
+		Status:          action,
63
+		ProgressMessage: progress.String(),
64
+		Progress:        progress,
65
+		ID:              id,
66
+		Aux:             auxJSON,
67
+	})
68
+	if err != nil {
69
+		return nil
70
+	}
71
+	return appendNewline(b)
72
+}
73
+
74
+type rawProgressFormatter struct{}
75
+
76
+func (sf *rawProgressFormatter) formatStatus(id, format string, a ...interface{}) []byte {
77
+	return []byte(fmt.Sprintf(format, a...) + streamNewline)
78
+}
79
+
80
+func (sf *rawProgressFormatter) formatProgress(id, action string, progress *jsonmessage.JSONProgress, aux interface{}) []byte {
81
+	if progress == nil {
82
+		progress = &jsonmessage.JSONProgress{}
98 83
 	}
99 84
 	endl := "\r"
100 85
 	if progress.String() == "" {
... ...
@@ -105,16 +90,23 @@ func (sf *StreamFormatter) FormatProgress(id, action string, progress *jsonmessa
105 105
 
106 106
 // NewProgressOutput returns a progress.Output object that can be passed to
107 107
 // progress.NewProgressReader.
108
-func (sf *StreamFormatter) NewProgressOutput(out io.Writer, newLines bool) progress.Output {
109
-	return &progressOutput{
110
-		sf:       sf,
111
-		out:      out,
112
-		newLines: newLines,
113
-	}
108
+func NewProgressOutput(out io.Writer) progress.Output {
109
+	return &progressOutput{sf: &rawProgressFormatter{}, out: out, newLines: true}
110
+}
111
+
112
+// NewJSONProgressOutput returns a progress.Output that that formats output
113
+// using JSON objects
114
+func NewJSONProgressOutput(out io.Writer, newLines bool) progress.Output {
115
+	return &progressOutput{sf: &jsonProgressFormatter{}, out: out, newLines: newLines}
116
+}
117
+
118
+type formatProgress interface {
119
+	formatStatus(id, format string, a ...interface{}) []byte
120
+	formatProgress(id, action string, progress *jsonmessage.JSONProgress, aux interface{}) []byte
114 121
 }
115 122
 
116 123
 type progressOutput struct {
117
-	sf       *StreamFormatter
124
+	sf       formatProgress
118 125
 	out      io.Writer
119 126
 	newLines bool
120 127
 }
... ...
@@ -123,10 +115,10 @@ type progressOutput struct {
123 123
 func (out *progressOutput) WriteProgress(prog progress.Progress) error {
124 124
 	var formatted []byte
125 125
 	if prog.Message != "" {
126
-		formatted = out.sf.FormatStatus(prog.ID, prog.Message)
126
+		formatted = out.sf.formatStatus(prog.ID, prog.Message)
127 127
 	} else {
128 128
 		jsonProgress := jsonmessage.JSONProgress{Current: prog.Current, Total: prog.Total, HideCounts: prog.HideCounts}
129
-		formatted = out.sf.FormatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux)
129
+		formatted = out.sf.formatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux)
130 130
 	}
131 131
 	_, err := out.out.Write(formatted)
132 132
 	if err != nil {
... ...
@@ -134,39 +126,9 @@ func (out *progressOutput) WriteProgress(prog progress.Progress) error {
134 134
 	}
135 135
 
136 136
 	if out.newLines && prog.LastUpdate {
137
-		_, err = out.out.Write(out.sf.FormatStatus("", ""))
137
+		_, err = out.out.Write(out.sf.formatStatus("", ""))
138 138
 		return err
139 139
 	}
140 140
 
141 141
 	return nil
142 142
 }
143
-
144
-// StdoutFormatter is a streamFormatter that writes to the standard output.
145
-type StdoutFormatter struct {
146
-	io.Writer
147
-	*StreamFormatter
148
-}
149
-
150
-func (sf *StdoutFormatter) Write(buf []byte) (int, error) {
151
-	formattedBuf := sf.StreamFormatter.FormatStream(string(buf))
152
-	n, err := sf.Writer.Write(formattedBuf)
153
-	if n != len(formattedBuf) {
154
-		return n, io.ErrShortWrite
155
-	}
156
-	return len(buf), err
157
-}
158
-
159
-// StderrFormatter is a streamFormatter that writes to the standard error.
160
-type StderrFormatter struct {
161
-	io.Writer
162
-	*StreamFormatter
163
-}
164
-
165
-func (sf *StderrFormatter) Write(buf []byte) (int, error) {
166
-	formattedBuf := sf.StreamFormatter.FormatStream("\033[91m" + string(buf) + "\033[0m")
167
-	n, err := sf.Writer.Write(formattedBuf)
168
-	if n != len(formattedBuf) {
169
-		return n, io.ErrShortWrite
170
-	}
171
-	return len(buf), err
172
-}
... ...
@@ -3,88 +3,65 @@ package streamformatter
3 3
 import (
4 4
 	"encoding/json"
5 5
 	"errors"
6
-	"reflect"
7 6
 	"strings"
8 7
 	"testing"
9 8
 
10 9
 	"github.com/docker/docker/pkg/jsonmessage"
10
+	"github.com/stretchr/testify/assert"
11
+	"github.com/stretchr/testify/require"
11 12
 )
12 13
 
13
-func TestFormatStream(t *testing.T) {
14
-	sf := NewStreamFormatter()
15
-	res := sf.FormatStream("stream")
16
-	if string(res) != "stream"+"\r" {
17
-		t.Fatalf("%q", res)
18
-	}
14
+func TestRawProgressFormatterFormatStatus(t *testing.T) {
15
+	sf := rawProgressFormatter{}
16
+	res := sf.formatStatus("ID", "%s%d", "a", 1)
17
+	assert.Equal(t, "a1\r\n", string(res))
19 18
 }
20 19
 
21
-func TestFormatJSONStatus(t *testing.T) {
22
-	sf := NewStreamFormatter()
23
-	res := sf.FormatStatus("ID", "%s%d", "a", 1)
24
-	if string(res) != "a1\r\n" {
25
-		t.Fatalf("%q", res)
26
-	}
27
-}
28
-
29
-func TestFormatSimpleError(t *testing.T) {
30
-	sf := NewStreamFormatter()
31
-	res := sf.FormatError(errors.New("Error for formatter"))
32
-	if string(res) != "Error: Error for formatter\r\n" {
33
-		t.Fatalf("%q", res)
34
-	}
35
-}
36
-
37
-func TestJSONFormatStream(t *testing.T) {
38
-	sf := NewJSONStreamFormatter()
39
-	res := sf.FormatStream("stream")
40
-	if string(res) != `{"stream":"stream"}`+"\r\n" {
41
-		t.Fatalf("%q", res)
20
+func TestRawProgressFormatterFormatProgress(t *testing.T) {
21
+	sf := rawProgressFormatter{}
22
+	progress := &jsonmessage.JSONProgress{
23
+		Current: 15,
24
+		Total:   30,
25
+		Start:   1,
42 26
 	}
27
+	res := sf.formatProgress("id", "action", progress, nil)
28
+	out := string(res)
29
+	assert.True(t, strings.HasPrefix(out, "action [===="))
30
+	assert.Contains(t, out, "15B/30B")
31
+	assert.True(t, strings.HasSuffix(out, "\r"))
43 32
 }
44 33
 
45
-func TestJSONFormatStatus(t *testing.T) {
46
-	sf := NewJSONStreamFormatter()
47
-	res := sf.FormatStatus("ID", "%s%d", "a", 1)
48
-	if string(res) != `{"status":"a1","id":"ID"}`+"\r\n" {
49
-		t.Fatalf("%q", res)
50
-	}
34
+func TestFormatStatus(t *testing.T) {
35
+	res := FormatStatus("ID", "%s%d", "a", 1)
36
+	expected := `{"status":"a1","id":"ID"}` + streamNewline
37
+	assert.Equal(t, expected, string(res))
51 38
 }
52 39
 
53
-func TestJSONFormatSimpleError(t *testing.T) {
54
-	sf := NewJSONStreamFormatter()
55
-	res := sf.FormatError(errors.New("Error for formatter"))
56
-	if string(res) != `{"errorDetail":{"message":"Error for formatter"},"error":"Error for formatter"}`+"\r\n" {
57
-		t.Fatalf("%q", res)
58
-	}
40
+func TestFormatError(t *testing.T) {
41
+	res := FormatError(errors.New("Error for formatter"))
42
+	expected := `{"errorDetail":{"message":"Error for formatter"},"error":"Error for formatter"}` + "\r\n"
43
+	assert.Equal(t, expected, string(res))
59 44
 }
60 45
 
61
-func TestJSONFormatJSONError(t *testing.T) {
62
-	sf := NewJSONStreamFormatter()
46
+func TestFormatJSONError(t *testing.T) {
63 47
 	err := &jsonmessage.JSONError{Code: 50, Message: "Json error"}
64
-	res := sf.FormatError(err)
65
-	if string(res) != `{"errorDetail":{"code":50,"message":"Json error"},"error":"Json error"}`+"\r\n" {
66
-		t.Fatalf("%q", res)
67
-	}
48
+	res := FormatError(err)
49
+	expected := `{"errorDetail":{"code":50,"message":"Json error"},"error":"Json error"}` + streamNewline
50
+	assert.Equal(t, expected, string(res))
68 51
 }
69 52
 
70
-func TestJSONFormatProgress(t *testing.T) {
71
-	sf := NewJSONStreamFormatter()
53
+func TestJsonProgressFormatterFormatProgress(t *testing.T) {
54
+	sf := &jsonProgressFormatter{}
72 55
 	progress := &jsonmessage.JSONProgress{
73 56
 		Current: 15,
74 57
 		Total:   30,
75 58
 		Start:   1,
76 59
 	}
77
-	res := sf.FormatProgress("id", "action", progress, nil)
60
+	res := sf.formatProgress("id", "action", progress, nil)
78 61
 	msg := &jsonmessage.JSONMessage{}
79
-	if err := json.Unmarshal(res, msg); err != nil {
80
-		t.Fatal(err)
81
-	}
82
-	if msg.ID != "id" {
83
-		t.Fatalf("ID must be 'id', got: %s", msg.ID)
84
-	}
85
-	if msg.Status != "action" {
86
-		t.Fatalf("Status must be 'action', got: %s", msg.Status)
87
-	}
62
+	require.NoError(t, json.Unmarshal(res, msg))
63
+	assert.Equal(t, "id", msg.ID)
64
+	assert.Equal(t, "action", msg.Status)
88 65
 
89 66
 	// The progress will always be in the format of:
90 67
 	// [=========================>                         ]      15B/30B 412910h51m30s
... ...
@@ -102,7 +79,5 @@ func TestJSONFormatProgress(t *testing.T) {
102 102
 			expectedProgress, expectedProgressShort, msg.ProgressMessage)
103 103
 	}
104 104
 
105
-	if !reflect.DeepEqual(msg.Progress, progress) {
106
-		t.Fatal("Original progress not equals progress from FormatProgress")
107
-	}
105
+	assert.Equal(t, progress, msg.Progress)
108 106
 }
109 107
new file mode 100644
... ...
@@ -0,0 +1,47 @@
0
+package streamformatter
1
+
2
+import (
3
+	"encoding/json"
4
+	"io"
5
+
6
+	"github.com/docker/docker/pkg/jsonmessage"
7
+)
8
+
9
+type streamWriter struct {
10
+	io.Writer
11
+	lineFormat func([]byte) string
12
+}
13
+
14
+func (sw *streamWriter) Write(buf []byte) (int, error) {
15
+	formattedBuf := sw.format(buf)
16
+	n, err := sw.Writer.Write(formattedBuf)
17
+	if n != len(formattedBuf) {
18
+		return n, io.ErrShortWrite
19
+	}
20
+	return len(buf), err
21
+}
22
+
23
+func (sw *streamWriter) format(buf []byte) []byte {
24
+	msg := &jsonmessage.JSONMessage{Stream: sw.lineFormat(buf)}
25
+	b, err := json.Marshal(msg)
26
+	if err != nil {
27
+		return FormatError(err)
28
+	}
29
+	return appendNewline(b)
30
+}
31
+
32
+// NewStdoutWriter returns a writer which formats the output as json message
33
+// representing stdout lines
34
+func NewStdoutWriter(out io.Writer) io.Writer {
35
+	return &streamWriter{Writer: out, lineFormat: func(buf []byte) string {
36
+		return string(buf)
37
+	}}
38
+}
39
+
40
+// NewStderrWriter returns a writer which formats the output as json message
41
+// representing stderr lines
42
+func NewStderrWriter(out io.Writer) io.Writer {
43
+	return &streamWriter{Writer: out, lineFormat: func(buf []byte) string {
44
+		return "\033[91m" + string(buf) + "\033[0m"
45
+	}}
46
+}
0 47
new file mode 100644
... ...
@@ -0,0 +1,35 @@
0
+package streamformatter
1
+
2
+import (
3
+	"testing"
4
+
5
+	"bytes"
6
+	"github.com/stretchr/testify/assert"
7
+	"github.com/stretchr/testify/require"
8
+)
9
+
10
+func TestStreamWriterStdout(t *testing.T) {
11
+	buffer := &bytes.Buffer{}
12
+	content := "content"
13
+	sw := NewStdoutWriter(buffer)
14
+	size, err := sw.Write([]byte(content))
15
+
16
+	require.NoError(t, err)
17
+	assert.Equal(t, len(content), size)
18
+
19
+	expected := `{"stream":"content"}` + streamNewline
20
+	assert.Equal(t, expected, buffer.String())
21
+}
22
+
23
+func TestStreamWriterStderr(t *testing.T) {
24
+	buffer := &bytes.Buffer{}
25
+	content := "content"
26
+	sw := NewStderrWriter(buffer)
27
+	size, err := sw.Write([]byte(content))
28
+
29
+	require.NoError(t, err)
30
+	assert.Equal(t, len(content), size)
31
+
32
+	expected := `{"stream":"\u001b[91mcontent\u001b[0m"}` + streamNewline
33
+	assert.Equal(t, expected, buffer.String())
34
+}