Skip to content

Commit 1459cdc

Browse files
committed
(WIP) smoke testing
1 parent d7181f3 commit 1459cdc

File tree

2 files changed

+131
-0
lines changed

2 files changed

+131
-0
lines changed

cmd/bob/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ func toolsEntry() *cobra.Command {
8080
Short: "Less often needed tools",
8181
}
8282

83+
cmd.AddCommand(smokeTestEntrypoint())
84+
8385
// TODO: move powerline here?
8486
cmd.AddCommand(initEntry())
8587
cmd.AddCommand(langserverEntry())

cmd/bob/smoketest.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"log"
8+
"os/exec"
9+
"strings"
10+
"time"
11+
12+
"github.com/function61/gokit/app/cli"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
func smokeTestEntrypoint() *cobra.Command {
17+
longRunning := false
18+
19+
cmd := &cobra.Command{
20+
Use: "smoketest",
21+
// Short: "Code bookmarks management",
22+
Args: cobra.NoArgs,
23+
Run: cli.RunnerNoArgs(func(ctx context.Context, _ *log.Logger) error {
24+
if longRunning {
25+
return smokeTest(ctx, smokeTestDefinition{
26+
imageRef: "smoketest-longrunning",
27+
kind: "stay-up",
28+
})
29+
} else {
30+
return smokeTest(ctx, smokeTestDefinition{
31+
imageRef: "smoketest",
32+
args: []string{"--help"},
33+
kind: "exit-immediately",
34+
exitImmediatelyOptions: exitImmediatelyOptions{
35+
outputContains: "Usage: curl [options...] <url>",
36+
},
37+
})
38+
}
39+
}),
40+
}
41+
42+
cmd.Flags().BoolVarP(&longRunning, "long-running", "l", longRunning, "Test a long-running daemon")
43+
44+
return cmd
45+
}
46+
47+
type smokeTestDefinition struct {
48+
imageRef string
49+
args []string
50+
kind string // "exit-immediately" | "stay-up"
51+
exitImmediatelyOptions exitImmediatelyOptions
52+
}
53+
54+
type exitImmediatelyOptions struct {
55+
outputContains string
56+
}
57+
58+
func smokeTest(ctx context.Context, testSpec smokeTestDefinition) error {
59+
withErr := func(err error) error { return fmt.Errorf("smokeTest: %w", err) }
60+
61+
// TODO: generate
62+
const containerName = "smoketest-123"
63+
64+
// kill signal to stop container as fast as possible when we want to stop.
65+
cmd := []string{"docker", "run", "--rm", "-t", "--stop-signal=SIGKILL", "--name=" + containerName, testSpec.imageRef}
66+
cmd = append(cmd, testSpec.args...)
67+
68+
smokeTest := exec.CommandContext(ctx, cmd[0], cmd[1:]...)
69+
70+
switch testSpec.kind {
71+
case "exit-immediately":
72+
return smokeTestExitImmediately(smokeTest, testSpec)
73+
case "stay-up":
74+
childCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
75+
defer cancel()
76+
return smokeTestStayUp(childCtx, smokeTest, containerName)
77+
default:
78+
return withErr(fmt.Errorf("unknown kind: %s", testSpec.kind))
79+
}
80+
}
81+
82+
func smokeTestExitImmediately(smokeTest *exec.Cmd, testSpec smokeTestDefinition) error {
83+
withErr := func(err error) error { return fmt.Errorf("smokeTestExitImmediately: %w", err) }
84+
85+
output, err := smokeTest.CombinedOutput()
86+
if err != nil {
87+
return withErr(fmt.Errorf("%w; output: %s", err, string(output)))
88+
}
89+
90+
if outputContains := testSpec.exitImmediatelyOptions.outputContains; outputContains != "" {
91+
if !strings.Contains(string(output), outputContains) {
92+
return withErr(fmt.Errorf("output should contain '%s' but did not. output was: %s", outputContains, string(output)))
93+
}
94+
}
95+
96+
return nil
97+
}
98+
99+
func smokeTestStayUp(ctx context.Context, smokeTest *exec.Cmd, containerName string) error {
100+
withErr := func(err error) error { return fmt.Errorf("smokeTestStayUp: %w", err) }
101+
102+
containerExited := make(chan error, 1)
103+
go func() {
104+
containerExited <- smokeTest.Run()
105+
}()
106+
107+
select {
108+
case err := <-containerExited: // program exited itself
109+
return withErr(fmt.Errorf("unexpected container exit: %w", err))
110+
case <-ctx.Done(): // stop requested before container exited by itself
111+
// ask the container to stop. for some reason sending SIGINT to `$ docker` will not work
112+
// (neither from shell, maybe it just forwards signals to the container?) but instead we must use `$ docker stop`.
113+
//
114+
// NOTE: using background context because we can't derive context from already-canceled context.
115+
if output, err := exec.CommandContext(context.Background(), "docker", "stop", containerName).CombinedOutput(); err != nil {
116+
// probably a race - program exited at the same time?
117+
return withErr(fmt.Errorf("got context cancel but stopping: %w: %s", err, string(output)))
118+
}
119+
120+
<-containerExited
121+
122+
if reason := ctx.Err(); errors.Is(reason, context.DeadlineExceeded) {
123+
// happy path: container stays up for at least the timeout that we wanted
124+
return nil
125+
} else { // maybe user cancelled the wait
126+
return withErr(reason)
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)