Skip to content

Commit 145c530

Browse files
committed
Refactor for unit testing
1 parent 75daed8 commit 145c530

2 files changed

Lines changed: 294 additions & 4 deletions

File tree

main.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func main() {
3333
Action: func(c *cli.Context) error {
3434
cfg := &config{
3535
ref: c.Args().First(),
36+
out: os.Stdout,
3637
}
3738
if err := inspect(cfg); err != nil {
3839
return cli.Exit(c.Command.Name+": "+err.Error(), 1)
@@ -48,6 +49,7 @@ func main() {
4849
ref: c.Args().First(),
4950
layerIDs: c.Args().Tail(),
5051
sort: c.Bool("sort"),
52+
out: os.Stdout,
5153
}
5254

5355
if err := ls(cfg); err != nil {
@@ -71,6 +73,7 @@ func main() {
7173
ref: c.Args().First(),
7274
files: c.Args().Tail(),
7375
outputDir: c.String("output_dir"),
76+
out: os.Stdout,
7477
}
7578
if err := extract(cfg); err != nil {
7679
return cli.Exit(c.Command.Name+": "+err.Error(), 1)
@@ -102,6 +105,8 @@ type config struct {
102105
files []string
103106
// outputDir is the directory to write extracted files to.
104107
outputDir string
108+
// out is the writer for output.
109+
out io.Writer
105110
}
106111

107112
// makeOptions returns the options for crane.
@@ -175,13 +180,17 @@ func inspect(cfg *config) error {
175180
if err != nil {
176181
return err
177182
}
183+
return inspectImage(cfg, image)
184+
}
178185

186+
// inspectImage prints info about the layers of an image.
187+
func inspectImage(cfg *config, image v1.Image) error {
179188
layers, err := image.Layers()
180189
if err != nil {
181190
return err
182191
}
183192

184-
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
193+
tw := tabwriter.NewWriter(cfg.out, 0, 0, 2, ' ', 0)
185194
tw.Write([]byte("N\tLayer\tSize\n"))
186195
for i, layer := range layers {
187196
hash, err := layer.DiffID()
@@ -205,7 +214,11 @@ func ls(cfg *config) error {
205214
if err != nil {
206215
return err
207216
}
217+
return lsImage(cfg, image)
218+
}
208219

220+
// lsImage lists files in the layers of an image.
221+
func lsImage(cfg *config, image v1.Image) error {
209222
layers, err := image.Layers()
210223
if err != nil {
211224
return fmt.Errorf("getting layers: %w", err)
@@ -264,8 +277,8 @@ func files(cfg *config, layer v1.Layer) error {
264277

265278
tarReader := tar.NewReader(uncompressed)
266279

267-
fmt.Printf("\n--- %s ---\n", hash)
268-
tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
280+
fmt.Fprintf(cfg.out, "\n--- %s ---\n", hash)
281+
tw := tabwriter.NewWriter(cfg.out, 0, 0, 2, ' ', 0)
269282
tw.Write([]byte("Mode\tSize\tName\n"))
270283

271284
headers := make([]*tar.Header, 0)
@@ -310,6 +323,11 @@ func extract(cfg *config) error {
310323
return err
311324
}
312325

326+
return extractFromImage(cfg, image)
327+
}
328+
329+
// extractFromImage extracts files from the given image.
330+
func extractFromImage(cfg *config, image v1.Image) error {
313331
layers, err := image.Layers()
314332
if err != nil {
315333
return fmt.Errorf("getting layers: %w", err)
@@ -384,7 +402,7 @@ func extract(cfg *config) error {
384402
// extractFile writes the contents of a tar entry to stdout or to a file under outputDir.
385403
func extractFile(cfg *config, name string, r io.Reader) error {
386404
if cfg.outputDir == "" {
387-
_, err := io.Copy(os.Stdout, r)
405+
_, err := io.Copy(cfg.out, r)
388406
return err
389407
}
390408

main_test.go

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
package main
2+
3+
import (
4+
"archive/tar"
5+
"bytes"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
"testing"
10+
11+
v1 "github.com/google/go-containerregistry/pkg/v1"
12+
"github.com/google/go-containerregistry/pkg/v1/empty"
13+
"github.com/google/go-containerregistry/pkg/v1/mutate"
14+
"github.com/google/go-containerregistry/pkg/v1/tarball"
15+
)
16+
17+
// createTestLayer creates a v1.Layer from a map of filename to content.
18+
func createTestLayer(t *testing.T, fileContents map[string]string) v1.Layer {
19+
t.Helper()
20+
var buf bytes.Buffer
21+
tw := tar.NewWriter(&buf)
22+
for name, content := range fileContents {
23+
if err := tw.WriteHeader(&tar.Header{
24+
Name: name,
25+
Mode: 0644,
26+
Size: int64(len(content)),
27+
Typeflag: tar.TypeReg,
28+
}); err != nil {
29+
t.Fatal(err)
30+
}
31+
if _, err := tw.Write([]byte(content)); err != nil {
32+
t.Fatal(err)
33+
}
34+
}
35+
if err := tw.Close(); err != nil {
36+
t.Fatal(err)
37+
}
38+
layer, err := tarball.LayerFromReader(&buf)
39+
if err != nil {
40+
t.Fatal(err)
41+
}
42+
return layer
43+
}
44+
45+
// createTestImage creates a v1.Image with the given layers.
46+
func createTestImage(t *testing.T, layers ...v1.Layer) v1.Image {
47+
t.Helper()
48+
img, err := mutate.AppendLayers(empty.Image, layers...)
49+
if err != nil {
50+
t.Fatal(err)
51+
}
52+
return img
53+
}
54+
55+
func TestInspectImage(t *testing.T) {
56+
layer1 := createTestLayer(t, map[string]string{"a.txt": "hello"})
57+
layer2 := createTestLayer(t, map[string]string{"b.txt": "world"})
58+
img := createTestImage(t, layer1, layer2)
59+
60+
var buf bytes.Buffer
61+
cfg := &config{out: &buf}
62+
63+
if err := inspectImage(cfg, img); err != nil {
64+
t.Fatal(err)
65+
}
66+
67+
output := buf.String()
68+
lines := strings.Split(strings.TrimSpace(output), "\n")
69+
// Header + 2 layer lines
70+
if len(lines) != 3 {
71+
t.Fatalf("expected 3 lines, got %d:\n%s", len(lines), output)
72+
}
73+
if !strings.Contains(lines[0], "N") || !strings.Contains(lines[0], "Layer") || !strings.Contains(lines[0], "Size") {
74+
t.Errorf("unexpected header: %s", lines[0])
75+
}
76+
if !strings.HasPrefix(strings.TrimSpace(lines[1]), "1") {
77+
t.Errorf("expected line 1 to start with '1': %s", lines[1])
78+
}
79+
if !strings.HasPrefix(strings.TrimSpace(lines[2]), "2") {
80+
t.Errorf("expected line 2 to start with '2': %s", lines[2])
81+
}
82+
}
83+
84+
func TestFiles(t *testing.T) {
85+
layer := createTestLayer(t, map[string]string{
86+
"app/main.go": "package main",
87+
"app/utils.go": "package utils",
88+
"README.md": "# Project",
89+
})
90+
91+
var buf bytes.Buffer
92+
cfg := &config{out: &buf}
93+
94+
if err := files(cfg, layer); err != nil {
95+
t.Fatal(err)
96+
}
97+
98+
output := buf.String()
99+
for _, name := range []string{"app/main.go", "app/utils.go", "README.md"} {
100+
if !strings.Contains(output, name) {
101+
t.Errorf("expected %q in output:\n%s", name, output)
102+
}
103+
}
104+
if !strings.Contains(output, "Mode") || !strings.Contains(output, "Size") || !strings.Contains(output, "Name") {
105+
t.Errorf("expected header in output:\n%s", output)
106+
}
107+
}
108+
109+
func TestFilesSorted(t *testing.T) {
110+
layer := createTestLayer(t, map[string]string{
111+
"small.txt": "x",
112+
"medium.txt": "xxxx",
113+
"large.txt": strings.Repeat("x", 100),
114+
})
115+
116+
var buf bytes.Buffer
117+
cfg := &config{out: &buf, sort: true}
118+
119+
if err := files(cfg, layer); err != nil {
120+
t.Fatal(err)
121+
}
122+
123+
output := buf.String()
124+
largeIdx := strings.Index(output, "large.txt")
125+
mediumIdx := strings.Index(output, "medium.txt")
126+
smallIdx := strings.Index(output, "small.txt")
127+
128+
if largeIdx < 0 || mediumIdx < 0 || smallIdx < 0 {
129+
t.Fatalf("missing files in output:\n%s", output)
130+
}
131+
if !(largeIdx < mediumIdx && mediumIdx < smallIdx) {
132+
t.Errorf("files not sorted by size (large=%d, medium=%d, small=%d):\n%s",
133+
largeIdx, mediumIdx, smallIdx, output)
134+
}
135+
}
136+
137+
func TestExtractToStdout(t *testing.T) {
138+
layer := createTestLayer(t, map[string]string{
139+
"hello.txt": "hello world",
140+
})
141+
img := createTestImage(t, layer)
142+
143+
var buf bytes.Buffer
144+
cfg := &config{
145+
files: []string{"hello.txt"},
146+
out: &buf,
147+
}
148+
149+
if err := extractFromImage(cfg, img); err != nil {
150+
t.Fatal(err)
151+
}
152+
153+
if got := buf.String(); got != "hello world" {
154+
t.Errorf("expected %q, got %q", "hello world", got)
155+
}
156+
}
157+
158+
func TestExtractLastLayerWins(t *testing.T) {
159+
layer1 := createTestLayer(t, map[string]string{
160+
"config.json": `{"version": 1}`,
161+
})
162+
layer2 := createTestLayer(t, map[string]string{
163+
"config.json": `{"version": 2}`,
164+
})
165+
img := createTestImage(t, layer1, layer2)
166+
167+
var buf bytes.Buffer
168+
cfg := &config{
169+
files: []string{"config.json"},
170+
out: &buf,
171+
}
172+
173+
if err := extractFromImage(cfg, img); err != nil {
174+
t.Fatal(err)
175+
}
176+
177+
if got := buf.String(); got != `{"version": 2}` {
178+
t.Errorf("expected version 2 from last layer, got %q", got)
179+
}
180+
}
181+
182+
func TestExtractFileNotFound(t *testing.T) {
183+
layer := createTestLayer(t, map[string]string{
184+
"exists.txt": "content",
185+
})
186+
img := createTestImage(t, layer)
187+
188+
var buf bytes.Buffer
189+
cfg := &config{
190+
files: []string{"nonexistent.txt"},
191+
out: &buf,
192+
}
193+
194+
err := extractFromImage(cfg, img)
195+
if err == nil {
196+
t.Fatal("expected error for missing file")
197+
}
198+
if !strings.Contains(err.Error(), "not found") {
199+
t.Errorf("expected 'not found' error, got: %v", err)
200+
}
201+
}
202+
203+
func TestExtractToOutputDir(t *testing.T) {
204+
layer := createTestLayer(t, map[string]string{
205+
"app/config.yaml": "key: value",
206+
})
207+
img := createTestImage(t, layer)
208+
209+
dir := t.TempDir()
210+
cfg := &config{
211+
files: []string{"app/config.yaml"},
212+
outputDir: dir,
213+
out: &bytes.Buffer{},
214+
}
215+
216+
if err := extractFromImage(cfg, img); err != nil {
217+
t.Fatal(err)
218+
}
219+
220+
data, err := os.ReadFile(filepath.Join(dir, "app", "config.yaml"))
221+
if err != nil {
222+
t.Fatal(err)
223+
}
224+
if string(data) != "key: value" {
225+
t.Errorf("expected %q, got %q", "key: value", string(data))
226+
}
227+
}
228+
229+
func TestLsImage(t *testing.T) {
230+
layer1 := createTestLayer(t, map[string]string{"layer1.txt": "a"})
231+
layer2 := createTestLayer(t, map[string]string{"layer2.txt": "b"})
232+
img := createTestImage(t, layer1, layer2)
233+
234+
var buf bytes.Buffer
235+
cfg := &config{out: &buf}
236+
237+
if err := lsImage(cfg, img); err != nil {
238+
t.Fatal(err)
239+
}
240+
241+
output := buf.String()
242+
if !strings.Contains(output, "layer1.txt") {
243+
t.Errorf("expected layer1.txt in output:\n%s", output)
244+
}
245+
if !strings.Contains(output, "layer2.txt") {
246+
t.Errorf("expected layer2.txt in output:\n%s", output)
247+
}
248+
}
249+
250+
func TestLsImageWithLayerID(t *testing.T) {
251+
layer1 := createTestLayer(t, map[string]string{"layer1.txt": "a"})
252+
layer2 := createTestLayer(t, map[string]string{"layer2.txt": "b"})
253+
img := createTestImage(t, layer1, layer2)
254+
255+
var buf bytes.Buffer
256+
cfg := &config{
257+
layerIDs: []string{"1"},
258+
out: &buf,
259+
}
260+
261+
if err := lsImage(cfg, img); err != nil {
262+
t.Fatal(err)
263+
}
264+
265+
output := buf.String()
266+
if !strings.Contains(output, "layer1.txt") {
267+
t.Errorf("expected layer1.txt in output:\n%s", output)
268+
}
269+
if strings.Contains(output, "layer2.txt") {
270+
t.Errorf("did not expect layer2.txt for layer 1 only:\n%s", output)
271+
}
272+
}

0 commit comments

Comments
 (0)