blob: c3cc65c85b7d025dd8e102d03c0771dd3aebbe65 [file] [log] [blame]
package dockerimg
import (
"cmp"
"context"
"flag"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"testing/fstest"
gcmp "github.com/google/go-cmp/cmp"
"sketch.dev/httprr"
"sketch.dev/llm/ant"
)
var flagRewriteWant = flag.Bool("rewritewant", false, "rewrite the dockerfiles we want from the model")
func TestCreateDockerfile(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
fsys fs.FS
}{
{
name: "Basic repo with README",
fsys: fstest.MapFS{
"README.md": &fstest.MapFile{Data: []byte("# Test Project\nA Go project for testing.")},
},
},
{
// TODO: this looks bogus.
name: "Repo with README and workflow",
fsys: fstest.MapFS{
"README.md": &fstest.MapFile{Data: []byte("# Test Project\nA Go project for testing.")},
".github/workflows/test.yml": &fstest.MapFile{Data: []byte(`name: Test
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install and activate corepack
run: |
npm install -g corepack
corepack enable
- run: go test ./...`)},
},
},
{
name: "mention a devtool in the readme",
fsys: fstest.MapFS{
"readme.md": &fstest.MapFile{Data: []byte("# Test Project\nYou must install `dot` to run the tests.")},
},
},
{
name: "empty repo",
fsys: fstest.MapFS{
"main.go": &fstest.MapFile{Data: []byte("package main\n\nfunc main() {}")},
},
},
{
name: "python misery",
fsys: fstest.MapFS{
"README.md": &fstest.MapFile{Data: []byte("# Our amazing repo\n\nTo use this project you need python 3.11 and the dvc tool")},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
basePath := "testdata/" + strings.ToLower(strings.Replace(t.Name(), "/", "_", -1))
rrPath := basePath + ".httprr"
rr, err := httprr.Open(rrPath, http.DefaultTransport)
if err != nil && !os.IsNotExist(err) {
t.Fatal(err)
}
rr.ScrubReq(func(req *http.Request) error {
req.Header.Del("x-api-key")
return nil
})
initFiles, err := readInitFiles(tt.fsys)
if err != nil {
t.Fatal(err)
}
apiKey := cmp.Or(os.Getenv("OUTER_SKETCH_MODEL_API_KEY"), os.Getenv("ANTHROPIC_API_KEY"))
srv := &ant.Service{
APIKey: apiKey,
HTTPC: rr.Client(),
}
result, err := createDockerfile(ctx, srv, initFiles, "", false)
if err != nil {
t.Fatal(err)
}
wantPath := basePath + ".dockerfile"
if *flagRewriteWant {
if err := os.WriteFile(wantPath, []byte(result), 0o666); err != nil {
t.Fatal(err)
}
return
}
wantBytes, err := os.ReadFile(wantPath)
if err != nil {
t.Fatal(err)
}
want := string(wantBytes)
if diff := gcmp.Diff(want, result); diff != "" {
t.Errorf("dockerfile does not match. got:\n----\n%s\n----\n\ndiff: %s", result, diff)
}
})
}
}
func TestReadInitFiles(t *testing.T) {
testFS := fstest.MapFS{
"README.md": &fstest.MapFile{Data: []byte("# Test Repo")},
".github/workflows/test.yml": &fstest.MapFile{Data: []byte("name: Test Workflow")},
"main.go": &fstest.MapFile{Data: []byte("package main")},
".git/HEAD": &fstest.MapFile{Data: []byte("ref: refs/heads/main")},
"random/README.md": &fstest.MapFile{Data: []byte("ignore me")},
}
files, err := readInitFiles(testFS)
if err != nil {
t.Fatalf("readInitFiles failed: %v", err)
}
// Should have 2 files: README.md and .github/workflows/test.yml
if len(files) != 2 {
t.Errorf("Expected 2 files, got %d", len(files))
}
if content, ok := files["README.md"]; !ok {
t.Error("README.md not found")
} else if content != "# Test Repo" {
t.Errorf("README.md has incorrect content: %q", content)
}
if content, ok := files[".github/workflows/test.yml"]; !ok {
t.Error(".github/workflows/test.yml not found")
} else if content != "name: Test Workflow" {
t.Errorf("Workflow file has incorrect content: %q", content)
}
if _, ok := files["main.go"]; ok {
t.Error("main.go should not be included")
}
if _, ok := files[".git/HEAD"]; ok {
t.Error(".git/HEAD should not be included")
}
}
func TestReadInitFilesWithSubdir(t *testing.T) {
// Create a file system with files in a subdirectory
testFS := fstest.MapFS{
"subdir/README.md": &fstest.MapFile{Data: []byte("# Test Repo")},
"subdir/.github/workflows/test.yml": &fstest.MapFile{Data: []byte("name: Test Workflow")},
"subdir/main.go": &fstest.MapFile{Data: []byte("package main")},
}
// Use fs.Sub to get a sub-filesystem
subFS, err := fs.Sub(testFS, "subdir")
if err != nil {
t.Fatalf("fs.Sub failed: %v", err)
}
files, err := readInitFiles(subFS)
if err != nil {
t.Fatalf("readInitFiles failed: %v", err)
}
// Should have 2 files: README.md and .github/workflows/test.yml
if len(files) != 2 {
t.Errorf("Expected 2 files, got %d", len(files))
}
// Verify README.md was found
if content, ok := files["README.md"]; !ok {
t.Error("README.md not found")
} else if content != "# Test Repo" {
t.Errorf("README.md has incorrect content: %q", content)
}
// Verify workflow file was found
if content, ok := files[".github/workflows/test.yml"]; !ok {
t.Error(".github/workflows/test.yml not found")
} else if content != "name: Test Workflow" {
t.Errorf("Workflow file has incorrect content: %q", content)
}
}
// TestDockerHashIsPushed ensures that any changes made to the
// dockerfile template have been pushed to the default image.
func TestDockerHashIsPushed(t *testing.T) {
name, _, tag := DefaultImage()
if err := checkTagExists(tag); err != nil {
if strings.Contains(err.Error(), "not found") {
t.Fatalf(`Currently released docker image %s does not match dockerfileCustomTmpl.
Inspecting the docker image shows the current hash of dockerfileBase is %s,
but it is not published in the GitHub container registry.
This means the template constants in createdockerfile.go have been
edited (e.g. dockerfileBase changed), but a new version
of the public default docker image has not been built and pushed.
To do so:
go run ./dockerimg/pushdockerimg.go
`, name, tag)
} else {
t.Fatalf("checkTagExists: %v", err)
}
}
}
func TestGetHostGoCacheDirs(t *testing.T) {
ctx := context.Background()
// Test getHostGoCacheDir
goCacheDir, err := getHostGoCacheDir(ctx)
if err != nil {
t.Fatalf("getHostGoCacheDir failed: %v", err)
}
if goCacheDir == "" {
t.Fatal("getHostGoCacheDir returned empty string")
}
t.Logf("GOCACHE: %s", goCacheDir)
// Test getHostGoModCacheDir
goModCacheDir, err := getHostGoModCacheDir(ctx)
if err != nil {
t.Fatalf("getHostGoModCacheDir failed: %v", err)
}
if goModCacheDir == "" {
t.Fatal("getHostGoModCacheDir returned empty string")
}
t.Logf("GOMODCACHE: %s", goModCacheDir)
// Both should be absolute paths
if !filepath.IsAbs(goCacheDir) {
t.Errorf("GOCACHE is not an absolute path: %s", goCacheDir)
}
if !filepath.IsAbs(goModCacheDir) {
t.Errorf("GOMODCACHE is not an absolute path: %s", goModCacheDir)
}
}
func TestPrioritizeDockerfiles(t *testing.T) {
tests := []struct {
name string
candidates []string
want string
}{
{
name: "empty list",
candidates: []string{},
want: "",
},
{
name: "single dockerfile",
candidates: []string{"/path/to/Dockerfile"},
want: "/path/to/Dockerfile",
},
{
name: "dockerfile.sketch preferred over dockerfile",
candidates: []string{"/path/to/Dockerfile", "/path/to/Dockerfile.sketch"},
want: "/path/to/Dockerfile.sketch",
},
{
name: "dockerfile.sketch preferred regardless of order",
candidates: []string{"/path/to/Dockerfile.dev", "/path/to/Dockerfile", "/path/to/Dockerfile.sketch"},
want: "/path/to/Dockerfile.sketch",
},
{
name: "dockerfile preferred over other variations",
candidates: []string{"/path/to/Dockerfile.dev", "/path/to/Dockerfile", "/path/to/Dockerfile.prod"},
want: "/path/to/Dockerfile",
},
{
name: "case insensitive dockerfile.sketch",
candidates: []string{"/path/to/dockerfile", "/path/to/dockerfile.sketch"},
want: "/path/to/dockerfile.sketch",
},
{
name: "case insensitive dockerfile",
candidates: []string{"/path/to/dockerfile.dev", "/path/to/dockerfile"},
want: "/path/to/dockerfile",
},
{
name: "first candidate when no priority matches",
candidates: []string{"/path/to/Dockerfile.dev", "/path/to/Dockerfile.prod"},
want: "/path/to/Dockerfile.dev",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := prioritizeDockerfiles(tt.candidates)
if got != tt.want {
t.Errorf("prioritizeDockerfiles() = %v, want %v", got, tt.want)
}
})
}
}
func TestFindDirDockerfilesIntegration(t *testing.T) {
// Create a temporary directory structure for testing
tmpDir, err := os.MkdirTemp("", "dockerfiles-test-*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
// Create test files
testFiles := []string{
"Dockerfile",
"Dockerfile.sketch",
"Dockerfile.dev",
"dockerfile.prod", // lowercase
"README.md", // should be ignored
}
for _, file := range testFiles {
path := filepath.Join(tmpDir, file)
if err := os.WriteFile(path, []byte("# test"), 0o644); err != nil {
t.Fatal(err)
}
}
// Test findDirDockerfiles
candidates, err := findDirDockerfiles(tmpDir)
if err != nil {
t.Fatal(err)
}
// Should find all Dockerfile* files but not README.md
expectedCount := 4
if len(candidates) != expectedCount {
t.Errorf("findDirDockerfiles() found %d files, want %d", len(candidates), expectedCount)
}
// Test prioritization
prioritized := prioritizeDockerfiles(candidates)
expectedPriority := filepath.Join(tmpDir, "Dockerfile.sketch")
if prioritized != expectedPriority {
t.Errorf("prioritizeDockerfiles() = %v, want %v", prioritized, expectedPriority)
}
// Test with only Dockerfile (no Dockerfile.sketch)
os.Remove(filepath.Join(tmpDir, "Dockerfile.sketch"))
candidates, err = findDirDockerfiles(tmpDir)
if err != nil {
t.Fatal(err)
}
prioritized = prioritizeDockerfiles(candidates)
expectedPriority = filepath.Join(tmpDir, "Dockerfile")
if prioritized != expectedPriority {
t.Errorf("prioritizeDockerfiles() without sketch = %v, want %v", prioritized, expectedPriority)
}
}
func TestFindRepoDockerfiles(t *testing.T) {
// Create a temporary directory structure that simulates a git repo
tmpDir, err := os.MkdirTemp("", "repo-test-*")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir)
// Create subdirectories
subDir := filepath.Join(tmpDir, "subdir")
if err := os.MkdirAll(subDir, 0o755); err != nil {
t.Fatal(err)
}
// Create Dockerfile in subdirectory
subDockerfile := filepath.Join(subDir, "Dockerfile")
if err := os.WriteFile(subDockerfile, []byte("FROM ubuntu"), 0o644); err != nil {
t.Fatal(err)
}
// Create Dockerfile.sketch in parent directory
rootSketchDockerfile := filepath.Join(tmpDir, "Dockerfile.sketch")
if err := os.WriteFile(rootSketchDockerfile, []byte("FROM alpine"), 0o644); err != nil {
t.Fatal(err)
}
// Test: when called from subdirectory, should find subdirectory Dockerfile first
candidates, err := findRepoDockerfiles(subDir, tmpDir)
if err != nil {
t.Fatal(err)
}
if len(candidates) == 0 {
t.Fatal("expected to find at least one dockerfile")
}
// Should find the Dockerfile in the subdirectory
if len(candidates) != 1 || candidates[0] != subDockerfile {
t.Errorf("expected to find %s, but got %v", subDockerfile, candidates)
}
// Test: when called from root with no subdirectory dockerfile, should find root dockerfile
os.Remove(subDockerfile) // Remove subdirectory dockerfile
candidates, err = findRepoDockerfiles(tmpDir, tmpDir)
if err != nil {
t.Fatal(err)
}
if len(candidates) != 1 || candidates[0] != rootSketchDockerfile {
t.Errorf("expected to find %s, but got %v", rootSketchDockerfile, candidates)
}
// Test prioritization: create both Dockerfile and Dockerfile.sketch in same directory
rootDockerfile := filepath.Join(tmpDir, "Dockerfile")
if err := os.WriteFile(rootDockerfile, []byte("FROM debian"), 0o644); err != nil {
t.Fatal(err)
}
candidates, err = findRepoDockerfiles(tmpDir, tmpDir)
if err != nil {
t.Fatal(err)
}
if len(candidates) != 2 {
t.Errorf("expected to find 2 dockerfiles, but got %d", len(candidates))
}
// Test that prioritization works correctly
prioritized := prioritizeDockerfiles(candidates)
if prioritized != rootSketchDockerfile {
t.Errorf("expected Dockerfile.sketch to be prioritized, but got %s", prioritized)
}
}
// TestDockerfileSketchImageReuse tests that Docker images built from Dockerfile.sketch
// are properly reused when the Dockerfile content hasn't changed.
func TestDockerfileSketchImageReuse(t *testing.T) {
// Create a temporary directory for the test repo
tmpDir := t.TempDir()
// Create a simple Dockerfile.sketch
dockerfileContent := `FROM ubuntu:24.04
LABEL test=true
CMD ["echo", "hello"]
`
dockerfilePath := filepath.Join(tmpDir, "Dockerfile.sketch")
err := os.WriteFile(dockerfilePath, []byte(dockerfileContent), 0o644)
if err != nil {
t.Fatalf("Failed to write Dockerfile.sketch: %v", err)
}
// Test that hashInitFiles produces consistent results
initFiles := map[string]string{
dockerfilePath: dockerfileContent,
}
hash1 := hashInitFiles(initFiles)
hash2 := hashInitFiles(initFiles)
if hash1 != hash2 {
t.Errorf("hashInitFiles should be deterministic, got %s and %s", hash1, hash2)
}
// Test that hash changes when content changes
modifiedFiles := map[string]string{
dockerfilePath: dockerfileContent + "RUN echo modified\n",
}
hash3 := hashInitFiles(modifiedFiles)
if hash1 == hash3 {
t.Errorf("hashInitFiles should produce different hashes for different content, got %s for both", hash1)
}
t.Logf("Original hash: %s", hash1)
t.Logf("Modified hash: %s", hash3)
}
// TestPrioritizeDockerfiles tests the Dockerfile prioritization logic
func TestDockerfileSketchPriority(t *testing.T) {
tests := []struct {
name string
candidates []string
expected string
}{
{
name: "dockerfile.sketch_wins_over_dockerfile",
candidates: []string{"/path/Dockerfile", "/path/Dockerfile.sketch"},
expected: "/path/Dockerfile.sketch",
},
{
name: "dockerfile.sketch_case_insensitive",
candidates: []string{"/path/dockerfile", "/path/dockerfile.sketch"},
expected: "/path/dockerfile.sketch",
},
{
name: "dockerfile_wins_over_variations",
candidates: []string{"/path/Dockerfile.dev", "/path/Dockerfile"},
expected: "/path/Dockerfile",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := prioritizeDockerfiles(tt.candidates)
if result != tt.expected {
t.Errorf("prioritizeDockerfiles(%v) = %s, want %s", tt.candidates, result, tt.expected)
}
})
}
}