-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbinary_test.go
More file actions
215 lines (171 loc) · 6.59 KB
/
binary_test.go
File metadata and controls
215 lines (171 loc) · 6.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
package testastic_test
import (
"context"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/monkescience/testastic"
)
func TestBuildBinary(t *testing.T) {
if testing.Short() {
t.Skip("integration test: builds and runs subprocesses")
}
t.Run("builds a reusable binary during a regular test", func(t *testing.T) {
// given: a CLI fixture import path
binary := testastic.BuildBinary(t, "./testdata/testcli")
// when: running the built binary
result := binary.Run(t, "stdout", "built in test")
// then: the reusable binary executes successfully
testastic.Equal(t, 0, result.ExitCode)
testastic.Equal(t, "built in test", result.Stdout)
})
}
func TestBinaryRun(t *testing.T) {
if testing.Short() {
t.Skip("integration test: runs subprocesses")
}
t.Run("captures stdout on success", func(t *testing.T) {
// given: a coverage-instrumented CLI fixture built in TestMain
// when: running the command successfully
result := testCLI.Run(t, "stdout", "binary success output")
// then: stdout is captured and the exit code is zero
testastic.Equal(t, 0, result.ExitCode)
testastic.Equal(t, "binary success output", result.Stdout)
testastic.Equal(t, "", result.Stderr)
})
t.Run("captures non-zero exit without failing the test", func(t *testing.T) {
// given: a CLI command that exits with a non-zero code
// when: running the command
result := testCLI.Run(t, "fail", "7", "binary failure output")
// then: stderr and the exit code are available to the caller
testastic.Equal(t, 7, result.ExitCode)
testastic.Equal(t, "", result.Stdout)
testastic.Equal(t, "binary failure output", result.Stderr)
})
t.Run("passes stdin to the process", func(t *testing.T) {
// given: a CLI command that reads from stdin
input := strings.NewReader("stdin payload")
// when: running with stdin configured
result := testCLI.RunWithOptions(t, []string{"stdin"}, testastic.WithStdin(input))
// then: the process output reflects the provided stdin
testastic.Equal(t, 0, result.ExitCode)
testastic.Equal(t, "stdin payload", result.Stdout)
testastic.Equal(t, "", result.Stderr)
})
t.Run("sets per-run environment variables", func(t *testing.T) {
// given: a CLI command that reads an environment variable
// when: running with a per-invocation environment override
result := testCLI.RunWithOptions(t, []string{"env", "BINARY_TEST_VALUE"},
testastic.WithRunEnv("BINARY_TEST_VALUE=env success value"),
)
// then: the process sees the configured environment variable
testastic.Equal(t, 0, result.ExitCode)
testastic.Equal(t, "env success value", result.Stdout)
})
t.Run("uses the build work dir by default", func(t *testing.T) {
// given: a CLI command that prints its working directory
repoRoot, err := os.Getwd()
testastic.NoError(t, err)
// when: running without an explicit work dir override
result := testCLI.Run(t, "cwd")
// then: the command runs from the same default directory used at build time
testastic.Equal(t, 0, result.ExitCode)
testastic.Equal(t, repoRoot, result.Stdout)
})
t.Run("overrides the work dir per run", func(t *testing.T) {
// given: a CLI command and a custom working directory
customDir := t.TempDir()
resolvedCustomDir, err := filepath.EvalSymlinks(customDir)
testastic.NoError(t, err)
// when: running with an explicit work dir override
result := testCLI.RunWithOptions(t, []string{"cwd"}, testastic.WithRunWorkDir(customDir))
// then: the command runs from the requested directory
testastic.Equal(t, 0, result.ExitCode)
testastic.Equal(t, resolvedCustomDir, result.Stdout)
})
t.Run("times out on hung commands", func(t *testing.T) {
// given: a CLI command that sleeps longer than the configured timeout
mt := newProcessMockT()
defer mt.cleanup()
// when: running with a short timeout
runExpectingFatal(func() {
testCLI.RunWithOptions(mt, []string{"sleep", "2s"}, testastic.WithRunTimeout(50*time.Millisecond))
})
// then: the test fails with a timeout message
fatal, msg := mt.result()
testastic.True(t, fatal)
testastic.Contains(t, msg, "timed out")
})
t.Run("rejects GOCOVERDIR in run env", func(t *testing.T) {
// given: an env override that tries to replace the coverage directory
mt := newProcessMockT()
defer mt.cleanup()
// when: running with a forbidden env key
runExpectingFatal(func() {
testCLI.RunWithOptions(mt, []string{"stdout", "ignored"}, testastic.WithRunEnv("GOCOVERDIR=/tmp/cover"))
})
// then: the test fails with a validation error
fatal, msg := mt.result()
testastic.True(t, fatal)
testastic.Contains(t, msg, "must not include GOCOVERDIR")
})
}
func TestCollectBinaryCoverage(t *testing.T) {
if testing.Short() {
t.Skip("integration test: runs go test on a harness package")
}
t.Run("exported helper collects CLI coverage through TestMain", func(t *testing.T) {
// given: a harness package that uses BuildBinaryMain and CollectSubprocessCoverage
outputPath := filepath.Join(t.TempDir(), "binary.out")
cmd := exec.CommandContext(t.Context(),
"go", "test", "-count=1", "-run", "^TestHarnessCollectsBinaryCoverage$", "./testdata/binaryharness",
)
cmd.Env = append(os.Environ(), "TESTASTIC_PROCESS_COVERAGE_OUT="+outputPath)
// when: running the harness package test suite
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("go test harness failed: %s", output)
}
// then: the exported helper produces a valid text coverage profile
content, readErr := os.ReadFile(outputPath)
testastic.NoError(t, readErr)
testastic.True(t, len(content) > 0)
testastic.HasPrefix(t, string(content), "mode:")
})
}
func TestRunWithOptions_usesTestContext(t *testing.T) {
if testing.Short() {
t.Skip("integration test: runs subprocesses")
}
t.Run("inherits cancellation from the test context", func(t *testing.T) {
// given: a command launched with a cancelled context-backed test double
ctx, cancel := context.WithCancel(context.Background())
cancel()
mt := newRunMockT(ctx)
defer mt.cleanup()
// when: running the binary after the context is already cancelled
runExpectingFatal(func() {
testCLI.Run(mt, "sleep", "10ms")
})
// then: the run fails as an infrastructure error instead of returning an exit code
fatal, msg := mt.result()
testastic.True(t, fatal)
testastic.Contains(t, msg, "context canceled")
})
}
type runMockT struct {
*processMockT
contextFunc func() context.Context
}
func newRunMockT(ctx context.Context) *runMockT {
return &runMockT{
processMockT: newProcessMockT(),
contextFunc: func() context.Context { return ctx },
}
}
func (m *runMockT) Context() context.Context {
return m.contextFunc()
}