Skip to content

Commit 419949f

Browse files
BREV-1585: add workbench integration test and actions workflow config (#249)
BREV-1585: add workbench integration test and actions workflow config
1 parent 4231101 commit 419949f

2 files changed

Lines changed: 369 additions & 0 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: CLI Output Compatibility Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
workflow_dispatch:
9+
10+
jobs:
11+
cli-output-compatibility:
12+
runs-on: ubuntu-22.04
13+
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v2
17+
18+
- name: Set up Go
19+
uses: actions/setup-go@v5
20+
with:
21+
go-version: '1.22.6'
22+
cache: true
23+
24+
- name: Install dependencies
25+
run: go mod download
26+
27+
- name: Run CLI output compatibility tests
28+
run: go test -v ./pkg/integration/
29+
30+
- name: Report test results
31+
if: failure()
32+
run: |
33+
echo "❌ CLI Output Compatibility Tests Failed"
34+
echo "This indicates a potential breaking change for external integrations."
35+
echo "Please review the failing tests and ensure backward compatibility."
36+
echo ""
37+
echo "These tests verify that CLI output formats remain stable for:"
38+
echo "- NVIDIA Workbench integration"
39+
echo "- Other external tools that parse brev CLI output"
40+
echo ""
41+
echo "If you intentionally changed output formats, please:"
42+
echo "1. Update the external integrations first"
43+
echo "2. Then update these tests to match the new format"
44+
echo "3. Coordinate with integration maintainers"
45+
46+
- name: Report success
47+
if: success()
48+
run: |
49+
echo "✅ CLI Output Compatibility Tests Passed"
50+
echo "No breaking changes detected for external integrations."
51+
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
package integration
2+
3+
import (
4+
"os/exec"
5+
"regexp"
6+
"strconv"
7+
"strings"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
// These tests verify CLI output formats that external integrations depend on.
15+
// Breaking these tests indicates a breaking change that could affect external tools
16+
// like NVIDIA Workbench that parse brev CLI output.
17+
18+
const (
19+
// Regular expressions
20+
versionPattern = `(\d+\.\d+\.\d+)`
21+
22+
// CLI binary path
23+
brevCLIPath = "../../main.go"
24+
25+
// Version prefixes
26+
currentVersionPrefix = "Current Version:"
27+
currentVersionPrefixAlt = "Current version:"
28+
newVersionPrefix = "New Version:"
29+
30+
// Table column headers
31+
nameColumn = "NAME"
32+
idColumn = "ID"
33+
statusColumn = "STATUS"
34+
machineColumn = "MACHINE"
35+
36+
// Minimum version for external integrations
37+
minBrevVersion = "0.6.306"
38+
)
39+
40+
// Test_VersionCommandOutputFormat verifies that 'brev --version' outputs a parseable version string
41+
func Test_VersionCommandOutputFormat(t *testing.T) {
42+
cmd := exec.Command("go", "run", brevCLIPath, "--version")
43+
output, err := cmd.CombinedOutput()
44+
require.NoError(t, err, "brev --version command should succeed")
45+
46+
outputStr := string(output)
47+
48+
// Test 1: Version should contain "Current version:" or "Current Version:" prefix
49+
hasVersionPrefix := strings.Contains(outputStr, currentVersionPrefix) || strings.Contains(outputStr, currentVersionPrefixAlt)
50+
assert.True(t, hasVersionPrefix, "Version output should contain 'Current Version:' or 'Current version:' prefix")
51+
52+
// Test 2: Should contain a version number in X.Y.Z format (dev builds should also have version)
53+
versionRegexp := regexp.MustCompile(versionPattern)
54+
matches := versionRegexp.FindAllString(outputStr, -1)
55+
56+
// Version should always be present, even in dev environments
57+
require.NotEmpty(t, matches, "CLI version should always be present, cannot be an empty string")
58+
59+
// If we found version numbers, verify the first one is properly formatted
60+
versionStr := matches[0]
61+
versionParts := strings.Split(versionStr, ".")
62+
assert.Len(t, versionParts, 3, "Version should have exactly 3 components")
63+
64+
for i, part := range versionParts {
65+
_, err := strconv.Atoi(part)
66+
assert.NoError(t, err, "Version part %d (%s) should be a valid integer", i, part)
67+
}
68+
}
69+
70+
// Test_OrgListCommandOutputFormat verifies that 'brev org ls' outputs parseable table format
71+
func Test_OrgListCommandOutputFormat(t *testing.T) {
72+
// Note: This test may require authentication, so we'll test the format when orgs exist
73+
cmd := exec.Command("go", "run", brevCLIPath, "org", "ls")
74+
output, err := cmd.CombinedOutput()
75+
if err != nil {
76+
t.Skip("maybe auth error")
77+
return
78+
}
79+
80+
outputStr := string(output)
81+
lines := strings.Split(outputStr, "\n")
82+
83+
// Look for table header format
84+
headerLineIdx := findHeaderLine(lines, nameColumn, idColumn)
85+
if headerLineIdx == -1 {
86+
return // No header found, nothing to validate
87+
}
88+
89+
// Test table header format
90+
headerLine := lines[headerLineIdx]
91+
assert.Contains(t, headerLine, nameColumn, "Org list should have NAME column")
92+
assert.Contains(t, headerLine, idColumn, "Org list should have ID column")
93+
94+
// Test that current org is marked with "*" prefix if there are data rows
95+
validateCurrentOrgMarker(t, lines, headerLineIdx)
96+
}
97+
98+
// findHeaderLine searches for a line containing the specified columns
99+
func findHeaderLine(lines []string, requiredCols ...string) int {
100+
for i, line := range lines {
101+
allPresent := true
102+
for _, col := range requiredCols {
103+
if !strings.Contains(line, col) {
104+
allPresent = false
105+
break
106+
}
107+
}
108+
if allPresent {
109+
return i
110+
}
111+
}
112+
return -1
113+
}
114+
115+
// validateCurrentOrgMarker checks that current org entries are properly marked
116+
func validateCurrentOrgMarker(t *testing.T, lines []string, headerLineIdx int) {
117+
t.Helper()
118+
119+
for i := headerLineIdx + 1; i < len(lines); i++ {
120+
line := strings.TrimSpace(lines[i])
121+
if line == "" {
122+
break
123+
}
124+
// If line starts with "* ", it's the current org
125+
if strings.HasPrefix(line, "* ") {
126+
fields := strings.Fields(line)
127+
assert.GreaterOrEqual(t, len(fields), 2, "Current org line should have at least name and ID")
128+
}
129+
}
130+
}
131+
132+
// Test_InstanceListCommandOutputFormat verifies that 'brev ls' outputs parseable table format
133+
func Test_InstanceListCommandOutputFormat(t *testing.T) {
134+
cmd := exec.Command("go", "run", brevCLIPath, "ls")
135+
output, err := cmd.CombinedOutput()
136+
if err != nil {
137+
t.Skip("maybe auth error")
138+
return
139+
}
140+
141+
outputStr := string(output)
142+
lines := strings.Split(outputStr, "\n")
143+
144+
// Look for table header format
145+
headerLineIdx := findHeaderLine(lines, nameColumn, statusColumn, idColumn, machineColumn)
146+
if headerLineIdx == -1 {
147+
return // No header found, nothing to validate
148+
}
149+
150+
// Test required columns exist
151+
headerLine := lines[headerLineIdx]
152+
assert.Contains(t, headerLine, nameColumn, "Instance list should have NAME column")
153+
assert.Contains(t, headerLine, statusColumn, "Instance list should have STATUS column")
154+
assert.Contains(t, headerLine, idColumn, "Instance list should have ID column")
155+
assert.Contains(t, headerLine, machineColumn, "Instance list should have MACHINE column")
156+
157+
// Test column positions for parsing (important for external integrations)
158+
validateColumnOrder(t, headerLine)
159+
}
160+
161+
// validateColumnOrder ensures columns appear in the expected order for external parsers
162+
func validateColumnOrder(t *testing.T, headerLine string) {
163+
t.Helper()
164+
165+
namePos := strings.Index(headerLine, nameColumn)
166+
statusPos := strings.Index(headerLine, statusColumn)
167+
idPos := strings.Index(headerLine, idColumn)
168+
machinePos := strings.Index(headerLine, machineColumn)
169+
170+
assert.GreaterOrEqual(t, statusPos, namePos, "STATUS column should come after NAME")
171+
assert.GreaterOrEqual(t, idPos, statusPos, "ID column should come after STATUS")
172+
assert.GreaterOrEqual(t, machinePos, idPos, "MACHINE column should come after ID")
173+
}
174+
175+
// Test_RefreshCommandExists verifies that 'brev refresh' command exists and is callable
176+
func Test_RefreshCommandExists(t *testing.T) {
177+
cmd := exec.Command("go", "run", brevCLIPath, "refresh", "--help")
178+
output, _ := cmd.CombinedOutput()
179+
180+
// Should succeed or fail with auth, but not with "command not found"
181+
outputStr := string(output)
182+
assert.NotContains(t, outputStr, "unknown command", "refresh command should exist")
183+
assert.NotContains(t, outputStr, "command not found", "refresh command should exist")
184+
}
185+
186+
// Test_OrgSetCommandExists verifies that 'brev org set' command exists and is callable
187+
func Test_OrgSetCommandExists(t *testing.T) {
188+
cmd := exec.Command("go", "run", brevCLIPath, "org", "set", "--help")
189+
output, _ := cmd.CombinedOutput()
190+
191+
// Should succeed with help output or show that command exists
192+
outputStr := string(output)
193+
// Command exists if it shows help or mentions "set" functionality
194+
hasSetCommand := strings.Contains(outputStr, "set") ||
195+
strings.Contains(outputStr, "organization") ||
196+
!strings.Contains(outputStr, "unknown command")
197+
assert.True(t, hasSetCommand, "org set command should exist and be callable")
198+
}
199+
200+
// Test_StartCommandFormat verifies that 'brev start' command accepts --org flag
201+
func Test_StartCommandFormat(t *testing.T) {
202+
cmd := exec.Command("go", "run", brevCLIPath, "start", "--help")
203+
output, _ := cmd.CombinedOutput()
204+
205+
outputStr := string(output)
206+
// Should mention org flag or organization, or at least not be unknown command
207+
hasOrgSupport := strings.Contains(outputStr, "--org") ||
208+
strings.Contains(outputStr, "organization") ||
209+
(strings.Contains(outputStr, "start") && !strings.Contains(outputStr, "unknown command"))
210+
assert.True(t, hasOrgSupport, "start command should exist and potentially support org specification")
211+
}
212+
213+
// Test_StopCommandExists verifies that 'brev stop' command exists
214+
func Test_StopCommandExists(t *testing.T) {
215+
cmd := exec.Command("go", "run", brevCLIPath, "stop", "--help")
216+
output, _ := cmd.CombinedOutput()
217+
218+
outputStr := string(output)
219+
// Command exists if it shows help or mentions "stop" functionality
220+
hasStopCommand := strings.Contains(outputStr, "stop") ||
221+
!strings.Contains(outputStr, "unknown command")
222+
assert.True(t, hasStopCommand, "stop command should exist")
223+
}
224+
225+
// Test_CommandLineInterfaceStability verifies CLI doesn't break basic usage patterns
226+
func Test_CommandLineInterfaceStability(t *testing.T) {
227+
// Test that help command works
228+
cmd := exec.Command("go", "run", brevCLIPath, "--help")
229+
output, err := cmd.CombinedOutput()
230+
require.NoError(t, err, "brev --help should work")
231+
232+
outputStr := string(output)
233+
234+
// Essential commands should be listed in help
235+
essentialCommands := []string{"ls", "org", "start", "stop", "refresh"}
236+
for _, command := range essentialCommands {
237+
assert.Contains(t, outputStr, command, "Help should list essential command: %s", command)
238+
}
239+
}
240+
241+
// Test_VersionParsingCompatibility tests the version parsing logic that external tools use
242+
func Test_VersionParsingCompatibility(t *testing.T) {
243+
testCases := []struct {
244+
name string
245+
versionOutput string
246+
expectsUpgrade bool
247+
shouldError bool
248+
}{
249+
{
250+
name: "current_version_sufficient",
251+
versionOutput: "Current Version: 0.6.306",
252+
expectsUpgrade: false,
253+
shouldError: false,
254+
},
255+
{
256+
name: "newer_version_no_upgrade",
257+
versionOutput: "Current Version: 0.6.307",
258+
expectsUpgrade: false,
259+
shouldError: false,
260+
},
261+
{
262+
name: "older_version_needs_upgrade",
263+
versionOutput: "Current Version: 0.6.305",
264+
expectsUpgrade: true,
265+
shouldError: false,
266+
},
267+
{
268+
name: "much_older_version",
269+
versionOutput: "Current Version: 0.5.999",
270+
expectsUpgrade: true,
271+
shouldError: false,
272+
},
273+
}
274+
275+
for _, tc := range testCases {
276+
t.Run(tc.name, func(t *testing.T) {
277+
// Extract version using same regex external tools use
278+
versionRegexp := regexp.MustCompile(versionPattern)
279+
versionStr := versionRegexp.FindString(tc.versionOutput)
280+
281+
if tc.shouldError {
282+
assert.Empty(t, versionStr, "Should not find version in malformed output")
283+
return
284+
}
285+
286+
assert.NotEmpty(t, versionStr, "Should find version number")
287+
288+
// Compare version parts (same logic as external integration)
289+
needsUpgrade := compareVersions(t, versionStr, minBrevVersion)
290+
assert.Equal(t, tc.expectsUpgrade, needsUpgrade,
291+
"Version upgrade requirement should match expected for %s", versionStr)
292+
})
293+
}
294+
}
295+
296+
// compareVersions compares two semantic versions and returns true if installed < minimum
297+
func compareVersions(t *testing.T, installedVersion, minVersion string) bool {
298+
t.Helper()
299+
300+
installedComponents := strings.Split(installedVersion, ".")
301+
minComponents := strings.Split(minVersion, ".")
302+
303+
for i := range installedComponents {
304+
installed, err := strconv.Atoi(installedComponents[i])
305+
require.NoError(t, err, "Version component should be integer")
306+
307+
desired, err := strconv.Atoi(minComponents[i])
308+
require.NoError(t, err, "Min version component should be integer")
309+
310+
if installed < desired {
311+
return true
312+
}
313+
if installed > desired {
314+
return false
315+
}
316+
}
317+
return false
318+
}

0 commit comments

Comments
 (0)