@@ -12,6 +12,7 @@ import (
1212 "os/exec"
1313 "path/filepath"
1414 "runtime"
15+ "strconv"
1516 "strings"
1617 "time"
1718
@@ -375,7 +376,8 @@ func (Test) Unit(ctx context.Context) error {
375376 return sh .Run ("go" , "test" , "-v" , "-covermode=count" , "-coverprofile=coverage/unit-test.profile" , "./..." )
376377}
377378
378- // Integration runs the integration tests.
379+ // Integration runs the integration tests in parallel.
380+ // Set GHOSTUNNEL_TEST_PARALLEL to control concurrency (default: NumCPU, max 16).
379381func (Test ) Integration (ctx context.Context ) error {
380382 mg .CtxDeps (ctx , Test .build )
381383
@@ -395,47 +397,93 @@ func (Test) Integration(ctx context.Context) error {
395397 return fmt .Errorf ("failed to find test files: %w" , err )
396398 }
397399
398- // Run each integration test directly
399- printf ("Running integration tests...\n " )
400- for _ , testFile := range testFiles {
401- if err := ctx .Err (); err != nil {
402- return fmt .Errorf ("context cancelled: %w" , err )
400+ // Determine parallelism
401+ parallel := runtime .NumCPU ()
402+ if parallel > 16 {
403+ parallel = 16
404+ }
405+ if envVal := os .Getenv ("GHOSTUNNEL_TEST_PARALLEL" ); envVal != "" {
406+ if n , err := strconv .Atoi (envVal ); err == nil && n > 0 {
407+ parallel = n
403408 }
409+ }
404410
405- testName := strings .TrimSuffix (filepath .Base (testFile ), ".py" )
406- printf ("=== RUN %s\n " , testName )
407-
408- // Run the Python test file directly from tests directory
409- start := time .Now ()
410- testFileName := filepath .Base (testFile )
411- cmd := exec .CommandContext (ctx , "python3" , testFileName )
412- cmd .Dir = "tests"
411+ printf ("Running %d integration tests with parallelism=%d...\n " , len (testFiles ), parallel )
413412
414- // Capture stdout and stderr
415- var stdout , stderr bytes.Buffer
416- cmd .Stdout = & stdout
417- cmd .Stderr = & stderr
413+ type testResult struct {
414+ name string
415+ stdout []byte
416+ stderr []byte
417+ err error
418+ duration time.Duration
419+ }
418420
419- err := cmd . Run ()
420- duration := time . Since ( start )
421- elapsed := duration . Seconds ( )
421+ // Channel-based semaphore for limiting concurrency
422+ sem := make ( chan struct {}, parallel )
423+ results := make ( chan testResult , len ( testFiles ) )
422424
423- if err == nil {
424- printf ("=== PASS: %s (%.2fs)\n " , testName , elapsed )
425- continue
425+ // Launch all tests as goroutines
426+ for _ , testFile := range testFiles {
427+ go func () {
428+ // Check for context cancellation before acquiring semaphore
429+ select {
430+ case <- ctx .Done ():
431+ results <- testResult {
432+ name : strings .TrimSuffix (filepath .Base (testFile ), ".py" ),
433+ err : ctx .Err (),
434+ }
435+ return
436+ case sem <- struct {}{}: // acquire
437+ }
438+ defer func () { <- sem }() // release
439+
440+ testName := strings .TrimSuffix (filepath .Base (testFile ), ".py" )
441+
442+ start := time .Now ()
443+ testFileName := filepath .Base (testFile )
444+ cmd := exec .CommandContext (ctx , "python3" , testFileName )
445+ cmd .Dir = "tests"
446+
447+ var stdout , stderr bytes.Buffer
448+ cmd .Stdout = & stdout
449+ cmd .Stderr = & stderr
450+
451+ err := cmd .Run ()
452+ duration := time .Since (start )
453+
454+ results <- testResult {
455+ name : testName ,
456+ stdout : stdout .Bytes (),
457+ stderr : stderr .Bytes (),
458+ err : err ,
459+ duration : duration ,
460+ }
461+ }()
462+ }
463+
464+ // Collect results
465+ var failed []testResult
466+ for i := 0 ; i < len (testFiles ); i ++ {
467+ r := <- results
468+ if r .err == nil {
469+ printf ("=== PASS: %s (%.2fs)\n " , r .name , r .duration .Seconds ())
470+ } else {
471+ fmt .Printf ("=== FAIL: %s (%.2fs)\n " , r .name , r .duration .Seconds ())
472+ failed = append (failed , r )
426473 }
474+ }
427475
428- // Test failed - output captured stdout/stderr and failure message
429- os .Stdout .Write (stdout .Bytes ())
430- os .Stderr .Write (stderr .Bytes ())
431- printf ("=== FAIL: %s (%.2fs)\n " , testName , elapsed )
432-
433- // Get exit code if available
434- if exitError , ok := err .(* exec.ExitError ); ok {
435- return fmt .Errorf ("integration test %s failed with exit code %d" , testName , exitError .ExitCode ())
476+ // Report failures
477+ if len (failed ) > 0 {
478+ fmt .Printf ("\n --- FAILURES ---\n " )
479+ for _ , r := range failed {
480+ fmt .Printf ("\n === FAIL: %s (%.2fs)\n " , r .name , r .duration .Seconds ())
481+ fmt .Printf ("--- stdout ---\n " )
482+ os .Stdout .Write (r .stdout )
483+ fmt .Printf ("--- stderr ---\n " )
484+ os .Stdout .Write (r .stderr )
436485 }
437-
438- return fmt .Errorf ("integration test %s failed: %w" , testName , err )
486+ return fmt .Errorf ("%d integration test(s) failed" , len (failed ))
439487 }
440488
441489 return nil
@@ -498,8 +546,10 @@ func (Test) Single(ctx context.Context, name string) error {
498546
499547 // On failure, show captured output if not already streaming
500548 if ! mg .Verbose () {
549+ fmt .Printf ("--- stdout ---\n " )
501550 os .Stdout .Write (stdout .Bytes ())
502- os .Stderr .Write (stderr .Bytes ())
551+ fmt .Printf ("--- stderr ---\n " )
552+ os .Stdout .Write (stderr .Bytes ())
503553 }
504554 fmt .Printf ("=== FAIL: %s (%.2fs)\n " , name , elapsed )
505555 if exitError , ok := err .(* exec.ExitError ); ok {
@@ -680,12 +730,21 @@ func (Test) Docker(ctx context.Context) error {
680730 return fmt .Errorf ("failed to get current directory: %w" , err )
681731 }
682732
683- args = []string {"run" , "-v" , fmt .Sprintf ("%s:/go/src/github.com/ghostunnel/ghostunnel" , pwd ), "ghostunnel/ghostunnel-test" , "--" }
733+ containerName := fmt .Sprintf ("ghostunnel-test-%d" , os .Getpid ())
734+ args = []string {"run" , "--rm" , "--name" , containerName , "-v" , fmt .Sprintf ("%s:/go/src/github.com/ghostunnel/ghostunnel" , pwd ), "ghostunnel/ghostunnel-test" , "--" }
684735 if mg .Verbose () {
685736 args = append (args , "-v" )
686737 }
687738 args = append (args , "test:softhsmimport" , "test:all" )
688- return sh .Run ("docker" , args ... )
739+
740+ defer func () {
741+ exec .Command ("docker" , "rm" , "-f" , containerName ).Run ()
742+ }()
743+
744+ cmd := exec .CommandContext (ctx , "docker" , args ... )
745+ cmd .Stdout = os .Stdout
746+ cmd .Stderr = os .Stderr
747+ return cmd .Run ()
689748}
690749
691750// Build builds and tags all Docker containers.
0 commit comments