From d2ecc273281dbf01698a4f1d16c5e25e30235edb Mon Sep 17 00:00:00 2001 From: Tim Ramlot <42113979+inteon@users.noreply.github.com> Date: Thu, 21 Mar 2024 11:57:23 +0100 Subject: [PATCH] bugfix: return correct error codes and add tests Signed-off-by: Tim Ramlot <42113979+inteon@users.noreply.github.com> --- internal/util/exit_test.go | 56 ++++++++++++++++ internal/util/signal.go | 4 +- internal/util/signal_test.go | 126 +++++++++++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 internal/util/exit_test.go create mode 100644 internal/util/signal_test.go diff --git a/internal/util/exit_test.go b/internal/util/exit_test.go new file mode 100644 index 0000000..ea27696 --- /dev/null +++ b/internal/util/exit_test.go @@ -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) + } + }) + } +} diff --git a/internal/util/signal.go b/internal/util/signal.go index b7a4673..7994417 100644 --- a/internal/util/signal.go +++ b/internal/util/signal.go @@ -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. @@ -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 diff --git a/internal/util/signal_test.go b/internal/util/signal_test.go new file mode 100644 index 0000000..34915b8 --- /dev/null +++ b/internal/util/signal_test.go @@ -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) + } +}