diff --git a/SqlServer.Rules.Test/BaselineSetup.cs b/SqlServer.Rules.Test/BaselineSetup.cs index c30d840..a18948f 100644 --- a/SqlServer.Rules.Test/BaselineSetup.cs +++ b/SqlServer.Rules.Test/BaselineSetup.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -8,129 +8,127 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using SqlServer.Rules.Tests.Utils; -namespace SqlServer.Rules.Test +namespace SqlServer.Rules.Test; + +internal sealed class BaselineSetup : RuleTest { - class BaselineSetup : RuleTest + private const string TestScriptsFolder = "TestScripts"; + private const string SetupScriptsFolder = "_Setup"; + private const string Output = "Output"; + private const string Baseline = "Baseline"; + private const string SqlExtension = ".sql"; + + public string ScriptsFolder { get; private set; } + public string SetupFolder { get; private set; } + public string OutputFilePath { get; private set; } + public string BaselineFilePath { get; private set; } + + public BaselineSetup(TestContext testContext, string testName, TSqlModelOptions databaseOptions, SqlServerVersion sqlServerVersion = SqlServerVersion.Sql150) + : base(new List>(), databaseOptions, sqlServerVersion) { - private const string TestScriptsFolder = "TestScripts"; - private const string SetupScriptsFolder = "_Setup"; - private const string Output = "Output"; - private const string Baseline = "Baseline"; - private const string DacpacBaseline = "DacpacBaseline"; - private const string SqlExtension = ".sql"; - - protected string ScriptsFolder { get; private set; } - protected string SetupFolder { get; private set; } - protected string OutputFilePath { get; private set; } - protected string BaselineFilePath { get; private set; } - - public BaselineSetup(TestContext testContext, string testName, TSqlModelOptions databaseOptions, SqlServerVersion sqlServerVersion = SqlServerVersion.Sql150) - : base(new List>(), databaseOptions, sqlServerVersion) - { - var folder = Path.Combine(GetBaseFolder(), TestScriptsFolder); - ScriptsFolder = Directory.EnumerateDirectories(folder, testName, SearchOption.AllDirectories).FirstOrDefault(); - Assert.IsTrue(Directory.Exists(ScriptsFolder), $"Expected the test folder '{ScriptsFolder}' to exist"); + var folder = Path.Combine(GetBaseFolder(), TestScriptsFolder); + ScriptsFolder = Directory.EnumerateDirectories(folder, testName, SearchOption.AllDirectories).FirstOrDefault(); + Assert.IsTrue(Directory.Exists(ScriptsFolder), $"Expected the test folder '{ScriptsFolder}' to exist"); - SetupFolder = Path.Combine(GetBaseFolder(), TestScriptsFolder, SetupScriptsFolder); + SetupFolder = Path.Combine(GetBaseFolder(), TestScriptsFolder, SetupScriptsFolder); - var outputDir = testContext.TestResultsDirectory; - var outputFilename = $"{testName}-{Output}.txt"; - OutputFilePath = Path.Combine(outputDir, testName, outputFilename); + var outputDir = testContext.TestResultsDirectory; + var outputFilename = $"{testName}-{Output}.txt"; + OutputFilePath = Path.Combine(outputDir, testName, outputFilename); - var baselineFilename = $"{testName}-{Baseline}.txt"; - BaselineFilePath = Path.Combine(ScriptsFolder, baselineFilename); - } + var baselineFilename = $"{testName}-{Baseline}.txt"; + BaselineFilePath = Path.Combine(ScriptsFolder, baselineFilename); + } - private string GetBaseFolder() - { - var testAssemply = GetType().Assembly; + private string GetBaseFolder() + { + var testAssemply = GetType().Assembly; - return Path.GetDirectoryName(testAssemply.Location); - } + return Path.GetDirectoryName(testAssemply.Location); + } - public override void RunTest(string fullId, Action verify) - { - LoadTestScripts(SetupFolder); - LoadTestScripts(ScriptsFolder); - base.RunTest(fullId, verify); - } + public override void RunTest(string fullId, Action verify) + { + LoadTestScripts(SetupFolder); + LoadTestScripts(ScriptsFolder); + base.RunTest(fullId, verify); + } - public void RunTest(string fullId) + public void RunTest(string fullId) + { + LoadTestScripts(SetupFolder); + LoadTestScripts(ScriptsFolder); + base.RunTest(fullId, RunVerification); + } + + private void LoadTestScripts(string folder) + { + if (!Directory.Exists(folder)) { - LoadTestScripts(SetupFolder); - LoadTestScripts(ScriptsFolder); - base.RunTest(fullId, RunVerification); + return; } - private void LoadTestScripts(string folder) + var directoryInfo = new DirectoryInfo(folder); + + var scriptFilePaths = from file in directoryInfo.GetFiles("*" + SqlExtension) + where SqlExtension.Equals(file.Extension, StringComparison.OrdinalIgnoreCase) + select file.FullName; + + foreach (var scriptFile in scriptFilePaths) { - if (!Directory.Exists(folder)) + try { - return; + var contents = RuleTestUtils.ReadFileToString(scriptFile); + TestScripts.Add(Tuple.Create(contents, Path.GetFileName(scriptFile))); + Console.WriteLine($"Test file '{scriptFile}' loaded successfully"); } - - var directoryInfo = new DirectoryInfo(folder); - - var scriptFilePaths = from file in directoryInfo.GetFiles("*" + SqlExtension) - where SqlExtension.Equals(file.Extension, StringComparison.OrdinalIgnoreCase) - select file.FullName; - - foreach (var scriptFile in scriptFilePaths) + catch (Exception ex) { - try - { - var contents = RuleTestUtils.ReadFileToString(scriptFile); - TestScripts.Add(Tuple.Create(contents, Path.GetFileName(scriptFile))); - Console.WriteLine($"Test file '{scriptFile}' loaded successfully"); - } - catch (Exception ex) - { - Console.WriteLine($"Error reading from file {scriptFile} with message '{ex.Message}'"); - Console.WriteLine("Execution will continue..."); - } + Console.WriteLine($"Error reading from file {scriptFile} with message '{ex.Message}'"); + Console.WriteLine("Execution will continue..."); } } + } - private void RunVerification(CodeAnalysisResult result, string resultsString) - { - var baseline = RuleTestUtils.ReadFileToString(BaselineFilePath); - RuleTestUtils.SaveStringToFile(resultsString, OutputFilePath); + private void RunVerification(CodeAnalysisResult result, string resultsString) + { + var baseline = RuleTestUtils.ReadFileToString(BaselineFilePath); + RuleTestUtils.SaveStringToFile(resultsString, OutputFilePath); - var loadedTestScriptFiles = ListScriptFilenames(); + var loadedTestScriptFiles = ListScriptFilenames(); - if (string.Compare(resultsString, baseline, false, System.Globalization.CultureInfo.CurrentCulture) != 0) - { - var failureMessage = new StringBuilder(); - - failureMessage.AppendLine($"The result is not the same as expected. Please compare actual output to baseline."); - failureMessage.AppendLine(string.Empty); - failureMessage.AppendLine($"### Loaded Test Script Files ###"); - failureMessage.AppendLine(loadedTestScriptFiles); - failureMessage.AppendLine(string.Empty); - failureMessage.AppendLine($"### View Baseline ###"); - failureMessage.AppendLine(BaselineFilePath); - failureMessage.AppendLine(string.Empty); - failureMessage.AppendLine($"### View Action Output ###"); - failureMessage.AppendLine(OutputFilePath); - failureMessage.AppendLine(string.Empty); - failureMessage.AppendLine($"### Test Folder ###"); - failureMessage.AppendLine(ScriptsFolder); - - Assert.Fail(failureMessage.ToString()); - } - } - - private string ListScriptFilenames() + if (string.Equals(resultsString, baseline, StringComparison.OrdinalIgnoreCase)) { - var loadedTestScriptFiles = new StringBuilder(); + var failureMessage = new StringBuilder(); + + failureMessage.AppendLine($"The result is not the same as expected. Please compare actual output to baseline."); + failureMessage.AppendLine(string.Empty); + failureMessage.AppendLine($"### Loaded Test Script Files ###"); + failureMessage.AppendLine(loadedTestScriptFiles); + failureMessage.AppendLine(string.Empty); + failureMessage.AppendLine($"### View Baseline ###"); + failureMessage.AppendLine(BaselineFilePath); + failureMessage.AppendLine(string.Empty); + failureMessage.AppendLine($"### View Action Output ###"); + failureMessage.AppendLine(OutputFilePath); + failureMessage.AppendLine(string.Empty); + failureMessage.AppendLine($"### Test Folder ###"); + failureMessage.AppendLine(ScriptsFolder); + + Assert.Fail(failureMessage.ToString()); + } + } - foreach (var scriptInfo in TestScripts) - { - var scriptPath = scriptInfo.Item2; - loadedTestScriptFiles.AppendLine(scriptPath); - } + private string ListScriptFilenames() + { + var loadedTestScriptFiles = new StringBuilder(); - return loadedTestScriptFiles.ToString(); + foreach (var scriptInfo in TestScripts) + { + var scriptPath = scriptInfo.Item2; + loadedTestScriptFiles.AppendLine(scriptPath); } + + return loadedTestScriptFiles.ToString(); } } diff --git a/SqlServer.Rules.Test/Design/DesignTestCases.cs b/SqlServer.Rules.Test/Design/DesignTestCases.cs index 422ae9f..717568f 100644 --- a/SqlServer.Rules.Test/Design/DesignTestCases.cs +++ b/SqlServer.Rules.Test/Design/DesignTestCases.cs @@ -3,48 +3,47 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using SqlServer.Rules.Design; -namespace SqlServer.Rules.Tests.Performance +namespace SqlServer.Rules.Tests.Performance; + +[TestClass] +[TestCategory("Design")] +public class DesignTestCases : TestCasesBase { - [TestClass] - [TestCategory("Design")] - public class DesignTestCases : TestCasesBase + [TestMethod] + public void TestAvoidNotForReplication() { - [TestMethod] - public void TestAvoidNotForReplication() - { - var problems = GetTestCaseProblems(nameof(NotForReplication), NotForReplication.RuleId); + var problems = GetTestCaseProblems(nameof(NotForReplication), NotForReplication.RuleId); - const int expected = 4; - Assert.AreEqual(expected, problems.Count, $"Expected {expected} problem(s) to be found"); + const int expected = 4; + Assert.AreEqual(expected, problems.Count, $"Expected {expected} problem(s) to be found"); - Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "dbo_table2_trigger_1_not_for_replication.sql"))); - Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "fk_table2_table1_1_not_for_replication.sql"))); - Assert.IsTrue(problems.Count(problem => Comparer.Equals(problem.SourceName, "table3.sql")) == 2); + Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "dbo_table2_trigger_1_not_for_replication.sql"))); + Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "fk_table2_table1_1_not_for_replication.sql"))); + Assert.IsTrue(problems.Count(problem => Comparer.Equals(problem.SourceName, "table3.sql")) == 2); - Assert.IsTrue(problems.All(problem => Comparer.Equals(problem.Description, NotForReplication.Message))); - Assert.IsTrue(problems.All(problem => problem.Severity == SqlRuleProblemSeverity.Warning)); - } + Assert.IsTrue(problems.All(problem => Comparer.Equals(problem.Description, NotForReplication.Message))); + Assert.IsTrue(problems.All(problem => problem.Severity == SqlRuleProblemSeverity.Warning)); + } - [TestMethod] - public void TestMissingJoinPredicateFalsePositive() - { - var problems = GetTestCaseProblems(nameof(MissingJoinPredicateRule), MissingJoinPredicateRule.RuleId); + [TestMethod] + public void TestMissingJoinPredicateFalsePositive() + { + var problems = GetTestCaseProblems(nameof(MissingJoinPredicateRule), MissingJoinPredicateRule.RuleId); - const int expected = 1; - Assert.AreEqual(expected, problems.Count, $"Expected {expected} problem(s) to be found"); + const int expected = 1; + Assert.AreEqual(expected, problems.Count, $"Expected {expected} problem(s) to be found"); - Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "mtgfunc.sql"))); + Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "mtgfunc.sql"))); - Assert.IsTrue(problems.All(problem => Comparer.Equals(problem.Description, MissingJoinPredicateRule.MessageNoJoin))); - Assert.IsTrue(problems.All(problem => problem.Severity == SqlRuleProblemSeverity.Warning)); - } + Assert.IsTrue(problems.All(problem => Comparer.Equals(problem.Description, MissingJoinPredicateRule.MessageNoJoin))); + Assert.IsTrue(problems.All(problem => problem.Severity == SqlRuleProblemSeverity.Warning)); + } - [TestMethod] - public void TestTableMissingClusteredIndexRule() - { - var problems = GetTestCaseProblems(nameof(TableMissingClusteredIndexRule), TableMissingClusteredIndexRule.RuleId); + [TestMethod] + public void TestTableMissingClusteredIndexRule() + { + var problems = GetTestCaseProblems(nameof(TableMissingClusteredIndexRule), TableMissingClusteredIndexRule.RuleId); - Assert.AreEqual(0, problems.Count, "Expected 0 problems to be found"); - } + Assert.AreEqual(0, problems.Count, "Expected 0 problems to be found"); } } diff --git a/SqlServer.Rules.Test/Directory.Build.props b/SqlServer.Rules.Test/Directory.Build.props deleted file mode 100644 index 7f21e00..0000000 --- a/SqlServer.Rules.Test/Directory.Build.props +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/SqlServer.Rules.Test/Docs/DocsGenerator.cs b/SqlServer.Rules.Test/Docs/DocsGenerator.cs index ae40f97..a8f757b 100644 --- a/SqlServer.Rules.Test/Docs/DocsGenerator.cs +++ b/SqlServer.Rules.Test/Docs/DocsGenerator.cs @@ -13,346 +13,347 @@ using SqlServer.Rules.Design; using TSQLSmellSCA; -namespace SqlServer.Rules.Tests.Docs +namespace SqlServer.Rules.Tests.Docs; + +[TestClass] +[TestCategory("Docs")] +public class DocsGenerator { - [TestClass] - [TestCategory("Docs")] - public class DocsGenerator + [TestMethod] + public void GenerateDocs() { - [TestMethod] - public void GenerateDocs() - { - var assembly = typeof(ObjectCreatedWithInvalidOptionsRule).Assembly; - var assemblyPath = assembly.Location; - const string docsFolder = "../../../../docs"; - const string rulesScriptFolder = "../../../../TSQLSmellsTest"; - - var rules = assembly.GetTypes() - .Where(t => t.IsClass - && !t.IsAbstract - && t.IsSubclassOf(typeof(BaseSqlCodeAnalysisRule)) - && t.GetCustomAttributes(typeof(ExportCodeAnalysisRuleAttribute), false).Any() - ) - .ToList(); + var assembly = typeof(ObjectCreatedWithInvalidOptionsRule).Assembly; + var assemblyPath = assembly.Location; + const string docsFolder = "../../../../docs"; + const string rulesScriptFolder = "../../../../TSQLSmellsTest"; - var ruleScripts = CollectRuleScripts(rulesScriptFolder); - var smellsAssembly = typeof(Smells).Assembly; + var rules = assembly.GetTypes() + .Where(t => t.IsClass + && !t.IsAbstract + && t.IsSubclassOf(typeof(BaseSqlCodeAnalysisRule)) + && t.GetCustomAttributes(typeof(ExportCodeAnalysisRuleAttribute), false).Any() + ) + .ToList(); - var tSqlSmellRules = smellsAssembly.GetTypes() - .Where(t => t.IsClass - && !t.IsAbstract - && t.GetCustomAttributes(typeof(ExportCodeAnalysisRuleAttribute), false).Any() - ) - .ToList(); + var ruleScripts = CollectRuleScripts(rulesScriptFolder); - rules.AddRange(tSqlSmellRules); + var smellsAssembly = typeof(Smells).Assembly; - var categories = rules.Select(t => - { - var ruleAttribute = t.GetCustomAttributes(typeof(ExportCodeAnalysisRuleAttribute), false).FirstOrDefault() as ExportCodeAnalysisRuleAttribute; - return ruleAttribute.Category; - }).Distinct().Order().ToList(); + var tSqlSmellRules = smellsAssembly.GetTypes() + .Where(t => t.IsClass + && !t.IsAbstract + && t.GetCustomAttributes(typeof(ExportCodeAnalysisRuleAttribute), false).Any() + ) + .ToList(); + + rules.AddRange(tSqlSmellRules); - CreateFolders(docsFolder, categories); + var categories = rules.Select(t => + { + var ruleAttribute = t.GetCustomAttributes(typeof(ExportCodeAnalysisRuleAttribute), false).FirstOrDefault() as ExportCodeAnalysisRuleAttribute; + return ruleAttribute.Category; + }).Distinct().Order().ToList(); - var xmlPath = assemblyPath.Replace(".dll", ".xml"); - Assert.IsTrue(File.Exists(xmlPath)); - var reader = new DocXmlReader(xmlPath); + CreateFolders(docsFolder, categories); - rules.ForEach(t => - { - var comments = reader.GetTypeComments(t); - var ruleAttribute = t.GetCustomAttributes(typeof(ExportCodeAnalysisRuleAttribute), false).FirstOrDefault() as ExportCodeAnalysisRuleAttribute; + var xmlPath = assemblyPath.Replace(".dll", ".xml", StringComparison.OrdinalIgnoreCase); + Assert.IsTrue(File.Exists(xmlPath)); + var reader = new DocXmlReader(xmlPath); - var elements = GetRuleElements(t, ruleAttribute); + rules.ForEach(t => + { + var comments = reader.GetTypeComments(t); + var ruleAttribute = t.GetCustomAttributes(typeof(ExportCodeAnalysisRuleAttribute), false).FirstOrDefault() as ExportCodeAnalysisRuleAttribute; - GenerateRuleMarkdown(comments, elements, ruleScripts, ruleAttribute, Path.Combine(docsFolder, ruleAttribute.Category), t.Assembly.GetName().Name, t.Namespace, t.Name); - }); + var elements = GetRuleElements(t, ruleAttribute); - GenerateTocMarkdown(rules, null, categories, reader, docsFolder); - } + GenerateRuleMarkdown(comments, elements, ruleScripts, ruleAttribute, Path.Combine(docsFolder, ruleAttribute.Category), t.Assembly.GetName().Name, t.Namespace, t.Name); + }); + + GenerateTocMarkdown(rules, categories, ruleScripts, reader, docsFolder); + } - private static void CreateFolders(string docsFolder, List categories) + private static void CreateFolders(string docsFolder, List categories) + { + if (!Directory.Exists(docsFolder)) { - if (!Directory.Exists(docsFolder)) - { - Directory.CreateDirectory(docsFolder); - } + Directory.CreateDirectory(docsFolder); + } - foreach (var category in categories) + foreach (var category in categories) + { + var categoryFolder = Path.Combine(docsFolder, category); + if (!Directory.Exists(categoryFolder)) { - var categoryFolder = Path.Combine(docsFolder, category); - if (!Directory.Exists(categoryFolder)) - { - Directory.CreateDirectory(categoryFolder); - } + Directory.CreateDirectory(categoryFolder); } } + } - private static List GetRuleElements(Type type, ExportCodeAnalysisRuleAttribute attribute) + private static List GetRuleElements(Type type, ExportCodeAnalysisRuleAttribute attribute) + { + var elements = new List(); + + if (attribute.RuleScope == SqlRuleScope.Element) { - var elements = new List(); + var constructor = type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null); - if (attribute.RuleScope == SqlRuleScope.Element) + if (constructor != null) { - var constructor = type.GetConstructor(BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null); - - if (constructor != null) + var instance = (BaseSqlCodeAnalysisRule)constructor.Invoke(null); + if (instance != null) { - var instance = (BaseSqlCodeAnalysisRule)constructor.Invoke(null); - if (instance != null) + foreach (var item in instance.SupportedElementTypes) { - foreach (var item in instance.SupportedElementTypes) - { - elements.Add(item.Name.ToSentence()); - } + elements.Add(item.Name.ToSentence()); } } } - else - { - elements.Add("Model"); - } - - return elements.Order().ToList(); } + else + { + elements.Add("Model"); + } + + return elements.Order().ToList(); + } + + private static void GenerateRuleMarkdown(TypeComments comments, List elements, Dictionary> ruleScripts, ExportCodeAnalysisRuleAttribute attribute, string docsFolder, string assemblyName, string nameSpace, string className) + { + var isIgnorable = string.Empty; + var friendlyName = string.Empty; + var exampleMd = string.Empty; - private static void GenerateRuleMarkdown(TypeComments comments, List elements, Dictionary> ruleScripts, ExportCodeAnalysisRuleAttribute attribute, string docsFolder, string assemblyName, string nameSpace, string className) + if (comments?.FullCommentText != null) { - var isIgnorable = string.Empty; - var friendlyName = string.Empty; - var exampleMd = string.Empty; + var fullXml = "" + comments.FullCommentText.Trim() + ""; + var fullComments = new XmlDocument(); + fullComments.LoadXml(fullXml); - if (comments?.FullCommentText != null) - { - var fullXml = "" + comments.FullCommentText.Trim() + ""; - var fullComments = new XmlDocument(); - fullComments.LoadXml(fullXml); + isIgnorable = fullComments.SelectSingleNode("comments/IsIgnorable")?.InnerText ?? "false"; + friendlyName = fullComments.SelectSingleNode("comments/FriendlyName")?.InnerText; + exampleMd = fullComments.SelectSingleNode("comments/ExampleMd")?.InnerText; + } - isIgnorable = fullComments.SelectSingleNode("comments/IsIgnorable")?.InnerText ?? "false"; - friendlyName = fullComments.SelectSingleNode("comments/FriendlyName")?.InnerText; - exampleMd = fullComments.SelectSingleNode("comments/ExampleMd")?.InnerText; - } + if (string.IsNullOrWhiteSpace(friendlyName)) + { + friendlyName = className.ToSentence(); + } - if (string.IsNullOrWhiteSpace(friendlyName)) - { - friendlyName = className.ToSentence(); - } + if (attribute.Id.StartsWith("Smells.", StringComparison.OrdinalIgnoreCase)) + { + friendlyName = attribute.Description; + isIgnorable = "false"; + } + + var stringBuilder = new StringBuilder(); + + var spaces = " "; + stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"# SQL Server Rule: {attribute.Id.ToId()}"); + stringBuilder.AppendLine(spaces); + stringBuilder.AppendLine("| | |"); + stringBuilder.AppendLine("|----|----|"); + stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| Assembly | {assemblyName} |"); + stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| Namespace | {nameSpace} |"); + stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| Class | {className} |"); + stringBuilder.AppendLine(spaces); + stringBuilder.AppendLine("## Rule Information"); + stringBuilder.AppendLine(spaces); + stringBuilder.AppendLine("| | |"); + stringBuilder.AppendLine("|----|----|"); + stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| Id | {attribute.Id.ToId()} |"); + stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| Friendly Name | {friendlyName} |"); + stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| Category | {attribute.Category} |"); + stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| Ignorable | {isIgnorable} |"); + stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| Applicable Types | {elements.First()} |"); + + if (elements.Count > 1) + { + elements.RemoveAt(0); - if (attribute.Id.StartsWith("Smells.")) + foreach (var element in elements) { - friendlyName = attribute.Description; - isIgnorable = "false"; + stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| | {element} |"); } + } - var stringBuilder = new StringBuilder(); + stringBuilder.AppendLine(spaces); + stringBuilder.AppendLine("## Description"); + stringBuilder.AppendLine(spaces); + stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{attribute.Description}"); - var spaces = " "; - stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"# SQL Server Rule: {attribute.Id.ToId()}"); - stringBuilder.AppendLine(spaces); - stringBuilder.AppendLine("| | |"); - stringBuilder.AppendLine("|----|----|"); - stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| Assembly | {assemblyName} |"); - stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| Namespace | {nameSpace} |"); - stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| Class | {className} |"); + if (!string.IsNullOrWhiteSpace(comments.Summary)) + { stringBuilder.AppendLine(spaces); - stringBuilder.AppendLine("## Rule Information"); + stringBuilder.AppendLine("## Summary"); stringBuilder.AppendLine(spaces); - stringBuilder.AppendLine("| | |"); - stringBuilder.AppendLine("|----|----|"); - stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| Id | {attribute.Id.ToId()} |"); - stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| Friendly Name | {friendlyName} |"); - stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| Category | {attribute.Category} |"); - stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| Ignorable | {isIgnorable} |"); - stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| Applicable Types | {elements.First()} |"); - - if (elements.Count > 1) - { - elements.RemoveAt(0); + stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{TrimLeadingWhitespace(comments.Summary)}"); + } - foreach (var element in elements) - { - stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| | {element} |"); - } - } + var scriptExamples = ruleScripts.ContainsKey(attribute.Id.ToId()) ? ruleScripts[attribute.Id.ToId()] : []; + if (!string.IsNullOrWhiteSpace(exampleMd) + || scriptExamples.Any()) + { stringBuilder.AppendLine(spaces); - stringBuilder.AppendLine("## Description"); - stringBuilder.AppendLine(spaces); - stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{attribute.Description}"); - - if (!string.IsNullOrWhiteSpace(comments.Summary)) - { - stringBuilder.AppendLine(spaces); - stringBuilder.AppendLine("## Summary"); - stringBuilder.AppendLine(spaces); - stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{TrimLeadingWhitespace(comments.Summary)}"); - } - - var scriptExamples = ruleScripts.ContainsKey(attribute.Id.ToId()) ? ruleScripts[attribute.Id.ToId()] : []; + stringBuilder.AppendLine("### Examples"); + } - if (!string.IsNullOrWhiteSpace(exampleMd) - || scriptExamples.Any()) - { - stringBuilder.AppendLine(spaces); - stringBuilder.AppendLine("### Examples"); - } + if (!string.IsNullOrWhiteSpace(exampleMd)) + { + stringBuilder.AppendLine(spaces); + stringBuilder.Append(CultureInfo.InvariantCulture, $"{TrimLeadingWhitespace(exampleMd)}"); + } - if (!string.IsNullOrWhiteSpace(exampleMd)) - { - stringBuilder.AppendLine(spaces); - stringBuilder.Append(CultureInfo.InvariantCulture, $"{TrimLeadingWhitespace(exampleMd)}"); - } + if (!string.IsNullOrWhiteSpace(comments.Remarks)) + { + stringBuilder.AppendLine(spaces); + stringBuilder.AppendLine("### Remarks"); + stringBuilder.AppendLine(spaces); + stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{comments.Remarks}"); + } - if (!string.IsNullOrWhiteSpace(comments.Remarks)) + if (scriptExamples.Any()) + { + stringBuilder.AppendLine(spaces); + foreach (var script in scriptExamples) { - stringBuilder.AppendLine(spaces); - stringBuilder.AppendLine("### Remarks"); - stringBuilder.AppendLine(spaces); - stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"{comments.Remarks}"); + stringBuilder.AppendLine("```sql"); + stringBuilder.AppendLine(script.Trim(Environment.NewLine.ToCharArray()).Trim()); + stringBuilder.AppendLine("```"); } + } - if (scriptExamples.Any()) - { - stringBuilder.AppendLine(spaces); - foreach (var script in scriptExamples) - { - stringBuilder.AppendLine("```sql"); - stringBuilder.AppendLine(script.Trim(Environment.NewLine.ToCharArray()).Trim()); - stringBuilder.AppendLine("```"); - } - } + stringBuilder.AppendLine(spaces); + stringBuilder.AppendLine("Generated by a tool"); - stringBuilder.AppendLine(spaces); - stringBuilder.AppendLine("Generated by a tool"); + var filePath = Path.Combine(docsFolder, $"{attribute.Id.ToId()}.md"); + File.WriteAllText(filePath, stringBuilder.ToString(), Encoding.UTF8); + } - var filePath = Path.Combine(docsFolder, $"{attribute.Id.ToId()}.md"); - File.WriteAllText(filePath, stringBuilder.ToString(), Encoding.UTF8); + private static string TrimLeadingWhitespace(string summary) + { + if (string.IsNullOrWhiteSpace(summary)) + { + return string.Empty; } - private static string TrimLeadingWhitespace(string summary) - { - if (string.IsNullOrWhiteSpace(summary)) - { - return string.Empty; - } + var lines = summary.Split([Environment.NewLine], StringSplitOptions.None); + var trimmedLines = lines.Select(l => l.TrimStart()).ToArray(); + return string.Join(Environment.NewLine, trimmedLines); + } - var lines = summary.Split([Environment.NewLine], StringSplitOptions.None); - var trimmedLines = lines.Select(l => l.TrimStart()).ToArray(); - return string.Join(Environment.NewLine, trimmedLines); - } + private static void GenerateTocMarkdown(List sqlServerRules, List categories, Dictionary> ruleScripts, DocXmlReader reader, string docsFolder) + { + const string spaces = " "; - private static void GenerateTocMarkdown(List sqlServerRules, List tSqlSmellRules, List categories, DocXmlReader reader, string docsFolder) - { - const string spaces = " "; + var stringBuilder = new StringBuilder(); + stringBuilder.AppendLine(spaces); + stringBuilder.AppendLine("# Table of Contents"); + stringBuilder.AppendLine(spaces); - var stringBuilder = new StringBuilder(); + foreach (var category in categories) + { stringBuilder.AppendLine(spaces); - stringBuilder.AppendLine("# Table of Contents"); + stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"## {category}"); stringBuilder.AppendLine(spaces); - foreach (var category in categories) + stringBuilder.AppendLine("| Rule Id | Friendly Name | Ignorable | Description | Example? |"); + stringBuilder.AppendLine("|----|----|----|----|----|"); + var categoryRules = sqlServerRules + .Where(t => ((ExportCodeAnalysisRuleAttribute)t.GetCustomAttributes(typeof(ExportCodeAnalysisRuleAttribute), false).FirstOrDefault()).Category == category) + .OrderBy(t => ((ExportCodeAnalysisRuleAttribute)t.GetCustomAttributes(typeof(ExportCodeAnalysisRuleAttribute), false).FirstOrDefault()).Id) + .ToList(); + foreach (var rule in categoryRules) { - stringBuilder.AppendLine(spaces); - stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"## {category}"); - stringBuilder.AppendLine(spaces); - - stringBuilder.AppendLine("| Rule Id | Friendly Name | Ignorable | Description | Example? |"); - stringBuilder.AppendLine("|----|----|----|----|----|"); - var categoryRules = sqlServerRules - .Where(t => ((ExportCodeAnalysisRuleAttribute)t.GetCustomAttributes(typeof(ExportCodeAnalysisRuleAttribute), false).FirstOrDefault()).Category == category) - .OrderBy(t => ((ExportCodeAnalysisRuleAttribute)t.GetCustomAttributes(typeof(ExportCodeAnalysisRuleAttribute), false).FirstOrDefault()).Id) - .ToList(); - foreach (var rule in categoryRules) - { - var comments = reader.GetTypeComments(rule); + var comments = reader.GetTypeComments(rule); - var isIgnorable = "No"; - var friendlyName = string.Empty; - var exampleMd = string.Empty; - - if (comments?.FullCommentText != null) - { - var fullXml = "" + comments.FullCommentText.Trim() + ""; - var fullComments = new XmlDocument(); - fullComments.LoadXml(fullXml); + var isIgnorable = "No"; + var friendlyName = string.Empty; + var exampleMd = string.Empty; - isIgnorable = fullComments.SelectSingleNode("comments/IsIgnorable")?.InnerText ?? "No"; - friendlyName = fullComments.SelectSingleNode("comments/FriendlyName")?.InnerText; - exampleMd = fullComments.SelectSingleNode("comments/ExampleMd")?.InnerText; - } + if (comments?.FullCommentText != null) + { + var fullXml = "" + comments.FullCommentText.Trim() + ""; + var fullComments = new XmlDocument(); + fullComments.LoadXml(fullXml); - if (string.IsNullOrWhiteSpace(friendlyName)) - { - friendlyName = rule.Name.ToSentence(); - } + isIgnorable = fullComments.SelectSingleNode("comments/IsIgnorable")?.InnerText ?? "No"; + friendlyName = fullComments.SelectSingleNode("comments/FriendlyName")?.InnerText; + exampleMd = fullComments.SelectSingleNode("comments/ExampleMd")?.InnerText; + } - friendlyName.Replace("|", "|"); + if (string.IsNullOrWhiteSpace(friendlyName)) + { + friendlyName = rule.Name.ToSentence(); + } - isIgnorable = isIgnorable != "false" ? "Yes" : " "; + friendlyName = friendlyName.Replace("|", "|", StringComparison.OrdinalIgnoreCase); - exampleMd = string.IsNullOrWhiteSpace(exampleMd) ? " " : "Yes"; + isIgnorable = isIgnorable != "false" ? "Yes" : " "; - var ruleAttribute = (ExportCodeAnalysisRuleAttribute)rule.GetCustomAttributes(typeof(ExportCodeAnalysisRuleAttribute), false).FirstOrDefault(); + exampleMd = string.IsNullOrWhiteSpace(exampleMd) ? " " : "Yes"; - if (ruleAttribute.Id.StartsWith("Smells.")) - { - friendlyName = ruleAttribute.Description; - isIgnorable = " "; - } + var ruleAttribute = (ExportCodeAnalysisRuleAttribute)rule.GetCustomAttributes(typeof(ExportCodeAnalysisRuleAttribute), false).FirstOrDefault(); - var ruleLink = $"[{ruleAttribute.Id.ToId()}]({category}/{ruleAttribute.Id.ToId()}.md)"; - stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| {ruleLink} | {friendlyName} | {isIgnorable} | {ruleAttribute.Description?.Replace("|", "|")} | {exampleMd} |"); + if (ruleAttribute.Id.StartsWith("Smells.", StringComparison.OrdinalIgnoreCase)) + { + friendlyName = ruleAttribute.Description; + isIgnorable = " "; + exampleMd = ruleScripts.Any(x => x.Key.Contains(ruleAttribute.Id.ToId(), StringComparison.OrdinalIgnoreCase)) ? "Yes" : " "; } + + var ruleLink = $"[{ruleAttribute.Id.ToId()}]({category}/{ruleAttribute.Id.ToId()}.md)"; + stringBuilder.AppendLine(CultureInfo.InvariantCulture, $"| {ruleLink} | {friendlyName} | {isIgnorable} | {ruleAttribute.Description?.Replace("|", "|", StringComparison.OrdinalIgnoreCase)} | {exampleMd} |"); } + } - stringBuilder.AppendLine(spaces); - stringBuilder.AppendLine("Generated by a tool"); + stringBuilder.AppendLine(spaces); + stringBuilder.AppendLine("Generated by a tool"); - File.WriteAllText(Path.Combine(docsFolder, "table_of_contents.md"), stringBuilder.ToString(), Encoding.UTF8); - } + File.WriteAllText(Path.Combine(docsFolder, "table_of_contents.md"), stringBuilder.ToString(), Encoding.UTF8); + } - private static Dictionary> CollectRuleScripts(string rulesScriptFolder) + private static Dictionary> CollectRuleScripts(string rulesScriptFolder) + { + var ruleScripts = new Dictionary>(); + var files = Directory.GetFiles(rulesScriptFolder, "*.sql", SearchOption.AllDirectories).ToList(); + foreach (var file in files) { - var ruleScripts = new Dictionary>(); - var files = Directory.GetFiles(rulesScriptFolder, "*.sql", SearchOption.AllDirectories).ToList(); - foreach (var file in files) - { - var ruleLine = File.ReadAllLines(file).FirstOrDefault(l => l.StartsWith("--", StringComparison.OrdinalIgnoreCase)); + var ruleLine = File.ReadAllLines(file).FirstOrDefault(l => l.StartsWith("--", StringComparison.OrdinalIgnoreCase)); - if (ruleLine == null || string.IsNullOrWhiteSpace(ruleLine)) - { - continue; - } + if (ruleLine == null || string.IsNullOrWhiteSpace(ruleLine)) + { + continue; + } - var ruleList = ruleLine.Replace("--", string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var ruleList = ruleLine.Replace("--", string.Empty, StringComparison.OrdinalIgnoreCase).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - foreach (var rule in ruleList) + foreach (var rule in ruleList) + { + if (!ruleScripts.ContainsKey(rule)) { - if (!ruleScripts.ContainsKey(rule)) - { - ruleScripts.Add(rule, []); - } - - ruleScripts[rule].Add(File.ReadAllText(file)); + ruleScripts.Add(rule, []); } - } - return ruleScripts; + ruleScripts[rule].Add(File.ReadAllText(file)); + } } + + return ruleScripts; } +} - public static class Extensions +public static class DocsExtensions +{ + public static string ToSentence(this string input) { - public static string ToSentence(this string input) - { - var parts = Regex.Split(input, @"([A-Z]?[a-z]+)").Where(str => !string.IsNullOrEmpty(str)); - return string.Join(' ', parts); - } + var parts = Regex.Split(input, @"([A-Z]?[a-z]+)").Where(str => !string.IsNullOrEmpty(str)); + return string.Join(' ', parts); + } - public static string ToId(this string Input) - { - return new string(Input.Split('.').Last()); - } + public static string ToId(this string input) + { + return new string(input.Split('.').Last()); } } \ No newline at end of file diff --git a/SqlServer.Rules.Test/GlobalSuppressions.cs b/SqlServer.Rules.Test/GlobalSuppressions.cs index fc8630e..fa748ac 100644 --- a/SqlServer.Rules.Test/GlobalSuppressions.cs +++ b/SqlServer.Rules.Test/GlobalSuppressions.cs @@ -1,4 +1,4 @@ -// This file is used by Code Analysis to maintain SuppressMessage +// This file is used by Code Analysis to maintain SuppressMessage // attributes that are applied to this project. // Project-level suppressions either have no target or are given // a specific target and scoped to a namespace, type, member, etc. @@ -6,3 +6,5 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Test")] +[assembly: SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "Test")] +[assembly: SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Test")] diff --git a/SqlServer.Rules.Test/Performance/PerformanceTestCases.cs b/SqlServer.Rules.Test/Performance/PerformanceTestCases.cs index fa09ad7..41cbe78 100644 --- a/SqlServer.Rules.Test/Performance/PerformanceTestCases.cs +++ b/SqlServer.Rules.Test/Performance/PerformanceTestCases.cs @@ -3,64 +3,63 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using SqlServer.Rules.Performance; -namespace SqlServer.Rules.Tests.Performance +namespace SqlServer.Rules.Tests.Performance; + +[TestClass] +[TestCategory("Performance")] +public class PerformanceTestCases : TestCasesBase { - [TestClass] - [TestCategory("Performance")] - public class PerformanceTestCases : TestCasesBase + [TestMethod] + public void TestNonSARGablePattern() { - [TestMethod] - public void TestNonSARGablePattern() - { - var problems = GetTestCaseProblems(nameof(AvoidEndsWithOrContainsRule), AvoidEndsWithOrContainsRule.RuleId); + var problems = GetTestCaseProblems(nameof(AvoidEndsWithOrContainsRule), AvoidEndsWithOrContainsRule.RuleId); - Assert.AreEqual(2, problems.Count, "Expected 2 problem to be found"); + Assert.AreEqual(2, problems.Count, "Expected 2 problem to be found"); - Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "nonsargable.sql"))); - Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "nonsargable2.sql"))); + Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "nonsargable.sql"))); + Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "nonsargable2.sql"))); - Assert.IsTrue(problems.All(problem => Comparer.Equals(problem.Description, AvoidEndsWithOrContainsRule.Message))); - Assert.IsTrue(problems.All(problem => problem.Severity == SqlRuleProblemSeverity.Warning)); - } + Assert.IsTrue(problems.All(problem => Comparer.Equals(problem.Description, AvoidEndsWithOrContainsRule.Message))); + Assert.IsTrue(problems.All(problem => problem.Severity == SqlRuleProblemSeverity.Warning)); + } - [TestMethod] - public void TestNotEqualToRule() - { - var problems = GetTestCaseProblems(nameof(AvoidNotEqualToRule), AvoidNotEqualToRule.RuleId); + [TestMethod] + public void TestNotEqualToRule() + { + var problems = GetTestCaseProblems(nameof(AvoidNotEqualToRule), AvoidNotEqualToRule.RuleId); - Assert.AreEqual(2, problems.Count, "Expected 2 problem to be found"); + Assert.AreEqual(2, problems.Count, "Expected 2 problem to be found"); - Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "ansi_not_equal.sql"))); - Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "alternate_not_equal.sql"))); + Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "ansi_not_equal.sql"))); + Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "alternate_not_equal.sql"))); - Assert.IsTrue(problems.All(problem => Comparer.Equals(problem.Description, AvoidNotEqualToRule.Message))); - Assert.IsTrue(problems.All(problem => problem.Severity == SqlRuleProblemSeverity.Warning)); - } + Assert.IsTrue(problems.All(problem => Comparer.Equals(problem.Description, AvoidNotEqualToRule.Message))); + Assert.IsTrue(problems.All(problem => problem.Severity == SqlRuleProblemSeverity.Warning)); + } - [TestMethod] - public void TestAvoidCalcOnColumn() - { - var problems = GetTestCaseProblems(nameof(AvoidColumnCalcsRule), AvoidColumnCalcsRule.RuleId); + [TestMethod] + public void TestAvoidCalcOnColumn() + { + var problems = GetTestCaseProblems(nameof(AvoidColumnCalcsRule), AvoidColumnCalcsRule.RuleId); - Assert.AreEqual(1, problems.Count, "Expected 1 problem to be found"); + Assert.AreEqual(1, problems.Count, "Expected 1 problem to be found"); - Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "calc_on_column.sql"))); + Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "calc_on_column.sql"))); - Assert.IsTrue(problems.All(problem => Comparer.Equals(problem.Description, AvoidColumnCalcsRule.Message))); - Assert.IsTrue(problems.All(problem => problem.Severity == SqlRuleProblemSeverity.Warning)); - } + Assert.IsTrue(problems.All(problem => Comparer.Equals(problem.Description, AvoidColumnCalcsRule.Message))); + Assert.IsTrue(problems.All(problem => problem.Severity == SqlRuleProblemSeverity.Warning)); + } - [TestMethod] - public void TestAvoidColumnFunctionsRule() - { - var problems = GetTestCaseProblems(nameof(AvoidColumnFunctionsRule), AvoidColumnFunctionsRule.RuleId); + [TestMethod] + public void TestAvoidColumnFunctionsRule() + { + var problems = GetTestCaseProblems(nameof(AvoidColumnFunctionsRule), AvoidColumnFunctionsRule.RuleId); - Assert.AreEqual(1, problems.Count, "Expected 1 problem to be found"); + Assert.AreEqual(1, problems.Count, "Expected 1 problem to be found"); - Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "func_on_column.sql"))); + Assert.IsTrue(problems.Any(problem => Comparer.Equals(problem.SourceName, "func_on_column.sql"))); - Assert.IsTrue(problems.All(problem => Comparer.Equals(problem.Description, AvoidColumnFunctionsRule.Message))); - Assert.IsTrue(problems.All(problem => problem.Severity == SqlRuleProblemSeverity.Warning)); - } + Assert.IsTrue(problems.All(problem => Comparer.Equals(problem.Description, AvoidColumnFunctionsRule.Message))); + Assert.IsTrue(problems.All(problem => problem.Severity == SqlRuleProblemSeverity.Warning)); } } diff --git a/SqlServer.Rules.Test/Properties/AssemblyInfo.cs b/SqlServer.Rules.Test/Properties/AssemblyInfo.cs deleted file mode 100644 index d55bc72..0000000 --- a/SqlServer.Rules.Test/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("SqlServer.Rules.Test")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("SqlServer.Rules.Test")] -[assembly: AssemblyCopyright("Copyright © 2021")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -[assembly: ComVisible(false)] - -[assembly: Guid("54816bb2-6952-4573-ba7c-b387d86d5425")] - -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SqlServer.Rules.Test/RuleTestUtils.cs b/SqlServer.Rules.Test/RuleTestUtils.cs index 6e7289f..839dd24 100644 --- a/SqlServer.Rules.Test/RuleTestUtils.cs +++ b/SqlServer.Rules.Test/RuleTestUtils.cs @@ -27,40 +27,39 @@ using System.IO; -namespace SqlServer.Rules.Test +namespace SqlServer.Rules.Test; + +internal sealed class RuleTestUtils { - internal sealed class RuleTestUtils - { - public static void SaveStringToFile(string contents, string filename) + public static void SaveStringToFile(string contents, string filename) + { + FileStream fileStream = null; + StreamWriter streamWriter = null; + try { - FileStream fileStream = null; - StreamWriter streamWriter = null; - try + var directory = Path.GetDirectoryName(filename); + if (!Directory.Exists(directory)) { - var directory = Path.GetDirectoryName(filename); - if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - fileStream = new FileStream(filename, FileMode.Create); - streamWriter = new StreamWriter(fileStream); - streamWriter.Write(contents); - } - finally - { - streamWriter?.Close(); - fileStream?.Close(); + Directory.CreateDirectory(directory); } + + fileStream = new FileStream(filename, FileMode.Create); + streamWriter = new StreamWriter(fileStream); + streamWriter.Write(contents); } + finally + { + streamWriter?.Close(); + fileStream?.Close(); + } + } - public static string ReadFileToString(string filePath) + public static string ReadFileToString(string filePath) + { + using (var reader = new StreamReader(filePath)) { - using (var reader = new StreamReader(filePath)) - { - return reader.ReadToEnd(); - } + return reader.ReadToEnd(); } } } \ No newline at end of file diff --git a/SqlServer.Rules.Test/SqlServer.Rules.Tests.csproj b/SqlServer.Rules.Test/SqlServer.Rules.Tests.csproj index 2bbe09b..881bb94 100644 --- a/SqlServer.Rules.Test/SqlServer.Rules.Tests.csproj +++ b/SqlServer.Rules.Test/SqlServer.Rules.Tests.csproj @@ -1,7 +1,6 @@ - + net8.0 - false @@ -22,4 +21,8 @@ + + + + \ No newline at end of file diff --git a/SqlServer.Rules.Test/TestCasesBase.cs b/SqlServer.Rules.Test/TestCasesBase.cs index 901eb30..997a8a0 100644 --- a/SqlServer.Rules.Test/TestCasesBase.cs +++ b/SqlServer.Rules.Test/TestCasesBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using Microsoft.SqlServer.Dac.CodeAnalysis; @@ -6,33 +6,32 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using SqlServer.Rules.Test; -namespace SqlServer.Rules.Tests +namespace SqlServer.Rules.Tests; + +[TestClass] +public class TestCasesBase { - [TestClass] - public class TestCasesBase - { - protected const SqlServerVersion SqlVersion = SqlServerVersion.Sql150; - protected StringComparer Comparer = StringComparer.OrdinalIgnoreCase; + protected const SqlServerVersion SqlVersion = SqlServerVersion.Sql150; + public StringComparer Comparer { get; private set; } = StringComparer.OrdinalIgnoreCase; - public virtual TestContext TestContext { get; set; } + public virtual TestContext TestContext { get; set; } - protected ReadOnlyCollection GetTestCaseProblems(string testCases, string ruleId) - { - var problems = new ReadOnlyCollection(new List()); + protected ReadOnlyCollection GetTestCaseProblems(string testCases, string ruleId) + { + var problems = new ReadOnlyCollection(new List()); - using (var test = new BaselineSetup(TestContext, testCases, new TSqlModelOptions(), SqlVersion)) + using (var test = new BaselineSetup(TestContext, testCases, new TSqlModelOptions(), SqlVersion)) + { + try { - try - { - test.RunTest(ruleId, (result, problemString) => problems = result.Problems); - } - catch (Exception ex) - { - Assert.Fail($"Exception thrown for ruleId '{ruleId}' for test cases '{testCases}': {ex.Message}"); - } + test.RunTest(ruleId, (result, problemString) => problems = result.Problems); + } + catch (Exception ex) + { + Assert.Fail($"Exception thrown for ruleId '{ruleId}' for test cases '{testCases}': {ex.Message}"); } - - return problems; } + + return problems; } } \ No newline at end of file diff --git a/SqlServer.Rules.Test/Utils/CommonConstants.cs b/SqlServer.Rules.Test/Utils/CommonConstants.cs index 3c6d49b..4a675d6 100644 --- a/SqlServer.Rules.Test/Utils/CommonConstants.cs +++ b/SqlServer.Rules.Test/Utils/CommonConstants.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // // The MIT License (MIT) @@ -25,14 +25,13 @@ // //------------------------------------------------------------------------------ -namespace SqlServer.Rules.Tests.Utils +namespace SqlServer.Rules.Tests.Utils; + +internal sealed class CommonConstants { - internal class CommonConstants - { - public const string MasterDatabaseName = "master"; + public const string MasterDatabaseName = "master"; - public const int DefaultSqlQueryTimeoutInSeconds = 60; + public const int DefaultSqlQueryTimeoutInSeconds = 60; - public const int DefaultCommandTimeout = 30; - } + public const int DefaultCommandTimeout = 30; } \ No newline at end of file diff --git a/SqlServer.Rules.Test/Utils/DisposableList.cs b/SqlServer.Rules.Test/Utils/DisposableList.cs index 3da6dbb..4cf76c1 100644 --- a/SqlServer.Rules.Test/Utils/DisposableList.cs +++ b/SqlServer.Rules.Test/Utils/DisposableList.cs @@ -1,42 +1,41 @@ using System; using System.Collections.Generic; -namespace SqlServer.Rules.Tests.Utils +namespace SqlServer.Rules.Tests.Utils; + +/// +/// Utility class for tracking and disposing of objects that implement IDisposable. +/// +/// Original Source: https://github.com/microsoft/DACExtensions/blob/master/Samples/DisposableList.cs +/// +public sealed class DisposableList : List, IDisposable { /// - /// Utility class for tracking and disposing of objects that implement IDisposable. - /// - /// Original Source: https://github.com/microsoft/DACExtensions/blob/master/Samples/DisposableList.cs + /// Disposes of all elements of list. /// - public sealed class DisposableList : List, IDisposable + public void Dispose() { - /// - /// Disposes of all elements of list. - /// - public void Dispose() - { - Dispose(true); - } + Dispose(true); + } - /// - /// Internal implementation of Dispose logic. - /// - private void Dispose(bool isDisposing) + /// + /// Internal implementation of Dispose logic. + /// + private void Dispose(bool isDisposing) + { + foreach (var disposable in this) { - foreach (var disposable in this) - { - disposable.Dispose(); - } + disposable.Dispose(); } + } - /// - /// Add an item to the list. - /// - public T Add(T item) where T : IDisposable - { - base.Add(item); + /// + /// Add an item to the list. + /// + public T Add(T item) where T : IDisposable + { + base.Add(item); - return item; - } + return item; } } \ No newline at end of file diff --git a/SqlServer.Rules.Test/Utils/ExceptionText.cs b/SqlServer.Rules.Test/Utils/ExceptionText.cs index f41c51a..ed6b176 100644 --- a/SqlServer.Rules.Test/Utils/ExceptionText.cs +++ b/SqlServer.Rules.Test/Utils/ExceptionText.cs @@ -1,32 +1,31 @@ using System; using System.Text; -namespace SqlServer.Rules.Tests.Utils +namespace SqlServer.Rules.Tests.Utils; + +public static class ExceptionText { - public static class ExceptionText + public static string GetText(Exception ex, bool stackTrace = false) { - public static string GetText(Exception ex, bool stackTrace = false) + var sb = new StringBuilder(); + var depth = 0; + while (ex != null) { - var sb = new StringBuilder(); - var depth = 0; - while (ex != null) + if (depth > 0) { - if (depth > 0) - { - sb.Append("Inner Exception: "); - } - - sb.AppendLine(ex.Message); - if (stackTrace) - { - sb.AppendLine(ex.StackTrace); - } + sb.Append("Inner Exception: "); + } - ex = ex.InnerException; - ++depth; + sb.AppendLine(ex.Message); + if (stackTrace) + { + sb.AppendLine(ex.StackTrace); } - return sb.ToString(); + ex = ex.InnerException; + ++depth; } + + return sb.ToString(); } } \ No newline at end of file diff --git a/SqlServer.Rules.Test/Utils/InstanceInfo.cs b/SqlServer.Rules.Test/Utils/InstanceInfo.cs index 00bac44..4f63aa7 100644 --- a/SqlServer.Rules.Test/Utils/InstanceInfo.cs +++ b/SqlServer.Rules.Test/Utils/InstanceInfo.cs @@ -29,139 +29,138 @@ using System.Globalization; using Microsoft.Data.SqlClient; -namespace SqlServer.Rules.Tests.Utils +namespace SqlServer.Rules.Tests.Utils; + +public class InstanceInfo { - public class InstanceInfo + public InstanceInfo(string dataSource) { - public InstanceInfo(string dataSource) - { - DataSource = dataSource; - } + DataSource = dataSource; + } - // Persisted data properties - public string DataSource { get; set; } + // Persisted data properties + public string DataSource { get; set; } - public string RemoteSharePath { get; set; } + public string RemoteSharePath { get; set; } - public int ConnectTimeout { get; set; } + public int ConnectTimeout { get; set; } - public string ConnectTimeoutAsString + public string ConnectTimeoutAsString + { + get { return ConnectTimeout.ToString(CultureInfo.InvariantCulture); } + set { - get { return ConnectTimeout.ToString(CultureInfo.InvariantCulture); } - set + int temp; + if (int.TryParse(value, out temp)) + { + ConnectTimeout = temp; + } + else { - int temp; - if (int.TryParse(value, out temp)) - { - ConnectTimeout = temp; - } - else - { - ConnectTimeout = 15; - } + ConnectTimeout = 15; } } + } - public string MachineName + public string MachineName + { + get { - get + var serverName = DataSource; + var index = DataSource.IndexOf('\\', StringComparison.OrdinalIgnoreCase); + if (index > 0) + { + serverName = DataSource.Substring(0, index); + } + + if (StringComparer.OrdinalIgnoreCase.Compare("(local)", serverName) == 0 + || StringComparer.OrdinalIgnoreCase.Compare(".", serverName) == 0) { - var serverName = DataSource; - var index = DataSource.IndexOf('\\', StringComparison.OrdinalIgnoreCase); - if (index > 0) - { - serverName = DataSource.Substring(0, index); - } - - if (StringComparer.OrdinalIgnoreCase.Compare("(local)", serverName) == 0 - || StringComparer.OrdinalIgnoreCase.Compare(".", serverName) == 0) - { - serverName = Environment.MachineName; - } - - return serverName; + serverName = Environment.MachineName; } + + return serverName; } + } - public string InstanceName + public string InstanceName + { + get { - get + string name = null; + var index = DataSource.IndexOf('\\', StringComparison.OrdinalIgnoreCase); + if (index > 0) { - string name = null; - var index = DataSource.IndexOf('\\', StringComparison.OrdinalIgnoreCase); - if (index > 0) - { - name = DataSource.Substring(index + 1); - } - - return name; + name = DataSource.Substring(index + 1); } + + return name; } + } - public string UserId { get; set; } - public string Password { get; set; } + public string UserId { get; set; } + public string Password { get; set; } - /// - /// Connection string to this instance with the master database as the default. - /// Integrated security is used - /// - /// - public string BuildConnectionString() - { - return CreateBuilder().ConnectionString; - } + /// + /// Connection string to this instance with the master database as the default. + /// Integrated security is used + /// + /// + public string BuildConnectionString() + { + return CreateBuilder().ConnectionString; + } - public SqlConnectionStringBuilder CreateBuilder() - { - return CreateBuilder(CommonConstants.MasterDatabaseName); - } + public SqlConnectionStringBuilder CreateBuilder() + { + return CreateBuilder(CommonConstants.MasterDatabaseName); + } - public string BuildConnectionString(string dbName) - { - return CreateBuilder(dbName).ConnectionString; - } + public string BuildConnectionString(string dbName) + { + return CreateBuilder(dbName).ConnectionString; + } + + public SqlConnectionStringBuilder CreateBuilder(string dbName) + { + return CreateBuilder(UserId, Password, dbName); + } + + /// + /// Build a connection string for this instance using the specified + /// username/password for security and specifying the dbName as the + /// initial catalog + /// + public string BuildConnectionString(string userId, string password, string dbName) + { + var scsb = CreateBuilder(userId, password, dbName); + return scsb.ConnectionString; + } - public SqlConnectionStringBuilder CreateBuilder(string dbName) + public SqlConnectionStringBuilder CreateBuilder(string userId, string password, string dbName) + { + var scsb = new SqlConnectionStringBuilder { + DataSource = DataSource, + InitialCatalog = dbName, + Pooling = false, + MultipleActiveResultSets = false, + }; + if (ConnectTimeout != 15) { - return CreateBuilder(UserId, Password, dbName); + scsb.ConnectTimeout = ConnectTimeout; } - /// - /// Build a connection string for this instance using the specified - /// username/password for security and specifying the dbName as the - /// initial catalog - /// - public string BuildConnectionString(string userId, string password, string dbName) + if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(password)) { - var scsb = CreateBuilder(userId, password, dbName); - return scsb.ConnectionString; + scsb.IntegratedSecurity = true; } - - public SqlConnectionStringBuilder CreateBuilder(string userId, string password, string dbName) + else { - var scsb = new SqlConnectionStringBuilder { - DataSource = DataSource, - InitialCatalog = dbName, - Pooling = false, - MultipleActiveResultSets = false, - }; - if (ConnectTimeout != 15) - { - scsb.ConnectTimeout = ConnectTimeout; - } - - if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(password)) - { - scsb.IntegratedSecurity = true; - } - else - { - scsb.IntegratedSecurity = false; - scsb.UserID = userId; - scsb.Password = password; - } - - return scsb; + scsb.IntegratedSecurity = false; + scsb.UserID = userId; + scsb.Password = password; } + + return scsb; } } \ No newline at end of file diff --git a/SqlServer.Rules.Test/Utils/RuleTest.cs b/SqlServer.Rules.Test/Utils/RuleTest.cs index 55bd1bb..10c01ab 100644 --- a/SqlServer.Rules.Test/Utils/RuleTest.cs +++ b/SqlServer.Rules.Test/Utils/RuleTest.cs @@ -10,357 +10,349 @@ using Microsoft.SqlServer.Dac.Model; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace SqlServer.Rules.Tests.Utils +namespace SqlServer.Rules.Tests.Utils; + +/// +/// Runs a test against the - initializes a model, +/// runs analysis and then performs some verification action. This class could be extended to +/// output a results file and compare this to a baseline. +/// +internal class RuleTest : IDisposable { + private DisposableList trash; + /// - /// Runs a test against the - initializes a model, - /// runs analysis and then performs some verification action. This class could be extended to - /// output a results file and compare this to a baseline. + /// What type of target should the test run against? Dacpacs are not backed by scripts, so + /// the model generated from them will be different from a scripted model. In particular the + /// s generated by calling + /// will be generated from the model instead of representing the script contents, or may return null if + /// the is not a top-level type. /// - public class RuleTest : IDisposable + public enum AnalysisTarget { - private DisposableList _trash; - - /// - /// What type of target should the test run against? Dacpacs are not backed by scripts, so - /// the model generated from them will be different from a scripted model. In particular the - /// s generated by calling - /// will be generated from the model instead of representing the script contents, or may return null if - /// the is not a top-level type. - /// - public enum AnalysisTarget - { - PublicModel, - DacpacModel, - Database, - } + PublicModel, + DacpacModel, + Database, + } - public RuleTest(IList> testScripts, TSqlModelOptions databaseOptions, SqlServerVersion sqlVersion) - { - _trash = new DisposableList(); - TestScripts = testScripts; - DatabaseOptions = databaseOptions ?? new TSqlModelOptions(); - SqlVersion = sqlVersion; - } + public RuleTest(IList> testScripts, TSqlModelOptions databaseOptions, SqlServerVersion sqlVersion) + { + trash = new DisposableList(); + TestScripts = testScripts; + DatabaseOptions = databaseOptions ?? new TSqlModelOptions(); + SqlVersion = sqlVersion; + } - public void Dispose() + public void Dispose() + { + if (trash != null) { - if (_trash != null) - { - _trash.Dispose(); - _trash = null; - } + trash.Dispose(); + trash = null; } + } - /// - /// List of tuples representing scripts and the logical source name for those scripts. - /// - public IList> TestScripts { get; set; } + /// + /// List of tuples representing scripts and the logical source name for those scripts. + /// + public IList> TestScripts { get; set; } - /// - /// Update the DatabaseOptions if you wish to test with different properties, such as a different collation. - /// - public TSqlModelOptions DatabaseOptions { get; set; } + /// + /// Update the DatabaseOptions if you wish to test with different properties, such as a different collation. + /// + public TSqlModelOptions DatabaseOptions { get; set; } - /// - /// Version to target the model at - the model will be compiled against that server version, and rules that do not - /// support that version will be ignored - /// - public SqlServerVersion SqlVersion { get; set; } + /// + /// Version to target the model at - the model will be compiled against that server version, and rules that do not + /// support that version will be ignored + /// + public SqlServerVersion SqlVersion { get; set; } - public AnalysisTarget Target { get; set; } + public AnalysisTarget Target { get; set; } - public TSqlModel ModelForAnalysis { get; set; } + public TSqlModel ModelForAnalysis { get; set; } - public string DacpacPath { get; set; } + public string DacpacPath { get; set; } - public string DatabaseName { get; set; } + public string DatabaseName { get; set; } - protected void CreateModelUsingTestScripts() + protected void CreateModelUsingTestScripts() + { + switch (Target) { - switch (Target) - { - case AnalysisTarget.Database: - ModelForAnalysis = CreateDatabaseModel(); - break; - case AnalysisTarget.DacpacModel: - var scriptedModel = CreateScriptedModel(); - ModelForAnalysis = CreateDacpacModel(scriptedModel); - scriptedModel.Dispose(); - break; - default: - ModelForAnalysis = CreateScriptedModel(); - break; - } - - _trash.Add(ModelForAnalysis); + case AnalysisTarget.Database: + ModelForAnalysis = CreateDatabaseModel(); + break; + case AnalysisTarget.DacpacModel: + var scriptedModel = CreateScriptedModel(); + ModelForAnalysis = CreateDacpacModel(scriptedModel); + scriptedModel.Dispose(); + break; + default: + ModelForAnalysis = CreateScriptedModel(); + break; } - private TSqlModel CreateScriptedModel() - { - var model = new TSqlModel(SqlVersion, DatabaseOptions); - AddScriptsToModel(model); - AssertModelValid(model); - - // Used to load the model from a dacpac, letting us use LoadAsScriptBackedModel option - // string fileName = $"{Path.GetTempFileName()}.dacpac"; - // DacPackageExtensions.BuildPackage(fileName, model, new PackageMetadata()); - - // model = TSqlModel.LoadFromDacpac( - // fileName - // , new ModelLoadOptions() - // { - // LoadAsScriptBackedModel = true, - // ModelStorageType = Microsoft.SqlServer.Dac.DacSchemaModelStorageType.Memory - // }); - - return model; - } + trash.Add(ModelForAnalysis); + } - private static void AssertModelValid(TSqlModel model) + private TSqlModel CreateScriptedModel() + { + var model = new TSqlModel(SqlVersion, DatabaseOptions); + AddScriptsToModel(model); + AssertModelValid(model); + + // Used to load the model from a dacpac, letting us use LoadAsScriptBackedModel option + // string fileName = $"{Path.GetTempFileName()}.dacpac"; + // DacPackageExtensions.BuildPackage(fileName, model, new PackageMetadata()); + + // model = TSqlModel.LoadFromDacpac( + // fileName + // , new ModelLoadOptions() + // { + // LoadAsScriptBackedModel = true, + // ModelStorageType = Microsoft.SqlServer.Dac.DacSchemaModelStorageType.Memory + // }); + + return model; + } + + private static void AssertModelValid(TSqlModel model) + { + var breakingIssuesFound = false; + var validationMessages = model.Validate(); + if (validationMessages.Count > 0) { - var breakingIssuesFound = false; - var validationMessages = model.Validate(); - if (validationMessages.Count > 0) + Console.WriteLine("Issues found during model build:"); + foreach (var message in validationMessages) { - Console.WriteLine("Issues found during model build:"); - foreach (var message in validationMessages) - { - Console.WriteLine("\t" + message.Message); - breakingIssuesFound = breakingIssuesFound || message.MessageType == DacMessageType.Error; - } + Console.WriteLine("\t" + message.Message); + breakingIssuesFound = breakingIssuesFound || message.MessageType == DacMessageType.Error; } - - Assert.IsFalse(breakingIssuesFound, "Cannot run analysis if there are model errors"); } - /// - /// Deploys test scripts to a database and creates a model directly against this DB. - /// Since this is a RuleTest we load the model as script backed to ensure that we have file names, - /// source code positions, and that programmability objects (stored procedures, views) have a full SQLDOM - /// syntax tree instead of just a snippet. - /// - private TSqlModel CreateDatabaseModel() - { - ArgumentValidation.CheckForEmptyString(DatabaseName, "DatabaseName"); - var db = TestUtils.CreateTestDatabase(TestUtils.DefaultInstanceInfo, DatabaseName); - _trash.Add(db); - - TestUtils.ExecuteNonQuery(db, TestScripts.Select(t => t.Item1).SelectMany(TestUtils.GetBatches).ToList()); - - var model = TSqlModel.LoadFromDatabase(db.BuildConnectionString(), new ModelExtractOptions { LoadAsScriptBackedModel = true }); - AssertModelValid(model); - return model; - } + Assert.IsFalse(breakingIssuesFound, "Cannot run analysis if there are model errors"); + } - /// - /// Builds a dacpac and returns the path to that dacpac. - /// If the file already exists it will be deleted - /// - private string BuildDacpacFromModel(TSqlModel model) - { - var path = DacpacPath; - Assert.IsFalse(string.IsNullOrWhiteSpace(DacpacPath), "DacpacPath must be set if target for analysis is a Dacpac"); + /// + /// Deploys test scripts to a database and creates a model directly against this DB. + /// Since this is a RuleTest we load the model as script backed to ensure that we have file names, + /// source code positions, and that programmability objects (stored procedures, views) have a full SQLDOM + /// syntax tree instead of just a snippet. + /// + private TSqlModel CreateDatabaseModel() + { + ArgumentValidation.CheckForEmptyString(DatabaseName, "DatabaseName"); + var db = TestUtils.CreateTestDatabase(TestUtils.DefaultInstanceInfo, DatabaseName); + trash.Add(db); - if (File.Exists(path)) - { - File.Delete(path); - } + TestUtils.ExecuteNonQuery(db, TestScripts.Select(t => t.Item1).SelectMany(TestUtils.GetBatches).ToList()); - var dacpacDir = Path.GetDirectoryName(path); - if (!Directory.Exists(dacpacDir)) - { - Directory.CreateDirectory(dacpacDir); - } + var model = TSqlModel.LoadFromDatabase(db.BuildConnectionString(), new ModelExtractOptions { LoadAsScriptBackedModel = true }); + AssertModelValid(model); + return model; + } - DacPackageExtensions.BuildPackage(path, model, new PackageMetadata()); - return path; - } + /// + /// Builds a dacpac and returns the path to that dacpac. + /// If the file already exists it will be deleted + /// + private string BuildDacpacFromModel(TSqlModel model) + { + var path = DacpacPath; + Assert.IsFalse(string.IsNullOrWhiteSpace(DacpacPath), "DacpacPath must be set if target for analysis is a Dacpac"); - /// - /// Creates a new Dacpac file on disk and returns the model from this. If the file exists already it will be deleted. - /// - /// The generated model will be automatically disposed when the ModelManager is disposed - /// - private TSqlModel CreateDacpacModel(TSqlModel model) + if (File.Exists(path)) { - var dacpacPath = BuildDacpacFromModel(model); - - // Note: when running Code Analysis most rules expect a scripted model. Use the - // static factory method on TSqlModel class to ensure you have scripts. If you - // didn't do this some rules would still work as expected, some would not, and - // a warning message would be included in the AnalysisErrors in the result. - return TSqlModel.LoadFromDacpac(dacpacPath, - new ModelLoadOptions(DacSchemaModelStorageType.Memory, loadAsScriptBackedModel: true)); + File.Delete(path); } - protected void AddScriptsToModel(TSqlModel model) + var dacpacDir = Path.GetDirectoryName(path); + if (!Directory.Exists(dacpacDir)) { - foreach (var tuple in TestScripts) - { - // Item1 = script, Item2 = (logical) source file name - model.AddOrUpdateObjects(tuple.Item1, tuple.Item2, new TSqlObjectOptions()); - } + Directory.CreateDirectory(dacpacDir); } - /// - /// RunTest for multiple scripts. - /// - /// ID of the single rule to be run. All other rules will be disabled - /// Action that runs verification on the result of analysis - public virtual void RunTest(string fullId, Action verify) + DacPackageExtensions.BuildPackage(path, model, new PackageMetadata()); + return path; + } + + /// + /// Creates a new Dacpac file on disk and returns the model from this. If the file exists already it will be deleted. + /// + /// The generated model will be automatically disposed when the ModelManager is disposed + /// + private TSqlModel CreateDacpacModel(TSqlModel model) + { + var dacpacPath = BuildDacpacFromModel(model); + + // Note: when running Code Analysis most rules expect a scripted model. Use the + // static factory method on TSqlModel class to ensure you have scripts. If you + // didn't do this some rules would still work as expected, some would not, and + // a warning message would be included in the AnalysisErrors in the result. + return TSqlModel.LoadFromDacpac(dacpacPath, + new ModelLoadOptions(DacSchemaModelStorageType.Memory, loadAsScriptBackedModel: true)); + } + + protected void AddScriptsToModel(TSqlModel model) + { + foreach (var tuple in TestScripts) { - if (fullId == null) - { - throw new ArgumentNullException(nameof(fullId)); - } + // Item1 = script, Item2 = (logical) source file name + model.AddOrUpdateObjects(tuple.Item1, tuple.Item2, new TSqlObjectOptions()); + } + } - if (fullId == null) - { - throw new ArgumentNullException(nameof(verify)); - } + /// + /// RunTest for multiple scripts. + /// + /// ID of the single rule to be run. All other rules will be disabled + /// Action that runs verification on the result of analysis + public virtual void RunTest(string fullId, Action verify) + { + ArgumentNullException.ThrowIfNull(fullId); + ArgumentNullException.ThrowIfNull(verify); - CreateModelUsingTestScripts(); + CreateModelUsingTestScripts(); - var service = CreateCodeAnalysisService(fullId); + var service = CreateCodeAnalysisService(fullId); - RunRulesAndVerifyResult(service, verify); - } + RunRulesAndVerifyResult(service, verify); + } - /// - /// Sets up the service and disables all rules except the rule you wish to test. - /// - /// If you want all rules to run then do not change the - /// flag, as it is set to "false" by default which - /// ensures that all rules are run. - /// - /// To run some (but not all) of the built-in rules then you could query the - /// method to get a list of all the rules, then set their - /// and other flags as needed, or alternatively call the - /// method to apply whatever rule settings you wish - /// - /// - private CodeAnalysisService CreateCodeAnalysisService(string ruleIdToRun) + /// + /// Sets up the service and disables all rules except the rule you wish to test. + /// + /// If you want all rules to run then do not change the + /// flag, as it is set to "false" by default which + /// ensures that all rules are run. + /// + /// To run some (but not all) of the built-in rules then you could query the + /// method to get a list of all the rules, then set their + /// and other flags as needed, or alternatively call the + /// method to apply whatever rule settings you wish + /// + /// + private CodeAnalysisService CreateCodeAnalysisService(string ruleIdToRun) + { + var factory = new CodeAnalysisServiceFactory(); + var ruleSettings = new CodeAnalysisRuleSettings { - var factory = new CodeAnalysisServiceFactory(); - var ruleSettings = new CodeAnalysisRuleSettings - { - new RuleConfiguration(ruleIdToRun), - }; - ruleSettings.DisableRulesNotInSettings = true; - var service = factory.CreateAnalysisService(ModelForAnalysis.Version, new CodeAnalysisServiceSettings - { - RuleSettings = ruleSettings, - }); + new RuleConfiguration(ruleIdToRun), + }; + ruleSettings.DisableRulesNotInSettings = true; + var service = factory.CreateAnalysisService(ModelForAnalysis.Version, new CodeAnalysisServiceSettings + { + RuleSettings = ruleSettings, + }); - DumpErrors(service.GetRuleLoadErrors()); + DumpErrors(service.GetRuleLoadErrors()); - Assert.IsTrue(service.GetRules().Any(rule => rule.RuleId.Equals(ruleIdToRun, StringComparison.OrdinalIgnoreCase)), - "Expected rule '{0}' not found by the service", ruleIdToRun); - return service; - } + Assert.IsTrue(service.GetRules().Any(rule => rule.RuleId.Equals(ruleIdToRun, StringComparison.OrdinalIgnoreCase)), + "Expected rule '{0}' not found by the service", ruleIdToRun); + return service; + } - private void RunRulesAndVerifyResult(CodeAnalysisService service, Action verify) - { - var analysisResult = service.Analyze(ModelForAnalysis); + private void RunRulesAndVerifyResult(CodeAnalysisService service, Action verify) + { + var analysisResult = service.Analyze(ModelForAnalysis); - // Only considering analysis errors for now - might want to expand to initialization and suppression errors in the future - DumpErrors(analysisResult.AnalysisErrors); + // Only considering analysis errors for now - might want to expand to initialization and suppression errors in the future + DumpErrors(analysisResult.AnalysisErrors); - var problemsString = DumpProblemsToString(analysisResult.Problems); + var problemsString = DumpProblemsToString(analysisResult.Problems); - verify(analysisResult, problemsString); - } + verify(analysisResult, problemsString); + } - private static void DumpErrors(IList errors) + private static void DumpErrors(IList errors) + { + if (errors.Count > 0) { - if (errors.Count > 0) - { - var hasError = false; - var errorMessage = new StringBuilder(); - errorMessage.AppendLine("Errors found:"); + var hasError = false; + var errorMessage = new StringBuilder(); + errorMessage.AppendLine("Errors found:"); - foreach (var error in errors) + foreach (var error in errors) + { + hasError = true; + if (error.Document != null) { - hasError = true; - if (error.Document != null) - { - errorMessage.AppendFormat(CultureInfo.InvariantCulture, "{0}({1}, {2}): ", error.Document, error.Line, error.Column); - } - - errorMessage.AppendLine(error.Message); + errorMessage.AppendFormat(CultureInfo.InvariantCulture, "{0}({1}, {2}): ", error.Document, error.Line, error.Column); } - if (hasError) - { - Assert.Fail(errorMessage.ToString()); - } + errorMessage.AppendLine(error.Message); } - } - private string DumpProblemsToString(IEnumerable problems) - { - var displayServices = ModelForAnalysis.DisplayServices; - List problemList = [..problems]; - - SortProblemsByFileName(problemList); - - var sb = new StringBuilder(); - foreach (var problem in problemList) + if (hasError) { - AppendOneProblemItem(sb, "Problem description", problem.Description); - AppendOneProblemItem(sb, "FullID", problem.RuleId); - AppendOneProblemItem(sb, "Severity", problem.Severity.ToString()); - AppendOneProblemItem(sb, "Model element", displayServices.GetElementName(problem.ModelElement, ElementNameStyle.FullyQualifiedName)); + Assert.Fail(errorMessage.ToString()); + } + } + } - string fileName; - if (problem.SourceName != null) - { - var fileInfo = new FileInfo(problem.SourceName); - fileName = fileInfo.Name; - } - else - { - fileName = string.Empty; - } + private string DumpProblemsToString(IEnumerable problems) + { + var displayServices = ModelForAnalysis.DisplayServices; + List problemList = [..problems]; - AppendOneProblemItem(sb, "Script file", fileName); - AppendOneProblemItem(sb, "Start line", problem.StartLine.ToString(CultureInfo.InvariantCulture)); - AppendOneProblemItem(sb, "Start column", problem.StartColumn.ToString(CultureInfo.InvariantCulture)); + SortProblemsByFileName(problemList); - sb.Append("========end of problem========\r\n\r\n"); + var sb = new StringBuilder(); + foreach (var problem in problemList) + { + AppendOneProblemItem(sb, "Problem description", problem.Description); + AppendOneProblemItem(sb, "FullID", problem.RuleId); + AppendOneProblemItem(sb, "Severity", problem.Severity.ToString()); + AppendOneProblemItem(sb, "Model element", displayServices.GetElementName(problem.ModelElement, ElementNameStyle.FullyQualifiedName)); + + string fileName; + if (problem.SourceName != null) + { + var fileInfo = new FileInfo(problem.SourceName); + fileName = fileInfo.Name; + } + else + { + fileName = string.Empty; } - return sb.ToString(); - } + AppendOneProblemItem(sb, "Script file", fileName); + AppendOneProblemItem(sb, "Start line", problem.StartLine.ToString(CultureInfo.InvariantCulture)); + AppendOneProblemItem(sb, "Start column", problem.StartColumn.ToString(CultureInfo.InvariantCulture)); - private static void AppendOneProblemItem(StringBuilder sb, string name, string content) - { - sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "{0}: {1}", name, content)); + sb.Append("========end of problem========\r\n\r\n"); } - public static void SortProblemsByFileName(List problemList) - { - problemList.Sort(new ProblemComparer()); - } + return sb.ToString(); + } - private class ProblemComparer : IComparer + private static void AppendOneProblemItem(StringBuilder sb, string name, string content) + { + sb.AppendLine(string.Format(CultureInfo.CurrentCulture, "{0}: {1}", name, content)); + } + + public static void SortProblemsByFileName(List problemList) + { + problemList.Sort(new ProblemComparer()); + } + + private sealed class ProblemComparer : IComparer + { + public int Compare(SqlRuleProblem x, SqlRuleProblem y) { - public int Compare(SqlRuleProblem x, SqlRuleProblem y) + var compare = string.Compare(x.SourceName, y.SourceName, StringComparison.OrdinalIgnoreCase); + if (compare == 0) { - var compare = string.Compare(x.SourceName, y.SourceName, StringComparison.CurrentCulture); + compare = x.StartLine - y.StartLine; if (compare == 0) { - compare = x.StartLine - y.StartLine; - if (compare == 0) - { - compare = x.StartColumn - y.StartColumn; - } + compare = x.StartColumn - y.StartColumn; } - - return compare; } + + return compare; } } } diff --git a/SqlServer.Rules.Test/Utils/SqlTestDB.cs b/SqlServer.Rules.Test/Utils/SqlTestDB.cs index 7a1c3c0..ff95777 100644 --- a/SqlServer.Rules.Test/Utils/SqlTestDB.cs +++ b/SqlServer.Rules.Test/Utils/SqlTestDB.cs @@ -34,385 +34,386 @@ using Microsoft.Data.SqlClient; using Microsoft.SqlServer.Dac; -namespace SqlServer.Rules.Tests.Utils +namespace SqlServer.Rules.Tests.Utils; + +/// +/// TestDB manages a database that is used during unit testing. It provides +/// services such as connection strings and attach/detach of the DB from +/// the test database server +/// +public sealed class SqlTestDB : IDisposable { - /// - /// TestDB manages a database that is used during unit testing. It provides - /// services such as connection strings and attach/detach of the DB from - /// the test database server - /// - public class SqlTestDB : IDisposable + public enum ReallyCleanUpDatabase { - public enum ReallyCleanUpDatabase - { - NotIfItCameFromABackupFile, YesReally, - } + NotIfItCameFromABackupFile, YesReally, + } - private readonly InstanceInfo _instance; - private readonly string _dbName; + private readonly InstanceInfo instance; + private readonly string dbName; - // Variables for tracking restored DB information - private bool _cleanupDatabase; - private readonly List _cleanupScripts; - public event EventHandler Disposing; + // Variables for tracking restored DB information + private bool cleanupDatabase; + private readonly List cleanupScripts; + public event EventHandler Disposing; - public static SqlTestDB CreateFromDacpac(InstanceInfo instance, string dacpacPath, DacDeployOptions deployOptions = null, bool dropDatabaseOnCleanup = false) + public static SqlTestDB CreateFromDacpac(InstanceInfo instance, string dacpacPath, DacDeployOptions deployOptions = null, bool dropDatabaseOnCleanup = false) + { + var dbName = Path.GetFileNameWithoutExtension(dacpacPath); + var ds = new DacServices(instance.BuildConnectionString(dbName)); + using (var dp = DacPackage.Load(dacpacPath, DacSchemaModelStorageType.Memory)) { - var dbName = Path.GetFileNameWithoutExtension(dacpacPath); - var ds = new DacServices(instance.BuildConnectionString(dbName)); - using (var dp = DacPackage.Load(dacpacPath, DacSchemaModelStorageType.Memory)) - { - ds.Deploy(dp, dbName, true, deployOptions); - } - - var sqlDb = new SqlTestDB(instance, dbName, dropDatabaseOnCleanup); - return sqlDb; + ds.Deploy(dp, dbName, true, deployOptions); } - public static SqlTestDB CreateFromBacpac(InstanceInfo instance, string bacpacPath, DacImportOptions importOptions = null, bool dropDatabaseOnCleanup = false) - { - var dbName = Path.GetFileNameWithoutExtension(bacpacPath); - var ds = new DacServices(instance.BuildConnectionString(dbName)); - using (var bp = BacPackage.Load(bacpacPath, DacSchemaModelStorageType.Memory)) - { - importOptions = FillDefaultImportOptionsForTest(importOptions); - ds.ImportBacpac(bp, dbName, importOptions); - } - - var sqlDb = new SqlTestDB(instance, dbName, dropDatabaseOnCleanup); - return sqlDb; - } + var sqlDb = new SqlTestDB(instance, dbName, dropDatabaseOnCleanup); + return sqlDb; + } - public static bool TryCreateFromDacpac(InstanceInfo instance, string dacpacPath, out SqlTestDB db, out string error, DacDeployOptions deployOptions = null, bool dropDatabaseOnCleanup = false) + public static SqlTestDB CreateFromBacpac(InstanceInfo instance, string bacpacPath, DacImportOptions importOptions = null, bool dropDatabaseOnCleanup = false) + { + var dbName = Path.GetFileNameWithoutExtension(bacpacPath); + var ds = new DacServices(instance.BuildConnectionString(dbName)); + using (var bp = BacPackage.Load(bacpacPath, DacSchemaModelStorageType.Memory)) { - error = null; - var dbName = string.Empty; - try - { - dbName = Path.GetFileNameWithoutExtension(dacpacPath); - db = CreateFromDacpac(instance, dacpacPath, deployOptions, dropDatabaseOnCleanup); - return true; - } - catch (Exception ex) - { - error = ExceptionText.GetText(ex); - db = null; + importOptions = FillDefaultImportOptionsForTest(importOptions); + ds.ImportBacpac(bp, dbName, importOptions); + } - var dbCreated = SafeDatabaseExists(instance, dbName); - if (dbCreated) - { - db = new SqlTestDB(instance, dbName, dropDatabaseOnCleanup); - } + var sqlDb = new SqlTestDB(instance, dbName, dropDatabaseOnCleanup); + return sqlDb; + } - return false; - } + public static bool TryCreateFromDacpac(InstanceInfo instance, string dacpacPath, out SqlTestDB db, out string error, DacDeployOptions deployOptions = null, bool dropDatabaseOnCleanup = false) + { + error = null; + var dbName = string.Empty; + try + { + dbName = Path.GetFileNameWithoutExtension(dacpacPath); + db = CreateFromDacpac(instance, dacpacPath, deployOptions, dropDatabaseOnCleanup); + return true; } - - public static bool TryCreateFromBacpac(InstanceInfo instance, string bacpacPath, out SqlTestDB db, out string error, DacImportOptions importOptions = null, bool dropDatabaseOnCleanup = false) + catch (Exception ex) { - error = null; - var dbName = string.Empty; - try + error = ExceptionText.GetText(ex); + db = null; + + var dbCreated = SafeDatabaseExists(instance, dbName); + if (dbCreated) { - dbName = Path.GetFileNameWithoutExtension(bacpacPath); - importOptions = FillDefaultImportOptionsForTest(importOptions); - db = CreateFromBacpac(instance, bacpacPath, importOptions, dropDatabaseOnCleanup); - return true; + db = new SqlTestDB(instance, dbName, dropDatabaseOnCleanup); } - catch (Exception ex) - { - error = ExceptionText.GetText(ex); - db = null; - - var dbCreated = SafeDatabaseExists(instance, dbName); - if (dbCreated) - { - db = new SqlTestDB(instance, dbName, dropDatabaseOnCleanup); - } - return false; - } + return false; } + } - private static DacImportOptions FillDefaultImportOptionsForTest(DacImportOptions importOptions) + public static bool TryCreateFromBacpac(InstanceInfo instance, string bacpacPath, out SqlTestDB db, out string error, DacImportOptions importOptions = null, bool dropDatabaseOnCleanup = false) + { + error = null; + var dbName = string.Empty; + try + { + dbName = Path.GetFileNameWithoutExtension(bacpacPath); + importOptions = FillDefaultImportOptionsForTest(importOptions); + db = CreateFromBacpac(instance, bacpacPath, importOptions, dropDatabaseOnCleanup); + return true; + } + catch (Exception ex) { - var result = new DacImportOptions(); + error = ExceptionText.GetText(ex); + db = null; - if (importOptions != null) + var dbCreated = SafeDatabaseExists(instance, dbName); + if (dbCreated) { - result.CommandTimeout = importOptions.CommandTimeout; - result.ImportContributorArguments = importOptions.ImportContributorArguments; - result.ImportContributors = importOptions.ImportContributors; + db = new SqlTestDB(instance, dbName, dropDatabaseOnCleanup); } - return result; + return false; + } + } + + private static DacImportOptions FillDefaultImportOptionsForTest(DacImportOptions importOptions) + { + var result = new DacImportOptions(); + + if (importOptions != null) + { + result.CommandTimeout = importOptions.CommandTimeout; + result.ImportContributorArguments = importOptions.ImportContributorArguments; + result.ImportContributors = importOptions.ImportContributors; } - private static bool SafeDatabaseExists(InstanceInfo instance, string dbName) + return result; + } + + private static bool SafeDatabaseExists(InstanceInfo instance, string dbName) + { + try { - try + using var masterDb = new SqlTestDB(instance, "master"); + using (var connection = masterDb.OpenSqlConnection()) { - var masterDb = new SqlTestDB(instance, "master"); - using (var connection = masterDb.OpenSqlConnection()) + using (var command = connection.CreateCommand()) { - using (var command = connection.CreateCommand()) - { - command.CommandText = string.Format(CultureInfo.CurrentCulture, "select count(*) from sys.databases where [name]='{0}'", dbName); - var result = command.ExecuteScalar(); - int count; - return result != null && int.TryParse(result.ToString(), out count) && count > 0; - } +#pragma warning disable CA2100 // Review SQL queries for security vulnerabilities + command.CommandText = string.Format(CultureInfo.CurrentCulture, "select count(*) from sys.databases where [name]='{0}'", dbName); +#pragma warning restore CA2100 // Review SQL queries for security vulnerabilities + var result = command.ExecuteScalar(); + int count; + return result != null && int.TryParse(result.ToString(), out count) && count > 0; } } - catch (Exception ex) - { - Debug.WriteLine(ex.Message); - return false; - } } - - private SqlTestDB() + catch (Exception ex) { - _cleanupScripts = []; + Debug.WriteLine(ex.Message); + return false; } + } + + private SqlTestDB() + { + cleanupScripts = []; + } - /// - /// Represents a test Database that was created for tests. The DB has already been attached/created, - /// and will not be removed unless dropDatabaseOnCleanup is true. - /// - /// - /// If true the db instance will be dropped when the Cleanup method is called - /// - public SqlTestDB(InstanceInfo instance, string dbName, bool dropDatabaseOnCleanup = false) + /// + /// Represents a test Database that was created for tests. The DB has already been attached/created, + /// and will not be removed unless dropDatabaseOnCleanup is true. + /// + /// + /// If true the db instance will be dropped when the Cleanup method is called + /// + public SqlTestDB(InstanceInfo instance, string dbName, bool dropDatabaseOnCleanup = false) + { + if (string.IsNullOrEmpty(dbName)) { - if (string.IsNullOrEmpty(dbName)) - { - throw new ArgumentOutOfRangeException(nameof(dbName)); - } + throw new ArgumentOutOfRangeException(nameof(dbName)); + } - _instance = instance ?? throw new ArgumentNullException(nameof(instance)); - _dbName = dbName; + this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); + this.dbName = dbName; - _cleanupDatabase = true; - } + cleanupDatabase = true; + } - /// - /// Server name - /// - public string ServerName + /// + /// Server name + /// + public string ServerName + { + get { - get - { - return _instance.DataSource; - } + return instance.DataSource; } + } - /// - /// Database name - /// - public string DatabaseName + /// + /// Database name + /// + public string DatabaseName + { + get { - get - { - return _dbName; - } + return dbName; } + } - /// - /// InstanceInfo - /// - public InstanceInfo Instance + /// + /// InstanceInfo + /// + public InstanceInfo Instance + { + get { - get - { - return _instance; - } + return instance; } + } - public void Dispose() - { - Cleanup(ReallyCleanUpDatabase.NotIfItCameFromABackupFile); + public void Dispose() + { + Cleanup(ReallyCleanUpDatabase.NotIfItCameFromABackupFile); - var h = Disposing; - h?.Invoke(this, EventArgs.Empty); - } + var h = Disposing; + h?.Invoke(this, EventArgs.Empty); + } - /// - /// Build a connection string that can be used to connect to the database - /// - /// - /// A new connection string configured to use the current user's domain credentials to - /// authenticate to the database - /// - public string BuildConnectionString() - { - return CreateBuilder().ConnectionString; - } + /// + /// Build a connection string that can be used to connect to the database + /// + /// + /// A new connection string configured to use the current user's domain credentials to + /// authenticate to the database + /// + public string BuildConnectionString() + { + return CreateBuilder().ConnectionString; + } - public SqlConnectionStringBuilder CreateBuilder() - { - return _instance.CreateBuilder(_dbName); - } + public SqlConnectionStringBuilder CreateBuilder() + { + return instance.CreateBuilder(dbName); + } - public string BuildConnectionString(string userName, string password) - { - return CreateBuilder(userName, password).ConnectionString; - } + public string BuildConnectionString(string userName, string password) + { + return CreateBuilder(userName, password).ConnectionString; + } - public SqlConnectionStringBuilder CreateBuilder(string userName, string password) - { - return _instance.CreateBuilder(userName, password, _dbName); - } + public SqlConnectionStringBuilder CreateBuilder(string userName, string password) + { + return instance.CreateBuilder(userName, password, dbName); + } - /// - /// Retrieve an open connection to the test database - /// - /// An open connection to the - public DbConnection OpenConnection() - { - return OpenSqlConnection(); - } + /// + /// Retrieve an open connection to the test database + /// + /// An open connection to the + public DbConnection OpenConnection() + { + return OpenSqlConnection(); + } - public SqlConnection OpenSqlConnection() - { - var conn = new SqlConnection(_instance.BuildConnectionString(_dbName)); - conn.Open(); - return conn; - } + public SqlConnection OpenSqlConnection() + { + var conn = new SqlConnection(instance.BuildConnectionString(dbName)); + conn.Open(); + return conn; + } - public DbConnection OpenConnection(string userName, string password) - { - var conn = new SqlConnection(_instance.BuildConnectionString(userName, password, _dbName)); - conn.Open(); - return conn; - } + public DbConnection OpenConnection(string userName, string password) + { + var conn = new SqlConnection(instance.BuildConnectionString(userName, password, dbName)); + conn.Open(); + return conn; + } - public void Execute(string script, int? timeout = null) + public void Execute(string script, int? timeout = null) + { + var batches = TestUtils.GetBatches(script); + using (var connection = OpenSqlConnection()) { - var batches = TestUtils.GetBatches(script); - using (var connection = OpenSqlConnection()) + foreach (var batch in batches) { - foreach (var batch in batches) - { - Debug.WriteLine(batch); - TestUtils.Execute(connection, batch, timeout); - } + Debug.WriteLine(batch); + TestUtils.Execute(connection, batch, timeout); } } + } - public void SafeExecute(string script, int? timeout = null) + public void SafeExecute(string script, int? timeout = null) + { + try { - try - { - Execute(script, timeout); - } - catch (Exception ex) - { - var message = string.Format(CultureInfo.CurrentCulture, "Executing script on server '{0}' database '{1}' failed. Error: {2}.\r\n\r\nScript: {3}.)", - Instance.DataSource, DatabaseName, ex.Message, script); - Debug.WriteLine(message); - } + Execute(script, timeout); } - - public void ExtractDacpac(string filePath, IEnumerable> tables = null, DacExtractOptions extractOptions = null) + catch (Exception ex) { - var ds = new DacServices(BuildConnectionString()); - ds.Extract(filePath, DatabaseName, DatabaseName, new Version(1, 0, 0), string.Empty, tables, extractOptions); + var message = string.Format(CultureInfo.CurrentCulture, "Executing script on server '{0}' database '{1}' failed. Error: {2}.\r\n\r\nScript: {3}.)", + Instance.DataSource, DatabaseName, ex.Message, script); + Debug.WriteLine(message); } + } - public bool TryExtractDacpac(string filePath, out string error, IEnumerable> tables = null, DacExtractOptions extractOptions = null) + public void ExtractDacpac(string filePath, IEnumerable> tables = null, DacExtractOptions extractOptions = null) + { + var ds = new DacServices(BuildConnectionString()); + ds.Extract(filePath, DatabaseName, DatabaseName, new Version(1, 0, 0), string.Empty, tables, extractOptions); + } + + public bool TryExtractDacpac(string filePath, out string error, IEnumerable> tables = null, DacExtractOptions extractOptions = null) + { + error = null; + try { - error = null; - try - { - ExtractDacpac(filePath, tables, extractOptions); - return true; - } - catch (Exception ex) - { - error = ex.Message; - return false; - } + ExtractDacpac(filePath, tables, extractOptions); + return true; } - - public void ExportBacpac(string filePath, IEnumerable> tables = null, DacExportOptions extractOptions = null) + catch (Exception ex) { - var ds = new DacServices(BuildConnectionString()); - ds.ExportBacpac(filePath, DatabaseName, extractOptions, tables); + error = ex.Message; + return false; } + } - public bool TryExportBacpac(string filePath, out string error, IEnumerable> tables = null, DacExportOptions exportOptions = null) + public void ExportBacpac(string filePath, IEnumerable> tables = null, DacExportOptions extractOptions = null) + { + var ds = new DacServices(BuildConnectionString()); + ds.ExportBacpac(filePath, DatabaseName, extractOptions, tables); + } + + public bool TryExportBacpac(string filePath, out string error, IEnumerable> tables = null, DacExportOptions exportOptions = null) + { + error = null; + try { - error = null; - try - { - ExportBacpac(filePath, tables, exportOptions); - return true; - } - catch (Exception ex) - { - error = ex.Message; - return false; - } + ExportBacpac(filePath, tables, exportOptions); + return true; } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } - /// - /// Cleanup the DB if it was restored during the testing process. A restoredDB will - /// removed from the server and then .mdf/ldf files deleted from disk - /// - /// ReallyCleanUpDatabase.NotIfItCameFromABackupFile: means to - /// check whether the database came from a backup file or has previously been cleaned. If either - /// of those two things is true, then the database is not cleaned up. - /// - /// ReallyCleanUpDatabase.YesReally: means to clean up the database regardless of its origin. - /// - public void Cleanup(ReallyCleanUpDatabase reallyCleanUpDatabase = ReallyCleanUpDatabase.YesReally) + /// + /// Cleanup the DB if it was restored during the testing process. A restoredDB will + /// removed from the server and then .mdf/ldf files deleted from disk + /// + /// ReallyCleanUpDatabase.NotIfItCameFromABackupFile: means to + /// check whether the database came from a backup file or has previously been cleaned. If either + /// of those two things is true, then the database is not cleaned up. + /// + /// ReallyCleanUpDatabase.YesReally: means to clean up the database regardless of its origin. + /// + public void Cleanup(ReallyCleanUpDatabase reallyCleanUpDatabase = ReallyCleanUpDatabase.YesReally) + { + if (cleanupDatabase || reallyCleanUpDatabase == ReallyCleanUpDatabase.YesReally) { - if (_cleanupDatabase || reallyCleanUpDatabase == ReallyCleanUpDatabase.YesReally) - { - DoCleanup(); - } + DoCleanup(); } + } - private void DoCleanup() + private void DoCleanup() + { + if (cleanupScripts != null && cleanupScripts.Count > 0) { - if (_cleanupScripts != null && _cleanupScripts.Count > 0) + Log("Running cleanup scripts for DB {0}", dbName); + using (var conn = new SqlConnection(instance.BuildConnectionString(dbName))) { - Log("Running cleanup scripts for DB {0}", _dbName); - using (var conn = new SqlConnection(_instance.BuildConnectionString(_dbName))) + conn.Open(); + foreach (var script in cleanupScripts) { - conn.Open(); - foreach (var script in _cleanupScripts) - { - TestUtils.Execute(conn, script); - } + TestUtils.Execute(conn, script); } } - - Log("Deleting DB {0}", _dbName); - try - { - TestUtils.DropDatabase(_instance, _dbName); - } - catch (Exception ex) - { - // We do not want a cleanup failure to block a test's execution result - Log("Exception thrown during cleanup of DB " + _dbName + " " + ex); - } - - _cleanupDatabase = false; } - private static void Log(string format, params object[] args) + Log("Deleting DB {0}", dbName); + try { - Trace.TraceInformation("*** {0} TEST {1}", - DateTime.Now.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture), string.Format(CultureInfo.InvariantCulture, format, args)); + TestUtils.DropDatabase(instance, dbName); } - - internal void AddCleanupScript(string script) + catch (Exception ex) { - _cleanupScripts.Add(script); + // We do not want a cleanup failure to block a test's execution result + Log("Exception thrown during cleanup of DB " + dbName + " " + ex); } + + cleanupDatabase = false; + } + + private static void Log(string format, params object[] args) + { + Trace.TraceInformation("*** {0} TEST {1}", + DateTime.Now.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture), string.Format(CultureInfo.InvariantCulture, format, args)); + } + + internal void AddCleanupScript(string script) + { + cleanupScripts.Add(script); } } \ No newline at end of file diff --git a/SqlServer.Rules.Test/Utils/TestUtils.cs b/SqlServer.Rules.Test/Utils/TestUtils.cs index d1a4147..c9d68fd 100644 --- a/SqlServer.Rules.Test/Utils/TestUtils.cs +++ b/SqlServer.Rules.Test/Utils/TestUtils.cs @@ -34,23 +34,23 @@ using System.Text.RegularExpressions; using Microsoft.Data.SqlClient; -namespace SqlServer.Rules.Tests.Utils +namespace SqlServer.Rules.Tests.Utils; + +/// +/// Utility class for test code. Useful for supporting dropping of databases after tests have completed, +/// verification a particular database exists, etc. +/// +internal static class TestUtils { - /// - /// Utility class for test code. Useful for supporting dropping of databases after tests have completed, - /// verification a particular database exists, etc. - /// - internal static class TestUtils - { - public const string DefaultDataSourceName = "(localdb)\\MSSQLLocalDB"; + public const string DefaultDataSourceName = "(localdb)\\MSSQLLocalDB"; - public const string MasterDatabaseName = "master"; + public const string MasterDatabaseName = "master"; - const string _setLockTimeoutDefault = "SET LOCK_TIMEOUT {0}"; // value configurable + const string SetLockTimeoutDefault = "SET LOCK_TIMEOUT {0}"; // value configurable - const string _queryDatabaseIfExist = @"SELECT COUNT(*) FROM [sys].[databases] WHERE [name] = '{0}'"; + const string QueryDatabaseIfExist = @"SELECT COUNT(*) FROM [sys].[databases] WHERE [name] = '{0}'"; - const string _dropDatabaseIfExist = @" + const string DropDatabaseIfExist = @" IF EXISTS (SELECT 1 FROM [sys].[databases] WHERE [name] = '{0}') BEGIN ALTER DATABASE [{0}] @@ -61,304 +61,303 @@ ALTER DATABASE [{0}] END "; - private const string _dropDatabaseIfExistAzure = @"DROP DATABASE [{0}];"; + private const string DropDatabaseIfExistAzure = @"DROP DATABASE [{0}];"; - private static readonly Regex _batch = new Regex(@"GO\s*$", RegexOptions.Multiline); - private static InstanceInfo _defaultInstanceInfo; + private static readonly Regex Batch = new Regex(@"GO\s*$", RegexOptions.Multiline); + private static InstanceInfo defaultInstanceInfo; - /// - /// Default connection string to LocalDB. Consider extending in the future to allow - /// specification of multiple server versions and paths. - /// - public static string ServerConnectionString - { - get { return "Data Source=" + DefaultDataSourceName + ";Integrated Security=True"; } - } + /// + /// Default connection string to LocalDB. Consider extending in the future to allow + /// specification of multiple server versions and paths. + /// + public static string ServerConnectionString + { + get { return "Data Source=" + DefaultDataSourceName + ";Integrated Security=True"; } + } - public static InstanceInfo DefaultInstanceInfo + public static InstanceInfo DefaultInstanceInfo + { + get { - get + if (defaultInstanceInfo == null) { - if (_defaultInstanceInfo == null) - { - _defaultInstanceInfo = new InstanceInfo(DefaultDataSourceName); - } - - return _defaultInstanceInfo; + defaultInstanceInfo = new InstanceInfo(DefaultDataSourceName); } - } - public static void DropDbAndDeleteFiles(string dbName, string mdfFilePath = null, string ldfFilePath = null) - { - DropDbAndDeleteFiles(ServerConnectionString, dbName, mdfFilePath, ldfFilePath); + return defaultInstanceInfo; } + } - public static void DropDbAndDeleteFiles(string serverName, string dbName, string mdfFilePath = null, string ldfFilePath = null) - { - DropDatabase(serverName, dbName); - DeleteIfExists(mdfFilePath); - DeleteIfExists(ldfFilePath); - } + public static void DropDbAndDeleteFiles(string dbName, string mdfFilePath = null, string ldfFilePath = null) + { + DropDbAndDeleteFiles(ServerConnectionString, dbName, mdfFilePath, ldfFilePath); + } - public static void DeleteIfExists(string filePath) - { - if (!string.IsNullOrWhiteSpace(filePath) - && File.Exists(filePath)) - { - File.Delete(filePath); - } - } + public static void DropDbAndDeleteFiles(string serverName, string dbName, string mdfFilePath = null, string ldfFilePath = null) + { + DropDatabase(serverName, dbName); + DeleteIfExists(mdfFilePath); + DeleteIfExists(ldfFilePath); + } - public static void DropDatabase(InstanceInfo instance, string databaseName, bool displayException = true) + public static void DeleteIfExists(string filePath) + { + if (!string.IsNullOrWhiteSpace(filePath) + && File.Exists(filePath)) { - DropDatabase(instance.BuildConnectionString(CommonConstants.MasterDatabaseName), databaseName, displayException); + File.Delete(filePath); } + } - public static bool DropDatabase( - string connString, - string databaseName, - bool displayException = true, - bool isAzureDb = false) - { - var rc = false; - var retryCount = 1; + public static void DropDatabase(InstanceInfo instance, string databaseName, bool displayException = true) + { + DropDatabase(instance.BuildConnectionString(CommonConstants.MasterDatabaseName), databaseName, displayException); + } + + public static bool DropDatabase( + string connString, + string databaseName, + bool displayException = true, + bool isAzureDb = false) + { + var rc = false; + var retryCount = 1; - for (var i = 0; i < retryCount && rc == false; i++) + for (var i = 0; i < retryCount && rc == false; i++) + { + SqlConnection conn = null; + try { - SqlConnection conn = null; - try + var scsb = new SqlConnectionStringBuilder(connString) { + InitialCatalog = "master", + Pooling = false, + }; + conn = new SqlConnection(scsb.ConnectionString); + conn.Open(); + + if (DoesDatabaseExist(conn, databaseName) == true) { - var scsb = new SqlConnectionStringBuilder(connString) { - InitialCatalog = "master", - Pooling = false, - }; - conn = new SqlConnection(scsb.ConnectionString); - conn.Open(); - - if (DoesDatabaseExist(conn, databaseName) == true) - { - string dropStatement; + string dropStatement; - if (isAzureDb) - { + if (isAzureDb) + { #pragma warning disable CA1863 // Use 'CompositeFormat' - dropStatement = string.Format(CultureInfo.InvariantCulture, - _dropDatabaseIfExistAzure, - databaseName); + dropStatement = string.Format(CultureInfo.InvariantCulture, + DropDatabaseIfExistAzure, + databaseName); #pragma warning restore CA1863 // Use 'CompositeFormat' - // Attempt a retry due to azure instability - retryCount = 2; - } - else - { - conn.ChangeDatabase(MasterDatabaseName); + // Attempt a retry due to azure instability + retryCount = 2; + } + else + { + conn.ChangeDatabase(MasterDatabaseName); #pragma warning disable CA1863 // Use 'CompositeFormat' - dropStatement = string.Format(CultureInfo.InvariantCulture, - _dropDatabaseIfExist, - databaseName); + dropStatement = string.Format(CultureInfo.InvariantCulture, + DropDatabaseIfExist, + databaseName); #pragma warning restore CA1863 // Use 'CompositeFormat' - } - - Execute(conn, dropStatement); } - rc = true; + Execute(conn, dropStatement); } - catch (SqlException exception) + + rc = true; + } + catch (SqlException exception) + { + if (displayException) { - if (displayException) - { - // Capture exception information, but don't fail test. + // Capture exception information, but don't fail test. #pragma warning disable CA1303 // Do not pass literals as localized parameters - Console.WriteLine("Exception while dropping database {0}", databaseName); + Console.WriteLine("Exception while dropping database {0}", databaseName); #pragma warning restore CA1303 // Do not pass literals as localized parameters - Console.WriteLine(exception); - } - } - finally - { - conn?.Close(); + Console.WriteLine(exception); } } - - return rc; + finally + { + conn?.Close(); + } } - public static bool DoesDatabaseExist(SqlConnection connection, string databaseName) - { + return rc; + } + + public static bool DoesDatabaseExist(SqlConnection connection, string databaseName) + { #pragma warning disable CA1863 // Use 'CompositeFormat' - var query = string.Format(CultureInfo.InvariantCulture, _queryDatabaseIfExist, databaseName); + var query = string.Format(CultureInfo.InvariantCulture, QueryDatabaseIfExist, databaseName); #pragma warning restore CA1863 // Use 'CompositeFormat' - var result = (int)ExecuteScalar(connection, query); + var result = (int)ExecuteScalar(connection, query); - return (result == 1); - } + return (result == 1); + } + + public static SqlTestDB CreateTestDatabase(InstanceInfo instance, string dbName) + { + // Cleanup the database if it already exists + DropDatabase(instance, dbName); + + // Create the test database + var createDB = string.Format(CultureInfo.InvariantCulture, "create database [{0}]", dbName); + ExecuteNonQuery(instance, "master", CommonConstants.DefaultCommandTimeout, createDB); + var db = new SqlTestDB(instance, dbName, true); + return db; + } - public static SqlTestDB CreateTestDatabase(InstanceInfo instance, string dbName) + /// + /// Executes the query, and returns the first column of the first row in the + /// result set returned by the query. Extra columns or rows are ignored. + /// + public static object ExecuteScalar(SqlConnection connection, string sqlCommandText, int commandTimeOut = 30) + { + ArgumentValidation.CheckForEmptyString(sqlCommandText, "sqlCommandText"); + + using (var cmd = GetCommandObject(connection, sqlCommandText, commandTimeOut)) { - // Cleanup the database if it already exists - DropDatabase(instance, dbName); - - // Create the test database - var createDB = string.Format(CultureInfo.InvariantCulture, "create database [{0}]", dbName); - ExecuteNonQuery(instance, "master", CommonConstants.DefaultCommandTimeout, createDB); - var db = new SqlTestDB(instance, dbName, true); - return db; + return cmd.ExecuteScalar(); } + } - /// - /// Executes the query, and returns the first column of the first row in the - /// result set returned by the query. Extra columns or rows are ignored. - /// - public static object ExecuteScalar(SqlConnection connection, string sqlCommandText, int commandTimeOut = 30) - { - ArgumentValidation.CheckForEmptyString(sqlCommandText, "sqlCommandText"); + /// + /// Executes commands such as Transact-SQL INSERT, DELETE, UPDATE, and SET statements. + /// + public static void Execute(SqlConnection connection, string sqlCommandText, int? commandTimeOut = null) + { + ArgumentValidation.CheckForEmptyString(sqlCommandText, "sqlCommandText"); - using (var cmd = GetCommandObject(connection, sqlCommandText, commandTimeOut)) - { - return cmd.ExecuteScalar(); - } + if (commandTimeOut == null) + { + // Assume infinite timeout in this case for now + commandTimeOut = 0; } - /// - /// Executes commands such as Transact-SQL INSERT, DELETE, UPDATE, and SET statements. - /// - public static void Execute(SqlConnection connection, string sqlCommandText, int? commandTimeOut = null) + using (var cmd = GetCommandObject(connection, sqlCommandText, commandTimeOut.Value)) { - ArgumentValidation.CheckForEmptyString(sqlCommandText, "sqlCommandText"); - - if (commandTimeOut == null) - { - // Assume infinite timeout in this case for now - commandTimeOut = 0; - } - - using (var cmd = GetCommandObject(connection, sqlCommandText, commandTimeOut.Value)) - { - cmd.ExecuteNonQuery(); - } + cmd.ExecuteNonQuery(); } + } - private static SqlCommand GetCommandObject(SqlConnection conn, string sqlCommandText, int commandTimeOut) - { - var cmd = conn.CreateCommand(); + private static SqlCommand GetCommandObject(SqlConnection conn, string sqlCommandText, int commandTimeOut) + { + var cmd = conn.CreateCommand(); - // reasonable hard code to prevent hang client. - cmd.CommandTimeout = commandTimeOut; + // reasonable hard code to prevent hang client. + cmd.CommandTimeout = commandTimeOut; #pragma warning disable CA2100 // Review SQL queries for security vulnerabilities #pragma warning disable CA1863 // Use 'CompositeFormat' - cmd.CommandText = string.Format(CultureInfo.InvariantCulture, _setLockTimeoutDefault, GetLockTimeoutMS()); + cmd.CommandText = String.Format(CultureInfo.InvariantCulture, SetLockTimeoutDefault, GetLockTimeoutMS()); #pragma warning restore CA1863 // Use 'CompositeFormat' - cmd.ExecuteNonQuery(); - cmd.CommandText = sqlCommandText; + cmd.ExecuteNonQuery(); + cmd.CommandText = sqlCommandText; #pragma warning restore CA2100 // Review SQL queries for security vulnerabilities - return cmd; - } + return cmd; + } - public static void ExecuteNonQuery(SqlTestDB db, int commandTimeout, params string[] scripts) - { - ExecuteNonQuery(db, (IList)scripts, commandTimeout); - } + public static void ExecuteNonQuery(SqlTestDB db, int commandTimeout, params string[] scripts) + { + ExecuteNonQuery(db, (IList)scripts, commandTimeout); + } - public static void ExecuteNonQuery(SqlTestDB db, params string[] scripts) - { - ExecuteNonQuery(db, (IList)scripts); - } + public static void ExecuteNonQuery(SqlTestDB db, params string[] scripts) + { + ExecuteNonQuery(db, (IList)scripts); + } - public static void ExecuteNonQuery(SqlTestDB db, IList scripts, int commandTimeout = CommonConstants.DefaultCommandTimeout) - { - ExecuteNonQuery(db.Instance, db.DatabaseName, scripts, commandTimeout); - } + public static void ExecuteNonQuery(SqlTestDB db, IList scripts, int commandTimeout = CommonConstants.DefaultCommandTimeout) + { + ExecuteNonQuery(db.Instance, db.DatabaseName, scripts, commandTimeout); + } - public static void ExecuteNonQuery(InstanceInfo instance, string dbName, int commandTimeout, params string[] scripts) - { - ExecuteNonQuery(instance, dbName, (IList)scripts, commandTimeout); - } + public static void ExecuteNonQuery(InstanceInfo instance, string dbName, int commandTimeout, params string[] scripts) + { + ExecuteNonQuery(instance, dbName, (IList)scripts, commandTimeout); + } - public static void ExecuteNonQuery(InstanceInfo instance, string dbName, params string[] scripts) - { - ExecuteNonQuery(instance, dbName, (IList)scripts); - } + public static void ExecuteNonQuery(InstanceInfo instance, string dbName, params string[] scripts) + { + ExecuteNonQuery(instance, dbName, (IList)scripts); + } - public static void ExecuteNonQuery(InstanceInfo instance, string dbName, IList scripts, int commandTimeout = CommonConstants.DefaultCommandTimeout) + public static void ExecuteNonQuery(InstanceInfo instance, string dbName, IList scripts, int commandTimeout = CommonConstants.DefaultCommandTimeout) + { + using (var conn = new SqlConnection(instance.BuildConnectionString(dbName))) { - using (var conn = new SqlConnection(instance.BuildConnectionString(dbName))) - { - conn.Open(); + conn.Open(); - foreach (var script in scripts) - { - // Replace SqlCmd variables with actual values - var exeScript = script.Replace("$(DatabaseName)", dbName, StringComparison.OrdinalIgnoreCase); - ExecuteNonQuery(conn, exeScript, commandTimeout); - } + foreach (var script in scripts) + { + // Replace SqlCmd variables with actual values + var exeScript = script.Replace("$(DatabaseName)", dbName, StringComparison.OrdinalIgnoreCase); + ExecuteNonQuery(conn, exeScript, commandTimeout); } } + } - public static void ExecuteNonQuery(SqlConnection conn, string sql, int commandTimeout = CommonConstants.DefaultCommandTimeout) - { + public static void ExecuteNonQuery(SqlConnection conn, string sql, int commandTimeout = CommonConstants.DefaultCommandTimeout) + { - var cmd = conn.CreateCommand(); - try - { - cmd.CommandType = CommandType.Text; - cmd.CommandTimeout = commandTimeout; + var cmd = conn.CreateCommand(); + try + { + cmd.CommandType = CommandType.Text; + cmd.CommandTimeout = commandTimeout; - // Set seven-sets - cmd.CommandText = "SET ANSI_NULLS, ANSI_PADDING, ANSI_WARNINGS, ARITHABORT, CONCAT_NULL_YIELDS_NULL, QUOTED_IDENTIFIER ON;"; - cmd.ExecuteNonQuery(); + // Set seven-sets + cmd.CommandText = "SET ANSI_NULLS, ANSI_PADDING, ANSI_WARNINGS, ARITHABORT, CONCAT_NULL_YIELDS_NULL, QUOTED_IDENTIFIER ON;"; + cmd.ExecuteNonQuery(); - cmd.CommandText = "SET NUMERIC_ROUNDABORT OFF;"; - cmd.ExecuteNonQuery(); + cmd.CommandText = "SET NUMERIC_ROUNDABORT OFF;"; + cmd.ExecuteNonQuery(); #pragma warning disable CA2100 // Review SQL queries for security vulnerabilities - cmd.CommandText = sql; + cmd.CommandText = sql; #pragma warning restore CA2100 // Review SQL queries for security vulnerabilities - cmd.ExecuteNonQuery(); - } - catch (SqlException ex) - { + cmd.ExecuteNonQuery(); + } + catch (SqlException ex) + { #pragma warning disable CA1303 // Do not pass literals as localized parameters - Console.WriteLine("Exception{0}{1}{0}While executing TSQL:{0}{2}", - Environment.NewLine, - ex, - sql); + Console.WriteLine("Exception{0}{1}{0}While executing TSQL:{0}{2}", + Environment.NewLine, + ex, + sql); #pragma warning restore CA1303 // Do not pass literals as localized parameters - throw; - } + throw; } + } - /// - /// Retrieves the default lock timeout in Milliseconds. This value should - /// be used to set the lock timeout on a connection. - /// - /// - private static int GetLockTimeoutMS() - { - // For now defaulting timeout to 90 sec. This could be replaced with a better method for calculating a smart timeout - // To have no timeout, use 0 - var timeoutMS = 90 * 1000; + /// + /// Retrieves the default lock timeout in Milliseconds. This value should + /// be used to set the lock timeout on a connection. + /// + /// + private static int GetLockTimeoutMS() + { + // For now defaulting timeout to 90 sec. This could be replaced with a better method for calculating a smart timeout + // To have no timeout, use 0 + var timeoutMS = 90 * 1000; - return timeoutMS; - } + return timeoutMS; + } - public static IList GetBatches(string script) - { - return _batch.Split(script).Where(s => !string.IsNullOrWhiteSpace(s)).ToList(); - } + public static IList GetBatches(string script) + { + return Batch.Split(script).Where(s => !string.IsNullOrWhiteSpace(s)).ToList(); } +} - internal class ArgumentValidation +internal sealed class ArgumentValidation +{ + public static void CheckForEmptyString(string arg, string argName) { - public static void CheckForEmptyString(string arg, string argName) + if (string.IsNullOrEmpty(arg)) { - if (string.IsNullOrEmpty(arg)) - { - throw new ArgumentException(argName); - } + throw new ArgumentException(argName); } } } \ No newline at end of file diff --git a/TSQLSmellsSSDTTest/Directory.Build.props b/TSQLSmellsSSDTTest/Directory.Build.props deleted file mode 100644 index 7f21e00..0000000 --- a/TSQLSmellsSSDTTest/Directory.Build.props +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/TSQLSmellsSSDTTest/GlobalSuppressions.cs b/TSQLSmellsSSDTTest/GlobalSuppressions.cs new file mode 100644 index 0000000..bd3045b --- /dev/null +++ b/TSQLSmellsSSDTTest/GlobalSuppressions.cs @@ -0,0 +1,9 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Design", "CA1002:Do not expose generic lists", Justification = "Test")] +[assembly: SuppressMessage("Maintainability", "CA1515:Consider making public types internal", Justification = "Test")] diff --git a/TSQLSmellsSSDTTest/Properties/AssemblyInfo.cs b/TSQLSmellsSSDTTest/Properties/AssemblyInfo.cs deleted file mode 100644 index b0f8936..0000000 --- a/TSQLSmellsSSDTTest/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("e6ec4630-a1fe-45d1-b30f-9b289a8fc65f")] diff --git a/TSQLSmellsSSDTTest/TSQLSmellsSSDTTest.csproj b/TSQLSmellsSSDTTest/TSQLSmellsSSDTTest.csproj index dbcacc4..f3737ab 100644 --- a/TSQLSmellsSSDTTest/TSQLSmellsSSDTTest.csproj +++ b/TSQLSmellsSSDTTest/TSQLSmellsSSDTTest.csproj @@ -1,7 +1,6 @@ - + net8.0 - false @@ -15,4 +14,8 @@ + + + + \ No newline at end of file diff --git a/TSQLSmellsSSDTTest/TestModel.cs b/TSQLSmellsSSDTTest/TestModel.cs index 0706791..eb9e037 100644 --- a/TSQLSmellsSSDTTest/TestModel.cs +++ b/TSQLSmellsSSDTTest/TestModel.cs @@ -8,9 +8,9 @@ namespace TSQLSmellsSSDTTest; public class TestModel { - public List ExpectedProblems { get; set; } = []; - public List FoundProblems { get; set; } = []; - public List TestFiles { get; set; } = []; + public List ExpectedProblems { get; private set; } = []; + public List FoundProblems { get; private set; } = []; + public List TestFiles { get; private set; } = []; private TSqlModel Model { get; set; } @@ -39,7 +39,7 @@ public void SerializeResultOutput(CodeAnalysisResult result) foreach (var Problem in result.Problems) { // Only concern ourselves with our problems - if (Problem.RuleId.StartsWith("Smells.")) + if (Problem.RuleId.StartsWith("Smells.", System.StringComparison.OrdinalIgnoreCase)) { var TestProblem = new TestProblem(Problem.StartLine, Problem.StartColumn, Problem.RuleId); FoundProblems.Add(TestProblem); diff --git a/TSQLSmellsSSDTTest/TestProblem.cs b/TSQLSmellsSSDTTest/TestProblem.cs index 39cd4b3..c419dee 100644 --- a/TSQLSmellsSSDTTest/TestProblem.cs +++ b/TSQLSmellsSSDTTest/TestProblem.cs @@ -31,6 +31,6 @@ public override bool Equals(object obj) public override int GetHashCode() { - return string.Format(CultureInfo.InvariantCulture, "{0}:{1}:{2}", RuleId, StartColumn, StartLine).GetHashCode(); + return string.Format(CultureInfo.InvariantCulture, "{0}:{1}:{2}", RuleId, StartColumn, StartLine).GetHashCode(StringComparison.OrdinalIgnoreCase); } } diff --git a/TSQLSmellsSSDTTest/UnitTest1.cs b/TSQLSmellsSSDTTest/UnitTest1.cs index 7fafd86..087bf29 100644 --- a/TSQLSmellsSSDTTest/UnitTest1.cs +++ b/TSQLSmellsSSDTTest/UnitTest1.cs @@ -2,6 +2,7 @@ namespace TSQLSmellsSSDTTest; +#pragma warning disable IDE1006 // Naming Styles [TestClass] public class testConvertDate : TestModel { @@ -1040,3 +1041,4 @@ public void DeprecatedTypesSP() RunTest(); } } +#pragma warning restore IDE1006 // Naming Styles diff --git a/docs/table_of_contents.md b/docs/table_of_contents.md index acd1a3b..fcde5c1 100644 --- a/docs/table_of_contents.md +++ b/docs/table_of_contents.md @@ -6,53 +6,53 @@ | Rule Id | Friendly Name | Ignorable | Description | Example? | |----|----|----|----|----| -| [SML001](CodeSmells/SML001.md) | Avoid cross server joins | | Avoid cross server joins | | -| [SML002](CodeSmells/SML002.md) | Best practice is to use two part naming | | Best practice is to use two part naming | | -| [SML003](CodeSmells/SML003.md) | Dirty Reads cause consistency errors | | Dirty Reads cause consistency errors | | -| [SML004](CodeSmells/SML004.md) | Dont Override the optimizer | | Dont Override the optimizer | | -| [SML005](CodeSmells/SML005.md) | Avoid use of 'Select *' | | Avoid use of 'Select *' | | -| [SML006](CodeSmells/SML006.md) | Avoid Explicit Conversion of Columnar data | | Avoid Explicit Conversion of Columnar data | | +| [SML001](CodeSmells/SML001.md) | Avoid cross server joins | | Avoid cross server joins | Yes | +| [SML002](CodeSmells/SML002.md) | Best practice is to use two part naming | | Best practice is to use two part naming | Yes | +| [SML003](CodeSmells/SML003.md) | Dirty Reads cause consistency errors | | Dirty Reads cause consistency errors | Yes | +| [SML004](CodeSmells/SML004.md) | Dont Override the optimizer | | Dont Override the optimizer | Yes | +| [SML005](CodeSmells/SML005.md) | Avoid use of 'Select *' | | Avoid use of 'Select *' | Yes | +| [SML006](CodeSmells/SML006.md) | Avoid Explicit Conversion of Columnar data | | Avoid Explicit Conversion of Columnar data | Yes | | [SML007](CodeSmells/SML007.md) | Avoid use of ordinal positions in ORDER BY Clauses | | Avoid use of ordinal positions in ORDER BY Clauses | | -| [SML008](CodeSmells/SML008.md) | Dont Change DateFormat | | Dont Change DateFormat | | -| [SML009](CodeSmells/SML009.md) | Dont Change DateFirst | | Dont Change DateFirst | | -| [SML010](CodeSmells/SML010.md) | ReadUnCommitted: Dirty reads can cause consistency errors | | ReadUnCommitted: Dirty reads can cause consistency errors | | -| [SML011](CodeSmells/SML011.md) | Single character aliases are poor practice | | Single character aliases are poor practice | | -| [SML012](CodeSmells/SML012.md) | Missing Column specifications on insert | | Missing Column specifications on insert | | -| [SML013](CodeSmells/SML013.md) | CONCAT_NULL_YIELDS_NULL should be on | | CONCAT_NULL_YIELDS_NULL should be on | | -| [SML014](CodeSmells/SML014.md) | ANSI_NULLS should be On | | ANSI_NULLS should be On | | -| [SML015](CodeSmells/SML015.md) | ANSI_PADDING should be On | | ANSI_PADDING should be On | | -| [SML016](CodeSmells/SML016.md) | ANSI_WARNINGS should be On | | ANSI_WARNINGS should be On | | -| [SML017](CodeSmells/SML017.md) | ARITHABORT should be On | | ARITHABORT should be On | | -| [SML018](CodeSmells/SML018.md) | NUMERIC_ROUNDABORT should be Off | | NUMERIC_ROUNDABORT should be Off | | -| [SML019](CodeSmells/SML019.md) | QUOTED_IDENTIFIER should be ON | | QUOTED_IDENTIFIER should be ON | | -| [SML020](CodeSmells/SML020.md) | FORCEPLAN should be OFF | | FORCEPLAN should be OFF | | -| [SML021](CodeSmells/SML021.md) | Use 2 part naming in EXECUTE statements | | Use 2 part naming in EXECUTE statements | | -| [SML022](CodeSmells/SML022.md) | Identity value should be agnostic | | Identity value should be agnostic | | +| [SML008](CodeSmells/SML008.md) | Dont Change DateFormat | | Dont Change DateFormat | Yes | +| [SML009](CodeSmells/SML009.md) | Dont Change DateFirst | | Dont Change DateFirst | Yes | +| [SML010](CodeSmells/SML010.md) | ReadUnCommitted: Dirty reads can cause consistency errors | | ReadUnCommitted: Dirty reads can cause consistency errors | Yes | +| [SML011](CodeSmells/SML011.md) | Single character aliases are poor practice | | Single character aliases are poor practice | Yes | +| [SML012](CodeSmells/SML012.md) | Missing Column specifications on insert | | Missing Column specifications on insert | Yes | +| [SML013](CodeSmells/SML013.md) | CONCAT_NULL_YIELDS_NULL should be on | | CONCAT_NULL_YIELDS_NULL should be on | Yes | +| [SML014](CodeSmells/SML014.md) | ANSI_NULLS should be On | | ANSI_NULLS should be On | Yes | +| [SML015](CodeSmells/SML015.md) | ANSI_PADDING should be On | | ANSI_PADDING should be On | Yes | +| [SML016](CodeSmells/SML016.md) | ANSI_WARNINGS should be On | | ANSI_WARNINGS should be On | Yes | +| [SML017](CodeSmells/SML017.md) | ARITHABORT should be On | | ARITHABORT should be On | Yes | +| [SML018](CodeSmells/SML018.md) | NUMERIC_ROUNDABORT should be Off | | NUMERIC_ROUNDABORT should be Off | Yes | +| [SML019](CodeSmells/SML019.md) | QUOTED_IDENTIFIER should be ON | | QUOTED_IDENTIFIER should be ON | Yes | +| [SML020](CodeSmells/SML020.md) | FORCEPLAN should be OFF | | FORCEPLAN should be OFF | Yes | +| [SML021](CodeSmells/SML021.md) | Use 2 part naming in EXECUTE statements | | Use 2 part naming in EXECUTE statements | Yes | +| [SML022](CodeSmells/SML022.md) | Identity value should be agnostic | | Identity value should be agnostic | Yes | | [SML023](CodeSmells/SML023.md) | Avoid single line comments | | Avoid single line comments | | -| [SML024](CodeSmells/SML024.md) | Use two part naming | | Use two part naming | | -| [SML025](CodeSmells/SML025.md) | RANGE windows are much slower then ROWS (Explicit use) | | RANGE windows are much slower then ROWS (Explicit use) | | -| [SML026](CodeSmells/SML026.md) | RANGE windows are much slower then ROWS (Implicit use) | | RANGE windows are much slower then ROWS (Implicit use) | | -| [SML027](CodeSmells/SML027.md) | Create table statements should specify schema | | Create table statements should specify schema | | -| [SML028](CodeSmells/SML028.md) | Ordering in a view does not guarantee result set ordering | | Ordering in a view does not guarantee result set ordering | | -| [SML029](CodeSmells/SML029.md) | Cursors default to writable. Specify FAST_FORWARD | | Cursors default to writable. Specify FAST_FORWARD | | -| [SML030](CodeSmells/SML030.md) | Include SET NOCOUNT ON inside stored procedures | | Include SET NOCOUNT ON inside stored procedures | | +| [SML024](CodeSmells/SML024.md) | Use two part naming | | Use two part naming | Yes | +| [SML025](CodeSmells/SML025.md) | RANGE windows are much slower then ROWS (Explicit use) | | RANGE windows are much slower then ROWS (Explicit use) | Yes | +| [SML026](CodeSmells/SML026.md) | RANGE windows are much slower then ROWS (Implicit use) | | RANGE windows are much slower then ROWS (Implicit use) | Yes | +| [SML027](CodeSmells/SML027.md) | Create table statements should specify schema | | Create table statements should specify schema | Yes | +| [SML028](CodeSmells/SML028.md) | Ordering in a view does not guarantee result set ordering | | Ordering in a view does not guarantee result set ordering | Yes | +| [SML029](CodeSmells/SML029.md) | Cursors default to writable. Specify FAST_FORWARD | | Cursors default to writable. Specify FAST_FORWARD | Yes | +| [SML030](CodeSmells/SML030.md) | Include SET NOCOUNT ON inside stored procedures | | Include SET NOCOUNT ON inside stored procedures | Yes | | [SML031](CodeSmells/SML031.md) | EXISTS/NOT EXISTS can be more performant than COUNT(*) | | EXISTS/NOT EXISTS can be more performant than COUNT(*) | | | [SML032](CodeSmells/SML032.md) | Ordering in a derived table does not guarantee result set ordering | | Ordering in a derived table does not guarantee result set ordering | | -| [SML033](CodeSmells/SML033.md) | Single character variable names are poor practice | | Single character variable names are poor practice | | -| [SML034](CodeSmells/SML034.md) | Expression used with TOP should be wrapped in parenthises | | Expression used with TOP should be wrapped in parenthises | | +| [SML033](CodeSmells/SML033.md) | Single character variable names are poor practice | | Single character variable names are poor practice | Yes | +| [SML034](CodeSmells/SML034.md) | Expression used with TOP should be wrapped in parenthises | | Expression used with TOP should be wrapped in parenthises | Yes | | [SML035](CodeSmells/SML035.md) | TOP(100) percent is ignored by the optimizer | | TOP(100) percent is ignored by the optimizer | | | [SML036](CodeSmells/SML036.md) | Foreign Key Constraints should be named | | Foreign Key Constraints should be named | | | [SML037](CodeSmells/SML037.md) | Check Constraints should be named | | Check Constraints should be named | | -| [SML038](CodeSmells/SML038.md) | Primary Key Constraints on temporary tables should not be named | | Primary Key Constraints on temporary tables should not be named | | -| [SML039](CodeSmells/SML039.md) | Default Constraints on temporary tables should not be named | | Default Constraints on temporary tables should not be named | | -| [SML040](CodeSmells/SML040.md) | Foreign Key Constraints on temporary tables should not be named | | Foreign Key Constraints on temporary tables should not be named | | +| [SML038](CodeSmells/SML038.md) | Primary Key Constraints on temporary tables should not be named | | Primary Key Constraints on temporary tables should not be named | Yes | +| [SML039](CodeSmells/SML039.md) | Default Constraints on temporary tables should not be named | | Default Constraints on temporary tables should not be named | Yes | +| [SML040](CodeSmells/SML040.md) | Foreign Key Constraints on temporary tables should not be named | | Foreign Key Constraints on temporary tables should not be named | Yes | | [SML041](CodeSmells/SML041.md) | Check Constraints on temporary tables should not be named | | Check Constraints on temporary tables should not be named | | | [SML042](CodeSmells/SML042.md) | Use of SET ROWCOUNT is deprecated : use TOP | | Use of SET ROWCOUNT is deprecated : use TOP | | -| [SML043](CodeSmells/SML043.md) | Potential SQL Injection Issue | | Potential SQL Injection Issue | | -| [SML044](CodeSmells/SML044.md) | Dont override the optimizer ( FORCESCAN ) | | Dont override the optimizer ( FORCESCAN ) | | -| [SML045](CodeSmells/SML045.md) | Dont override the optimizer ( Index Hint) | | Dont override the optimizer ( Index Hint) | | -| [SML046](CodeSmells/SML046.md) | "= Null" Comparison | | "= Null" Comparison | | -| [SML047](CodeSmells/SML047.md) | Use of deprecated data type | | Use of deprecated data type | | +| [SML043](CodeSmells/SML043.md) | Potential SQL Injection Issue | | Potential SQL Injection Issue | Yes | +| [SML044](CodeSmells/SML044.md) | Dont override the optimizer ( FORCESCAN ) | | Dont override the optimizer ( FORCESCAN ) | Yes | +| [SML045](CodeSmells/SML045.md) | Dont override the optimizer ( Index Hint) | | Dont override the optimizer ( Index Hint) | Yes | +| [SML046](CodeSmells/SML046.md) | "= Null" Comparison | | "= Null" Comparison | Yes | +| [SML047](CodeSmells/SML047.md) | Use of deprecated data type | | Use of deprecated data type | Yes | ## Design