Skip to content

Commit

Permalink
Merge pull request #40 from inteon/exit_code_bugfix
Browse files Browse the repository at this point in the history
BUGFIX: return correct error codes and add tests
  • Loading branch information
jetstack-bot authored Mar 25, 2024
2 parents dc17964 + d2ecc27 commit b99f57b
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 2 deletions.
56 changes: 56 additions & 0 deletions internal/util/exit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
Copyright 2020 The cert-manager Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package util

import (
"context"
"errors"
"fmt"
"testing"
)

func TestSetExitCode(t *testing.T) {
tests := []struct {
name string
err error
expCode int
}{
{"Test context.Canceled", context.Canceled, 0},
{"Test wrapped context.Canceled", fmt.Errorf("wrapped: %w", context.Canceled), 0},
{"Test context.DeadlineExceeded", context.DeadlineExceeded, 124},
{"Test wrapped context.DeadlineExceeded", fmt.Errorf("wrapped: %w", context.DeadlineExceeded), 124},
{"Test error", errors.New("error"), 1},
{"Test nil", nil, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Every testExitCode call has to be run in its own test, because
// it calls the test again filtered by the name of the subtest with
// the variable BE_CRASHER=1.
exitCode := testExitCode(t, func(t *testing.T) {
SetExitCode(tt.err)

_, complete := SetupExitHandler(context.Background(), AlwaysErrCode)
complete()
})

if exitCode != tt.expCode {
t.Errorf("Test %s: expected exit code %d, got %d", tt.name, tt.expCode, exitCode)
}
})
}
}
4 changes: 2 additions & 2 deletions internal/util/signal.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func SetupExitHandler(parentCtx context.Context, exitBehavior ExitBehavior) (con
// first signal. Cancel context and pass exit code to errorExitCodeChannel.
signalInt := int((<-c).(syscall.Signal))
if exitBehavior == AlwaysErrCode {
errorExitCodeChannel <- signalInt
errorExitCodeChannel <- (128 + signalInt)
}
cancel(fmt.Errorf("received signal %d", signalInt))
// second signal. Exit directly.
Expand All @@ -70,7 +70,7 @@ func SetupExitHandler(parentCtx context.Context, exitBehavior ExitBehavior) (con
return ctx, func() {
select {
case signalInt := <-errorExitCodeChannel:
os.Exit(128 + signalInt)
os.Exit(signalInt)
default:
// Do not exit, there are no exit codes in the channel,
// so just continue and let the main function go out of
Expand Down
126 changes: 126 additions & 0 deletions internal/util/signal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//go:build !windows

/*
Copyright 2020 The cert-manager Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package util

import (
"context"
"os"
"os/exec"
"syscall"
"testing"
)

// based on https://go.dev/talks/2014/testing.slide#23 and
// https://stackoverflow.com/a/33404435
func testExitCode(
t *testing.T,
fn func(t *testing.T),
) int {
if os.Getenv("BE_CRASHER") == "1" {
fn(t)
os.Exit(0)
}

cmd := exec.Command(os.Args[0], "-test.run="+t.Name())
cmd.Env = append(os.Environ(), "BE_CRASHER=1")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()

if e, ok := err.(*exec.ExitError); ok {
return e.ExitCode()
}

return 0
}

func TestSetupExitHandlerAlwaysErrCodeSIGTERM(t *testing.T) {
exitCode := testExitCode(t, func(t *testing.T) {
ctx := context.Background()
ctx, complete := SetupExitHandler(ctx, AlwaysErrCode)
defer complete()

if err := syscall.Kill(syscall.Getpid(), syscall.SIGTERM); err != nil {
t.Fatal(err)
os.Exit(99)
}

// Wait for the program to shut down.
<-ctx.Done()

if context.Cause(ctx).Error() != "received signal 15" {
t.Errorf("expected signal 15, got %s", ctx.Err().Error())
os.Exit(99)
}
})

if exitCode != 143 {
t.Errorf("expected exit code 143, got %d", exitCode)
}
}

func TestSetupExitHandlerAlwaysErrCodeSIGINT(t *testing.T) {
exitCode := testExitCode(t, func(t *testing.T) {
ctx := context.Background()
ctx, complete := SetupExitHandler(ctx, AlwaysErrCode)
defer complete()

if err := syscall.Kill(syscall.Getpid(), syscall.SIGINT); err != nil {
t.Fatal(err)
os.Exit(99)
}

// Wait for the program to shut down.
<-ctx.Done()

if context.Cause(ctx).Error() != "received signal 2" {
t.Errorf("expected signal 2, got %s", ctx.Err().Error())
os.Exit(99)
}
})

if exitCode != 130 {
t.Errorf("expected exit code 130, got %d", exitCode)
}
}

func TestSetupExitHandlerGracefulShutdownSIGINT(t *testing.T) {
exitCode := testExitCode(t, func(t *testing.T) {
ctx := context.Background()
ctx, complete := SetupExitHandler(ctx, GracefulShutdown)
defer complete()

if err := syscall.Kill(syscall.Getpid(), syscall.SIGINT); err != nil {
t.Fatal(err)
os.Exit(99)
}

// Wait for the program to shut down.
<-ctx.Done()

if context.Cause(ctx).Error() != "received signal 2" {
t.Errorf("expected signal 2, got %s", ctx.Err().Error())
os.Exit(99)
}
})

if exitCode != 0 {
t.Errorf("expected exit code 0, got %d", exitCode)
}
}

0 comments on commit b99f57b

Please sign in to comment.