Skip to content

Commit e9e7878

Browse files
feat: add end-to-end tests for sqlcmd binary (fixes microsoft#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 microsoft#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 microsoft#640.
1 parent e31f42f commit e9e7878

1 file changed

Lines changed: 287 additions & 0 deletions

File tree

cmd/modern/e2e_test.go

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

0 commit comments

Comments
 (0)