diff --git a/.gitignore b/.gitignore index 8b9f79d5..240cf4d4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ bin/ summary.md /.speakeasy/ temp/ -integration/temp +integrationTests/ .DS_Store .vscode/launch.json __debug_bin* diff --git a/go.mod b/go.mod index 7cd6fead..ed5dd9fb 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( github.com/speakeasy-api/huh v1.1.2 github.com/speakeasy-api/jq v0.1.1-0.20251107233444-84d7e49e84a4 github.com/speakeasy-api/openapi v1.12.1 - github.com/speakeasy-api/openapi-generation/v2 v2.778.0 + github.com/speakeasy-api/openapi-generation/v2 v2.778.5 github.com/speakeasy-api/sdk-gen-config v1.43.1 github.com/speakeasy-api/speakeasy-client-sdk-go/v3 v3.26.7 github.com/speakeasy-api/speakeasy-core v0.21.0 diff --git a/go.sum b/go.sum index c9c71ab7..b3920e3a 100644 --- a/go.sum +++ b/go.sum @@ -535,8 +535,8 @@ github.com/speakeasy-api/libopenapi v0.21.9-fixhiddencomps-fixed h1:PL/kpBY5vkBm github.com/speakeasy-api/libopenapi v0.21.9-fixhiddencomps-fixed/go.mod h1:Gc8oQkjr2InxwumK0zOBtKN9gIlv9L2VmSVIUk2YxcU= github.com/speakeasy-api/openapi v1.12.1 h1:q8KqVo6P9SkDD4hulXBT6KoPU3BY4eeBiSUHbVSHAes= github.com/speakeasy-api/openapi v1.12.1/go.mod h1:ITV3em4IFe1Hd4gX5Peq9TE7+Rfd/WIHZE/aqxNgihg= -github.com/speakeasy-api/openapi-generation/v2 v2.778.0 h1:Mpu4F3zs3DsmC7BMYzVryYeLzdc/p+0N1vPaVbT2zO0= -github.com/speakeasy-api/openapi-generation/v2 v2.778.0/go.mod h1:ol7GV+VKS4rlkH1pkIdw55n0gXa3OOL+V32h65JRruA= +github.com/speakeasy-api/openapi-generation/v2 v2.778.5 h1:Dl6A8vNWryhJkOmtSg7uYFAN0pgJert11f/ClIH9PaY= +github.com/speakeasy-api/openapi-generation/v2 v2.778.5/go.mod h1:ol7GV+VKS4rlkH1pkIdw55n0gXa3OOL+V32h65JRruA= github.com/speakeasy-api/sdk-gen-config v1.43.1 h1:rhhv6mAVV2yl1I6TqHkpunnOnSXyH5milgzEA9vJPEo= github.com/speakeasy-api/sdk-gen-config v1.43.1/go.mod h1:kD0NPNX5yaG4j+dcCpLL0hHKQbFk6X93obp+v1XlK5E= github.com/speakeasy-api/speakeasy-client-sdk-go/v3 v3.26.7 h1:SoWZkRlpFlv8qibCfXWrBZay1JeLS9uqJ+1cu+DFgXo= diff --git a/integration/main_test.go b/integration/main_test.go index 5837a5c3..ad97b180 100644 --- a/integration/main_test.go +++ b/integration/main_test.go @@ -6,6 +6,7 @@ import ( "os/exec" "path/filepath" "runtime" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -13,67 +14,72 @@ import ( // prebuiltBinary holds the path to the pre-built speakeasy binary. // This avoids recompiling on every `go run main.go` call, saving ~20s per invocation. -var prebuiltBinary string +var ( + prebuiltBinary string + buildOnce sync.Once + buildErr error +) + +// ensureBinary builds the speakeasy binary once on first call. +// Subsequent calls return immediately. This is called lazily by execute() +// so that executeI() invocations don't pay the build cost. +func ensureBinary() (string, error) { + buildOnce.Do(func() { + _, filename, _, _ := runtime.Caller(0) + baseFolder := filepath.Join(filepath.Dir(filename), "..") + // Use PID to avoid collision between parallel test runs on the same machine + binaryName := fmt.Sprintf("speakeasy-test-binary-%d", os.Getpid()) + if runtime.GOOS == "windows" { + binaryName += ".exe" + } + binaryPath := filepath.Join(os.TempDir(), binaryName) + + fmt.Println("Pre-building speakeasy binary for integration tests...") + buildCmd := exec.Command("go", "build", "-o", binaryPath, filepath.Join(baseFolder, "main.go")) + buildCmd.Dir = baseFolder + buildCmd.Stdout = os.Stdout + buildCmd.Stderr = os.Stderr + if err := buildCmd.Run(); err != nil { + buildErr = fmt.Errorf("failed to pre-build speakeasy binary: %w", err) + return + } + prebuiltBinary = binaryPath + fmt.Println("Pre-built speakeasy binary:", prebuiltBinary) + }) + return prebuiltBinary, buildErr +} // Entrypoint for CLI integration tests func TestMain(m *testing.M) { - // Create a temporary directory - if _, err := os.Stat(tempDir); err == nil { - if err := os.RemoveAll(tempDir); err != nil { - panic(err) - } - } + testDir := integrationTestsDir() - if err := os.Mkdir(tempDir, 0o755); err != nil { + // Create the integrationTests directory (MkdirAll is safe for parallel test processes) + if err := os.MkdirAll(testDir, 0o755); err != nil { panic(err) } - // Pre-build the speakeasy binary once to avoid ~20s compilation overhead per test - _, filename, _, _ := runtime.Caller(0) - baseFolder := filepath.Join(filepath.Dir(filename), "..") - binaryName := "speakeasy-test-binary" - if runtime.GOOS == "windows" { - binaryName += ".exe" - } - binaryPath := filepath.Join(os.TempDir(), binaryName) - - fmt.Println("Pre-building speakeasy binary for integration tests...") - buildCmd := exec.Command("go", "build", "-o", binaryPath, filepath.Join(baseFolder, "main.go")) - buildCmd.Dir = baseFolder - buildCmd.Stdout = os.Stdout - buildCmd.Stderr = os.Stderr - if err := buildCmd.Run(); err != nil { - panic(fmt.Sprintf("failed to pre-build speakeasy binary: %v", err)) - } - prebuiltBinary = binaryPath - fmt.Println("Pre-built speakeasy binary:", prebuiltBinary) + code := m.Run() - // Defer the removal of the temp directory and binary - defer func() { - if err := os.RemoveAll(tempDir); err != nil { - panic(err) - } + // Cleanup must happen before os.Exit (defer is not executed with os.Exit) + if prebuiltBinary != "" { os.Remove(prebuiltBinary) - }() + } - code := m.Run() os.Exit(code) } func setupTestDir(t *testing.T) string { t.Helper() - _, filename, _, _ := runtime.Caller(0) - workingDir := filepath.Dir(filename) - temp, err := createTempDir(workingDir) + temp, err := createTempDir("") assert.NoError(t, err) - registerCleanup(t, workingDir, temp) + registerCleanup(t, temp) return temp } -func registerCleanup(t *testing.T, workingDir string, temp string) { +func registerCleanup(t *testing.T, temp string) { t.Helper() t.Cleanup(func() { - os.RemoveAll(filepath.Join(workingDir, temp)) + os.RemoveAll(temp) }) } diff --git a/integration/patches_git_test.go b/integration/patches_git_test.go index eca5aaeb..9f90be3c 100644 --- a/integration/patches_git_test.go +++ b/integration/patches_git_test.go @@ -7,7 +7,6 @@ import ( "os/exec" "path/filepath" "regexp" - "runtime" "strings" "testing" @@ -695,14 +694,9 @@ func TestGitArchitecture_DeltaCompressionEfficiency(t *testing.T) { func TestGitArchitecture_ImplicitFetchFromRemote(t *testing.T) { t.Parallel() - // Create temp directories inside the module tree (required for `go run` to work) - // Using the integration folder as base ensures go.mod is findable - _, filename, _, _ := runtime.Caller(0) - integrationDir := filepath.Dir(filename) - - remoteDir := filepath.Join(integrationDir, "temp", "remote-"+randStringBytes(7)+".git") - envADir := filepath.Join(integrationDir, "temp", "envA-"+randStringBytes(7)) - envBDir := filepath.Join(integrationDir, "temp", "envB-"+randStringBytes(7)) + remoteDir := filepath.Join(integrationTestsDir(), "remote-"+randStringBytes(7)+".git") + envADir := filepath.Join(integrationTestsDir(), "envA-"+randStringBytes(7)) + envBDir := filepath.Join(integrationTestsDir(), "envB-"+randStringBytes(7)) // Create directories require.NoError(t, os.MkdirAll(remoteDir, 0755)) @@ -1216,10 +1210,7 @@ func TestGitArchitecture_MultipleTypeScriptTargetsSameRepo(t *testing.T) { func setupDualTypeScriptTargetsTestDir(t *testing.T) string { t.Helper() - // Create temp directory using runtime.Caller pattern (required for go run to work) - _, filename, _, _ := runtime.Caller(0) - integrationDir := filepath.Dir(filename) - temp := filepath.Join(integrationDir, "temp", "dual-ts-"+randStringBytes(7)) + temp := filepath.Join(integrationTestsDir(), "dual-ts-"+randStringBytes(7)) require.NoError(t, os.MkdirAll(temp, 0755)) t.Cleanup(func() { diff --git a/integration/utils.go b/integration/utils.go index 865740ee..18096311 100644 --- a/integration/utils.go +++ b/integration/utils.go @@ -11,14 +11,20 @@ import ( ) const ( - tempDir = "temp" letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" version = "0.0.1" artifactArch = "linux_amd64" ) -func createTempDir(wd string) (string, error) { - target := filepath.Join(wd, tempDir, randStringBytes(7)) +// integrationTestsDir returns the path to the integrationTests directory at repo root. +// This is outside the integration/ package directory to avoid interference with `go test ./integration/...`. +func integrationTestsDir() string { + _, filename, _, _ := runtime.Caller(0) + return filepath.Join(filepath.Dir(filename), "..", "integrationTests") +} + +func createTempDir(_ string) (string, error) { + target := filepath.Join(integrationTestsDir(), randStringBytes(7)) if err := os.Mkdir(target, 0o755); err != nil { return "", err } diff --git a/integration/workflow_test.go b/integration/workflow_test.go index f206f8fa..aa3391d3 100644 --- a/integration/workflow_test.go +++ b/integration/workflow_test.go @@ -8,7 +8,6 @@ import ( "os" "os/exec" "path/filepath" - "runtime" "strings" "sync" "testing" @@ -282,16 +281,11 @@ func (r *subprocessRunner) Run() error { func execute(t *testing.T, wd string, args ...string) Runnable { t.Helper() - // Use pre-built binary if available (set by TestMain), otherwise fall back to go run - var execCmd *exec.Cmd - if prebuiltBinary != "" { - execCmd = exec.Command(prebuiltBinary, args...) - } else { - _, filename, _, _ := runtime.Caller(0) - baseFolder := filepath.Join(filepath.Dir(filename), "..") - mainGo := filepath.Join(baseFolder, "main.go") - execCmd = exec.Command("go", append([]string{"run", mainGo}, args...)...) - } + // Build the binary lazily on first execute() call + binaryPath, err := ensureBinary() + require.NoError(t, err, "failed to build speakeasy binary") + + execCmd := exec.Command(binaryPath, args...) execCmd.Env = os.Environ() execCmd.Dir = wd diff --git a/internal/patches/pregeneration.go b/internal/patches/pregeneration.go index c9035173..202835ed 100644 --- a/internal/patches/pregeneration.go +++ b/internal/patches/pregeneration.go @@ -252,7 +252,7 @@ func PrepareForGeneration(outDir string, autoYes bool, promptFunc PromptFunc, wa warnFunc("Failed to save lockfile with file change markers: %v", err) } } - } else if !persistentEdits.IsNever() && !env.IsCI() && os.Getenv("PROMPT_CUSTOM_CODE") == "true" { + } else if !persistentEdits.IsNever() && !env.IsCI() { // Not enabled and not "never" - check for dirty files and prompt isDirty, modifiedPaths, err := DetectFileChanges(outDir, cfg.LockFile) if err != nil {