-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathresult.go
More file actions
245 lines (212 loc) · 7.91 KB
/
Copy pathresult.go
File metadata and controls
245 lines (212 loc) · 7.91 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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
package processkit
import (
"fmt"
"strings"
"time"
)
// Outcome describes how a process ended: a normal exit with a code, a kill by a
// signal (Unix only), or a timeout. Inspect it via the accessors; a missing exit
// code is reported as (0, false), never a fabricated -1 sentinel.
//
// Outcomes are normally produced by running a command; for a fake [ProcessRunner]
// or a custom runner, build one with [Exited], [Signalled], or [TimedOut] (and a
// whole [Result] with [NewResult]). The zero value is exited(0) — build outcomes
// with the constructors rather than relying on a bare Outcome{}.
type Outcome struct {
kind outcomeKind
code int // valid when kind == outcomeExited
signal int // valid when kind == outcomeSignalled && hasSignal
hasSignal bool // whether a signal number is known
}
type outcomeKind uint8
const (
outcomeExited outcomeKind = iota
outcomeSignalled
outcomeTimedOut
)
// exited builds an Outcome for a normal termination with the given exit code.
func exited(code int) Outcome { return Outcome{kind: outcomeExited, code: code} }
// signalled builds an Outcome for a signal kill; sig is the signal number, or nil
// when unknown. Unix-only — a Windows kill surfaces as exited, never signalled
// (report the platform truth, don't fabricate a signal from an NTSTATUS).
func signalled(sig *int) Outcome {
o := Outcome{kind: outcomeSignalled}
if sig != nil {
o.signal, o.hasSignal = *sig, true
}
return o
}
// timedOut builds an Outcome for a run killed by its own deadline.
func timedOut() Outcome { return Outcome{kind: outcomeTimedOut} }
// Exited builds an [Outcome] for a normal termination with the given exit code.
// It is a construction seam for fake [ProcessRunner]s (see [NewResult]); real
// runs produce outcomes themselves.
func Exited(code int) Outcome { return exited(code) }
// Signalled builds an [Outcome] for a Unix signal kill with the given signal
// number — for fakes modelling a signal kill. (A real Windows kill is reported as
// exited, never signalled.)
func Signalled(signal int) Outcome { return signalled(&signal) }
// TimedOut builds an [Outcome] for a run killed by its own deadline — for fakes.
func TimedOut() Outcome { return timedOut() }
// Code returns the exit code and true for a normal exit; (0, false) for a signal
// kill or a timeout.
func (o Outcome) Code() (int, bool) {
if o.kind == outcomeExited {
return o.code, true
}
return 0, false
}
// Signal returns the signal number and true when the process was killed by a
// known signal (Unix only); (0, false) otherwise.
func (o Outcome) Signal() (int, bool) {
if o.kind == outcomeSignalled && o.hasSignal {
return o.signal, true
}
return 0, false
}
// TimedOut reports whether the run was killed by its own timeout.
func (o Outcome) TimedOut() bool { return o.kind == outcomeTimedOut }
// String renders the outcome, e.g. "exited(0)", "signalled(9)", "timedOut".
func (o Outcome) String() string {
switch o.kind {
case outcomeExited:
return fmt.Sprintf("exited(%d)", o.code)
case outcomeSignalled:
if o.hasSignal {
return fmt.Sprintf("signalled(%d)", o.signal)
}
return "signalled"
case outcomeTimedOut:
return "timedOut"
default:
return "unknown"
}
}
// Result is the captured result of a run. A non-zero exit is *data* here, not an
// error: it is reported in the Outcome and only turned into an error by the
// success-requiring verbs ([Cmd.Run], [Cmd.ExitCode], [Cmd.Probe]) or by
// [Result.Err].
type Result struct {
program string
args []string
outcome Outcome
stdout []byte // captured stdout, exactly as produced (use Stdout for \n-normalized text)
stderr string // captured stderr, normalized to \n
duration time.Duration
okCodes []int // exit codes treated as success in addition to 0
mechanism Mechanism
}
// NewResult builds a [Result] for a fake [ProcessRunner] to return from its Output
// method — the construction seam the dependency-injection model needs (a real run
// produces a Result through the verbs; a double or a custom runner builds one
// here). The program, args, and ok-codes are taken from inv; stdout is stored as
// produced and stderr is normalized to \n. The mechanism is [MechanismUnknown] and
// the duration is zero (set neither for a fake). Build the outcome with [Exited],
// [Signalled], or [TimedOut].
func NewResult(inv Invocation, outcome Outcome, stdout, stderr []byte) *Result {
return &Result{
program: inv.Program,
args: append([]string(nil), inv.Args...),
outcome: outcome,
stdout: append([]byte(nil), stdout...),
stderr: normalizeNewlines(stderr),
okCodes: append([]int(nil), inv.OkCodes...),
mechanism: MechanismUnknown,
}
}
// Program returns the program that was run.
func (r *Result) Program() string { return r.program }
// Args returns a copy of the arguments the program was run with.
func (r *Result) Args() []string { return append([]string(nil), r.args...) }
// Outcome returns how the process ended.
func (r *Result) Outcome() Outcome { return r.outcome }
// StdoutBytes returns a copy of the captured stdout, exactly as produced (line
// endings preserved). Use this for binary output; use [Result.Stdout] for text.
func (r *Result) StdoutBytes() []byte { return append([]byte(nil), r.stdout...) }
// Stdout returns the captured stdout as text, with line endings normalized to
// \n. Use [Result.StdoutBytes] for exact bytes.
func (r *Result) Stdout() string { return normalizeNewlines(r.stdout) }
// Stderr returns the captured stderr as text (normalized to \n).
func (r *Result) Stderr() string { return r.stderr }
// Duration returns how long the run took.
func (r *Result) Duration() time.Duration { return r.duration }
// Mechanism returns the containment mechanism that was in effect for the run.
func (r *Result) Mechanism() Mechanism { return r.mechanism }
// Code returns the exit code and true for a normal exit; (0, false) otherwise.
func (r *Result) Code() (int, bool) { return r.outcome.Code() }
// TimedOut reports whether the run was killed by its own timeout.
func (r *Result) TimedOut() bool { return r.outcome.TimedOut() }
// Success reports whether the run succeeded: a clean exit whose code is 0 or in
// the configured OkCodes set.
func (r *Result) Success() bool {
code, ok := r.outcome.Code()
if !ok {
return false
}
if code == 0 {
return true
}
for _, ok2 := range r.okCodes {
if code == ok2 {
return true
}
}
return false
}
// Err returns nil when the run was a success, otherwise an [*ExitError] carrying
// the captured streams (as sanitized text). The success-requiring verbs report this.
func (r *Result) Err() error {
if r.Success() {
return nil
}
return r.toExitError()
}
// resultRun, resultExitCode, and resultProbe are the shared success-requiring
// interpretations behind the Run / ExitCode / Probe verbs of both [Cmd] and
// [Pipeline] (Cmd runs them through retryRun; Pipeline applies them to its folded
// Result). Keeping them here makes the verb semantics one definition, not two.
func resultRun(res *Result) (string, error) {
if err := res.Err(); err != nil {
return "", err
}
return strings.TrimRight(res.Stdout(), " \t\r\n"), nil
}
func resultExitCode(res *Result) (int, error) {
code, ok := res.Code()
if !ok {
return 0, res.toExitError()
}
return code, nil
}
func resultProbe(res *Result) (bool, error) {
code, ok := res.Code()
if !ok {
return false, res.toExitError()
}
switch code {
case 0:
return true, nil
case 1:
return false, nil
default:
return false, res.toExitError()
}
}
func (r *Result) toExitError() *ExitError {
return &ExitError{
Program: r.program,
Outcome: r.outcome,
Stdout: normalizeNewlines(r.stdout),
Stderr: r.stderr,
Mechanism: r.mechanism,
}
}
// normalizeNewlines converts CRLF and lone CR to LF, for text accessors.
func normalizeNewlines(b []byte) string {
s := string(b)
if !strings.ContainsRune(s, '\r') {
return s
}
s = strings.ReplaceAll(s, "\r\n", "\n")
return strings.ReplaceAll(s, "\r", "\n")
}