@@ -22,11 +22,13 @@ import (
2222 "database/sql"
2323 "database/sql/driver"
2424 "io"
25+ "strings"
2526 "sync/atomic"
2627 "testing"
2728 "time"
2829
2930 "github.com/bluele/gcache"
31+ "github.com/go-sql-driver/mysql"
3032 "github.com/golang/mock/gomock"
3133 "github.com/google/uuid"
3234 "github.com/stretchr/testify/assert"
@@ -118,10 +120,23 @@ func (mi *mockTxHook) BeforeRollback(tx *Tx) {
118120 }
119121}
120122
123+ // simulateExecContextError allows tests to inject driver errors for certain SQL strings.
124+ // When set, baseMockConn will call this hook for each ExecContext.
125+ var simulateExecContextError func (query string ) error
126+
121127func baseMockConn (mockConn * mock.MockTestDriverConn ) {
122128 branchStatusCache = gcache .New (1024 ).LRU ().Expiration (time .Minute * 10 ).Build ()
123129
124- mockConn .EXPECT ().ExecContext (gomock .Any (), gomock .Any (), gomock .Any ()).AnyTimes ().Return (& driver .ResultNoRows , nil )
130+ mockConn .EXPECT ().ExecContext (gomock .Any (), gomock .Any (), gomock .Any ()).AnyTimes ().DoAndReturn (
131+ func (ctx context.Context , query string , args []driver.NamedValue ) (driver.Result , error ) {
132+ if simulateExecContextError != nil {
133+ if err := simulateExecContextError (query ); err != nil {
134+ return & driver .ResultNoRows , err
135+ }
136+ }
137+ return & driver .ResultNoRows , nil
138+ },
139+ )
125140 mockConn .EXPECT ().Exec (gomock .Any (), gomock .Any ()).AnyTimes ().Return (& driver .ResultNoRows , nil )
126141 mockConn .EXPECT ().ResetSession (gomock .Any ()).AnyTimes ().Return (nil )
127142 mockConn .EXPECT ().Close ().AnyTimes ().Return (nil )
@@ -329,3 +344,96 @@ func TestXAConn_BeginTx(t *testing.T) {
329344 })
330345
331346}
347+
348+ func TestXAConn_Rollback_XAER_RMFAIL (t * testing.T ) {
349+ tests := []struct {
350+ name string
351+ err error
352+ want bool
353+ }{
354+ {
355+ name : "no error case" ,
356+ err : nil ,
357+ want : false ,
358+ },
359+ {
360+ name : "matching XAER_RMFAIL error with IDLE state" ,
361+ err : & mysql.MySQLError {
362+ Number : 1399 ,
363+ Message : "Error 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the IDLE state" ,
364+ },
365+ want : true ,
366+ },
367+ {
368+ name : "matching XAER_RMFAIL error with already ended" ,
369+ err : & mysql.MySQLError {
370+ Number : 1399 ,
371+ Message : "Error 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction has already ended" ,
372+ },
373+ want : true ,
374+ },
375+ {
376+ name : "matching error code but mismatched message" ,
377+ err : & mysql.MySQLError {
378+ Number : 1399 ,
379+ Message : "Error 1399 (XAE07): XAER_RMFAIL: Other error message" ,
380+ },
381+ want : false ,
382+ },
383+ {
384+ name : "mismatched error code but matching message" ,
385+ err : & mysql.MySQLError {
386+ Number : 1234 ,
387+ Message : "The command cannot be executed when global transaction is in the IDLE state" ,
388+ },
389+ want : false ,
390+ },
391+ }
392+
393+ for _ , tt := range tests {
394+ t .Run (tt .name , func (t * testing.T ) {
395+ if got := isXAER_RMFAILAlreadyEnded (tt .err ); got != tt .want {
396+ t .Errorf ("isXAER_RMFAILAlreadyEnded() = %v, want %v" , got , tt .want )
397+ }
398+ })
399+ }
400+ }
401+
402+ // Covers the XA rollback flow when End() returns XAER_RMFAIL (IDLE/already ended)
403+ func TestXAConn_Rollback_HandleXAERRMFAILAlreadyEnded (t * testing.T ) {
404+ ctrl , db , _ , ti := initXAConnTestResource (t )
405+ defer func () {
406+ simulateExecContextError = nil
407+ db .Close ()
408+ ctrl .Finish ()
409+ CleanTxHooks ()
410+ }()
411+
412+ ctx := tm .InitSeataContext (context .Background ())
413+ tm .SetXID (ctx , uuid .New ().String ())
414+
415+ // Ensure Tx.Rollback has a non-nil underlying target to avoid nil-deref when test triggers rollback
416+ ti .beforeRollback = func (tx * Tx ) {
417+ mtx := mock .NewMockTestDriverTx (ctrl )
418+ mtx .EXPECT ().Rollback ().AnyTimes ().Return (nil )
419+ tx .target = mtx
420+ }
421+
422+ // Inject: XA END returns XAER_RMFAIL(IDLE), normal SQL returns an error to trigger rollback
423+ simulateExecContextError = func (query string ) error {
424+ upper := strings .ToUpper (query )
425+ if strings .HasPrefix (upper , "XA END" ) {
426+ return & mysql.MySQLError {Number : types .ErrCodeXAER_RMFAIL_IDLE , Message : "Error 1399 (XAE07): XAER_RMFAIL: The command cannot be executed when global transaction is in the IDLE state" }
427+ }
428+ if ! strings .HasPrefix (upper , "XA " ) {
429+ return io .EOF
430+ }
431+ return nil
432+ }
433+
434+ // Execute to enter XA flow; the user SQL fails, but rollback should proceed without panicking
435+ _ , err := db .ExecContext (ctx , "SELECT 1" )
436+ if err == nil {
437+ t .Fatalf ("expected error to trigger rollback path" )
438+ }
439+ }
0 commit comments