1package formatter
2
3import (
4	"bytes"
5	"fmt"
6	"strings"
7	"testing"
8	"time"
9
10	"github.com/docker/cli/internal/test"
11	"github.com/docker/docker/api/types"
12	"github.com/docker/docker/pkg/stringid"
13	"gotest.tools/v3/assert"
14	is "gotest.tools/v3/assert/cmp"
15)
16
17func TestImageContext(t *testing.T) {
18	imageID := stringid.GenerateRandomID()
19	unix := time.Now().Unix()
20	zeroTime := int64(-62135596800)
21
22	var ctx imageContext
23	cases := []struct {
24		imageCtx imageContext
25		expValue string
26		call     func() string
27	}{
28		{
29			imageCtx: imageContext{i: types.ImageSummary{ID: imageID}, trunc: true},
30			expValue: stringid.TruncateID(imageID),
31			call:     ctx.ID,
32		},
33		{
34			imageCtx: imageContext{i: types.ImageSummary{ID: imageID}, trunc: false},
35			expValue: imageID,
36			call:     ctx.ID,
37		},
38		{
39			imageCtx: imageContext{i: types.ImageSummary{Size: 10, VirtualSize: 10}, trunc: true},
40			expValue: "10B",
41			call:     ctx.Size,
42		},
43		{
44			imageCtx: imageContext{i: types.ImageSummary{Created: unix}, trunc: true},
45			expValue: time.Unix(unix, 0).String(), call: ctx.CreatedAt,
46		},
47		// FIXME
48		// {imageContext{
49		// 	i:     types.ImageSummary{Created: unix},
50		// 	trunc: true,
51		// }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince},
52		{
53			imageCtx: imageContext{i: types.ImageSummary{}, repo: "busybox"},
54			expValue: "busybox",
55			call:     ctx.Repository,
56		},
57		{
58			imageCtx: imageContext{i: types.ImageSummary{}, tag: "latest"},
59			expValue: "latest",
60			call:     ctx.Tag,
61		},
62		{
63			imageCtx: imageContext{i: types.ImageSummary{}, digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a"},
64			expValue: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a",
65			call:     ctx.Digest,
66		},
67		{
68			imageCtx: imageContext{i: types.ImageSummary{Containers: 10}},
69			expValue: "10",
70			call:     ctx.Containers,
71		},
72		{
73			imageCtx: imageContext{i: types.ImageSummary{VirtualSize: 10000}},
74			expValue: "10kB",
75			call:     ctx.VirtualSize,
76		},
77		{
78			imageCtx: imageContext{i: types.ImageSummary{SharedSize: 10000}},
79			expValue: "10kB",
80			call:     ctx.SharedSize,
81		},
82		{
83			imageCtx: imageContext{i: types.ImageSummary{SharedSize: 5000, VirtualSize: 20000}},
84			expValue: "15kB",
85			call:     ctx.UniqueSize,
86		},
87		{
88			imageCtx: imageContext{i: types.ImageSummary{Created: zeroTime}},
89			expValue: "",
90			call:     ctx.CreatedSince,
91		},
92	}
93
94	for _, c := range cases {
95		ctx = c.imageCtx
96		v := c.call()
97		if strings.Contains(v, ",") {
98			test.CompareMultipleValues(t, v, c.expValue)
99		} else {
100			assert.Check(t, is.Equal(c.expValue, v))
101		}
102	}
103}
104
105func TestImageContextWrite(t *testing.T) {
106	unixTime := time.Now().AddDate(0, 0, -1).Unix()
107	zeroTime := int64(-62135596800)
108	expectedTime := time.Unix(unixTime, 0).String()
109	expectedZeroTime := time.Unix(zeroTime, 0).String()
110
111	cases := []struct {
112		context  ImageContext
113		expected string
114	}{
115		// Errors
116		{
117			ImageContext{
118				Context: Context{
119					Format: "{{InvalidFunction}}",
120				},
121			},
122			`Template parsing error: template: :1: function "InvalidFunction" not defined
123`,
124		},
125		{
126			ImageContext{
127				Context: Context{
128					Format: "{{nil}}",
129				},
130			},
131			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
132`,
133		},
134		// Table Format
135		{
136			ImageContext{
137				Context: Context{
138					Format: NewImageFormat("table", false, false),
139				},
140			},
141			`REPOSITORY   TAG       IMAGE ID   CREATED        SIZE
142image        tag1      imageID1   24 hours ago   0B
143image        tag2      imageID2   N/A            0B
144<none>       <none>    imageID3   24 hours ago   0B
145`,
146		},
147		{
148			ImageContext{
149				Context: Context{
150					Format: NewImageFormat("table {{.Repository}}", false, false),
151				},
152			},
153			"REPOSITORY\nimage\nimage\n<none>\n",
154		},
155		{
156			ImageContext{
157				Context: Context{
158					Format: NewImageFormat("table {{.Repository}}", false, true),
159				},
160				Digest: true,
161			},
162			`REPOSITORY   DIGEST
163image        sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
164image        <none>
165<none>       <none>
166`,
167		},
168		{
169			ImageContext{
170				Context: Context{
171					Format: NewImageFormat("table {{.Repository}}", true, false),
172				},
173			},
174			"REPOSITORY\nimage\nimage\n<none>\n",
175		},
176		{
177			ImageContext{
178				Context: Context{
179					Format: NewImageFormat("table {{.Digest}}", true, false),
180				},
181			},
182			"DIGEST\nsha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf\n<none>\n<none>\n",
183		},
184		{
185			ImageContext{
186				Context: Context{
187					Format: NewImageFormat("table", true, false),
188				},
189			},
190			"imageID1\nimageID2\nimageID3\n",
191		},
192		{
193			ImageContext{
194				Context: Context{
195					Format: NewImageFormat("table", false, true),
196				},
197				Digest: true,
198			},
199			`REPOSITORY   TAG       DIGEST                                                                    IMAGE ID   CREATED        SIZE
200image        tag1      sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf   imageID1   24 hours ago   0B
201image        tag2      <none>                                                                    imageID2   N/A            0B
202<none>       <none>    <none>                                                                    imageID3   24 hours ago   0B
203`,
204		},
205		{
206			ImageContext{
207				Context: Context{
208					Format: NewImageFormat("table", true, true),
209				},
210				Digest: true,
211			},
212			"imageID1\nimageID2\nimageID3\n",
213		},
214		// Raw Format
215		{
216			ImageContext{
217				Context: Context{
218					Format: NewImageFormat("raw", false, false),
219				},
220			},
221			fmt.Sprintf(`repository: image
222tag: tag1
223image_id: imageID1
224created_at: %s
225virtual_size: 0B
226
227repository: image
228tag: tag2
229image_id: imageID2
230created_at: %s
231virtual_size: 0B
232
233repository: <none>
234tag: <none>
235image_id: imageID3
236created_at: %s
237virtual_size: 0B
238
239`, expectedTime, expectedZeroTime, expectedTime),
240		},
241		{
242			ImageContext{
243				Context: Context{
244					Format: NewImageFormat("raw", false, true),
245				},
246				Digest: true,
247			},
248			fmt.Sprintf(`repository: image
249tag: tag1
250digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf
251image_id: imageID1
252created_at: %s
253virtual_size: 0B
254
255repository: image
256tag: tag2
257digest: <none>
258image_id: imageID2
259created_at: %s
260virtual_size: 0B
261
262repository: <none>
263tag: <none>
264digest: <none>
265image_id: imageID3
266created_at: %s
267virtual_size: 0B
268
269`, expectedTime, expectedZeroTime, expectedTime),
270		},
271		{
272			ImageContext{
273				Context: Context{
274					Format: NewImageFormat("raw", true, false),
275				},
276			},
277			`image_id: imageID1
278image_id: imageID2
279image_id: imageID3
280`,
281		},
282		// Custom Format
283		{
284			ImageContext{
285				Context: Context{
286					Format: NewImageFormat("{{.Repository}}", false, false),
287				},
288			},
289			"image\nimage\n<none>\n",
290		},
291		{
292			ImageContext{
293				Context: Context{
294					Format: NewImageFormat("{{.Repository}}", false, true),
295				},
296				Digest: true,
297			},
298			"image\nimage\n<none>\n",
299		},
300	}
301
302	images := []types.ImageSummary{
303		{ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime},
304		{ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: zeroTime},
305		{ID: "imageID3", RepoTags: []string{"<none>:<none>"}, RepoDigests: []string{"<none>@<none>"}, Created: unixTime},
306	}
307
308	for _, tc := range cases {
309		tc := tc
310		t.Run(string(tc.context.Format), func(t *testing.T) {
311			var out bytes.Buffer
312			tc.context.Output = &out
313			err := ImageWrite(tc.context, images)
314			if err != nil {
315				assert.Error(t, err, tc.expected)
316			} else {
317				assert.Equal(t, out.String(), tc.expected)
318			}
319		})
320	}
321}
322
323func TestImageContextWriteWithNoImage(t *testing.T) {
324	out := bytes.NewBufferString("")
325	images := []types.ImageSummary{}
326
327	cases := []struct {
328		context  ImageContext
329		expected string
330	}{
331		{
332			ImageContext{
333				Context: Context{
334					Format: NewImageFormat("{{.Repository}}", false, false),
335					Output: out,
336				},
337			},
338			"",
339		},
340		{
341			ImageContext{
342				Context: Context{
343					Format: NewImageFormat("table {{.Repository}}", false, false),
344					Output: out,
345				},
346			},
347			"REPOSITORY\n",
348		},
349		{
350			ImageContext{
351				Context: Context{
352					Format: NewImageFormat("{{.Repository}}", false, true),
353					Output: out,
354				},
355			},
356			"",
357		},
358		{
359			ImageContext{
360				Context: Context{
361					Format: NewImageFormat("table {{.Repository}}", false, true),
362					Output: out,
363				},
364			},
365			"REPOSITORY   DIGEST\n",
366		},
367	}
368
369	for _, tc := range cases {
370		tc := tc
371		t.Run(string(tc.context.Format), func(t *testing.T) {
372			err := ImageWrite(tc.context, images)
373			assert.NilError(t, err)
374			assert.Equal(t, out.String(), tc.expected)
375			// Clean buffer
376			out.Reset()
377		})
378	}
379}
380