repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

4.2 KB · 132 lines · Go Raw History
  1package tools
  2
  3import (
  4	"encoding/json"
  5	"testing"
  6)
  7
  8func chartJSON(t *testing.T, price, prev float64, closes []any, stamps []int64) chartPayload {
  9	t.Helper()
 10	body := map[string]any{"chart": map[string]any{"result": []any{map[string]any{
 11		"meta": map[string]any{"currency": "USD", "symbol": "VTI",
 12			"regularMarketPrice": price, "chartPreviousClose": prev, "shortName": "Vanguard"},
 13		"timestamp":  stamps,
 14		"indicators": map[string]any{"quote": []any{map[string]any{"close": closes}}},
 15	}}}}
 16	b, err := json.Marshal(body)
 17	if err != nil {
 18		t.Fatal(err)
 19	}
 20	var p chartPayload
 21	if err := json.Unmarshal(b, &p); err != nil {
 22		t.Fatal(err)
 23	}
 24	return p
 25}
 26
 27// The intraday chart is the only one measured against yesterday's close. Every
 28// longer span is measured against its own first bar, because "up 18% this year"
 29// is a claim about a year ago and not about yesterday.
 30func TestBuildSeriesBaseline(t *testing.T) {
 31	closes := []any{100.0, 105.0, 110.0}
 32	stamps := []int64{1, 2, 3}
 33
 34	day, err := buildSeries(chartJSON(t, 110, 50, closes, stamps), RangeByKey("1d"), "VTI")
 35	if err != nil {
 36		t.Fatal(err)
 37	}
 38	if day.Previous != 50 || day.Percent != 120 {
 39		t.Fatalf("intraday should measure from the previous close: prev=%v pct=%v", day.Previous, day.Percent)
 40	}
 41
 42	year, err := buildSeries(chartJSON(t, 110, 50, closes, stamps), RangeByKey("1y"), "VTI")
 43	if err != nil {
 44		t.Fatal(err)
 45	}
 46	if year.Previous != 100 || year.Percent != 10 {
 47		t.Fatalf("a year should measure from its own first bar: prev=%v pct=%v", year.Previous, year.Percent)
 48	}
 49}
 50
 51// A null close is a bar the exchange never printed. Zeroing one draws a spike
 52// to the floor of the chart, so it has to be dropped along with its timestamp.
 53func TestBuildSeriesDropsNullBars(t *testing.T) {
 54	s, err := buildSeries(
 55		chartJSON(t, 0, 0, []any{100.0, nil, 102.0}, []int64{10, 20, 30}),
 56		RangeByKey("1d"), "VTI")
 57	if err != nil {
 58		t.Fatal(err)
 59	}
 60	if len(s.Points) != 2 {
 61		t.Fatalf("want 2 points, got %d: %+v", len(s.Points), s.Points)
 62	}
 63	if s.Points[1].T != 30 || s.Points[1].C != 102 {
 64		t.Fatalf("the surviving bar kept the wrong timestamp: %+v", s.Points[1])
 65	}
 66	// An index carries no quote price, so the last bar has to become it.
 67	if s.Price != 102 {
 68		t.Fatalf("want the last bar as the price, got %v", s.Price)
 69	}
 70}
 71
 72func TestBuildSeriesNoPrices(t *testing.T) {
 73	if _, err := buildSeries(chartJSON(t, 0, 0, []any{nil}, []int64{1}), RangeByKey("1d"), "VTI"); err == nil {
 74		t.Fatal("a chart with no printed bar should be an error, not an empty chart")
 75	}
 76}
 77
 78func TestRangeByKeyFallsBackToTheDay(t *testing.T) {
 79	if got := RangeByKey("wat"); got.Key != "1d" {
 80		t.Fatalf("an unknown range should fall back to the day, got %q", got.Key)
 81	}
 82	for _, k := range []string{"1d", "1w", "1m", "1y"} {
 83		if RangeByKey(k).Key != k {
 84			t.Fatalf("%s did not resolve to itself", k)
 85		}
 86	}
 87}
 88
 89// The bands are what the tile prints, and an off-by-one at a breakpoint is the
 90// difference between "good" and "moderate" on the same reading.
 91func TestBands(t *testing.T) {
 92	for _, c := range []struct {
 93		v    float64
 94		want string
 95	}{{50, "good"}, {51, "moderate"}, {100, "moderate"}, {101, "unhealthy for some"},
 96		{301, "hazardous"}} {
 97		if got := aqiBand(c.v); got != c.want {
 98			t.Errorf("aqiBand(%v) = %q, want %q", c.v, got, c.want)
 99		}
100	}
101	for _, c := range []struct {
102		v    float64
103		want string
104	}{{0, "low"}, {2.4, "low-medium"}, {4.8, "medium"}, {7.2, "medium-high"}, {9.7, "high"}} {
105		if got := pollenBand(c.v); got != c.want {
106			t.Errorf("pollenBand(%v) = %q, want %q", c.v, got, c.want)
107		}
108	}
109}
110
111// A sink is per turn and a model that asks for the same symbol twice should not
112// stack two identical charts.
113func TestSinkIgnoresARepeat(t *testing.T) {
114	s := NewSink()
115	s.Add(Widget{Kind: "ticker", Symbol: "VTI"})
116	s.Add(Widget{Kind: "ticker", Symbol: "VTI"})
117	s.Add(Widget{Kind: "ticker", Symbol: "VOO"})
118	s.Add(Widget{Kind: "weather", Place: "Boone"})
119	if got := len(s.List()); got != 3 {
120		t.Fatalf("want 3 widgets, got %d: %+v", got, s.List())
121	}
122}
123
124// A nil sink is what a tool sees outside a turn, and it must not panic.
125func TestNilSinkIsSafe(t *testing.T) {
126	var s *Sink
127	s.Add(Widget{Kind: "ticker", Symbol: "VTI"})
128	if s.List() != nil {
129		t.Fatal("a nil sink should list nothing")
130	}
131}