mock_test.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. package pricing
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "testing"
  7. "github.com/opencost/opencost/core/pkg/model/shared"
  8. "github.com/opencost/opencost/core/pkg/reader"
  9. )
  10. func TestMockPricingModule(t *testing.T) {
  11. var source PricingSource
  12. pricingModule, err := NewMockPricingModule()
  13. if err != nil {
  14. t.Fatalf("unexpected error initializing mock repository: %s", err)
  15. }
  16. source = pricingModule
  17. // Simple example of a sink for pricing data (will be database tables in reality)
  18. bufferSize := 10
  19. ingestor := newMockIngestor(bufferSize)
  20. // Test ingestion of mock node reader
  21. nodePricingReader, err := source.NewNodePricingReader(t.Context())
  22. if err != nil {
  23. t.Errorf("unexpected error initializing node reader: %s", err)
  24. }
  25. n, err := ingestor.ingestNodePricing(context.Background(), nodePricingReader)
  26. if err != nil {
  27. t.Errorf("unexpected error ingesting node pricing: %s", err)
  28. }
  29. if n != 39 {
  30. t.Errorf("expected to ingest %d node pricing records; ingested %d", 39, n)
  31. }
  32. nodePricingCount := ingestor.countNodePricing()
  33. if nodePricingCount != 39 {
  34. t.Errorf("expected %d node pricing records; received %d", 39, nodePricingCount)
  35. }
  36. // Test ingestion of mock persistent volume reader
  37. volumePricingReader, err := source.NewPersistentVolumePricingReader(t.Context())
  38. if err != nil {
  39. t.Errorf("unexpected error initializing volume reader: %s", err)
  40. }
  41. n, err = ingestor.ingestPersistentVolumePricing(context.Background(), volumePricingReader)
  42. if err != nil {
  43. t.Errorf("unexpected error ingesting volume pricing: %s", err)
  44. }
  45. if n != 20 {
  46. t.Errorf("expected to ingest %d volume pricing records; ingested %d", 20, n)
  47. }
  48. volumePricingCount := ingestor.countVolumePricing()
  49. if volumePricingCount != 20 {
  50. t.Errorf("expected %d volume pricing records; received %d", 20, volumePricingCount)
  51. }
  52. }
  53. // TestMockGetNodePricing verifies node lookup by properties, that the matching
  54. // entry carries the prices loaded from YAML, and that a missing entry errors.
  55. func TestMockGetNodePricing(t *testing.T) {
  56. mpm := newMock(t)
  57. np, err := mpm.GetNodePricing(t.Context(), NodePricingProperties{
  58. Provider: shared.ProviderAWS,
  59. Region: "us-east-1",
  60. InstanceType: "m5.large",
  61. Provisioning: ProvisioningOnDemand,
  62. })
  63. if err != nil {
  64. t.Fatalf("unexpected error: %v", err)
  65. }
  66. // Guards against the YAML tag regression: prices must actually load.
  67. price, ok := np.Prices[ResourceNode]
  68. if !ok {
  69. t.Fatalf("expected node price to be present, prices=%v", np.Prices)
  70. }
  71. if price.Price != 0.096 {
  72. t.Errorf("expected on-demand price 0.096, got %v", price.Price)
  73. }
  74. // Missing entry should error rather than return a zero value.
  75. if _, err := mpm.GetNodePricing(t.Context(), NodePricingProperties{
  76. Provider: shared.ProviderAWS,
  77. Region: "eu-west-1",
  78. InstanceType: "m5.large",
  79. Provisioning: ProvisioningOnDemand,
  80. }); err == nil {
  81. t.Errorf("expected error for unknown region, got nil")
  82. }
  83. }
  84. // TestMockGetNodePricingProvisioningDiscriminates verifies that on-demand and
  85. // spot entries with otherwise identical properties are not conflated.
  86. func TestMockGetNodePricingProvisioningDiscriminates(t *testing.T) {
  87. mpm := newMock(t)
  88. base := NodePricingProperties{
  89. Provider: shared.ProviderAWS,
  90. Region: "us-east-1",
  91. InstanceType: "m5.large",
  92. }
  93. onDemand := base
  94. onDemand.Provisioning = ProvisioningOnDemand
  95. spot := base
  96. spot.Provisioning = ProvisioningSpot
  97. od, err := mpm.GetNodePricing(t.Context(), onDemand)
  98. if err != nil {
  99. t.Fatalf("unexpected error (on-demand): %v", err)
  100. }
  101. sp, err := mpm.GetNodePricing(t.Context(), spot)
  102. if err != nil {
  103. t.Fatalf("unexpected error (spot): %v", err)
  104. }
  105. if od.Prices[ResourceNode].Price == sp.Prices[ResourceNode].Price {
  106. t.Errorf("expected on-demand and spot to differ, both = %v", od.Prices[ResourceNode].Price)
  107. }
  108. if od.Prices[ResourceNode].Price != 0.096 {
  109. t.Errorf("expected on-demand 0.096, got %v", od.Prices[ResourceNode].Price)
  110. }
  111. if sp.Prices[ResourceNode].Price != 0.043 {
  112. t.Errorf("expected spot 0.043, got %v", sp.Prices[ResourceNode].Price)
  113. }
  114. }
  115. // TestMockGetPersistentVolumePricing verifies volume lookup, that prices load,
  116. // and that a missing entry errors.
  117. func TestMockGetPersistentVolumePricing(t *testing.T) {
  118. mpm := newMock(t)
  119. pv, err := mpm.GetPersistentVolumePricing(t.Context(), PersistentVolumePricingProperties{
  120. Provider: shared.ProviderAWS,
  121. Region: "us-east-1",
  122. VolumeType: VolumeTypeGP3,
  123. })
  124. if err != nil {
  125. t.Fatalf("unexpected error: %v", err)
  126. }
  127. if price, ok := pv.Prices[ResourceStorage]; !ok || price.Price != 0.0001096 {
  128. t.Errorf("expected gp3 storage price 0.0001096, got %v (ok=%t)", pv.Prices[ResourceStorage].Price, ok)
  129. }
  130. if _, err := mpm.GetPersistentVolumePricing(t.Context(), PersistentVolumePricingProperties{
  131. Provider: shared.ProviderAWS,
  132. Region: "us-east-1",
  133. VolumeType: VolumeTypeIO2,
  134. }); err == nil {
  135. t.Errorf("expected error for unknown volume type, got nil")
  136. }
  137. }
  138. // TestMockGetClusterPricing verifies cluster lookup by provider.
  139. func TestMockGetClusterPricing(t *testing.T) {
  140. mpm := newMock(t)
  141. cp, err := mpm.GetClusterPricing(t.Context(), ClusterPricingProperties{Provider: shared.ProviderAWS})
  142. if err != nil {
  143. t.Fatalf("unexpected error: %v", err)
  144. }
  145. if price, ok := cp.Prices[ResourceCluster]; !ok || price.Price != 0.10 {
  146. t.Errorf("expected cluster price 0.10, got %v (ok=%t)", cp.Prices[ResourceCluster].Price, ok)
  147. }
  148. if _, err := mpm.GetClusterPricing(t.Context(), ClusterPricingProperties{Provider: shared.ProviderOracle}); err == nil {
  149. t.Errorf("expected error for unknown provider, got nil")
  150. }
  151. }
  152. // TestMockGetNetworkPricing verifies network lookup by provider and that the
  153. // per-egress-type resource prices load from YAML.
  154. func TestMockGetNetworkPricing(t *testing.T) {
  155. mpm := newMock(t)
  156. np, err := mpm.GetNetworkPricing(t.Context(), NetworkPricingProperties{
  157. Provider: shared.ProviderAWS,
  158. })
  159. if err != nil {
  160. t.Fatalf("unexpected error: %v", err)
  161. }
  162. internet, ok := np.Prices[ResourceInternetEgress]
  163. if !ok || internet.Price != 0.09 {
  164. t.Errorf("expected internet egress price 0.09, got %v (ok=%t)", internet.Price, ok)
  165. }
  166. nat, ok := np.Prices[ResourceNATGatewayEgress]
  167. if !ok || nat.Price != 0.045 {
  168. t.Errorf("expected NAT gateway egress price 0.045, got %v (ok=%t)", nat.Price, ok)
  169. }
  170. if internet.Price == nat.Price {
  171. t.Errorf("expected internet egress and NAT gateway egress prices to differ")
  172. }
  173. // Missing provider should error rather than return a zero value.
  174. if _, err := mpm.GetNetworkPricing(t.Context(), NetworkPricingProperties{
  175. Provider: shared.ProviderOracle,
  176. }); err == nil {
  177. t.Errorf("expected error for unknown provider, got nil")
  178. }
  179. }
  180. // TestMockGetServicePricing verifies service lookup by provider and region.
  181. func TestMockGetServicePricing(t *testing.T) {
  182. mpm := newMock(t)
  183. sp, err := mpm.GetServicePricing(t.Context(), ServicePricingProperties{
  184. Provider: shared.ProviderAWS,
  185. Region: "us-east-1",
  186. })
  187. if err != nil {
  188. t.Fatalf("unexpected error: %v", err)
  189. }
  190. if price, ok := sp.Prices[ResourceService]; !ok || price.Price != 0.025 {
  191. t.Errorf("expected service price 0.025, got %v (ok=%t)", sp.Prices[ResourceService].Price, ok)
  192. }
  193. if _, err := mpm.GetServicePricing(t.Context(), ServicePricingProperties{
  194. Provider: shared.ProviderAWS,
  195. Region: "us-west-2",
  196. }); err == nil {
  197. t.Errorf("expected error for unknown region, got nil")
  198. }
  199. }
  200. // newMock is a helper that constructs a fresh MockPricingModule and fails the
  201. // test if construction errors.
  202. func newMock(t *testing.T) *MockPricingModule {
  203. t.Helper()
  204. mpm, err := NewMockPricingModule()
  205. if err != nil {
  206. t.Fatalf("unexpected error initializing mock pricing module: %v", err)
  207. }
  208. return mpm
  209. }
  210. type mockPricingIngestor struct {
  211. bufferSize int
  212. clusterPricing []*ClusterPricing
  213. networkPricing []*NetworkPricing
  214. nodePricing []*NodePricing
  215. persistentVolumePricing []*PersistentVolumePricing
  216. servicePricing []*ServicePricing
  217. }
  218. func newMockIngestor(bufferSize int) *mockPricingIngestor {
  219. if bufferSize == 0 {
  220. bufferSize = 100
  221. }
  222. return &mockPricingIngestor{
  223. bufferSize: bufferSize,
  224. clusterPricing: []*ClusterPricing{},
  225. networkPricing: []*NetworkPricing{},
  226. nodePricing: []*NodePricing{},
  227. persistentVolumePricing: []*PersistentVolumePricing{},
  228. servicePricing: []*ServicePricing{},
  229. }
  230. }
  231. func (ing *mockPricingIngestor) countNodePricing() int {
  232. return len(ing.nodePricing)
  233. }
  234. func (ing *mockPricingIngestor) ingestNodePricing(ctx context.Context, pricingReader reader.Reader[*NodePricing]) (int, error) {
  235. defer pricingReader.Close()
  236. nodeBuf := make([]*NodePricing, ing.bufferSize)
  237. totalCount := 0
  238. for {
  239. n, err := pricingReader.Read(ctx, nodeBuf)
  240. if n > 0 {
  241. ing.nodePricing = append(ing.nodePricing, nodeBuf[:n]...)
  242. }
  243. if errors.Is(err, reader.Done) {
  244. break
  245. }
  246. if err != nil {
  247. return totalCount, fmt.Errorf("unexpected error reading node pricing: %s", err)
  248. }
  249. totalCount += n
  250. }
  251. return totalCount, nil
  252. }
  253. func (ing *mockPricingIngestor) countVolumePricing() int {
  254. return len(ing.persistentVolumePricing)
  255. }
  256. func (ing *mockPricingIngestor) ingestPersistentVolumePricing(ctx context.Context, pricingReader reader.Reader[*PersistentVolumePricing]) (int, error) {
  257. defer pricingReader.Close()
  258. volBuf := make([]*PersistentVolumePricing, ing.bufferSize)
  259. totalCount := 0
  260. for {
  261. n, err := pricingReader.Read(ctx, volBuf)
  262. if n > 0 {
  263. ing.persistentVolumePricing = append(ing.persistentVolumePricing, volBuf[:n]...)
  264. }
  265. if errors.Is(err, reader.Done) {
  266. break
  267. }
  268. if err != nil {
  269. return totalCount, fmt.Errorf("unexpected error reading volume pricing: %s", err)
  270. }
  271. totalCount += n
  272. }
  273. return totalCount, nil
  274. }