1package coordinate
2
3import (
4	"math"
5	"testing"
6	"time"
7)
8
9func TestPerformance_Line(t *testing.T) {
10	const spacing = 10 * time.Millisecond
11	const nodes, cycles = 10, 1000
12	config := DefaultConfig()
13	clients, err := GenerateClients(nodes, config)
14	if err != nil {
15		t.Fatal(err)
16	}
17	truth := GenerateLine(nodes, spacing)
18	Simulate(clients, truth, cycles)
19	stats := Evaluate(clients, truth)
20	if stats.ErrorAvg > 0.0018 || stats.ErrorMax > 0.0092 {
21		t.Fatalf("performance stats are out of spec: %v", stats)
22	}
23}
24
25func TestPerformance_Grid(t *testing.T) {
26	const spacing = 10 * time.Millisecond
27	const nodes, cycles = 25, 1000
28	config := DefaultConfig()
29	clients, err := GenerateClients(nodes, config)
30	if err != nil {
31		t.Fatal(err)
32	}
33	truth := GenerateGrid(nodes, spacing)
34	Simulate(clients, truth, cycles)
35	stats := Evaluate(clients, truth)
36	if stats.ErrorAvg > 0.0015 || stats.ErrorMax > 0.022 {
37		t.Fatalf("performance stats are out of spec: %v", stats)
38	}
39}
40
41func TestPerformance_Split(t *testing.T) {
42	const lan, wan = 1 * time.Millisecond, 10 * time.Millisecond
43	const nodes, cycles = 25, 1000
44	config := DefaultConfig()
45	clients, err := GenerateClients(nodes, config)
46	if err != nil {
47		t.Fatal(err)
48	}
49	truth := GenerateSplit(nodes, lan, wan)
50	Simulate(clients, truth, cycles)
51	stats := Evaluate(clients, truth)
52	if stats.ErrorAvg > 0.000060 || stats.ErrorMax > 0.00048 {
53		t.Fatalf("performance stats are out of spec: %v", stats)
54	}
55}
56
57func TestPerformance_Height(t *testing.T) {
58	const radius = 100 * time.Millisecond
59	const nodes, cycles = 25, 1000
60
61	// Constrain us to two dimensions so that we can just exactly represent
62	// the circle.
63	config := DefaultConfig()
64	config.Dimensionality = 2
65	clients, err := GenerateClients(nodes, config)
66	if err != nil {
67		t.Fatal(err)
68	}
69
70	// Generate truth where the first coordinate is in the "middle" because
71	// it's equidistant from all the nodes, but it will have an extra radius
72	// added to the distance, so it should come out above all the others.
73	truth := GenerateCircle(nodes, radius)
74	Simulate(clients, truth, cycles)
75
76	// Make sure the height looks reasonable with the regular nodes all in a
77	// plane, and the center node up above.
78	for i, _ := range clients {
79		coord := clients[i].GetCoordinate()
80		if i == 0 {
81			if coord.Height < 0.97*radius.Seconds() {
82				t.Fatalf("height is out of spec: %9.6f", coord.Height)
83			}
84		} else {
85			if coord.Height > 0.03*radius.Seconds() {
86				t.Fatalf("height is out of spec: %9.6f", coord.Height)
87			}
88		}
89	}
90	stats := Evaluate(clients, truth)
91	if stats.ErrorAvg > 0.0025 || stats.ErrorMax > 0.064 {
92		t.Fatalf("performance stats are out of spec: %v", stats)
93	}
94}
95
96func TestPerformance_Drift(t *testing.T) {
97	const dist = 500 * time.Millisecond
98	const nodes = 4
99	config := DefaultConfig()
100	config.Dimensionality = 2
101	clients, err := GenerateClients(nodes, config)
102	if err != nil {
103		t.Fatal(err)
104	}
105
106	// Do some icky surgery on the clients to put them into a square, up in
107	// the first quadrant.
108	clients[0].coord.Vec = []float64{0.0, 0.0}
109	clients[1].coord.Vec = []float64{0.0, dist.Seconds()}
110	clients[2].coord.Vec = []float64{dist.Seconds(), dist.Seconds()}
111	clients[3].coord.Vec = []float64{dist.Seconds(), dist.Seconds()}
112
113	// Make a corresponding truth matrix. The nodes are laid out like this
114	// so the distances are all equal, except for the diagonal:
115	//
116	// (1)  <- dist ->  (2)
117	//
118	//  | <- dist        |
119	//  |                |
120	//  |        dist -> |
121	//
122	// (0)  <- dist ->  (3)
123	//
124	truth := make([][]time.Duration, nodes)
125	for i := range truth {
126		truth[i] = make([]time.Duration, nodes)
127	}
128	for i := 0; i < nodes; i++ {
129		for j := i + 1; j < nodes; j++ {
130			rtt := dist
131			if (i%2 == 0) && (j%2 == 0) {
132				rtt = time.Duration(math.Sqrt2 * float64(rtt))
133			}
134			truth[i][j], truth[j][i] = rtt, rtt
135		}
136	}
137
138	calcCenterError := func() float64 {
139		min, max := clients[0].GetCoordinate(), clients[0].GetCoordinate()
140		for i := 1; i < nodes; i++ {
141			coord := clients[i].GetCoordinate()
142			for j, v := range coord.Vec {
143				min.Vec[j] = math.Min(min.Vec[j], v)
144				max.Vec[j] = math.Max(max.Vec[j], v)
145			}
146		}
147
148		mid := make([]float64, config.Dimensionality)
149		for i, _ := range mid {
150			mid[i] = min.Vec[i] + (max.Vec[i]-min.Vec[i])/2
151		}
152		return magnitude(mid)
153	}
154
155	// Let the simulation run for a while to stabilize, then snap a baseline
156	// for the center error.
157	Simulate(clients, truth, 1000)
158	baseline := calcCenterError()
159
160	// Now run for a bunch more cycles and see if gravity pulls the center
161	// in the right direction.
162	Simulate(clients, truth, 10000)
163	if error := calcCenterError(); error > 0.8*baseline {
164		t.Fatalf("drift performance out of spec: %9.6f -> %9.6f", baseline, error)
165	}
166}
167
168func TestPerformance_Random(t *testing.T) {
169	const mean, deviation = 100 * time.Millisecond, 10 * time.Millisecond
170	const nodes, cycles = 25, 1000
171	config := DefaultConfig()
172	clients, err := GenerateClients(nodes, config)
173	if err != nil {
174		t.Fatal(err)
175	}
176	truth := GenerateRandom(nodes, mean, deviation)
177	Simulate(clients, truth, cycles)
178	stats := Evaluate(clients, truth)
179	if stats.ErrorAvg > 0.075 || stats.ErrorMax > 0.33 {
180		t.Fatalf("performance stats are out of spec: %v", stats)
181	}
182}
183