1package coordinate 2 3import ( 4 "math" 5 "reflect" 6 "testing" 7 "time" 8) 9 10// verifyDimensionPanic will run the supplied func and make sure it panics with 11// the expected error type. 12func verifyDimensionPanic(t *testing.T, f func()) { 13 defer func() { 14 if r := recover(); r != nil { 15 if _, ok := r.(DimensionalityConflictError); !ok { 16 t.Fatalf("panic isn't the right type") 17 } 18 } else { 19 t.Fatalf("didn't get expected panic") 20 } 21 }() 22 f() 23} 24 25func TestCoordinate_NewCoordinate(t *testing.T) { 26 config := DefaultConfig() 27 c := NewCoordinate(config) 28 if uint(len(c.Vec)) != config.Dimensionality { 29 t.Fatalf("dimensionality not set correctly %d != %d", 30 len(c.Vec), config.Dimensionality) 31 } 32} 33 34func TestCoordinate_Clone(t *testing.T) { 35 c := NewCoordinate(DefaultConfig()) 36 c.Vec[0], c.Vec[1], c.Vec[2] = 1.0, 2.0, 3.0 37 c.Error = 5.0 38 c.Adjustment = 10.0 39 c.Height = 4.2 40 41 other := c.Clone() 42 if !reflect.DeepEqual(c, other) { 43 t.Fatalf("coordinate clone didn't make a proper copy") 44 } 45 46 other.Vec[0] = c.Vec[0] + 0.5 47 if reflect.DeepEqual(c, other) { 48 t.Fatalf("cloned coordinate is still pointing at its ancestor") 49 } 50} 51 52func TestCoordinate_IsValid(t *testing.T) { 53 c := NewCoordinate(DefaultConfig()) 54 55 var fields []*float64 56 for i := range c.Vec { 57 fields = append(fields, &c.Vec[i]) 58 } 59 fields = append(fields, &c.Error) 60 fields = append(fields, &c.Adjustment) 61 fields = append(fields, &c.Height) 62 63 for i, field := range fields { 64 if !c.IsValid() { 65 t.Fatalf("field %d should be valid", i) 66 } 67 68 *field = math.NaN() 69 if c.IsValid() { 70 t.Fatalf("field %d should not be valid (NaN)", i) 71 } 72 73 *field = 0.0 74 if !c.IsValid() { 75 t.Fatalf("field %d should be valid", i) 76 } 77 78 *field = math.Inf(0) 79 if c.IsValid() { 80 t.Fatalf("field %d should not be valid (Inf)", i) 81 } 82 83 *field = 0.0 84 if !c.IsValid() { 85 t.Fatalf("field %d should be valid", i) 86 } 87 } 88} 89 90func TestCoordinate_IsCompatibleWith(t *testing.T) { 91 config := DefaultConfig() 92 93 config.Dimensionality = 3 94 c1 := NewCoordinate(config) 95 c2 := NewCoordinate(config) 96 97 config.Dimensionality = 2 98 alien := NewCoordinate(config) 99 100 if !c1.IsCompatibleWith(c1) || !c2.IsCompatibleWith(c2) || 101 !alien.IsCompatibleWith(alien) { 102 t.Fatalf("coordinates should be compatible with themselves") 103 } 104 105 if !c1.IsCompatibleWith(c2) || !c2.IsCompatibleWith(c1) { 106 t.Fatalf("coordinates should be compatible with each other") 107 } 108 109 if c1.IsCompatibleWith(alien) || c2.IsCompatibleWith(alien) || 110 alien.IsCompatibleWith(c1) || alien.IsCompatibleWith(c2) { 111 t.Fatalf("alien should not be compatible with the other coordinates") 112 } 113} 114 115func TestCoordinate_ApplyForce(t *testing.T) { 116 config := DefaultConfig() 117 config.Dimensionality = 3 118 config.HeightMin = 0 119 120 origin := NewCoordinate(config) 121 122 // This proves that we normalize, get the direction right, and apply the 123 // force multiplier correctly. 124 above := NewCoordinate(config) 125 above.Vec = []float64{0.0, 0.0, 2.9} 126 c := origin.ApplyForce(config, 5.3, above) 127 verifyEqualVectors(t, c.Vec, []float64{0.0, 0.0, -5.3}) 128 129 // Scoot a point not starting at the origin to make sure there's nothing 130 // special there. 131 right := NewCoordinate(config) 132 right.Vec = []float64{3.4, 0.0, -5.3} 133 c = c.ApplyForce(config, 2.0, right) 134 verifyEqualVectors(t, c.Vec, []float64{-2.0, 0.0, -5.3}) 135 136 // If the points are right on top of each other, then we should end up 137 // in a random direction, one unit away. This makes sure the unit vector 138 // build up doesn't divide by zero. 139 c = origin.ApplyForce(config, 1.0, origin) 140 verifyEqualFloats(t, origin.DistanceTo(c).Seconds(), 1.0) 141 142 // Enable a minimum height and make sure that gets factored in properly. 143 config.HeightMin = 10.0e-6 144 origin = NewCoordinate(config) 145 c = origin.ApplyForce(config, 5.3, above) 146 verifyEqualVectors(t, c.Vec, []float64{0.0, 0.0, -5.3}) 147 verifyEqualFloats(t, c.Height, config.HeightMin+5.3*config.HeightMin/2.9) 148 149 // Make sure the height minimum is enforced. 150 c = origin.ApplyForce(config, -5.3, above) 151 verifyEqualVectors(t, c.Vec, []float64{0.0, 0.0, 5.3}) 152 verifyEqualFloats(t, c.Height, config.HeightMin) 153 154 // Shenanigans should get called if the dimensions don't match. 155 bad := c.Clone() 156 bad.Vec = make([]float64, len(bad.Vec)+1) 157 verifyDimensionPanic(t, func() { c.ApplyForce(config, 1.0, bad) }) 158} 159 160func TestCoordinate_DistanceTo(t *testing.T) { 161 config := DefaultConfig() 162 config.Dimensionality = 3 163 config.HeightMin = 0 164 165 c1, c2 := NewCoordinate(config), NewCoordinate(config) 166 c1.Vec = []float64{-0.5, 1.3, 2.4} 167 c2.Vec = []float64{1.2, -2.3, 3.4} 168 169 verifyEqualFloats(t, c1.DistanceTo(c1).Seconds(), 0.0) 170 verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), c2.DistanceTo(c1).Seconds()) 171 verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), 4.104875150354758) 172 173 // Make sure negative adjustment factors are ignored. 174 c1.Adjustment = -1.0e6 175 verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), 4.104875150354758) 176 177 // Make sure positive adjustment factors affect the distance. 178 c1.Adjustment = 0.1 179 c2.Adjustment = 0.2 180 verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), 4.104875150354758+0.3) 181 182 // Make sure the heights affect the distance. 183 c1.Height = 0.7 184 c2.Height = 0.1 185 verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), 4.104875150354758+0.3+0.8) 186 187 // Shenanigans should get called if the dimensions don't match. 188 bad := c1.Clone() 189 bad.Vec = make([]float64, len(bad.Vec)+1) 190 verifyDimensionPanic(t, func() { _ = c1.DistanceTo(bad) }) 191} 192 193// dist is a self-contained example that appears in documentation. 194func dist(a *Coordinate, b *Coordinate) time.Duration { 195 // Coordinates will always have the same dimensionality, so this is 196 // just a sanity check. 197 if len(a.Vec) != len(b.Vec) { 198 panic("dimensions aren't compatible") 199 } 200 201 // Calculate the Euclidean distance plus the heights. 202 sumsq := 0.0 203 for i := 0; i < len(a.Vec); i++ { 204 diff := a.Vec[i] - b.Vec[i] 205 sumsq += diff * diff 206 } 207 rtt := math.Sqrt(sumsq) + a.Height + b.Height 208 209 // Apply the adjustment components, guarding against negatives. 210 adjusted := rtt + a.Adjustment + b.Adjustment 211 if adjusted > 0.0 { 212 rtt = adjusted 213 } 214 215 // Go's times are natively nanoseconds, so we convert from seconds. 216 const secondsToNanoseconds = 1.0e9 217 return time.Duration(rtt * secondsToNanoseconds) 218} 219 220func TestCoordinate_dist_Example(t *testing.T) { 221 config := DefaultConfig() 222 c1, c2 := NewCoordinate(config), NewCoordinate(config) 223 c1.Vec = []float64{-0.5, 1.3, 2.4} 224 c2.Vec = []float64{1.2, -2.3, 3.4} 225 c1.Adjustment = 0.1 226 c2.Adjustment = 0.2 227 c1.Height = 0.7 228 c2.Height = 0.1 229 verifyEqualFloats(t, c1.DistanceTo(c2).Seconds(), dist(c1, c2).Seconds()) 230} 231 232func TestCoordinate_rawDistanceTo(t *testing.T) { 233 config := DefaultConfig() 234 config.Dimensionality = 3 235 config.HeightMin = 0 236 237 c1, c2 := NewCoordinate(config), NewCoordinate(config) 238 c1.Vec = []float64{-0.5, 1.3, 2.4} 239 c2.Vec = []float64{1.2, -2.3, 3.4} 240 241 verifyEqualFloats(t, c1.rawDistanceTo(c1), 0.0) 242 verifyEqualFloats(t, c1.rawDistanceTo(c2), c2.rawDistanceTo(c1)) 243 verifyEqualFloats(t, c1.rawDistanceTo(c2), 4.104875150354758) 244 245 // Make sure that the adjustment doesn't factor into the raw 246 // distance. 247 c1.Adjustment = 1.0e6 248 verifyEqualFloats(t, c1.rawDistanceTo(c2), 4.104875150354758) 249 250 // Make sure the heights affect the distance. 251 c1.Height = 0.7 252 c2.Height = 0.1 253 verifyEqualFloats(t, c1.rawDistanceTo(c2), 4.104875150354758+0.8) 254} 255 256func TestCoordinate_add(t *testing.T) { 257 vec1 := []float64{1.0, -3.0, 3.0} 258 vec2 := []float64{-4.0, 5.0, 6.0} 259 verifyEqualVectors(t, add(vec1, vec2), []float64{-3.0, 2.0, 9.0}) 260 261 zero := []float64{0.0, 0.0, 0.0} 262 verifyEqualVectors(t, add(vec1, zero), vec1) 263} 264 265func TestCoordinate_diff(t *testing.T) { 266 vec1 := []float64{1.0, -3.0, 3.0} 267 vec2 := []float64{-4.0, 5.0, 6.0} 268 verifyEqualVectors(t, diff(vec1, vec2), []float64{5.0, -8.0, -3.0}) 269 270 zero := []float64{0.0, 0.0, 0.0} 271 verifyEqualVectors(t, diff(vec1, zero), vec1) 272} 273 274func TestCoordinate_magnitude(t *testing.T) { 275 zero := []float64{0.0, 0.0, 0.0} 276 verifyEqualFloats(t, magnitude(zero), 0.0) 277 278 vec := []float64{1.0, -2.0, 3.0} 279 verifyEqualFloats(t, magnitude(vec), 3.7416573867739413) 280} 281 282func TestCoordinate_unitVectorAt(t *testing.T) { 283 vec1 := []float64{1.0, 2.0, 3.0} 284 vec2 := []float64{0.5, 0.6, 0.7} 285 u, mag := unitVectorAt(vec1, vec2) 286 verifyEqualVectors(t, u, []float64{0.18257418583505536, 0.511207720338155, 0.8398412548412546}) 287 verifyEqualFloats(t, magnitude(u), 1.0) 288 verifyEqualFloats(t, mag, magnitude(diff(vec1, vec2))) 289 290 // If we give positions that are equal we should get a random unit vector 291 // returned to us, rather than a divide by zero. 292 u, mag = unitVectorAt(vec1, vec1) 293 verifyEqualFloats(t, magnitude(u), 1.0) 294 verifyEqualFloats(t, mag, 0.0) 295 296 // We can't hit the final clause without heroics so I manually forced it 297 // there to verify it works. 298} 299