spotapi_test.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. package aws
  2. import (
  3. "context"
  4. "fmt"
  5. "testing"
  6. "time"
  7. awsSDK "github.com/aws/aws-sdk-go-v2/aws"
  8. "github.com/aws/aws-sdk-go-v2/service/ec2"
  9. ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
  10. )
  11. // fakeSpotClient implements spotPriceHistoryClient by returning a fixed set of
  12. // pages. Each element in pages is one page of results.
  13. type fakeSpotClient struct {
  14. pages [][]ec2Types.SpotPrice
  15. calls int
  16. err error // if set, returned on every NextPage call
  17. }
  18. func (f *fakeSpotClient) DescribeSpotPriceHistory(
  19. _ context.Context,
  20. _ *ec2.DescribeSpotPriceHistoryInput,
  21. _ ...func(*ec2.Options),
  22. ) (*ec2.DescribeSpotPriceHistoryOutput, error) {
  23. if f.err != nil {
  24. return nil, f.err
  25. }
  26. if f.calls >= len(f.pages) {
  27. return &ec2.DescribeSpotPriceHistoryOutput{}, nil
  28. }
  29. page := f.pages[f.calls]
  30. f.calls++
  31. var nextToken *string
  32. if f.calls < len(f.pages) {
  33. nextToken = awsSDK.String("next")
  34. }
  35. return &ec2.DescribeSpotPriceHistoryOutput{
  36. SpotPriceHistory: page,
  37. NextToken: nextToken,
  38. }, nil
  39. }
  40. func spotItem(instanceType, price string, az string) ec2Types.SpotPrice {
  41. ts := time.Now()
  42. return ec2Types.SpotPrice{
  43. InstanceType: ec2Types.InstanceType(instanceType),
  44. SpotPrice: awsSDK.String(price),
  45. Timestamp: &ts,
  46. AvailabilityZone: awsSDK.String(az),
  47. }
  48. }
  49. func TestQuerySpotPrices_Basic(t *testing.T) {
  50. client := &fakeSpotClient{
  51. pages: [][]ec2Types.SpotPrice{
  52. {
  53. spotItem("m5.large", "0.05", "us-west-2a"),
  54. spotItem("c5.xlarge", "0.10", "us-west-2b"),
  55. },
  56. },
  57. }
  58. results, err := querySpotPrices(context.Background(), "us-west-2", client)
  59. if err != nil {
  60. t.Fatalf("unexpected error: %v", err)
  61. }
  62. if len(results) != 2 {
  63. t.Fatalf("expected 2 results, got %d", len(results))
  64. }
  65. byType := make(map[string]float64)
  66. for _, r := range results {
  67. byType[r.InstanceType] = r.Price
  68. }
  69. if byType["m5.large"] != 0.05 {
  70. t.Errorf("m5.large price = %f, want 0.05", byType["m5.large"])
  71. }
  72. if byType["c5.xlarge"] != 0.10 {
  73. t.Errorf("c5.xlarge price = %f, want 0.10", byType["c5.xlarge"])
  74. }
  75. }
  76. func TestQuerySpotPrices_DeduplicatesAcrossAZs(t *testing.T) {
  77. // Same instance type appears in multiple AZs — only the first should be kept.
  78. client := &fakeSpotClient{
  79. pages: [][]ec2Types.SpotPrice{
  80. {
  81. spotItem("m5.large", "0.05", "us-west-2a"),
  82. spotItem("m5.large", "0.09", "us-west-2b"), // duplicate, should be skipped
  83. },
  84. },
  85. }
  86. results, err := querySpotPrices(context.Background(), "us-west-2", client)
  87. if err != nil {
  88. t.Fatalf("unexpected error: %v", err)
  89. }
  90. if len(results) != 1 {
  91. t.Fatalf("expected 1 result after dedup, got %d", len(results))
  92. }
  93. if results[0].Price != 0.05 {
  94. t.Errorf("expected first price 0.05, got %f", results[0].Price)
  95. }
  96. }
  97. func TestQuerySpotPrices_MultiPage(t *testing.T) {
  98. client := &fakeSpotClient{
  99. pages: [][]ec2Types.SpotPrice{
  100. {spotItem("m5.large", "0.05", "us-west-2a")},
  101. {spotItem("c5.xlarge", "0.10", "us-west-2a")},
  102. {spotItem("r5.2xlarge", "0.20", "us-west-2a")},
  103. },
  104. }
  105. results, err := querySpotPrices(context.Background(), "us-west-2", client)
  106. if err != nil {
  107. t.Fatalf("unexpected error: %v", err)
  108. }
  109. if len(results) != 3 {
  110. t.Fatalf("expected 3 results across pages, got %d", len(results))
  111. }
  112. }
  113. func TestQuerySpotPrices_APIError(t *testing.T) {
  114. client := &fakeSpotClient{
  115. err: fmt.Errorf("api unavailable"),
  116. }
  117. _, err := querySpotPrices(context.Background(), "us-west-2", client)
  118. if err == nil {
  119. t.Fatal("expected error, got nil")
  120. }
  121. }