Skip to content

Commit cb84ded

Browse files
committed
Deterministic calc of POSIX return code for Fatal
1 parent 75dacb4 commit cb84ded

File tree

6 files changed

+95
-34
lines changed

6 files changed

+95
-34
lines changed

internal/exit/exit.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,32 @@ package exit
2424

2525
import "os"
2626

27-
var real = func() { os.Exit(1) }
27+
var realFn = func(code int) { os.Exit(code) }
2828

2929
// Exit normally terminates the process by calling os.Exit(1). If the package
3030
// is stubbed, it instead records a call in the testing spy.
31+
// Deprecated: use With() instead.
3132
func Exit() {
32-
real()
33+
With(1)
34+
}
35+
36+
// With terminates the process by calling os.Exit(code). If the package is
37+
// stubbed, it instead records a call in the testing spy.
38+
func With(code int) {
39+
realFn(code)
3340
}
3441

3542
// A StubbedExit is a testing fake for os.Exit.
3643
type StubbedExit struct {
3744
Exited bool
38-
prev func()
45+
Code int
46+
prev func(code int)
3947
}
4048

4149
// Stub substitutes a fake for the call to os.Exit(1).
4250
func Stub() *StubbedExit {
43-
s := &StubbedExit{prev: real}
44-
real = s.exit
51+
s := &StubbedExit{prev: realFn}
52+
realFn = s.exit
4553
return s
4654
}
4755

@@ -56,9 +64,10 @@ func WithStub(f func()) *StubbedExit {
5664

5765
// Unstub restores the previous exit function.
5866
func (se *StubbedExit) Unstub() {
59-
real = se.prev
67+
realFn = se.prev
6068
}
6169

62-
func (se *StubbedExit) exit() {
70+
func (se *StubbedExit) exit(code int) {
6371
se.Exited = true
72+
se.Code = code
6473
}

internal/exit/exit_test.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,34 @@
1818
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1919
// THE SOFTWARE.
2020

21-
package exit
21+
package exit_test
2222

2323
import (
2424
"testing"
2525

2626
"github.com/stretchr/testify/assert"
27+
"go.uber.org/zap/internal/exit"
2728
)
2829

2930
func TestStub(t *testing.T) {
31+
type want struct {
32+
exit bool
33+
code int
34+
}
3035
tests := []struct {
31-
f func()
32-
want bool
36+
f func()
37+
want
3338
}{
34-
{Exit, true},
35-
{func() {}, false},
39+
{func() {
40+
exit.With(42)
41+
}, want{exit: true, code: 42}},
42+
{exit.Exit, want{exit: true, code: 1}},
43+
{func() {}, want{}},
3644
}
3745

3846
for _, tt := range tests {
39-
s := WithStub(tt.f)
40-
assert.Equal(t, tt.want, s.Exited, "Stub captured unexpected exit value.")
47+
s := exit.WithStub(tt.f)
48+
assert.Equal(t, tt.want.exit, s.Exited, "Stub captured unexpected exit value.")
49+
assert.Equal(t, tt.want.code, s.Code, "Stub captured unexpected exit value.")
4150
}
4251
}

logger.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,9 +288,9 @@ func (log *Logger) check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry {
288288
ce = ce.Should(ent, zapcore.WriteThenPanic)
289289
case zapcore.FatalLevel:
290290
onFatal := log.onFatal
291-
// Noop is the default value for CheckWriteAction, and it leads to
291+
// The nil is the default value for CheckWriteAction, and it leads to
292292
// continued execution after a Fatal which is unexpected.
293-
if onFatal == zapcore.WriteThenNoop {
293+
if onFatal == nil {
294294
onFatal = zapcore.WriteThenFatal
295295
}
296296
ce = ce.Should(ent, onFatal)

logger_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,15 +213,23 @@ func TestLoggerAlwaysFatals(t *testing.T) {
213213
// Users can disable writing out fatal-level logs, but calls to logger.Fatal()
214214
// should still terminate the process.
215215
withLogger(t, FatalLevel+1, nil, func(logger *Logger, logs *observer.ObservedLogs) {
216-
stub := exit.WithStub(func() { logger.Fatal("") })
216+
stub := exit.WithStub(func() { logger.Fatal("foo") })
217217
assert.True(t, stub.Exited, "Expected calls to logger.Fatal to terminate process.")
218+
assert.Equal(t, 1, stub.Code, "Expected calls to logger.Fatal to terminate process with predictable retcode.")
219+
220+
logger = logger.WithOptions(OnFatal(zapcore.WriteThenPosixExitCode))
221+
err := errors.New("bar")
222+
stub = exit.WithStub(func() { logger.Fatal("foo", Error(err)) })
223+
assert.True(t, stub.Exited, "Expected calls to logger.Fatal to terminate process.")
224+
assert.Equal(t, 129, stub.Code, "Expected calls to logger.Fatal to terminate process with predictable retcode.")
218225

219226
stub = exit.WithStub(func() {
220227
if ce := logger.Check(FatalLevel, ""); ce != nil {
221228
ce.Write()
222229
}
223230
})
224231
assert.True(t, stub.Exited, "Expected calls to logger.Check(FatalLevel, ...) to terminate process.")
232+
assert.Equal(t, 1, stub.Code, "Expected calls to logger.Check(FatalLevel, ...) to terminate process with predictable retcode.")
225233

226234
assert.Equal(t, 0, logs.Len(), "Shouldn't write out logs when fatal-level logging is disabled.")
227235
})

zapcore/entry.go

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package zapcore
2222

2323
import (
2424
"fmt"
25+
"hash/crc32"
2526
"runtime"
2627
"strings"
2728
"sync"
@@ -154,18 +155,30 @@ type Entry struct {
154155

155156
// CheckWriteAction indicates what action to take after a log entry is
156157
// processed. Actions are ordered in increasing severity.
157-
type CheckWriteAction uint8
158+
type CheckWriteAction func(*CheckedEntry, []Field)
158159

159-
const (
160+
var (
160161
// WriteThenNoop indicates that nothing special needs to be done. It's the
161162
// default behavior.
162-
WriteThenNoop CheckWriteAction = iota
163+
WriteThenNoop CheckWriteAction
163164
// WriteThenGoexit runs runtime.Goexit after Write.
164-
WriteThenGoexit
165+
WriteThenGoexit CheckWriteAction = func(ce *CheckedEntry, fields []Field) {
166+
runtime.Goexit()
167+
}
165168
// WriteThenPanic causes a panic after Write.
166-
WriteThenPanic
167-
// WriteThenFatal causes a fatal os.Exit after Write.
168-
WriteThenFatal
169+
WriteThenPanic CheckWriteAction = func(ce *CheckedEntry, fields []Field) {
170+
panic(ce.Message)
171+
}
172+
// WriteThenFatal causes a fatal os.Exit(1) after Write.
173+
WriteThenFatal CheckWriteAction = func(ce *CheckedEntry, fields []Field) {
174+
exit.With(1)
175+
}
176+
// WriteThenPosixExitCode causes an os.Exit(code) after Write. The code is
177+
// calculated deterministically from the message, or from attached error
178+
// Field.
179+
WriteThenPosixExitCode CheckWriteAction = func(ce *CheckedEntry, fields []Field) {
180+
exit.With(retcode(ce, fields))
181+
}
169182
)
170183

171184
// CheckedEntry is an Entry together with a collection of Cores that have
@@ -186,7 +199,9 @@ func (ce *CheckedEntry) reset() {
186199
ce.Entry = Entry{}
187200
ce.ErrorOutput = nil
188201
ce.dirty = false
189-
ce.should = WriteThenNoop
202+
if ce.should != nil {
203+
ce.should = WriteThenNoop
204+
}
190205
for i := range ce.cores {
191206
// don't keep references to cores
192207
ce.cores[i] = nil
@@ -224,16 +239,11 @@ func (ce *CheckedEntry) Write(fields ...Field) {
224239
ce.ErrorOutput.Sync()
225240
}
226241

227-
should, msg := ce.should, ce.Message
228242
putCheckedEntry(ce)
229243

230-
switch should {
231-
case WriteThenPanic:
232-
panic(msg)
233-
case WriteThenFatal:
234-
exit.Exit()
235-
case WriteThenGoexit:
236-
runtime.Goexit()
244+
// Terminal operation
245+
if ce.should != nil {
246+
ce.should(ce, fields)
237247
}
238248
}
239249

@@ -260,3 +270,14 @@ func (ce *CheckedEntry) Should(ent Entry, should CheckWriteAction) *CheckedEntry
260270
ce.should = should
261271
return ce
262272
}
273+
274+
func retcode(ce *CheckedEntry, fields []Field) int {
275+
msg := ce.Message
276+
for _, field := range fields {
277+
if field.Type == ErrorType {
278+
msg = field.Interface.(error).Error()
279+
break
280+
}
281+
}
282+
return int(crc32.ChecksumIEEE([]byte(msg)))%254 + 1
283+
}

zapcore/entry_test.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func TestPutNilEntry(t *testing.T) {
6464
assert.NotNil(t, ce, "Expected only non-nil CheckedEntries in pool.")
6565
assert.False(t, ce.dirty, "Unexpected dirty bit set.")
6666
assert.Nil(t, ce.ErrorOutput, "Non-nil ErrorOutput.")
67-
assert.Equal(t, WriteThenNoop, ce.should, "Unexpected terminal behavior.")
67+
assert.Nil(t, ce.should, "Unexpected terminal behavior.")
6868
assert.Equal(t, 0, len(ce.cores), "Expected empty slice of cores.")
6969
assert.True(t, cap(ce.cores) > 0, "Expected pooled CheckedEntries to pre-allocate slice of Cores.")
7070
}
@@ -124,9 +124,23 @@ func TestCheckedEntryWrite(t *testing.T) {
124124
t.Run("WriteThenFatal", func(t *testing.T) {
125125
var ce *CheckedEntry
126126
ce = ce.Should(Entry{}, WriteThenFatal)
127+
ce.Message = t.Name()
127128
stub := exit.WithStub(func() {
128129
ce.Write()
129130
})
130131
assert.True(t, stub.Exited, "Expected to exit when WriteThenFatal is set.")
132+
assert.Equal(t, 1, stub.Code, "Expected to exit when WriteThenFatal is set.")
133+
})
134+
135+
t.Run("WriteThenPosixExitCode", func(t *testing.T) {
136+
var ce *CheckedEntry
137+
ce = ce.Should(Entry{
138+
Message: "foo",
139+
}, WriteThenPosixExitCode)
140+
stub := exit.WithStub(func() {
141+
ce.Write()
142+
})
143+
assert.True(t, stub.Exited, "Expected to exit when WriteThenPosixExitCode is set.")
144+
assert.Equal(t, 38, stub.Code, "Expected to exit with specific code when WriteThenPosixExitCode is set.")
131145
})
132146
}

0 commit comments

Comments
 (0)