schedparked_notice_test.go
Rama main · réplica de lectura del repositorio original.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/pingular/pingularity/internal/store"
)
// A schedule switched on with no weekday selected can never be active, so the
// feature it gates never runs - and an install measuring nothing looks exactly
// like an install with nothing to report. The dashboard refuses to save that
// state, so it arrives from the settings API, a restored backup or a
// hand-edited database, where nothing else would ever mention it. Boot the REAL
// binary on a store holding one and require the boot output to name the
// schedule, what it parked, and the way out.
func TestParkedScheduleStartupOutputSaysProbingIsParked(t *testing.T) {
if testing.Short() {
t.Skip("skipping build-and-boot parked-schedule notice test in -short mode")
}
goBin, err := exec.LookPath("go")
if err != nil {
t.Skip("go toolchain not on PATH; skipping build-and-boot parked-schedule notice test")
}
dir := t.TempDir()
bin := filepath.Join(dir, "pingularity")
if runtime.GOOS == "windows" {
bin += ".exe"
}
// Stamped with a version of its own, so the boots below can tell this
// binary's daemon from any other answering on the same port.
version := noticeBuildVersion()
build := exec.Command(goBin, "build", "-ldflags=-X main.version="+version, "-o", bin, ".")
build.Stderr = os.Stderr
if err := build.Run(); err != nil {
t.Fatalf("build failed: %v", err)
}
// The rows an older build leaves behind, or a config script writes: the
// toggle on, and one window that selects no day.
parked := bootSeededInstall(t, bin, version, map[string]string{
"sched_lat_enabled": "1",
"sched_lat_windows": `[{"days":"0000000","start":0,"end":0}]`,
})
for _, want := range []string{
"latency schedule", // which schedule
"no active days", // what is wrong with it, in the dashboard's own words
"parked", // what that costs
"pick a day", // the way out, also the dashboard's own words
} {
if !strings.Contains(parked, want) {
t.Errorf("boot output on a parked latency schedule never says %q - the install measures nothing and says nothing.\nfull output:\n%s", want, parked)
}
}
// The same schedule with a real day in it is an ordinary schedule: it gates
// probing, as it is meant to, and a notice here would teach operators to
// ignore the one above.
live := bootSeededInstall(t, bin, version, map[string]string{
"sched_lat_enabled": "1",
"sched_lat_windows": `[{"days":"0111110","start":540,"end":1020}]`,
})
if strings.Contains(live, "parked") {
t.Errorf("boot output on a working schedule announces a parked one:\n%s", live)
}
}
// bootSeededInstall writes rows into a fresh store the way an older build or a
// config restore leaves them, boots the already-built binary against it until
// that binary is serving (see bootUntilServing), and returns everything it
// printed on both streams.
func bootSeededInstall(t *testing.T, bin, version string, rows map[string]string) string {
t.Helper()
out, _ := bootUntilServing(t, bin, version, func(dbPath string) {
st, err := store.Open(dbPath)
if err != nil {
t.Fatalf("open %s: %v", dbPath, err)
}
if _, err := st.SetSettingsDiff(context.Background(), rows); err != nil {
st.Close()
t.Fatalf("seed: %v", err)
}
st.Close()
})
return out
}
// noticeBuildVersion is the version a boot-notice test stamps into the binary
// it builds: unique to this process and this build, so that daemon can be told
// apart from every other one on the machine.
func noticeBuildVersion() string {
return fmt.Sprintf("notice-test-%d-%d", os.Getpid(), time.Now().UnixNano())
}
// bootUntilServing boots the already-built binary on a store that seed
// prepares, on a free high port, and waits until THAT binary answers
// /api/status - so boot is past every startup line - then kills it and returns
// everything it printed on both streams plus the -listen address it used.
//
// Any 200 on the port is not the same answer. reserveHighPort hands every test
// process the lowest free port from 19000 up and lets go of it just before the
// child binds, so two processes running at once can be handed the same one:
// one child loses the bind and exits, and a poll that takes any 200 gets it
// from the OTHER process's daemon, kills its own child before that child has
// printed a word, and reports the notice missing from an empty output. So the
// answer has to carry the version this test stamped into its own build, and a
// child that exits before it is seen serving is booted again - on a fresh port
// and a fresh store - rather than reported as a boot that said nothing. A daemon
// that never serves at all still fails, with what it printed.
func bootUntilServing(t *testing.T, bin, version string, seed func(dbPath string)) (output, listenAddr string) {
t.Helper()
const attempts = 5
last := ""
for i := 0; i < attempts; i++ {
dbPath := filepath.Join(t.TempDir(), "seeded.db")
seed(dbPath)
port, releasePort := reserveHighPort(t)
addr := "127.0.0.1:" + port
// -quick-setup=skip answers the first-run offer: these installs have been
// answered already, and the point is the boot output, not the hold.
cmd := exec.Command(bin, "run", "-listen", addr, "-db", dbPath, "-quick-setup=skip")
// Buffers rather than pipes: Wait closes a pipe the moment the child exits,
// which can beat the reader to the boot lines and hand the assertions an
// empty output that reads as "the notice was never printed". Wait drains a
// buffer's copier before it returns, and nothing here reads the buffers
// until it has.
var outBuf, errBuf bytes.Buffer
cmd.Stdout, cmd.Stderr = &outBuf, &errBuf
releasePort()
if err := cmd.Start(); err != nil {
t.Fatalf("start failed: %v", err)
}
exited := make(chan struct{})
go func() {
_ = cmd.Wait()
close(exited)
}()
ready := servesAsVersion(addr, version, exited)
select {
case <-exited:
default:
_ = cmd.Process.Kill()
<-exited
}
if ready {
return outBuf.String() + errBuf.String(), addr
}
last = "stdout:\n" + outBuf.String() + "\nstderr:\n" + errBuf.String()
}
t.Fatalf("the daemon built as %q never served /api/status in %d boots; the last one printed\n%s", version, attempts, last)
return "", ""
}
// servesAsVersion polls addr until the daemon answering there reports version,
// the child exits, or thirty seconds pass.
func servesAsVersion(addr, version string, exited <-chan struct{}) bool {
client := &http.Client{Timeout: 2 * time.Second}
deadline := time.Now().Add(30 * time.Second)
for time.Now().Before(deadline) {
select {
case <-exited:
return false
default:
}
if resp, err := client.Get("http://" + addr + "/api/status"); err == nil {
var status struct {
Version string `json:"version"`
}
decodeErr := json.NewDecoder(resp.Body).Decode(&status)
resp.Body.Close()
if resp.StatusCode == http.StatusOK && decodeErr == nil && status.Version == version {
return true
}
}
time.Sleep(100 * time.Millisecond)
}
return false
}
// The notice must be ONE line, in the startup-line voice, and it must name the
// schedule, what it parked and the way out - the point of the line is that
// nobody should have to go read the docs to find out why the install is
// measuring nothing. The exit is worded as the dashboard words it, so the two
// places an operator meets this state say the same thing.
func TestSchedParkedLineIsOneActionableLine(t *testing.T) {
for feature, parked := range map[string]string{
"latency": "latency probing is parked",
"speedtest": "no automatic speedtest will run",
} {
line := schedParkedLine(feature)
if strings.Contains(line, "\n") {
t.Errorf("parked-schedule notice for %q is not a single line: %q", feature, line)
}
if !strings.HasPrefix(line, "pingularity") {
t.Errorf("parked-schedule notice for %q breaks the startup-line style (no \"pingularity\" prefix): %q", feature, line)
}
for _, want := range []string{
feature + " schedule", // which schedule is at fault
"no active days", // what is wrong with it
parked, // what that costs, in this feature's own terms
"pick a day or turn it off",
} {
if !strings.Contains(line, want) {
t.Errorf("parked-schedule notice for %q missing %q: %q", feature, want, line)
}
}
}
// The two features must not render the same sentence: an operator with both
// schedules parked gets two lines, and two identical lines say nothing about
// which is which.
if schedParkedLine("latency") == schedParkedLine("speedtest") {
t.Error("both schedules render the same notice - neither line says which feature stopped")
}
}