Skip to content

Commit f43706d

Browse files
feat: add end-to-end tests for sqlcmd binary (fixes #641) (#642)
1 parent 8f3ee3b commit f43706d

File tree

1 file changed

+297
-0
lines changed

1 file changed

+297
-0
lines changed

cmd/modern/e2e_test.go

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

0 commit comments

Comments
 (0)