From 0570e0e02b7afa51ed049de6ee89719a176a269f Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 24 Sep 2024 03:19:15 +1000 Subject: [PATCH] Fix CLI module versions and version pre-release handling #2557 (#2558) --- PSRule.sln | 9 +- docs/CHANGELOG-v3.md | 11 ++ .../PSRule/en-US/about_PSRule_Assert.md | 56 +++--- docs/upgrade-notes.md | 37 ++++ .../Commands/ModuleCommand.cs | 14 +- .../Properties/launchSettings.json | 9 +- src/PSRule.Types/Data/DateVersion.cs | 88 ++++++--- .../Data/IDateVersionConstraint.cs | 15 ++ .../Data/ISemanticVersionConstraint.cs | 15 ++ src/PSRule.Types/Data/ModuleConstraint.cs | 41 ++-- src/PSRule.Types/Data/SemanticVersion.cs | 164 +++++++++++----- .../Expressions/LanguageExpressions.cs | 8 +- src/PSRule/Pipeline/PipelineBuilderBase.cs | 2 +- .../Resources/ReasonStrings.Designer.cs | 4 +- src/PSRule/Resources/ReasonStrings.resx | 2 +- src/PSRule/Runtime/Assert.cs | 4 +- .../ModuleConstraintTests.cs | 13 ++ .../PSRule.CommandLine.Tests.csproj | 4 + tests/PSRule.Tests/DateVersionTests.cs | 114 +++++------ tests/PSRule.Tests/SelectorTests.cs | 27 ++- tests/PSRule.Tests/Selectors.Rule.jsonc | 30 +++ tests/PSRule.Tests/Selectors.Rule.yaml | 24 +++ tests/PSRule.Tests/SemanticVersionTests.cs | 180 ++++++++++-------- tests/PSRule.Tests/TestEnumValue.cs | 11 ++ .../Data/ModuleConstraintTests.cs | 42 ++++ tests/PSRule.Types.Tests/GlobalUsings.cs | 4 + .../PSRule.Types.Tests.csproj | 28 +++ 27 files changed, 677 insertions(+), 279 deletions(-) create mode 100644 src/PSRule.Types/Data/IDateVersionConstraint.cs create mode 100644 src/PSRule.Types/Data/ISemanticVersionConstraint.cs create mode 100644 tests/PSRule.CommandLine.Tests/ModuleConstraintTests.cs create mode 100644 tests/PSRule.Tests/TestEnumValue.cs create mode 100644 tests/PSRule.Types.Tests/Data/ModuleConstraintTests.cs create mode 100644 tests/PSRule.Types.Tests/GlobalUsings.cs create mode 100644 tests/PSRule.Types.Tests/PSRule.Types.Tests.csproj diff --git a/PSRule.sln b/PSRule.sln index 22ad753e7..79368d339 100644 --- a/PSRule.sln +++ b/PSRule.sln @@ -32,7 +32,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.Tool.Tests", "tests\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.CommandLine", "src\PSRule.CommandLine\PSRule.CommandLine.csproj", "{9A556814-8E9D-4C76-8F6D-1AF2DA23A9E0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSRule.CommandLine.Tests", "tests\PSRule.CommandLine.Tests\PSRule.CommandLine.Tests.csproj", "{C25E2FC1-E306-4D99-925C-15E5DD51F6A2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.CommandLine.Tests", "tests\PSRule.CommandLine.Tests\PSRule.CommandLine.Tests.csproj", "{C25E2FC1-E306-4D99-925C-15E5DD51F6A2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.Types.Tests", "tests\PSRule.Types.Tests\PSRule.Types.Tests.csproj", "{34095F78-CDA3-4E72-B64C-6366EA4B3EAF}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -88,6 +90,10 @@ Global {C25E2FC1-E306-4D99-925C-15E5DD51F6A2}.Debug|Any CPU.Build.0 = Debug|Any CPU {C25E2FC1-E306-4D99-925C-15E5DD51F6A2}.Release|Any CPU.ActiveCfg = Release|Any CPU {C25E2FC1-E306-4D99-925C-15E5DD51F6A2}.Release|Any CPU.Build.0 = Release|Any CPU + {34095F78-CDA3-4E72-B64C-6366EA4B3EAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34095F78-CDA3-4E72-B64C-6366EA4B3EAF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34095F78-CDA3-4E72-B64C-6366EA4B3EAF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34095F78-CDA3-4E72-B64C-6366EA4B3EAF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -96,6 +102,7 @@ Global {D3488CE2-779F-4474-B38A-F894A4B689F7} = {E0EA0CBA-96C5-4447-8B69-BC13EF0D7A4A} {DA46C891-08F1-4D01-9F98-1F8BB10CAFEC} = {E0EA0CBA-96C5-4447-8B69-BC13EF0D7A4A} {C25E2FC1-E306-4D99-925C-15E5DD51F6A2} = {E0EA0CBA-96C5-4447-8B69-BC13EF0D7A4A} + {34095F78-CDA3-4E72-B64C-6366EA4B3EAF} = {E0EA0CBA-96C5-4447-8B69-BC13EF0D7A4A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {533491EB-BAE9-472E-B57F-A675ECD335B5} diff --git a/docs/CHANGELOG-v3.md b/docs/CHANGELOG-v3.md index 97ad5fdcf..b0ce89548 100644 --- a/docs/CHANGELOG-v3.md +++ b/docs/CHANGELOG-v3.md @@ -27,6 +27,17 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers ## Unreleased +What's changed since pre-release v3.0.0-B0267: + +- General improvements: + - **Breaking change**: Empty version comparison only accepts stable versions by default by @BernieWhite. + [#2557](https://github.com/microsoft/PSRule/issues/2557) + - `version` and `apiVersion` assertions only accept stable versions by default for all cases. + - Pre-release versions can be accepted by setting `includePrerelease` to `true`. +- Bug fixes: + - Fixed CLI upgrade uses pre-release module by @BernieWhite. + [#2549](https://github.com/microsoft/PSRule/issues/2549) + ## v3.0.0-B0267 (pre-release) What's changed since pre-release v3.0.0-B0203: diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Assert.md b/docs/concepts/PSRule/en-US/about_PSRule_Assert.md index c154e412e..7bbb71a98 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Assert.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Assert.md @@ -141,11 +141,12 @@ Notable differences between object paths and JSONPath are: ### APIVersion -The `APIVersion` assertion method checks the field value is a valid date version. +The `APIVersion` assertion method checks the field value is a valid stable date version. A constraint can optionally be provided to require the date version to be within a range. +By default, only stable versions are accepted unless pre-releases are included. A date version uses the format `yyyy-MM-dd` (`2015-10-01`). -Additionally an optional string prerelease identifier can be used `yyyy-MM-dd-prerelease` (`2015-10-01-preview.1`). +Additionally an optional string pre-release identifier can be used `yyyy-MM-dd-prerelease` (`2015-10-01-preview.1`). The following parameters are accepted: @@ -153,7 +154,7 @@ The following parameters are accepted: - `field` - The name of the field to check. This is a case insensitive compare. - `constraint` (optional) - A version constraint, see below for details of version constrain format. -- `includePrerelease` (optional) - Determines if prerelease versions are included. +- `includePrerelease` (optional) - Determines if pre-release versions are included. Unless specified this defaults to `$False`. The following are supported constraints: @@ -183,14 +184,14 @@ By example: - Pass: `2014-01-01`, `2015-10-01`, `2019-06-30`, `2022-02-01`. - Fail: `2015-01-01`, `2022-09-01`. -Handling for prerelease versions: +Handling for pre-release versions: -- Constraints and versions containing prerelease identifiers are supported. +- Constraints and versions containing pre-release identifiers are supported. i.e. `>=2015-10-01-preview` or `2015-10-01-preview`. -- A version containing a prerelease identifer follows similar ordering to semantic versioning. +- A version containing a pre-release identifier follows similar ordering to semantic versioning. i.e. `2015-10-01-preview` < `2015-10-01-preview.1` < `2015-10-01` < `2022-03-01-preview` < `2022-03-01`. -- A constraint without a prerelease identifer will only match a stable version by default. - Set `includePrerelease` to `$True` to include prerelease versions. +- A constraint without a pre-release identifier will only match a stable version by default. + Set `includePrerelease` to `$True` to include pre-;release versions. Alternatively use the `@pre` or `@prerelease` flag in a constraint. Reasons include: @@ -204,10 +205,14 @@ Reasons include: Examples: ```powershell -Rule 'ValidAPIVersion' { +Rule 'ValidStableAPIVersion' { $Assert.APIVersion($TargetObject, 'apiVersion') } +Rule 'AnyValidAPIVersion' { + $Assert.APIVersion($TargetObject, 'apiVersion', '', $True) +} + Rule 'MinimumAPIVersion' { $Assert.APIVersion($TargetObject, 'apiVersion', '>=2015-10-01') } @@ -1584,17 +1589,18 @@ Rule 'Subset' { ### Version -The `Version` assertion method checks the field value is a valid semantic version. +The `Version` assertion method checks the field value is a valid stable semantic version. A constraint can optionally be provided to require the semantic version to be within a range. +By default, only stable versions are accepted unless pre-releases are included. The following parameters are accepted: - `inputObject` - The object being checked for the specified field. - `field` - The name of the field to check. -This is a case insensitive compare. + This is a case insensitive compare. - `constraint` (optional) - A version constraint, see below for details of version constrain format. -- `includePrerelease` (optional) - Determines if prerelease versions are included. -Unless specified this defaults to `$False`. +- `includePrerelease` (optional) - Determines if pre-release versions are included. + Unless specified this defaults to `$False`. The following are supported constraints: @@ -1627,17 +1633,17 @@ By example: - Pass: `1.2.3`, `3.4.5`, `3.5.0`, `4.9.9`. - Fail: `3.0.0`, `5.0.0`. -Handling for prerelease versions: +Handling for pre-release versions: -- Constraints and versions containing prerelease identifiers are supported. -i.e. `>=1.2.3-build.1` or `1.2.3-build.1`. -- A version containing a prerelease identifer follows semantic versioning rules. +- Constraints and versions containing pre-release identifiers are supported. + i.e. `>=1.2.3-build.1` or `1.2.3-build.1`. +- A version containing a pre-release identifier follows semantic versioning rules. i.e. `1.2.3-alpha` < `1.2.3-alpha.1` < `1.2.3-alpha.beta` < `1.2.3-beta` < `1.2.3-beta.2` < `1.2.3-beta.11` < `1.2.3-rc.1` < `1.2.3`. -- A constraint without a prerelease identifer will only match a stable version by default. -Set `includePrerelease` to `$True` to include prerelease versions. -- Constraints with a prerelease identifer will only match: - - Matching prerelease versions of the same major.minor.patch version by default. - Set `includePrerelease` to `$True` to include prerelease versions of all matching versions. +- A constraint without a pre-release identifier will only match a stable version by default. + Set `includePrerelease` to `$True` to include pre-release versions. +- Constraints with a pre-release identifier will only match: + - Matching pre-release versions of the same major.minor.patch version by default. + Set `includePrerelease` to `$True` to include pre-release versions of all matching versions. Alternatively use the `@pre` or `@prerelease` flag in a constraint. - Matching stable versions. @@ -1672,10 +1678,14 @@ Reasons include: Examples: ```powershell -Rule 'ValidVersion' { +Rule 'ValidStableVersion' { $Assert.Version($TargetObject, 'version') } +Rule 'AnyValidVersion' { + $Assert.Version($TargetObject, 'version', '', $True) +} + Rule 'MinimumVersion' { $Assert.Version($TargetObject, 'version', '>=1.2.3') } diff --git a/docs/upgrade-notes.md b/docs/upgrade-notes.md index b9e152560..2317812f1 100644 --- a/docs/upgrade-notes.md +++ b/docs/upgrade-notes.md @@ -62,6 +62,43 @@ From _v3.0.0_, the `module restore` command installs modules based on: [5]: concepts/cli/module.md [6]: concepts/lockfile.md +### Version and APIVersion accept stable + +Prior to _v3.0.0_, some usage of `version` and `apiVersion` accepted pre-release versions by default. +For example: + +```yaml +--- +# Synopsis: Any version example +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: PreviousAnyVersionExample +spec: + if: + field: dateVersion + apiVersion: '' +``` + +When `apiVersion` is empty any version is accepted including pre-releases. + +From _v3.0.0_ pre-release versions are not accepted by default. +Set the `includePrerelease` property to `true`. + +```yaml +--- +# Synopsis: Test comparison with apiVersion. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: AnyVersion +spec: + if: + field: dateVersion + apiVersion: '' + includePrerelease: true +``` + ## Upgrading to v2.0.0 ### Resources naming restrictions diff --git a/src/PSRule.CommandLine/Commands/ModuleCommand.cs b/src/PSRule.CommandLine/Commands/ModuleCommand.cs index b92661f80..26d6cffab 100644 --- a/src/PSRule.CommandLine/Commands/ModuleCommand.cs +++ b/src/PSRule.CommandLine/Commands/ModuleCommand.cs @@ -98,7 +98,7 @@ public sealed class ModuleCommand // Check if the installed version matches the constraint. if (IsInstalled(pwsh, includeModule, null, out var installedVersion) && !operationOptions.Force && - (moduleConstraint == null || moduleConstraint.Equals(installedVersion))) + (moduleConstraint == null || moduleConstraint.Accepts(installedVersion))) { // invocation.Log(Messages.UsingModule, includeModule, installedVersion.ToString()); clientContext.LogVerbose($"The module {includeModule} is already installed."); @@ -224,13 +224,13 @@ public sealed class ModuleCommand if (!file.Modules.TryGetValue(module, out var item) || operationOptions.Force) { // Get a constraint if set from options. - var moduleConstraint = requires.TryGetValue(module, out var c) ? c : null; + var moduleConstraint = requires.TryGetValue(module, out var c) ? c : ModuleConstraint.Any(module, includePrerelease: false); // Get target version if specified in command-line. var targetVersion = !string.IsNullOrEmpty(operationOptions.Version) && SemanticVersion.TryParseVersion(operationOptions.Version, out var v) && v != null ? v : null; // Check if the target version is valid with the constraint if set. - if (targetVersion != null && moduleConstraint != null && !moduleConstraint.Constraint.Equals(targetVersion)) + if (targetVersion != null && moduleConstraint != null && !moduleConstraint.Constraint.Accepts(targetVersion)) { clientContext.LogError(Messages.Error_503, operationOptions.Version!); return ERROR_MODULE_ADD_VIOLATES_CONSTRAINT; @@ -316,7 +316,7 @@ public sealed class ModuleCommand foreach (var kv in file.Modules) { // Get a constraint if set from options. - var moduleConstraint = requires.TryGetValue(kv.Key, out var c) ? c : null; + var moduleConstraint = requires.TryGetValue(kv.Key, out var c) ? c : ModuleConstraint.Any(kv.Key, includePrerelease: false); // Find the ideal version. var idealVersion = await FindVersionAsync(kv.Key, moduleConstraint, null, null, cancellationToken); @@ -408,7 +408,7 @@ public sealed class ModuleCommand versionString != null && SemanticVersion.TryParseVersion(versionString, out var v) && v != null && - (targetVersion == null || targetVersion.Equals(v)) && + (targetVersion == null || targetVersion.CompareTo(v) == 0) && v.CompareTo(installedVersion) > 0) installedVersion = v; } @@ -452,8 +452,8 @@ public sealed class ModuleCommand if (version.ToFullString() is string versionString && SemanticVersion.TryParseVersion(versionString, out var v) && v != null && - (constraint == null || constraint.Constraint.Equals(v)) && - (targetVersion == null || targetVersion.Equals(v)) && + (constraint == null || constraint.Accepts(v)) && + (targetVersion == null || targetVersion.CompareTo(v) == 0) && v.CompareTo(result) > 0 && v.CompareTo(installedVersion) > 0) result = v; diff --git a/src/PSRule.Tool/Properties/launchSettings.json b/src/PSRule.Tool/Properties/launchSettings.json index 87369a406..f2faf7115 100644 --- a/src/PSRule.Tool/Properties/launchSettings.json +++ b/src/PSRule.Tool/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "ps-rule module add": { "commandName": "Project", - "commandLineArgs": "module add abc --version 1.0.0", + "commandLineArgs": "module add PSRule.Rules.Azure", "workingDirectory": "../../" }, "ps-rule run": { @@ -14,6 +14,11 @@ "commandName": "Project", "commandLineArgs": "module restore", "workingDirectory": "../../" + }, + "ps-rule module upgrade": { + "commandName": "Project", + "commandLineArgs": "module upgrade", + "workingDirectory": "../../" } } -} \ No newline at end of file +} diff --git a/src/PSRule.Types/Data/DateVersion.cs b/src/PSRule.Types/Data/DateVersion.cs index 92a32d9b7..c457b22ca 100644 --- a/src/PSRule.Types/Data/DateVersion.cs +++ b/src/PSRule.Types/Data/DateVersion.cs @@ -5,17 +5,6 @@ using System.Diagnostics; namespace PSRule.Data; -/// -/// An date version constraint. -/// -public interface IDateVersionConstraint -{ - /// - /// Determines if the date version meets the requirments of the constraint. - /// - bool Equals(DateVersion.Version version); -} - /// /// A helper for comparing date version strings. /// An date version is represented as YYYY-MM-DD-prerelease. @@ -75,18 +64,37 @@ public static class DateVersion public sealed class VersionConstraint : IDateVersionConstraint { private List? _Constraints; + private readonly string _Value; + private readonly bool _IncludePrerelease; + + /// + /// A version constraint that accepts any version including pre-releases. + /// + public static readonly VersionConstraint Any = new(string.Empty, includePrerelease: true); + + /// + /// A version constraint that accepts any stable version. + /// + public static readonly VersionConstraint AnyStable = new(string.Empty, includePrerelease: false); + + internal VersionConstraint(string value, bool includePrerelease) + { + _Value = value; + _IncludePrerelease = includePrerelease; + } /// - public bool Equals(Version version) + public bool Accepts(Version? version) { + if (version is null) return false; if (_Constraints == null || _Constraints.Count == 0) - return true; + return version.Stable || _IncludePrerelease; var match = false; var i = 0; while (!match && i < _Constraints.Count) { - var result = _Constraints[i].Equals(version); + var result = _Constraints[i].Accepts(version); // True OR if (result && _Constraints[i].Join == JoinOperator.Or) @@ -159,12 +167,13 @@ public static class DateVersion return TryParseConstraint(value, out constraint); } - public bool Equals(Version version) + /// + public bool Accepts(Version? version) { - return Equals(version.Year, version.Month, version.Day, version.Prerelease); + return version is not null && Accepts(version.Year, version.Month, version.Day, version.Prerelease); } - public bool Equals(int year, int month, int day, PR prid) + public bool Accepts(int year, int month, int day, PR prid) { if (_Flag == ComparisonOperator.Equals) return EQ(year, month, day, prid); @@ -277,8 +286,11 @@ public static class DateVersion /// /// An date version. /// + [DebuggerDisplay("{_VersionString}")] public sealed class Version : IComparable, IEquatable { + private readonly string _VersionString; + /// /// The year part of the version. /// @@ -305,12 +317,19 @@ public static class DateVersion Month = month; Day = day; Prerelease = prerelease; + + _VersionString = GetVersionString(); } + /// + /// Determines if the version is stable or a pre-release. + /// + public bool Stable => Prerelease == null || Prerelease.Stable; + /// public override string ToString() { - return string.Concat(Year, DASH, Month, DASH, Day); + return _VersionString; } /// @@ -338,7 +357,7 @@ public static class DateVersion /// public bool Equals(Version other) { - return other != null && + return other is not null && Equals(other.Year, other.Month, other.Day); } @@ -357,7 +376,7 @@ public static class DateVersion /// public int CompareTo(Version other) { - if (other == null) + if (other is null) return 1; if (Year != other.Year) @@ -369,13 +388,34 @@ public static class DateVersion if (Day != other.Day) return Day > other.Day ? 8 : -8; - if ((Prerelease == null || Prerelease.Stable) && (other.Prerelease == null || other.Prerelease.Stable)) + if ((Prerelease is null || Prerelease.Stable) && (other.Prerelease is null || other.Prerelease.Stable)) return 0; - if (Prerelease != null && !Prerelease.Stable && other.Prerelease != null && !other.Prerelease.Stable) + if (Prerelease is not null && !Prerelease.Stable && other.Prerelease is not null && !other.Prerelease.Stable) return Prerelease.CompareTo(other.Prerelease); - return Prerelease == null || Prerelease.Stable ? 1 : -1; + return Prerelease is null || Prerelease.Stable ? 1 : -1; + } + + private string GetVersionString() + { + var count = 5 + (Prerelease != null && !Prerelease.Stable ? 2 : 0); + var parts = new object[count]; + + parts[0] = Year; + parts[1] = DASH; + parts[2] = Month; + parts[3] = DASH; + parts[4] = Day; + + var next = 5; + if (Prerelease != null && !Prerelease.Stable) + { + parts[next++] = DASH; + parts[next++] = Prerelease.Value; + } + + return string.Concat(parts); } } @@ -730,7 +770,7 @@ public static class DateVersion /// public static bool TryParseConstraint(string value, out IDateVersionConstraint constraint, bool includePrerelease = false) { - var c = new VersionConstraint(); + var c = new VersionConstraint(value, includePrerelease); constraint = c; if (string.IsNullOrEmpty(value)) return true; diff --git a/src/PSRule.Types/Data/IDateVersionConstraint.cs b/src/PSRule.Types/Data/IDateVersionConstraint.cs new file mode 100644 index 000000000..d872039ba --- /dev/null +++ b/src/PSRule.Types/Data/IDateVersionConstraint.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Data; + +/// +/// An date version constraint. +/// +public interface IDateVersionConstraint +{ + /// + /// Determines if the date version meets the requirments of the constraint. + /// + bool Accepts(DateVersion.Version? version); +} diff --git a/src/PSRule.Types/Data/ISemanticVersionConstraint.cs b/src/PSRule.Types/Data/ISemanticVersionConstraint.cs new file mode 100644 index 000000000..d74097af8 --- /dev/null +++ b/src/PSRule.Types/Data/ISemanticVersionConstraint.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Data; + +/// +/// A semantic version constraint. +/// +public interface ISemanticVersionConstraint +{ + /// + /// Determines if the semantic version meets the requirments of the constraint. + /// + bool Accepts(SemanticVersion.Version? version); +} diff --git a/src/PSRule.Types/Data/ModuleConstraint.cs b/src/PSRule.Types/Data/ModuleConstraint.cs index e163ec9b5..bda9c70b8 100644 --- a/src/PSRule.Types/Data/ModuleConstraint.cs +++ b/src/PSRule.Types/Data/ModuleConstraint.cs @@ -1,31 +1,44 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics; + namespace PSRule.Data; /// /// A version constraint for a PSRule module. /// -public sealed class ModuleConstraint +/// The name of the module. +/// The version constraint of the module. +/// Both and must not be null or empty. +[DebuggerDisplay("{Module}")] +public sealed class ModuleConstraint(string module, ISemanticVersionConstraint constraint) : ISemanticVersionConstraint { - /// - /// Create a version constraint for a PSRule module. - /// - /// The name of the module. - /// The version constraint of the module. - public ModuleConstraint(string module, ISemanticVersionConstraint constraint) - { - Module = module; - Constraint = constraint; - } - /// /// The name of the module. /// - public string Module { get; } + public string Module { get; } = !string.IsNullOrEmpty(module) ? module : throw new ArgumentNullException(nameof(module)); /// /// The version constraint of the module. /// - public ISemanticVersionConstraint Constraint { get; } + public ISemanticVersionConstraint Constraint { get; } = constraint ?? throw new ArgumentNullException(nameof(constraint)); + + /// + public bool Accepts(SemanticVersion.Version? version) => Constraint.Accepts(version); + + /// + /// Get a constraint that accepts any version of the specified module. + /// + /// + /// Determines if pre-releases are accepted or only stable versions. + /// A . + public static ModuleConstraint Any(string module, bool includePrerelease = false) + { + return new ModuleConstraint + ( + module, + includePrerelease ? SemanticVersion.VersionConstraint.Any : SemanticVersion.VersionConstraint.AnyStable + ); + } } diff --git a/src/PSRule.Types/Data/SemanticVersion.cs b/src/PSRule.Types/Data/SemanticVersion.cs index 3d4d92175..0dddd1a35 100644 --- a/src/PSRule.Types/Data/SemanticVersion.cs +++ b/src/PSRule.Types/Data/SemanticVersion.cs @@ -5,17 +5,6 @@ using System.Diagnostics; namespace PSRule.Data; -/// -/// A semantic version constraint. -/// -public interface ISemanticVersionConstraint -{ - /// - /// Determines if the semantic version meets the requirments of the constraint. - /// - bool Equals(SemanticVersion.Version version); -} - /// /// A helper for comparing semantic version strings. /// @@ -28,7 +17,7 @@ public static class SemanticVersion private const char VLOWER = 'v'; private const char GREATER = '>'; private const char LESS = '<'; - private const char SEPARATOR = '.'; + private const char DOT = '.'; private const char DASH = '-'; private const char PLUS = '+'; private const char ZERO = '0'; @@ -89,23 +78,36 @@ public static class SemanticVersion { private List? _Constraints; private readonly string _Value; + private readonly bool _IncludePrerelease; - internal VersionConstraint(string value) + /// + /// A version constraint that accepts any version including pre-releases. + /// + public static readonly VersionConstraint Any = new(string.Empty, includePrerelease: true); + + /// + /// A version constraint that accepts any stable version. + /// + public static readonly VersionConstraint AnyStable = new(string.Empty, includePrerelease: false); + + internal VersionConstraint(string value, bool includePrerelease) { _Value = value; + _IncludePrerelease = includePrerelease; } /// - public bool Equals(Version version) + public bool Accepts(Version? version) { + if (version is null) return false; if (_Constraints == null || _Constraints.Count == 0) - return true; + return version.Stable || _IncludePrerelease; var match = false; var i = 0; while (!match && i < _Constraints.Count) { - var result = _Constraints[i].Equals(version); + var result = _Constraints[i].Accepts(version); // True OR if (result && _Constraints[i].Join == JoinOperator.Or) @@ -133,20 +135,6 @@ public static class SemanticVersion return false; } - internal void Join(int major, int minor, int patch, PR prid, ComparisonOperator flag, JoinOperator join, bool includePrerelease) - { - _Constraints ??= new List(); - _Constraints.Add(new ConstraintExpression( - major, - minor, - patch, - prid, - flag, - join == JoinOperator.None ? JoinOperator.Or : join, - includePrerelease - )); - } - /// public override string ToString() { @@ -158,6 +146,20 @@ public static class SemanticVersion { return _Value.GetHashCode(); } + + internal void Join(int major, int minor, int patch, PR prid, ComparisonOperator flag, JoinOperator join, bool includePrerelease) + { + _Constraints ??= []; + _Constraints.Add(new ConstraintExpression( + major, + minor, + patch, + prid, + flag, + join == JoinOperator.None ? JoinOperator.Or : join, + includePrerelease + )); + } } [DebuggerDisplay("{_Major}.{_Minor}.{_Patch}")] @@ -190,17 +192,18 @@ public static class SemanticVersion return TryParseConstraint(value, out constraint); } - public bool Equals(System.Version version) + public bool Accepts(System.Version version) { - return Equals(version.Major, version.Minor, version.Build, null); + return Accepts(version.Major, version.Minor, version.Build, null); } - public bool Equals(Version version) + /// + public bool Accepts(Version? version) { - return Equals(version.Major, version.Minor, version.Patch, version.Prerelease); + return version is not null && Accepts(version.Major, version.Minor, version.Patch, version.Prerelease); } - public bool Equals(int major, int minor, int patch, PR? prid) + public bool Accepts(int major, int minor, int patch, PR? prid) { if (_Flag == ComparisonOperator.Equals) return EQ(major, minor, patch, prid); @@ -340,8 +343,11 @@ public static class SemanticVersion /// /// A semantic version. /// + [DebuggerDisplay("{_VersionString}")] public sealed class Version : IComparable, IEquatable { + private readonly string _VersionString; + /// /// The major part of the version. /// @@ -374,8 +380,15 @@ public static class SemanticVersion Patch = patch; Prerelease = prerelease; Build = build; + + _VersionString = GetVersionString(); } + /// + /// Determines if the version is stable or a pre-release. + /// + public bool Stable => Prerelease == null || Prerelease.Stable; + /// /// Try to parse a semantic version from a string. /// @@ -387,7 +400,7 @@ public static class SemanticVersion /// public override string ToString() { - return string.Concat(Major, '.', Minor, '.', Patch); + return _VersionString; } /// @@ -432,7 +445,7 @@ public static class SemanticVersion /// public bool Equals(Version? other) { - return other != null && + return other is not null && Equals(other.Major, other.Minor, other.Patch, other.Prerelease?.Value); } @@ -452,7 +465,7 @@ public static class SemanticVersion /// public int CompareTo(Version? other) { - if (other == null) + if (other is null) return 1; if (Major != other.Major) @@ -461,7 +474,37 @@ public static class SemanticVersion if (Minor != other.Minor) return Minor > other.Minor ? 16 : -16; - return Patch != other.Patch ? Patch > other.Patch ? 8 : -8 : 0; + if (Patch != other.Patch) + return Patch > other.Patch ? 8 : -8; + + return Prerelease != other.Prerelease ? PR.Compare(Prerelease, other.Prerelease) : 0; + } + + private string GetVersionString() + { + var count = 5 + (Prerelease != null && !Prerelease.Stable ? 2 : 0) + (Build != null && Build.Length > 0 ? 2 : 0); + var parts = new object[count]; + + parts[0] = Major; + parts[1] = DOT; + parts[2] = Minor; + parts[3] = DOT; + parts[4] = Patch; + + var next = 5; + if (Prerelease != null && !Prerelease.Stable) + { + parts[next++] = DASH; + parts[next++] = Prerelease.Value; + } + + if (Build != null && Build.Length > 0) + { + parts[next++] = PLUS; + parts[next++] = Build; + } + + return string.Concat(parts); } } @@ -469,10 +512,10 @@ public static class SemanticVersion /// A semantic version pre-release identifier. /// [DebuggerDisplay("{Value}")] - public sealed class PR + public sealed class PR : IComparable, IEquatable { internal static readonly PR Empty = new(); - private static readonly char[] SEPARATORS = new char[] { SEPARATOR }; + private static readonly char[] SEPARATORS = new char[] { DOT }; private readonly string[]? _Identifiers; @@ -501,16 +544,16 @@ public static class SemanticVersion /// /// Compare the pre-release identifer to another pre-release identifier. /// - public int CompareTo(PR? pr) + public int CompareTo(PR? other) { - if (pr == null || pr.Stable || pr._Identifiers == null) + if (other is null || other.Stable || other._Identifiers == null) return Stable ? 0 : -1; else if (Stable || _Identifiers == null) return 1; var i = -1; var left = _Identifiers; - var right = pr._Identifiers; + var right = other._Identifiers; while (++i < left.Length && i < right.Length) { @@ -543,10 +586,21 @@ public static class SemanticVersion return left.Length > right.Length ? 1 : -1; } + /// + public bool Equals(PR? other) + { + if (other is null) + return Stable; + + return Stable && other.Stable || + Value.Equals(other.Value); + } + + /// public override bool Equals(object obj) { - return obj is PR prerelease && Value.Equals(prerelease.Value); + return obj is PR other && Equals(other); } /// @@ -560,6 +614,18 @@ public static class SemanticVersion { return Value.ToString(); } + + /// + /// Compare two instances. + /// + public static int Compare(PR pr1, PR pr2) + { + if (pr1 == pr2) return 0; + if (pr1 == null || pr1.Stable) return 1; + if (pr2 == null || pr2.Stable) return -1; + + return pr1.CompareTo(pr2); + } } [DebuggerDisplay("Current = {_Current}, Position = {_Position}, Value = {_Value}")] @@ -652,7 +718,7 @@ public static class SemanticVersion internal bool TrySegments(out int[] segments) { - segments = new int[] { -1, -1, -1, -1 }; + segments = [-1, -1, -1, -1]; var segmentIndex = 0; SkipLeading(); while (!EOF) @@ -766,7 +832,7 @@ public static class SemanticVersion [DebuggerStepThrough()] private static bool IsSeparator(char c) { - return c == SEPARATOR; + return c == DOT; } [DebuggerStepThrough()] @@ -782,7 +848,7 @@ public static class SemanticVersion return true; numeric = false; - return char.IsDigit(c) || IsLetter(c) || c == DASH || c == SEPARATOR; + return char.IsDigit(c) || IsLetter(c) || c == DASH || c == DOT; } [DebuggerStepThrough()] @@ -819,7 +885,7 @@ public static class SemanticVersion /// public static bool TryParseConstraint(string value, out ISemanticVersionConstraint constraint, bool includePrerelease = false) { - var c = new VersionConstraint(value); + var c = new VersionConstraint(value, includePrerelease); constraint = c; if (string.IsNullOrEmpty(value)) return true; diff --git a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs index d44c6f22e..fb1bd8497 100644 --- a/src/PSRule/Definitions/Expressions/LanguageExpressions.cs +++ b/src/PSRule/Definitions/Expressions/LanguageExpressions.cs @@ -1128,8 +1128,8 @@ internal sealed class LanguageExpressions if (!SemanticVersion.TryParseConstraint(expectedValue, out var constraint, includePrerelease)) throw new RuleException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.VersionConstraintInvalid, expectedValue)); - if (constraint != null && !constraint.Equals(actualVersion)) - return Fail(context, operand, ReasonStrings.VersionContraint, actualVersion, constraint); + if (constraint != null && !constraint.Accepts(actualVersion)) + return Fail(context, operand, ReasonStrings.VersionConstraint, actualVersion, constraint); return Pass(); } @@ -1153,8 +1153,8 @@ internal sealed class LanguageExpressions if (!DateVersion.TryParseConstraint(expectedValue, out var constraint, includePrerelease)) throw new RuleException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.VersionConstraintInvalid, expectedValue)); - if (constraint != null && !constraint.Equals(actualVersion)) - return Fail(context, operand, ReasonStrings.VersionContraint, actualVersion, constraint); + if (constraint != null && !constraint.Accepts(actualVersion)) + return Fail(context, operand, ReasonStrings.VersionConstraint, actualVersion, constraint); return Pass(); } diff --git a/src/PSRule/Pipeline/PipelineBuilderBase.cs b/src/PSRule/Pipeline/PipelineBuilderBase.cs index 4f4a0b290..d60d46334 100644 --- a/src/PSRule/Pipeline/PipelineBuilderBase.cs +++ b/src/PSRule/Pipeline/PipelineBuilderBase.cs @@ -160,7 +160,7 @@ internal abstract class PipelineBuilderBase : IPipelineBuilder { return SemanticVersion.TryParseVersion(moduleVersion, out var version) && SemanticVersion.TryParseConstraint(requiredVersion, out var constraint) && - constraint.Equals(version); + constraint.Accepts(version); } /// diff --git a/src/PSRule/Resources/ReasonStrings.Designer.cs b/src/PSRule/Resources/ReasonStrings.Designer.cs index b9dacde3f..c2f6b1fa0 100644 --- a/src/PSRule/Resources/ReasonStrings.Designer.cs +++ b/src/PSRule/Resources/ReasonStrings.Designer.cs @@ -711,9 +711,9 @@ namespace PSRule.Resources { /// /// Looks up a localized string similar to The version '{0}' does not match the constraint '{1}'.. /// - internal static string VersionContraint { + internal static string VersionConstraint { get { - return ResourceManager.GetString("VersionContraint", resourceCulture); + return ResourceManager.GetString("VersionConstraint", resourceCulture); } } diff --git a/src/PSRule/Resources/ReasonStrings.resx b/src/PSRule/Resources/ReasonStrings.resx index 2b51ac2ac..4cdaa6103 100644 --- a/src/PSRule/Resources/ReasonStrings.resx +++ b/src/PSRule/Resources/ReasonStrings.resx @@ -307,7 +307,7 @@ The field value '{0}' is not a version string. - + The version '{0}' does not match the constraint '{1}'. diff --git a/src/PSRule/Runtime/Assert.cs b/src/PSRule/Runtime/Assert.cs index b7a0987b3..d249f99eb 100644 --- a/src/PSRule/Runtime/Assert.cs +++ b/src/PSRule/Runtime/Assert.cs @@ -859,7 +859,7 @@ public sealed class Assert throw new RuleException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.VersionConstraintInvalid, value)); // Assert - return c != null && !c.Equals(value) ? Fail(Operand.FromPath(field), ReasonStrings.VersionContraint, value, constraint) : Pass(); + return c != null && !c.Accepts(value) ? Fail(Operand.FromPath(field), ReasonStrings.VersionConstraint, value, constraint) : Pass(); } /// @@ -883,7 +883,7 @@ public sealed class Assert throw new RuleException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.VersionConstraintInvalid, value)); // Assert - return c != null && !c.Equals(value) ? Fail(Operand.FromPath(field), ReasonStrings.VersionContraint, value, constraint) : Pass(); + return c != null && !c.Accepts(value) ? Fail(Operand.FromPath(field), ReasonStrings.VersionConstraint, value, constraint) : Pass(); } /// diff --git a/tests/PSRule.CommandLine.Tests/ModuleConstraintTests.cs b/tests/PSRule.CommandLine.Tests/ModuleConstraintTests.cs new file mode 100644 index 000000000..a7fb4a980 --- /dev/null +++ b/tests/PSRule.CommandLine.Tests/ModuleConstraintTests.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Data; + +namespace PSRule.CommandLine; + +/// +/// Tests for . +/// +public sealed class ModuleConstraintTests +{ +} diff --git a/tests/PSRule.CommandLine.Tests/PSRule.CommandLine.Tests.csproj b/tests/PSRule.CommandLine.Tests/PSRule.CommandLine.Tests.csproj index 3bc51a4da..2e92acd2e 100644 --- a/tests/PSRule.CommandLine.Tests/PSRule.CommandLine.Tests.csproj +++ b/tests/PSRule.CommandLine.Tests/PSRule.CommandLine.Tests.csproj @@ -21,4 +21,8 @@ + + + + diff --git a/tests/PSRule.Tests/DateVersionTests.cs b/tests/PSRule.Tests/DateVersionTests.cs index e23165eb1..3a250cfff 100644 --- a/tests/PSRule.Tests/DateVersionTests.cs +++ b/tests/PSRule.Tests/DateVersionTests.cs @@ -87,67 +87,67 @@ public sealed class DateVersionTests Assert.True(DateVersion.TryParseConstraint("@prerelease <=2022-03-01-0", out var actual21)); // Version1 - 2015-10-01 - Assert.True(actual1.Equals(version1)); - Assert.False(actual2.Equals(version1)); - Assert.True(actual3.Equals(version1)); - Assert.True(actual4.Equals(version1)); - Assert.False(actual5.Equals(version1)); - Assert.True(actual7.Equals(version1)); - Assert.True(actual8.Equals(version1)); - Assert.True(actual9.Equals(version1)); - Assert.True(actual10.Equals(version1)); - Assert.True(actual11.Equals(version1)); - Assert.True(actual12.Equals(version1)); - Assert.False(actual14.Equals(version1)); - Assert.True(actual15.Equals(version1)); - Assert.True(actual16.Equals(version1)); - Assert.True(actual17.Equals(version1)); - Assert.True(actual18.Equals(version1)); - Assert.True(actual19.Equals(version1)); - Assert.True(actual20.Equals(version1)); - Assert.True(actual21.Equals(version1)); + Assert.True(actual1.Accepts(version1)); + Assert.False(actual2.Accepts(version1)); + Assert.True(actual3.Accepts(version1)); + Assert.True(actual4.Accepts(version1)); + Assert.False(actual5.Accepts(version1)); + Assert.True(actual7.Accepts(version1)); + Assert.True(actual8.Accepts(version1)); + Assert.True(actual9.Accepts(version1)); + Assert.True(actual10.Accepts(version1)); + Assert.True(actual11.Accepts(version1)); + Assert.True(actual12.Accepts(version1)); + Assert.False(actual14.Accepts(version1)); + Assert.True(actual15.Accepts(version1)); + Assert.True(actual16.Accepts(version1)); + Assert.True(actual17.Accepts(version1)); + Assert.True(actual18.Accepts(version1)); + Assert.True(actual19.Accepts(version1)); + Assert.True(actual20.Accepts(version1)); + Assert.True(actual21.Accepts(version1)); // Version3 - 2015-10-01-alpha.9 - Assert.False(actual1.Equals(version2)); - Assert.False(actual2.Equals(version2)); - Assert.True(actual3.Equals(version2)); - Assert.True(actual4.Equals(version2)); - Assert.True(actual5.Equals(version2)); - Assert.False(actual7.Equals(version2)); - Assert.False(actual8.Equals(version2)); - Assert.False(actual9.Equals(version2)); - Assert.True(actual10.Equals(version2)); - Assert.False(actual11.Equals(version2)); - Assert.False(actual12.Equals(version2)); - Assert.False(actual14.Equals(version2)); - Assert.False(actual15.Equals(version2)); - Assert.False(actual16.Equals(version2)); - Assert.False(actual17.Equals(version2)); - Assert.True(actual18.Equals(version2)); - Assert.True(actual19.Equals(version2)); - Assert.True(actual20.Equals(version2)); - Assert.True(actual21.Equals(version2)); + Assert.False(actual1.Accepts(version2)); + Assert.False(actual2.Accepts(version2)); + Assert.True(actual3.Accepts(version2)); + Assert.True(actual4.Accepts(version2)); + Assert.True(actual5.Accepts(version2)); + Assert.False(actual7.Accepts(version2)); + Assert.False(actual8.Accepts(version2)); + Assert.False(actual9.Accepts(version2)); + Assert.True(actual10.Accepts(version2)); + Assert.False(actual11.Accepts(version2)); + Assert.False(actual12.Accepts(version2)); + Assert.False(actual14.Accepts(version2)); + Assert.False(actual15.Accepts(version2)); + Assert.False(actual16.Accepts(version2)); + Assert.False(actual17.Accepts(version2)); + Assert.True(actual18.Accepts(version2)); + Assert.True(actual19.Accepts(version2)); + Assert.True(actual20.Accepts(version2)); + Assert.True(actual21.Accepts(version2)); // Version4 - 2022-03-01 - Assert.False(actual1.Equals(version3)); - Assert.False(actual2.Equals(version3)); - Assert.True(actual3.Equals(version3)); - Assert.True(actual4.Equals(version3)); - Assert.False(actual5.Equals(version3)); - Assert.False(actual7.Equals(version3)); - Assert.False(actual8.Equals(version3)); - Assert.True(actual9.Equals(version3)); - Assert.True(actual10.Equals(version3)); - Assert.False(actual11.Equals(version3)); - Assert.False(actual12.Equals(version3)); - Assert.False(actual14.Equals(version3)); - Assert.True(actual15.Equals(version3)); - Assert.True(actual16.Equals(version3)); - Assert.True(actual17.Equals(version3)); - Assert.True(actual18.Equals(version3)); - Assert.False(actual19.Equals(version3)); - Assert.True(actual20.Equals(version3)); - Assert.False(actual21.Equals(version3)); + Assert.False(actual1.Accepts(version3)); + Assert.False(actual2.Accepts(version3)); + Assert.True(actual3.Accepts(version3)); + Assert.True(actual4.Accepts(version3)); + Assert.False(actual5.Accepts(version3)); + Assert.False(actual7.Accepts(version3)); + Assert.False(actual8.Accepts(version3)); + Assert.True(actual9.Accepts(version3)); + Assert.True(actual10.Accepts(version3)); + Assert.False(actual11.Accepts(version3)); + Assert.False(actual12.Accepts(version3)); + Assert.False(actual14.Accepts(version3)); + Assert.True(actual15.Accepts(version3)); + Assert.True(actual16.Accepts(version3)); + Assert.True(actual17.Accepts(version3)); + Assert.True(actual18.Accepts(version3)); + Assert.False(actual19.Accepts(version3)); + Assert.True(actual20.Accepts(version3)); + Assert.False(actual21.Accepts(version3)); } /// diff --git a/tests/PSRule.Tests/SelectorTests.cs b/tests/PSRule.Tests/SelectorTests.cs index cf23d27b8..108c3679b 100644 --- a/tests/PSRule.Tests/SelectorTests.cs +++ b/tests/PSRule.Tests/SelectorTests.cs @@ -14,13 +14,6 @@ using PSRule.Runtime; namespace PSRule; -internal enum TestEnumValue -{ - None = 0, - - All = 1 -} - public sealed class SelectorTests { private const string SelectorYamlFileName = "Selectors.Rule.yaml"; @@ -39,7 +32,7 @@ public sealed class SelectorTests context.Begin(); var selector = HostHelper.GetSelectorForTests(GetSource(path), context).ToArray(); Assert.NotNull(selector); - Assert.Equal(102, selector.Length); + Assert.Equal(104, selector.Length); var actual = selector[0]; var visitor = new SelectorVisitor(context, actual.Id, actual.Source, actual.Spec.If); @@ -1573,6 +1566,15 @@ public sealed class SelectorTests Assert.True(version.Match(actual5)); Assert.False(version.Match(actual6)); Assert.False(version.Match(actual7)); + + version = GetSelectorVisitor($"{type}VersionAnyStableVersion", GetSource(path), out _); + Assert.True(version.Match(actual1)); + Assert.True(version.Match(actual2)); + Assert.True(version.Match(actual3)); + Assert.True(version.Match(actual4)); + Assert.False(version.Match(actual5)); + Assert.False(version.Match(actual6)); + Assert.False(version.Match(actual7)); } [Theory] @@ -1614,6 +1616,15 @@ public sealed class SelectorTests Assert.True(version.Match(actual5)); Assert.False(version.Match(actual6)); Assert.False(version.Match(actual7)); + + version = GetSelectorVisitor($"{type}APIVersionAnyStableVersion", GetSource(path), out _); + Assert.True(version.Match(actual1)); + Assert.True(version.Match(actual2)); + Assert.True(version.Match(actual3)); + Assert.False(version.Match(actual4)); + Assert.False(version.Match(actual5)); + Assert.False(version.Match(actual6)); + Assert.False(version.Match(actual7)); } [Theory] diff --git a/tests/PSRule.Tests/Selectors.Rule.jsonc b/tests/PSRule.Tests/Selectors.Rule.jsonc index 342d69d78..9a0ccefdb 100644 --- a/tests/PSRule.Tests/Selectors.Rule.jsonc +++ b/tests/PSRule.Tests/Selectors.Rule.jsonc @@ -1353,6 +1353,21 @@ "metadata": { "name": "JsonVersionAnyVersion" }, + "spec": { + "if": { + "field": "version", + "version": "", + "includePrerelease": true + } + } + }, + { + // Synopsis: Test any valid stable version + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonVersionAnyStableVersion" + }, "spec": { "if": { "field": "version", @@ -1898,6 +1913,21 @@ "metadata": { "name": "JsonAPIVersionAnyVersion" }, + "spec": { + "if": { + "field": "dateVersion", + "apiVersion": "", + "includePrerelease": true + } + } + }, + { + // Synopsis: Test comparison with apiVersion. + "apiVersion": "github.com/microsoft/PSRule/v1", + "kind": "Selector", + "metadata": { + "name": "JsonAPIVersionAnyStableVersion" + }, "spec": { "if": { "field": "dateVersion", diff --git a/tests/PSRule.Tests/Selectors.Rule.yaml b/tests/PSRule.Tests/Selectors.Rule.yaml index 4bbf6a30b..04111bf97 100644 --- a/tests/PSRule.Tests/Selectors.Rule.yaml +++ b/tests/PSRule.Tests/Selectors.Rule.yaml @@ -935,6 +935,18 @@ apiVersion: github.com/microsoft/PSRule/v1 kind: Selector metadata: name: YamlVersionAnyVersion +spec: + if: + field: 'version' + version: '' + includePrerelease: true + +--- +# Synopsis: Test any valid stable version +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: YamlVersionAnyStableVersion spec: if: field: 'version' @@ -1359,6 +1371,18 @@ apiVersion: github.com/microsoft/PSRule/v1 kind: Selector metadata: name: YamlAPIVersionAnyVersion +spec: + if: + field: dateVersion + apiVersion: '' + includePrerelease: true + +--- +# Synopsis: Test comparison with apiVersion. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Selector +metadata: + name: YamlAPIVersionAnyStableVersion spec: if: field: dateVersion diff --git a/tests/PSRule.Tests/SemanticVersionTests.cs b/tests/PSRule.Tests/SemanticVersionTests.cs index 3c6b65889..fc44cee22 100644 --- a/tests/PSRule.Tests/SemanticVersionTests.cs +++ b/tests/PSRule.Tests/SemanticVersionTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using Microsoft.PowerShell; using PSRule.Data; namespace PSRule; @@ -97,96 +98,96 @@ public sealed class SemanticVersionTests Assert.True(SemanticVersion.TryParseConstraint("@prerelease <=3.4.5-0", out var actual21)); // Version1 - 1.2.3 - Assert.True(actual1.Equals(version1)); - Assert.False(actual2.Equals(version1)); - Assert.True(actual3.Equals(version1)); - Assert.True(actual4.Equals(version1)); - Assert.False(actual5.Equals(version1)); - Assert.True(actual6.Equals(version1)); - Assert.True(actual7.Equals(version1)); - Assert.True(actual8.Equals(version1)); - Assert.True(actual9.Equals(version1)); - Assert.True(actual10.Equals(version1)); - Assert.True(actual11.Equals(version1)); - Assert.True(actual12.Equals(version1)); - Assert.True(actual13.Equals(version1)); - Assert.False(actual14.Equals(version1)); - Assert.True(actual15.Equals(version1)); - Assert.True(actual16.Equals(version1)); - Assert.True(actual17.Equals(version1)); - Assert.True(actual18.Equals(version1)); - Assert.True(actual19.Equals(version1)); - Assert.True(actual20.Equals(version1)); - Assert.True(actual21.Equals(version1)); + Assert.True(actual1.Accepts(version1)); + Assert.False(actual2.Accepts(version1)); + Assert.True(actual3.Accepts(version1)); + Assert.True(actual4.Accepts(version1)); + Assert.False(actual5.Accepts(version1)); + Assert.True(actual6.Accepts(version1)); + Assert.True(actual7.Accepts(version1)); + Assert.True(actual8.Accepts(version1)); + Assert.True(actual9.Accepts(version1)); + Assert.True(actual10.Accepts(version1)); + Assert.True(actual11.Accepts(version1)); + Assert.True(actual12.Accepts(version1)); + Assert.True(actual13.Accepts(version1)); + Assert.False(actual14.Accepts(version1)); + Assert.True(actual15.Accepts(version1)); + Assert.True(actual16.Accepts(version1)); + Assert.True(actual17.Accepts(version1)); + Assert.True(actual18.Accepts(version1)); + Assert.True(actual19.Accepts(version1)); + Assert.True(actual20.Accepts(version1)); + Assert.True(actual21.Accepts(version1)); // Version2 - 1.2.3-alpha.3+7223b39 - Assert.False(actual1.Equals(version2)); - Assert.True(actual2.Equals(version2)); - Assert.False(actual3.Equals(version2)); - Assert.True(actual4.Equals(version2)); - Assert.True(actual5.Equals(version2)); - Assert.True(actual6.Equals(version2)); - Assert.False(actual7.Equals(version2)); - Assert.False(actual8.Equals(version2)); - Assert.False(actual9.Equals(version2)); - Assert.True(actual10.Equals(version2)); - Assert.False(actual11.Equals(version2)); - Assert.False(actual12.Equals(version2)); - Assert.False(actual13.Equals(version2)); - Assert.False(actual14.Equals(version2)); - Assert.False(actual15.Equals(version2)); - Assert.False(actual16.Equals(version2)); - Assert.False(actual17.Equals(version2)); - Assert.False(actual18.Equals(version2)); - Assert.True(actual19.Equals(version2)); - Assert.False(actual20.Equals(version2)); - Assert.True(actual21.Equals(version2)); + Assert.False(actual1.Accepts(version2)); + Assert.True(actual2.Accepts(version2)); + Assert.False(actual3.Accepts(version2)); + Assert.True(actual4.Accepts(version2)); + Assert.True(actual5.Accepts(version2)); + Assert.True(actual6.Accepts(version2)); + Assert.False(actual7.Accepts(version2)); + Assert.False(actual8.Accepts(version2)); + Assert.False(actual9.Accepts(version2)); + Assert.True(actual10.Accepts(version2)); + Assert.False(actual11.Accepts(version2)); + Assert.False(actual12.Accepts(version2)); + Assert.False(actual13.Accepts(version2)); + Assert.False(actual14.Accepts(version2)); + Assert.False(actual15.Accepts(version2)); + Assert.False(actual16.Accepts(version2)); + Assert.False(actual17.Accepts(version2)); + Assert.False(actual18.Accepts(version2)); + Assert.True(actual19.Accepts(version2)); + Assert.False(actual20.Accepts(version2)); + Assert.True(actual21.Accepts(version2)); // Version3 - 3.4.5-alpha.9 - Assert.False(actual1.Equals(version3)); - Assert.False(actual2.Equals(version3)); - Assert.False(actual3.Equals(version3)); - Assert.False(actual4.Equals(version3)); - Assert.False(actual5.Equals(version3)); - Assert.False(actual6.Equals(version3)); - Assert.False(actual7.Equals(version3)); - Assert.False(actual8.Equals(version3)); - Assert.False(actual9.Equals(version3)); - Assert.False(actual10.Equals(version3)); - Assert.False(actual11.Equals(version3)); - Assert.False(actual12.Equals(version3)); - Assert.False(actual13.Equals(version3)); - Assert.False(actual14.Equals(version3)); - Assert.False(actual15.Equals(version3)); - Assert.True(actual16.Equals(version3)); - Assert.False(actual17.Equals(version3)); - Assert.True(actual18.Equals(version3)); - Assert.False(actual19.Equals(version3)); - Assert.True(actual20.Equals(version3)); - Assert.False(actual21.Equals(version3)); + Assert.False(actual1.Accepts(version3)); + Assert.False(actual2.Accepts(version3)); + Assert.False(actual3.Accepts(version3)); + Assert.False(actual4.Accepts(version3)); + Assert.False(actual5.Accepts(version3)); + Assert.False(actual6.Accepts(version3)); + Assert.False(actual7.Accepts(version3)); + Assert.False(actual8.Accepts(version3)); + Assert.False(actual9.Accepts(version3)); + Assert.False(actual10.Accepts(version3)); + Assert.False(actual11.Accepts(version3)); + Assert.False(actual12.Accepts(version3)); + Assert.False(actual13.Accepts(version3)); + Assert.False(actual14.Accepts(version3)); + Assert.False(actual15.Accepts(version3)); + Assert.True(actual16.Accepts(version3)); + Assert.False(actual17.Accepts(version3)); + Assert.True(actual18.Accepts(version3)); + Assert.False(actual19.Accepts(version3)); + Assert.True(actual20.Accepts(version3)); + Assert.False(actual21.Accepts(version3)); // Version4 - 3.4.5 - Assert.False(actual1.Equals(version4)); - Assert.False(actual2.Equals(version4)); - Assert.True(actual3.Equals(version4)); - Assert.True(actual4.Equals(version4)); - Assert.False(actual5.Equals(version4)); - Assert.False(actual6.Equals(version4)); - Assert.True(actual7.Equals(version4)); - Assert.False(actual8.Equals(version4)); - Assert.True(actual9.Equals(version4)); - Assert.True(actual10.Equals(version4)); - Assert.False(actual11.Equals(version4)); - Assert.False(actual12.Equals(version4)); - Assert.False(actual13.Equals(version4)); - Assert.False(actual14.Equals(version4)); - Assert.True(actual15.Equals(version4)); - Assert.True(actual16.Equals(version4)); - Assert.True(actual17.Equals(version4)); - Assert.True(actual18.Equals(version4)); - Assert.False(actual19.Equals(version4)); - Assert.True(actual20.Equals(version4)); - Assert.False(actual21.Equals(version4)); + Assert.False(actual1.Accepts(version4)); + Assert.False(actual2.Accepts(version4)); + Assert.True(actual3.Accepts(version4)); + Assert.True(actual4.Accepts(version4)); + Assert.False(actual5.Accepts(version4)); + Assert.False(actual6.Accepts(version4)); + Assert.True(actual7.Accepts(version4)); + Assert.False(actual8.Accepts(version4)); + Assert.True(actual9.Accepts(version4)); + Assert.True(actual10.Accepts(version4)); + Assert.False(actual11.Accepts(version4)); + Assert.False(actual12.Accepts(version4)); + Assert.False(actual13.Accepts(version4)); + Assert.False(actual14.Accepts(version4)); + Assert.True(actual15.Accepts(version4)); + Assert.True(actual16.Accepts(version4)); + Assert.True(actual17.Accepts(version4)); + Assert.True(actual18.Accepts(version4)); + Assert.False(actual19.Accepts(version4)); + Assert.True(actual20.Accepts(version4)); + Assert.False(actual21.Accepts(version4)); } /// @@ -216,4 +217,15 @@ public sealed class SemanticVersionTests Assert.True(actual8.CompareTo(actual1) < 0); Assert.True(actual8.CompareTo(actual2) > 0); } + + [Theory] + [InlineData("1.2.3")] + [InlineData("1.2.3-alpha.3+7223b39")] + [InlineData("3.4.5-alpha.9")] + [InlineData("3.4.5+7223b39")] + public void ToString_WhenValid_ShouldReturnString(string version) + { + Assert.True(SemanticVersion.TryParseVersion(version, out var actual)); + Assert.Equal(version, actual.ToString()); + } } diff --git a/tests/PSRule.Tests/TestEnumValue.cs b/tests/PSRule.Tests/TestEnumValue.cs new file mode 100644 index 000000000..fbdcfc8c6 --- /dev/null +++ b/tests/PSRule.Tests/TestEnumValue.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule; + +internal enum TestEnumValue +{ + None = 0, + + All = 1 +} diff --git a/tests/PSRule.Types.Tests/Data/ModuleConstraintTests.cs b/tests/PSRule.Types.Tests/Data/ModuleConstraintTests.cs new file mode 100644 index 000000000..0e03ee97b --- /dev/null +++ b/tests/PSRule.Types.Tests/Data/ModuleConstraintTests.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Data; + +/// +/// Tests for . +/// +public sealed class ModuleConstraintTests +{ + [Theory] + [InlineData("1.0.0")] + [InlineData("0.1.0+build.1")] + public void Any_WhenIncludePrereleaseIsFalse_ShouldAcceptStableVersions(string version) + { + var constraint = ModuleConstraint.Any("test", includePrerelease: false); + Assert.True(SemanticVersion.TryParseVersion(version, out var actualVersion)); + Assert.True(constraint.Accepts(actualVersion)); + } + + [Theory] + [InlineData("1.0.0-preview")] + [InlineData("0.1.0-alpha.1+build.1")] + public void Any_WhenIncludePrereleaseIsFalse_ShouldNotAcceptPrereleaseVersions(string version) + { + var constraint = ModuleConstraint.Any("test", includePrerelease: false); + Assert.True(SemanticVersion.TryParseVersion(version, out var actualVersion)); + Assert.False(constraint.Accepts(actualVersion)); + } + + [Theory] + [InlineData("1.0.0")] + [InlineData("0.1.0+build.1")] + [InlineData("1.0.0-preview")] + [InlineData("0.1.0-alpha.1+build.1")] + public void Any_WhenIncludePrereleaseIsTrue_ShouldAcceptStableOrPrereleaseVersions(string version) + { + var constraint = ModuleConstraint.Any("test", includePrerelease: true); + Assert.True(SemanticVersion.TryParseVersion(version, out var actualVersion)); + Assert.True(constraint.Accepts(actualVersion)); + } +} diff --git a/tests/PSRule.Types.Tests/GlobalUsings.cs b/tests/PSRule.Types.Tests/GlobalUsings.cs new file mode 100644 index 000000000..8c07c6cf4 --- /dev/null +++ b/tests/PSRule.Types.Tests/GlobalUsings.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +global using Xunit; diff --git a/tests/PSRule.Types.Tests/PSRule.Types.Tests.csproj b/tests/PSRule.Types.Tests/PSRule.Types.Tests.csproj new file mode 100644 index 000000000..5200d4db8 --- /dev/null +++ b/tests/PSRule.Types.Tests/PSRule.Types.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + PSRule + {8860178f-4b4a-4e28-8cc3-85dfe2a2fe4b} + 12.0 + enable + enable + + false + true + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + +