Skip to content

Commit 387076e

Browse files
committed
modify foreground test to document behavior
Signed-off-by: Jeremy Drouillard <[email protected]>
1 parent cd240a6 commit 387076e

File tree

2 files changed

+68
-33
lines changed

2 files changed

+68
-33
lines changed

test/e2e/helpers.go

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"os/exec"
99
"strings"
10+
"syscall"
1011
"time"
1112

1213
. "github.com/onsi/ginkgo/v2" //nolint:staticcheck // Standard practice for Ginkgo
@@ -42,6 +43,9 @@ type THVCommand struct {
4243
env []string
4344
dir string
4445
stdin string
46+
47+
// cmd is the underlying exec.Cmd once a Run method is called.
48+
cmd *exec.Cmd
4549
}
4650

4751
// NewTHVCommand creates a new ToolHive command
@@ -82,24 +86,54 @@ func (c *THVCommand) RunWithTimeout(timeout time.Duration) (string, string, erro
8286
ctx, cancel := context.WithTimeout(context.Background(), timeout)
8387
defer cancel()
8488

85-
cmd := exec.CommandContext(ctx, c.config.THVBinary, c.args...) //nolint:gosec // Intentional for e2e testing
86-
cmd.Env = c.env
89+
c.cmd = exec.CommandContext(ctx, c.config.THVBinary, c.args...) //nolint:gosec // Intentional for e2e testing
90+
c.cmd.Env = c.env
8791
if c.dir != "" {
88-
cmd.Dir = c.dir
92+
c.cmd.Dir = c.dir
8993
}
9094
if c.stdin != "" {
91-
cmd.Stdin = strings.NewReader(c.stdin)
95+
c.cmd.Stdin = strings.NewReader(c.stdin)
9296
}
9397

9498
var stdout, stderr strings.Builder
95-
cmd.Stdout = &stdout
96-
cmd.Stderr = &stderr
99+
c.cmd.Stdout = &stdout
100+
c.cmd.Stderr = &stderr
97101

98-
err := cmd.Run()
102+
err := c.cmd.Run()
99103

100104
return stdout.String(), stderr.String(), err
101105
}
102106

107+
// InterruptAndWaitForExit interrupts the command and waits for it to exit.
108+
// If the context is cancelled, it returns an error.
109+
func (c *THVCommand) InterruptAndWaitForExit(ctx context.Context) error {
110+
if c.cmd.ProcessState != nil {
111+
// The command has already exited, so we can return early.
112+
// Calling wait again will produce an error if called after the command has exited.
113+
return nil
114+
}
115+
err := c.cmd.Process.Signal(syscall.SIGINT)
116+
if err != nil {
117+
return fmt.Errorf("failed to interrupt command: %w", err)
118+
}
119+
120+
exitCh := make(chan struct{}, 1)
121+
go func() {
122+
err = c.cmd.Wait()
123+
if err != nil {
124+
GinkgoWriter.Printf("command exited with error after interrupt: %v", err)
125+
}
126+
exitCh <- struct{}{}
127+
}()
128+
129+
select {
130+
case <-ctx.Done():
131+
return fmt.Errorf("context cancelled while waiting for command to exit")
132+
case <-exitCh:
133+
return nil
134+
}
135+
}
136+
103137
// ExpectSuccess runs the command and expects it to succeed
104138
func (c *THVCommand) ExpectSuccess() (string, string) {
105139
stdout, stderr, err := c.Run()

test/e2e/osv_mcp_server_test.go

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -464,28 +464,31 @@ var _ = Describe("OsvMcpServer", Label("mcp", "streamable-http", "e2e"), Serial,
464464
serverName := generateUniqueServerName("osv-foreground-test")
465465

466466
// 1) Start the foreground process in the background (goroutine) with a generous timeout.
467-
done := make(chan struct{})
468467
fgStdout := ""
469468
fgStderr := ""
469+
470+
// maintain a reference to the command so we can interrupt it when we're done.
471+
runCommand := e2e.NewTHVCommand(
472+
config, "run",
473+
"--name", serverName,
474+
"--transport", "streamable-http",
475+
"--foreground",
476+
"osv",
477+
)
470478
go func() {
471-
out, errOut, _ := e2e.NewTHVCommand(
472-
config, "run",
473-
"--name", serverName,
474-
"--transport", "streamable-http",
475-
"--foreground",
476-
"osv",
477-
).RunWithTimeout(5 * time.Minute)
479+
out, errOut, _ := runCommand.RunWithTimeout(5 * time.Minute)
478480
fgStdout, fgStderr = out, errOut
479-
close(done)
480481
}()
481482

482483
// Always try to stop the server at the end so the goroutine returns.
483484
defer func() {
484-
_, _, _ = e2e.NewTHVCommand(config, "stop", serverName).Run()
485-
select {
486-
case <-done:
487-
case <-time.After(15 * time.Second):
488-
// Nothing else we can signal directly; the RunWithTimeout will eventually kill it.
485+
// If this takes too long, the timeout willl kill the server eventually.
486+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
487+
defer cancel()
488+
err := runCommand.InterruptAndWaitForExit(ctx)
489+
if err != nil {
490+
// This may be safe to ignore if the server is already stopped.
491+
GinkgoWriter.Printf("Error interrupting foreground server during last cleanup: %v\n", err)
489492
}
490493
}()
491494

@@ -530,15 +533,11 @@ var _ = Describe("OsvMcpServer", Label("mcp", "streamable-http", "e2e"), Serial,
530533

531534
// 6) Stop the server; this should unblock the goroutine.
532535
By("stopping the foreground server")
533-
_, _ = e2e.NewTHVCommand(config, "stop", serverName).ExpectSuccess()
534-
535-
// Wait for the run goroutine to exit.
536-
select {
537-
case <-done:
538-
// ok
539-
case <-time.After(15 * time.Second):
540-
Fail("foreground run did not exit after stop; stdout="+fgStdout+" stderr="+fgStderr, 1)
541-
}
536+
537+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
538+
defer cancel()
539+
err = runCommand.InterruptAndWaitForExit(ctx)
540+
Expect(err).ToNot(HaveOccurred(), "server should be stopped; stdout="+fgStdout+" stderr="+fgStderr)
542541

543542
// 7) Workload should be stopped via workload manager.
544543
By("verifying workload status is stopped via workload manager")
@@ -556,9 +555,11 @@ var _ = Describe("OsvMcpServer", Label("mcp", "streamable-http", "e2e"), Serial,
556555
return workload.Status
557556
}, 15*time.Second, 200*time.Millisecond).Should(Equal(runtime.WorkloadStatusStopped), "workload should be in stopped status after stop")
558557

559-
// 8) Verify status file still exists with stopped status (it's not deleted, just marked as stopped)
560-
By("verifying status file still exists after stop")
561-
Expect(statusFileExists(serverName)).To(BeTrue(), "status file should still exist after stop")
558+
// 8) Verify status file does NOT exist. Interrupting a foreground server should delete the status file.
559+
// We may want to change this behavior and prefer the status to remain in a stopped state.
560+
// For now, this test documents the current behavior.
561+
By("verifying status file does not exist after stop")
562+
Expect(!statusFileExists(serverName)).To(BeTrue(), "status file should not exist after stop")
562563

563564
})
564565
})

0 commit comments

Comments
 (0)