mirror of
https://github.com/hanzoai/pack.git
synced 2026-09-30 23:43:13 +00:00
200 lines
5.4 KiB
Go
200 lines
5.4 KiB
Go
package integration_tests
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/railwayapp/railpack/buildkit"
|
|
"github.com/railwayapp/railpack/core"
|
|
"github.com/railwayapp/railpack/core/app"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var buildkitCacheImport = flag.String("buildkit-cache-import", "", "BuildKit cache import configuration")
|
|
var buildkitCacheExport = flag.String("buildkit-cache-export", "", "BuildKit cache export configuration")
|
|
|
|
type TestCase struct {
|
|
ExpectedOutput string `json:"expectedOutput"`
|
|
Envs map[string]string `json:"envs"`
|
|
ConfigFilePath string `json:"configFile"`
|
|
JustBuild bool `json:"justBuild"`
|
|
}
|
|
|
|
func TestExamplesIntegration(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test")
|
|
}
|
|
|
|
wd, err := os.Getwd()
|
|
require.NoError(t, err)
|
|
|
|
examplesDir := filepath.Join(filepath.Dir(wd), "examples")
|
|
entries, err := os.ReadDir(examplesDir)
|
|
require.NoError(t, err)
|
|
|
|
for _, entry := range entries {
|
|
entry := entry // capture for parallel execution
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
|
|
testConfigPath := filepath.Join(examplesDir, entry.Name(), "test.json")
|
|
if _, err := os.Stat(testConfigPath); os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
|
|
testConfigBytes, err := os.ReadFile(testConfigPath)
|
|
require.NoError(t, err)
|
|
|
|
var testCases []TestCase
|
|
err = json.Unmarshal(testConfigBytes, &testCases)
|
|
require.NoError(t, err)
|
|
|
|
for i, testCase := range testCases {
|
|
testCase := testCase // capture for parallel execution
|
|
i := i
|
|
|
|
testName := fmt.Sprintf("%s/case-%d", entry.Name(), i)
|
|
t.Run(testName, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
examplePath := filepath.Join(examplesDir, entry.Name())
|
|
userApp, err := app.NewApp(examplePath)
|
|
if err != nil {
|
|
t.Fatalf("failed to create app: %v", err)
|
|
}
|
|
|
|
env := app.NewEnvironment(&testCase.Envs)
|
|
buildResult := core.GenerateBuildPlan(userApp, env, &core.GenerateBuildPlanOptions{
|
|
ConfigFilePath: testCase.ConfigFilePath,
|
|
})
|
|
if !buildResult.Success {
|
|
t.Fatalf("failed to generate build plan: %v", buildResult.Logs)
|
|
}
|
|
if buildResult == nil {
|
|
t.Fatal("build result is nil")
|
|
}
|
|
|
|
imageName := fmt.Sprintf("railpack-test-%s-%s",
|
|
strings.ToLower(strings.ReplaceAll(testName, "/", "-")),
|
|
strings.ToLower(uuid.New().String()))
|
|
|
|
if err := buildkit.BuildWithBuildkitClient(examplePath, buildResult.Plan, buildkit.BuildWithBuildkitClientOptions{
|
|
ImageName: imageName,
|
|
ImportCache: *buildkitCacheImport,
|
|
ExportCache: *buildkitCacheExport,
|
|
Secrets: testCase.Envs,
|
|
}); err != nil {
|
|
t.Fatalf("failed to build image: %v", err)
|
|
}
|
|
|
|
if testCase.JustBuild {
|
|
return
|
|
}
|
|
|
|
if err := runContainerWithTimeout(t, imageName, testCase.ExpectedOutput, testCase.Envs); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func cmdDoneChan(cmd *exec.Cmd) chan error {
|
|
ch := make(chan error, 1)
|
|
go func() { ch <- cmd.Wait() }()
|
|
return ch
|
|
}
|
|
|
|
func runContainerWithTimeout(t *testing.T, imageName, expectedOutput string, envs map[string]string) error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
defer cancel()
|
|
|
|
// Generate a unique container name so we can reference it later for cleanup
|
|
containerName := fmt.Sprintf("railpack-test-%s", uuid.New().String())
|
|
|
|
// Build docker run command with environment variables
|
|
args := []string{"run", "--rm", "--name", containerName}
|
|
for key, value := range envs {
|
|
args = append(args, "-e", fmt.Sprintf("%s=%s", key, value))
|
|
}
|
|
args = append(args, imageName)
|
|
|
|
cmd := exec.CommandContext(ctx, "docker", args...)
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create stdout pipe: %v", err)
|
|
}
|
|
stderr, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create stderr pipe: %v", err)
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to start container: %v", err)
|
|
}
|
|
|
|
// Ensure cleanup on function exit
|
|
defer func() {
|
|
// Stop the container if it's still running
|
|
stopCmd := exec.Command("docker", "stop", containerName)
|
|
_ = stopCmd.Run()
|
|
// Remove the container if it still exists
|
|
rmCmd := exec.Command("docker", "rm", "-f", containerName)
|
|
_ = rmCmd.Run()
|
|
}()
|
|
|
|
var output, errOutput strings.Builder
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
scanner := bufio.NewScanner(stdout)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
output.WriteString(line + "\n")
|
|
if strings.Contains(line, expectedOutput) {
|
|
done <- nil
|
|
return
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
done <- fmt.Errorf("error reading stdout: %v", err)
|
|
return
|
|
}
|
|
done <- fmt.Errorf("container output:\n%s\nErrors:\n%s", output.String(), errOutput.String())
|
|
}()
|
|
|
|
go func() {
|
|
scanner := bufio.NewScanner(stderr)
|
|
for scanner.Scan() {
|
|
errOutput.WriteString(scanner.Text() + "\n")
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return fmt.Errorf("container timed out after 2 minutes")
|
|
case err := <-done:
|
|
if err != nil {
|
|
require.Contains(t, output.String(), expectedOutput, "container output did not contain expected string")
|
|
return err
|
|
}
|
|
return nil
|
|
case err := <-cmdDoneChan(cmd):
|
|
if err != nil && !strings.Contains(err.Error(), "signal: killed") {
|
|
return fmt.Errorf("container failed: %v", err)
|
|
}
|
|
require.Contains(t, output.String(), expectedOutput, "container output did not contain expected string")
|
|
return nil
|
|
}
|
|
}
|