diff --git a/.vscode/settings.json b/.vscode/settings.json index ee2db9cdd8..f7fe937e3f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,7 +16,8 @@ "yaml.schemas": { "./schemas/PSRule-options.schema.json": [ "/tests/PSRule.Tests/PSRule.*.yml", - "/docs/scenarios/*/ps-rule.yaml" + "/docs/scenarios/*/ps-rule.yaml", + "/ps-rule.yaml" ], "./schemas/PSRule-language.schema.json": [ "/tests/PSRule.Tests/**.Rule.yaml", diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index ab00662844..bcce624632 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -12,8 +12,13 @@ See [upgrade notes][upgrade-notes] for helpful information when upgrading from p What's changed since pre-release v1.4.0-B2105004: +- General improvements: + - Improved support for version constraints by: + - Constraints can include prerelease versions of other matching versions. [#714](https://github.com/microsoft/PSRule/issues/714) + - Constraint sets allow multiple constraints to be joined together. [#715](https://github.com/microsoft/PSRule/issues/715) + - See [about_PSRule_Assert] for details. - Bug fixes: - - Fixed pre-release constraint handling for pre-releases versions. [#712](https://github.com/microsoft/PSRule/issues/712) + - Fixed prerelease constraint handling for prerelease versions. [#712](https://github.com/microsoft/PSRule/issues/712) ## v1.4.0-B2105004 (pre-release) diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Assert.md b/docs/concepts/PSRule/en-US/about_PSRule_Assert.md index 2dd539d938..a59cf35324 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Assert.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Assert.md @@ -1178,6 +1178,8 @@ 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. +Unless specified this defaults to `$False`. The following are supported constraints: @@ -1198,15 +1200,29 @@ The following are supported constraints: An empty, null or `*` constraint matches all valid semantic versions. +Multiple constraints can be joined together: + +- Use a _space_ to separate multiple constraints, each must be true (_logical AND_). +- Separates constraint sets with the double pipe `||`. +Only one constraint set must be true (_logical OR_). + +By example: + +- `1.2.3 || >=3.4.5 <5.0.0` results in: + - Pass: `1.2.3`, `3.4.5`, `3.5.0`, `4.9.9`. + - Fail: `3.0.0`, `5.0.0`. + Handling for pre-release versions: - 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 identifer 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 pre-release identifer will only match a stable version. +- A constraint without a pre-release identifer will only match a stable version by default. +Set `includePrerelease` to `$True` to include prerelease versions. - Constraints with a pre-release identifer will only match: - - Matching pre-release versions of the same major.minor.patch version. + - Matching pre-release versions of the same major.minor.patch version by default. + Set `includePrerelease` to `$True` to include prerelease versions of all matching versions. - Matching stable versions. By example: diff --git a/schemas/PSRule-options.schema.json b/schemas/PSRule-options.schema.json index 97d8044c8c..161114a4aa 100644 --- a/schemas/PSRule-options.schema.json +++ b/schemas/PSRule-options.schema.json @@ -513,8 +513,19 @@ "description": "Specifies the required version of a module to use.", "markdownDescription": "Specifies the required version of a module to use. [See help](https://microsoft.github.io/PSRule/concepts/PSRule/en-US/about_PSRule_Options.html#requires)", "additionalProperties": { - "type": "string" - } + "type": "string", + "title": "Version constraint", + "description": "Specifies a module to constrain to a specific version.", + "markdownDescription": "Specifies a module to constrain to a specific version. [See help](https://microsoft.github.io/PSRule/concepts/PSRule/en-US/about_PSRule_Options.html#requires)" + }, + "defaultSnippets": [ + { + "label": "Version constraint", + "body": { + "${1:Module}": "${2:'>=1.0.0'}" + } + } + ] }, "rule-option": { "type": "object", diff --git a/src/PSRule/Common/EnvironmentHelper.cs b/src/PSRule/Common/EnvironmentHelper.cs index e32a321ef4..88b388ff5a 100644 --- a/src/PSRule/Common/EnvironmentHelper.cs +++ b/src/PSRule/Common/EnvironmentHelper.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Collections; using System.Collections.Generic; using System.Net; using System.Security; diff --git a/src/PSRule/Common/KeyMapDictionary.cs b/src/PSRule/Common/KeyMapDictionary.cs index a7c764e4ef..425a29dba3 100644 --- a/src/PSRule/Common/KeyMapDictionary.cs +++ b/src/PSRule/Common/KeyMapDictionary.cs @@ -136,7 +136,7 @@ internal void Load(string prefix, EnvironmentHelper env, Func fo suffix = format(suffix); _Map[suffix] = (TValue)variable.Value; - } + } } } diff --git a/src/PSRule/Configuration/BaselineOption.cs b/src/PSRule/Configuration/BaselineOption.cs index da09d621bb..f97309b644 100644 --- a/src/PSRule/Configuration/BaselineOption.cs +++ b/src/PSRule/Configuration/BaselineOption.cs @@ -4,7 +4,6 @@ using PSRule.Definitions; using System.Collections; using System.Collections.Generic; -using System.Linq; namespace PSRule.Configuration { diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs index 486c261826..f67d9c3953 100644 --- a/src/PSRule/Pipeline/PipelineBuilder.cs +++ b/src/PSRule/Pipeline/PipelineBuilder.cs @@ -235,7 +235,7 @@ private bool GuardModuleVersion(string moduleName, string moduleVersion, string private static bool TryModuleVersion(string moduleVersion, string requiredVersion) { - if (!(SemanticVersion.TryParseVersion(moduleVersion, out SemanticVersion.Version version) && SemanticVersion.TryParseConstraint(requiredVersion, out SemanticVersion.Constraint constraint))) + if (!(SemanticVersion.TryParseVersion(moduleVersion, out SemanticVersion.Version version) && SemanticVersion.TryParseConstraint(requiredVersion, out SemanticVersion.IConstraint constraint))) return false; return constraint.Equals(version); diff --git a/src/PSRule/Pipeline/PipelineReciever.cs b/src/PSRule/Pipeline/PipelineReciever.cs index 52063d747d..b66bd6d033 100644 --- a/src/PSRule/Pipeline/PipelineReciever.cs +++ b/src/PSRule/Pipeline/PipelineReciever.cs @@ -4,7 +4,6 @@ using Newtonsoft.Json; using PSRule.Data; using PSRule.Parser; -using PSRule.Resources; using PSRule.Runtime; using System; using System.Collections; @@ -17,7 +16,6 @@ using YamlDotNet.Core.Events; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NodeDeserializers; -using YamlDotNet.Serialization.ValueDeserializers; namespace PSRule.Pipeline { diff --git a/src/PSRule/Runtime/Assert.cs b/src/PSRule/Runtime/Assert.cs index e9dfa63507..475973c479 100644 --- a/src/PSRule/Runtime/Assert.cs +++ b/src/PSRule/Runtime/Assert.cs @@ -8,7 +8,6 @@ using PSRule.Pipeline; using PSRule.Resources; using System; -using System.Collections; using System.IO; using System.Management.Automation; using System.Net; @@ -571,7 +570,7 @@ public AssertResult TypeOf(PSObject inputObject, string field, string[] type) /// /// The object field value should match the version constraint. Only applies to strings. /// - public AssertResult Version(PSObject inputObject, string field, string constraint = null) + public AssertResult Version(PSObject inputObject, string field, string constraint = null, bool includePrerelease = false) { // Guard parameters if (GuardNullParam(inputObject, nameof(inputObject), out AssertResult result) || @@ -580,7 +579,7 @@ public AssertResult Version(PSObject inputObject, string field, string constrain GuardSemanticVersion(fieldValue, out SemanticVersion.Version value, out result)) return result; - if (!Runtime.SemanticVersion.TryParseConstraint(constraint, out SemanticVersion.Constraint c)) + if (!SemanticVersion.TryParseConstraint(constraint, out SemanticVersion.IConstraint c, includePrerelease)) throw new RuleException(string.Format(Thread.CurrentThread.CurrentCulture, PSRuleResources.VersionConstraintInvalid, value)); // Assert diff --git a/src/PSRule/Runtime/ObjectHelper.cs b/src/PSRule/Runtime/ObjectHelper.cs index d70fbb9820..f53c13c485 100644 --- a/src/PSRule/Runtime/ObjectHelper.cs +++ b/src/PSRule/Runtime/ObjectHelper.cs @@ -385,7 +385,7 @@ private static IEnumerable GetIndexerProperties(Type baseType) else { var properties = baseType.GetProperties(); - foreach(PropertyInfo pi in properties) + foreach (PropertyInfo pi in properties) { var p = pi.GetIndexParameters(); if (p.Length > 0) diff --git a/src/PSRule/Runtime/SemanticVersion.cs b/src/PSRule/Runtime/SemanticVersion.cs index b51a961785..25b70a09b8 100644 --- a/src/PSRule/Runtime/SemanticVersion.cs +++ b/src/PSRule/Runtime/SemanticVersion.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Threading; @@ -23,6 +24,8 @@ internal static class SemanticVersion private const char DASH = '-'; private const char PLUS = '+'; private const char ZERO = '0'; + private const char PIPE = '|'; + private const char SPACE = ' '; [Flags] internal enum CompareFlag @@ -49,27 +52,102 @@ internal enum CompareFlag LessThan = 16 } + internal enum JoinOperator + { + None, + And, + Or + } + + internal interface IConstraint + { + bool Equals(Version version); + } + + internal sealed class VersionConstraint : IConstraint + { + private List _Constraints; + + public bool Equals(Version version) + { + if (_Constraints == null || _Constraints.Count == 0) + return true; + + var match = false; + var i = 0; + while (!match && i < _Constraints.Count) + { + var result = _Constraints[i].Equals(version); + + // True OR + if (_Constraints[i].Join == JoinOperator.Or && result) + return true; + + // True AND + if (_Constraints[i].Join == JoinOperator.And && result && ++i < _Constraints.Count) + continue; + + // False OR + if (_Constraints[i].Join == JoinOperator.Or && ++i < _Constraints.Count) + continue; + + // False AND + while (++i < _Constraints.Count) + { + // Move to after the next OR. + if (_Constraints[i].Join == JoinOperator.Or) + { + i++; + continue; + } + } + } + return false; + } + + internal void Join(int major, int minor, int patch, PR prid, CompareFlag flag, JoinOperator join, bool includePrerelease) + { + if (_Constraints == null) + _Constraints = new List(); + + _Constraints.Add(new ConstraintExpression( + major, + minor, + patch, + prid, + flag, + join == JoinOperator.None ? JoinOperator.Or : join, + includePrerelease + )); + } + } + [DebuggerDisplay("{_Major}.{_Minor}.{_Patch}")] - internal sealed class Constraint + internal sealed class ConstraintExpression : IConstraint { private readonly CompareFlag _Flag; private readonly int _Major; private readonly int _Minor; private readonly int _Patch; - private readonly PRID _PRID; + private readonly PR _PRID; + private readonly bool _IncludePrerelease; - internal Constraint(int major, int minor, int patch, PRID prid, CompareFlag flag) + internal ConstraintExpression(int major, int minor, int patch, PR prid, CompareFlag flag, JoinOperator join, bool includePrerelease) { _Flag = flag == CompareFlag.None ? CompareFlag.Equals : flag; _Major = major; _Minor = minor; _Patch = patch; _PRID = prid; + Join = join; + _IncludePrerelease = includePrerelease; } public bool Stable => IsStable(_PRID); - public static bool TryParse(string value, out Constraint constraint) + public JoinOperator Join { get; } + + public static bool TryParse(string value, out IConstraint constraint) { return TryParseConstraint(value, out constraint); } @@ -81,10 +159,10 @@ public bool Equals(System.Version version) public bool Equals(Version version) { - return Equals(version.Major, version.Minor, version.Patch, version.PreRelease); + return Equals(version.Major, version.Minor, version.Patch, version.Prerelease); } - public bool Equals(int major, int minor, int patch, PRID prid) + public bool Equals(int major, int minor, int patch, PR prid) { if (_Flag == CompareFlag.Equals) return EQ(major, minor, patch, prid); @@ -124,22 +202,22 @@ public bool Equals(int major, int minor, int patch, PRID prid) return true; } - private bool GuardLessOrEqual(int major, int minor, int patch, PRID prid) + private bool GuardLessOrEqual(int major, int minor, int patch, PR prid) { return _Flag == (CompareFlag.LessThan | CompareFlag.Equals) && !(LT(major, minor, patch, prid) || EQ(major, minor, patch, prid)); } - private bool GaurdLess(int major, int minor, int patch, PRID prid) + private bool GaurdLess(int major, int minor, int patch, PR prid) { return _Flag == CompareFlag.LessThan && !LT(major, minor, patch, prid); } - private bool GuardGreaterOrEqual(int major, int minor, int patch, PRID prid) + private bool GuardGreaterOrEqual(int major, int minor, int patch, PR prid) { return _Flag == (CompareFlag.GreaterThan | CompareFlag.Equals) && !(GT(major, minor, patch, prid) || EQ(major, minor, patch, prid)); } - private bool GuardGreater(int major, int minor, int patch, PRID prid) + private bool GuardGreater(int major, int minor, int patch, PR prid) { return _Flag == CompareFlag.GreaterThan && !GT(major, minor, patch, prid); } @@ -159,12 +237,15 @@ private bool GuardMajor(int major) return (_Flag == CompareFlag.MinorUplift || _Flag == CompareFlag.PatchUplift) && major != _Major; } - private bool GuardPRID(PRID prid) + private bool GuardPRID(PR prid) { + if (_IncludePrerelease) + return false; + return Stable && !IsStable(prid); } - private bool EQ(int major, int minor, int patch, PRID prid) + private bool EQ(int major, int minor, int patch, PR prid) { return EQCore(major, minor, patch) && PR(prid) == 0; } @@ -183,46 +264,44 @@ private bool GTCore(int major, int minor, int patch) (major == _Major && minor == _Minor && patch > _Patch); } + private bool LTCore(int major, int minor, int patch) + { + return (major < _Major) || + (major == _Major && minor < _Minor) || + (major == _Major && minor == _Minor && patch < _Patch); + } + /// /// Greater Than. /// - /// - /// When constraint is not pre-release the compared version must not be any pre-release version and be greater. - /// When constraint is a pre-release the compared version must be: - /// - A non-pre-release version of the >= major.minor.patch. - /// - A pre-release version == major.minor.patch with a greater pre-release identifer. - /// - private bool GT(int major, int minor, int patch, PRID prid) - { - var versionStable = prid == null || prid.Stable; - if (Stable && versionStable && GTCore(major, minor, patch)) - return true; + private bool GT(int major, int minor, int patch, PR prid) + { + if (!IsStable(prid) && !_IncludePrerelease) + return EQCore(major, minor, patch) && PR(prid) < 0; - return (versionStable && GTCore(major, minor, patch)) || - (!Stable && versionStable && EQCore(major, minor, patch)) || - (EQCore(major, minor, patch) && PR(prid) > 0); + return GTCore(major, minor, patch) || (EQCore(major, minor, patch) && PR(prid) < 0); } - private bool LT(int major, int minor, int patch, PRID prid) + /// + /// Less Than. + /// + private bool LT(int major, int minor, int patch, PR prid) { - if (prid != null && !prid.Stable) - return EQCore(major, minor, patch) && PR(prid) < 0; + if (!IsStable(prid) && !_IncludePrerelease) + return EQCore(major, minor, patch) && PR(prid) > 0; - return major < _Major || - (major == _Major && minor < _Minor) || - (major == _Major && minor == _Minor && patch < _Patch) || - (major == _Major && minor == _Minor && patch == _Patch && PR(prid) < 0); + return LTCore(major, minor, patch) || (EQCore(major, minor, patch) && PR(prid) > 0); } /// /// Compare pre-release. /// - private int PR(PRID prid) + private int PR(PR prid) { - return _PRID.Compare(prid); + return _PRID.CompareTo(prid); } - private static bool IsStable(PRID prid) + private static bool IsStable(PR prid) { return prid == null || prid.Stable; } @@ -233,15 +312,15 @@ internal sealed class Version : IComparable, IEquatable public readonly int Major; public readonly int Minor; public readonly int Patch; - public readonly PRID PreRelease; + public readonly PR Prerelease; public readonly string Build; - internal Version(int major, int minor, int patch, PRID prerelease, string build) + internal Version(int major, int minor, int patch, PR prerelease, string build) { Major = major; Minor = minor; Patch = patch; - PreRelease = prerelease; + Prerelease = prerelease; Build = build; } @@ -286,54 +365,80 @@ public int CompareTo(Version other) } } - internal sealed class PRID + [DebuggerDisplay("{Value}")] + internal sealed class PR { - public static readonly PRID Empty = new PRID(); + public static readonly PR Empty = new PR(); + + private static readonly char[] SEPARATOR = new char[] { '.' }; - private readonly string _Prerelease; - private readonly bool _Numeric; + private readonly string[] _Identifiers; - private PRID() + private PR() { - _Prerelease = null; + Value = string.Empty; + _Identifiers = null; } - internal PRID(string identifier, bool numeric) + internal PR(string value) { - _Prerelease = string.IsNullOrEmpty(identifier) ? null : identifier; - _Numeric = numeric; + Value = string.IsNullOrEmpty(value) ? string.Empty : value; + _Identifiers = string.IsNullOrEmpty(value) ? null : value.Split(SEPARATOR, StringSplitOptions.RemoveEmptyEntries); } - public bool Stable => _Prerelease == null; + public string Value { get; } + + public bool Stable => _Identifiers == null; - public int Compare(PRID identifier) + public int CompareTo(PR pr) { - if (identifier == null || identifier.Stable) - return Stable ? 0 : 1; + if (pr == null || pr.Stable) + return Stable ? 0 : -1; else if (Stable) - return -1; + return 1; - if (identifier._Numeric) - return _Numeric ? string.Compare(identifier._Prerelease, _Prerelease, StringComparison.Ordinal) : 1; + var i = -1; + var left = _Identifiers; + var right = pr._Identifiers; - if (identifier._Prerelease.Length == _Prerelease.Length) - return string.Compare(identifier._Prerelease, _Prerelease, StringComparison.Ordinal); + while (++i < left.Length && i < right.Length) + { + var leftNumeric = false; + var rightNumeric = false; + if (long.TryParse(left[i], out long l)) + leftNumeric = true; - var compareLength = identifier._Prerelease.Length > _Prerelease.Length ? _Prerelease.Length : identifier._Prerelease.Length; - var left = identifier._Prerelease.Substring(0, compareLength); - var right = _Prerelease.Substring(0, compareLength); - if (left == right) - return identifier._Prerelease.Length == compareLength ? -1 : 1; + if (long.TryParse(right[i], out long r)) + rightNumeric = true; - return string.Compare(left, right, StringComparison.Ordinal); + if (leftNumeric != rightNumeric) + return leftNumeric ? -1 : 1; + + if (leftNumeric && rightNumeric && l == r) + continue; + + if (leftNumeric && rightNumeric) + return l.CompareTo(r); + + var result = string.Compare(left[i], right[i], StringComparison.Ordinal); + if (result == 0) + continue; + + return result; + } + if (left.Length == right.Length) + return 0; + + return left.Length > right.Length ? 1 : -1; } public override string ToString() { - return _Prerelease.ToString(); + return Value.ToString(); } } + [DebuggerDisplay("Current = {_Current}, Position = {_Position}, Value = {_Value}")] private sealed class VersionStream { private readonly string _Value; @@ -357,7 +462,7 @@ internal void Next() _Current = _Value[_Position]; } - internal void GetConstraint(out CompareFlag flag) + internal void GetConstraintFlag(out CompareFlag flag) { flag = CompareFlag.None; while (!EOF && IsConstraint(_Current)) @@ -418,15 +523,18 @@ internal bool TrySegments(out int[] segments) Next(); } - if (_Current == DASH || _Current == PLUS) + if (IsIdentifier(_Current)) + return true; + + if (IsJoin(_Current)) return true; } return segmentIndex > 0; } - internal bool TryPrerelease(out PRID identifier) + internal bool ConsumePrerelease(out PR identifier) { - identifier = PRID.Empty; + identifier = PR.Empty; if (EOF || _Current != DASH) return true; @@ -447,11 +555,11 @@ internal bool TryPrerelease(out PRID identifier) if (numeric && id.Length > 1 && id[0] == ZERO) return false; - identifier = new PRID(id, numeric); + identifier = new PR(id); return true; } - internal bool TryBuild(out string label) + internal bool ConsumeBuild(out string label) { label = string.Empty; if (EOF || _Current != PLUS) @@ -469,6 +577,26 @@ internal bool TryBuild(out string label) return label.Length > 0; } + /// + /// 1.2.3 || 3.4.5 + /// >=1.2.3 <3.4.5 + /// + internal JoinOperator GetJoin() + { + var result = JoinOperator.None; + while ((_Current == SPACE || _Current == PIPE) && !EOF) + { + if (result == JoinOperator.None && _Current == SPACE) + result = JoinOperator.And; + + if (_Current == PIPE) + result = JoinOperator.Or; + + Next(); + } + return _Current != SPACE || _Current != PIPE ? result : JoinOperator.Or; + } + [DebuggerStepThrough()] private static bool IsConstraint(char c) { @@ -496,7 +624,7 @@ private static bool IsSeparator(char c) [DebuggerStepThrough()] private static bool IsAllowedChar(char c) { - return IsVersionDigit(c) || IsSeparator(c) || IsWildcard(c) || c == DASH || c == PLUS; + return IsVersionDigit(c) || IsSeparator(c) || IsWildcard(c) || IsIdentifier(c); } [DebuggerStepThrough()] @@ -515,6 +643,18 @@ private static bool IsBuildChar(char c) return char.IsDigit(c) || IsLetter(c); } + [DebuggerStepThrough()] + private static bool IsIdentifier(char c) + { + return c == DASH || c == PLUS; + } + + [DebuggerStepThrough()] + private static bool IsJoin(char c) + { + return c == SPACE || c == PIPE; + } + /// /// Is the character within the reduced set of allowed characters. a-z or A-Z. /// @@ -526,23 +666,24 @@ private static bool IsLetter(char c) } } - public static bool TryParseConstraint(string value, out Constraint constraint) + public static bool TryParseConstraint(string value, out IConstraint constraint, bool includePrerelease = false) { - constraint = null; + var c = new VersionConstraint(); + constraint = c; if (string.IsNullOrEmpty(value)) return true; var stream = new VersionStream(value); while (!stream.EOF) { - stream.GetConstraint(out CompareFlag flag); + stream.GetConstraintFlag(out CompareFlag flag); if (!stream.TrySegments(out int[] segments)) return false; - if (!stream.TryPrerelease(out PRID prerelease) || !stream.TryBuild(out _) || !stream.EOF) - return false; + stream.ConsumePrerelease(out PR prerelease); + stream.ConsumeBuild(out _); - constraint = new Constraint(segments[0], segments[1], segments[2], prerelease, flag); + c.Join(segments[0], segments[1], segments[2], prerelease, flag, stream.GetJoin(), includePrerelease); } return true; } @@ -557,10 +698,10 @@ public static bool TryParseVersion(string value, out Version version) if (!stream.TrySegments(out int[] segments)) return false; - if (!stream.TryPrerelease(out PRID prerelease)) + if (!stream.ConsumePrerelease(out PR prerelease)) return false; - stream.TryBuild(out string build); + stream.ConsumeBuild(out string build); version = new Version(segments[0], segments[1], segments[2], prerelease, build); return true; } diff --git a/tests/PSRule.Tests/AssertTests.cs b/tests/PSRule.Tests/AssertTests.cs index 1482ff5c84..f785ce545f 100644 --- a/tests/PSRule.Tests/AssertTests.cs +++ b/tests/PSRule.Tests/AssertTests.cs @@ -499,6 +499,10 @@ public void Version() Assert.True(assert.Version(value, "version", ">=1.2.3-0").Result); Assert.True(assert.Version(value, "version2", ">=1.2.3-0").Result); + Assert.True(assert.Version(value, "version", ">=1.2.3", includePrerelease: true).Result); + Assert.False(assert.Version(value, "version2", ">=1.2.3", includePrerelease: true).Result); + Assert.True(assert.Version(value, "version3", ">=1.2.3", includePrerelease: true).Result); + Assert.False(assert.Version(value, "notversion", null).Result); Assert.Throws(() => assert.Version(value, "version", "2.0.0<").Result); Assert.Throws(() => assert.Version(value, "version", "z2.0.0").Result); diff --git a/tests/PSRule.Tests/SemanticVersionTests.cs b/tests/PSRule.Tests/SemanticVersionTests.cs index 6d637490cf..8f34842e8c 100644 --- a/tests/PSRule.Tests/SemanticVersionTests.cs +++ b/tests/PSRule.Tests/SemanticVersionTests.cs @@ -14,15 +14,20 @@ public void Version() Assert.Equal(1, actual1.Major); Assert.Equal(2, actual1.Minor); Assert.Equal(3, actual1.Patch); - Assert.Equal("alpha.3", actual1.PreRelease.ToString()); + Assert.Equal("alpha.3", actual1.Prerelease.Value); Assert.Equal("7223b39", actual1.Build); - Assert.True(Runtime.SemanticVersion.TryParseVersion("v1.2.3-alpha.3+7223b39", out Runtime.SemanticVersion.Version actual2)); + Assert.True(Runtime.SemanticVersion.TryParseVersion("v1.2.3-alpha.3", out Runtime.SemanticVersion.Version actual2)); Assert.Equal(1, actual2.Major); Assert.Equal(2, actual2.Minor); Assert.Equal(3, actual2.Patch); - Assert.Equal("alpha.3", actual2.PreRelease.ToString()); - Assert.Equal("7223b39", actual2.Build); + Assert.Equal("alpha.3", actual2.Prerelease.Value); + + Assert.True(Runtime.SemanticVersion.TryParseVersion("v1.2.3+7223b39", out Runtime.SemanticVersion.Version actual3)); + Assert.Equal(1, actual3.Major); + Assert.Equal(2, actual3.Minor); + Assert.Equal(3, actual3.Patch); + Assert.Equal("7223b39", actual3.Build); } [Fact] @@ -39,20 +44,25 @@ public void Constraint() Assert.True(Runtime.SemanticVersion.TryParseVersion("1.2.3-0A", out Runtime.SemanticVersion.Version _)); // Constraints - Assert.True(Runtime.SemanticVersion.TryParseConstraint("1.2.3", out Runtime.SemanticVersion.Constraint actual1)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("1.2.3-alpha.3", out Runtime.SemanticVersion.Constraint actual2)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint(">1.2.3-alpha.3", out Runtime.SemanticVersion.Constraint actual3)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint(">1.2.3-alpha.1", out Runtime.SemanticVersion.Constraint actual4)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("<1.2.3-beta", out Runtime.SemanticVersion.Constraint actual5)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("^1.2.3-alpha", out Runtime.SemanticVersion.Constraint actual6)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("<3.4.6", out Runtime.SemanticVersion.Constraint actual7)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("=v1.2.3", out Runtime.SemanticVersion.Constraint actual8)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint(">=v1.2.3", out Runtime.SemanticVersion.Constraint actual9)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint(">=v1.2.3-0", out Runtime.SemanticVersion.Constraint actual10)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("<3.4.5", out Runtime.SemanticVersion.Constraint actual11)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("<3.4.5-9999999999", out Runtime.SemanticVersion.Constraint actual12)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("^1.0.0", out Runtime.SemanticVersion.Constraint actual13)); - Assert.True(Runtime.SemanticVersion.TryParseConstraint("<1.2.3-0", out Runtime.SemanticVersion.Constraint actual14)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint("1.2.3", out Runtime.SemanticVersion.IConstraint actual1)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint("1.2.3-alpha.3", out Runtime.SemanticVersion.IConstraint actual2)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint(">1.2.3-alpha.3", out Runtime.SemanticVersion.IConstraint actual3)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint(">1.2.3-alpha.1", out Runtime.SemanticVersion.IConstraint actual4)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint("<1.2.3-beta", out Runtime.SemanticVersion.IConstraint actual5)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint("^1.2.3-alpha", out Runtime.SemanticVersion.IConstraint actual6)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint("<3.4.6", out Runtime.SemanticVersion.IConstraint actual7)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint("=v1.2.3", out Runtime.SemanticVersion.IConstraint actual8)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint(">=v1.2.3", out Runtime.SemanticVersion.IConstraint actual9)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint(">=v1.2.3-0", out Runtime.SemanticVersion.IConstraint actual10)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint("<3.4.5", out Runtime.SemanticVersion.IConstraint actual11)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint("<3.4.5-9999999999", out Runtime.SemanticVersion.IConstraint actual12)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint("^1.0.0", out Runtime.SemanticVersion.IConstraint actual13)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint("<1.2.3-0", out Runtime.SemanticVersion.IConstraint actual14)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint("1.2.3|| >=3.4.5-0 3.4.5", out Runtime.SemanticVersion.IConstraint actual15)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint("1.2.3 ||>=3.4.5-0 || 3.4.5", out Runtime.SemanticVersion.IConstraint actual16)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint("1.2.3||3.4.5", out Runtime.SemanticVersion.IConstraint actual17)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint(">=1.2.3", out Runtime.SemanticVersion.IConstraint actual18, includePrerelease: true)); + Assert.True(Runtime.SemanticVersion.TryParseConstraint("<=3.4.5-0", out Runtime.SemanticVersion.IConstraint actual19, includePrerelease: true)); // Version1 - 1.2.3 Assert.True(actual1.Equals(version1)); @@ -69,6 +79,11 @@ public void Constraint() 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)); // Version2 - 1.2.3-alpha.3+7223b39 Assert.False(actual1.Equals(version2)); @@ -85,6 +100,11 @@ public void Constraint() 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)); // Version3 - 3.4.5-alpha.9 Assert.False(actual1.Equals(version3)); @@ -101,6 +121,11 @@ public void Constraint() 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)); // Version4 - 3.4.5 Assert.False(actual1.Equals(version4)); @@ -117,6 +142,36 @@ public void Constraint() 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)); + } + + [Fact] + public void Prerelease() + { + var actual1 = new Runtime.SemanticVersion.PR(null); + var actual2 = new Runtime.SemanticVersion.PR("alpha"); + var actual3 = new Runtime.SemanticVersion.PR("alpha.1"); + var actual4 = new Runtime.SemanticVersion.PR("alpha.beta"); + var actual5 = new Runtime.SemanticVersion.PR("beta"); + var actual6 = new Runtime.SemanticVersion.PR("beta.2"); + var actual7 = new Runtime.SemanticVersion.PR("beta.11"); + var actual8 = new Runtime.SemanticVersion.PR("rc.1"); + + Assert.True(actual1.CompareTo(actual1) == 0); + Assert.True(actual1.CompareTo(actual2) > 0); + Assert.True(actual1.CompareTo(actual6) > 0); + Assert.True(actual2.CompareTo(actual3) < 0); + Assert.True(actual3.CompareTo(actual4) < 0); + Assert.True(actual4.CompareTo(actual5) < 0); + Assert.True(actual5.CompareTo(actual6) < 0); + Assert.True(actual6.CompareTo(actual7) < 0); + Assert.True(actual7.CompareTo(actual8) < 0); + Assert.True(actual8.CompareTo(actual1) < 0); + Assert.True(actual8.CompareTo(actual2) > 0); } } }