Skip to content

Commit 2485eeb

Browse files
feat: add end-to-end tests for sqlcmd binary (fixes #641)
Add e2e tests that build the sqlcmd binary and exercise real-world scenarios: Non-connection tests (always run): - TestE2E_Help: verifies --help flag works - TestE2E_Version: verifies --version flag works - TestE2E_PipedInput_NoPanic: regression test for #607 (piped input panic) - TestE2E_PipedInput_EmptyInput: empty piped input doesn't panic - TestE2E_InvalidFlag: invalid flags produce helpful errors - TestE2E_QueryFlag_NoServer: -Q flag without server doesn't panic - TestE2E_InputFile_NotFound: missing input file errors gracefully - TestE2E_PipedInput_WithStdinReader: GO batches in piped input work Live connection tests (run when SQLCMDSERVER is set): - TestE2E_PipedInput_LiveConnection: piped SQL with real server - TestE2E_QueryFlag_LiveConnection: -Q flag with real server - TestE2E_InputFile_LiveConnection: -i flag with real server The tests build the binary once and reuse it for all tests. Live connection tests use SQLCMDSERVER, SQLCMDUSER, SQLCMDPASSWORD env vars. Addresses feedback from @shueybubbles in PR #640.
1 parent e31f42f commit 2485eeb

1 file changed

Lines changed: 281 additions & 0 deletions

File tree

cmd/modern/e2e_test.go

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
package main
5+
6+
import (
7+
"bytes"
8+
"os"
9+
"os/exec"
10+
"path/filepath"
11+
"runtime"
12+
"strings"
13+
"sync"
14+
"testing"
15+
16+
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/require"
18+
)
19+
20+
var (
21+
binaryPath string
22+
buildOnce sync.Once
23+
buildErr error
24+
)
25+
26+
// buildBinary compiles the sqlcmd binary once for all e2e tests.
27+
// The binary is placed in a temporary directory and cleaned up after tests complete.
28+
func buildBinary(t *testing.T) string {
29+
t.Helper()
30+
buildOnce.Do(func() {
31+
tmpDir, err := os.MkdirTemp("", "sqlcmd-e2e-*")
32+
if err != nil {
33+
buildErr = err
34+
return
35+
}
36+
37+
binaryName := "sqlcmd"
38+
if runtime.GOOS == "windows" {
39+
binaryName = "sqlcmd.exe"
40+
}
41+
binaryPath = filepath.Join(tmpDir, binaryName)
42+
43+
cmd := exec.Command("go", "build", "-o", binaryPath, ".")
44+
// Build from the cmd/modern directory
45+
wd, _ := os.Getwd()
46+
cmd.Dir = wd
47+
output, err := cmd.CombinedOutput()
48+
if err != nil {
49+
buildErr = &buildError{err: err, output: string(output)}
50+
return
51+
}
52+
})
53+
if buildErr != nil {
54+
t.Fatalf("Failed to build sqlcmd binary: %v", buildErr)
55+
}
56+
return binaryPath
57+
}
58+
59+
// hasLiveConnection returns true if SQLCMDSERVER environment variable is set,
60+
// indicating a live SQL Server connection is available for testing.
61+
func hasLiveConnection() bool {
62+
return os.Getenv("SQLCMDSERVER") != ""
63+
}
64+
65+
// skipIfNoLiveConnection skips the test if no live SQL Server connection is available.
66+
func skipIfNoLiveConnection(t *testing.T) {
67+
t.Helper()
68+
if !hasLiveConnection() {
69+
t.Skip("Skipping: SQLCMDSERVER not set, no live connection available")
70+
}
71+
}
72+
73+
type buildError struct {
74+
err error
75+
output string
76+
}
77+
78+
func (e *buildError) Error() string {
79+
return e.err.Error() + ": " + e.output
80+
}
81+
82+
// TestE2E_Help verifies that --help flag works and produces expected output.
83+
func TestE2E_Help(t *testing.T) {
84+
binary := buildBinary(t)
85+
86+
cmd := exec.Command(binary, "--help")
87+
output, err := cmd.CombinedOutput()
88+
89+
require.NoError(t, err, "sqlcmd --help should not error")
90+
assert.Contains(t, string(output), "sqlcmd", "help output should mention sqlcmd")
91+
assert.Contains(t, string(output), "Usage:", "help output should contain Usage section")
92+
}
93+
94+
// TestE2E_Version verifies that --version flag works.
95+
func TestE2E_Version(t *testing.T) {
96+
binary := buildBinary(t)
97+
98+
cmd := exec.Command(binary, "--version")
99+
output, err := cmd.CombinedOutput()
100+
101+
require.NoError(t, err, "sqlcmd --version should not error")
102+
// Version output should contain version info
103+
outputStr := string(output)
104+
assert.True(t, strings.Contains(outputStr, "Version") || strings.Contains(outputStr, "version") || strings.Contains(outputStr, "v"),
105+
"version output should contain version info: %s", outputStr)
106+
}
107+
108+
// TestE2E_PipedInput_NoPanic verifies that piping input to sqlcmd with -G flag
109+
// does not cause a nil pointer panic. This is a regression test for issue #607.
110+
// The command will fail to connect because it targets a non-existent server, but it should
111+
// NOT panic - that's the key behavior we're testing.
112+
func TestE2E_PipedInput_NoPanic(t *testing.T) {
113+
binary := buildBinary(t)
114+
115+
// Create a command that pipes input
116+
cmd := exec.Command(binary, "-G", "-S", "nonexistent.database.windows.net", "-d", "testdb")
117+
cmd.Stdin = strings.NewReader("SELECT 1")
118+
119+
// Run the command - we expect it to fail (can't connect), but NOT panic
120+
output, err := cmd.CombinedOutput()
121+
outputStr := string(output)
122+
123+
// The command should fail with a connection error, not a panic
124+
if err != nil {
125+
// This is expected - we can't connect to a non-existent server
126+
// But we should NOT see a panic in the output
127+
assert.NotContains(t, outputStr, "panic:", "sqlcmd should not panic when piping input")
128+
assert.NotContains(t, outputStr, "nil pointer", "sqlcmd should not have nil pointer error")
129+
assert.NotContains(t, outputStr, "runtime error", "sqlcmd should not have runtime error")
130+
}
131+
// If it somehow succeeded (unlikely), that's fine too
132+
}
133+
134+
// TestE2E_PipedInput_LiveConnection tests piping input with a real SQL Server connection.
135+
// This test only runs when SQLCMDSERVER is set.
136+
func TestE2E_PipedInput_LiveConnection(t *testing.T) {
137+
skipIfNoLiveConnection(t)
138+
binary := buildBinary(t)
139+
140+
cmd := exec.Command(binary, "-C")
141+
cmd.Stdin = strings.NewReader("SELECT 1 AS TestValue\nGO\n")
142+
cmd.Env = os.Environ() // Inherit SQLCMDSERVER, SQLCMDUSER, SQLCMDPASSWORD
143+
144+
output, err := cmd.CombinedOutput()
145+
outputStr := string(output)
146+
147+
require.NoError(t, err, "piped query should succeed with live connection: %s", outputStr)
148+
assert.Contains(t, outputStr, "TestValue", "output should contain column name")
149+
assert.Contains(t, outputStr, "1", "output should contain query result")
150+
}
151+
152+
// TestE2E_PipedInput_EmptyInput verifies that piping empty input doesn't panic.
153+
func TestE2E_PipedInput_EmptyInput(t *testing.T) {
154+
binary := buildBinary(t)
155+
156+
cmd := exec.Command(binary, "-S", "nonexistent.server")
157+
cmd.Stdin = strings.NewReader("")
158+
159+
output, err := cmd.CombinedOutput()
160+
outputStr := string(output)
161+
162+
// Should fail with connection error, not panic
163+
if err != nil {
164+
assert.NotContains(t, outputStr, "panic:", "sqlcmd should not panic with empty piped input")
165+
assert.NotContains(t, outputStr, "nil pointer", "sqlcmd should not have nil pointer error")
166+
}
167+
}
168+
169+
// TestE2E_InvalidFlag verifies that invalid flags produce a helpful error message.
170+
func TestE2E_InvalidFlag(t *testing.T) {
171+
binary := buildBinary(t)
172+
173+
cmd := exec.Command(binary, "--this-flag-does-not-exist")
174+
output, err := cmd.CombinedOutput()
175+
176+
assert.Error(t, err, "invalid flag should cause an error")
177+
outputStr := string(output)
178+
// Should have some kind of error message about unknown flag
179+
assert.True(t, strings.Contains(outputStr, "unknown") || strings.Contains(outputStr, "invalid") || strings.Contains(outputStr, "flag"),
180+
"error message should indicate unknown/invalid flag: %s", outputStr)
181+
}
182+
183+
// TestE2E_QueryFlag_NoServer verifies -Q flag behavior without a server.
184+
func TestE2E_QueryFlag_NoServer(t *testing.T) {
185+
binary := buildBinary(t)
186+
187+
cmd := exec.Command(binary, "-Q", "SELECT 1")
188+
output, err := cmd.CombinedOutput()
189+
outputStr := string(output)
190+
191+
// Should fail because no server is specified, but not panic
192+
if err != nil {
193+
assert.NotContains(t, outputStr, "panic:", "sqlcmd should not panic")
194+
}
195+
}
196+
197+
// TestE2E_QueryFlag_LiveConnection tests the -Q flag with a real SQL Server connection.
198+
// This test only runs when SQLCMDSERVER is set.
199+
func TestE2E_QueryFlag_LiveConnection(t *testing.T) {
200+
skipIfNoLiveConnection(t)
201+
binary := buildBinary(t)
202+
203+
cmd := exec.Command(binary, "-C", "-Q", "SELECT 42 AS Answer")
204+
cmd.Env = os.Environ()
205+
206+
output, err := cmd.CombinedOutput()
207+
outputStr := string(output)
208+
209+
require.NoError(t, err, "-Q query should succeed: %s", outputStr)
210+
assert.Contains(t, outputStr, "Answer", "output should contain column name")
211+
assert.Contains(t, outputStr, "42", "output should contain query result")
212+
}
213+
214+
// TestE2E_InputFile_NotFound verifies proper error when input file doesn't exist.
215+
func TestE2E_InputFile_NotFound(t *testing.T) {
216+
binary := buildBinary(t)
217+
218+
cmd := exec.Command(binary, "-i", "/nonexistent/path/to/file.sql", "-S", "localhost")
219+
output, err := cmd.CombinedOutput()
220+
221+
assert.Error(t, err, "non-existent input file should cause an error")
222+
outputStr := string(output)
223+
assert.NotContains(t, outputStr, "panic:", "should not panic on missing input file")
224+
}
225+
226+
// TestE2E_InputFile_LiveConnection tests the -i flag with a real SQL Server connection.
227+
// This test only runs when SQLCMDSERVER is set.
228+
func TestE2E_InputFile_LiveConnection(t *testing.T) {
229+
skipIfNoLiveConnection(t)
230+
binary := buildBinary(t)
231+
232+
// Create a temporary SQL file
233+
tmpFile, err := os.CreateTemp("", "e2e-test-*.sql")
234+
require.NoError(t, err)
235+
defer os.Remove(tmpFile.Name())
236+
237+
_, err = tmpFile.WriteString("SELECT 'InputFileTest' AS Source\nGO\n")
238+
require.NoError(t, err)
239+
require.NoError(t, tmpFile.Close())
240+
241+
cmd := exec.Command(binary, "-C", "-i", tmpFile.Name())
242+
cmd.Env = os.Environ()
243+
244+
output, err := cmd.CombinedOutput()
245+
outputStr := string(output)
246+
247+
require.NoError(t, err, "-i input file should succeed: %s", outputStr)
248+
assert.Contains(t, outputStr, "InputFileTest", "output should contain query result from input file")
249+
}
250+
251+
// TestE2E_PipedInput_WithStdinReader verifies piping works with bytes.Buffer.
252+
func TestE2E_PipedInput_WithStdinReader(t *testing.T) {
253+
binary := buildBinary(t)
254+
255+
input := bytes.NewBufferString("SELECT @@VERSION\nGO\n")
256+
cmd := exec.Command(binary, "-S", "nonexistent.server", "-C")
257+
cmd.Stdin = input
258+
259+
output, err := cmd.CombinedOutput()
260+
outputStr := string(output)
261+
262+
// Should fail to connect, but not panic
263+
if err != nil {
264+
assert.NotContains(t, outputStr, "panic:", "should not panic when piping SQL with GO")
265+
assert.NotContains(t, outputStr, "nil pointer", "should not have nil pointer error")
266+
}
267+
}
268+
269+
// cleanupBinary can be called to remove the test binary.
270+
// In practice, os.TempDir cleanup handles this, but this is here for explicit cleanup if needed.
271+
func cleanupBinary() {
272+
if binaryPath != "" {
273+
os.RemoveAll(filepath.Dir(binaryPath))
274+
}
275+
}
276+
277+
func TestMain(m *testing.M) {
278+
code := m.Run()
279+
cleanupBinary()
280+
os.Exit(code)
281+
}

0 commit comments

Comments
 (0)