sqlinsight-pro/tests/SQLInsight.Rules.Tests/RuleEngineTests.cs
Søren Eggert Lundsteen Olsen 244153e9e4 Rules the version does not support are "not applicable", not "not measurable"
On SQL Server 2005 twenty rules stood as "not measurable" (Query Store,
system_health, memory columns …) between the real gaps, and counted in the
overview's "regler ikke målbare". They are not gaps: the version simply
lacks what they look at.

- New RuleOutcome.NotApplicable (last in the enum: outcomes are stored as
  numbers, so existing rows keep their meaning).
- A rule or collector missing only capabilities inherent to the instance
  (version-gated, plan cache memory columns, or Azure-managed) is not
  applicable, with the version each needs: "Ikke relevant for SQL Server
  2005: kræver QueryStore (fra SQL Server 2016)". Missing settings or rights
  (HADR not enabled …), collector failures and permission errors stay
  "not measurable". CollectedData.MarkNotApplicable carries this from
  collectors to the rules that use their data; community rules below their
  minimumVersion are not applicable too.
- Counts are unchanged in shape: the overview, KPI, JSON API and Prometheus
  count outcomes 2 and 3 only. The instance page lists the rest in a
  collapsed "Ikke relevant for SQL Server 2005 (n)" (details/summary, no
  JS) after the real gaps. Report controls whose core rule does not apply
  count neither as met nor as not measured.

Tests: engine (version vs. setting vs. collected data), inherent
capabilities, the existing version tests now expect not applicable, report
control status, and the web page and overview count.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 16:05:39 +02:00

154 lines
6 KiB
C#

using SQLInsight.Core.Findings;
using SQLInsight.Core.Instances;
using SQLInsight.Rules.Engine;
namespace SQLInsight.Rules.Tests;
public class RuleEngineTests
{
private sealed class StubRule(string id, IReadOnlyCollection<Capability> required, Func<RuleContext, RuleEvaluation> evaluate) : IRule
{
public int Calls { get; private set; }
public string Id => id;
public string Title => $"Stub {id}";
public IReadOnlyCollection<Capability> RequiredCapabilities => required;
public RuleEvaluation Evaluate(RuleContext context)
{
Calls++;
return evaluate(context);
}
}
[Fact]
public void Rule_needing_what_the_version_lacks_is_not_applicable_and_never_runs()
{
var rule = new StubRule("QS", [Capability.QueryStore], _ => RuleEvaluation.Passed());
var engine = new RuleEngine([rule]);
var result = Assert.Single(engine.Evaluate(TestInstances.Context("9.0.5057.0")));
Assert.Equal(RuleOutcome.NotApplicable, result.Outcome);
Assert.Equal([Capability.QueryStore], result.MissingCapabilities);
Assert.Equal("Ikke relevant for SQL Server 2005: kræver QueryStore (fra SQL Server 2016).", result.Reason);
Assert.Equal(0, rule.Calls);
}
[Fact]
public void Rule_needing_a_setting_the_instance_lacks_is_still_not_measurable()
{
// Always On not enabled is a setting, not the version: the rule could run, so it is a gap, not "not applicable".
var rule = new StubRule("AG", [Capability.HadrEnabled], _ => RuleEvaluation.Passed());
var result = Assert.Single(new RuleEngine([rule]).Evaluate(TestInstances.Context("16.0.4135.4")));
Assert.Equal(RuleOutcome.NotMeasurable, result.Outcome);
Assert.Contains("Ikke målbar", result.Reason, StringComparison.Ordinal);
}
[Fact]
public void Rule_needing_data_the_version_does_not_have_is_not_applicable()
{
var rule = new StubRule("D", [], ctx =>
{
ctx.Data.Get<SQLInsight.Core.Data.DeadlockSnapshot>();
return RuleEvaluation.Passed();
});
var context = TestInstances.Context("9.0.5057.0");
context.Data.MarkNotApplicable(typeof(SQLInsight.Core.Data.DeadlockSnapshot), "Ikke relevant for SQL Server 2005: kræver SystemHealthSession (fra SQL Server 2008).");
var result = Assert.Single(new RuleEngine([rule]).Evaluate(context));
Assert.Equal(RuleOutcome.NotApplicable, result.Outcome);
Assert.Contains("SystemHealthSession", result.Reason, StringComparison.Ordinal);
// Collected later (or failing for another reason): the flag does not stick.
context.Data.MarkUnavailable<SQLInsight.Core.Data.DeadlockSnapshot>("Collector 'deadlocks' fejlede: adgang nægtet.");
Assert.Equal(RuleOutcome.NotMeasurable, Assert.Single(new RuleEngine([rule]).Evaluate(context)).Outcome);
}
[Theory]
[InlineData(Capability.QueryStore, true)]
[InlineData(Capability.SystemHealthSession, true)]
[InlineData(Capability.TextQueryPlan, true)]
[InlineData(Capability.QueryStatsMemoryColumns, true)]
[InlineData(Capability.SelfManagedInfrastructure, true)]
[InlineData(Capability.HadrEnabled, false)]
public void Only_what_the_instance_is_makes_a_rule_not_applicable(Capability capability, bool inherent) =>
Assert.Equal(inherent, InstanceCapabilities.IsInherent(capability));
[Fact]
public void Rule_with_capabilities_runs()
{
var rule = new StubRule("QS", [Capability.QueryStore], _ => RuleEvaluation.Passed());
var engine = new RuleEngine([rule]);
var result = Assert.Single(engine.Evaluate(TestInstances.Context("16.0.4135.4")));
Assert.Equal(RuleOutcome.Passed, result.Outcome);
Assert.Equal(1, rule.Calls);
}
[Fact]
public void Throwing_rule_is_reported_as_error_and_does_not_stop_others()
{
var failing = new StubRule("A", [], _ => throw new InvalidOperationException("boom"));
var finding = new Finding
{
RuleId = "B",
Severity = Severity.Low,
Title = "t",
Explanation = "e",
Recommendation = "r",
};
var working = new StubRule("B", [], _ => RuleEvaluation.Found(finding));
var engine = new RuleEngine([failing, working]);
var results = engine.Evaluate(TestInstances.Context("16.0.4135.4"));
Assert.Equal(RuleOutcome.Error, results[0].Outcome);
Assert.Contains("boom", results[0].Reason, StringComparison.Ordinal);
Assert.Equal(RuleOutcome.Findings, results[1].Outcome);
Assert.Same(finding, Assert.Single(results[1].Findings));
}
[Fact]
public void Rule_needing_uncollected_data_is_not_measurable_with_the_collectors_reason()
{
var rule = new StubRule("W", [], ctx =>
{
ctx.Data.Get<SQLInsight.Core.Data.WaitStatsSnapshot>();
return RuleEvaluation.Passed();
});
var context = TestInstances.Context("16.0.4135.4");
context.Data.MarkUnavailable<SQLInsight.Core.Data.WaitStatsSnapshot>("Adgang nægtet til sys.dm_os_wait_stats.");
var result = Assert.Single(new RuleEngine([rule]).Evaluate(context));
Assert.Equal(RuleOutcome.NotMeasurable, result.Outcome);
Assert.Equal("Adgang nægtet til sys.dm_os_wait_stats.", result.Reason);
}
[Fact]
public void Duplicate_rule_ids_are_rejected()
{
var a = new StubRule("SAME", [], _ => RuleEvaluation.Passed());
var b = new StubRule("same", [], _ => RuleEvaluation.Passed());
Assert.Throws<ArgumentException>(() => new RuleEngine([a, b]));
}
[Fact]
public void Found_requires_at_least_one_finding()
{
Assert.Throws<ArgumentException>(() => RuleEvaluation.Found());
}
[Fact]
public void Built_in_rule_ids_are_unique()
{
var engine = new RuleEngine(BuiltInRules.Create());
Assert.NotEmpty(engine.Evaluate(TestInstances.Context("16.0.4135.4")));
}
}