sqlinsight-pro/tests/SQLInsight.Collectors.Tests/SqlScriptTests.cs
Søren Eggert Lundsteen Olsen 3a0bbe22f1 Review 1.4: SQL Scripts for Dynamics AX in the 1.3 structure
The AX scripts written in 1.4 (own header style and a generator) now follow
main's "SQL Scripts" structure: the review header, "-- Kilde:" blocks
registered in SqlScriptSources and checked word for word against the code.

- SqlScriptSources: the shared AX detection (DatabasesSql, LoginSql,
  BuildDetectionSql), companies, batch jobs, batch history (first read and
  from the watermark) and number sequences / RecId for Axapta 3.0, AX 4.0,
  AX 2009 and AX 2012; BlockingCollector split 2008 / 2005, as CONTEXT_INFO
  comes from sys.dm_exec_sessions from 2008 and from sysprocesses on 2005.
- SQLI-AX-001..010 and _collectors/Ax*.sql in main's format; COM-AX-001..007
  with the 1.4 queries, the AX detection and USE [AXDB]; BLK-001,
  BlockingCollector and DatabaseDiscovery updated (CONTEXT_INFO,
  sp_executesql).
- Scripts_are_read_only stays strict (no INTO #temp anywhere) and now also
  checks the batch inside EXEC [db].sys.sp_executesql N'...'.
- SQLINSIGHT_UPDATE_SQL_SCRIPTS=1 (from 1.4) rewrites out-of-date blocks from
  the code; the 1.4 generator test now checks that every AX script shows its
  sources for every AX version and says what is unverified.
- README: SQLI-AX-001..010, the AX collector scripts, 108 rules.
- AxDatabaseDetector: LoginSql and BuildDetectionSql as members, so the
  registry sees them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 14:11:45 +02:00

111 lines
5.5 KiB
C#

using SQLInsight.Collectors.Tests.SqlScripts;
namespace SQLInsight.Collectors.Tests;
/// <summary>
/// The Dynamics AX part of "SQL Scripts" (1.4): every AX rule script and AX collector script shows the SQL its rule or
/// collector sends, for every AX version SQLInsight has a schema for. The SQL itself is compared with the code by
/// <see cref="SqlScripts.SqlScriptTests"/> (the <c>-- Kilde:</c> blocks, registered in <see cref="SqlScriptSources"/>);
/// rewrite out-of-date blocks with SQLINSIGHT_UPDATE_SQL_SCRIPTS=1.
/// </summary>
public sealed class SqlScriptTests
{
private static readonly string[] AxVersions = ["3.0", "4.0", "2009", "2012"];
private static readonly string[] Detection = ["AxDatabaseDetector.DatabasesSql", "AxDatabaseDetector.BuildDetectionSql"];
private static string[] Batch =>
[
.. Detection, .. AxVersions.Select(v => $"AxSchemaAdapter.BatchJobs({v})"),
"AxSchemaAdapter.BatchHistory(2009, første)", "AxSchemaAdapter.BatchHistory(2009, næste)",
"AxSchemaAdapter.BatchHistory(2012, første)", "AxSchemaAdapter.BatchHistory(2012, næste)",
];
private static string[] NumberSequences => [.. Detection, .. AxVersions.Select(v => $"AxSchemaAdapter.NumberSequences({v})")];
/// <summary>File → the sources (keys) whose SQL it must show.</summary>
internal static Dictionary<string, string[]> Expected()
{
var files = new Dictionary<string, string[]>(StringComparer.Ordinal)
{
["_collectors/AxDatabaseDetector.sql"] = [.. Detection, "AxDatabaseDetector.LoginSql", .. AxVersions.Select(v => $"AxSchemaAdapter.Companies({v})")],
["_collectors/AxBatchCollector.sql"] = Batch[Detection.Length..],
["_collectors/AxSequenceCollector.sql"] = [.. AxVersions.Select(v => $"AxSchemaAdapter.NumberSequences({v})"), .. AxVersions.Select(v => $"AxSchemaAdapter.RecIds({v})")],
["SQLI-AX-001.sql"] = Batch,
["SQLI-AX-002.sql"] = Batch,
["SQLI-AX-003.sql"] = Batch,
["SQLI-AX-004.sql"] = Batch,
["SQLI-AX-005.sql"] = NumberSequences,
["SQLI-AX-006.sql"] = NumberSequences,
["SQLI-AX-007.sql"] = NumberSequences,
["SQLI-AX-008.sql"] = NumberSequences,
["SQLI-AX-009.sql"] = [.. Detection, .. AxVersions.Select(v => $"AxSchemaAdapter.RecIds({v})")],
["SQLI-AX-010.sql"] =
[
.. Detection, "BlockingCollector.BuildSql2005(2012+)", "BlockingCollector.BuildSql2005(2008)", "BlockingCollector.BuildSql2005(2005)", "BlockingCollector.Sql2000",
],
};
// The AX community rules run only in the monitored databases the shared detection recognises.
for (var i = 1; i <= 7; i++)
{
files[$"COM-AX-00{i}.sql"] = ["CustomQueryCollector.DatabasesSql", .. Detection, "CustomQueryCollector.BuildRuleSql"];
}
return files;
}
public static TheoryData<string> Files() => [.. Expected().Keys];
[Theory]
[MemberData(nameof(Files))]
public void Script_holds_the_sql_sqlinsight_runs_for_every_ax_version(string file)
{
var path = Path.Combine(Folder, file);
Assert.True(File.Exists(path), $"SQL Scripts/{file} mangler.");
var keys = File.ReadAllLines(path).Where(l => l.StartsWith("-- Kilde: ", StringComparison.Ordinal)).Select(l => l["-- Kilde: ".Length..].Trim()).ToHashSet(StringComparer.Ordinal);
var missing = Expected()[file].Where(k => !keys.Contains(k)).ToList();
Assert.True(missing.Count == 0, $"SQL Scripts/{file} mangler blokkene: " + string.Join(", ", missing));
Assert.All(Expected()[file], k => Assert.True(SqlScriptSources.ByKey.ContainsKey(k), $"'{k}' er ikke registreret i SqlScriptSources"));
}
[Theory]
[MemberData(nameof(Files))]
public void Header_says_what_the_reader_needs(string file)
{
var text = File.ReadAllText(Path.Combine(Folder, file));
// The header as running text: comment lines wrapped at 120 characters are joined again.
var header = System.Text.RegularExpressions.Regex.Replace(text[..text.IndexOf("-- Variant:", StringComparison.Ordinal)], @"\r?\n--\s+", " ");
Assert.Contains("Versioner:", header, StringComparison.Ordinal);
Assert.Contains("Read-only:", header, StringComparison.Ordinal);
Assert.Contains("ikke verificeret mod kundedata", header, StringComparison.Ordinal);
Assert.Contains("sp_executesql", header, StringComparison.Ordinal);
if (!file.StartsWith("COM-", StringComparison.Ordinal))
{
// The AX table reads (the community rules read metadata only).
Assert.Contains("aldrig NOLOCK", header, StringComparison.Ordinal);
}
Assert.DoesNotContain("PXI_", text, StringComparison.OrdinalIgnoreCase);
if (!file.StartsWith('_'))
{
Assert.Contains("Alvor:", header, StringComparison.Ordinal);
Assert.Contains("Rettigheder", header, StringComparison.Ordinal);
}
}
private static string Folder
{
get
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "SQLInsight.slnx")))
{
directory = directory.Parent;
}
return Path.Combine(directory?.FullName ?? throw new InvalidOperationException("Repository-roden blev ikke fundet."), "SQL Scripts");
}
}
}