Skip to content

Commit 8323fc5

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: - 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 The tests build the binary once and reuse it for all tests. Non-connection tests can run anywhere without a SQL Server. Addresses feedback from @shueybubbles in PR microsoft#640.
1 parent e31f42f commit 8323fc5

1 file changed

Lines changed: 208 additions & 0 deletions

File tree

cmd/modern/e2e_test.go

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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+
cmd.Dir = filepath.Dir(binaryPath)
45+
// Build from the cmd/modern directory
46+
wd, _ := os.Getwd()
47+
cmd.Dir = wd
48+
output, err := cmd.CombinedOutput()
49+
if err != nil {
50+
buildErr = &buildError{err: err, output: string(output)}
51+
return
52+
}
53+
})
54+
if buildErr != nil {
55+
t.Fatalf("Failed to build sqlcmd binary: %v", buildErr)
56+
}
57+
return binaryPath
58+
}
59+
60+
type buildError struct {
61+
err error
62+
output string
63+
}
64+
65+
func (e *buildError) Error() string {
66+
return e.err.Error() + ": " + e.output
67+
}
68+
69+
// TestE2E_Help verifies that --help flag works and produces expected output.
70+
func TestE2E_Help(t *testing.T) {
71+
binary := buildBinary(t)
72+
73+
cmd := exec.Command(binary, "--help")
74+
output, err := cmd.CombinedOutput()
75+
76+
require.NoError(t, err, "sqlcmd --help should not error")
77+
assert.Contains(t, string(output), "sqlcmd", "help output should mention sqlcmd")
78+
assert.Contains(t, string(output), "Usage:", "help output should contain Usage section")
79+
}
80+
81+
// TestE2E_Version verifies that --version flag works.
82+
func TestE2E_Version(t *testing.T) {
83+
binary := buildBinary(t)
84+
85+
cmd := exec.Command(binary, "--version")
86+
output, err := cmd.CombinedOutput()
87+
88+
require.NoError(t, err, "sqlcmd --version should not error")
89+
// Version output should contain version info
90+
outputStr := string(output)
91+
assert.True(t, strings.Contains(outputStr, "Version") || strings.Contains(outputStr, "version") || strings.Contains(outputStr, "v"),
92+
"version output should contain version info: %s", outputStr)
93+
}
94+
95+
// TestE2E_PipedInput_NoPanic verifies that piping input to sqlcmd with -G flag
96+
// does not cause a nil pointer panic. This is a regression test for issue #607.
97+
// The command will fail to connect (no server specified properly), but it should
98+
// NOT panic - that's the key behavior we're testing.
99+
func TestE2E_PipedInput_NoPanic(t *testing.T) {
100+
binary := buildBinary(t)
101+
102+
// Create a command that pipes input
103+
cmd := exec.Command(binary, "-G", "-S", "nonexistent.database.windows.net", "-d", "testdb")
104+
cmd.Stdin = strings.NewReader("SELECT 1")
105+
106+
// Run the command - we expect it to fail (can't connect), but NOT panic
107+
output, err := cmd.CombinedOutput()
108+
outputStr := string(output)
109+
110+
// The command should fail with a connection error, not a panic
111+
if err != nil {
112+
// This is expected - we can't connect to a non-existent server
113+
// But we should NOT see a panic in the output
114+
assert.NotContains(t, outputStr, "panic:", "sqlcmd should not panic when piping input")
115+
assert.NotContains(t, outputStr, "nil pointer", "sqlcmd should not have nil pointer error")
116+
assert.NotContains(t, outputStr, "runtime error", "sqlcmd should not have runtime error")
117+
}
118+
// If it somehow succeeded (unlikely), that's fine too
119+
}
120+
121+
// TestE2E_PipedInput_EmptyInput verifies that piping empty input doesn't panic.
122+
func TestE2E_PipedInput_EmptyInput(t *testing.T) {
123+
binary := buildBinary(t)
124+
125+
cmd := exec.Command(binary, "-S", "nonexistent.server")
126+
cmd.Stdin = strings.NewReader("")
127+
128+
output, err := cmd.CombinedOutput()
129+
outputStr := string(output)
130+
131+
// Should fail with connection error, not panic
132+
if err != nil {
133+
assert.NotContains(t, outputStr, "panic:", "sqlcmd should not panic with empty piped input")
134+
assert.NotContains(t, outputStr, "nil pointer", "sqlcmd should not have nil pointer error")
135+
}
136+
}
137+
138+
// TestE2E_InvalidFlag verifies that invalid flags produce a helpful error message.
139+
func TestE2E_InvalidFlag(t *testing.T) {
140+
binary := buildBinary(t)
141+
142+
cmd := exec.Command(binary, "--this-flag-does-not-exist")
143+
output, err := cmd.CombinedOutput()
144+
145+
assert.Error(t, err, "invalid flag should cause an error")
146+
outputStr := string(output)
147+
// Should have some kind of error message about unknown flag
148+
assert.True(t, strings.Contains(outputStr, "unknown") || strings.Contains(outputStr, "invalid") || strings.Contains(outputStr, "flag"),
149+
"error message should indicate unknown/invalid flag: %s", outputStr)
150+
}
151+
152+
// TestE2E_QueryFlag_NoServer verifies -Q flag behavior without a server.
153+
func TestE2E_QueryFlag_NoServer(t *testing.T) {
154+
binary := buildBinary(t)
155+
156+
cmd := exec.Command(binary, "-Q", "SELECT 1")
157+
output, err := cmd.CombinedOutput()
158+
outputStr := string(output)
159+
160+
// Should fail because no server is specified, but not panic
161+
if err != nil {
162+
assert.NotContains(t, outputStr, "panic:", "sqlcmd should not panic")
163+
}
164+
}
165+
166+
// TestE2E_InputFile_NotFound verifies proper error when input file doesn't exist.
167+
func TestE2E_InputFile_NotFound(t *testing.T) {
168+
binary := buildBinary(t)
169+
170+
cmd := exec.Command(binary, "-i", "/nonexistent/path/to/file.sql", "-S", "localhost")
171+
output, err := cmd.CombinedOutput()
172+
173+
assert.Error(t, err, "non-existent input file should cause an error")
174+
outputStr := string(output)
175+
assert.NotContains(t, outputStr, "panic:", "should not panic on missing input file")
176+
}
177+
178+
// TestE2E_PipedInput_WithStdinReader verifies piping works with bytes.Buffer.
179+
func TestE2E_PipedInput_WithStdinReader(t *testing.T) {
180+
binary := buildBinary(t)
181+
182+
input := bytes.NewBufferString("SELECT @@VERSION\nGO\n")
183+
cmd := exec.Command(binary, "-S", "nonexistent.server", "-C")
184+
cmd.Stdin = input
185+
186+
output, err := cmd.CombinedOutput()
187+
outputStr := string(output)
188+
189+
// Should fail to connect, but not panic
190+
if err != nil {
191+
assert.NotContains(t, outputStr, "panic:", "should not panic when piping SQL with GO")
192+
assert.NotContains(t, outputStr, "nil pointer", "should not have nil pointer error")
193+
}
194+
}
195+
196+
// cleanupBinary can be called to remove the test binary.
197+
// In practice, os.TempDir cleanup handles this, but this is here for explicit cleanup if needed.
198+
func cleanupBinary() {
199+
if binaryPath != "" {
200+
os.RemoveAll(filepath.Dir(binaryPath))
201+
}
202+
}
203+
204+
func TestMain(m *testing.M) {
205+
code := m.Run()
206+
cleanupBinary()
207+
os.Exit(code)
208+
}

0 commit comments

Comments
 (0)