diagnostics_test.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. package metric
  2. import (
  3. "fmt"
  4. "testing"
  5. "time"
  6. "github.com/kubecost/events"
  7. "github.com/opencost/opencost/modules/collector-source/pkg/event"
  8. )
  9. // MockUpdater implements the Updater interface for testing
  10. type MockUpdater struct {
  11. }
  12. func (m *MockUpdater) Update(updateSet *UpdateSet) {
  13. }
  14. // Test Update func in DiagnosticsModule and check if diagnostics pass
  15. func TestDiagnosticsModule_Update(t *testing.T) {
  16. mockUpdater := &MockUpdater{}
  17. module := NewDiagnosticsModule(mockUpdater)
  18. // Test with valid update set containing node metrics
  19. timestamp := time.Now()
  20. updateSet := &UpdateSet{
  21. Timestamp: timestamp,
  22. Updates: []Update{
  23. {
  24. Name: KubeNodeStatusCapacityCPUCores,
  25. Value: 4.0,
  26. },
  27. {
  28. Name: NodeTotalHourlyCost,
  29. Value: 0.50,
  30. },
  31. },
  32. }
  33. module.Update(updateSet)
  34. // Check both diagnostics
  35. nodeDetails, err := module.DiagnosticsDetails(NodesDiagnosticMetricID)
  36. if err != nil {
  37. t.Error("Expected no error for valid diagnostic ID")
  38. }
  39. if nodeDetails["passed"] != true {
  40. t.Error("Expected node diagnostic to pass")
  41. }
  42. opencostDetails, err := module.DiagnosticsDetails(OpencostDiagnosticMetricID)
  43. if err != nil {
  44. t.Error("Expected no error for valid diagnostic ID")
  45. }
  46. if opencostDetails["passed"] != true {
  47. t.Error("Expected kubecost diagnostic to pass")
  48. }
  49. }
  50. func TestDiagnosticsModule_ScrapeDiagnostics(t *testing.T) {
  51. mockUpdater := &MockUpdater{}
  52. module := NewDiagnosticsModule(mockUpdater)
  53. // dispatch some faux scrape events
  54. events.Dispatch(event.ScrapeEvent{
  55. ScraperName: event.NetworkCostsScraperName,
  56. Targets: 10,
  57. Errors: []error{},
  58. })
  59. events.Dispatch(event.ScrapeEvent{
  60. ScraperName: event.KubernetesClusterScraperName,
  61. ScrapeType: event.NodeScraperType,
  62. Targets: 8,
  63. Errors: []error{
  64. fmt.Errorf("failed to scrape node 'foo'"),
  65. fmt.Errorf("failed to scrape node 'bar'"),
  66. },
  67. })
  68. time.Sleep(500 * time.Millisecond)
  69. networkDiagnosticDetails, err := module.DiagnosticsDetails(NetworkCostsScraperDiagnosticID)
  70. if err != nil {
  71. t.Fatalf("unexpected error: %s", err)
  72. return
  73. }
  74. stats := networkDiagnosticDetails["stats"].(map[string]any)
  75. errors := networkDiagnosticDetails["errors"].([]string)
  76. label := networkDiagnosticDetails["label"].(string)
  77. statsTotal := stats["total"].(int)
  78. statsSuccess := stats["success"].(int)
  79. statsFail := stats["fail"].(int)
  80. if statsTotal != 10 {
  81. t.Fatalf("expected networkCostsDetails[\"stats\"][\"total\"] to equal 10, got: %d", statsTotal)
  82. return
  83. }
  84. if statsSuccess != 10 {
  85. t.Fatalf("expected networkCostsDetails[\"stats\"][\"success\"] to equal 10, got: %d", statsSuccess)
  86. return
  87. }
  88. if statsFail != 0 {
  89. t.Fatalf("expected networkCostsDetails[\"stats\"][\"fail\"] to equal 0, got: %d", statsFail)
  90. return
  91. }
  92. if len(errors) != 0 {
  93. t.Fatalf("expected len(networkCostsDetails[\"errors\"]) to equal 0, got: %d", len(errors))
  94. return
  95. }
  96. if len(label) == 0 {
  97. t.Fatalf("expected len(networkCostsDetails[\"label\"]) to be non-zero. Got 0.")
  98. return
  99. }
  100. nodeScrapeDetails, err := module.DiagnosticsDetails(KubernetesNodesScraperDiagnosticID)
  101. if err != nil {
  102. t.Fatalf("unexpected error: %s", err)
  103. return
  104. }
  105. stats = nodeScrapeDetails["stats"].(map[string]any)
  106. errors = nodeScrapeDetails["errors"].([]string)
  107. label = nodeScrapeDetails["label"].(string)
  108. statsTotal = stats["total"].(int)
  109. statsSuccess = stats["success"].(int)
  110. statsFail = stats["fail"].(int)
  111. if statsTotal != 8 {
  112. t.Fatalf("expected nodeScrapeDetails[\"stats\"][\"total\"] to equal 8, got: %d", statsTotal)
  113. return
  114. }
  115. if statsSuccess != 6 {
  116. t.Fatalf("expected nodeScrapeDetails[\"stats\"][\"success\"] to equal 6, got: %d", statsSuccess)
  117. return
  118. }
  119. if statsFail != 2 {
  120. t.Fatalf("expected nodeScrapeDetails[\"stats\"][\"fail\"] to equal 2, got: %d", statsFail)
  121. return
  122. }
  123. if len(errors) != 2 {
  124. t.Fatalf("expected len(nodeScrapeDetails[\"errors\"]) to equal 2, got: %d", len(errors))
  125. return
  126. }
  127. if len(label) == 0 {
  128. t.Fatalf("expected len(nodeScrapeDetails[\"label\"]) to be non-zero. Got 0.")
  129. return
  130. }
  131. }
  132. // Test Update func in DiagnosticsModule with missing metrics and test if diagnostics fail
  133. func TestDiagnosticsModule_UpdateWithMissingMetrics(t *testing.T) {
  134. mockUpdater := &MockUpdater{}
  135. module := NewDiagnosticsModule(mockUpdater)
  136. timestamp := time.Now()
  137. updateSet := &UpdateSet{
  138. Timestamp: timestamp,
  139. Updates: []Update{
  140. {
  141. Name: "some_other_metric",
  142. Value: 1.0,
  143. },
  144. },
  145. }
  146. module.Update(updateSet)
  147. // Check that diagnostics fail when their metrics are missing
  148. nodeDetails, err := module.DiagnosticsDetails(NodesDiagnosticMetricID)
  149. if err != nil {
  150. t.Error("Expected no error for valid diagnostic ID")
  151. }
  152. if nodeDetails["passed"] != false {
  153. t.Error("Expected node diagnostic to fail when metric is missing")
  154. }
  155. kubecostDetails, err := module.DiagnosticsDetails(OpencostDiagnosticMetricID)
  156. if err != nil {
  157. t.Error("Expected no error for valid diagnostic ID")
  158. }
  159. if kubecostDetails["passed"] != false {
  160. t.Error("Expected kubecost diagnostic to fail when metric is missing")
  161. }
  162. }
  163. // Test DiagnosticsDetails func in DiagnosticsModule with invalid and valid diagnostic IDs
  164. func TestDiagnosticsModule_DiagnosticsDetails(t *testing.T) {
  165. mockUpdater := &MockUpdater{}
  166. module := NewDiagnosticsModule(mockUpdater)
  167. // Test with invalid diagnostic ID
  168. _, err := module.DiagnosticsDetails("invalid_id")
  169. if err.Error() != "invalid diagnostic id: invalid_id not found" {
  170. t.Error("Expected error for invalid diagnostic ID")
  171. }
  172. // Test with valid diagnostic ID
  173. details, err := module.DiagnosticsDetails(NodesDiagnosticMetricID)
  174. if err != nil {
  175. t.Error("Expected no error for valid diagnostic ID")
  176. }
  177. if details["error"] != nil {
  178. t.Error("Expected no error for valid diagnostic ID")
  179. }
  180. // Check required fields
  181. requiredFields := []string{"query", "label", "result", "passed", "docLink"}
  182. for _, field := range requiredFields {
  183. if details[field] == nil {
  184. t.Errorf("Expected field %s to be present", field)
  185. }
  186. }
  187. }
  188. // Test concurrent access(race condition) to DiagnosticsModule
  189. func TestDiagnosticsModule_ConcurrentAccess(t *testing.T) {
  190. mockUpdater := &MockUpdater{}
  191. module := NewDiagnosticsModule(mockUpdater)
  192. // Test concurrent access to diagnostics
  193. done := make(chan bool, 2)
  194. go func() {
  195. for i := 0; i < 100; i++ {
  196. module.DiagnosticsDefinitions()
  197. }
  198. done <- true
  199. }()
  200. go func() {
  201. for i := 0; i < 100; i++ {
  202. timestamp := time.Now()
  203. updateSet := &UpdateSet{
  204. Timestamp: timestamp,
  205. Updates: []Update{
  206. {
  207. Name: KubeNodeStatusCapacityCPUCores,
  208. Value: float64(i),
  209. },
  210. },
  211. }
  212. module.Update(updateSet)
  213. }
  214. done <- true
  215. }()
  216. <-done
  217. <-done
  218. // If we get here without a race condition, the test passes
  219. }
  220. // Test reset of diagnostics after details are retrieved
  221. func TestDiagnosticsModule_ResetAfterDetails(t *testing.T) {
  222. mockUpdater := &MockUpdater{}
  223. module := NewDiagnosticsModule(mockUpdater)
  224. // Add some data
  225. timestamp := time.Now()
  226. updateSet := &UpdateSet{
  227. Timestamp: timestamp,
  228. Updates: []Update{
  229. {
  230. Name: KubeNodeStatusCapacityCPUCores,
  231. Value: 4.0,
  232. },
  233. },
  234. }
  235. module.Update(updateSet)
  236. // Get details (this should reset the diagnostic)
  237. details, err := module.DiagnosticsDetails(NodesDiagnosticMetricID)
  238. if err != nil {
  239. t.Error("Expected no error for valid diagnostic ID")
  240. }
  241. if details["passed"] != true {
  242. t.Error("Expected diagnostic to pass before reset")
  243. }
  244. // Get details again (should be reset)
  245. details2, err := module.DiagnosticsDetails(NodesDiagnosticMetricID)
  246. if err != nil {
  247. t.Error("Expected no error for valid diagnostic ID")
  248. }
  249. if details2["passed"] != false {
  250. t.Error("Expected diagnostic to be reset after first details call")
  251. }
  252. }