diff --git a/pkg/internal/testing/process/process.go b/pkg/internal/testing/process/process.go index f1bfda425f..9436c91786 100644 --- a/pkg/internal/testing/process/process.go +++ b/pkg/internal/testing/process/process.go @@ -18,6 +18,7 @@ package process import ( "crypto/tls" + "errors" "fmt" "io" "net" @@ -128,6 +129,8 @@ func (ps *State) Init(name string) error { type stopChannel chan struct{} +var signalProcess = signalProcessImpl + // CheckFlag checks the help output of this command for the presence of the given flag, specified // without the leading `--` (e.g. `CheckFlag("insecure-port")` checks for `--insecure-port`), // returning true if the flag is present. @@ -266,10 +269,11 @@ func (ps *State) Stop() error { case <-ps.waitDone: break case <-timedOut: + timeoutErr := fmt.Errorf("timeout waiting for process %s to stop", path.Base(ps.Path)) if err := signalProcess(ps.Cmd.Process, syscall.SIGKILL); err != nil { - return fmt.Errorf("unable to kill process %s: %w", ps.Path, err) + return errors.Join(timeoutErr, fmt.Errorf("unable to kill process %s: %w", ps.Path, err)) } - return fmt.Errorf("timeout waiting for process %s to stop", path.Base(ps.Path)) + return timeoutErr } ps.ready = false return nil diff --git a/pkg/internal/testing/process/process_internal_test.go b/pkg/internal/testing/process/process_internal_test.go new file mode 100644 index 0000000000..1c8b0f484d --- /dev/null +++ b/pkg/internal/testing/process/process_internal_test.go @@ -0,0 +1,69 @@ +/* +Copyright 2026 The Kubernetes 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 process + +import ( + "errors" + "os" + "os/exec" + "strings" + "syscall" + "testing" + "time" +) + +func TestStopReturnsTimeoutWhenKillAfterTimeoutFails(t *testing.T) { + process, err := os.FindProcess(os.Getpid()) + if err != nil { + t.Fatalf("failed to find current process: %v", err) + } + + originalSignalProcess := signalProcess + defer func() { + signalProcess = originalSignalProcess + }() + + signalCount := 0 + signalProcess = func(_ *os.Process, sig syscall.Signal) error { + signalCount++ + if sig == syscall.SIGKILL { + return errors.New("kill failed") + } + return nil + } + + ps := &State{ + Cmd: &exec.Cmd{Process: process}, + Path: "/path/to/test-process", + StopTimeout: time.Nanosecond, + waitDone: make(chan struct{}), + } + + err = ps.Stop() + if err == nil { + t.Fatal("expected Stop to return an error") + } + if !strings.Contains(err.Error(), "timeout waiting for process test-process to stop") { + t.Fatalf("expected timeout error, got %q", err.Error()) + } + if !strings.Contains(err.Error(), "unable to kill process /path/to/test-process: kill failed") { + t.Fatalf("expected kill error to be included, got %q", err.Error()) + } + if signalCount != 2 { + t.Fatalf("expected SIGTERM and SIGKILL to be sent, got %d signals", signalCount) + } +} diff --git a/pkg/internal/testing/process/signal_other.go b/pkg/internal/testing/process/signal_other.go index 5cbce8f416..e358ae9448 100644 --- a/pkg/internal/testing/process/signal_other.go +++ b/pkg/internal/testing/process/signal_other.go @@ -23,6 +23,6 @@ import ( "syscall" ) -func signalProcess(process *os.Process, sig syscall.Signal) error { +func signalProcessImpl(process *os.Process, sig syscall.Signal) error { return process.Signal(sig) } diff --git a/pkg/internal/testing/process/signal_unix.go b/pkg/internal/testing/process/signal_unix.go index d86fc97213..1993962abb 100644 --- a/pkg/internal/testing/process/signal_unix.go +++ b/pkg/internal/testing/process/signal_unix.go @@ -23,6 +23,6 @@ import ( "syscall" ) -func signalProcess(process *os.Process, sig syscall.Signal) error { +func signalProcessImpl(process *os.Process, sig syscall.Signal) error { return syscall.Kill(-process.Pid, sig) }