MissingCapabilities { get; init; } = [];
/// Why the rule was not measurable or failed.
diff --git a/src/SQLInsight.Web/Components/Pages/Instance.razor b/src/SQLInsight.Web/Components/Pages/Instance.razor
index 9151969..2f83a42 100644
--- a/src/SQLInsight.Web/Components/Pages/Instance.razor
+++ b/src/SQLInsight.Web/Components/Pages/Instance.razor
@@ -159,7 +159,7 @@ else
Ikke målbart på denne instans
@if (_notMeasurable.Count == 0)
{
- Alle regler kunne evalueres.
+ Alle regler, der er relevante for instansen, kunne evalueres.
}
else
{
@@ -174,6 +174,22 @@ else
}
+@if (_notApplicable.Count > 0)
+{
+
+ Ikke relevant for @Display.Release(_instance.ProductVersion, _instance.EngineEdition) (@_notApplicable.Count)
+ Regler om noget, denne SQL Server-version ikke har (fx Query Store eller system_health). Det er ikke et hul i overvågningen.
+
+ | Regel | | Hvorfor |
+
+ @foreach (var r in _notApplicable)
+ {
+ | @r.RuleId | @r.RuleTitle | @r.Reason |
+ }
+
+
+
+}
@@ -203,6 +219,7 @@ else
private IReadOnlyList _findings = [];
private IReadOnlyList _resolved = [];
private IReadOnlyList _notMeasurable = [];
+ private IReadOnlyList _notApplicable = [];
private IReadOnlyList _cpuSql = [], _cpuOther = [], _batches = [], _ple = [];
private IReadOnlyList _waits = [];
private IReadOnlyList _files = [];
@@ -221,7 +238,9 @@ else
var day = DateTimeOffset.UtcNow.AddDays(-1);
_findings = await Repository.GetActiveFindingsAsync(id);
_resolved = await Repository.GetRecentlyResolvedAsync(id, 30);
- _notMeasurable = (await Repository.GetRuleStatesAsync(id)).Where(r => r.Outcome is RuleOutcome.NotMeasurable or RuleOutcome.Error).ToList();
+ var states = await Repository.GetRuleStatesAsync(id);
+ _notMeasurable = states.Where(r => r.Outcome is RuleOutcome.NotMeasurable or RuleOutcome.Error).ToList();
+ _notApplicable = states.Where(r => r.Outcome == RuleOutcome.NotApplicable).ToList();
_cpuSql = await Repository.GetMetricAsync(id, Metrics.CpuSqlServer, day);
_cpuOther = await Repository.GetMetricAsync(id, Metrics.CpuOther, day);
_batches = await Repository.GetMetricAsync(id, Metrics.BatchRequestsPerSecond, day);
diff --git a/src/SQLInsight.Web/wwwroot/app.css b/src/SQLInsight.Web/wwwroot/app.css
index 787ba35..7dc9ab0 100644
--- a/src/SQLInsight.Web/wwwroot/app.css
+++ b/src/SQLInsight.Web/wwwroot/app.css
@@ -188,3 +188,8 @@ a.button:hover { filter: brightness(1.08); }
.panel.unusual { border-left: 4px solid var(--critical); }
tr.unusual td { background: var(--critical-soft); }
pre.sql { white-space: pre-wrap; word-break: break-word; max-height: 9rem; overflow: auto; margin: .25rem 0 0; font-size: .8rem; }
+
+/* Rules that do not apply to the instance's version: collapsed, out of the way of real gaps */
+details.not-applicable { margin-top: 1rem; }
+details.not-applicable summary { cursor: pointer; color: var(--text-2); font-weight: 600; }
+details.not-applicable > table { margin-top: .75rem; }
diff --git a/tests/SQLInsight.Agent.Tests/ComplianceControlTests.cs b/tests/SQLInsight.Agent.Tests/ComplianceControlTests.cs
new file mode 100644
index 0000000..028b39a
--- /dev/null
+++ b/tests/SQLInsight.Agent.Tests/ComplianceControlTests.cs
@@ -0,0 +1,28 @@
+using SQLInsight.Reports;
+using SQLInsight.Repository;
+using SQLInsight.Rules.Engine;
+
+namespace SQLInsight.Agent.Tests;
+
+public class ComplianceControlTests
+{
+ private static readonly ComplianceControl Integrity = ComplianceControl.All.Single(c => c.Name == "Integritetstjek");
+
+ [Fact]
+ public void Control_whose_core_rule_does_not_apply_to_the_version_is_not_counted_as_not_measured()
+ {
+ var states = new[]
+ {
+ new InstanceRuleState("KGAA-S05", "SQLI-DB-009", RuleOutcome.NotApplicable, "Ikke relevant for SQL Server 2005"),
+ new InstanceRuleState("KGAA-S05", "SQLI-DB-010", RuleOutcome.Passed, null),
+ new InstanceRuleState("SQL01", "SQLI-DB-009", RuleOutcome.NotMeasurable, "Adgang nægtet"),
+ new InstanceRuleState("SQL02", "SQLI-DB-009", RuleOutcome.Passed, null),
+ new InstanceRuleState("SQL03", "SQLI-DB-010", RuleOutcome.Findings, null),
+ };
+
+ Assert.Equal(ControlStatus.NotApplicable, Integrity.StatusOn("KGAA-S05", states));
+ Assert.Equal(ControlStatus.NotMeasured, Integrity.StatusOn("SQL01", states)); // a real gap stays a gap
+ Assert.Equal(ControlStatus.Met, Integrity.StatusOn("SQL02", states));
+ Assert.Equal(ControlStatus.NotMet, Integrity.StatusOn("SQL03", states)); // a finding always counts
+ }
+}
diff --git a/tests/SQLInsight.IntegrationTests/WebTests.cs b/tests/SQLInsight.IntegrationTests/WebTests.cs
index c08d5eb..a3ddee1 100644
--- a/tests/SQLInsight.IntegrationTests/WebTests.cs
+++ b/tests/SQLInsight.IntegrationTests/WebTests.cs
@@ -104,6 +104,29 @@ public class WebTests(SqlServerFixture sql) : IAsyncLifetime
Assert.Contains($"/deadlocks/{_deadlockId}.xdl", html, StringComparison.Ordinal);
}
+ [Fact]
+ public async Task Rules_not_applicable_to_the_version_are_collapsed_and_not_counted_as_not_measurable()
+ {
+ var id = (await _repository.GetInstanceAsync("prod-sql01"))!.InstanceId;
+ await _repository.SaveRuleResultsAsync(id,
+ [
+ new RuleResult { RuleId = "SQLI-QS-001", RuleTitle = "Query Store er slået fra", Outcome = RuleOutcome.NotApplicable, Reason = "Ikke relevant for SQL Server 2019: kræver noget nyere." },
+ new RuleResult { RuleId = "SQLI-QS-002", RuleTitle = "Query Store er skrivebeskyttet", Outcome = RuleOutcome.NotApplicable, Reason = "Ikke relevant for SQL Server 2019: kræver noget nyere." },
+ ], DateTimeOffset.UtcNow);
+
+ var overview = (await _repository.GetInstancesAsync()).Single(i => i.Name == "prod-sql01");
+ Assert.Equal(1, overview.NotMeasurable); // only SQLI-DB-011, a real gap
+
+ var html = await _factory.CreateClient().GetStringAsync("/instanser/prod-sql01");
+ var notMeasurable = html[html.IndexOf("Ikke målbart på denne instans", StringComparison.Ordinal)..];
+ var collapsed = notMeasurable[notMeasurable.IndexOf("", StringComparison.Ordinal)..];
+ Assert.Contains("Ikke relevant for SQL Server 2019 (2)", collapsed, StringComparison.Ordinal);
+ Assert.Contains("SQLI-QS-001", collapsed, StringComparison.Ordinal);
+ // The real gap stays visible, before and outside the collapsed list.
+ Assert.DoesNotContain("SQLI-DB-011", collapsed, StringComparison.Ordinal);
+ Assert.Contains("SQLI-DB-011", notMeasurable, StringComparison.Ordinal);
+ }
+
[Fact]
public async Task Finding_page_shows_explanation_evidence_and_script_html_encoded()
{
diff --git a/tests/SQLInsight.Rules.Tests/CustomRuleTests.cs b/tests/SQLInsight.Rules.Tests/CustomRuleTests.cs
index b1630fa..657f883 100644
--- a/tests/SQLInsight.Rules.Tests/CustomRuleTests.cs
+++ b/tests/SQLInsight.Rules.Tests/CustomRuleTests.cs
@@ -115,13 +115,13 @@ public class CustomRuleTests
}
[Fact]
- public void Rules_needing_a_newer_version_are_not_measurable()
+ public void Rules_needing_a_newer_version_are_not_applicable()
{
var rule = Rule(Valid.Replace("\"severity\"", "\"minimumVersion\": \"13.0\", \"severity\"", StringComparison.Ordinal));
var result = Assert.Single(new RuleEngine([], Catalog(rule)).Evaluate(TestInstances.Context("11.0.7001.0")));
- Assert.Equal(RuleOutcome.NotMeasurable, result.Outcome);
+ Assert.Equal(RuleOutcome.NotApplicable, result.Outcome);
Assert.Contains("13.0", result.Reason, StringComparison.Ordinal);
}
diff --git a/tests/SQLInsight.Rules.Tests/DatabaseRuleTests.cs b/tests/SQLInsight.Rules.Tests/DatabaseRuleTests.cs
index 25494bf..d8de50e 100644
--- a/tests/SQLInsight.Rules.Tests/DatabaseRuleTests.cs
+++ b/tests/SQLInsight.Rules.Tests/DatabaseRuleTests.cs
@@ -147,11 +147,11 @@ public class DatabaseRuleTests
}
[Fact]
- public void CheckDb_is_not_measurable_before_2016_sp2()
+ public void CheckDb_is_not_applicable_before_2016_sp2()
{
var result = Run(new CheckDbRule(), WithVersion("13.0.4001.0", DatabaseList(Database())));
- Assert.Equal(RuleOutcome.NotMeasurable, result.Outcome);
+ Assert.Equal(RuleOutcome.NotApplicable, result.Outcome);
}
[Fact]
diff --git a/tests/SQLInsight.Rules.Tests/ManagedInstanceTests.cs b/tests/SQLInsight.Rules.Tests/ManagedInstanceTests.cs
index 7ac4ec2..f306588 100644
--- a/tests/SQLInsight.Rules.Tests/ManagedInstanceTests.cs
+++ b/tests/SQLInsight.Rules.Tests/ManagedInstanceTests.cs
@@ -30,7 +30,7 @@ public class ManagedInstanceTests
foreach (var id in new[] { "SQLI-VER-001", "SQLI-VER-002", "SQLI-CFG-001", "SQLI-DB-007", "SQLI-DB-009", "SQLI-DB-015" })
{
var result = results.Single(r => r.RuleId == id);
- Assert.Equal(RuleOutcome.NotMeasurable, result.Outcome);
+ Assert.Equal(RuleOutcome.NotApplicable, result.Outcome);
Assert.Equal("Håndteres af Azure på Azure SQL Managed Instance.", result.Reason);
}
}
diff --git a/tests/SQLInsight.Rules.Tests/PlanRuleTests.cs b/tests/SQLInsight.Rules.Tests/PlanRuleTests.cs
index 1076492..0dbda6f 100644
--- a/tests/SQLInsight.Rules.Tests/PlanRuleTests.cs
+++ b/tests/SQLInsight.Rules.Tests/PlanRuleTests.cs
@@ -86,9 +86,9 @@ public class PlanRuleTests
}
[Fact]
- public void Memory_rules_are_not_measurable_without_the_columns()
+ public void Memory_rules_are_not_applicable_without_the_columns()
{
- Assert.Equal(RuleOutcome.NotMeasurable, Run(new SpillRule(), WithVersion("14.0.1000.169", new PlanAnalysisSnapshot { CapturedAt = Now, Statements = [] })).Outcome);
+ Assert.Equal(RuleOutcome.NotApplicable, Run(new SpillRule(), WithVersion("14.0.1000.169", new PlanAnalysisSnapshot { CapturedAt = Now, Statements = [] })).Outcome);
}
[Fact]
diff --git a/tests/SQLInsight.Rules.Tests/QueryStoreRuleTests.cs b/tests/SQLInsight.Rules.Tests/QueryStoreRuleTests.cs
index b3df142..ab83006 100644
--- a/tests/SQLInsight.Rules.Tests/QueryStoreRuleTests.cs
+++ b/tests/SQLInsight.Rules.Tests/QueryStoreRuleTests.cs
@@ -121,10 +121,10 @@ public class QueryStoreRuleTests
}
[Fact]
- public void Query_store_rules_are_not_measurable_before_2016()
+ public void Query_store_rules_are_not_applicable_before_2016()
{
var result = Run(new PlanRegressionRule(), WithVersion("12.0.6024.0"));
- Assert.Equal(RuleOutcome.NotMeasurable, result.Outcome);
+ Assert.Equal(RuleOutcome.NotApplicable, result.Outcome);
}
}
diff --git a/tests/SQLInsight.Rules.Tests/RuleEngineTests.cs b/tests/SQLInsight.Rules.Tests/RuleEngineTests.cs
index 9e90316..1965d65 100644
--- a/tests/SQLInsight.Rules.Tests/RuleEngineTests.cs
+++ b/tests/SQLInsight.Rules.Tests/RuleEngineTests.cs
@@ -21,19 +21,62 @@ public class RuleEngineTests
}
[Fact]
- public void Rule_missing_capabilities_is_not_measurable_and_never_runs()
+ 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("11.0.7001.0")));
+ var result = Assert.Single(engine.Evaluate(TestInstances.Context("9.0.5057.0")));
- Assert.Equal(RuleOutcome.NotMeasurable, result.Outcome);
+ Assert.Equal(RuleOutcome.NotApplicable, result.Outcome);
Assert.Equal([Capability.QueryStore], result.MissingCapabilities);
- Assert.Contains("SQL Server 2012", result.Reason, StringComparison.Ordinal);
+ 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();
+ 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("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()
{