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 ` 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) }) } }