blob: c48e96be12ca159e5522a1cb70942123d972b688 [file] [log] [blame]
Earl Lee2e463fb2025-04-17 11:22:22 -07001package dockerimg
2
3import (
Josh Bleecher Snyder4d5e9972025-05-01 15:56:37 -07004 "cmp"
Earl Lee2e463fb2025-04-17 11:22:22 -07005 "context"
6 "flag"
7 "io/fs"
8 "net/http"
9 "os"
10 "strings"
11 "testing"
12 "testing/fstest"
13
Josh Bleecher Snyder4d5e9972025-05-01 15:56:37 -070014 gcmp "github.com/google/go-cmp/cmp"
Earl Lee2e463fb2025-04-17 11:22:22 -070015 "sketch.dev/httprr"
Josh Bleecher Snyder4f84ab72025-04-22 16:40:54 -070016 "sketch.dev/llm/ant"
Earl Lee2e463fb2025-04-17 11:22:22 -070017)
18
19var flagRewriteWant = flag.Bool("rewritewant", false, "rewrite the dockerfiles we want from the model")
20
21func TestCreateDockerfile(t *testing.T) {
22 ctx := context.Background()
23
24 tests := []struct {
25 name string
26 fsys fs.FS
27 }{
28 {
29 name: "Basic repo with README",
30 fsys: fstest.MapFS{
31 "README.md": &fstest.MapFile{Data: []byte("# Test Project\nA Go project for testing.")},
32 },
33 },
34 {
35 // TODO: this looks bogus.
36 name: "Repo with README and workflow",
37 fsys: fstest.MapFS{
38 "README.md": &fstest.MapFile{Data: []byte("# Test Project\nA Go project for testing.")},
39 ".github/workflows/test.yml": &fstest.MapFile{Data: []byte(`name: Test
40on: [push]
41jobs:
42 test:
43 runs-on: ubuntu-latest
44 steps:
45 - uses: actions/checkout@v2
46 - uses: actions/setup-node@v3
47 with:
48 node-version: '18'
49 - name: Install and activate corepack
50 run: |
51 npm install -g corepack
52 corepack enable
53 - run: go test ./...`)},
54 },
55 },
56 {
57 name: "mention a devtool in the readme",
58 fsys: fstest.MapFS{
59 "readme.md": &fstest.MapFile{Data: []byte("# Test Project\nYou must install `dot` to run the tests.")},
60 },
61 },
62 {
63 name: "empty repo",
64 fsys: fstest.MapFS{
65 "main.go": &fstest.MapFile{Data: []byte("package main\n\nfunc main() {}")},
66 },
67 },
68 {
69 name: "python misery",
70 fsys: fstest.MapFS{
71 "README.md": &fstest.MapFile{Data: []byte("# Our amazing repo\n\nTo use this project you need python 3.11 and the dvc tool")},
72 },
73 },
74 }
75
76 for _, tt := range tests {
77 t.Run(tt.name, func(t *testing.T) {
78 basePath := "testdata/" + strings.ToLower(strings.Replace(t.Name(), "/", "_", -1))
79 rrPath := basePath + ".httprr"
80 rr, err := httprr.Open(rrPath, http.DefaultTransport)
81 if err != nil && !os.IsNotExist(err) {
82 t.Fatal(err)
83 }
84 rr.ScrubReq(func(req *http.Request) error {
85 req.Header.Del("x-api-key")
86 return nil
87 })
88 initFiles, err := readInitFiles(tt.fsys)
89 if err != nil {
90 t.Fatal(err)
91 }
David Crawshaw3659d872025-05-05 17:52:23 -070092 apiKey := cmp.Or(os.Getenv("OUTER_SKETCH_MODEL_API_KEY"), os.Getenv("ANTHROPIC_API_KEY"))
Josh Bleecher Snyder4f84ab72025-04-22 16:40:54 -070093 srv := &ant.Service{
94 APIKey: apiKey,
95 HTTPC: rr.Client(),
96 }
97 result, err := createDockerfile(ctx, srv, initFiles, "")
Earl Lee2e463fb2025-04-17 11:22:22 -070098 if err != nil {
99 t.Fatal(err)
100 }
101
102 wantPath := basePath + ".dockerfile"
103
104 if *flagRewriteWant {
105 if err := os.WriteFile(wantPath, []byte(result), 0o666); err != nil {
106 t.Fatal(err)
107 }
108 return
109 }
110
111 wantBytes, err := os.ReadFile(wantPath)
112 if err != nil {
113 t.Fatal(err)
114 }
115 want := string(wantBytes)
Josh Bleecher Snyder4d5e9972025-05-01 15:56:37 -0700116 if diff := gcmp.Diff(want, result); diff != "" {
Earl Lee2e463fb2025-04-17 11:22:22 -0700117 t.Errorf("dockerfile does not match. got:\n----\n%s\n----\n\ndiff: %s", result, diff)
118 }
119 })
120 }
121}
122
123func TestReadInitFiles(t *testing.T) {
124 testFS := fstest.MapFS{
125 "README.md": &fstest.MapFile{Data: []byte("# Test Repo")},
126 ".github/workflows/test.yml": &fstest.MapFile{Data: []byte("name: Test Workflow")},
127 "main.go": &fstest.MapFile{Data: []byte("package main")},
128 ".git/HEAD": &fstest.MapFile{Data: []byte("ref: refs/heads/main")},
129 "random/README.md": &fstest.MapFile{Data: []byte("ignore me")},
130 }
131
132 files, err := readInitFiles(testFS)
133 if err != nil {
134 t.Fatalf("readInitFiles failed: %v", err)
135 }
136
137 // Should have 2 files: README.md and .github/workflows/test.yml
138 if len(files) != 2 {
139 t.Errorf("Expected 2 files, got %d", len(files))
140 }
141
142 if content, ok := files["README.md"]; !ok {
143 t.Error("README.md not found")
144 } else if content != "# Test Repo" {
145 t.Errorf("README.md has incorrect content: %q", content)
146 }
147
148 if content, ok := files[".github/workflows/test.yml"]; !ok {
149 t.Error(".github/workflows/test.yml not found")
150 } else if content != "name: Test Workflow" {
151 t.Errorf("Workflow file has incorrect content: %q", content)
152 }
153
154 if _, ok := files["main.go"]; ok {
155 t.Error("main.go should not be included")
156 }
157
158 if _, ok := files[".git/HEAD"]; ok {
159 t.Error(".git/HEAD should not be included")
160 }
161}
162
163func TestReadInitFilesWithSubdir(t *testing.T) {
164 // Create a file system with files in a subdirectory
165 testFS := fstest.MapFS{
166 "subdir/README.md": &fstest.MapFile{Data: []byte("# Test Repo")},
167 "subdir/.github/workflows/test.yml": &fstest.MapFile{Data: []byte("name: Test Workflow")},
168 "subdir/main.go": &fstest.MapFile{Data: []byte("package main")},
169 }
170
171 // Use fs.Sub to get a sub-filesystem
172 subFS, err := fs.Sub(testFS, "subdir")
173 if err != nil {
174 t.Fatalf("fs.Sub failed: %v", err)
175 }
176
177 files, err := readInitFiles(subFS)
178 if err != nil {
179 t.Fatalf("readInitFiles failed: %v", err)
180 }
181
182 // Should have 2 files: README.md and .github/workflows/test.yml
183 if len(files) != 2 {
184 t.Errorf("Expected 2 files, got %d", len(files))
185 }
186
187 // Verify README.md was found
188 if content, ok := files["README.md"]; !ok {
189 t.Error("README.md not found")
190 } else if content != "# Test Repo" {
191 t.Errorf("README.md has incorrect content: %q", content)
192 }
193
194 // Verify workflow file was found
195 if content, ok := files[".github/workflows/test.yml"]; !ok {
196 t.Error(".github/workflows/test.yml not found")
197 } else if content != "name: Test Workflow" {
198 t.Errorf("Workflow file has incorrect content: %q", content)
199 }
200}
David Crawshaw11129492025-04-25 20:41:53 -0700201
202// TestDockerHashIsPushed ensures that any changes made to the
203// dockerfile template have been pushed to the default image.
204func TestDockerHashIsPushed(t *testing.T) {
David Crawshaw2a5bd6d2025-04-30 14:29:46 -0700205 name, _, tag := DefaultImage()
David Crawshaw11129492025-04-25 20:41:53 -0700206
David Crawshaw2a5bd6d2025-04-30 14:29:46 -0700207 if err := checkTagExists(tag); err != nil {
208 if strings.Contains(err.Error(), "not found") {
209 t.Fatalf(`Currently released docker image %s does not match dockerfileCustomTmpl.
David Crawshaw11129492025-04-25 20:41:53 -0700210
David Crawshaw2a5bd6d2025-04-30 14:29:46 -0700211Inspecting the docker image shows the current hash of dockerfileBase is %s,
212but it is not published in the GitHub container registry.
David Crawshaw11129492025-04-25 20:41:53 -0700213
214This means the template constants in createdockerfile.go have been
David Crawshaw2a5bd6d2025-04-30 14:29:46 -0700215edited (e.g. dockerfileBase changed), but a new version
David Crawshaw11129492025-04-25 20:41:53 -0700216of the public default docker image has not been built and pushed.
217
218To do so:
219
220 go run ./dockerimg/pushdockerimg.go
221
David Crawshaw2a5bd6d2025-04-30 14:29:46 -0700222`, name, tag)
223 } else {
224 t.Fatalf("checkTagExists: %v", err)
225 }
David Crawshaw11129492025-04-25 20:41:53 -0700226 }
227}