service_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. package diagnostics
  2. import (
  3. "cmp"
  4. "context"
  5. "fmt"
  6. "slices"
  7. "testing"
  8. "time"
  9. "github.com/opencost/opencost/core/pkg/util/json"
  10. )
  11. const (
  12. TestDiagnosticNameA = "TestDiagnosticA"
  13. TestDiagnosticNameB = "TestDiagnosticB"
  14. TestDiagnosticNameC = "TestDiagnosticC"
  15. TestDiagnosticNameD = "TestDiagnosticD"
  16. TestDiagnosticNameE = "TestDiagnosticE"
  17. TestDiagnosticNameF = "TestDiagnosticF"
  18. TestDiagnosticNameTimeout = "TestDiagnosticTimeout"
  19. TestDiagnosticDescriptionA = "Diagnostic A Description..."
  20. TestDiagnosticDescriptionB = "Diagnostic B Description..."
  21. TestDiagnosticDescriptionC = "Diagnostic C Description..."
  22. TestDiagnosticDescriptionD = "Diagnostic D Description..."
  23. TestDiagnosticDescriptionE = "Diagnostic E Description..."
  24. TestDiagnosticDescriptionF = "Diagnostic F Description..."
  25. TestDiagnosticDescriptionTimeout = "Diagnostic Timeout will run for longer than 5 seconds..."
  26. TestDiagnosticCategoryBlue = "TestCategoryBlue"
  27. TestDiagnosticCategoryRed = "TestCategoryRed"
  28. TestDiagnosticCategoryGreen = "TestCategoryGreen"
  29. )
  30. // TestDiagnostic is a general structure used to capture test diagnostic data
  31. type TestDiagnostic struct {
  32. Name string
  33. Description string
  34. Category string
  35. Run DiagnosticRunner
  36. }
  37. // generate a runner func that will run for the provided duration and return a map with the key: "test"
  38. // and the value of testName provided.
  39. func runnerFor(testName string, duration time.Duration) DiagnosticRunner {
  40. return func(ctx context.Context) (map[string]any, error) {
  41. fmt.Printf("Running Diagnostic: %s\n", testName)
  42. defer fmt.Printf("Finished Diagnostic: %s\n", testName)
  43. select {
  44. case <-ctx.Done():
  45. fmt.Printf("context cancelled: %v\n", ctx.Err())
  46. return nil, ctx.Err()
  47. case <-time.After(duration):
  48. return map[string]any{
  49. "test": testName,
  50. }, nil
  51. }
  52. }
  53. }
  54. var (
  55. TestDiagnosticA = TestDiagnostic{
  56. Name: TestDiagnosticNameA,
  57. Description: TestDiagnosticDescriptionA,
  58. Category: TestDiagnosticCategoryRed,
  59. Run: runnerFor(TestDiagnosticNameA, 250*time.Millisecond),
  60. }
  61. TestDiagnosticB = TestDiagnostic{
  62. Name: TestDiagnosticNameB,
  63. Description: TestDiagnosticDescriptionB,
  64. Category: TestDiagnosticCategoryRed,
  65. Run: runnerFor(TestDiagnosticNameB, 150*time.Millisecond),
  66. }
  67. TestDiagnosticC = TestDiagnostic{
  68. Name: TestDiagnosticNameC,
  69. Description: TestDiagnosticDescriptionC,
  70. Category: TestDiagnosticCategoryBlue,
  71. Run: runnerFor(TestDiagnosticNameC, 350*time.Millisecond),
  72. }
  73. TestDiagnosticD = TestDiagnostic{
  74. Name: TestDiagnosticNameD,
  75. Description: TestDiagnosticDescriptionD,
  76. Category: TestDiagnosticCategoryBlue,
  77. Run: runnerFor(TestDiagnosticNameD, 450*time.Millisecond),
  78. }
  79. TestDiagnosticE = TestDiagnostic{
  80. Name: TestDiagnosticNameE,
  81. Description: TestDiagnosticDescriptionE,
  82. Category: TestDiagnosticCategoryGreen,
  83. Run: runnerFor(TestDiagnosticNameE, 550*time.Millisecond),
  84. }
  85. TestDiagnosticF = TestDiagnostic{
  86. Name: TestDiagnosticNameF,
  87. Description: TestDiagnosticDescriptionF,
  88. Category: TestDiagnosticCategoryGreen,
  89. Run: runnerFor(TestDiagnosticNameF, 650*time.Millisecond),
  90. }
  91. TestDiagnosticTimeout = TestDiagnostic{
  92. Name: TestDiagnosticNameTimeout,
  93. Description: TestDiagnosticDescriptionTimeout,
  94. Category: TestDiagnosticCategoryGreen,
  95. Run: runnerFor(TestDiagnosticNameTimeout, 6*time.Second),
  96. }
  97. )
  98. func TestDiagnosticsRegisterAndRun(t *testing.T) {
  99. t.Parallel()
  100. d := NewDiagnosticService()
  101. diags := []TestDiagnostic{
  102. TestDiagnosticA,
  103. TestDiagnosticB,
  104. TestDiagnosticC,
  105. TestDiagnosticD,
  106. TestDiagnosticE,
  107. TestDiagnosticF,
  108. }
  109. for _, diag := range diags {
  110. if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
  111. t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
  112. }
  113. }
  114. // Register a duplicate diagnostic and expect an error
  115. err := d.Register(TestDiagnosticA.Name, TestDiagnosticA.Description, TestDiagnosticA.Category, TestDiagnosticA.Run)
  116. if err == nil {
  117. t.Fatalf("expected error when registering duplicate diagnostic %s", TestDiagnosticA.Name)
  118. }
  119. c := context.Background()
  120. results := d.Run(c)
  121. if len(results) != len(diags) {
  122. t.Fatalf("expected %d results, got %d", len(diags), len(results))
  123. }
  124. for _, result := range results {
  125. if result.Error != "" {
  126. t.Errorf("expected no error, got %s", result.Error)
  127. }
  128. if result.Category == "" {
  129. t.Errorf("expected category, got empty")
  130. }
  131. if result.Name == "" {
  132. t.Errorf("expected name, got empty")
  133. }
  134. if result.Timestamp.IsZero() {
  135. t.Errorf("expected timestamp, got zero")
  136. }
  137. if result.Details == nil {
  138. t.Errorf("expected details, got nil")
  139. }
  140. if result.Details["test"] != result.Name {
  141. t.Errorf("expected test name %s, got %s", result.Name, result.Details["test"])
  142. }
  143. j, err := json.Marshal(result)
  144. if err != nil {
  145. t.Errorf("failed to marshal result: %v", err)
  146. }
  147. js := string(j)
  148. if js == "" {
  149. t.Errorf("expected non-empty JSON, got empty")
  150. }
  151. t.Logf("%s", js)
  152. }
  153. }
  154. func TestDiagnosticsServiceTimeout(t *testing.T) {
  155. t.Parallel()
  156. d := NewDiagnosticService()
  157. diags := []TestDiagnostic{
  158. TestDiagnosticA,
  159. TestDiagnosticB,
  160. TestDiagnosticC,
  161. TestDiagnosticTimeout,
  162. }
  163. for _, diag := range diags {
  164. if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
  165. t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
  166. }
  167. }
  168. c := context.Background()
  169. results := d.Run(c)
  170. if len(results) != len(diags) {
  171. t.Fatalf("expected %d results, got %d", len(diags), len(results))
  172. }
  173. foundTimeoutDiagnostic := false
  174. for _, result := range results {
  175. if result.Name == TestDiagnosticNameTimeout {
  176. foundTimeoutDiagnostic = true
  177. if result.Error == "" {
  178. t.Errorf("expected timeout error, but got empty error")
  179. } else {
  180. t.Logf("Diagnostic %s/%s completed with error as expected: %s", result.Category, result.Name, result.Error)
  181. }
  182. } else {
  183. t.Logf("Diagnostic %s/%s completed successfully", result.Category, result.Name)
  184. }
  185. }
  186. if !foundTimeoutDiagnostic {
  187. t.Errorf("expected to find timeout diagnostic, but it was not found")
  188. }
  189. }
  190. func TestDiagnosticsList(t *testing.T) {
  191. t.Parallel()
  192. d := NewDiagnosticService()
  193. diags := []TestDiagnostic{
  194. TestDiagnosticA,
  195. TestDiagnosticB,
  196. TestDiagnosticC,
  197. TestDiagnosticD,
  198. TestDiagnosticE,
  199. TestDiagnosticF,
  200. }
  201. for _, diag := range diags {
  202. if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
  203. t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
  204. }
  205. }
  206. diagList := d.Diagnostics()
  207. slices.SortFunc(diagList, func(a, b Diagnostic) int {
  208. return cmp.Compare(a.Category+"/"+a.Name, b.Category+"/"+b.Name)
  209. })
  210. slices.SortFunc(diags, func(a, b TestDiagnostic) int {
  211. return cmp.Compare(a.Category+"/"+a.Name, b.Category+"/"+b.Name)
  212. })
  213. if !slices.EqualFunc(diags, diagList, isEqual) {
  214. t.Errorf("expected diagnostics list to match registered diagnostics")
  215. }
  216. for _, diagItem := range diagList {
  217. t.Logf("Diagnostic: %+v", diagItem)
  218. }
  219. }
  220. func TestUnregisterDiagnostic(t *testing.T) {
  221. t.Parallel()
  222. d := NewDiagnosticService()
  223. diags := []TestDiagnostic{
  224. TestDiagnosticA,
  225. TestDiagnosticB,
  226. TestDiagnosticC,
  227. TestDiagnosticD,
  228. TestDiagnosticE,
  229. TestDiagnosticF,
  230. }
  231. for _, diag := range diags {
  232. if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
  233. t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
  234. }
  235. }
  236. if !d.Unregister(TestDiagnosticNameA, TestDiagnosticCategoryRed) {
  237. t.Errorf("failed to unregister diagnostic %s/%s", TestDiagnosticCategoryRed, TestDiagnosticNameA)
  238. }
  239. if d.Unregister(TestDiagnosticNameA, TestDiagnosticCategoryRed) {
  240. t.Errorf("unregistering diagnostic %s/%s again should fail", TestDiagnosticCategoryRed, TestDiagnosticNameA)
  241. }
  242. if d.Unregister(TestDiagnosticNameB, "nonexistent") {
  243. t.Errorf("unregistering nonexistent diagnostic should fail")
  244. }
  245. results := d.Run(context.Background())
  246. if len(results) != len(diags)-1 {
  247. t.Fatalf("expected %d results, got %d", len(diags)-1, len(results))
  248. }
  249. for _, result := range results {
  250. if result.Name == TestDiagnosticNameA {
  251. t.Errorf("expected diagnostic %s/%s to be unregistered", TestDiagnosticCategoryRed, TestDiagnosticNameA)
  252. }
  253. }
  254. }
  255. func TestUnregisterAllFromCategory(t *testing.T) {
  256. t.Parallel()
  257. d := NewDiagnosticService()
  258. diags := []TestDiagnostic{
  259. TestDiagnosticA,
  260. TestDiagnosticB,
  261. TestDiagnosticC,
  262. TestDiagnosticD,
  263. TestDiagnosticE,
  264. TestDiagnosticF,
  265. }
  266. for _, diag := range diags {
  267. if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
  268. t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
  269. }
  270. }
  271. if !d.Unregister(TestDiagnosticNameA, TestDiagnosticCategoryRed) {
  272. t.Errorf("failed to unregister diagnostic %s/%s", TestDiagnosticCategoryRed, TestDiagnosticNameA)
  273. }
  274. if !d.Unregister(TestDiagnosticNameB, TestDiagnosticCategoryRed) {
  275. t.Errorf("failed to unregister diagnostic %s/%s", TestDiagnosticCategoryRed, TestDiagnosticNameB)
  276. }
  277. results := d.RunCategory(context.Background(), TestDiagnosticCategoryRed)
  278. if len(results) != 0 {
  279. t.Fatalf("expected 0 results for category %s, got %d", TestDiagnosticCategoryRed, len(results))
  280. }
  281. }
  282. func TestRunCategoryDiagnostics(t *testing.T) {
  283. t.Parallel()
  284. d := NewDiagnosticService()
  285. diags := []TestDiagnostic{
  286. TestDiagnosticA,
  287. TestDiagnosticB,
  288. TestDiagnosticC,
  289. TestDiagnosticD,
  290. TestDiagnosticE,
  291. TestDiagnosticF,
  292. }
  293. for _, diag := range diags {
  294. if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
  295. t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
  296. }
  297. }
  298. c := context.Background()
  299. results := d.RunCategory(c, TestDiagnosticCategoryBlue)
  300. if len(results) != 2 {
  301. t.Fatalf("expected 2 results for category %s, got %d", TestDiagnosticCategoryBlue, len(results))
  302. }
  303. for _, result := range results {
  304. if result.Category != TestDiagnosticCategoryBlue {
  305. t.Errorf("expected category %s, got %s", TestDiagnosticCategoryBlue, result.Category)
  306. }
  307. }
  308. }
  309. func TestRunSingleDiagnostic(t *testing.T) {
  310. t.Parallel()
  311. d := NewDiagnosticService()
  312. diags := []TestDiagnostic{
  313. TestDiagnosticA,
  314. TestDiagnosticB,
  315. TestDiagnosticC,
  316. TestDiagnosticD,
  317. TestDiagnosticE,
  318. TestDiagnosticF,
  319. }
  320. for _, diag := range diags {
  321. if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
  322. t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
  323. }
  324. }
  325. c := context.Background()
  326. result := d.RunDiagnostic(c, TestDiagnosticCategoryGreen, TestDiagnosticNameF)
  327. if result == nil {
  328. t.Fatalf("expected a result for diagnostic %s, got nil", TestDiagnosticNameF)
  329. }
  330. if result.Name != TestDiagnosticNameF {
  331. t.Errorf("expected name %s, got %s", TestDiagnosticNameF, result.Name)
  332. }
  333. // Run category without name
  334. result = d.RunDiagnostic(c, TestDiagnosticCategoryGreen, "not-a-valid-diagnostic-name")
  335. if result != nil {
  336. t.Fatalf("expected nil result for invalid diagnostic name, got %v", result)
  337. }
  338. // Run without category
  339. result = d.RunDiagnostic(c, "not-a-valid-category", TestDiagnosticNameF)
  340. if result != nil {
  341. t.Fatalf("expected nil result for invalid category, got %v", result)
  342. }
  343. }
  344. func isEqual(a TestDiagnostic, b Diagnostic) bool {
  345. if a.Name != b.Name {
  346. return false
  347. }
  348. if a.Description != b.Description {
  349. return false
  350. }
  351. if a.Category != b.Category {
  352. return false
  353. }
  354. return true
  355. }