sparkline_test.gno
3.66 Kb · 125 lines
1package sparkline
2
3import (
4 "testing"
5
6 "gno.land/p/nt/uassert/v0"
7)
8
9func TestInts(t *testing.T) {
10 tests := []struct {
11 name string
12 values []int
13 want string
14 }{
15 {"empty", nil, ""},
16 {"empty slice", []int{}, ""},
17 // A single value is its own min and max, i.e. flat: mid-ramp.
18 {"single", []int{42}, "▅"},
19 {"flat", []int{7, 7, 7}, "▅▅▅"},
20 {"flat at zero", []int{0, 0}, "▅▅"},
21 {"two apart", []int{0, 1}, "▁█"},
22 {"ramp", []int{0, 1, 2, 3, 4, 5, 6, 7}, "▁▂▃▄▅▆▇█"},
23 {"descending", []int{7, 6, 5, 4, 3, 2, 1, 0}, "█▇▆▅▄▃▂▁"},
24 {"negatives only", []int{-7, -4, 0}, "▁▄█"},
25 {"spans zero", []int{-7, 0, 7}, "▁▄█"},
26 {"wide range", []int{0, 50, 100}, "▁▄█"},
27 }
28 for _, tt := range tests {
29 t.Run(tt.name, func(t *testing.T) {
30 uassert.Equal(t, tt.want, Ints(tt.values))
31 })
32 }
33}
34
35func TestScaledClampsOutliers(t *testing.T) {
36 // The window is the contract: anything outside it pins to an end rather
37 // than rescaling the picture or panicking.
38 uassert.Equal(t, "▁▁▄██", Scaled([]int{-100, 0, 50, 100, 999}, 0, 100))
39}
40
41func TestScaledEmpty(t *testing.T) {
42 uassert.Equal(t, "", Scaled(nil, 0, 10))
43}
44
45func TestScaledFlatWindow(t *testing.T) {
46 // hi <= lo carries no shape; every value lands mid-ramp.
47 uassert.Equal(t, "▅▅▅", Scaled([]int{1, 500, 9}, 5, 5))
48 uassert.Equal(t, "▅▅", Scaled([]int{1, 2}, 10, 3))
49}
50
51func TestLevel(t *testing.T) {
52 tests := []struct {
53 name string
54 v, lo, hi int
55 want int
56 }{
57 {"at floor", 0, 0, 7, 0},
58 {"at ceiling", 7, 0, 7, 7},
59 {"below floor clamps", -5, 0, 7, 0},
60 {"above ceiling clamps", 99, 0, 7, 7},
61 {"flat window", 3, 5, 5, Steps / 2},
62 {"inverted window", 3, 9, 2, Steps / 2},
63 // Floor rounding: only hi itself reaches the top.
64 {"just below ceiling", 99, 0, 100, 6},
65 {"exact ceiling", 100, 0, 100, 7},
66 {"midpoint", 50, 0, 100, 3},
67 }
68 for _, tt := range tests {
69 t.Run(tt.name, func(t *testing.T) {
70 uassert.Equal(t, tt.want, Level(tt.v, tt.lo, tt.hi))
71 })
72 }
73}
74
75// Level must stay inside the ramp for every input, including the extremes that
76// make the scaling arithmetic wrap. An out-of-range index here would panic the
77// realm that called Scaled, so this is the load-bearing invariant.
78func TestLevelAlwaysInRange(t *testing.T) {
79 const minInt = -maxInt - 1
80 vals := []int{minInt, -maxInt, -1000, -1, 0, 1, 1000, maxInt}
81 windows := [][2]int{
82 {0, 7}, {0, 100}, {-100, 100}, {5, 5}, {9, 2},
83 {minInt, maxInt}, // span wraps int
84 {0, maxInt}, // span survives, but d*top would wrap
85 {minInt, 0},
86 }
87 for _, w := range windows {
88 for _, v := range vals {
89 got := Level(v, w[0], w[1])
90 if got < 0 || got >= Steps {
91 t.Fatalf("Level(%d, %d, %d) = %d, outside [0,%d)", v, w[0], w[1], got, Steps)
92 }
93 }
94 }
95}
96
97// The huge-span path halves its way down to safe arithmetic; it must still be
98// monotonic, or the picture would lie about the direction of the trend.
99func TestLevelMonotonicOnHugeSpan(t *testing.T) {
100 prev := -1
101 for _, v := range []int{0, maxInt / 8, maxInt / 4, maxInt / 2, maxInt} {
102 got := Level(v, 0, maxInt)
103 if got < prev {
104 t.Fatalf("Level(%d, 0, maxInt) = %d, went down from %d", v, got, prev)
105 }
106 prev = got
107 }
108}
109
110func TestBounds(t *testing.T) {
111 lo, hi, ok := Bounds([]int{3, -1, 9, 4})
112 uassert.True(t, ok, "non-empty series should report ok")
113 uassert.Equal(t, -1, lo)
114 uassert.Equal(t, 9, hi)
115
116 lo, hi, ok = Bounds(nil)
117 uassert.False(t, ok, "empty series should report not-ok")
118 uassert.Equal(t, 0, lo)
119 uassert.Equal(t, 0, hi)
120}
121
122func TestLevelsRampIsEightRunes(t *testing.T) {
123 // Scaled indexes this ramp by Level's result, so the two must agree.
124 uassert.Equal(t, Steps, len([]rune(Levels)))
125}