From 4f27b2fc16d65e95ab46bdaf10ca70c38cbbe867 Mon Sep 17 00:00:00 2001 From: chengzeyi Date: Fri, 21 Aug 2026 05:41:54 +0000 Subject: [PATCH] fix(test): remove the data race on attemptCount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `go test -race` fails deterministically on main: TestSubmitSingleShotOnConnectionFailure reads attemptCount from the test goroutine while the HTTP handler goroutine increments it, with nothing ordering the two. The handler hijacks and closes the connection, so the client observes EOF and the assertion runs while the handler is still on its way out — unlike the other tests, there is no response delivery to inform an ordering. - All four attemptCount counters become atomic.Int64. The other three have the same shape and only avoid the detector because the response write happens to order them; making them uniform removes the class rather than the one instance. - The hijack handler called t.Fatal/t.Fatalf. testing.FailNow must only be called from the goroutine running the test, so those become t.Error/t.Errorf followed by return. Test-only; no library code touched. Reproduced 6/6 before, 0/10 after. gofmt and go vet clean, and the full -race suite passes. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LJJXU9zyoDSBjApcUpteDt --- api/client_test.go | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/api/client_test.go b/api/client_test.go index f3d23b4..646193c 100644 --- a/api/client_test.go +++ b/api/client_test.go @@ -11,6 +11,7 @@ import ( "path/filepath" "runtime" "strings" + "sync/atomic" "testing" "time" ) @@ -585,10 +586,10 @@ func TestUploadRealAPI(t *testing.T) { func TestRunAllRetriesFailed(t *testing.T) { // Test scenario where all retries are exhausted - attemptCount := 0 + var attemptCount atomic.Int64 mux := http.NewServeMux() mux.HandleFunc("/api/v3/wavespeed-ai/z-image/turbo", func(w http.ResponseWriter, r *http.Request) { - attemptCount++ + attemptCount.Add(1) // Return 500 error which is retryable w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(`{"code":500,"message":"Internal Server Error"}`)) @@ -604,17 +605,17 @@ func TestRunAllRetriesFailed(t *testing.T) { } // Should have attempted 3 times (initial + 2 retries) - if attemptCount < 3 { - t.Errorf("expected at least 3 attempts, got %d", attemptCount) + if attemptCount.Load() < 3 { + t.Errorf("expected at least 3 attempts, got %d", attemptCount.Load()) } } func TestGetResultConnectionRetry(t *testing.T) { // Test that getResult does NOT retry on HTTP status code errors (only on connection errors) - attemptCount := 0 + var attemptCount atomic.Int64 mux := http.NewServeMux() mux.HandleFunc("/api/v3/predictions/req-123/result", func(w http.ResponseWriter, r *http.Request) { - attemptCount++ + attemptCount.Add(1) // Return 500 - this should NOT trigger a retry w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("Server Error")) @@ -630,8 +631,8 @@ func TestGetResultConnectionRetry(t *testing.T) { } // HTTP errors should NOT retry, only connection errors do - if attemptCount != 1 { - t.Errorf("expected exactly 1 attempt (no retry for HTTP errors), got %d", attemptCount) + if attemptCount.Load() != 1 { + t.Errorf("expected exactly 1 attempt (no retry for HTTP errors), got %d", attemptCount.Load()) } if !strings.Contains(err.Error(), "HTTP 500") { @@ -671,10 +672,10 @@ func TestIsRetryableError(t *testing.T) { func TestSubmitConnectionRetry(t *testing.T) { // Test that submit does NOT retry on HTTP status code errors (only on connection errors) - attemptCount := 0 + var attemptCount atomic.Int64 mux := http.NewServeMux() mux.HandleFunc("/api/v3/wavespeed-ai/z-image/turbo", func(w http.ResponseWriter, r *http.Request) { - attemptCount++ + attemptCount.Add(1) // Return 502 - this should NOT trigger a retry w.WriteHeader(http.StatusBadGateway) w.Write([]byte("Bad Gateway")) @@ -690,8 +691,8 @@ func TestSubmitConnectionRetry(t *testing.T) { } // HTTP errors should NOT retry, only connection errors do - if attemptCount != 1 { - t.Errorf("expected exactly 1 attempt (no retry for HTTP errors), got %d", attemptCount) + if attemptCount.Load() != 1 { + t.Errorf("expected exactly 1 attempt (no retry for HTTP errors), got %d", attemptCount.Load()) } if !strings.Contains(err.Error(), "HTTP 502") { @@ -796,17 +797,20 @@ func TestSubmitSingleShotOnConnectionFailure(t *testing.T) { // The submission POST must fire exactly once when the connection fails: // the task may already have been created server-side, so retrying could // create duplicate tasks. - attemptCount := 0 + var attemptCount atomic.Int64 mux := http.NewServeMux() mux.HandleFunc("/api/v3/wavespeed-ai/z-image/turbo", func(w http.ResponseWriter, r *http.Request) { - attemptCount++ + attemptCount.Add(1) hj, ok := w.(http.Hijacker) if !ok { - t.Fatal("server does not support hijacking") + // t.FailNow must not be called outside the test goroutine. + t.Error("server does not support hijacking") + return } conn, _, err := hj.Hijack() if err != nil { - t.Fatalf("hijack failed: %v", err) + t.Errorf("hijack failed: %v", err) + return } conn.Close() // drop the connection without responding }) @@ -819,8 +823,8 @@ func TestSubmitSingleShotOnConnectionFailure(t *testing.T) { if err == nil { t.Fatal("expected error for dropped connection") } - if attemptCount != 1 { - t.Errorf("expected exactly 1 submission attempt, got %d", attemptCount) + if attemptCount.Load() != 1 { + t.Errorf("expected exactly 1 submission attempt, got %d", attemptCount.Load()) } var submissionErr *SubmissionError