blob: be5344a8653cea0477938118f1f76a719142e6ca [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)
}
}