diff --git a/build/builders/docker_test.go b/build/builders/docker_test.go new file mode 100644 index 0000000..3b60f46 --- /dev/null +++ b/build/builders/docker_test.go @@ -0,0 +1,83 @@ +package builders + +import ( + "os" + "path/filepath" + "testing" + + "forge.lthn.ai/core/go-devops/build" + "forge.lthn.ai/core/go/pkg/io" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDockerBuilder_Name_Good(t *testing.T) { + builder := NewDockerBuilder() + assert.Equal(t, "docker", builder.Name()) +} + +func TestDockerBuilder_Detect_Good(t *testing.T) { + fs := io.Local + + t.Run("detects Dockerfile", func(t *testing.T) { + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "Dockerfile"), []byte("FROM alpine\n"), 0644) + require.NoError(t, err) + + builder := NewDockerBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.True(t, detected) + }) + + t.Run("returns false for empty directory", func(t *testing.T) { + dir := t.TempDir() + + builder := NewDockerBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.False(t, detected) + }) + + t.Run("returns false for non-Docker project", func(t *testing.T) { + dir := t.TempDir() + // Create a Go project instead + err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test"), 0644) + require.NoError(t, err) + + builder := NewDockerBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.False(t, detected) + }) + + t.Run("does not match docker-compose.yml", func(t *testing.T) { + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "docker-compose.yml"), []byte("version: '3'\n"), 0644) + require.NoError(t, err) + + builder := NewDockerBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.False(t, detected) + }) + + t.Run("does not match Dockerfile in subdirectory", func(t *testing.T) { + dir := t.TempDir() + subDir := filepath.Join(dir, "subdir") + require.NoError(t, os.MkdirAll(subDir, 0755)) + err := os.WriteFile(filepath.Join(subDir, "Dockerfile"), []byte("FROM alpine\n"), 0644) + require.NoError(t, err) + + builder := NewDockerBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.False(t, detected) + }) +} + +func TestDockerBuilder_Interface_Good(t *testing.T) { + // Verify DockerBuilder implements Builder interface + var _ build.Builder = (*DockerBuilder)(nil) + var _ build.Builder = NewDockerBuilder() +} diff --git a/build/builders/linuxkit_test.go b/build/builders/linuxkit_test.go new file mode 100644 index 0000000..e437872 --- /dev/null +++ b/build/builders/linuxkit_test.go @@ -0,0 +1,224 @@ +package builders + +import ( + "os" + "path/filepath" + "testing" + + "forge.lthn.ai/core/go-devops/build" + "forge.lthn.ai/core/go/pkg/io" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLinuxKitBuilder_Name_Good(t *testing.T) { + builder := NewLinuxKitBuilder() + assert.Equal(t, "linuxkit", builder.Name()) +} + +func TestLinuxKitBuilder_Detect_Good(t *testing.T) { + fs := io.Local + + t.Run("detects linuxkit.yml in root", func(t *testing.T) { + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "linuxkit.yml"), []byte("kernel:\n image: test\n"), 0644) + require.NoError(t, err) + + builder := NewLinuxKitBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.True(t, detected) + }) + + t.Run("detects .core/linuxkit/*.yml", func(t *testing.T) { + dir := t.TempDir() + lkDir := filepath.Join(dir, ".core", "linuxkit") + require.NoError(t, os.MkdirAll(lkDir, 0755)) + err := os.WriteFile(filepath.Join(lkDir, "server.yml"), []byte("kernel:\n image: test\n"), 0644) + require.NoError(t, err) + + builder := NewLinuxKitBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.True(t, detected) + }) + + t.Run("detects .core/linuxkit with multiple yml files", func(t *testing.T) { + dir := t.TempDir() + lkDir := filepath.Join(dir, ".core", "linuxkit") + require.NoError(t, os.MkdirAll(lkDir, 0755)) + err := os.WriteFile(filepath.Join(lkDir, "server.yml"), []byte("kernel:\n"), 0644) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(lkDir, "desktop.yml"), []byte("kernel:\n"), 0644) + require.NoError(t, err) + + builder := NewLinuxKitBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.True(t, detected) + }) + + t.Run("returns false for empty directory", func(t *testing.T) { + dir := t.TempDir() + + builder := NewLinuxKitBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.False(t, detected) + }) + + t.Run("returns false for non-LinuxKit project", func(t *testing.T) { + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test"), 0644) + require.NoError(t, err) + + builder := NewLinuxKitBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.False(t, detected) + }) + + t.Run("returns false for empty .core/linuxkit directory", func(t *testing.T) { + dir := t.TempDir() + lkDir := filepath.Join(dir, ".core", "linuxkit") + require.NoError(t, os.MkdirAll(lkDir, 0755)) + + builder := NewLinuxKitBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.False(t, detected) + }) + + t.Run("returns false when .core/linuxkit has only non-yml files", func(t *testing.T) { + dir := t.TempDir() + lkDir := filepath.Join(dir, ".core", "linuxkit") + require.NoError(t, os.MkdirAll(lkDir, 0755)) + err := os.WriteFile(filepath.Join(lkDir, "README.md"), []byte("# LinuxKit\n"), 0644) + require.NoError(t, err) + + builder := NewLinuxKitBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.False(t, detected) + }) + + t.Run("ignores subdirectories in .core/linuxkit", func(t *testing.T) { + dir := t.TempDir() + lkDir := filepath.Join(dir, ".core", "linuxkit") + subDir := filepath.Join(lkDir, "subdir") + require.NoError(t, os.MkdirAll(subDir, 0755)) + // Put yml in subdir only, not in lkDir itself + err := os.WriteFile(filepath.Join(subDir, "server.yml"), []byte("kernel:\n"), 0644) + require.NoError(t, err) + + builder := NewLinuxKitBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.False(t, detected) + }) +} + +func TestLinuxKitBuilder_GetFormatExtension_Good(t *testing.T) { + builder := NewLinuxKitBuilder() + + tests := []struct { + format string + expected string + }{ + {"iso", ".iso"}, + {"iso-bios", ".iso"}, + {"iso-efi", ".iso"}, + {"raw", ".raw"}, + {"raw-bios", ".raw"}, + {"raw-efi", ".raw"}, + {"qcow2", ".qcow2"}, + {"qcow2-bios", ".qcow2"}, + {"qcow2-efi", ".qcow2"}, + {"vmdk", ".vmdk"}, + {"vhd", ".vhd"}, + {"gcp", ".img.tar.gz"}, + {"aws", ".raw"}, + {"custom", ".custom"}, + } + + for _, tc := range tests { + t.Run(tc.format, func(t *testing.T) { + ext := builder.getFormatExtension(tc.format) + assert.Equal(t, tc.expected, ext) + }) + } +} + +func TestLinuxKitBuilder_GetArtifactPath_Good(t *testing.T) { + builder := NewLinuxKitBuilder() + + t.Run("constructs correct path", func(t *testing.T) { + path := builder.getArtifactPath("/dist", "server-amd64", "iso") + assert.Equal(t, "/dist/server-amd64.iso", path) + }) + + t.Run("constructs correct path for qcow2", func(t *testing.T) { + path := builder.getArtifactPath("/output/linuxkit", "server-arm64", "qcow2-bios") + assert.Equal(t, "/output/linuxkit/server-arm64.qcow2", path) + }) +} + +func TestLinuxKitBuilder_BuildLinuxKitArgs_Good(t *testing.T) { + builder := NewLinuxKitBuilder() + + t.Run("builds args for amd64 without --arch", func(t *testing.T) { + args := builder.buildLinuxKitArgs("/config.yml", "iso", "output", "/dist", "amd64") + assert.Contains(t, args, "build") + assert.Contains(t, args, "--format") + assert.Contains(t, args, "iso") + assert.Contains(t, args, "--name") + assert.Contains(t, args, "output") + assert.Contains(t, args, "--dir") + assert.Contains(t, args, "/dist") + assert.Contains(t, args, "/config.yml") + assert.NotContains(t, args, "--arch") + }) + + t.Run("builds args for arm64 with --arch", func(t *testing.T) { + args := builder.buildLinuxKitArgs("/config.yml", "qcow2", "output", "/dist", "arm64") + assert.Contains(t, args, "--arch") + assert.Contains(t, args, "arm64") + }) +} + +func TestLinuxKitBuilder_FindArtifact_Good(t *testing.T) { + fs := io.Local + builder := NewLinuxKitBuilder() + + t.Run("finds artifact with exact extension", func(t *testing.T) { + dir := t.TempDir() + artifactPath := filepath.Join(dir, "server-amd64.iso") + require.NoError(t, os.WriteFile(artifactPath, []byte("fake iso"), 0644)) + + found := builder.findArtifact(fs, dir, "server-amd64", "iso") + assert.Equal(t, artifactPath, found) + }) + + t.Run("returns empty for missing artifact", func(t *testing.T) { + dir := t.TempDir() + + found := builder.findArtifact(fs, dir, "nonexistent", "iso") + assert.Empty(t, found) + }) + + t.Run("finds artifact with alternate naming", func(t *testing.T) { + dir := t.TempDir() + // Create file matching the name prefix + known image extension + artifactPath := filepath.Join(dir, "server-amd64.qcow2") + require.NoError(t, os.WriteFile(artifactPath, []byte("fake qcow2"), 0644)) + + found := builder.findArtifact(fs, dir, "server-amd64", "qcow2") + assert.Equal(t, artifactPath, found) + }) +} + +func TestLinuxKitBuilder_Interface_Good(t *testing.T) { + // Verify LinuxKitBuilder implements Builder interface + var _ build.Builder = (*LinuxKitBuilder)(nil) + var _ build.Builder = NewLinuxKitBuilder() +} diff --git a/build/builders/taskfile_test.go b/build/builders/taskfile_test.go new file mode 100644 index 0000000..66e9e1f --- /dev/null +++ b/build/builders/taskfile_test.go @@ -0,0 +1,234 @@ +package builders + +import ( + "os" + "path/filepath" + "testing" + + "forge.lthn.ai/core/go-devops/build" + "forge.lthn.ai/core/go/pkg/io" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTaskfileBuilder_Name_Good(t *testing.T) { + builder := NewTaskfileBuilder() + assert.Equal(t, "taskfile", builder.Name()) +} + +func TestTaskfileBuilder_Detect_Good(t *testing.T) { + fs := io.Local + + t.Run("detects Taskfile.yml", func(t *testing.T) { + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "Taskfile.yml"), []byte("version: '3'\n"), 0644) + require.NoError(t, err) + + builder := NewTaskfileBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.True(t, detected) + }) + + t.Run("detects Taskfile.yaml", func(t *testing.T) { + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "Taskfile.yaml"), []byte("version: '3'\n"), 0644) + require.NoError(t, err) + + builder := NewTaskfileBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.True(t, detected) + }) + + t.Run("detects Taskfile (no extension)", func(t *testing.T) { + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "Taskfile"), []byte("version: '3'\n"), 0644) + require.NoError(t, err) + + builder := NewTaskfileBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.True(t, detected) + }) + + t.Run("detects lowercase taskfile.yml", func(t *testing.T) { + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "taskfile.yml"), []byte("version: '3'\n"), 0644) + require.NoError(t, err) + + builder := NewTaskfileBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.True(t, detected) + }) + + t.Run("detects lowercase taskfile.yaml", func(t *testing.T) { + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "taskfile.yaml"), []byte("version: '3'\n"), 0644) + require.NoError(t, err) + + builder := NewTaskfileBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.True(t, detected) + }) + + t.Run("returns false for empty directory", func(t *testing.T) { + dir := t.TempDir() + + builder := NewTaskfileBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.False(t, detected) + }) + + t.Run("returns false for non-Taskfile project", func(t *testing.T) { + dir := t.TempDir() + err := os.WriteFile(filepath.Join(dir, "Makefile"), []byte("all:\n\techo hello\n"), 0644) + require.NoError(t, err) + + builder := NewTaskfileBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.False(t, detected) + }) + + t.Run("does not match Taskfile in subdirectory", func(t *testing.T) { + dir := t.TempDir() + subDir := filepath.Join(dir, "subdir") + require.NoError(t, os.MkdirAll(subDir, 0755)) + err := os.WriteFile(filepath.Join(subDir, "Taskfile.yml"), []byte("version: '3'\n"), 0644) + require.NoError(t, err) + + builder := NewTaskfileBuilder() + detected, err := builder.Detect(fs, dir) + assert.NoError(t, err) + assert.False(t, detected) + }) +} + +func TestTaskfileBuilder_FindArtifacts_Good(t *testing.T) { + fs := io.Local + builder := NewTaskfileBuilder() + + t.Run("finds files in output directory", func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "myapp"), []byte("binary"), 0755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "myapp.tar.gz"), []byte("archive"), 0644)) + + artifacts := builder.findArtifacts(fs, dir) + assert.Len(t, artifacts, 2) + }) + + t.Run("skips hidden files", func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "myapp"), []byte("binary"), 0755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, ".hidden"), []byte("hidden"), 0644)) + + artifacts := builder.findArtifacts(fs, dir) + assert.Len(t, artifacts, 1) + assert.Contains(t, artifacts[0].Path, "myapp") + }) + + t.Run("skips CHECKSUMS.txt", func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "myapp"), []byte("binary"), 0755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "CHECKSUMS.txt"), []byte("sha256"), 0644)) + + artifacts := builder.findArtifacts(fs, dir) + assert.Len(t, artifacts, 1) + assert.Contains(t, artifacts[0].Path, "myapp") + }) + + t.Run("skips directories", func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "myapp"), []byte("binary"), 0755)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "subdir"), 0755)) + + artifacts := builder.findArtifacts(fs, dir) + assert.Len(t, artifacts, 1) + }) + + t.Run("returns empty for empty directory", func(t *testing.T) { + dir := t.TempDir() + + artifacts := builder.findArtifacts(fs, dir) + assert.Empty(t, artifacts) + }) + + t.Run("returns empty for nonexistent directory", func(t *testing.T) { + artifacts := builder.findArtifacts(fs, "/nonexistent/path") + assert.Empty(t, artifacts) + }) +} + +func TestTaskfileBuilder_FindArtifactsForTarget_Good(t *testing.T) { + fs := io.Local + builder := NewTaskfileBuilder() + + t.Run("finds artifacts in platform subdirectory", func(t *testing.T) { + dir := t.TempDir() + platformDir := filepath.Join(dir, "linux_amd64") + require.NoError(t, os.MkdirAll(platformDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(platformDir, "myapp"), []byte("binary"), 0755)) + + target := build.Target{OS: "linux", Arch: "amd64"} + artifacts := builder.findArtifactsForTarget(fs, dir, target) + assert.Len(t, artifacts, 1) + assert.Equal(t, "linux", artifacts[0].OS) + assert.Equal(t, "amd64", artifacts[0].Arch) + }) + + t.Run("finds artifacts by name pattern in root", func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "myapp-linux-amd64"), []byte("binary"), 0755)) + + target := build.Target{OS: "linux", Arch: "amd64"} + artifacts := builder.findArtifactsForTarget(fs, dir, target) + assert.NotEmpty(t, artifacts) + }) + + t.Run("returns empty when no matching artifacts", func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "myapp"), []byte("binary"), 0755)) + + target := build.Target{OS: "linux", Arch: "arm64"} + artifacts := builder.findArtifactsForTarget(fs, dir, target) + assert.Empty(t, artifacts) + }) + + t.Run("handles .app bundles on darwin", func(t *testing.T) { + dir := t.TempDir() + platformDir := filepath.Join(dir, "darwin_arm64") + appDir := filepath.Join(platformDir, "MyApp.app") + require.NoError(t, os.MkdirAll(appDir, 0755)) + + target := build.Target{OS: "darwin", Arch: "arm64"} + artifacts := builder.findArtifactsForTarget(fs, dir, target) + assert.Len(t, artifacts, 1) + assert.Contains(t, artifacts[0].Path, "MyApp.app") + }) +} + +func TestTaskfileBuilder_MatchPattern_Good(t *testing.T) { + builder := NewTaskfileBuilder() + + t.Run("matches simple glob", func(t *testing.T) { + assert.True(t, builder.matchPattern("myapp-linux-amd64", "*-linux-amd64")) + }) + + t.Run("does not match different pattern", func(t *testing.T) { + assert.False(t, builder.matchPattern("myapp-linux-amd64", "*-darwin-arm64")) + }) + + t.Run("matches wildcard", func(t *testing.T) { + assert.True(t, builder.matchPattern("test_linux_arm64.bin", "*_linux_arm64*")) + }) +} + +func TestTaskfileBuilder_Interface_Good(t *testing.T) { + // Verify TaskfileBuilder implements Builder interface + var _ build.Builder = (*TaskfileBuilder)(nil) + var _ build.Builder = NewTaskfileBuilder() +}