233 lines
7.2 KiB
Go
233 lines
7.2 KiB
Go
package tests
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
tests "github.com/jfraeys/fetch_ml/tests/fixtures"
|
|
)
|
|
|
|
// TestPodmanIntegration tests podman workflow with examples
|
|
func TestPodmanIntegration(t *testing.T) {
|
|
if os.Getenv("FETCH_ML_E2E_PODMAN") != "1" {
|
|
t.Skip("Skipping PodmanIntegration (set FETCH_ML_E2E_PODMAN=1 to enable)")
|
|
}
|
|
|
|
if testing.Short() {
|
|
t.Skip("Skipping podman integration test in short mode")
|
|
}
|
|
|
|
// Check if podman is available
|
|
if _, err := exec.LookPath("podman"); err != nil {
|
|
t.Skip("Podman not available, skipping integration test")
|
|
}
|
|
|
|
// Check if podman daemon is running
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
podmanCheck := exec.CommandContext(ctx, "podman", "info")
|
|
if err := podmanCheck.Run(); err != nil {
|
|
t.Skip("Podman daemon not running, skipping integration test")
|
|
}
|
|
|
|
// Determine project root (two levels up from tests/e2e)
|
|
projectRoot, err := filepath.Abs(filepath.Join("..", ".."))
|
|
if err != nil {
|
|
t.Fatalf("Failed to resolve project root: %v", err)
|
|
}
|
|
|
|
// Test build
|
|
t.Run("BuildContainer", func(t *testing.T) {
|
|
if os.Getenv("FETCH_ML_E2E_PODMAN_REBUILD") != "1" {
|
|
// Fast path: reuse existing image.
|
|
checkCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
// `podman image exists <name>` exits 0 if present, 1 if missing.
|
|
check := exec.CommandContext(checkCtx, "podman", "image", "exists", "secure-ml-runner:test")
|
|
if err := check.Run(); err == nil {
|
|
t.Log("Podman image secure-ml-runner:test already exists; skipping rebuild")
|
|
return
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
|
defer cancel()
|
|
|
|
//nolint:gosec // G204: Subprocess launched with potential tainted input - this is a test
|
|
cmd := exec.CommandContext(ctx, "podman", "build",
|
|
"-f", filepath.Join("podman", "secure-ml-runner.podfile"),
|
|
"-t", "secure-ml-runner:test",
|
|
"podman")
|
|
|
|
cmd.Dir = projectRoot
|
|
t.Logf("Building container with command: %v", cmd)
|
|
t.Logf("Current directory: %s", cmd.Dir)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("Failed to build container: %v\nOutput: %s", err, string(output))
|
|
}
|
|
|
|
t.Logf("Container build successful")
|
|
})
|
|
|
|
// Test execution with examples
|
|
t.Run("ExecuteExample", func(t *testing.T) {
|
|
examplesDir := tests.NewExamplesDir(filepath.Join("..", "fixtures", "examples"))
|
|
|
|
type tc struct {
|
|
name string
|
|
project string
|
|
depsFile string
|
|
preparePodIn func(ctx context.Context, workspaceDir, project string) error
|
|
}
|
|
|
|
cases := []tc{
|
|
{
|
|
name: "RequirementsTxt",
|
|
project: "standard_ml_project",
|
|
depsFile: "requirements.txt",
|
|
},
|
|
{
|
|
name: "PyprojectToml",
|
|
project: "pyproject_project",
|
|
depsFile: "pyproject.toml",
|
|
},
|
|
{
|
|
name: "PoetryLock",
|
|
project: "poetry_project",
|
|
depsFile: "poetry.lock",
|
|
preparePodIn: func(ctx context.Context, workspaceDir, project string) error {
|
|
// Generate lock inside container so it matches the container's Poetry/version.
|
|
//nolint:gosec // G204: Subprocess launched with potential tainted input - this is a test
|
|
cmd := exec.CommandContext(ctx, "podman", "run", "--rm",
|
|
"--security-opt", "no-new-privileges",
|
|
"--cap-drop", "ALL",
|
|
"--memory", "2g",
|
|
"--cpus", "1",
|
|
"--userns", "keep-id",
|
|
"-v", workspaceDir+":/workspace:rw",
|
|
"-w", "/workspace/"+project,
|
|
"--entrypoint", "conda",
|
|
"secure-ml-runner:test",
|
|
"run", "-n", "ml_env", "poetry", "lock",
|
|
)
|
|
cmd.Dir = ".."
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate poetry.lock: %v\nOutput: %s", err, string(out))
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
c := c
|
|
t.Run(c.name, func(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
workspaceDir := filepath.Join(tempDir, "workspace")
|
|
resultsDir := filepath.Join(tempDir, "results")
|
|
|
|
if err := os.MkdirAll(workspaceDir, 0750); err != nil {
|
|
t.Fatalf("Failed to create workspace directory: %v", err)
|
|
}
|
|
if err := os.MkdirAll(resultsDir, 0750); err != nil {
|
|
t.Fatalf("Failed to create results directory: %v", err)
|
|
}
|
|
|
|
dstDir := filepath.Join(workspaceDir, c.project)
|
|
if err := examplesDir.CopyProject(c.project, dstDir); err != nil {
|
|
t.Fatalf("Failed to copy example project: %v (dst: %s)", err, dstDir)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
|
defer cancel()
|
|
|
|
if c.preparePodIn != nil {
|
|
if err := c.preparePodIn(ctx, workspaceDir, c.project); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
//nolint:gosec // G204: Subprocess launched with potential tainted input - this is a test
|
|
cmd := exec.CommandContext(ctx, "podman", "run", "--rm",
|
|
"--security-opt", "no-new-privileges",
|
|
"--cap-drop", "ALL",
|
|
"--memory", "2g",
|
|
"--cpus", "1",
|
|
"--userns", "keep-id",
|
|
"-v", workspaceDir+":/workspace:rw",
|
|
"-v", resultsDir+":/workspace/results:rw",
|
|
"secure-ml-runner:test",
|
|
"--workspace", "/workspace/"+c.project,
|
|
"--deps", "/workspace/"+c.project+"/"+c.depsFile,
|
|
"--script", "/workspace/"+c.project+"/train.py",
|
|
"--args", "--output_dir", "/workspace/results",
|
|
)
|
|
|
|
cmd.Dir = ".."
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("Failed to execute example in container: %v\nOutput: %s", err, string(output))
|
|
}
|
|
|
|
resultsFile := filepath.Join(resultsDir, "results.json")
|
|
if _, err := os.Stat(resultsFile); os.IsNotExist(err) {
|
|
t.Fatalf("Expected results.json not found in output. Container output:\n%s", string(output))
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestPodmanExamplesSync tests the sync functionality using temp directories
|
|
func TestPodmanExamplesSync(t *testing.T) {
|
|
// Use temporary directory to avoid modifying actual workspace
|
|
tempDir := t.TempDir()
|
|
tempWorkspace := filepath.Join(tempDir, "workspace")
|
|
|
|
// Use fixtures for examples directory operations
|
|
examplesDir := tests.NewExamplesDir(filepath.Join("..", "fixtures", "examples"))
|
|
|
|
// Create temporary workspace
|
|
if err := os.MkdirAll(tempWorkspace, 0750); err != nil {
|
|
t.Fatalf("Failed to create temp workspace: %v", err)
|
|
}
|
|
|
|
// Get all example projects using fixtures
|
|
projects, err := examplesDir.ListProjects()
|
|
if err != nil {
|
|
t.Fatalf("Failed to read examples directory: %v", err)
|
|
}
|
|
|
|
for _, projectName := range projects {
|
|
dstDir := filepath.Join(tempWorkspace, projectName)
|
|
|
|
t.Run("Sync_"+projectName, func(t *testing.T) {
|
|
// Remove existing destination
|
|
_ = os.RemoveAll(dstDir)
|
|
|
|
// Copy project using fixtures
|
|
if err := examplesDir.CopyProject(projectName, dstDir); err != nil {
|
|
t.Fatalf("Failed to copy %s to test workspace: %v", projectName, err)
|
|
}
|
|
|
|
// Verify copy
|
|
requiredFiles := []string{"train.py", "requirements.txt", "README.md"}
|
|
for _, file := range requiredFiles {
|
|
dstFile := filepath.Join(dstDir, file)
|
|
if _, err := os.Stat(dstFile); os.IsNotExist(err) {
|
|
t.Errorf("Missing file %s in copied project %s", file, projectName)
|
|
}
|
|
}
|
|
|
|
t.Logf("Successfully synced %s to temp workspace", projectName)
|
|
})
|
|
}
|
|
}
|