github.com/grailbio/base@v0.0.11/cloud/spotadvisor/spotadvisor_test.go (about) 1 // Copyright 2021 GRAIL, Inc. All rights reserved. 2 // Use of this source code is governed by the Apache 2.0 3 // license that can be found in the LICENSE file. 4 5 package spotadvisor_test 6 7 import ( 8 "context" 9 "fmt" 10 "io/ioutil" 11 "log" 12 "net/http" 13 "net/http/httptest" 14 "reflect" 15 "sort" 16 "testing" 17 18 sa "github.com/grailbio/base/cloud/spotadvisor" 19 ) 20 21 // Contains an abridged version of a real response to make got/want comparisons easier. 22 const testDataPath = "./testdata/test-spot-advisor-data.json" 23 24 // TestGetAndFilterByInterruptRate tests both GetInstancesWithMaxInterruptProbability and FilterByMaxInterruptProbability. 25 func TestGetAndFilterByInterruptRate(t *testing.T) { 26 defer setupMockTestServer(t).Close() 27 adv, err := sa.NewSpotAdvisor(testLogger, context.Background().Done()) 28 if err != nil { 29 t.Fatalf(err.Error()) 30 } 31 tests := []struct { 32 name string 33 osType sa.OsType 34 region sa.AwsRegion 35 maxInterruptProb sa.InterruptProbability 36 candidates []string 37 want []string 38 wantErr error 39 }{ 40 { 41 name: "simple", 42 osType: sa.Windows, 43 region: sa.AwsRegion("eu-west-2"), 44 candidates: testAvailableInstanceTypes, 45 maxInterruptProb: sa.LessThanFivePct, 46 want: []string{"r4.xlarge"}, 47 }, 48 { 49 name: "<5%", 50 osType: sa.Linux, 51 region: sa.AwsRegion("eu-west-2"), 52 candidates: testAvailableInstanceTypes, 53 maxInterruptProb: sa.LessThanFivePct, 54 want: []string{"m5a.4xlarge"}, 55 }, 56 { 57 name: "<10%", 58 osType: sa.Linux, 59 region: sa.AwsRegion("eu-west-2"), 60 candidates: testAvailableInstanceTypes, 61 maxInterruptProb: sa.LessThanTenPct, 62 want: []string{"m5a.4xlarge", "t3.nano"}, 63 }, 64 { 65 name: "<15%", 66 osType: sa.Linux, 67 region: sa.AwsRegion("eu-west-2"), 68 candidates: testAvailableInstanceTypes, 69 maxInterruptProb: sa.LessThanFifteenPct, 70 want: []string{"m5a.4xlarge", "t3.nano", "g4dn.12xlarge"}, 71 }, 72 { 73 name: "<20%", 74 osType: sa.Linux, 75 region: sa.AwsRegion("eu-west-2"), 76 candidates: testAvailableInstanceTypes, 77 maxInterruptProb: sa.LessThanTwentyPct, 78 want: []string{"m5a.4xlarge", "t3.nano", "g4dn.12xlarge", "r5d.8xlarge"}, 79 }, 80 { 81 name: "Any", 82 osType: sa.Linux, 83 region: sa.AwsRegion("eu-west-2"), 84 candidates: testAvailableInstanceTypes, 85 maxInterruptProb: sa.Any, 86 want: testAvailableInstanceTypes, 87 }, 88 { 89 name: "bad_interrupt_prob_neg", 90 osType: sa.Linux, 91 region: sa.AwsRegion("eu-west-2"), 92 candidates: testAvailableInstanceTypes, 93 maxInterruptProb: sa.InterruptProbability(-1), 94 want: nil, 95 wantErr: fmt.Errorf("invalid InterruptProbability: -1"), 96 }, 97 { 98 name: "bad_interrupt_prob_pos", 99 osType: sa.Linux, 100 region: sa.AwsRegion("eu-west-2"), 101 candidates: testAvailableInstanceTypes, 102 maxInterruptProb: sa.InterruptProbability(6), 103 want: nil, 104 wantErr: fmt.Errorf("invalid InterruptProbability: 6"), 105 }, 106 { 107 name: "bad_instance_region", 108 osType: sa.Linux, 109 region: sa.AwsRegion("us-foo-2"), 110 candidates: testAvailableInstanceTypes, 111 maxInterruptProb: sa.LessThanFifteenPct, 112 want: nil, 113 wantErr: fmt.Errorf("no spot advisor data for: {Linux, us-foo-2, < 15%%}"), 114 }, 115 } 116 for _, tt := range tests { 117 name := fmt.Sprintf("%s_%s_%s_%d", tt.name, tt.osType, tt.region, tt.maxInterruptProb) 118 t.Run(name, func(t *testing.T) { 119 got, gotErr := adv.FilterByMaxInterruptProbability(tt.osType, tt.region, tt.candidates, tt.maxInterruptProb) 120 checkErr(t, tt.wantErr, gotErr) 121 if tt.wantErr == nil { 122 checkEqual(t, tt.want, got) 123 } 124 }) 125 } 126 } 127 128 func TestGetInterruptRange(t *testing.T) { 129 defer setupMockTestServer(t).Close() 130 adv, err := sa.NewSpotAdvisor(testLogger, context.Background().Done()) 131 if err != nil { 132 t.Fatalf(err.Error()) 133 } 134 tests := []struct { 135 name string 136 osType sa.OsType 137 region sa.AwsRegion 138 instanceType sa.InstanceType 139 want sa.InterruptRange 140 wantErr error 141 }{ 142 { 143 name: "simple", 144 osType: sa.Windows, 145 region: sa.AwsRegion("us-west-2"), 146 instanceType: "c5a.24xlarge", 147 want: sa.TenToFifteenPct, 148 }, 149 { 150 name: "bad_region", 151 osType: sa.Windows, 152 region: sa.AwsRegion("us-foo-2"), 153 instanceType: "c5a.24xlarge", 154 want: -1, 155 wantErr: fmt.Errorf("no spot advisor data for: us-foo-2"), 156 }, 157 { 158 name: "bad_os", 159 osType: sa.OsType("Unix"), 160 region: sa.AwsRegion("us-west-2"), 161 instanceType: "c5a.24xlarge", 162 want: -1, 163 wantErr: fmt.Errorf("invalid OS: Unix"), 164 }, 165 { 166 name: "bad_instance_type", 167 osType: sa.Linux, 168 region: sa.AwsRegion("us-west-2"), 169 instanceType: "foo.bar", 170 want: -1, 171 wantErr: fmt.Errorf("no spot advisor data for Linux instance type 'foo.bar' in us-west-2"), 172 }, 173 } 174 for _, tt := range tests { 175 name := fmt.Sprintf("%s_%s_%s", tt.name, tt.osType, tt.region) 176 t.Run(name, func(t *testing.T) { 177 got, gotErr := adv.GetInterruptRange(tt.osType, tt.region, tt.instanceType) 178 checkErr(t, tt.wantErr, gotErr) 179 if tt.wantErr == nil && tt.want != got { 180 t.Fatalf("want: %s, got: %s", tt.want, got) 181 } 182 }) 183 } 184 } 185 186 func TestGetMaxInterruptProbability(t *testing.T) { 187 defer setupMockTestServer(t).Close() 188 adv, err := sa.NewSpotAdvisor(testLogger, context.Background().Done()) 189 if err != nil { 190 t.Fatalf(err.Error()) 191 } 192 tests := []struct { 193 name string 194 osType sa.OsType 195 region sa.AwsRegion 196 instanceType sa.InstanceType 197 want sa.InterruptProbability 198 wantErr error 199 }{ 200 { 201 name: "simple_<5%", 202 osType: sa.Linux, 203 region: sa.AwsRegion("eu-west-2"), 204 instanceType: "m5a.4xlarge", 205 want: sa.LessThanFivePct, 206 }, 207 { 208 name: "simple_<10%", 209 osType: sa.Linux, 210 region: sa.AwsRegion("eu-west-2"), 211 instanceType: "t3.nano", 212 want: sa.LessThanTenPct, 213 }, 214 { 215 name: "simple_<15%", 216 osType: sa.Linux, 217 region: sa.AwsRegion("eu-west-2"), 218 instanceType: "g4dn.12xlarge", 219 want: sa.LessThanFifteenPct, 220 }, 221 { 222 name: "simple_<20%", 223 osType: sa.Linux, 224 region: sa.AwsRegion("eu-west-2"), 225 instanceType: "r5d.8xlarge", 226 want: sa.LessThanTwentyPct, 227 }, 228 { 229 name: "simple_Any", 230 osType: sa.Linux, 231 region: sa.AwsRegion("eu-west-2"), 232 instanceType: "i3.2xlarge", 233 want: sa.Any, 234 }, 235 { 236 name: "bad_region", 237 osType: sa.Windows, 238 region: sa.AwsRegion("us-foo-2"), 239 instanceType: "c5a.24xlarge", 240 want: -1, 241 wantErr: fmt.Errorf("no spot advisor data for: us-foo-2"), 242 }, 243 { 244 name: "bad_os", 245 osType: sa.OsType("Unix"), 246 region: sa.AwsRegion("us-west-2"), 247 instanceType: "c5a.24xlarge", 248 want: -1, 249 wantErr: fmt.Errorf("invalid OS: Unix"), 250 }, 251 { 252 name: "bad_instance_type", 253 osType: sa.Linux, 254 region: sa.AwsRegion("us-west-2"), 255 instanceType: "foo.bar", 256 want: -1, 257 wantErr: fmt.Errorf("no spot advisor data for Linux instance type 'foo.bar' in us-west-2"), 258 }, 259 } 260 for _, tt := range tests { 261 t.Run(tt.name, func(t *testing.T) { 262 got, gotErr := adv.GetMaxInterruptProbability(tt.osType, tt.region, tt.instanceType) 263 checkErr(t, tt.wantErr, gotErr) 264 if tt.wantErr == nil && tt.want != got { 265 t.Fatalf("want: %s, got: %s", tt.want, got) 266 } 267 }) 268 } 269 270 } 271 272 // setupMockTestServer starts a test server and replaces the actual spot advisor 273 // data URL with the test server's URL. A request to the server will return the 274 // contents of the file at testDataPath. The caller is expected to call Close() 275 // on the returned test server. 276 func setupMockTestServer(t *testing.T) *httptest.Server { 277 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 278 b, err := ioutil.ReadFile(testDataPath) 279 if err != nil { 280 t.Fatal(err) 281 } 282 if _, err := w.Write(b); err != nil { 283 t.Fatal(err) 284 } 285 })) 286 sa.SetSpotAdvisorDataUrl(ts.URL) 287 return ts 288 } 289 290 func checkEqual(t *testing.T, want []string, got []string) { 291 if len(want) != len(got) { 292 t.Fatalf("\nwant:\t%s\ngot:\t%s", want, got) 293 } 294 sort.Strings(want) 295 sort.Strings(got) 296 if !reflect.DeepEqual(got, got) { 297 t.Fatalf("\nwant:\t%s\ngot:\t%s", want, got) 298 } 299 } 300 301 func checkErr(t *testing.T, want error, got error) { 302 if want != nil && got != nil { 303 if want.Error() != got.Error() { 304 t.Fatalf("want: %s, got: %s", want, got) 305 } else { 306 return 307 } 308 } 309 if want != got { 310 t.Fatalf("want: %s, got: %s", want, got) 311 } 312 } 313 314 var testLogger = log.New(ioutil.Discard, "", 0) 315 316 var testAvailableInstanceTypes = []string{ 317 "a1.2xlarge", "a1.4xlarge", "a1.large", "a1.metal", "a1.xlarge", "c1.xlarge", "c3.2xlarge", "c3.4xlarge", "c3.8xlarge", "c3.large", "c3.xlarge", 318 "c4.2xlarge", "c4.4xlarge", "c4.8xlarge", "c4.large", "c4.xlarge", "c5.12xlarge", "c5.18xlarge", "c5.24xlarge", "c5.2xlarge", "c5.4xlarge", "c5.9xlarge", 319 "c5.large", "c5.metal", "c5.xlarge", "c5a.12xlarge", "c5a.16xlarge", "c5a.24xlarge", "c5a.2xlarge", "c5a.4xlarge", "c5a.8xlarge", "c5a.large", "c5a.xlarge", 320 "c5ad.12xlarge", "c5ad.16xlarge", "c5ad.24xlarge", "c5ad.2xlarge", "c5ad.4xlarge", "c5ad.8xlarge", "c5ad.large", "c5ad.xlarge", "c5d.12xlarge", "c5d.18xlarge", 321 "c5d.24xlarge", "c5d.2xlarge", "c5d.4xlarge", "c5d.9xlarge", "c5d.large", "c5d.metal", "c5d.xlarge", "c5n.18xlarge", "c5n.2xlarge", "c5n.4xlarge", "c5n.9xlarge", 322 "c5n.large", "c5n.metal", "c5n.xlarge", "c6g.12xlarge", "c6g.16xlarge", "c6g.2xlarge", "c6g.4xlarge", "c6g.8xlarge", "c6g.large", "c6g.metal", "c6g.xlarge", 323 "c6gd.12xlarge", "c6gd.16xlarge", "c6gd.2xlarge", "c6gd.4xlarge", "c6gd.8xlarge", "c6gd.large", "c6gd.metal", "c6gd.xlarge", "c6gn.12xlarge", "c6gn.16xlarge", "c6gn.2xlarge", 324 "c6gn.4xlarge", "c6gn.8xlarge", "c6gn.large", "c6gn.xlarge", "cr1.8xlarge", "d2.2xlarge", "d2.4xlarge", "d2.8xlarge", "d2.xlarge", "d3.2xlarge", "d3.4xlarge", 325 "d3.8xlarge", "d3.xlarge", "d3en.12xlarge", "d3en.2xlarge", "d3en.4xlarge", "d3en.6xlarge", "d3en.8xlarge", "d3en.xlarge", "f1.16xlarge", "f1.2xlarge", "f1.4xlarge", 326 "g2.2xlarge", "g2.8xlarge", "g3.16xlarge", "g3.4xlarge", "g3.8xlarge", "g3s.xlarge", "g4ad.16xlarge", "g4ad.4xlarge", "g4ad.8xlarge", "g4dn.12xlarge", "g4dn.16xlarge", 327 "g4dn.2xlarge", "g4dn.4xlarge", "g4dn.8xlarge", "g4dn.metal", "g4dn.xlarge", "h1.16xlarge", "h1.2xlarge", "h1.4xlarge", "h1.8xlarge", "hs1.8xlarge", "i2.2xlarge", 328 "i2.4xlarge", "i2.8xlarge", "i2.xlarge", "i3.16xlarge", "i3.2xlarge", "i3.4xlarge", "i3.8xlarge", "i3.large", "i3.metal", "i3.xlarge", "i3en.12xlarge", 329 "i3en.24xlarge", "i3en.2xlarge", "i3en.3xlarge", "i3en.6xlarge", "i3en.large", "i3en.metal", "i3en.xlarge", "inf1.24xlarge", "inf1.2xlarge", "inf1.6xlarge", "inf1.xlarge", 330 "m1.large", "m1.xlarge", "m2.2xlarge", "m2.4xlarge", "m2.xlarge", "m3.2xlarge", "m3.large", "m3.xlarge", "m4.10xlarge", "m4.16xlarge", "m4.2xlarge", 331 "m4.4xlarge", "m4.large", "m4.xlarge", "m5.12xlarge", "m5.16xlarge", "m5.24xlarge", "m5.2xlarge", "m5.4xlarge", "m5.8xlarge", "m5.large", "m5.metal", 332 "m5.xlarge", "m5a.12xlarge", "m5a.16xlarge", "m5a.24xlarge", "m5a.2xlarge", "m5a.4xlarge", "m5a.8xlarge", "m5a.large", "m5a.xlarge", "m5ad.12xlarge", "m5ad.16xlarge", 333 "m5ad.24xlarge", "m5ad.2xlarge", "m5ad.4xlarge", "m5ad.8xlarge", "m5ad.large", "m5ad.xlarge", "m5d.12xlarge", "m5d.16xlarge", "m5d.24xlarge", "m5d.2xlarge", "m5d.4xlarge", 334 "m5d.8xlarge", "m5d.large", "m5d.metal", "m5d.xlarge", "m5dn.12xlarge", "m5dn.16xlarge", "m5dn.24xlarge", "m5dn.2xlarge", "m5dn.4xlarge", "m5dn.8xlarge", "m5dn.large", 335 "m5dn.metal", "m5dn.xlarge", "m5n.12xlarge", "m5n.16xlarge", "m5n.24xlarge", "m5n.2xlarge", "m5n.4xlarge", "m5n.8xlarge", "m5n.large", "m5n.metal", "m5n.xlarge", 336 "m5zn.12xlarge", "m5zn.2xlarge", "m5zn.3xlarge", "m5zn.6xlarge", "m5zn.large", "m5zn.metal", "m5zn.xlarge", "m6g.12xlarge", "m6g.16xlarge", "m6g.2xlarge", "m6g.4xlarge", 337 "m6g.8xlarge", "m6g.large", "m6g.metal", "m6g.xlarge", "m6gd.12xlarge", "m6gd.16xlarge", "m6gd.2xlarge", "m6gd.4xlarge", "m6gd.8xlarge", "m6gd.large", "m6gd.metal", 338 "m6gd.xlarge", "p2.16xlarge", "p2.8xlarge", "p2.xlarge", "p3.16xlarge", "p3.2xlarge", "p3.8xlarge", "p3dn.24xlarge", "p4d.24xlarge", "r3.2xlarge", "r3.4xlarge", 339 "r3.8xlarge", "r3.large", "r3.xlarge", "r4.16xlarge", "r4.2xlarge", "r4.4xlarge", "r4.8xlarge", "r4.large", "r4.xlarge", "r5.12xlarge", "r5.16xlarge", 340 "r5.24xlarge", "r5.2xlarge", "r5.4xlarge", "r5.8xlarge", "r5.large", "r5.metal", "r5.xlarge", "r5a.12xlarge", "r5a.16xlarge", "r5a.24xlarge", "r5a.2xlarge", 341 "r5a.4xlarge", "r5a.8xlarge", "r5a.large", "r5a.xlarge", "r5ad.12xlarge", "r5ad.16xlarge", "r5ad.24xlarge", "r5ad.2xlarge", "r5ad.4xlarge", "r5ad.8xlarge", "r5ad.large", 342 "r5ad.xlarge", "r5b.12xlarge", "r5b.16xlarge", "r5b.24xlarge", "r5b.2xlarge", "r5b.4xlarge", "r5b.8xlarge", "r5b.large", "r5b.metal", "r5b.xlarge", "r5d.12xlarge", 343 "r5d.16xlarge", "r5d.24xlarge", "r5d.2xlarge", "r5d.4xlarge", "r5d.8xlarge", "r5d.large", "r5d.metal", "r5d.xlarge", "r5dn.12xlarge", "r5dn.16xlarge", "r5dn.24xlarge", 344 "r5dn.2xlarge", "r5dn.4xlarge", "r5dn.8xlarge", "r5dn.large", "r5dn.metal", "r5dn.xlarge", "r5n.12xlarge", "r5n.16xlarge", "r5n.24xlarge", "r5n.2xlarge", "r5n.4xlarge", 345 "r5n.8xlarge", "r5n.large", "r5n.metal", "r5n.xlarge", "r6g.12xlarge", "r6g.16xlarge", "r6g.2xlarge", "r6g.4xlarge", "r6g.8xlarge", "r6g.large", "r6g.metal", 346 "r6g.xlarge", "r6gd.12xlarge", "r6gd.16xlarge", "r6gd.2xlarge", "r6gd.4xlarge", "r6gd.8xlarge", "r6gd.large", "r6gd.metal", "r6gd.xlarge", "t1.micro", "t2.2xlarge", 347 "t2.large", "t2.micro", "t2.nano", "t2.xlarge", "t3.2xlarge", "t3.large", "t3.micro", "t3.nano", "t3.xlarge", "t3a.2xlarge", "t3a.large", 348 "t3a.micro", "t3a.nano", "t3a.xlarge", "t4g.2xlarge", "t4g.large", "t4g.micro", "t4g.nano", "t4g.xlarge", "u-12tb1.112xlarge", "u-6tb1.112xlarge", "u-6tb1.56xlarge", 349 "u-9tb1.112xlarge", "x1.16xlarge", "x1.32xlarge", "x1e.16xlarge", "x1e.2xlarge", "x1e.32xlarge", "x1e.4xlarge", "x1e.8xlarge", "x1e.xlarge", "x2gd.12xlarge", "x2gd.16xlarge", 350 "x2gd.2xlarge", "x2gd.4xlarge", "x2gd.8xlarge", "x2gd.large", "x2gd.metal", "x2gd.xlarge", "z1d.12xlarge", "z1d.2xlarge", "z1d.3xlarge", "z1d.6xlarge", "z1d.large", 351 "z1d.metal", "z1d.xlarge", 352 }