|
|
@@ -0,0 +1,430 @@
|
|
|
+package diagnostics
|
|
|
+
|
|
|
+import (
|
|
|
+ "cmp"
|
|
|
+ "context"
|
|
|
+ "fmt"
|
|
|
+ "slices"
|
|
|
+ "testing"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "github.com/opencost/opencost/core/pkg/util/json"
|
|
|
+)
|
|
|
+
|
|
|
+const (
|
|
|
+ TestDiagnosticNameA = "TestDiagnosticA"
|
|
|
+ TestDiagnosticNameB = "TestDiagnosticB"
|
|
|
+ TestDiagnosticNameC = "TestDiagnosticC"
|
|
|
+ TestDiagnosticNameD = "TestDiagnosticD"
|
|
|
+ TestDiagnosticNameE = "TestDiagnosticE"
|
|
|
+ TestDiagnosticNameF = "TestDiagnosticF"
|
|
|
+ TestDiagnosticNameTimeout = "TestDiagnosticTimeout"
|
|
|
+
|
|
|
+ TestDiagnosticDescriptionA = "Diagnostic A Description..."
|
|
|
+ TestDiagnosticDescriptionB = "Diagnostic B Description..."
|
|
|
+ TestDiagnosticDescriptionC = "Diagnostic C Description..."
|
|
|
+ TestDiagnosticDescriptionD = "Diagnostic D Description..."
|
|
|
+ TestDiagnosticDescriptionE = "Diagnostic E Description..."
|
|
|
+ TestDiagnosticDescriptionF = "Diagnostic F Description..."
|
|
|
+ TestDiagnosticDescriptionTimeout = "Diagnostic Timeout will run for longer than 5 seconds..."
|
|
|
+
|
|
|
+ TestDiagnosticCategoryBlue = "TestCategoryBlue"
|
|
|
+ TestDiagnosticCategoryRed = "TestCategoryRed"
|
|
|
+ TestDiagnosticCategoryGreen = "TestCategoryGreen"
|
|
|
+)
|
|
|
+
|
|
|
+// TestDiagnostic is a general structure used to capture test diagnostic data
|
|
|
+type TestDiagnostic struct {
|
|
|
+ Name string
|
|
|
+ Description string
|
|
|
+ Category string
|
|
|
+ Run DiagnosticRunner
|
|
|
+}
|
|
|
+
|
|
|
+// generate a runner func that will run for the provided duration and return a map with the key: "test"
|
|
|
+// and the value of testName provided.
|
|
|
+func runnerFor(testName string, duration time.Duration) DiagnosticRunner {
|
|
|
+ return func(ctx context.Context) (map[string]any, error) {
|
|
|
+ fmt.Printf("Running Diagnostic: %s\n", testName)
|
|
|
+ defer fmt.Printf("Finished Diagnostic: %s\n", testName)
|
|
|
+
|
|
|
+ select {
|
|
|
+ case <-ctx.Done():
|
|
|
+ fmt.Printf("context cancelled: %v\n", ctx.Err())
|
|
|
+ return nil, ctx.Err()
|
|
|
+ case <-time.After(duration):
|
|
|
+ return map[string]any{
|
|
|
+ "test": testName,
|
|
|
+ }, nil
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+var (
|
|
|
+ TestDiagnosticA = TestDiagnostic{
|
|
|
+ Name: TestDiagnosticNameA,
|
|
|
+ Description: TestDiagnosticDescriptionA,
|
|
|
+ Category: TestDiagnosticCategoryRed,
|
|
|
+ Run: runnerFor(TestDiagnosticNameA, 250*time.Millisecond),
|
|
|
+ }
|
|
|
+ TestDiagnosticB = TestDiagnostic{
|
|
|
+ Name: TestDiagnosticNameB,
|
|
|
+ Description: TestDiagnosticDescriptionB,
|
|
|
+ Category: TestDiagnosticCategoryRed,
|
|
|
+ Run: runnerFor(TestDiagnosticNameB, 150*time.Millisecond),
|
|
|
+ }
|
|
|
+ TestDiagnosticC = TestDiagnostic{
|
|
|
+ Name: TestDiagnosticNameC,
|
|
|
+ Description: TestDiagnosticDescriptionC,
|
|
|
+ Category: TestDiagnosticCategoryBlue,
|
|
|
+ Run: runnerFor(TestDiagnosticNameC, 350*time.Millisecond),
|
|
|
+ }
|
|
|
+ TestDiagnosticD = TestDiagnostic{
|
|
|
+ Name: TestDiagnosticNameD,
|
|
|
+ Description: TestDiagnosticDescriptionD,
|
|
|
+ Category: TestDiagnosticCategoryBlue,
|
|
|
+ Run: runnerFor(TestDiagnosticNameD, 450*time.Millisecond),
|
|
|
+ }
|
|
|
+ TestDiagnosticE = TestDiagnostic{
|
|
|
+ Name: TestDiagnosticNameE,
|
|
|
+ Description: TestDiagnosticDescriptionE,
|
|
|
+ Category: TestDiagnosticCategoryGreen,
|
|
|
+ Run: runnerFor(TestDiagnosticNameE, 550*time.Millisecond),
|
|
|
+ }
|
|
|
+ TestDiagnosticF = TestDiagnostic{
|
|
|
+ Name: TestDiagnosticNameF,
|
|
|
+ Description: TestDiagnosticDescriptionF,
|
|
|
+ Category: TestDiagnosticCategoryGreen,
|
|
|
+ Run: runnerFor(TestDiagnosticNameF, 650*time.Millisecond),
|
|
|
+ }
|
|
|
+ TestDiagnosticTimeout = TestDiagnostic{
|
|
|
+ Name: TestDiagnosticNameTimeout,
|
|
|
+ Description: TestDiagnosticDescriptionTimeout,
|
|
|
+ Category: TestDiagnosticCategoryGreen,
|
|
|
+ Run: runnerFor(TestDiagnosticNameTimeout, 6*time.Second),
|
|
|
+ }
|
|
|
+)
|
|
|
+
|
|
|
+func TestDiagnosticsRegisterAndRun(t *testing.T) {
|
|
|
+ t.Parallel()
|
|
|
+
|
|
|
+ d := NewDiagnosticService()
|
|
|
+
|
|
|
+ diags := []TestDiagnostic{
|
|
|
+ TestDiagnosticA,
|
|
|
+ TestDiagnosticB,
|
|
|
+ TestDiagnosticC,
|
|
|
+ TestDiagnosticD,
|
|
|
+ TestDiagnosticE,
|
|
|
+ TestDiagnosticF,
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, diag := range diags {
|
|
|
+ if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
|
|
|
+ t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Register a duplicate diagnostic and expect an error
|
|
|
+ err := d.Register(TestDiagnosticA.Name, TestDiagnosticA.Description, TestDiagnosticA.Category, TestDiagnosticA.Run)
|
|
|
+ if err == nil {
|
|
|
+ t.Fatalf("expected error when registering duplicate diagnostic %s", TestDiagnosticA.Name)
|
|
|
+ }
|
|
|
+
|
|
|
+ c := context.Background()
|
|
|
+ results := d.Run(c)
|
|
|
+
|
|
|
+ if len(results) != len(diags) {
|
|
|
+ t.Fatalf("expected %d results, got %d", len(diags), len(results))
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, result := range results {
|
|
|
+ if result.Error != "" {
|
|
|
+ t.Errorf("expected no error, got %s", result.Error)
|
|
|
+ }
|
|
|
+
|
|
|
+ if result.Category == "" {
|
|
|
+ t.Errorf("expected category, got empty")
|
|
|
+ }
|
|
|
+
|
|
|
+ if result.Name == "" {
|
|
|
+ t.Errorf("expected name, got empty")
|
|
|
+ }
|
|
|
+
|
|
|
+ if result.Timestamp.IsZero() {
|
|
|
+ t.Errorf("expected timestamp, got zero")
|
|
|
+ }
|
|
|
+
|
|
|
+ if result.Details == nil {
|
|
|
+ t.Errorf("expected details, got nil")
|
|
|
+ }
|
|
|
+
|
|
|
+ if result.Details["test"] != result.Name {
|
|
|
+ t.Errorf("expected test name %s, got %s", result.Name, result.Details["test"])
|
|
|
+ }
|
|
|
+
|
|
|
+ j, err := json.Marshal(result)
|
|
|
+ if err != nil {
|
|
|
+ t.Errorf("failed to marshal result: %v", err)
|
|
|
+ }
|
|
|
+ js := string(j)
|
|
|
+ if js == "" {
|
|
|
+ t.Errorf("expected non-empty JSON, got empty")
|
|
|
+ }
|
|
|
+
|
|
|
+ t.Logf("%s", js)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestDiagnosticsServiceTimeout(t *testing.T) {
|
|
|
+ t.Parallel()
|
|
|
+
|
|
|
+ d := NewDiagnosticService()
|
|
|
+
|
|
|
+ diags := []TestDiagnostic{
|
|
|
+ TestDiagnosticA,
|
|
|
+ TestDiagnosticB,
|
|
|
+ TestDiagnosticC,
|
|
|
+ TestDiagnosticTimeout,
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, diag := range diags {
|
|
|
+ if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
|
|
|
+ t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ c := context.Background()
|
|
|
+ results := d.Run(c)
|
|
|
+
|
|
|
+ if len(results) != len(diags) {
|
|
|
+ t.Fatalf("expected %d results, got %d", len(diags), len(results))
|
|
|
+ }
|
|
|
+
|
|
|
+ foundTimeoutDiagnostic := false
|
|
|
+
|
|
|
+ for _, result := range results {
|
|
|
+ if result.Name == TestDiagnosticNameTimeout {
|
|
|
+ foundTimeoutDiagnostic = true
|
|
|
+ if result.Error == "" {
|
|
|
+ t.Errorf("expected timeout error, but got empty error")
|
|
|
+ } else {
|
|
|
+ t.Logf("Diagnostic %s/%s completed with error as expected: %s", result.Category, result.Name, result.Error)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ t.Logf("Diagnostic %s/%s completed successfully", result.Category, result.Name)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if !foundTimeoutDiagnostic {
|
|
|
+ t.Errorf("expected to find timeout diagnostic, but it was not found")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestDiagnosticsList(t *testing.T) {
|
|
|
+ t.Parallel()
|
|
|
+
|
|
|
+ d := NewDiagnosticService()
|
|
|
+
|
|
|
+ diags := []TestDiagnostic{
|
|
|
+ TestDiagnosticA,
|
|
|
+ TestDiagnosticB,
|
|
|
+ TestDiagnosticC,
|
|
|
+ TestDiagnosticD,
|
|
|
+ TestDiagnosticE,
|
|
|
+ TestDiagnosticF,
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, diag := range diags {
|
|
|
+ if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
|
|
|
+ t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ diagList := d.Diagnostics()
|
|
|
+ slices.SortFunc(diagList, func(a, b Diagnostic) int {
|
|
|
+ return cmp.Compare(a.Category+"/"+a.Name, b.Category+"/"+b.Name)
|
|
|
+ })
|
|
|
+
|
|
|
+ slices.SortFunc(diags, func(a, b TestDiagnostic) int {
|
|
|
+ return cmp.Compare(a.Category+"/"+a.Name, b.Category+"/"+b.Name)
|
|
|
+ })
|
|
|
+
|
|
|
+ if !slices.EqualFunc(diags, diagList, isEqual) {
|
|
|
+ t.Errorf("expected diagnostics list to match registered diagnostics")
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, diagItem := range diagList {
|
|
|
+ t.Logf("Diagnostic: %+v", diagItem)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestUnregisterDiagnostic(t *testing.T) {
|
|
|
+ t.Parallel()
|
|
|
+
|
|
|
+ d := NewDiagnosticService()
|
|
|
+
|
|
|
+ diags := []TestDiagnostic{
|
|
|
+ TestDiagnosticA,
|
|
|
+ TestDiagnosticB,
|
|
|
+ TestDiagnosticC,
|
|
|
+ TestDiagnosticD,
|
|
|
+ TestDiagnosticE,
|
|
|
+ TestDiagnosticF,
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, diag := range diags {
|
|
|
+ if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
|
|
|
+ t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if !d.Unregister(TestDiagnosticNameA, TestDiagnosticCategoryRed) {
|
|
|
+ t.Errorf("failed to unregister diagnostic %s/%s", TestDiagnosticCategoryRed, TestDiagnosticNameA)
|
|
|
+ }
|
|
|
+
|
|
|
+ if d.Unregister(TestDiagnosticNameA, TestDiagnosticCategoryRed) {
|
|
|
+ t.Errorf("unregistering diagnostic %s/%s again should fail", TestDiagnosticCategoryRed, TestDiagnosticNameA)
|
|
|
+ }
|
|
|
+
|
|
|
+ if d.Unregister(TestDiagnosticNameB, "nonexistent") {
|
|
|
+ t.Errorf("unregistering nonexistent diagnostic should fail")
|
|
|
+ }
|
|
|
+
|
|
|
+ results := d.Run(context.Background())
|
|
|
+ if len(results) != len(diags)-1 {
|
|
|
+ t.Fatalf("expected %d results, got %d", len(diags)-1, len(results))
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, result := range results {
|
|
|
+ if result.Name == TestDiagnosticNameA {
|
|
|
+ t.Errorf("expected diagnostic %s/%s to be unregistered", TestDiagnosticCategoryRed, TestDiagnosticNameA)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestUnregisterAllFromCategory(t *testing.T) {
|
|
|
+ t.Parallel()
|
|
|
+
|
|
|
+ d := NewDiagnosticService()
|
|
|
+
|
|
|
+ diags := []TestDiagnostic{
|
|
|
+ TestDiagnosticA,
|
|
|
+ TestDiagnosticB,
|
|
|
+ TestDiagnosticC,
|
|
|
+ TestDiagnosticD,
|
|
|
+ TestDiagnosticE,
|
|
|
+ TestDiagnosticF,
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, diag := range diags {
|
|
|
+ if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
|
|
|
+ t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if !d.Unregister(TestDiagnosticNameA, TestDiagnosticCategoryRed) {
|
|
|
+ t.Errorf("failed to unregister diagnostic %s/%s", TestDiagnosticCategoryRed, TestDiagnosticNameA)
|
|
|
+ }
|
|
|
+
|
|
|
+ if !d.Unregister(TestDiagnosticNameB, TestDiagnosticCategoryRed) {
|
|
|
+ t.Errorf("failed to unregister diagnostic %s/%s", TestDiagnosticCategoryRed, TestDiagnosticNameB)
|
|
|
+ }
|
|
|
+
|
|
|
+ results := d.RunCategory(context.Background(), TestDiagnosticCategoryRed)
|
|
|
+ if len(results) != 0 {
|
|
|
+ t.Fatalf("expected 0 results for category %s, got %d", TestDiagnosticCategoryRed, len(results))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestRunCategoryDiagnostics(t *testing.T) {
|
|
|
+ t.Parallel()
|
|
|
+
|
|
|
+ d := NewDiagnosticService()
|
|
|
+
|
|
|
+ diags := []TestDiagnostic{
|
|
|
+ TestDiagnosticA,
|
|
|
+ TestDiagnosticB,
|
|
|
+ TestDiagnosticC,
|
|
|
+ TestDiagnosticD,
|
|
|
+ TestDiagnosticE,
|
|
|
+ TestDiagnosticF,
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, diag := range diags {
|
|
|
+ if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
|
|
|
+ t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ c := context.Background()
|
|
|
+ results := d.RunCategory(c, TestDiagnosticCategoryBlue)
|
|
|
+
|
|
|
+ if len(results) != 2 {
|
|
|
+ t.Fatalf("expected 2 results for category %s, got %d", TestDiagnosticCategoryBlue, len(results))
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, result := range results {
|
|
|
+ if result.Category != TestDiagnosticCategoryBlue {
|
|
|
+ t.Errorf("expected category %s, got %s", TestDiagnosticCategoryBlue, result.Category)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestRunSingleDiagnostic(t *testing.T) {
|
|
|
+ t.Parallel()
|
|
|
+
|
|
|
+ d := NewDiagnosticService()
|
|
|
+
|
|
|
+ diags := []TestDiagnostic{
|
|
|
+ TestDiagnosticA,
|
|
|
+ TestDiagnosticB,
|
|
|
+ TestDiagnosticC,
|
|
|
+ TestDiagnosticD,
|
|
|
+ TestDiagnosticE,
|
|
|
+ TestDiagnosticF,
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, diag := range diags {
|
|
|
+ if err := d.Register(diag.Name, diag.Description, diag.Category, diag.Run); err != nil {
|
|
|
+ t.Fatalf("failed to register diagnostic %s: %v", diag.Name, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ c := context.Background()
|
|
|
+ result := d.RunDiagnostic(c, TestDiagnosticCategoryGreen, TestDiagnosticNameF)
|
|
|
+
|
|
|
+ if result == nil {
|
|
|
+ t.Fatalf("expected a result for diagnostic %s, got nil", TestDiagnosticNameF)
|
|
|
+ }
|
|
|
+
|
|
|
+ if result.Name != TestDiagnosticNameF {
|
|
|
+ t.Errorf("expected name %s, got %s", TestDiagnosticNameF, result.Name)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Run category without name
|
|
|
+ result = d.RunDiagnostic(c, TestDiagnosticCategoryGreen, "not-a-valid-diagnostic-name")
|
|
|
+ if result != nil {
|
|
|
+ t.Fatalf("expected nil result for invalid diagnostic name, got %v", result)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Run without category
|
|
|
+ result = d.RunDiagnostic(c, "not-a-valid-category", TestDiagnosticNameF)
|
|
|
+ if result != nil {
|
|
|
+ t.Fatalf("expected nil result for invalid category, got %v", result)
|
|
|
+ }
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+func isEqual(a TestDiagnostic, b Diagnostic) bool {
|
|
|
+ if a.Name != b.Name {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if a.Description != b.Description {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ if a.Category != b.Category {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return true
|
|
|
+}
|