github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/cloud/spotfeed/spotfeed_test.go (about) 1 package spotfeed 2 3 import ( 4 "bytes" 5 "compress/gzip" 6 "context" 7 "fmt" 8 "io/ioutil" 9 "log" 10 "sync" 11 "testing" 12 "time" 13 14 "github.com/aws/aws-sdk-go/aws" 15 "github.com/aws/aws-sdk-go/aws/request" 16 "github.com/aws/aws-sdk-go/service/s3" 17 "github.com/aws/aws-sdk-go/service/s3/s3iface" 18 "github.com/stretchr/testify/require" 19 ) 20 21 const ( 22 testAccountId = "123456789000" 23 ) 24 25 func TestFilter(t *testing.T) { 26 ptr := func(t time.Time) *time.Time { 27 return &t 28 } 29 30 for _, test := range []struct { 31 name string 32 filters filters 33 filterable filterable 34 expectedFilterResult bool 35 }{ 36 { 37 "passes_filter", 38 filters{ 39 AccountId: testAccountId, 40 StartTime: ptr(time.Date(2020, 01, 01, 01, 00, 00, 00, time.UTC)), 41 EndTime: ptr(time.Date(2020, 02, 01, 01, 00, 00, 00, time.UTC)), 42 Version: 1, 43 }, 44 &fileMeta{ 45 nil, 46 testAccountId + ".2021-02-23-04.004.8a6d6bb8", 47 testAccountId, 48 time.Date(2020, 01, 10, 04, 0, 0, 0, time.UTC), 49 1, 50 false, 51 }, 52 false, 53 }, 54 { 55 "filtered_out_by_meta", 56 filters{ 57 Version: 1, 58 }, 59 &fileMeta{ 60 nil, 61 testAccountId + ".2021-02-23-04.004.8a6d6bb8", 62 testAccountId, 63 time.Date(2021, 02, 23, 04, 0, 0, 0, time.UTC), 64 4, 65 false, 66 }, 67 true, 68 }, 69 { 70 "filtered_out_by_start_time", 71 filters{ 72 StartTime: ptr(time.Date(2020, 01, 01, 01, 00, 00, 00, time.UTC)), 73 }, 74 &fileMeta{ 75 nil, 76 testAccountId + ".2021-02-23-04.004.8a6d6bb8", 77 testAccountId, 78 time.Date(2019, 02, 23, 04, 0, 0, 0, time.UTC), 79 4, 80 false, 81 }, 82 true, 83 }, 84 } { 85 t.Run(test.name, func(t *testing.T) { 86 require.Equal(t, test.expectedFilterResult, test.filters.filter(test.filterable)) 87 }) 88 } 89 } 90 91 // gzipBytes takes some bytes and performs compression with compress/gzip. 92 func gzipBytes(b []byte) ([]byte, error) { 93 var buffer bytes.Buffer 94 gw := gzip.NewWriter(&buffer) 95 if _, err := gw.Write(b); err != nil { 96 return nil, err 97 } 98 if err := gw.Close(); err != nil { 99 return nil, err 100 } 101 return buffer.Bytes(), nil 102 } 103 104 type mockS3Client struct { 105 s3iface.S3API 106 107 getObjectResults map[string]string // path (bucket/key) -> decompressed response contents 108 109 listMu sync.Mutex 110 listObjectPageResults [][]string // [](result path slices) 111 } 112 113 func (m *mockS3Client) ListObjectsV2PagesWithContext(_ aws.Context, input *s3.ListObjectsV2Input, callback func(*s3.ListObjectsV2Output, bool) bool, _ ...request.Option) error { 114 m.listMu.Lock() 115 defer m.listMu.Unlock() 116 117 if len(m.listObjectPageResults) == 0 { 118 return fmt.Errorf("unexpected attempt to list s3 objects at path %s/%s", aws.StringValue(input.Bucket), aws.StringValue(input.Prefix)) 119 } 120 var currKeys []string 121 currKeys, m.listObjectPageResults = m.listObjectPageResults[0], m.listObjectPageResults[1:] 122 123 resultObjects := make([]*s3.Object, len(currKeys)) 124 for i, key := range currKeys { 125 resultObjects[i] = &s3.Object{ 126 Key: aws.String(key), 127 } 128 } 129 130 callback(&s3.ListObjectsV2Output{Contents: resultObjects}, true) 131 return nil 132 } 133 134 func (m *mockS3Client) GetObjectWithContext(_ aws.Context, input *s3.GetObjectInput, _ ...request.Option) (*s3.GetObjectOutput, error) { 135 path := fmt.Sprintf("%s/%s", aws.StringValue(input.Bucket), aws.StringValue(input.Key)) 136 if v, ok := m.getObjectResults[path]; ok { 137 // compress the response via gzip 138 gzContents, err := gzipBytes([]byte(v)) 139 if err != nil { 140 return nil, fmt.Errorf("failed to compress test response at %s: %s", path, err) 141 } 142 return &s3.GetObjectOutput{ 143 // compress the response contents via gzip 144 Body: ioutil.NopCloser(bytes.NewReader(gzContents)), 145 }, nil 146 } else { 147 return nil, fmt.Errorf("attempted to get unexpected path %s from mock s3", path) 148 } 149 } 150 151 func TestLoaderFetch(t *testing.T) { 152 ctx := context.Background() 153 devNull := log.New(ioutil.Discard, "", 0) 154 for _, test := range []struct { 155 name string 156 loader Loader 157 expectedEntries []*Entry 158 }{ 159 { 160 "s3_no_source_files", 161 NewS3Loader("test-bucket", "", &mockS3Client{ 162 listObjectPageResults: [][]string{ 163 {}, // single empty response 164 }, 165 }, devNull, testAccountId, nil, nil, 0), 166 []*Entry{}, 167 }, 168 { 169 "s3_single_source_file", 170 NewS3Loader("test-bucket", "", &mockS3Client{ 171 listObjectPageResults: [][]string{ 172 { // single populated response 173 testAccountId + ".2021-02-25-15.002.3eb820a5.gz", 174 }, 175 }, 176 getObjectResults: map[string]string{ 177 "test-bucket/" + testAccountId + ".2021-02-25-15.002.3eb820a5.gz": `#Version: 1.0 178 #Fields: Timestamp UsageType Operation InstanceID MyBidID MyMaxPrice MarketPrice Charge Version 179 2021-02-20 18:52:50 UTC USW2-SpotUsage:x1.16xlarge RunInstances:SV002 i-0053c2917e2afa2f0 sir-yb3gavgp 6.669 USD 2.001 USD 0.073 USD 2 180 2021-02-20 18:50:58 UTC USW2-SpotUsage:x1.16xlarge RunInstances:SV002 i-07eaa4b2bf27c4b75 sir-w13gadim 6.669 USD 2.001 USD 1.741 USD 2`, 181 }, 182 }, devNull, testAccountId, nil, nil, 0), 183 []*Entry{ 184 { 185 AccountId: testAccountId, 186 Timestamp: time.Date(2021, 02, 20, 18, 52, 50, 0, time.UTC), 187 UsageType: "USW2-SpotUsage:x1.16xlarge", 188 Instance: "x1.16xlarge", 189 Operation: "RunInstances:SV002", 190 InstanceID: "i-0053c2917e2afa2f0", 191 MyBidID: "sir-yb3gavgp", 192 MyMaxPriceUSD: 6.669, 193 MarketPriceUSD: 2.001, 194 ChargeUSD: 0.073, 195 Version: 2, 196 }, 197 { 198 AccountId: testAccountId, 199 Timestamp: time.Date(2021, 02, 20, 18, 50, 58, 0, time.UTC), 200 UsageType: "USW2-SpotUsage:x1.16xlarge", 201 Instance: "x1.16xlarge", 202 Operation: "RunInstances:SV002", 203 InstanceID: "i-07eaa4b2bf27c4b75", 204 MyBidID: "sir-w13gadim", 205 MyMaxPriceUSD: 6.669, 206 MarketPriceUSD: 2.001, 207 ChargeUSD: 1.741, 208 Version: 2, 209 }, 210 }, 211 }, 212 { 213 "local_no_source_files", 214 NewLocalLoader( 215 "testdata/no_source_files", 216 devNull, testAccountId, nil, nil, 0), 217 []*Entry{}, 218 }, 219 { 220 "local_single_source_file", 221 NewLocalLoader( 222 "testdata/single_source_file", 223 devNull, testAccountId, nil, nil, 0), 224 []*Entry{ 225 { 226 AccountId: testAccountId, 227 Timestamp: time.Date(2021, 02, 20, 18, 52, 50, 0, time.UTC), 228 UsageType: "USW2-SpotUsage:x1.16xlarge", 229 Instance: "x1.16xlarge", 230 Operation: "RunInstances:SV002", 231 InstanceID: "i-0053c2917e2afa2f0", 232 MyBidID: "sir-yb3gavgp", 233 MyMaxPriceUSD: 6.669, 234 MarketPriceUSD: 2.001, 235 ChargeUSD: 0.073, 236 Version: 2, 237 }, 238 { 239 AccountId: testAccountId, 240 Timestamp: time.Date(2021, 02, 20, 18, 50, 58, 0, time.UTC), 241 UsageType: "USW2-SpotUsage:x1.16xlarge", 242 Instance: "x1.16xlarge", 243 Operation: "RunInstances:SV002", 244 InstanceID: "i-07eaa4b2bf27c4b75", 245 MyBidID: "sir-w13gadim", 246 MyMaxPriceUSD: 6.669, 247 MarketPriceUSD: 2.001, 248 ChargeUSD: 1.741, 249 Version: 2, 250 }, 251 { 252 AccountId: testAccountId, 253 Timestamp: time.Date(2021, 02, 20, 18, 56, 14, 0, time.UTC), 254 UsageType: "USW2-SpotUsage:x1.32xlarge", 255 Instance: "x1.32xlarge", 256 Operation: "RunInstances:SV002", 257 InstanceID: "i-000e2cebfe213246e", 258 MyBidID: "sir-fcg8btin", 259 MyMaxPriceUSD: 13.338, 260 MarketPriceUSD: 4.001, 261 ChargeUSD: 2.636, 262 Version: 2, 263 }, 264 { 265 AccountId: testAccountId, 266 Timestamp: time.Date(2021, 02, 20, 18, 56, 01, 0, time.UTC), 267 UsageType: "USW2-SpotUsage:x1.32xlarge", 268 Instance: "x1.32xlarge", 269 Operation: "RunInstances:SV002", 270 InstanceID: "i-032a1a622fb441a7b", 271 MyBidID: "sir-c6ag9vxn", 272 MyMaxPriceUSD: 13.338, 273 MarketPriceUSD: 4.001, 274 ChargeUSD: 4.001, 275 Version: 2, 276 }, 277 }, 278 }, 279 } { 280 t.Run(test.name, func(t *testing.T) { 281 entries, err := test.loader.Fetch(ctx, false) 282 require.NoError(t, err, "unexpected error fetching local feed files") 283 require.Equal(t, test.expectedEntries, entries) 284 }) 285 } 286 } 287 288 func TestLoaderStream(t *testing.T) { 289 ctx := context.Background() 290 devNull := log.New(ioutil.Discard, "", 0) 291 292 loader := NewS3Loader("test-bucket", "", &mockS3Client{ 293 listObjectPageResults: [][]string{ 294 { 295 // initial empty response 296 }, 297 { // single file response 298 testAccountId + ".2100-02-20-15.002.3eb820a5.gz", 299 }, 300 }, 301 getObjectResults: map[string]string{ 302 "test-bucket/" + testAccountId + "2100-02-20-15.002.3eb820a5.gz": `#Version: 1.0 303 #Fields: Timestamp UsageType Operation InstanceID MyBidID MyMaxPrice MarketPrice Charge Version 304 2100-02-20 18:52:50 UTC USW2-SpotUsage:x1.16xlarge RunInstances:SV002 i-0053c2917e2afa2f0 sir-yb3gavgp 6.669 USD 2.001 USD 0.073 USD 2 305 2100-02-20 18:50:58 UTC USW2-SpotUsage:x1.16xlarge RunInstances:SV002 i-07eaa4b2bf27c4b75 sir-w13gadim 6.669 USD 2.001 USD 1.741 USD 2`, 306 }, 307 }, devNull, testAccountId, nil, nil, 0) 308 309 // speed up sleep duration to drain list objects slice 310 streamSleepDuration = time.Second 311 312 entryChan, err := loader.Stream(ctx, false) 313 require.NoError(t, err, "unexpected err streaming s3 entries") 314 315 // test successful drain of two entries channel 316 for i := 0; i < 2; i++ { 317 <-entryChan 318 } 319 320 // kill the Stream goroutine 321 ctx.Done() 322 }