2
0

networkcosts_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. package costmodel
  2. import (
  3. "io"
  4. "testing"
  5. "github.com/opencost/opencost/core/pkg/clustercache"
  6. "github.com/opencost/opencost/core/pkg/source"
  7. "github.com/opencost/opencost/core/pkg/util"
  8. "github.com/opencost/opencost/pkg/cloud/models"
  9. )
  10. // mockProvider is a mock implementation of the Provider interface for testing
  11. type mockProvider struct {
  12. network *models.Network
  13. err error
  14. }
  15. func (m *mockProvider) NetworkPricing() (*models.Network, error) {
  16. return m.network, m.err
  17. }
  18. func (m *mockProvider) GetKey(map[string]string, *clustercache.Node) models.Key {
  19. return nil
  20. }
  21. func (m *mockProvider) PVPricing(models.PVKey) (*models.PV, error) {
  22. return nil, nil
  23. }
  24. func (m *mockProvider) NodePricing(models.Key) (*models.Node, models.PricingMetadata, error) {
  25. return nil, models.PricingMetadata{}, nil
  26. }
  27. func (m *mockProvider) LoadBalancerPricing() (*models.LoadBalancer, error) {
  28. return nil, nil
  29. }
  30. func (m *mockProvider) AllNodePricing() (interface{}, error) {
  31. return nil, nil
  32. }
  33. func (m *mockProvider) ClusterInfo() (map[string]string, error) {
  34. return nil, nil
  35. }
  36. func (m *mockProvider) GetAddresses() ([]byte, error) {
  37. return nil, nil
  38. }
  39. func (m *mockProvider) GetDisks() ([]byte, error) {
  40. return nil, nil
  41. }
  42. func (m *mockProvider) GetOrphanedResources() ([]models.OrphanedResource, error) {
  43. return nil, nil
  44. }
  45. func (m *mockProvider) GpuPricing(map[string]string) (string, error) {
  46. return "", nil
  47. }
  48. func (m *mockProvider) DownloadPricingData() error {
  49. return nil
  50. }
  51. func (m *mockProvider) GetPVKey(*clustercache.PersistentVolume, map[string]string, string) models.PVKey {
  52. return nil
  53. }
  54. func (m *mockProvider) UpdateConfig(io.Reader, string) (*models.CustomPricing, error) {
  55. return nil, nil
  56. }
  57. func (m *mockProvider) UpdateConfigFromConfigMap(map[string]string) (*models.CustomPricing, error) {
  58. return nil, nil
  59. }
  60. func (m *mockProvider) GetConfig() (*models.CustomPricing, error) {
  61. return nil, nil
  62. }
  63. func (m *mockProvider) GetManagementPlatform() (string, error) {
  64. return "", nil
  65. }
  66. func (m *mockProvider) ApplyReservedInstancePricing(map[string]*models.Node) {
  67. }
  68. func (m *mockProvider) ServiceAccountStatus() *models.ServiceAccountStatus {
  69. return nil
  70. }
  71. func (m *mockProvider) PricingSourceStatus() map[string]*models.PricingSource {
  72. return nil
  73. }
  74. func (m *mockProvider) ClusterManagementPricing() (string, float64, error) {
  75. return "", 0.0, nil
  76. }
  77. func (m *mockProvider) CombinedDiscountForNode(string, bool, float64, float64) float64 {
  78. return 0.0
  79. }
  80. func (m *mockProvider) Regions() []string {
  81. return nil
  82. }
  83. func (m *mockProvider) PricingSourceSummary() interface{} {
  84. return nil
  85. }
  86. // TestGetNetworkUsageData tests the aggregation of NAT Gateway egress and ingress data
  87. func TestGetNetworkUsageData(t *testing.T) {
  88. defaultClusterID := "default-cluster"
  89. testCases := []struct {
  90. name string
  91. zoneResults []*source.NetZoneGiBResult
  92. regionResults []*source.NetRegionGiBResult
  93. internetResults []*source.NetInternetGiBResult
  94. natGatewayEgressResults []*source.NetNatGatewayGiBResult
  95. natGatewayIngressResults []*source.NetNatGatewayIngressGiBResult
  96. expectedKeys []string
  97. validateFunc func(t *testing.T, result map[string]*NetworkUsageData)
  98. }{
  99. {
  100. name: "NAT Gateway egress only",
  101. zoneResults: nil,
  102. regionResults: nil,
  103. internetResults: nil,
  104. natGatewayEgressResults: []*source.NetNatGatewayGiBResult{
  105. {
  106. Pod: "pod1",
  107. Namespace: "ns1",
  108. Cluster: "cluster1",
  109. Data: []*util.Vector{
  110. {Value: 10.5, Timestamp: 1000},
  111. {Value: 20.3, Timestamp: 2000},
  112. },
  113. },
  114. },
  115. natGatewayIngressResults: nil,
  116. expectedKeys: []string{"ns1,pod1,cluster1"},
  117. validateFunc: func(t *testing.T, result map[string]*NetworkUsageData) {
  118. key := "ns1,pod1,cluster1"
  119. if data, ok := result[key]; ok {
  120. if len(data.NetworkNatGatewayEgress) != 2 {
  121. t.Errorf("expected 2 NAT Gateway egress vectors, got %d", len(data.NetworkNatGatewayEgress))
  122. }
  123. if data.NetworkNatGatewayEgress[0].Value != 10.5 {
  124. t.Errorf("expected first egress value 10.5, got %f", data.NetworkNatGatewayEgress[0].Value)
  125. }
  126. if len(data.NetworkNatGatewayIngress) != 0 {
  127. t.Errorf("expected 0 NAT Gateway ingress vectors, got %d", len(data.NetworkNatGatewayIngress))
  128. }
  129. } else {
  130. t.Errorf("expected key %s not found in result", key)
  131. }
  132. },
  133. },
  134. {
  135. name: "NAT Gateway ingress only",
  136. zoneResults: nil,
  137. regionResults: nil,
  138. internetResults: nil,
  139. natGatewayEgressResults: nil,
  140. natGatewayIngressResults: []*source.NetNatGatewayIngressGiBResult{
  141. {
  142. Pod: "pod2",
  143. Namespace: "ns2",
  144. Cluster: "cluster2",
  145. Data: []*util.Vector{
  146. {Value: 5.2, Timestamp: 1000},
  147. },
  148. },
  149. },
  150. expectedKeys: []string{"ns2,pod2,cluster2"},
  151. validateFunc: func(t *testing.T, result map[string]*NetworkUsageData) {
  152. key := "ns2,pod2,cluster2"
  153. if data, ok := result[key]; ok {
  154. if len(data.NetworkNatGatewayIngress) != 1 {
  155. t.Errorf("expected 1 NAT Gateway ingress vector, got %d", len(data.NetworkNatGatewayIngress))
  156. }
  157. if data.NetworkNatGatewayIngress[0].Value != 5.2 {
  158. t.Errorf("expected ingress value 5.2, got %f", data.NetworkNatGatewayIngress[0].Value)
  159. }
  160. if len(data.NetworkNatGatewayEgress) != 0 {
  161. t.Errorf("expected 0 NAT Gateway egress vectors, got %d", len(data.NetworkNatGatewayEgress))
  162. }
  163. } else {
  164. t.Errorf("expected key %s not found in result", key)
  165. }
  166. },
  167. },
  168. {
  169. name: "NAT Gateway egress and ingress for same pod",
  170. zoneResults: nil,
  171. regionResults: nil,
  172. internetResults: nil,
  173. natGatewayEgressResults: []*source.NetNatGatewayGiBResult{
  174. {
  175. Pod: "pod3",
  176. Namespace: "ns3",
  177. Cluster: "cluster3",
  178. Data: []*util.Vector{
  179. {Value: 15.0, Timestamp: 1000},
  180. },
  181. },
  182. },
  183. natGatewayIngressResults: []*source.NetNatGatewayIngressGiBResult{
  184. {
  185. Pod: "pod3",
  186. Namespace: "ns3",
  187. Cluster: "cluster3",
  188. Data: []*util.Vector{
  189. {Value: 8.5, Timestamp: 1000},
  190. },
  191. },
  192. },
  193. expectedKeys: []string{"ns3,pod3,cluster3"},
  194. validateFunc: func(t *testing.T, result map[string]*NetworkUsageData) {
  195. key := "ns3,pod3,cluster3"
  196. if data, ok := result[key]; ok {
  197. if len(data.NetworkNatGatewayEgress) != 1 {
  198. t.Errorf("expected 1 NAT Gateway egress vector, got %d", len(data.NetworkNatGatewayEgress))
  199. }
  200. if data.NetworkNatGatewayEgress[0].Value != 15.0 {
  201. t.Errorf("expected egress value 15.0, got %f", data.NetworkNatGatewayEgress[0].Value)
  202. }
  203. if len(data.NetworkNatGatewayIngress) != 1 {
  204. t.Errorf("expected 1 NAT Gateway ingress vector, got %d", len(data.NetworkNatGatewayIngress))
  205. }
  206. if data.NetworkNatGatewayIngress[0].Value != 8.5 {
  207. t.Errorf("expected ingress value 8.5, got %f", data.NetworkNatGatewayIngress[0].Value)
  208. }
  209. } else {
  210. t.Errorf("expected key %s not found in result", key)
  211. }
  212. },
  213. },
  214. {
  215. name: "Mixed network traffic with NAT Gateway",
  216. zoneResults: []*source.NetZoneGiBResult{
  217. {
  218. Pod: "pod4",
  219. Namespace: "ns4",
  220. Cluster: "cluster4",
  221. Data: []*util.Vector{
  222. {Value: 3.0, Timestamp: 1000},
  223. },
  224. },
  225. },
  226. regionResults: []*source.NetRegionGiBResult{
  227. {
  228. Pod: "pod4",
  229. Namespace: "ns4",
  230. Cluster: "cluster4",
  231. Data: []*util.Vector{
  232. {Value: 7.0, Timestamp: 1000},
  233. },
  234. },
  235. },
  236. internetResults: []*source.NetInternetGiBResult{
  237. {
  238. Pod: "pod4",
  239. Namespace: "ns4",
  240. Cluster: "cluster4",
  241. Data: []*util.Vector{
  242. {Value: 12.0, Timestamp: 1000},
  243. },
  244. },
  245. },
  246. natGatewayEgressResults: []*source.NetNatGatewayGiBResult{
  247. {
  248. Pod: "pod4",
  249. Namespace: "ns4",
  250. Cluster: "cluster4",
  251. Data: []*util.Vector{
  252. {Value: 18.0, Timestamp: 1000},
  253. },
  254. },
  255. },
  256. natGatewayIngressResults: []*source.NetNatGatewayIngressGiBResult{
  257. {
  258. Pod: "pod4",
  259. Namespace: "ns4",
  260. Cluster: "cluster4",
  261. Data: []*util.Vector{
  262. {Value: 9.0, Timestamp: 1000},
  263. },
  264. },
  265. },
  266. expectedKeys: []string{"ns4,pod4,cluster4"},
  267. validateFunc: func(t *testing.T, result map[string]*NetworkUsageData) {
  268. key := "ns4,pod4,cluster4"
  269. if data, ok := result[key]; ok {
  270. // Verify all network types are present
  271. if len(data.NetworkZoneEgress) != 1 {
  272. t.Errorf("expected 1 zone egress vector, got %d", len(data.NetworkZoneEgress))
  273. }
  274. if len(data.NetworkRegionEgress) != 1 {
  275. t.Errorf("expected 1 region egress vector, got %d", len(data.NetworkRegionEgress))
  276. }
  277. if len(data.NetworkInternetEgress) != 1 {
  278. t.Errorf("expected 1 internet egress vector, got %d", len(data.NetworkInternetEgress))
  279. }
  280. if len(data.NetworkNatGatewayEgress) != 1 {
  281. t.Errorf("expected 1 NAT Gateway egress vector, got %d", len(data.NetworkNatGatewayEgress))
  282. }
  283. if len(data.NetworkNatGatewayIngress) != 1 {
  284. t.Errorf("expected 1 NAT Gateway ingress vector, got %d", len(data.NetworkNatGatewayIngress))
  285. }
  286. // Verify values
  287. if data.NetworkNatGatewayEgress[0].Value != 18.0 {
  288. t.Errorf("expected NAT Gateway egress 18.0, got %f", data.NetworkNatGatewayEgress[0].Value)
  289. }
  290. if data.NetworkNatGatewayIngress[0].Value != 9.0 {
  291. t.Errorf("expected NAT Gateway ingress 9.0, got %f", data.NetworkNatGatewayIngress[0].Value)
  292. }
  293. } else {
  294. t.Errorf("expected key %s not found in result", key)
  295. }
  296. },
  297. },
  298. {
  299. name: "Default cluster ID fallback for NAT Gateway",
  300. zoneResults: nil,
  301. regionResults: nil,
  302. internetResults: nil,
  303. natGatewayEgressResults: []*source.NetNatGatewayGiBResult{
  304. {
  305. Pod: "pod5",
  306. Namespace: "ns5",
  307. Cluster: "", // Empty cluster ID should use default
  308. Data: []*util.Vector{
  309. {Value: 5.0, Timestamp: 1000},
  310. },
  311. },
  312. },
  313. natGatewayIngressResults: nil,
  314. expectedKeys: []string{"ns5,pod5,default-cluster"},
  315. validateFunc: func(t *testing.T, result map[string]*NetworkUsageData) {
  316. key := "ns5,pod5,default-cluster"
  317. if data, ok := result[key]; ok {
  318. if data.ClusterID != "default-cluster" {
  319. t.Errorf("expected cluster ID 'default-cluster', got %s", data.ClusterID)
  320. }
  321. } else {
  322. t.Errorf("expected key %s not found in result", key)
  323. }
  324. },
  325. },
  326. }
  327. for _, tc := range testCases {
  328. t.Run(tc.name, func(t *testing.T) {
  329. result, err := GetNetworkUsageData(
  330. tc.zoneResults,
  331. tc.regionResults,
  332. tc.internetResults,
  333. tc.natGatewayEgressResults,
  334. tc.natGatewayIngressResults,
  335. defaultClusterID,
  336. )
  337. if err != nil {
  338. t.Fatalf("unexpected error: %v", err)
  339. }
  340. if len(result) != len(tc.expectedKeys) {
  341. t.Errorf("expected %d keys, got %d", len(tc.expectedKeys), len(result))
  342. }
  343. for _, key := range tc.expectedKeys {
  344. if _, ok := result[key]; !ok {
  345. t.Errorf("expected key %s not found in result", key)
  346. }
  347. }
  348. if tc.validateFunc != nil {
  349. tc.validateFunc(t, result)
  350. }
  351. })
  352. }
  353. }
  354. // TestGetNetworkCost tests the calculation of NAT Gateway costs
  355. func TestGetNetworkCost(t *testing.T) {
  356. testCases := []struct {
  357. name string
  358. usage *NetworkUsageData
  359. pricing *models.Network
  360. expectedCost float64
  361. expectedLength int
  362. }{
  363. {
  364. name: "NAT Gateway egress cost only",
  365. usage: &NetworkUsageData{
  366. ClusterID: "cluster1",
  367. PodName: "pod1",
  368. Namespace: "ns1",
  369. NetworkNatGatewayEgress: []*util.Vector{
  370. {Value: 10.0, Timestamp: 1000}, // 10 GiB
  371. },
  372. },
  373. pricing: &models.Network{
  374. NatGatewayEgressCost: 0.05, // $0.05 per GiB
  375. },
  376. expectedCost: 0.50, // 10 * 0.05 = 0.50
  377. expectedLength: 1,
  378. },
  379. {
  380. name: "NAT Gateway ingress cost only",
  381. usage: &NetworkUsageData{
  382. ClusterID: "cluster1",
  383. PodName: "pod1",
  384. Namespace: "ns1",
  385. NetworkNatGatewayIngress: []*util.Vector{
  386. {Value: 20.0, Timestamp: 1000}, // 20 GiB
  387. },
  388. },
  389. pricing: &models.Network{
  390. NatGatewayIngressCost: 0.02, // $0.02 per GiB
  391. },
  392. expectedCost: 0.40, // 20 * 0.02 = 0.40
  393. expectedLength: 1,
  394. },
  395. {
  396. name: "NAT Gateway egress and ingress costs",
  397. usage: &NetworkUsageData{
  398. ClusterID: "cluster1",
  399. PodName: "pod1",
  400. Namespace: "ns1",
  401. NetworkNatGatewayEgress: []*util.Vector{
  402. {Value: 10.0, Timestamp: 1000},
  403. },
  404. NetworkNatGatewayIngress: []*util.Vector{
  405. {Value: 5.0, Timestamp: 1000},
  406. },
  407. },
  408. pricing: &models.Network{
  409. NatGatewayEgressCost: 0.05, // $0.05 per GiB
  410. NatGatewayIngressCost: 0.02, // $0.02 per GiB
  411. },
  412. expectedCost: 0.60, // (10 * 0.05) + (5 * 0.02) = 0.50 + 0.10 = 0.60
  413. expectedLength: 1,
  414. },
  415. {
  416. name: "Mixed network costs with NAT Gateway",
  417. usage: &NetworkUsageData{
  418. ClusterID: "cluster1",
  419. PodName: "pod1",
  420. Namespace: "ns1",
  421. NetworkZoneEgress: []*util.Vector{
  422. {Value: 5.0, Timestamp: 1000},
  423. },
  424. NetworkRegionEgress: []*util.Vector{
  425. {Value: 8.0, Timestamp: 1000},
  426. },
  427. NetworkInternetEgress: []*util.Vector{
  428. {Value: 12.0, Timestamp: 1000},
  429. },
  430. NetworkNatGatewayEgress: []*util.Vector{
  431. {Value: 15.0, Timestamp: 1000},
  432. },
  433. NetworkNatGatewayIngress: []*util.Vector{
  434. {Value: 10.0, Timestamp: 1000},
  435. },
  436. },
  437. pricing: &models.Network{
  438. ZoneNetworkEgressCost: 0.01,
  439. RegionNetworkEgressCost: 0.02,
  440. InternetNetworkEgressCost: 0.09,
  441. NatGatewayEgressCost: 0.05,
  442. NatGatewayIngressCost: 0.02,
  443. },
  444. expectedCost: 2.24, // (5*0.01) + (8*0.02) + (12*0.09) + (15*0.05) + (10*0.02) = 0.05 + 0.16 + 1.08 + 0.75 + 0.20 = 2.24
  445. expectedLength: 1,
  446. },
  447. {
  448. name: "Multiple time points with NAT Gateway",
  449. usage: &NetworkUsageData{
  450. ClusterID: "cluster1",
  451. PodName: "pod1",
  452. Namespace: "ns1",
  453. NetworkNatGatewayEgress: []*util.Vector{
  454. {Value: 10.0, Timestamp: 1000},
  455. {Value: 15.0, Timestamp: 2000},
  456. {Value: 20.0, Timestamp: 3000},
  457. },
  458. NetworkNatGatewayIngress: []*util.Vector{
  459. {Value: 5.0, Timestamp: 1000},
  460. {Value: 8.0, Timestamp: 2000},
  461. {Value: 12.0, Timestamp: 3000},
  462. },
  463. },
  464. pricing: &models.Network{
  465. NatGatewayEgressCost: 0.05,
  466. NatGatewayIngressCost: 0.02,
  467. },
  468. expectedLength: 3,
  469. },
  470. {
  471. name: "Zero NAT Gateway costs",
  472. usage: &NetworkUsageData{
  473. ClusterID: "cluster1",
  474. PodName: "pod1",
  475. Namespace: "ns1",
  476. NetworkNatGatewayEgress: []*util.Vector{
  477. {Value: 100.0, Timestamp: 1000},
  478. },
  479. NetworkNatGatewayIngress: []*util.Vector{
  480. {Value: 50.0, Timestamp: 1000},
  481. },
  482. },
  483. pricing: &models.Network{
  484. NatGatewayEgressCost: 0.0,
  485. NatGatewayIngressCost: 0.0,
  486. },
  487. expectedCost: 0.0,
  488. expectedLength: 1,
  489. },
  490. }
  491. for _, tc := range testCases {
  492. t.Run(tc.name, func(t *testing.T) {
  493. provider := &mockProvider{
  494. network: tc.pricing,
  495. }
  496. result, err := GetNetworkCost(tc.usage, provider)
  497. if err != nil {
  498. t.Fatalf("unexpected error: %v", err)
  499. }
  500. if len(result) != tc.expectedLength {
  501. t.Errorf("expected %d result vectors, got %d", tc.expectedLength, len(result))
  502. }
  503. if tc.expectedLength > 0 {
  504. totalCost := 0.0
  505. for _, v := range result {
  506. totalCost += v.Value
  507. }
  508. if tc.expectedCost > 0 {
  509. if diff := totalCost - tc.expectedCost; diff > 0.001 || diff < -0.001 {
  510. t.Errorf("expected total cost %f, got %f", tc.expectedCost, totalCost)
  511. }
  512. }
  513. }
  514. })
  515. }
  516. }
  517. // TestGetNetworkCost_NATGatewayMisalignedVectors tests NAT Gateway cost calculation with different vector lengths
  518. func TestGetNetworkCost_NATGatewayMisalignedVectors(t *testing.T) {
  519. usage := &NetworkUsageData{
  520. ClusterID: "cluster1",
  521. PodName: "pod1",
  522. Namespace: "ns1",
  523. NetworkZoneEgress: []*util.Vector{
  524. {Value: 5.0, Timestamp: 1000},
  525. {Value: 6.0, Timestamp: 2000},
  526. },
  527. NetworkNatGatewayEgress: []*util.Vector{
  528. {Value: 10.0, Timestamp: 1000},
  529. {Value: 15.0, Timestamp: 2000},
  530. {Value: 20.0, Timestamp: 3000}, // Extra NAT Gateway data point
  531. },
  532. NetworkNatGatewayIngress: []*util.Vector{
  533. {Value: 5.0, Timestamp: 1000}, // Only one ingress data point
  534. },
  535. }
  536. pricing := &models.Network{
  537. ZoneNetworkEgressCost: 0.01,
  538. NatGatewayEgressCost: 0.05,
  539. NatGatewayIngressCost: 0.02,
  540. }
  541. provider := &mockProvider{
  542. network: pricing,
  543. }
  544. result, err := GetNetworkCost(usage, provider)
  545. if err != nil {
  546. t.Fatalf("unexpected error: %v", err)
  547. }
  548. // Should have 3 result vectors (max of all vector lengths including NAT Gateway)
  549. if len(result) != 3 {
  550. t.Errorf("expected 3 result vectors (max of all vectors including NAT Gateway), got %d", len(result))
  551. }
  552. // First vector: zone (5*0.01) + natEgress (10*0.05) + natIngress (5*0.02) = 0.05 + 0.50 + 0.10 = 0.65
  553. expectedFirst := (5.0 * 0.01) + (10.0 * 0.05) + (5.0 * 0.02)
  554. if diff := result[0].Value - expectedFirst; diff > 0.001 || diff < -0.001 {
  555. t.Errorf("expected first vector cost %f, got %f", expectedFirst, result[0].Value)
  556. }
  557. // Second vector: zone (6*0.01) + natEgress (15*0.05) = 0.06 + 0.75 = 0.81
  558. // (no NAT ingress for second time point)
  559. expectedSecond := (6.0 * 0.01) + (15.0 * 0.05)
  560. if diff := result[1].Value - expectedSecond; diff > 0.001 || diff < -0.001 {
  561. t.Errorf("expected second vector cost %f, got %f", expectedSecond, result[1].Value)
  562. }
  563. // Third vector: only natEgress (20*0.05) = 1.00
  564. // (no zone, region, internet, or NAT ingress for third time point)
  565. expectedThird := 20.0 * 0.05
  566. if diff := result[2].Value - expectedThird; diff > 0.001 || diff < -0.001 {
  567. t.Errorf("expected third vector cost %f, got %f", expectedThird, result[2].Value)
  568. }
  569. }