test(integration): add retry mecanism

This commit is contained in:
Julien Neuhart
2026-03-28 21:09:22 +01:00
parent e0eff40f74
commit 6430bc6a5c
3 changed files with 106 additions and 13 deletions

View File

@@ -127,6 +127,9 @@ NO_CONCURRENCY=false
# chromium-convert-html # chromium-convert-html
# chromium-convert-markdown # chromium-convert-markdown
# chromium-convert-url # chromium-convert-url
# chromium-screenshot-html
# chromium-screenshot-markdown
# chromium-screenshot-url
# debug # debug
# health # health
# libreoffice # libreoffice
@@ -162,7 +165,7 @@ NO_CONCURRENCY=false
TAGS= TAGS=
.PHONY: test-integration .PHONY: test-integration
test-integration: ## Run integration tests test-integration: ## Run integration tests (automatically retries failed scenarios up to 3 times)
go test -timeout 40m -tags=integration -v github.com/gotenberg/gotenberg/v8/test/integration -args \ go test -timeout 40m -tags=integration -v github.com/gotenberg/gotenberg/v8/test/integration -args \
--gotenberg-docker-repository=$(DOCKER_REPOSITORY) \ --gotenberg-docker-repository=$(DOCKER_REPOSITORY) \
--gotenberg-version=$(GOTENBERG_VERSION) \ --gotenberg-version=$(GOTENBERG_VERSION) \

View File

@@ -3,6 +3,7 @@
package integration package integration
import ( import (
"fmt"
"os" "os"
"runtime" "runtime"
"testing" "testing"
@@ -40,17 +41,42 @@ func TestMain(m *testing.M) {
concurrency = 0 concurrency = 0
} }
code := godog.TestSuite{ maxAttempts := 4 // 1 initial run + up to 3 retries
Name: "integration", paths := []string{"features"}
ScenarioInitializer: scenario.InitializeScenario,
Options: &godog.Options{
Format: "pretty",
Paths: []string{"features"},
Output: colors.Colored(os.Stdout),
Concurrency: concurrency,
Tags: *tags,
},
}.Run()
os.Exit(code) for attempt := 1; attempt <= maxAttempts; attempt++ {
// Reset the failure collector before each run.
scenario.ResetFailedScenarios()
code := godog.TestSuite{
Name: "integration",
ScenarioInitializer: scenario.InitializeScenario,
Options: &godog.Options{
Format: "pretty",
Paths: paths,
Output: colors.Colored(os.Stdout),
Concurrency: concurrency,
Tags: *tags,
},
}.Run()
if code == 0 {
os.Exit(0)
}
failedPaths := scenario.FailedScenarioPaths()
if len(failedPaths) == 0 || attempt == maxAttempts {
os.Exit(code)
}
fmt.Fprintf(colors.Colored(os.Stdout),
"\n\n--- %d scenario(s) failed, retrying (%d retries left) ---\n\n",
len(failedPaths), maxAttempts-attempt,
)
// Next run: only the failed scenarios via file:line paths.
paths = failedPaths
}
os.Exit(1)
} }

View File

@@ -23,6 +23,66 @@ import (
"github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go"
) )
var (
failedScenariosMu sync.Mutex
failedScenarios []string // file:line paths for re-run
)
// ResetFailedScenarios clears the collected failures.
func ResetFailedScenarios() {
failedScenariosMu.Lock()
defer failedScenariosMu.Unlock()
failedScenarios = nil
}
// FailedScenarioPaths returns file:line paths of failed scenarios.
func FailedScenarioPaths() []string {
failedScenariosMu.Lock()
defer failedScenariosMu.Unlock()
result := make([]string, len(failedScenarios))
copy(result, failedScenarios)
return result
}
func recordFailedScenario(sc *godog.Scenario) {
// When godog runs with file:line paths, the Uri may contain
// the line suffix (e.g., "features/root.feature:4"). Strip it
// to get the actual file path for reading.
filePath := sc.Uri
if idx := strings.LastIndex(filePath, ":"); idx > 0 {
if _, err := strconv.Atoi(filePath[idx+1:]); err == nil {
filePath = filePath[:idx]
}
}
line := findScenarioLine(filePath, sc.Name)
if line <= 0 {
return
}
path := fmt.Sprintf("%s:%d", filePath, line)
failedScenariosMu.Lock()
defer failedScenariosMu.Unlock()
failedScenarios = append(failedScenarios, path)
}
func findScenarioLine(filePath, name string) int {
data, err := os.ReadFile(filePath)
if err != nil {
return 0
}
target := "Scenario: " + name
for i, line := range strings.Split(string(data), "\n") {
if strings.TrimSpace(line) == target {
return i + 1
}
}
return 0
}
type scenario struct { type scenario struct {
resp *httptest.ResponseRecorder resp *httptest.ResponseRecorder
concurrentResps []*httptest.ResponseRecorder concurrentResps []*httptest.ResponseRecorder
@@ -1226,6 +1286,10 @@ func InitializeScenario(ctx *godog.ScenarioContext) {
return ctx, nil return ctx, nil
}) })
ctx.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) { ctx.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) {
if err != nil {
recordFailedScenario(sc)
}
errReset := s.reset(ctx) errReset := s.reset(ctx)
if errReset != nil { if errReset != nil {
return ctx, fmt.Errorf("reset scenario: %w", errReset) return ctx, fmt.Errorf("reset scenario: %w", errReset)