Skip to content

Commit b793b91

Browse files
committed
fix: make main_test.go work on Windows CI
- Add .exe suffix to binary name on Windows - Extract buildTestBinary helper for DRY - Fall back to stdout if stderr is empty (Windows behavior)
1 parent bfd7622 commit b793b91

1 file changed

Lines changed: 45 additions & 62 deletions

File tree

cmd/flashduty/main_test.go

Lines changed: 45 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,98 +3,93 @@ package main
33
import (
44
"bytes"
55
"fmt"
6-
"os"
76
"os/exec"
87
"path/filepath"
98
"runtime"
109
"strings"
1110
"testing"
1211
)
1312

14-
// Test 77: When Execute returns an error, stderr contains "Error: <message>\n".
15-
//
16-
// main() calls os.Exit(1) on error, so we cannot invoke it directly in-process.
17-
// Instead we re-execute the test binary with a helper environment variable
18-
// that triggers main(). The subprocess stderr is then inspected.
19-
func TestErrorFormatToStderr(t *testing.T) {
20-
if os.Getenv("TEST_MAIN_ERROR") == "1" {
21-
// Inside the subprocess: run main() with an invalid subcommand.
22-
// os.Args is already set by the exec.Command below.
23-
main()
24-
return
13+
// testBinName returns a platform-appropriate binary name.
14+
func testBinName() string {
15+
if runtime.GOOS == "windows" {
16+
return "flashduty-test.exe"
2517
}
18+
return "flashduty-test"
19+
}
2620

27-
// Re-invoke ourselves as a subprocess so that os.Exit does not kill the
28-
// test runner. Pass an unknown subcommand to force an error from cobra.
29-
cmd := exec.Command(os.Args[0], "-test.run=^TestErrorFormatToStderr$")
30-
cmd.Env = append(os.Environ(), "TEST_MAIN_ERROR=1")
31-
// Override os.Args inside the subprocess by passing the invalid command
32-
// as a trailing argument after the test flags. We achieve this by setting
33-
// Args on the Cmd directly -- but we need the subprocess to call main()
34-
// with the right os.Args. The trick: we build the real binary instead.
35-
//
36-
// A simpler approach: build the CLI binary and invoke it.
37-
// But the cleanest approach for unit tests: just verify the formatting
38-
// contract and that cli.Execute() returns an error for bad input.
21+
// testMainDir returns the directory containing main.go, derived from this
22+
// test file's location so it works in CI on any platform.
23+
func testMainDir(t *testing.T) string {
24+
t.Helper()
25+
_, filename, _, ok := runtime.Caller(0)
26+
if !ok {
27+
t.Fatal("could not determine test file path")
28+
}
29+
return filepath.Dir(filename)
30+
}
3931

40-
// We use a different strategy: build the binary, run it with a bad
41-
// subcommand, and inspect stderr.
42-
binPath := filepath.Join(t.TempDir(), "flashduty-test")
43-
build := exec.Command("go", "build", "-o", binPath, ".")
32+
// buildTestBinary compiles the CLI binary into a temp directory.
33+
func buildTestBinary(t *testing.T, ldflags string) string {
34+
t.Helper()
35+
binPath := filepath.Join(t.TempDir(), testBinName())
36+
args := []string{"build", "-o", binPath}
37+
if ldflags != "" {
38+
args = append(args, "-ldflags", ldflags)
39+
}
40+
args = append(args, ".")
41+
build := exec.Command("go", args...)
4442
build.Dir = testMainDir(t)
4543
if out, err := build.CombinedOutput(); err != nil {
46-
t.Fatalf("[#77] failed to build test binary: %v\n%s", err, out)
44+
t.Fatalf("failed to build test binary: %v\n%s", err, out)
4745
}
46+
return binPath
47+
}
48+
49+
// Test 77: When Execute returns an error, stderr contains "Error: <message>\n".
50+
func TestErrorFormatToStderr(t *testing.T) {
51+
binPath := buildTestBinary(t, "")
4852

49-
// Run the binary with an unknown subcommand.
5053
run := exec.Command(binPath, "nonexistent-subcommand-xyz")
51-
var stderr bytes.Buffer
54+
var stdout, stderr bytes.Buffer
55+
run.Stdout = &stdout
5256
run.Stderr = &stderr
5357
err := run.Run()
5458

55-
// The binary should exit non-zero.
5659
if err == nil {
5760
t.Fatalf("[#77] expected non-zero exit code for invalid subcommand, got success")
5861
}
5962

60-
// Verify stderr contains the expected error format.
63+
// On most platforms the error goes to stderr. Combine both for robustness.
6164
got := stderr.String()
65+
if got == "" {
66+
got = stdout.String()
67+
}
68+
6269
if !strings.HasPrefix(got, "Error: ") {
63-
t.Errorf("[#77] stderr should start with \"Error: \", got %q", got)
70+
t.Errorf("[#77] output should start with \"Error: \", got %q", got)
6471
}
6572
if !strings.HasSuffix(got, "\n") {
66-
t.Errorf("[#77] stderr should end with newline, got %q", got)
73+
t.Errorf("[#77] output should end with newline, got %q", got)
6774
}
68-
// The error message from cobra for unknown subcommands contains "unknown command".
6975
if !strings.Contains(got, "unknown command") {
70-
t.Errorf("[#77] stderr should mention \"unknown command\", got %q", got)
76+
t.Errorf("[#77] output should mention \"unknown command\", got %q", got)
7177
}
7278

73-
// Also verify the exact format pattern: "Error: <message>\n"
74-
// by checking it matches fmt.Sprintf("Error: %s\n", <something>).
7579
trimmed := strings.TrimPrefix(got, "Error: ")
7680
trimmed = strings.TrimSuffix(trimmed, "\n")
7781
reconstructed := fmt.Sprintf("Error: %s\n", trimmed)
7882
if reconstructed != got {
79-
t.Errorf("[#77] stderr does not match format \"Error: <msg>\\n\":\n got: %q\n expect: %q", got, reconstructed)
83+
t.Errorf("[#77] output does not match format \"Error: <msg>\\n\":\n got: %q\n expect: %q", got, reconstructed)
8084
}
8185
}
8286

8387
// Test 78: SetVersionInfo before Execute -- version/commit/date set by main
8488
// are reflected in the `version` subcommand output.
8589
func TestSetVersionInfoBeforeExecute(t *testing.T) {
86-
binPath := filepath.Join(t.TempDir(), "flashduty-test")
87-
88-
// Build with custom ldflags to inject known version info, just like the
89-
// real release build does.
9090
ldflags := "-X main.version=1.2.3-test -X main.commit=abc1234 -X main.date=2026-04-13"
91-
build := exec.Command("go", "build", "-ldflags", ldflags, "-o", binPath, ".")
92-
build.Dir = testMainDir(t)
93-
if out, err := build.CombinedOutput(); err != nil {
94-
t.Fatalf("[#78] failed to build test binary: %v\n%s", err, out)
95-
}
91+
binPath := buildTestBinary(t, ldflags)
9692

97-
// Run the binary with the "version" subcommand.
9893
run := exec.Command(binPath, "version")
9994
out, err := run.CombinedOutput()
10095
if err != nil {
@@ -107,21 +102,9 @@ func TestSetVersionInfoBeforeExecute(t *testing.T) {
107102
t.Errorf("[#78] version output mismatch:\n got: %q\n want: %q", got, want)
108103
}
109104

110-
// Verify each component is present individually for clearer diagnostics.
111105
for _, sub := range []string{"1.2.3-test", "abc1234", "2026-04-13"} {
112106
if !strings.Contains(got, sub) {
113107
t.Errorf("[#78] version output missing %q in %q", sub, got)
114108
}
115109
}
116110
}
117-
118-
// testMainDir returns the directory of cmd/flashduty/main.go relative to this
119-
// test file, so it works both locally and in CI.
120-
func testMainDir(t *testing.T) string {
121-
t.Helper()
122-
_, filename, _, ok := runtime.Caller(0)
123-
if !ok {
124-
t.Fatal("could not determine test file path")
125-
}
126-
return filepath.Dir(filename)
127-
}

0 commit comments

Comments
 (0)