2
0

module_test.go 20 KB

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