test(cabana,flare,dev,summer): json, reported errors and dev serve

- Docker: admin:token:create and list --json, -q keeps the token
- flare generate and test-push failures print once and are Reported
- summer dev starts serve with the forwarded globals, reports rebuilds,
  failures and restarts, and stops serve with SIGTERM returning nil
- delegates carry two examples and say the global options pass through
This commit is contained in:
Jakub Zych
2026-10-21 19:38:19 +02:00
parent 6720caebd3
commit 2baec56420
4 changed files with 390 additions and 0 deletions

View File

@@ -472,3 +472,151 @@ func waitUntil(t *testing.T, name string, ok func() bool) {
}
t.Fatalf("timed out waiting for %s", name)
}
// fakeServe writes an executable that records its arguments in
// $DEV_ARGS_FILE and, on SIGTERM, writes "term" to $DEV_TERM_FILE and exits
// 0, like serve's graceful stop.
func fakeServe(t *testing.T, path string) (argsFile, termFile string) {
t.Helper()
dir := t.TempDir()
argsFile, termFile = filepath.Join(dir, "args"), filepath.Join(dir, "term")
t.Setenv("DEV_ARGS_FILE", argsFile)
t.Setenv("DEV_TERM_FILE", termFile)
script := "#!/bin/sh\n" +
"printf '%s\\n' \"$*\" > \"$DEV_ARGS_FILE\"\n" +
"trap 'echo term > \"$DEV_TERM_FILE\"; exit 0' TERM\n" +
"while :; do sleep 0.05; done\n"
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(script), 0o755); err != nil {
t.Fatal(err)
}
return argsFile, termFile
}
func readTrimmed(path string) string {
b, _ := os.ReadFile(path)
return strings.TrimSpace(string(b))
}
// TestStartBinServe: summer dev starts bin/<binary> with "serve" first and
// then the forwarded global options, through the real startBin.
func TestStartBinServe(t *testing.T) {
bin := filepath.Join(t.TempDir(), "bin", "hello")
argsFile, _ := fakeServe(t, bin)
var out safeBuffer
cmd, err := startBin(t.Context(), bin, []string{"serve", "-vv", "--env=staging"}, &out, &out)
if err != nil {
t.Fatal(err)
}
waitUntil(t, "args written", func() bool { return readTrimmed(argsFile) != "" })
_ = cmd.Process.Signal(syscall.SIGTERM)
_ = cmd.Wait()
if got := readTrimmed(argsFile); got != "serve -vv --env=staging" {
t.Fatalf("child args = %q", got)
}
// Through watch: the default Start runs serve with the globals summer
// dev was given.
dir := t.TempDir()
writeWatchApp(t, dir)
argsFile, _ = fakeServe(t, filepath.Join(dir, "bin", "hello"))
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
done := make(chan error, 1)
opts := Options{Debounce: 40 * time.Millisecond, Build: func(context.Context, string, io.Writer) error { return nil }}
go func() { done <- watch(ctx, dir, testOutput(&out), []string{"-q", "--no-ansi"}, opts) }()
waitUntil(t, "child started", func() bool { return readTrimmed(argsFile) != "" })
if got := readTrimmed(argsFile); got != "serve -q --no-ansi" {
t.Fatalf("watch started the child with %q", got)
}
if !strings.Contains(out.String(), "running bin/hello serve -q --no-ansi") {
t.Errorf("watch line lacks the child command:\n%s", out.String())
}
cancel()
<-done
}
// TestDevReport: the dev report counts successful and failed rebuilds and
// restarts; a failed build keeps the running child.
func TestDevReport(t *testing.T) {
r := devReport{rebuilds: 2, failed: 1, restarts: 1, started: time.Now().Add(-90 * time.Second)}
d := r.details()
if d[0].Value != "3 (1 failed)" || !d[0].Warn || d[1].Value != "1" || d[2].Value != "1m30s" {
t.Fatalf("details = %+v", d)
}
if clean := (devReport{rebuilds: 1, started: time.Now()}).details(); clean[0].Value != "1 (0 failed)" || clean[0].Warn {
t.Fatalf("clean details = %+v", clean)
}
dir := t.TempDir()
writeWatchApp(t, dir)
var out safeBuffer
var builds, starts atomic.Int32
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
opts := Options{
Debounce: 40 * time.Millisecond,
Build: func(context.Context, string, io.Writer) error {
if builds.Add(1) == 2 {
return fmt.Errorf("syntax error")
}
return nil
},
Start: func(context.Context, string, []string, io.Writer, io.Writer) (*exec.Cmd, error) {
starts.Add(1)
cmd := exec.Command("sleep", "30")
return cmd, cmd.Start()
},
}
done := make(chan error, 1)
go func() { done <- watch(ctx, dir, testOutput(&out), nil, opts) }()
waitUntil(t, "initial start", func() bool { return starts.Load() == 1 })
write(t, filepath.Join(dir, "plugin.go"), "package p\n// broken\n")
waitUntil(t, "failed build", func() bool { return builds.Load() == 2 })
waitUntil(t, "build failure line", func() bool { return strings.Contains(out.String(), "Build failed: syntax error") })
write(t, filepath.Join(dir, "plugin.go"), "package p\n// fixed\n")
waitUntil(t, "restart", func() bool { return starts.Load() == 2 })
cancel()
if err := <-done; err != nil {
t.Fatalf("watch returned %v", err)
}
got := out.String()
for _, want := range []string{"Dev report", "Rebuilds 3 (1 failed)", "Restarts 1", "Uptime"} {
if !strings.Contains(got, want) {
t.Errorf("dev report lacks %q:\n%s", want, got)
}
}
}
// TestDevGracefulNil: when summer dev is stopped it sends SIGTERM to the
// running serve, which stops gracefully, prints the dev report and returns
// nil (exit 0).
func TestDevGracefulNil(t *testing.T) {
dir := t.TempDir()
writeWatchApp(t, dir)
argsFile, termFile := fakeServe(t, filepath.Join(dir, "bin", "hello"))
var out safeBuffer
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
done := make(chan error, 1)
opts := Options{Debounce: 40 * time.Millisecond, Build: func(context.Context, string, io.Writer) error { return nil }}
go func() { done <- watch(ctx, dir, testOutput(&out), nil, opts) }()
waitUntil(t, "child started", func() bool { return readTrimmed(argsFile) == "serve" })
cancel()
select {
case err := <-done:
if err != nil {
t.Fatalf("watch returned %v after stop, want nil", err)
}
case <-time.After(5 * time.Second):
t.Fatal("watch did not stop")
}
if readTrimmed(termFile) != "term" {
t.Fatal("serve did not get SIGTERM")
}
if !strings.Contains(out.String(), "Dev report") || !strings.Contains(out.String(), "Restarts 0") {
t.Fatalf("no dev report:\n%s", out.String())
}
}