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  }*/