Files
scrabble-game/backend/internal/game/metrics_test.go
T
Ilia Denisov 26aa154547
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 37s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
R1: schema & naming reset — squash migrations, rename variants
Squash the 12 goose migrations into one 00001_baseline.sql (there is no prod
data; verified schema-identical to the chain via a pg_dump diff + the green
integration suite) and rename the game-variant labels
english/russian_scrabble/erudit -> scrabble_en/scrabble_ru/erudit_ru across the
backend, the FlatBuffers wire values and the UI.

dawg filenames and the Go enum identifiers are unchanged; the i18n display keys
are kept. Adds PRERELEASE.md (the R1-R7 pre-release tracker), linked from
CLAUDE.md. Contour DB wipe and the scrabble-dictionary tidy are follow-ups.
2026-06-09 12:09:50 +02:00

111 lines
3.5 KiB
Go

package game
import (
"context"
"testing"
"time"
"go.opentelemetry.io/otel/attribute"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
"scrabble/backend/internal/engine"
)
// TestGameMetrics records each game instrument through a manual reader and asserts
// the counters carry the right "variant" attribute and the histograms observe.
func TestGameMetrics(t *testing.T) {
ctx := context.Background()
reader := sdkmetric.NewManualReader()
meter := sdkmetric.NewMeterProvider(sdkmetric.WithReader(reader)).Meter("test")
m := newGameMetrics(meter)
m.recordStarted(ctx, engine.VariantEnglish)
m.recordStarted(ctx, engine.VariantEnglish)
m.recordStarted(ctx, engine.VariantRussianScrabble)
m.recordAbandoned(ctx, engine.VariantErudit)
m.recordReplay(ctx, engine.VariantEnglish, time.Now().Add(-time.Millisecond))
m.recordValidate(ctx, engine.VariantRussianScrabble, time.Now().Add(-time.Millisecond))
m.recordMoveDuration(ctx, engine.VariantEnglish, 3, 5*time.Second)
m.recordMoveDuration(ctx, engine.VariantEnglish, 3, 0) // non-positive: dropped
var rm metricdata.ResourceMetrics
if err := reader.Collect(ctx, &rm); err != nil {
t.Fatalf("collect: %v", err)
}
started := counterByAttr(t, rm, "games_started_total", "variant")
if started["scrabble_en"] != 2 || started["scrabble_ru"] != 1 {
t.Errorf("games_started_total = %v, want scrabble_en:2 scrabble_ru:1", started)
}
if abandoned := counterByAttr(t, rm, "games_abandoned_total", "variant"); abandoned["erudit_ru"] != 1 {
t.Errorf("games_abandoned_total = %v, want erudit_ru:1", abandoned)
}
if c := histogramCount(t, rm, "game_replay_duration"); c != 1 {
t.Errorf("game_replay_duration observations = %d, want 1", c)
}
if c := histogramCount(t, rm, "game_move_validate_duration"); c != 1 {
t.Errorf("game_move_validate_duration observations = %d, want 1", c)
}
if c := histogramCount(t, rm, "game_move_duration"); c != 1 {
t.Errorf("game_move_duration observations = %d, want 1 (zero-duration dropped)", c)
}
}
// TestPhaseOf checks the move-ordinal to phase bucketing.
func TestPhaseOf(t *testing.T) {
cases := map[int]string{1: "opening", 8: "opening", 9: "middle", 20: "middle", 21: "endgame", 50: "endgame"}
for mc, want := range cases {
if got := phaseOf(mc); got != want {
t.Errorf("phaseOf(%d) = %q, want %q", mc, got, want)
}
}
}
// counterByAttr sums the int64 counter named name, grouped by the value of the
// attribute key attr.
func counterByAttr(t *testing.T, rm metricdata.ResourceMetrics, name, attr string) map[string]int64 {
t.Helper()
out := map[string]int64{}
for _, sm := range rm.ScopeMetrics {
for _, md := range sm.Metrics {
if md.Name != name {
continue
}
sum, ok := md.Data.(metricdata.Sum[int64])
if !ok {
t.Fatalf("%s is not an int64 sum", name)
}
for _, dp := range sum.DataPoints {
v, _ := dp.Attributes.Value(attribute.Key(attr))
out[v.AsString()] += dp.Value
}
}
}
return out
}
// histogramCount returns the total observation count of the float64 histogram
// named name.
func histogramCount(t *testing.T, rm metricdata.ResourceMetrics, name string) uint64 {
t.Helper()
for _, sm := range rm.ScopeMetrics {
for _, md := range sm.Metrics {
if md.Name != name {
continue
}
h, ok := md.Data.(metricdata.Histogram[float64])
if !ok {
t.Fatalf("%s is not a float64 histogram", name)
}
var n uint64
for _, dp := range h.DataPoints {
n += dp.Count
}
return n
}
}
t.Fatalf("%s not found", name)
return 0
}