package analyzer import ( "testing" ) func TestAnalyze(t *testing.T) { defaultThresholds := Thresholds{ DataSkewRatio: 3.0, GCPressureRatio: 0.1, BottleneckShuffleGB: 50.0, } t.Run("data_skew", func(t *testing.T) { t.Run("normal", func(t *testing.T) { in := Input{ StageMetrics: map[string]StageMetric{ "stage 0": {MaxPartitionBytes: 100, MinPartitionBytes: 50}, }, } if got := Analyze(in, defaultThresholds); len(got) != 0 { t.Fatalf("expected no findings, got %+v", got) } }) t.Run("critical", func(t *testing.T) { in := Input{ StageMetrics: map[string]StageMetric{ "stage 1": {MaxPartitionBytes: 1000, MinPartitionBytes: 10}, }, } got := Analyze(in, defaultThresholds) if len(got) != 1 { t.Fatalf("expected 1 finding, got %+v", got) } if got[0].Rule != "data_skew" || got[0].Severity != SeverityCritical || got[0].Stage != "stage 1" { t.Fatalf("unexpected finding: %+v", got[0]) } }) t.Run("zero_min_partition_bytes", func(t *testing.T) { in := Input{ StageMetrics: map[string]StageMetric{ "stage 2": {MaxPartitionBytes: 1000, MinPartitionBytes: 0}, }, } if got := Analyze(in, defaultThresholds); len(got) != 0 { t.Fatalf("expected no findings for zero min, got %+v", got) } }) }) t.Run("gc_pressure", func(t *testing.T) { t.Run("normal", func(t *testing.T) { in := Input{ ExecutorMetrics: map[string]ExecutorMetric{ "1": {GCTimeMS: 100, CPUTimeMS: 10000}, }, } if got := Analyze(in, defaultThresholds); len(got) != 0 { t.Fatalf("expected no findings, got %+v", got) } }) t.Run("warning", func(t *testing.T) { in := Input{ ExecutorMetrics: map[string]ExecutorMetric{ "1": {GCTimeMS: 2000, CPUTimeMS: 10000}, }, } got := Analyze(in, defaultThresholds) if len(got) != 1 { t.Fatalf("expected 1 finding, got %+v", got) } if got[0].Rule != "gc_pressure" || got[0].Severity != SeverityWarning { t.Fatalf("unexpected finding: %+v", got[0]) } }) t.Run("critical", func(t *testing.T) { in := Input{ ExecutorMetrics: map[string]ExecutorMetric{ "1": {GCTimeMS: 5000, CPUTimeMS: 10000}, }, } got := Analyze(in, defaultThresholds) if len(got) != 1 { t.Fatalf("expected 1 finding, got %+v", got) } if got[0].Rule != "gc_pressure" || got[0].Severity != SeverityCritical { t.Fatalf("unexpected finding: %+v", got[0]) } }) t.Run("zero_cpu_time", func(t *testing.T) { in := Input{ ExecutorMetrics: map[string]ExecutorMetric{ "1": {GCTimeMS: 5000, CPUTimeMS: 0}, }, } if got := Analyze(in, defaultThresholds); len(got) != 0 { t.Fatalf("expected no findings for zero cpu, got %+v", got) } }) }) t.Run("bottleneck", func(t *testing.T) { t.Run("normal", func(t *testing.T) { in := Input{ StageMetrics: map[string]StageMetric{ "stage 0": {ShuffleReadBytes: 1 << 30, ShuffleWriteBytes: 0}, }, } if got := Analyze(in, defaultThresholds); len(got) != 0 { t.Fatalf("expected no findings, got %+v", got) } }) t.Run("warning", func(t *testing.T) { in := Input{ StageMetrics: map[string]StageMetric{ "stage 1": {ShuffleReadBytes: 120 << 30, ShuffleWriteBytes: 0}, }, } got := Analyze(in, defaultThresholds) if len(got) != 1 { t.Fatalf("expected 1 finding, got %+v", got) } if got[0].Rule != "bottleneck" || got[0].Severity != SeverityWarning { t.Fatalf("unexpected finding: %+v", got[0]) } }) t.Run("critical", func(t *testing.T) { in := Input{ StageMetrics: map[string]StageMetric{ "stage 2": {ShuffleReadBytes: 300 << 30, ShuffleWriteBytes: 0}, }, } got := Analyze(in, defaultThresholds) if len(got) != 1 { t.Fatalf("expected 1 finding, got %+v", got) } if got[0].Rule != "bottleneck" || got[0].Severity != SeverityCritical { t.Fatalf("unexpected finding: %+v", got[0]) } }) }) }