module_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. package basic
  2. import (
  3. "context"
  4. "errors"
  5. "io"
  6. "os"
  7. "testing"
  8. "github.com/opencost/opencost/core/pkg/pricing"
  9. "github.com/opencost/opencost/core/pkg/storage"
  10. "github.com/opencost/opencost/core/pkg/unit"
  11. "github.com/stretchr/testify/require"
  12. )
  13. // TestNewBasicPricingModuleEmptyStore verifies the constructor populates a
  14. // default pricing set when given an empty store.
  15. func TestNewBasicPricingModuleEmptyStore(t *testing.T) {
  16. store := pricing.NewMemoryPricingStore()
  17. pm, err := NewBasicPricingModule(store)
  18. require.NoError(t, err)
  19. ps, err := store.GetPricingSet(t.Context())
  20. require.NoError(t, err)
  21. require.False(t, ps.IsEmpty())
  22. np, err := pm.getNodePricing(t.Context())
  23. require.NoError(t, err)
  24. require.NotNil(t, np)
  25. }
  26. func TestPricingModule(t *testing.T) {
  27. memoryPricingStore := pricing.NewMemoryPricingStore()
  28. filePricingStore, err := pricing.NewStoragePricingStore(t.Context(), newFileStorage(t), "pricing.json")
  29. require.NoError(t, err)
  30. stores := map[string]pricing.PricingStore{
  31. "MemoryPricingStore": memoryPricingStore,
  32. "StoragePricingStore": filePricingStore,
  33. }
  34. for name, store := range stores {
  35. t.Run(name, testPricingModuleWithStore(store))
  36. }
  37. }
  38. func testPricingModuleWithStore(store pricing.PricingStore) func(t *testing.T) {
  39. return func(t *testing.T) {
  40. ctx := t.Context()
  41. pm, err := NewBasicPricingModule(store)
  42. require.NoError(t, err)
  43. t.Run("DefaultPricing", func(t *testing.T) {
  44. testDefaultPricing(t, ctx, pm)
  45. })
  46. t.Run("Metadata", func(t *testing.T) {
  47. testMetadata(t, pm)
  48. })
  49. t.Run("GetPricingSet", func(t *testing.T) {
  50. testGetPricingSet(t, ctx, pm)
  51. })
  52. t.Run("SetClusterPricePerHour", func(t *testing.T) {
  53. testSetClusterPricePerHour(t, ctx, pm)
  54. })
  55. t.Run("SetNetworkPrices", func(t *testing.T) {
  56. testSetNetworkPrices(t, ctx, pm)
  57. })
  58. t.Run("SetServicePricePerHour", func(t *testing.T) {
  59. testSetServicePricePerHour(t, ctx, pm)
  60. })
  61. t.Run("Checksum", func(t *testing.T) {
  62. testChecksum(t, ctx, pm)
  63. })
  64. t.Run("SetNodePricePerCPUCoreHour", func(t *testing.T) {
  65. testSetNodePricePerCPUCoreHour(t, ctx, pm)
  66. })
  67. t.Run("SetNodePricePerRAMGiBHour", func(t *testing.T) {
  68. testSetNodePricePerRAMGiBHour(t, ctx, pm)
  69. })
  70. t.Run("SetNodePricePerGPUHour", func(t *testing.T) {
  71. testSetNodePricePerGPUHour(t, ctx, pm)
  72. })
  73. t.Run("SetNodePricePerLocalDiskGiBHour", func(t *testing.T) {
  74. testSetNodePricePerLocalDiskGiBHour(t, ctx, pm)
  75. })
  76. t.Run("SetVolumePricePerStorageGiBHour", func(t *testing.T) {
  77. testSetVolumePricePerStorageGiBHour(t, ctx, pm)
  78. })
  79. t.Run("NewNodePricingReader", func(t *testing.T) {
  80. testNewNodePricingReader(t, ctx, pm)
  81. })
  82. t.Run("NewVolumePricingReader", func(t *testing.T) {
  83. testNewVolumePricingReader(t, ctx, pm)
  84. })
  85. t.Run("NewClusterPricingReader", func(t *testing.T) {
  86. testNewClusterPricingReader(t, ctx, pm)
  87. })
  88. t.Run("NewNetworkPricingReader", func(t *testing.T) {
  89. testNewNetworkPricingReader(t, ctx, pm)
  90. })
  91. t.Run("NewServicePricingReader", func(t *testing.T) {
  92. testNewServicePricingReader(t, ctx, pm)
  93. })
  94. t.Run("ModulePersistence", func(t *testing.T) {
  95. // Create a new PricingModule with the same store
  96. pm2, err := NewBasicPricingModule(store)
  97. require.NoError(t, err)
  98. // Verify that pricing persists across all resource kinds.
  99. np, err := pm2.getNodePricing(ctx)
  100. require.NoError(t, err, "getting node pricing")
  101. require.NotNil(t, np, "expected node pricing to be persisted")
  102. cp, err := pm2.getClusterPricing(ctx)
  103. require.NoError(t, err, "getting cluster pricing")
  104. require.NotNil(t, cp, "expected cluster pricing to be persisted")
  105. netp, err := pm2.getNetworkPricing(ctx)
  106. require.NoError(t, err, "getting network pricing")
  107. require.NotNil(t, netp, "expected network pricing to be persisted")
  108. vp, err := pm2.getPersistentVolumePricing(ctx)
  109. require.NoError(t, err, "getting volume pricing")
  110. require.NotNil(t, vp, "expected volume pricing to be persisted")
  111. sp, err := pm2.getServicePricing(ctx)
  112. require.NoError(t, err, "getting service pricing")
  113. require.NotNil(t, sp, "expected service pricing to be persisted")
  114. })
  115. }
  116. }
  117. // testDefaultPricing verifies that a freshly created PricingModule contains default pricing
  118. func testDefaultPricing(t *testing.T, ctx context.Context, pm *PricingModule) {
  119. // Test default node pricing
  120. np, err := pm.getNodePricing(ctx)
  121. if err != nil {
  122. t.Fatalf("Failed to get node pricing: %v", err)
  123. }
  124. if np == nil {
  125. t.Fatal("Expected node pricing to exist")
  126. }
  127. // Prices are keyed by Resource. RAM and local disk share the GiB-hr unit,
  128. // so the Resource key is what distinguishes them.
  129. nodeChecks := []struct {
  130. resource pricing.Resource
  131. want float64
  132. }{
  133. {pricing.ResourceCPU, DefaultNodePricePerVCPUHour},
  134. {pricing.ResourceRAM, DefaultNodePricePerRAMGiBHour},
  135. {pricing.ResourceGPU, DefaultNodePricePerGPUHour},
  136. {pricing.ResourceStorage, DefaultNodePricePerLocalDiskGiBHour},
  137. }
  138. for _, c := range nodeChecks {
  139. price, ok := np.Prices[c.resource]
  140. if !ok {
  141. t.Errorf("Expected to find %s pricing", c.resource)
  142. continue
  143. }
  144. if price.Price != c.want {
  145. t.Errorf("Expected %s price to be %f, got %f", c.resource, c.want, price.Price)
  146. }
  147. }
  148. // Test default volume pricing
  149. vp, err := pm.getPersistentVolumePricing(ctx)
  150. if err != nil {
  151. t.Fatalf("Failed to get volume pricing: %v", err)
  152. }
  153. if vp == nil {
  154. t.Fatal("Expected volume pricing to exist")
  155. }
  156. volumePrice, ok := vp.Prices[pricing.ResourceStorage]
  157. if !ok {
  158. t.Fatal("Expected to find volume storage pricing")
  159. }
  160. if volumePrice.Price != DefaultPersistentVolumePricePerGiBHour {
  161. t.Errorf("Expected volume price to be %f, got %f", DefaultPersistentVolumePricePerGiBHour, volumePrice.Price)
  162. }
  163. // Test default cluster pricing
  164. cp, err := pm.getClusterPricing(ctx)
  165. require.NoError(t, err, "getting cluster pricing")
  166. require.NotNil(t, cp, "expected cluster pricing to exist")
  167. clusterPrice, ok := cp.Prices[pricing.ResourceCluster]
  168. require.True(t, ok, "expected to find cluster pricing")
  169. require.Equal(t, DefaultClusterPricePerHour, clusterPrice.Price)
  170. require.Equal(t, unit.Hour, clusterPrice.Unit)
  171. // Test default network pricing. RAM, egress types, etc. all share the GiB
  172. // unit, so the Resource key distinguishes them.
  173. netp, err := pm.getNetworkPricing(ctx)
  174. require.NoError(t, err, "getting network pricing")
  175. require.NotNil(t, netp, "expected network pricing to exist")
  176. networkChecks := []struct {
  177. resource pricing.Resource
  178. want float64
  179. }{
  180. {pricing.ResourceLocalEgress, DefaultNetworkLocalEgressPricePerGiB},
  181. {pricing.ResourceCrossZoneEgress, DefaultNetworkCrossZoneEgressPricePerGiB},
  182. {pricing.ResourceCrossRegionEgress, DefaultNetworkCrossRegionEgressPricePerGiB},
  183. {pricing.ResourceInternetEgress, DefaultNetworkInternetEgressPricePerGiB},
  184. {pricing.ResourceNATGatewayEgress, DefaultNetworkNATGatewayEgressPricePerGiB},
  185. {pricing.ResourceNATGatewayIngress, DefaultNetworkNATGatewayIngressPricePerGiB},
  186. }
  187. for _, c := range networkChecks {
  188. price, ok := netp.Prices[c.resource]
  189. require.Truef(t, ok, "expected to find %s pricing", c.resource)
  190. require.Equalf(t, c.want, price.Price, "price for %s", c.resource)
  191. require.Equalf(t, unit.GiB, price.Unit, "unit for %s", c.resource)
  192. }
  193. // Test default service pricing
  194. sp, err := pm.getServicePricing(ctx)
  195. require.NoError(t, err, "getting service pricing")
  196. require.NotNil(t, sp, "expected service pricing to exist")
  197. servicePrice, ok := sp.Prices[pricing.ResourceService]
  198. require.True(t, ok, "expected to find service pricing")
  199. require.Equal(t, DefaultServicePricePerHour, servicePrice.Price)
  200. require.Equal(t, unit.Hour, servicePrice.Unit)
  201. }
  202. // testSetNodePricePerCPUCoreHour tests the SetNodePricePerCPUCoreHour function
  203. func testSetNodePricePerCPUCoreHour(t *testing.T, ctx context.Context, pm *PricingModule) {
  204. newPrice := 0.075
  205. err := pm.SetNodePricePerCPUCoreHour(ctx, newPrice)
  206. if err != nil {
  207. t.Fatalf("Failed to set CPU price: %v", err)
  208. }
  209. // Verify the price was set
  210. np, err := pm.getNodePricing(ctx)
  211. if err != nil {
  212. t.Fatalf("Failed to get node pricing: %v", err)
  213. }
  214. price, ok := np.Prices[pricing.ResourceCPU]
  215. if !ok {
  216. t.Fatal("Expected to find CPU pricing")
  217. }
  218. if price.Price != newPrice {
  219. t.Errorf("Expected CPU price to be %f, got %f", newPrice, price.Price)
  220. }
  221. }
  222. // testSetNodePricePerRAMGiBHour tests the SetNodePricePerRAMGiBHour function
  223. func testSetNodePricePerRAMGiBHour(t *testing.T, ctx context.Context, pm *PricingModule) {
  224. newPrice := 0.008
  225. err := pm.SetNodePricePerRAMGiBHour(ctx, newPrice)
  226. if err != nil {
  227. t.Fatalf("Failed to set RAM price: %v", err)
  228. }
  229. // Verify the price was set
  230. np, err := pm.getNodePricing(ctx)
  231. if err != nil {
  232. t.Fatalf("Failed to get node pricing: %v", err)
  233. }
  234. price, ok := np.Prices[pricing.ResourceRAM]
  235. if !ok {
  236. t.Fatal("Expected to find RAM pricing")
  237. }
  238. if price.Price != newPrice {
  239. t.Errorf("Expected RAM price to be %f, got %f", newPrice, price.Price)
  240. }
  241. }
  242. // testSetNodePricePerGPUHour tests the SetNodePricePerGPUHour function
  243. func testSetNodePricePerGPUHour(t *testing.T, ctx context.Context, pm *PricingModule) {
  244. newPrice := 2.0
  245. err := pm.SetNodePricePerGPUHour(ctx, newPrice)
  246. if err != nil {
  247. t.Fatalf("Failed to set GPU price: %v", err)
  248. }
  249. // Verify the price was set
  250. np, err := pm.getNodePricing(ctx)
  251. if err != nil {
  252. t.Fatalf("Failed to get node pricing: %v", err)
  253. }
  254. price, ok := np.Prices[pricing.ResourceGPU]
  255. if !ok {
  256. t.Fatal("Expected to find GPU pricing")
  257. }
  258. if price.Price != newPrice {
  259. t.Errorf("Expected GPU price to be %f, got %f", newPrice, price.Price)
  260. }
  261. }
  262. // testSetNodePricePerLocalDiskGiBHour tests the SetNodePricePerLocalDiskGiBHour function
  263. func testSetNodePricePerLocalDiskGiBHour(t *testing.T, ctx context.Context, pm *PricingModule) {
  264. newPrice := 0.0007
  265. err := pm.SetNodePricePerLocalDiskGiBHour(ctx, newPrice)
  266. if err != nil {
  267. t.Fatalf("Failed to set local disk price: %v", err)
  268. }
  269. // Verify the price was set
  270. np, err := pm.getNodePricing(ctx)
  271. if err != nil {
  272. t.Fatalf("Failed to get node pricing: %v", err)
  273. }
  274. price, ok := np.Prices[pricing.ResourceStorage]
  275. if !ok {
  276. t.Fatal("Expected to find local disk pricing")
  277. }
  278. if price.Price != newPrice {
  279. t.Errorf("Expected local disk price to be %f, got %f", newPrice, price.Price)
  280. }
  281. }
  282. // testSetVolumePricePerStorageGiBHour tests the SetVolumePricePerStorageGiBHour function
  283. func testSetVolumePricePerStorageGiBHour(t *testing.T, ctx context.Context, pm *PricingModule) {
  284. newPrice := 0.0003
  285. err := pm.SetPersistentVolumePricePerStorageGiBHour(ctx, newPrice)
  286. if err != nil {
  287. t.Fatalf("Failed to set volume storage price: %v", err)
  288. }
  289. // Verify the price was set
  290. vp, err := pm.getPersistentVolumePricing(ctx)
  291. if err != nil {
  292. t.Fatalf("Failed to get volume pricing: %v", err)
  293. }
  294. price, ok := vp.Prices[pricing.ResourceStorage]
  295. if !ok {
  296. t.Fatal("Expected to find volume storage pricing")
  297. }
  298. if price.Price != newPrice {
  299. t.Errorf("Expected volume storage price to be %f, got %f", newPrice, price.Price)
  300. }
  301. }
  302. // testNewNodePricingReader tests the NewNodePricingReader function
  303. func testNewNodePricingReader(t *testing.T, ctx context.Context, pm *PricingModule) {
  304. // Test that NewNodePricingReader always produces a reader
  305. rdr, err := pm.NewNodePricingReader(ctx)
  306. if err != nil {
  307. t.Fatalf("Failed to create node pricing reader: %v", err)
  308. }
  309. if rdr == nil {
  310. t.Fatal("Expected reader to be non-nil")
  311. }
  312. // Test that the reader produces precisely one *NodePricing struct
  313. dst := make([]*pricing.NodePricing, 10) // Buffer larger than expected
  314. count := 0
  315. for {
  316. n, err := rdr.Read(ctx, dst)
  317. count += n
  318. // Verify all read items are non-nil
  319. for i := 0; i < n; i++ {
  320. if dst[i] == nil {
  321. t.Error("Expected non-nil NodePricing")
  322. }
  323. }
  324. if errors.Is(err, io.EOF) {
  325. break
  326. }
  327. if err != nil {
  328. t.Fatalf("Reader error: %v", err)
  329. }
  330. }
  331. if count != 1 {
  332. t.Errorf("Expected reader to produce exactly 1 NodePricing, got %d", count)
  333. }
  334. // Clean up
  335. if err := rdr.Close(); err != nil {
  336. t.Errorf("Failed to close reader: %v", err)
  337. }
  338. }
  339. // testNewVolumePricingReader tests the NewVolumePricingReader function
  340. func testNewVolumePricingReader(t *testing.T, ctx context.Context, pm *PricingModule) {
  341. // Test that NewVolumePricingReader always produces a reader
  342. rdr, err := pm.NewPersistentVolumePricingReader(ctx)
  343. if err != nil {
  344. t.Fatalf("Failed to create volume pricing reader: %v", err)
  345. }
  346. if rdr == nil {
  347. t.Fatal("Expected reader to be non-nil")
  348. }
  349. // Test that the reader produces precisely one *VolumePricing struct
  350. dst := make([]*pricing.PersistentVolumePricing, 10) // Buffer larger than expected
  351. count := 0
  352. for {
  353. n, err := rdr.Read(ctx, dst)
  354. count += n
  355. // Verify all read items are non-nil
  356. for i := 0; i < n; i++ {
  357. if dst[i] == nil {
  358. t.Error("Expected non-nil VolumePricing")
  359. }
  360. }
  361. if errors.Is(err, io.EOF) {
  362. break
  363. }
  364. if err != nil {
  365. t.Fatalf("Reader error: %v", err)
  366. }
  367. }
  368. if count != 1 {
  369. t.Errorf("Expected reader to produce exactly 1 VolumePricing, got %d", count)
  370. }
  371. // Clean up
  372. if err := rdr.Close(); err != nil {
  373. t.Errorf("Failed to close reader: %v", err)
  374. }
  375. }
  376. // testMetadata verifies the source identity accessors.
  377. func testMetadata(t *testing.T, pm *PricingModule) {
  378. require.Equal(t, SourceKind, pm.SourceKind())
  379. require.Equal(t, SourceName, pm.SourceName())
  380. }
  381. // testGetPricingSet verifies GetPricingSet returns a populated set.
  382. func testGetPricingSet(t *testing.T, ctx context.Context, pm *PricingModule) {
  383. ps, err := pm.GetPricingSet(ctx)
  384. require.NoError(t, err)
  385. require.NotNil(t, ps)
  386. require.False(t, ps.IsEmpty())
  387. }
  388. // testSetClusterPricePerHour tests the SetClusterPricePerHour function.
  389. func testSetClusterPricePerHour(t *testing.T, ctx context.Context, pm *PricingModule) {
  390. newPrice := 1.25
  391. err := pm.SetClusterPricePerHour(ctx, newPrice)
  392. require.NoError(t, err)
  393. cp, err := pm.getClusterPricing(ctx)
  394. require.NoError(t, err)
  395. price, ok := cp.Prices[pricing.ResourceCluster]
  396. require.True(t, ok, "expected to find cluster pricing")
  397. require.Equal(t, newPrice, price.Price)
  398. require.Equal(t, unit.Hour, price.Unit)
  399. }
  400. // testSetNetworkPrices tests each network setter. Each case verifies that only
  401. // the targeted Resource changes and that the other network prices are left
  402. // intact, guarding against a setter writing the wrong Resource key.
  403. func testSetNetworkPrices(t *testing.T, ctx context.Context, pm *PricingModule) {
  404. cases := []struct {
  405. name string
  406. resource pricing.Resource
  407. newPrice float64
  408. set func(context.Context, float64) error
  409. }{
  410. {"LocalEgress", pricing.ResourceLocalEgress, 0.002, pm.SetNetworkLocalEgressPricePerGiB},
  411. {"CrossZoneEgress", pricing.ResourceCrossZoneEgress, 0.012, pm.SetNetworkCrossZoneEgressPricePerGiB},
  412. {"CrossRegionEgress", pricing.ResourceCrossRegionEgress, 0.022, pm.SetNetworkCrossRegionEgressPricePerGiB},
  413. {"InternetEgress", pricing.ResourceInternetEgress, 0.111, pm.SetNetworkInternetEgressPricePerGiB},
  414. {"NATGatewayEgress", pricing.ResourceNATGatewayEgress, 0.055, pm.SetNetworkNATGatewayEgressPricePerGiB},
  415. {"NATGatewayIngress", pricing.ResourceNATGatewayIngress, 0.066, pm.SetNetworkNATGatewayIngressPricePerGiB},
  416. }
  417. for _, c := range cases {
  418. t.Run(c.name, func(t *testing.T) {
  419. // Snapshot existing prices so we can verify non-targeted
  420. // resources are untouched.
  421. before, err := pm.getNetworkPricing(ctx)
  422. require.NoError(t, err)
  423. prev := make(map[pricing.Resource]float64, len(before.Prices))
  424. for r, p := range before.Prices {
  425. prev[r] = p.Price
  426. }
  427. require.NoError(t, c.set(ctx, c.newPrice))
  428. after, err := pm.getNetworkPricing(ctx)
  429. require.NoError(t, err)
  430. price, ok := after.Prices[c.resource]
  431. require.Truef(t, ok, "expected to find %s pricing", c.resource)
  432. require.Equal(t, c.newPrice, price.Price)
  433. require.Equal(t, unit.GiB, price.Unit)
  434. // All other network resources must be unchanged.
  435. for r, want := range prev {
  436. if r == c.resource {
  437. continue
  438. }
  439. require.Equalf(t, want, after.Prices[r].Price, "unexpected change to %s", r)
  440. }
  441. })
  442. }
  443. }
  444. // testSetServicePricePerHour tests the SetServicePricePerHour function.
  445. func testSetServicePricePerHour(t *testing.T, ctx context.Context, pm *PricingModule) {
  446. newPrice := 0.05
  447. err := pm.SetServicePricePerHour(ctx, newPrice)
  448. require.NoError(t, err)
  449. sp, err := pm.getServicePricing(ctx)
  450. require.NoError(t, err)
  451. price, ok := sp.Prices[pricing.ResourceService]
  452. require.True(t, ok, "expected to find service pricing")
  453. require.Equal(t, newPrice, price.Price)
  454. require.Equal(t, unit.Hour, price.Unit)
  455. }
  456. // testChecksum verifies the checksum is non-empty, deterministic, and sensitive
  457. // to pricing mutations.
  458. func testChecksum(t *testing.T, ctx context.Context, pm *PricingModule) {
  459. sum1, err := pm.Checksum(ctx)
  460. require.NoError(t, err)
  461. require.NotEmpty(t, sum1)
  462. // Deterministic: recomputing without mutation yields the same checksum.
  463. sum2, err := pm.Checksum(ctx)
  464. require.NoError(t, err)
  465. require.Equal(t, sum1, sum2)
  466. // Sensitive: mutating pricing changes the checksum.
  467. require.NoError(t, pm.SetServicePricePerHour(ctx, sum1Sentinel))
  468. sum3, err := pm.Checksum(ctx)
  469. require.NoError(t, err)
  470. require.NotEqual(t, sum1, sum3, "expected checksum to change after a price mutation")
  471. }
  472. // sum1Sentinel is an arbitrary price unlikely to match the existing service
  473. // price, used to force a checksum change.
  474. const sum1Sentinel = 0.0419
  475. // testNewClusterPricingReader verifies the cluster pricing reader yields exactly
  476. // one non-nil element.
  477. func testNewClusterPricingReader(t *testing.T, ctx context.Context, pm *PricingModule) {
  478. rdr, err := pm.NewClusterPricingReader(ctx)
  479. require.NoError(t, err)
  480. require.NotNil(t, rdr)
  481. dst := make([]*pricing.ClusterPricing, 10)
  482. count := 0
  483. for {
  484. n, err := rdr.Read(ctx, dst)
  485. count += n
  486. for i := 0; i < n; i++ {
  487. require.NotNil(t, dst[i], "expected non-nil ClusterPricing")
  488. }
  489. if errors.Is(err, io.EOF) {
  490. break
  491. }
  492. require.NoError(t, err)
  493. }
  494. require.Equal(t, 1, count, "expected exactly 1 ClusterPricing")
  495. require.NoError(t, rdr.Close())
  496. }
  497. // testNewNetworkPricingReader verifies the network pricing reader yields exactly
  498. // one non-nil element.
  499. func testNewNetworkPricingReader(t *testing.T, ctx context.Context, pm *PricingModule) {
  500. rdr, err := pm.NewNetworkPricingReader(ctx)
  501. require.NoError(t, err)
  502. require.NotNil(t, rdr)
  503. dst := make([]*pricing.NetworkPricing, 10)
  504. count := 0
  505. for {
  506. n, err := rdr.Read(ctx, dst)
  507. count += n
  508. for i := 0; i < n; i++ {
  509. require.NotNil(t, dst[i], "expected non-nil NetworkPricing")
  510. }
  511. if errors.Is(err, io.EOF) {
  512. break
  513. }
  514. require.NoError(t, err)
  515. }
  516. require.Equal(t, 1, count, "expected exactly 1 NetworkPricing")
  517. require.NoError(t, rdr.Close())
  518. }
  519. // testNewServicePricingReader verifies the service pricing reader yields exactly
  520. // one non-nil element.
  521. func testNewServicePricingReader(t *testing.T, ctx context.Context, pm *PricingModule) {
  522. rdr, err := pm.NewServicePricingReader(ctx)
  523. require.NoError(t, err)
  524. require.NotNil(t, rdr)
  525. dst := make([]*pricing.ServicePricing, 10)
  526. count := 0
  527. for {
  528. n, err := rdr.Read(ctx, dst)
  529. count += n
  530. for i := 0; i < n; i++ {
  531. require.NotNil(t, dst[i], "expected non-nil ServicePricing")
  532. }
  533. if errors.Is(err, io.EOF) {
  534. break
  535. }
  536. require.NoError(t, err)
  537. }
  538. require.Equal(t, 1, count, "expected exactly 1 ServicePricing")
  539. require.NoError(t, rdr.Close())
  540. }
  541. func newFileStorage(t *testing.T) storage.Storage {
  542. tempDir, err := os.MkdirTemp("", "pricing-test-*")
  543. if err != nil {
  544. t.Fatalf("Failed to create temp directory: %v", err)
  545. }
  546. defer os.RemoveAll(tempDir)
  547. return storage.NewFileStorage(tempDir)
  548. }