github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/cloud/providers/ec2/ec2_test.go (about) 1 package ec2 2 3 import ( 4 "testing" 5 "time" 6 7 "github.com/evergreen-ci/evergreen/testutil" 8 "github.com/pkg/errors" 9 . "github.com/smartystreets/goconvey/convey" 10 ) 11 12 var testConfig = testutil.TestConfig() 13 14 // mins returns a time X minutes after UNIX epoch 15 func mins(x int64) time.Time { 16 return time.Unix(60*x, 0) 17 } 18 19 func TestCostForRange(t *testing.T) { 20 Convey("With 5 hours of test host billing", t, func() { 21 rates := []spotRate{ 22 {Time: mins(0), Price: 1.0}, 23 {Time: mins(60), Price: .5}, 24 {Time: mins(2 * 60), Price: 1.0}, 25 {Time: mins(3 * 60), Price: 2.0}, 26 {Time: mins(4 * 60), Price: 1.0}, 27 } 28 Convey("and a 30-min task that ran within the first hour", func() { 29 price := spotCostForRange(mins(10), mins(40), rates) 30 Convey("should cost .5", func() { 31 So(price, ShouldEqual, .5) 32 }) 33 }) 34 Convey("and a 30-min task that ran within the last hour", func() { 35 price := spotCostForRange(mins(10+4*60), mins(40+4*60), rates) 36 Convey("should cost .5", func() { 37 So(price, ShouldEqual, .5) 38 }) 39 }) 40 Convey("and a 30-min task that ran between the first and second hour", func() { 41 price := spotCostForRange(mins(45), mins(75), rates) 42 Convey("should cost .25 + .125", func() { 43 So(price, ShouldEqual, .375) 44 }) 45 }) 46 Convey("and a 120-min task that ran between the first three hours", func() { 47 price := spotCostForRange(mins(30), mins(150), rates) 48 Convey("should cost .5 + .5 + .5", func() { 49 So(price, ShouldEqual, 1.5) 50 }) 51 }) 52 Convey("and an 30-min task started after the last reported time", func() { 53 price := spotCostForRange(mins(4*60+30), mins(5*60), rates) 54 Convey("should cost .5", func() { 55 So(price, ShouldEqual, .5) 56 }) 57 }) 58 Convey("and an earlier 30-min task started after the last reported time", func() { 59 price := spotCostForRange(mins(4*60), mins(5*60), rates) 60 Convey("should cost 1", func() { 61 So(price, ShouldEqual, 1) 62 }) 63 }) 64 Convey("and a task that starts at the same time as the first bucket", func() { 65 price := spotCostForRange(mins(0), mins(30), rates) 66 Convey("should still report a proper time", func() { 67 So(price, ShouldEqual, .5) 68 }) 69 }) 70 Convey("and a task that starts at before the first bucket", func() { 71 price := spotCostForRange(mins(-60), mins(45), rates) 72 Convey("should compute based on the first bucket", func() { 73 So(price, ShouldEqual, 1.75) 74 }) 75 }) 76 }) 77 } 78 79 func TestSpotPriceHistory(t *testing.T) { 80 testutil.ConfigureIntegrationTest(t, testConfig, "TestSpotPriceHistory") 81 Convey("With a Spot Manager", t, func() { 82 m := &EC2SpotManager{} 83 testutil.HandleTestingErr(m.Configure(testConfig), t, "failed to configure spot manager") 84 Convey("loading 2 hours of price history should succeed", func() { 85 ps, err := m.describeHourlySpotPriceHistory("m3.large", "us-east-1a", osLinux, 86 time.Now().Add(-2*time.Hour), time.Now()) 87 So(err, ShouldBeNil) 88 So(len(ps), ShouldBeGreaterThan, 2) 89 Convey("and the results should be sane", func() { 90 So(ps[len(ps)-1].Time, ShouldHappenBetween, 91 time.Now().Add(-10*time.Minute), time.Now()) 92 So(ps[0].Price, ShouldBeBetween, 0.0, 2.0) 93 So(ps[0].Time, ShouldHappenBefore, ps[1].Time) 94 }) 95 }) 96 Convey("loading 10 days of price history should succeed", func() { 97 ps, err := m.describeHourlySpotPriceHistory("m3.large", "us-east-1a", osLinux, 98 time.Now().Add(-240*time.Hour), time.Now()) 99 So(err, ShouldBeNil) 100 So(len(ps), ShouldBeGreaterThan, 240) 101 Convey("and the results should be sane", func() { 102 So(ps[len(ps)-1].Time, ShouldHappenBetween, time.Now().Add(-30*time.Minute), time.Now()) 103 So(ps[0].Time, ShouldHappenWithin, 104 time.Hour, time.Now().Add(-241*time.Hour)) 105 So(ps[0].Price, ShouldBeBetween, 0.0, 2.0) 106 So(ps[0].Time, ShouldHappenBefore, ps[1].Time) 107 }) 108 }) 109 }) 110 } 111 112 func TestFetchEBSPricing(t *testing.T) { 113 testutil.ConfigureIntegrationTest(t, testConfig, "TestFetchEBSPricing") 114 Convey("Fetching the map of EBS pricing should succeed", t, func() { 115 prices, err := fetchEBSPricing() 116 So(err, ShouldBeNil) 117 Convey("and the resulting map should be sane", func() { 118 So(len(prices), ShouldBeGreaterThan, 5) 119 So(prices["us-east-1"], ShouldBeBetween, 0, 1) 120 }) 121 }) 122 } 123 124 type mockEBSPriceFetcher struct { 125 response map[string]float64 126 err error 127 } 128 129 func (mpf mockEBSPriceFetcher) FetchEBSPrices() (map[string]float64, error) { 130 if mpf.err != nil { 131 return nil, mpf.err 132 } 133 return mpf.response, nil 134 } 135 136 func TestEBSCostCalculation(t *testing.T) { 137 Convey("With a price of $1.00/GB-Month", t, func() { 138 region := "X" 139 pf := mockEBSPriceFetcher{ 140 response: map[string]float64{ 141 region: 1.00, 142 }, 143 } 144 Convey("a 1-GB drive for 1 month should cost $1", func() { 145 cost, err := ebsCost(pf, region, 1, time.Hour*24*30) 146 So(err, ShouldBeNil) 147 So(cost, ShouldEqual, 1.00) 148 }) 149 Convey("a 20-GB drive for 1 month should cost $20", func() { 150 cost, err := ebsCost(pf, region, 20, time.Hour*24*30) 151 So(err, ShouldBeNil) 152 So(cost, ShouldEqual, 20) 153 }) 154 Convey("a 100-GB drive for 1 hour should cost around $0.14", func() { 155 cost, err := ebsCost(pf, region, 100, time.Hour) 156 So(err, ShouldBeNil) 157 So(cost, ShouldBeBetween, 0.13, 0.14) 158 }) 159 Convey("a 100-GB drive for 20 mins should cost around $0.04", func() { 160 cost, err := ebsCost(pf, region, 100, time.Minute*20) 161 So(err, ShouldBeNil) 162 So(cost, ShouldBeBetween, 0.04, 0.05) 163 }) 164 }) 165 166 Convey("With erroring price fetchers", t, func() { 167 Convey("a network error should bubble up", func() { 168 pf := mockEBSPriceFetcher{err: errors.New("NETWORK OH NO")} 169 _, err := ebsCost(pf, "", 100, time.Minute*20) 170 So(err, ShouldNotBeNil) 171 }) 172 Convey("a made-up region should return an error", func() { 173 pf := mockEBSPriceFetcher{response: map[string]float64{}} 174 _, err := ebsCost(pf, "mars-west-1", 100, time.Minute*20) 175 So(err, ShouldNotBeNil) 176 }) 177 }) 178 } 179 180 func TestEBSPriceCaching(t *testing.T) { 181 testutil.ConfigureIntegrationTest(t, testConfig, "TestEBSPriceCaching") 182 Convey("With an empty cachedEBSPriceFetcher", t, func() { 183 pf := cachedEBSPriceFetcher{} 184 So(pf.prices, ShouldBeNil) 185 Convey("running FetchEBSPrices should return a map and cache it", func() { 186 prices, err := pf.FetchEBSPrices() 187 So(err, ShouldBeNil) 188 So(prices, ShouldNotBeNil) 189 So(prices, ShouldResemble, pf.prices) 190 Convey("but a cache should not change if we call fetch again", func() { 191 pf.m.Lock() 192 pf.prices["NEW"] = 1 193 pf.m.Unlock() 194 prices, err := pf.FetchEBSPrices() 195 So(err, ShouldBeNil) 196 So(prices, ShouldNotBeNil) 197 So(prices["NEW"], ShouldEqual, 1.0) 198 }) 199 }) 200 }) 201 } 202 203 func TestOnDemandPriceAPITranslation(t *testing.T) { 204 Convey("With a set of OS types", t, func() { 205 Convey("Linux/UNIX should become Linux", func() { 206 So(osBillingName(osLinux), ShouldEqual, "Linux") 207 }) 208 Convey("other OSes should stay the same", func() { 209 So(osBillingName(osSUSE), ShouldEqual, string(osSUSE)) 210 So(osBillingName(osWindows), ShouldEqual, string(osWindows)) 211 }) 212 }) 213 214 Convey("With a set of region names", t, func() { 215 Convey("the full region name should be returned", func() { 216 r, err := regionFullname("us-east-1") 217 So(err, ShouldBeNil) 218 So(r, ShouldEqual, "US East (N. Virginia)") 219 r, err = regionFullname("us-west-1") 220 So(err, ShouldBeNil) 221 So(r, ShouldEqual, "US West (N. California)") 222 r, err = regionFullname("us-west-2") 223 So(err, ShouldBeNil) 224 So(r, ShouldEqual, "US West (Oregon)") 225 Convey("but an unknown region will return an error", func() { 226 r, err = regionFullname("amazing") 227 So(err, ShouldNotBeNil) 228 }) 229 }) 230 }) 231 } 232 233 type mockODPriceFetcher struct { 234 price float64 235 err error 236 } 237 238 func (mpf *mockODPriceFetcher) FetchPrice(_ osType, _, _ string) (float64, error) { 239 if mpf.err != nil { 240 return 0, mpf.err 241 } 242 return mpf.price, nil 243 } 244 245 func TestOnDemandPriceCalculation(t *testing.T) { 246 Convey("With prices of $1.00/hr", t, func() { 247 pf := &mockODPriceFetcher{1.0, nil} 248 Convey("a half-hour task should cost 50ยข", func() { 249 cost, err := onDemandCost(pf, osLinux, "m3.4xlarge", "us-east-1", time.Minute*30) 250 So(err, ShouldBeNil) 251 So(cost, ShouldEqual, .50) 252 }) 253 Convey("an hour task should cost $1", func() { 254 cost, err := onDemandCost(pf, osLinux, "m3.4xlarge", "us-east-1", time.Hour) 255 So(err, ShouldBeNil) 256 So(cost, ShouldEqual, 1) 257 }) 258 Convey("a two-hour task should cost $2", func() { 259 cost, err := onDemandCost(pf, osLinux, "m3.4xlarge", "us-east-1", time.Hour*2) 260 So(err, ShouldBeNil) 261 So(cost, ShouldEqual, 2) 262 }) 263 }) 264 Convey("With prices of $0.00/hr", t, func() { 265 pf := &mockODPriceFetcher{0, nil} 266 Convey("onDemandPrice should return a 'not found' error", func() { 267 cost, err := onDemandCost(pf, osLinux, "m3.4xlarge", "us-east-1", time.Hour) 268 So(err, ShouldNotBeNil) 269 So(err.Error(), ShouldContainSubstring, "not found") 270 So(cost, ShouldEqual, 0) 271 }) 272 }) 273 Convey("With an erroring PriceFetcher", t, func() { 274 pf := &mockODPriceFetcher{1, errors.New("bad thing")} 275 Convey("errors should be bubbled up", func() { 276 cost, err := onDemandCost(pf, osLinux, "m3.4xlarge", "us-east-1", time.Hour*2) 277 So(err, ShouldNotBeNil) 278 So(err.Error(), ShouldContainSubstring, "bad thing") 279 So(cost, ShouldEqual, 0) 280 }) 281 }) 282 } 283 284 func TestFetchOnDemandPricing(t *testing.T) { 285 testutil.ConfigureIntegrationTest(t, testConfig, "TestOnDemandPriceCaching") 286 Convey("With an empty cachedOnDemandPriceFetcher", t, func() { 287 pf := cachedOnDemandPriceFetcher{} 288 So(pf.prices, ShouldBeNil) 289 Convey("various prices in us-east-1 should be sane", func() { 290 c34x, err := pf.FetchPrice(osLinux, "c3.4xlarge", "us-east-1") 291 So(err, ShouldBeNil) 292 So(c34x, ShouldBeGreaterThan, .80) 293 c3x, err := pf.FetchPrice(osLinux, "c3.xlarge", "us-east-1") 294 So(err, ShouldBeNil) 295 So(c3x, ShouldBeGreaterThan, .20) 296 So(c34x, ShouldBeGreaterThan, c3x) 297 wc3x, err := pf.FetchPrice(osWindows, "c3.xlarge", "us-east-1") 298 So(err, ShouldBeNil) 299 So(wc3x, ShouldBeGreaterThan, .20) 300 So(wc3x, ShouldBeGreaterThan, c3x) 301 302 Convey("and prices should be cached", func() { 303 So(len(pf.prices), ShouldBeGreaterThan, 50) 304 }) 305 }) 306 }) 307 } 308 309 /* This is an example of how the cost calculation functions work. 310 This function can be uncommented to manually play with 311 func TestCostForDuration(t *testing.T) { 312 testutil.ConfigureIntegrationTest(t, testConfig, "TestSpotPriceHistory") 313 m := &EC2Manager{} 314 m.Configure(testConfig) 315 h := &host.Host{Id: "i-359e91ac"} 316 h.Distro.Arch = "windows_amd64" 317 layout := "Jan 2, 2006 3:04:05 pm -0700" 318 start, err := time.Parse(layout, "Sep 8, 2016 11:00:22 am -0400") 319 if err != nil { 320 panic(err) 321 } 322 fmt.Println(start) 323 end, err := time.Parse(layout, "Sep 8, 2016 12:00:49 pm -0400") 324 if err != nil { 325 panic(err) 326 } 327 cost, err := m.CostForDuration(h, start, end) 328 if err != nil { 329 panic(err) 330 } 331 fmt.Println("PRICE", cost) 332 cost, err = m.CostForDuration(h, start, end) 333 if err != nil { 334 panic(err) 335 } 336 fmt.Println("PRICE AGAIN", cost) 337 }*/