From a36de6d96d6b9aebbd8d951873926b6748e8f54f Mon Sep 17 00:00:00 2001 From: Arthur van de Vondervoort Date: Sun, 5 Oct 2025 09:11:57 +0200 Subject: [PATCH] Add NavCodeAnalysisBase class for version-dependent analyzer tests --- README.md | 51 +++ .../Helpers/NavCodeAnalysisBase.cs | 387 ++++++++++++++++++ src/RoslynTestKit/RoslynTestKit.csproj | 1 + 3 files changed, 439 insertions(+) create mode 100644 src/RoslynTestKit/Helpers/NavCodeAnalysisBase.cs diff --git a/README.md b/README.md index 50a1209..b6e330f 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,57 @@ public async Task HasFix(string testCase) } ``` +### NavCodeAnalysisBase +The `NavCodeAnalysisBase` class is a base class for analyzer tests that need to behave differently depending on the version of the AL Language (`Microsoft.Dynamics.Nav.CodeAnalysis`). By inheriting from this class, your tests automatically gain utilities for: +* Detecting the currently loaded Microsoft.Dynamics.Nav.CodeAnalysis assembly version. +* Comparing that version against minimum/maximum requirements. +* Skipping or adjusting tests when features are introduced, changed, or removed between versions. + +```CS +// Example 1: Skip specific test cases based on version +[TestCase("FeatureOne")] +[TestCase("FeatureTwo")] +public void TestFeatures(string testCase) +{ + SkipTestIfVersionIsTooLow( + ["FeatureOne"], + testCase, + "15.0.0" + ); + + SkipTestIfVersionIsTooHigh( + ["FeatureTwo"], + testCase, + "14.9.99" + ); + // Test code +} + +// Example 2: Require minimum version for entire test +[Test] +public void TestModernFeature() +{ + RequireMinimumVersion("15.0.0", "Requires new API"); + // Test code that uses version 15+ features +} + +// Example 3: Test feature that only exists in specific range +[Test] +public void TestTransitionalFeature() +{ + RequireVersionRange("15.0.0", "16.5.0", "Feature deprecated after 16.5"); + // Test code +} + +// Example 4: Ensure version detection worked +[Test] +public void TestVersionDependentBehavior() +{ + RequireVersionDetection(); + // Now safe to use version comparison methods +} +``` + ### Basic folder structure Working with `.al` files instead of declaring the code inline the test method itself, requires a structure. A example for this could be something like this. diff --git a/src/RoslynTestKit/Helpers/NavCodeAnalysisBase.cs b/src/RoslynTestKit/Helpers/NavCodeAnalysisBase.cs new file mode 100644 index 0000000..1a47b0f --- /dev/null +++ b/src/RoslynTestKit/Helpers/NavCodeAnalysisBase.cs @@ -0,0 +1,387 @@ +using System; +using System.Linq; +using System.Reflection; +using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics; +using NUnit.Framework; + +namespace RoslynTestKit +{ + /// + /// NUnit base class that detects the installed Microsoft.Dynamics.Nav.CodeAnalysis (AL DevTools) + /// assembly version and provides helpers to conditionally run or skip analyzer tests based on + /// version constraints. + /// + public abstract class NavCodeAnalysisBase + { + #region Fields + + protected static Version? _navCodeAnalysisVersion; + private static readonly Assembly? _navCodeAnalysisAssembly = Assembly.GetAssembly(typeof(DiagnosticAnalyzer)); + + #endregion + + #region Initialization + + [OneTimeSetUp] + public void BaseSetUp() + { + RetrieveNavCodeAnalysisVersion(); + } + + /// + /// Retrieves the AssemblyFileVersionAttribute from Microsoft.Dynamics.Nav.CodeAnalysis assembly + /// + private static void RetrieveNavCodeAnalysisVersion() + { + var navCodeAnalysisAssembly = _navCodeAnalysisAssembly; + if (navCodeAnalysisAssembly == null) + { + throw new InvalidOperationException("Unable to locate Microsoft.Dynamics.Nav.CodeAnalysis assembly."); + } + + var fileVersionAttribute = navCodeAnalysisAssembly.GetCustomAttribute(); + if (fileVersionAttribute == null) + { + throw new InvalidOperationException("AssemblyFileVersionAttribute not found in Microsoft.Dynamics.Nav.CodeAnalysis assembly."); + } + + if (string.IsNullOrEmpty(fileVersionAttribute.Version)) + { + throw new InvalidOperationException("AssemblyFileVersionAttribute.Version is null or empty in Microsoft.Dynamics.Nav.CodeAnalysis assembly."); + } + + if (!Version.TryParse(fileVersionAttribute.Version, out var parsedVersion)) + { + throw new InvalidOperationException($"Unable to parse AssemblyFileVersionAttribute.Version '{fileVersionAttribute.Version}'."); + } + + _navCodeAnalysisVersion = parsedVersion; + + TestContext.WriteLine($"Microsoft.Dynamics.Nav.CodeAnalysis version: {_navCodeAnalysisVersion}"); + } + + #endregion + + #region Version Information Getters + + /// + /// Gets the current Nav.CodeAnalysis version as a parsed Version object + /// + protected static Version? GetNavCodeAnalysisParsed() => _navCodeAnalysisVersion; + + /// + /// Checks if the Nav.CodeAnalysis version was successfully detected + /// + /// True if version information is available, false otherwise + protected static bool IsVersionDetected() => _navCodeAnalysisVersion != null; + + #endregion + + #region Version Comparison Methods + + private static Version? TryParseVersion(string version) + { + return Version.TryParse(version, out var parsedVersion) ? parsedVersion : null; + } + + /// + /// Checks if the current Nav.CodeAnalysis version is greater than or equal to the specified version + /// + /// The version to compare against (e.g., "15.0.20") + /// True if current version is greater than or equal to the specified version + /// + /// Use when you need features introduced in a specific version or later: + /// if (IsVersionGreaterOrEqual("15.0.20")) { /* use new feature */ } + /// + protected static bool IsVersionGreaterOrEqual(string version) + { + var compareVersion = TryParseVersion(version); + if (compareVersion == null || _navCodeAnalysisVersion == null) + { + return false; + } + + return _navCodeAnalysisVersion >= compareVersion; + } + + /// + /// Checks if the current Nav.CodeAnalysis version is less than the specified version + /// + /// The version to compare against (e.g., "16.0.0") + /// True if current version is less than the specified version + /// + /// Use when excluding tests for newer versions that don't support legacy features: + /// if (IsVersionLessThan("16.0.0")) { /* test legacy behavior */ } + /// + protected static bool IsVersionLessThan(string version) + { + var compareVersion = TryParseVersion(version); + if (compareVersion == null || _navCodeAnalysisVersion == null) + { + return false; + } + + return _navCodeAnalysisVersion < compareVersion; + } + + /// + /// Checks if the current Nav.CodeAnalysis version is greater than the specified version + /// + /// The version to compare against (e.g., "15.0.19") + /// True if current version is greater than the specified version + /// + /// Use when you need versions newer than a specific version (excluding that version): + /// if (IsVersionGreaterThan("15.0.19")) { /* requires newer than 15.0.19 */ } + /// + protected static bool IsVersionGreaterThan(string version) + { + var compareVersion = TryParseVersion(version); + if (compareVersion == null || _navCodeAnalysisVersion == null) + { + return false; + } + + return _navCodeAnalysisVersion > compareVersion; + } + + /// + /// Checks if the current Nav.CodeAnalysis version is less than or equal to the specified version + /// + /// The version to compare against (e.g., "15.0.20") + /// True if current version is less than or equal to the specified version + /// + /// Use when testing features that were deprecated or changed after a specific version: + /// if (IsVersionLessOrEqual("15.0.20")) { /* test behavior up to and including 15.0.20 */ } + /// + protected static bool IsVersionLessOrEqual(string version) + { + var compareVersion = TryParseVersion(version); + if (compareVersion == null || _navCodeAnalysisVersion == null) + { + return false; + } + + return _navCodeAnalysisVersion <= compareVersion; + } + + /// + /// Checks if the current Nav.CodeAnalysis version matches a specific version pattern + /// + /// Major version to match + /// Minor version to match (optional) + /// True if the version matches the pattern + protected static bool IsVersion(int majorVersion, int? minorVersion = null) + { + if (_navCodeAnalysisVersion == null) + { + return false; + } + + if (minorVersion.HasValue) + { + return _navCodeAnalysisVersion.Major == majorVersion && + _navCodeAnalysisVersion.Minor == minorVersion.Value; + } + + return _navCodeAnalysisVersion.Major == majorVersion; + } + + #endregion + + #region Test Helper Methods - Test Case Skipping + + /// + /// Skips test cases that require a minimum version if the current version doesn't meet the requirement + /// + /// Array of test case names that require the minimum version + /// The current test case being executed + /// The minimum required version (e.g., "15.0.0") + /// Optional custom reason for skipping (if null, a default message will be used) + /// + /// Use in test methods to skip version-specific test cases: + /// + /// SkipTestIfVersionIsTooLow( + /// ["TestCase1", "TestCase2"], + /// currentTestCase, + /// "15.0.0", + /// "Feature requires obsolete table support" + /// ); + /// + /// + protected static void SkipTestIfVersionIsTooLow(string[] testCases, string currentTestCase, string minimumVersion, string? reason = null) + { + if (testCases.Contains(currentTestCase) && !IsVersionGreaterOrEqual(minimumVersion)) + { + var message = reason ?? $"Test case requires AL version {minimumVersion} or higher."; + Assert.Ignore(message); + } + } + + /// + /// Skips test cases that require a maximum version if the current version exceeds the requirement + /// + /// Array of test case names that have a maximum version + /// The current test case being executed + /// The maximum supported version (e.g., "16.0.0") + /// Optional custom reason for skipping (if null, a default message will be used) + /// + /// Use in test methods to skip test cases for versions that are too high: + /// + /// SkipTestIfVersionIsTooHigh( + /// ["LegacyTestCase1", "LegacyTestCase2"], + /// currentTestCase, + /// "16.0.0", + /// "Feature was removed in version 16.0.0" + /// ); + /// + /// + protected static void SkipTestIfVersionIsTooHigh(string[] testCases, string currentTestCase, string maximumVersion, string? reason = null) + { + if (testCases.Contains(currentTestCase) && !IsVersionLessOrEqual(maximumVersion)) + { + var message = reason ?? $"Test case requires AL version {maximumVersion} or lower."; + Assert.Ignore(message); + } + } + + /// + /// Skips test cases that require a specific version range if the current version is outside that range + /// + /// Array of test case names that require the version range + /// The current test case being executed + /// The minimum required version (inclusive, e.g., "15.0.0") + /// The maximum supported version (inclusive, e.g., "16.5.0") + /// Optional custom reason for skipping (if null, a default message will be used) + /// + /// Use in test methods to skip test cases that only apply to a specific version range: + /// + /// SkipTestIfVersionOutsideRange( + /// ["SpecificFeatureTest"], + /// currentTestCase, + /// "15.0.0", + /// "16.5.0", + /// "Feature only exists in versions 15.0.0 to 16.5.0" + /// ); + /// + /// + protected static void SkipTestIfVersionOutsideRange(string[] testCases, string currentTestCase, string minimumVersion, string maximumVersion, string? reason = null) + { + if (testCases.Contains(currentTestCase)) + { + var inRange = IsVersionGreaterOrEqual(minimumVersion) && IsVersionLessOrEqual(maximumVersion); + if (!inRange) + { + var message = reason ?? $"Test case requires AL version between {minimumVersion} and {maximumVersion}."; + Assert.Ignore(message); + } + } + } + + #endregion + + #region Test Helper Methods - Whole Test Requirements + + /// + /// Skips the current test if the minimum version requirement is not met + /// + /// The minimum required version (e.g., "15.0.0") + /// Optional custom reason for skipping (if null, a default message will be used) + /// + /// Use at the beginning of a test method to skip if version is too low: + /// + /// [Test] + /// public void TestNewFeature() + /// { + /// RequireMinimumVersion("15.0.0", "Feature requires obsolete table support"); + /// // Test code here + /// } + /// + /// + protected static void RequireMinimumVersion(string minimumVersion, string? reason = null) + { + if (!IsVersionGreaterOrEqual(minimumVersion)) + { + var message = reason ?? $"Test requires AL version {minimumVersion} or higher."; + Assert.Ignore(message); + } + } + + /// + /// Skips the current test if the version exceeds the maximum version + /// + /// The maximum supported version (e.g., "16.0.0") + /// Optional custom reason for skipping (if null, a default message will be used) + /// + /// Use at the beginning of a test method to skip if version is too high: + /// + /// [Test] + /// public void TestLegacyFeature() + /// { + /// RequireMaximumVersion("16.0.0", "Feature was removed in version 16.0.0"); + /// // Test code here + /// } + /// + /// + protected static void RequireMaximumVersion(string maximumVersion, string? reason = null) + { + if (!IsVersionLessOrEqual(maximumVersion)) + { + var message = reason ?? $"Test requires AL version {maximumVersion} or lower."; + Assert.Ignore(message); + } + } + + /// + /// Skips the current test if the version is outside the specified range + /// + /// The minimum required version (inclusive, e.g., "15.0.0") + /// The maximum supported version (inclusive, e.g., "16.5.0") + /// Optional custom reason for skipping (if null, a default message will be used) + /// + /// Use at the beginning of a test method to skip if version is outside range: + /// + /// [Test] + /// public void TestVersionSpecificFeature() + /// { + /// RequireVersionRange("15.0.0", "16.5.0", "Feature only exists in this version range"); + /// // Test code here + /// } + /// + /// + protected static void RequireVersionRange(string minimumVersion, string maximumVersion, string? reason = null) + { + var inRange = IsVersionGreaterOrEqual(minimumVersion) && IsVersionLessOrEqual(maximumVersion); + if (!inRange) + { + var message = reason ?? $"Test requires AL version between {minimumVersion} and {maximumVersion}."; + Assert.Ignore(message); + } + } + + /// + /// Skips the current test if the Nav.CodeAnalysis version could not be detected + /// + /// Optional custom reason for skipping (if null, a default message will be used) + /// + /// Use at the beginning of a test method that absolutely requires version information: + /// + /// [Test] + /// public void TestVersionDependentFeature() + /// { + /// RequireVersionDetection("This test requires version detection to work properly"); + /// // Test code here + /// } + /// + /// + protected static void RequireVersionDetection(string? reason = null) + { + if (_navCodeAnalysisVersion == null) + { + var message = reason ?? "Test requires Nav.CodeAnalysis version detection to be successful."; + Assert.Ignore(message); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/RoslynTestKit/RoslynTestKit.csproj b/src/RoslynTestKit/RoslynTestKit.csproj index 69ecd6c..767c537 100644 --- a/src/RoslynTestKit/RoslynTestKit.csproj +++ b/src/RoslynTestKit/RoslynTestKit.csproj @@ -17,6 +17,7 @@ +