Skip to content
quarry Artifact [269b3ddef4]
Login

Artifact [269b3ddef4]

Artifact 269b3ddef493c85659f591b42b51d0c6afc9f0e4445c20d0de72bfe652f08e9b:


//go:build !js && !wasip1

package obsduck

import (
	"testing"
	"time"

	"craftdesign.group/quarry/internal/obs"
)

// The live panels are one process's memory, so they cannot answer the
// question an operator has after a deploy: is this getting worse. The
// store held the rows and nothing read them back.
func TestTrendSeparatesTheDays(t *testing.T) {
	s := openSink(t)
	now := time.Now()
	for i := 0; i < 20; i++ {
		s.Render(obs.Render{At: now.Add(-48 * time.Hour), Path: "GET /{$}",
			Elapsed: time.Duration(10+i) * time.Millisecond})
		s.Render(obs.Render{At: now, Path: "GET /{$}",
			Elapsed: time.Duration(100+i*10) * time.Millisecond})
	}
	pts, err := s.Trend("GET /{$}", 7)
	if err != nil {
		t.Fatal(err)
	}
	if len(pts) != 2 {
		t.Fatalf("got %d days, want 2: %+v", len(pts), pts)
	}
	if pts[0].Day >= pts[1].Day {
		t.Error("days are not oldest first")
	}
	if pts[0].N != 20 || pts[1].N != 20 {
		t.Errorf("counts %d/%d, want 20 each", pts[0].N, pts[1].N)
	}
	// The whole point: the regression is visible.
	if pts[1].P95Ms <= pts[0].P95Ms {
		t.Errorf("p95 went %d -> %d; a route that got 10x slower reads as flat",
			pts[0].P95Ms, pts[1].P95Ms)
	}
	if pts[0].P50Ms > pts[0].P95Ms || pts[0].P95Ms > pts[0].MaxMs {
		t.Errorf("percentiles out of order: %+v", pts[0])
	}
}

// Trend must answer for one route, not for the forge's whole traffic
// mixed together — the drilldown exists to isolate a route.
func TestTrendIsScopedToItsRoute(t *testing.T) {
	s := openSink(t)
	now := time.Now()
	s.Render(obs.Render{At: now, Path: "GET /{$}", Elapsed: 500 * time.Millisecond})
	s.Render(obs.Render{At: now, Path: "GET /r/{repo}", Elapsed: 5 * time.Millisecond})

	pts, err := s.Trend("GET /{$}", 7)
	if err != nil {
		t.Fatal(err)
	}
	if len(pts) != 1 || pts[0].N != 1 || pts[0].MaxMs != 500 {
		t.Fatalf("route trend picked up the other route: %+v", pts)
	}
	// ...and the whole forge when nothing is named.
	if all, err := s.Trend("", 7); err != nil || len(all) != 1 || all[0].N != 2 {
		t.Fatalf("fleet-wide trend %+v err=%v, want one day of 2", all, err)
	}
}

// The window is a window: rows outside it are not the answer to "is
// this getting worse lately".
func TestTrendHonoursItsWindow(t *testing.T) {
	s := openSink(t)
	now := time.Now()
	s.Render(obs.Render{At: now.Add(-30 * 24 * time.Hour), Path: "GET /{$}", Elapsed: time.Second})
	s.Render(obs.Render{At: now, Path: "GET /{$}", Elapsed: 5 * time.Millisecond})
	pts, err := s.Trend("GET /{$}", 7)
	if err != nil {
		t.Fatal(err)
	}
	if len(pts) != 1 {
		t.Fatalf("a 7-day window returned %d days: %+v", len(pts), pts)
	}
}

// A recorder whose sink cannot answer must return nothing rather than
// inventing a trend from the ring, which covers minutes.
func TestRecorderWithoutADurableSinkHasNoTrend(t *testing.T) {
	r := obs.NewRecorder(8, nil)
	pts, err := r.Trend("GET /{$}", 7)
	if err != nil || pts != nil {
		t.Fatalf("history-off recorder returned %+v err=%v", pts, err)
	}
}

// ...and one that can, does — the seam the ops op reaches through.
func TestRecorderReachesTheDurableTrend(t *testing.T) {
	s := openSink(t)
	r := obs.NewRecorder(8, nil)
	r.SetSink(s)
	s.Render(obs.Render{At: time.Now(), Path: "GET /{$}", Elapsed: 42 * time.Millisecond})
	pts, err := r.Trend("GET /{$}", 7)
	if err != nil {
		t.Fatal(err)
	}
	if len(pts) != 1 || pts[0].MaxMs != 42 {
		t.Fatalf("recorder did not reach the store: %+v", pts)
	}
}