Browse code

Merge pull request #26519 from AkihiroSuda/fix-cli-command-formatter-json-support

Fix broken JSON support in cli/command/formatter

Justin Cormack authored on 2016/10/18 06:44:05
Showing 11 changed files
... ...
@@ -79,6 +79,10 @@ type containerContext struct {
79 79
 	c     types.Container
80 80
 }
81 81
 
82
+func (c *containerContext) MarshalJSON() ([]byte, error) {
83
+	return marshalJSON(c)
84
+}
85
+
82 86
 func (c *containerContext) ID() string {
83 87
 	c.AddHeader(containerIDHeader)
84 88
 	if c.trunc {
... ...
@@ -2,6 +2,7 @@ package formatter
2 2
 
3 3
 import (
4 4
 	"bytes"
5
+	"encoding/json"
5 6
 	"fmt"
6 7
 	"strings"
7 8
 	"testing"
... ...
@@ -323,3 +324,49 @@ func TestContainerContextWriteWithNoContainers(t *testing.T) {
323 323
 		out.Reset()
324 324
 	}
325 325
 }
326
+
327
+func TestContainerContextWriteJSON(t *testing.T) {
328
+	unix := time.Now().Add(-65 * time.Second).Unix()
329
+	containers := []types.Container{
330
+		{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unix},
331
+		{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unix},
332
+	}
333
+	expectedCreated := time.Unix(unix, 0).String()
334
+	expectedJSONs := []map[string]interface{}{
335
+		{"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Ports": "", "RunningFor": "About a minute", "Size": "0 B", "Status": ""},
336
+		{"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Ports": "", "RunningFor": "About a minute", "Size": "0 B", "Status": ""},
337
+	}
338
+	out := bytes.NewBufferString("")
339
+	err := ContainerWrite(Context{Format: "{{json .}}", Output: out}, containers)
340
+	if err != nil {
341
+		t.Fatal(err)
342
+	}
343
+	for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
344
+		t.Logf("Output: line %d: %s", i, line)
345
+		var m map[string]interface{}
346
+		if err := json.Unmarshal([]byte(line), &m); err != nil {
347
+			t.Fatal(err)
348
+		}
349
+		assert.DeepEqual(t, m, expectedJSONs[i])
350
+	}
351
+}
352
+
353
+func TestContainerContextWriteJSONField(t *testing.T) {
354
+	containers := []types.Container{
355
+		{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu"},
356
+		{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu"},
357
+	}
358
+	out := bytes.NewBufferString("")
359
+	err := ContainerWrite(Context{Format: "{{json .ID}}", Output: out}, containers)
360
+	if err != nil {
361
+		t.Fatal(err)
362
+	}
363
+	for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
364
+		t.Logf("Output: line %d: %s", i, line)
365
+		var s string
366
+		if err := json.Unmarshal([]byte(line), &s); err != nil {
367
+			t.Fatal(err)
368
+		}
369
+		assert.Equal(t, s, containers[i].ID)
370
+	}
371
+}
... ...
@@ -53,6 +53,10 @@ type networkContext struct {
53 53
 	n     types.NetworkResource
54 54
 }
55 55
 
56
+func (c *networkContext) MarshalJSON() ([]byte, error) {
57
+	return marshalJSON(c)
58
+}
59
+
56 60
 func (c *networkContext) ID() string {
57 61
 	c.AddHeader(networkIDHeader)
58 62
 	if c.trunc {
... ...
@@ -2,6 +2,7 @@ package formatter
2 2
 
3 3
 import (
4 4
 	"bytes"
5
+	"encoding/json"
5 6
 	"strings"
6 7
 	"testing"
7 8
 
... ...
@@ -160,3 +161,48 @@ foobar_bar
160 160
 		}
161 161
 	}
162 162
 }
163
+
164
+func TestNetworkContextWriteJSON(t *testing.T) {
165
+	networks := []types.NetworkResource{
166
+		{ID: "networkID1", Name: "foobar_baz"},
167
+		{ID: "networkID2", Name: "foobar_bar"},
168
+	}
169
+	expectedJSONs := []map[string]interface{}{
170
+		{"Driver": "", "ID": "networkID1", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_baz", "Scope": ""},
171
+		{"Driver": "", "ID": "networkID2", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_bar", "Scope": ""},
172
+	}
173
+
174
+	out := bytes.NewBufferString("")
175
+	err := NetworkWrite(Context{Format: "{{json .}}", Output: out}, networks)
176
+	if err != nil {
177
+		t.Fatal(err)
178
+	}
179
+	for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
180
+		t.Logf("Output: line %d: %s", i, line)
181
+		var m map[string]interface{}
182
+		if err := json.Unmarshal([]byte(line), &m); err != nil {
183
+			t.Fatal(err)
184
+		}
185
+		assert.DeepEqual(t, m, expectedJSONs[i])
186
+	}
187
+}
188
+
189
+func TestNetworkContextWriteJSONField(t *testing.T) {
190
+	networks := []types.NetworkResource{
191
+		{ID: "networkID1", Name: "foobar_baz"},
192
+		{ID: "networkID2", Name: "foobar_bar"},
193
+	}
194
+	out := bytes.NewBufferString("")
195
+	err := NetworkWrite(Context{Format: "{{json .ID}}", Output: out}, networks)
196
+	if err != nil {
197
+		t.Fatal(err)
198
+	}
199
+	for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
200
+		t.Logf("Output: line %d: %s", i, line)
201
+		var s string
202
+		if err := json.Unmarshal([]byte(line), &s); err != nil {
203
+			t.Fatal(err)
204
+		}
205
+		assert.Equal(t, s, networks[i].ID)
206
+	}
207
+}
163 208
new file mode 100644
... ...
@@ -0,0 +1,65 @@
0
+package formatter
1
+
2
+import (
3
+	"encoding/json"
4
+	"fmt"
5
+	"reflect"
6
+	"unicode"
7
+)
8
+
9
+func marshalJSON(x interface{}) ([]byte, error) {
10
+	m, err := marshalMap(x)
11
+	if err != nil {
12
+		return nil, err
13
+	}
14
+	return json.Marshal(m)
15
+}
16
+
17
+// marshalMap marshals x to map[string]interface{}
18
+func marshalMap(x interface{}) (map[string]interface{}, error) {
19
+	val := reflect.ValueOf(x)
20
+	if val.Kind() != reflect.Ptr {
21
+		return nil, fmt.Errorf("expected a pointer to a struct, got %v", val.Kind())
22
+	}
23
+	if val.IsNil() {
24
+		return nil, fmt.Errorf("expxected a pointer to a struct, got nil pointer")
25
+	}
26
+	valElem := val.Elem()
27
+	if valElem.Kind() != reflect.Struct {
28
+		return nil, fmt.Errorf("expected a pointer to a struct, got a pointer to %v", valElem.Kind())
29
+	}
30
+	typ := val.Type()
31
+	m := make(map[string]interface{})
32
+	for i := 0; i < val.NumMethod(); i++ {
33
+		k, v, err := marshalForMethod(typ.Method(i), val.Method(i))
34
+		if err != nil {
35
+			return nil, err
36
+		}
37
+		if k != "" {
38
+			m[k] = v
39
+		}
40
+	}
41
+	return m, nil
42
+}
43
+
44
+var unmarshallableNames = map[string]struct{}{"FullHeader": {}}
45
+
46
+// marshalForMethod returns the map key and the map value for marshalling the method.
47
+// It returns ("", nil, nil) for valid but non-marshallable parameter. (e.g. "unexportedFunc()")
48
+func marshalForMethod(typ reflect.Method, val reflect.Value) (string, interface{}, error) {
49
+	if val.Kind() != reflect.Func {
50
+		return "", nil, fmt.Errorf("expected func, got %v", val.Kind())
51
+	}
52
+	name, numIn, numOut := typ.Name, val.Type().NumIn(), val.Type().NumOut()
53
+	_, blackListed := unmarshallableNames[name]
54
+	// FIXME: In text/template, (numOut == 2) is marshallable,
55
+	//        if the type of the second param is error.
56
+	marshallable := unicode.IsUpper(rune(name[0])) && !blackListed &&
57
+		numIn == 0 && numOut == 1
58
+	if !marshallable {
59
+		return "", nil, nil
60
+	}
61
+	result := val.Call(make([]reflect.Value, numIn))
62
+	intf := result[0].Interface()
63
+	return name, intf, nil
64
+}
0 65
new file mode 100644
... ...
@@ -0,0 +1,66 @@
0
+package formatter
1
+
2
+import (
3
+	"reflect"
4
+	"testing"
5
+)
6
+
7
+type dummy struct {
8
+}
9
+
10
+func (d *dummy) Func1() string {
11
+	return "Func1"
12
+}
13
+
14
+func (d *dummy) func2() string {
15
+	return "func2(should not be marshalled)"
16
+}
17
+
18
+func (d *dummy) Func3() (string, int) {
19
+	return "Func3(should not be marshalled)", -42
20
+}
21
+
22
+func (d *dummy) Func4() int {
23
+	return 4
24
+}
25
+
26
+type dummyType string
27
+
28
+func (d *dummy) Func5() dummyType {
29
+	return dummyType("Func5")
30
+}
31
+
32
+func (d *dummy) FullHeader() string {
33
+	return "FullHeader(should not be marshalled)"
34
+}
35
+
36
+var dummyExpected = map[string]interface{}{
37
+	"Func1": "Func1",
38
+	"Func4": 4,
39
+	"Func5": dummyType("Func5"),
40
+}
41
+
42
+func TestMarshalMap(t *testing.T) {
43
+	d := dummy{}
44
+	m, err := marshalMap(&d)
45
+	if err != nil {
46
+		t.Fatal(err)
47
+	}
48
+	if !reflect.DeepEqual(dummyExpected, m) {
49
+		t.Fatalf("expected %+v, got %+v",
50
+			dummyExpected, m)
51
+	}
52
+}
53
+
54
+func TestMarshalMapBad(t *testing.T) {
55
+	if _, err := marshalMap(nil); err == nil {
56
+		t.Fatal("expected an error (argument is nil)")
57
+	}
58
+	if _, err := marshalMap(dummy{}); err == nil {
59
+		t.Fatal("expected an error (argument is non-pointer)")
60
+	}
61
+	x := 42
62
+	if _, err := marshalMap(&x); err == nil {
63
+		t.Fatal("expected an error (argument is a pointer to non-struct)")
64
+	}
65
+}
... ...
@@ -139,6 +139,10 @@ type serviceInspectContext struct {
139 139
 	subContext
140 140
 }
141 141
 
142
+func (ctx *serviceInspectContext) MarshalJSON() ([]byte, error) {
143
+	return marshalJSON(ctx)
144
+}
145
+
142 146
 func (ctx *serviceInspectContext) ID() string {
143 147
 	return ctx.Service.ID
144 148
 }
... ...
@@ -53,6 +53,10 @@ type volumeContext struct {
53 53
 	v types.Volume
54 54
 }
55 55
 
56
+func (c *volumeContext) MarshalJSON() ([]byte, error) {
57
+	return marshalJSON(c)
58
+}
59
+
56 60
 func (c *volumeContext) Name() string {
57 61
 	c.AddHeader(volumeNameHeader)
58 62
 	return c.v.Name
... ...
@@ -2,6 +2,7 @@ package formatter
2 2
 
3 3
 import (
4 4
 	"bytes"
5
+	"encoding/json"
5 6
 	"strings"
6 7
 	"testing"
7 8
 
... ...
@@ -142,3 +143,47 @@ foobar_bar
142 142
 		}
143 143
 	}
144 144
 }
145
+
146
+func TestVolumeContextWriteJSON(t *testing.T) {
147
+	volumes := []*types.Volume{
148
+		{Driver: "foo", Name: "foobar_baz"},
149
+		{Driver: "bar", Name: "foobar_bar"},
150
+	}
151
+	expectedJSONs := []map[string]interface{}{
152
+		{"Driver": "foo", "Labels": "", "Links": "N/A", "Mountpoint": "", "Name": "foobar_baz", "Scope": "", "Size": "N/A"},
153
+		{"Driver": "bar", "Labels": "", "Links": "N/A", "Mountpoint": "", "Name": "foobar_bar", "Scope": "", "Size": "N/A"},
154
+	}
155
+	out := bytes.NewBufferString("")
156
+	err := VolumeWrite(Context{Format: "{{json .}}", Output: out}, volumes)
157
+	if err != nil {
158
+		t.Fatal(err)
159
+	}
160
+	for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
161
+		t.Logf("Output: line %d: %s", i, line)
162
+		var m map[string]interface{}
163
+		if err := json.Unmarshal([]byte(line), &m); err != nil {
164
+			t.Fatal(err)
165
+		}
166
+		assert.DeepEqual(t, m, expectedJSONs[i])
167
+	}
168
+}
169
+
170
+func TestVolumeContextWriteJSONField(t *testing.T) {
171
+	volumes := []*types.Volume{
172
+		{Driver: "foo", Name: "foobar_baz"},
173
+		{Driver: "bar", Name: "foobar_bar"},
174
+	}
175
+	out := bytes.NewBufferString("")
176
+	err := VolumeWrite(Context{Format: "{{json .Name}}", Output: out}, volumes)
177
+	if err != nil {
178
+		t.Fatal(err)
179
+	}
180
+	for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
181
+		t.Logf("Output: line %d: %s", i, line)
182
+		var s string
183
+		if err := json.Unmarshal([]byte(line), &s); err != nil {
184
+			t.Fatal(err)
185
+		}
186
+		assert.Equal(t, s, volumes[i].Name)
187
+	}
188
+}
... ...
@@ -2,15 +2,17 @@ package service
2 2
 
3 3
 import (
4 4
 	"bytes"
5
+	"encoding/json"
5 6
 	"strings"
6 7
 	"testing"
7 8
 	"time"
8 9
 
9 10
 	"github.com/docker/docker/api/types/swarm"
10 11
 	"github.com/docker/docker/cli/command/formatter"
12
+	"github.com/docker/docker/pkg/testutil/assert"
11 13
 )
12 14
 
13
-func TestPrettyPrintWithNoUpdateConfig(t *testing.T) {
15
+func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) string {
14 16
 	b := new(bytes.Buffer)
15 17
 
16 18
 	endpointSpec := &swarm.EndpointSpec{
... ...
@@ -29,8 +31,8 @@ func TestPrettyPrintWithNoUpdateConfig(t *testing.T) {
29 29
 		ID: "de179gar9d0o7ltdybungplod",
30 30
 		Meta: swarm.Meta{
31 31
 			Version:   swarm.Version{Index: 315},
32
-			CreatedAt: time.Now(),
33
-			UpdatedAt: time.Now(),
32
+			CreatedAt: now,
33
+			UpdatedAt: now,
34 34
 		},
35 35
 		Spec: swarm.ServiceSpec{
36 36
 			Annotations: swarm.Annotations{
... ...
@@ -73,14 +75,14 @@ func TestPrettyPrintWithNoUpdateConfig(t *testing.T) {
73 73
 			},
74 74
 		},
75 75
 		UpdateStatus: swarm.UpdateStatus{
76
-			StartedAt:   time.Now(),
77
-			CompletedAt: time.Now(),
76
+			StartedAt:   now,
77
+			CompletedAt: now,
78 78
 		},
79 79
 	}
80 80
 
81 81
 	ctx := formatter.Context{
82 82
 		Output: b,
83
-		Format: formatter.NewServiceFormat("pretty"),
83
+		Format: format,
84 84
 	}
85 85
 
86 86
 	err := formatter.ServiceInspectWrite(ctx, []string{"de179gar9d0o7ltdybungplod"}, func(ref string) (interface{}, []byte, error) {
... ...
@@ -89,8 +91,39 @@ func TestPrettyPrintWithNoUpdateConfig(t *testing.T) {
89 89
 	if err != nil {
90 90
 		t.Fatal(err)
91 91
 	}
92
+	return b.String()
93
+}
92 94
 
93
-	if strings.Contains(b.String(), "UpdateStatus") {
95
+func TestPrettyPrintWithNoUpdateConfig(t *testing.T) {
96
+	s := formatServiceInspect(t, formatter.NewServiceFormat("pretty"), time.Now())
97
+	if strings.Contains(s, "UpdateStatus") {
94 98
 		t.Fatal("Pretty print failed before parsing UpdateStatus")
95 99
 	}
96 100
 }
101
+
102
+func TestJSONFormatWithNoUpdateConfig(t *testing.T) {
103
+	now := time.Now()
104
+	// s1: [{"ID":..}]
105
+	// s2: {"ID":..}
106
+	s1 := formatServiceInspect(t, formatter.NewServiceFormat(""), now)
107
+	t.Log("// s1")
108
+	t.Logf("%s", s1)
109
+	s2 := formatServiceInspect(t, formatter.NewServiceFormat("{{json .}}"), now)
110
+	t.Log("// s2")
111
+	t.Logf("%s", s2)
112
+	var m1Wrap []map[string]interface{}
113
+	if err := json.Unmarshal([]byte(s1), &m1Wrap); err != nil {
114
+		t.Fatal(err)
115
+	}
116
+	if len(m1Wrap) != 1 {
117
+		t.Fatalf("strange s1=%s", s1)
118
+	}
119
+	m1 := m1Wrap[0]
120
+	t.Logf("m1=%+v", m1)
121
+	var m2 map[string]interface{}
122
+	if err := json.Unmarshal([]byte(s2), &m2); err != nil {
123
+		t.Fatal(err)
124
+	}
125
+	t.Logf("m2=%+v", m2)
126
+	assert.DeepEqual(t, m2, m1)
127
+}
... ...
@@ -4,6 +4,7 @@ package assert
4 4
 import (
5 5
 	"fmt"
6 6
 	"path/filepath"
7
+	"reflect"
7 8
 	"runtime"
8 9
 	"strings"
9 10
 )
... ...
@@ -44,6 +45,14 @@ func NilError(t TestingT, err error) {
44 44
 	}
45 45
 }
46 46
 
47
+// DeepEqual compare the actual value to the expected value and fails the test if
48
+// they are not "deeply equal".
49
+func DeepEqual(t TestingT, actual, expected interface{}) {
50
+	if !reflect.DeepEqual(actual, expected) {
51
+		fatal(t, "Expected '%v' (%T) got '%v' (%T)", expected, expected, actual, actual)
52
+	}
53
+}
54
+
47 55
 // Error asserts that error is not nil, and contains the expected text,
48 56
 // otherwise it fails the test.
49 57
 func Error(t TestingT, err error, contains string) {