Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions Darling/Darling.Tests/PlanViewerWarningColorAndFixTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Copyright (c) 2026 Erik Darling, Darling Data LLC
*
* This file is part of the SQL Server Performance Monitor.
*
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
*/

using System;
using System.IO;
using PerformanceMonitor.PlanAnalysis;
using Xunit;

namespace Darling.Tests;

/// <summary>
/// #4572: <see cref="PlanWarningDisplay.WarningSeverityColorHex"/> is the one place a warning's
/// severity becomes a colour, replacing three duplicated ternaries in
/// <c>PlanViewerControl.Properties.cs</c> and <c>PlanViewerControl.Tooltips.cs</c>. Also pins that
/// the properties panel now renders <see cref="PlanWarning.ActionableFix"/> under each warning
/// message. The colour-helper assertions are compile-only RED on dev (the member doesn't exist);
/// the source-census assertion is a runtime RED on dev (the old ternaries are still there).
/// </summary>
public sealed class PlanViewerWarningColorAndFixTests
{
[Fact]
public void WarningSeverityColorHex_Critical_IsRed() =>
Assert.Equal("#E57373", PlanWarningDisplay.WarningSeverityColorHex(PlanWarningSeverity.Critical));

[Fact]
public void WarningSeverityColorHex_Warning_IsAmber() =>
Assert.Equal("#FFB347", PlanWarningDisplay.WarningSeverityColorHex(PlanWarningSeverity.Warning));

[Fact]
public void WarningSeverityColorHex_Info_IsBlue() =>
Assert.Equal("#6BB5FF", PlanWarningDisplay.WarningSeverityColorHex(PlanWarningSeverity.Info));

[Fact]
public void PlanWarning_WithActionableFix_CarriesTheFixText()
{
var warning = new PlanWarning
{
WarningType = "Serial Plan",
Message = "This operator ran single-threaded.",
ActionableFix = "Consider raising cost threshold for parallelism."
};

Assert.Equal("Consider raising cost threshold for parallelism.", warning.ActionableFix);
}

[Fact]
public void PlanWarning_WithoutActionableFix_IsNull()
{
var warning = new PlanWarning { WarningType = "Local Variables", Message = "Uses a local variable." };

Assert.Null(warning.ActionableFix);
}

/// <summary>
/// No inline severity-colour hex literal is left in the WPF viewer control: every site resolves
/// through <see cref="PlanWarningDisplay.WarningSeverityColorHex"/> instead of repeating the
/// ternary. RED on dev, where the three sites (plan warnings, node warnings, tooltip warnings)
/// each still spell out "#E57373"/"#FFB347"/"#6BB5FF" directly.
/// </summary>
[Fact]
public void PlanViewerControl_HasNoInlineSeverityColorLiterals()
{
var repoRoot = FindRepoRoot();
var uiDir = Path.Combine(repoRoot, "PerformanceMonitor.Ui");
Assert.True(Directory.Exists(uiDir), $"Expected {uiDir} to exist.");

foreach (var file in Directory.EnumerateFiles(uiDir, "PlanViewerControl.*.cs"))
{
var text = File.ReadAllText(file);
Assert.DoesNotContain("#E57373", text, StringComparison.Ordinal);
Assert.DoesNotContain("#FFB347", text, StringComparison.Ordinal);
Assert.DoesNotContain("#6BB5FF", text, StringComparison.Ordinal);
}
}

private static string FindRepoRoot()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "PerformanceMonitor.sln")))
dir = dir.Parent;

Assert.NotNull(dir);
return dir!.FullName;
}
}
10 changes: 10 additions & 0 deletions PerformanceMonitor.PlanAnalysis/PlanWarningDisplay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ private static string WarningSourceTag(PlanWarning warning) =>
private static string FormatBenefitPercent(double pct) =>
pct >= 100 ? $"{pct:N0}" : $"{pct:N1}";

/// <summary>
/// #4572: the one place a warning's severity becomes a colour. Mirrors PerformanceStudio's
/// <c>WarningSeverityBrush</c> (one helper instead of the same ternary repeated at every
/// warning-rendering site), but keeps PM's existing literal hex values rather than PS's theme
/// tokens, so this change carries no visual difference.
/// </summary>
public static string WarningSeverityColorHex(PlanWarningSeverity severity) =>
severity == PlanWarningSeverity.Critical ? "#E57373"
: severity == PlanWarningSeverity.Warning ? "#FFB347" : "#6BB5FF";

/// <summary>
/// #4534: the text the viewer needs to turn a warning header into a link to the operator it came
/// from. Null when <paramref name="originNodeIds"/> is empty — a finding with no known operator
Expand Down
30 changes: 26 additions & 4 deletions PerformanceMonitor.Ui/PlanViewerControl.Properties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -813,8 +813,7 @@ private void ShowPropertiesPanel(PlanNode node)
AddPropertySection("Plan Warnings");
foreach (var w in PlanWarningDisplay.OrderByBenefit(s.PlanWarnings))
{
var warnColor = w.Severity == PlanWarningSeverity.Critical ? "#E57373"
: w.Severity == PlanWarningSeverity.Warning ? "#FFB347" : "#6BB5FF";
var warnColor = PlanWarningDisplay.WarningSeverityColorHex(w.Severity);
var warnPanel = new StackPanel { Margin = new Thickness(10, 2, 10, 2) };
var planWarnHeaderText = PlanWarningDisplay.PlanWarningHeader(w);
var planWarnHeaderBlock = new TextBlock
Expand All @@ -834,6 +833,18 @@ private void ShowPropertiesPanel(PlanNode node)
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(16, 0, 0, 0)
});
if (!string.IsNullOrEmpty(w.ActionableFix))
{
warnPanel.Children.Add(new TextBlock
{
Text = w.ActionableFix,
FontSize = 11,
FontStyle = FontStyles.Italic,
Foreground = TooltipFgBrush,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(16, 2, 0, 0)
});
}
(_currentPropertySection ?? PropertiesContent).Children.Add(warnPanel);
}
}
Expand All @@ -857,8 +868,7 @@ private void ShowPropertiesPanel(PlanNode node)
AddPropertySection("Warnings");
foreach (var w in PlanWarningDisplay.OrderByBenefit(node.Warnings))
{
var warnColor = w.Severity == PlanWarningSeverity.Critical ? "#E57373"
: w.Severity == PlanWarningSeverity.Warning ? "#FFB347" : "#6BB5FF";
var warnColor = PlanWarningDisplay.WarningSeverityColorHex(w.Severity);
var warnPanel = new StackPanel { Margin = new Thickness(10, 2, 10, 2) };
var opWarnHeaderText = PlanWarningDisplay.PlanWarningHeader(w);
var opWarnHeaderBlock = new TextBlock
Expand All @@ -878,6 +888,18 @@ private void ShowPropertiesPanel(PlanNode node)
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(16, 0, 0, 0)
});
if (!string.IsNullOrEmpty(w.ActionableFix))
{
warnPanel.Children.Add(new TextBlock
{
Text = w.ActionableFix,
FontSize = 11,
FontStyle = FontStyles.Italic,
Foreground = TooltipFgBrush,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(16, 2, 0, 0)
});
}
PropertiesContent.Children.Add(warnPanel);
}
}
Expand Down
6 changes: 2 additions & 4 deletions PerformanceMonitor.Ui/PlanViewerControl.Tooltips.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,7 @@ private ToolTip BuildNodeTooltip(PlanNode node, List<PlanWarning>? allWarnings =

foreach (var (type, severity, count) in distinct)
{
var warnColor = severity == PlanWarningSeverity.Critical ? "#E57373"
: severity == PlanWarningSeverity.Warning ? "#FFB347" : "#6BB5FF";
var warnColor = PlanWarningDisplay.WarningSeverityColorHex(severity);
var label = count > 1 ? $"\u26A0 {type} ({count})" : $"\u26A0 {type}";
stack.Children.Add(new TextBlock
{
Expand All @@ -198,8 +197,7 @@ private ToolTip BuildNodeTooltip(PlanNode node, List<PlanWarning>? allWarnings =
// Individual node: show full warning messages
foreach (var w in warnings)
{
var warnColor = w.Severity == PlanWarningSeverity.Critical ? "#E57373"
: w.Severity == PlanWarningSeverity.Warning ? "#FFB347" : "#6BB5FF";
var warnColor = PlanWarningDisplay.WarningSeverityColorHex(w.Severity);
stack.Children.Add(new TextBlock
{
Text = $"\u26A0 {w.WarningType}: {w.Message}",
Expand Down
Loading