diff --git a/cmd/modern/e2e_test.go b/cmd/modern/e2e_test.go index 9c3d14e6..75c07e34 100644 --- a/cmd/modern/e2e_test.go +++ b/cmd/modern/e2e_test.go @@ -12,6 +12,7 @@ import ( "strings" "sync" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -312,6 +313,41 @@ func TestE2E_PipedInput_WithBytesBuffer_NoPanic(t *testing.T) { assert.NotContains(t, outputStr, "nil pointer", "should not have nil pointer error") } +// TestE2E_QueryTimeout_LiveConnection tests that the -t flag properly times out queries +// and doesn't hang for 10 minutes on Linux. This is a regression test for the issue +// where sqlcmd would print "Timeout expired" but then hang for ~10 minutes before exiting. +func TestE2E_QueryTimeout_LiveConnection(t *testing.T) { + skipIfNoLiveConnection(t) + binary := buildBinary(t) + + // Use a 1 second timeout with a query that would take 10 seconds + args := append([]string{"-C", "-t", "1", "-Q", "WAITFOR DELAY '00:00:10'"}, getAuthArgs(t)...) + cmd := exec.Command(binary, args...) + cmd.Env = os.Environ() + + // Measure execution time - this is the key validation + // The command should complete in a few seconds, not 10 minutes + start := time.Now() + output, err := cmd.CombinedOutput() + elapsed := time.Since(start) + outputStr := string(output) + + // The command should fail due to timeout + assert.Error(t, err, "query should timeout and return an error") + + // Output should contain timeout message + assert.Contains(t, outputStr, "Timeout expired", "output should contain 'Timeout expired' message") + + // Critical: verify the command completed quickly (within 30 seconds, not 10 minutes) + // If the bug exists, this would take ~10 minutes on Linux + maxDuration := 30 * time.Second + if elapsed > maxDuration { + t.Errorf("Command took too long to complete: %v (expected < %v). The timeout hang bug may still exist.", elapsed, maxDuration) + } + + t.Logf("Query timed out correctly in %v: %s", elapsed, outputStr) +} + // cleanupBinary removes the temporary build directory containing the test binary. // TestMain calls this to ensure deterministic cleanup instead of relying on // eventual OS temp directory maintenance. diff --git a/pkg/sqlcmd/sqlcmd.go b/pkg/sqlcmd/sqlcmd.go index 5e572a94..ba6a74e4 100644 --- a/pkg/sqlcmd/sqlcmd.go +++ b/pkg/sqlcmd/sqlcmd.go @@ -431,6 +431,9 @@ func (s *Sqlcmd) runQuery(query string) (int, error) { } retmsg := &sqlexp.ReturnMessage{} rows, qe := s.db.QueryContext(ctx, query, retmsg) + if rows != nil { + defer rows.Close() + } if qe != nil { s.Format.AddError(qe) }