Make SemanticVersion compatible with SemVer 2.0 (#5037)

Made 'SemanticVersion' compatible with SymVer 2.0 http://semver.org/ (p.10)
- Fix comparisons
- Refactor and add more tests
This commit is contained in:
Ilya
2017-11-09 07:44:58 -08:00
committed by Dongbo Wang
parent ab24ac2b8a
commit c4269cb6be
4 changed files with 445 additions and 180 deletions
@@ -43,6 +43,14 @@ namespace System.Management.Automation.Runspaces
"System.Version",
ViewsOf_System_Version());
yield return new ExtendedTypeDefinition(
"System.Version#IncludeLabel",
ViewsOf_System_Version_With_Label());
yield return new ExtendedTypeDefinition(
"System.Management.Automation.SemanticVersion",
ViewsOf_Semantic_Version_With_Label());
yield return new ExtendedTypeDefinition(
"System.Drawing.Printing.PrintDocument",
ViewsOf_System_Drawing_Printing_PrintDocument());
@@ -478,6 +486,46 @@ namespace System.Management.Automation.Runspaces
.EndTable());
}
private static IEnumerable<FormatViewDefinition> ViewsOf_System_Version_With_Label()
{
yield return new FormatViewDefinition("System.Version",
TableControl.Create()
.AddHeader(width: 6)
.AddHeader(width: 6)
.AddHeader(width: 6)
.AddHeader(width: 8)
.AddHeader(width: 26)
.AddHeader(width: 27)
.StartRowDefinition()
.AddPropertyColumn("Major")
.AddPropertyColumn("Minor")
.AddPropertyColumn("Build")
.AddPropertyColumn("Revision")
.AddPropertyColumn("PSSemVerPreReleaseLabel")
.AddPropertyColumn("PSSemVerBuildLabel")
.EndRowDefinition()
.EndTable());
}
private static IEnumerable<FormatViewDefinition> ViewsOf_Semantic_Version_With_Label()
{
yield return new FormatViewDefinition("System.Management.Automation.SemanticVersion",
TableControl.Create()
.AddHeader(width: 6)
.AddHeader(width: 6)
.AddHeader(width: 6)
.AddHeader(width: 9)
.AddHeader(width: 11)
.StartRowDefinition()
.AddPropertyColumn("Major")
.AddPropertyColumn("Minor")
.AddPropertyColumn("Patch")
.AddPropertyColumn("PreReleaseLabel")
.AddPropertyColumn("BuildLabel")
.EndRowDefinition()
.EndTable());
}
private static IEnumerable<FormatViewDefinition> ViewsOf_System_Drawing_Printing_PrintDocument()
{
yield return new FormatViewDefinition("System.Drawing.Printing.PrintDocument",
@@ -7,6 +7,8 @@ using System.Reflection;
using System.Collections;
using System.Globalization;
using System.Management.Automation.Internal;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Win32;
namespace System.Management.Automation
@@ -384,12 +386,18 @@ namespace System.Management.Automation
/// </summary>
public sealed class SemanticVersion : IComparable, IComparable<SemanticVersion>, IEquatable<SemanticVersion>
{
private const string VersionSansRegEx = @"^(?<major>\d+)(\.(?<minor>\d+))?(\.(?<patch>\d+))?$";
private const string LabelRegEx = @"^((?<preLabel>[0-9A-Za-z][0-9A-Za-z\-\.]*))?(\+(?<buildLabel>[0-9A-Za-z][0-9A-Za-z\-\.]*))?$";
private const string LabelUnitRegEx = @"^[0-9A-Za-z][0-9A-Za-z\-\.]*$";
private const string PreLabelPropertyName = "PSSemVerPreReleaseLabel";
private const string BuildLabelPropertyName = "PSSemVerBuildLabel";
private const string TypeNameForVersionWithLabel = "System.Version#IncludeLabel";
private string versionString;
/// <summary>
/// Construct a SemanticVersion from a string.
/// </summary>
/// <param name="version">The version to parse</param>
/// <exception cref="PSArgumentException"></exception>
/// <exception cref="ValidationMetadataException"></exception>
/// <exception cref="FormatException"></exception>
/// <exception cref="OverflowException"></exception>
public SemanticVersion(string version)
@@ -398,8 +406,39 @@ namespace System.Management.Automation
Major = v.Major;
Minor = v.Minor;
Patch = v.Patch;
Label = v.Label;
Patch = v.Patch < 0 ? 0 : v.Patch;
PreReleaseLabel = v.PreReleaseLabel;
BuildLabel = v.BuildLabel;
}
/// <summary>
/// Construct a SemanticVersion.
/// </summary>
/// <param name="major">The major version</param>
/// <param name="minor">The minor version</param>
/// <param name="patch">The patch version</param>
/// <param name="preReleaseLabel">The pre-release label for the version</param>
/// <param name="buildLabel">The build metadata for the version</param>
/// <exception cref="FormatException">
/// If <paramref name="preReleaseLabel"/> don't match 'LabelUnitRegEx'.
/// If <paramref name="buildLabel"/> don't match 'LabelUnitRegEx'.
/// </exception>
public SemanticVersion(int major, int minor, int patch, string preReleaseLabel, string buildLabel)
: this(major, minor, patch)
{
if (!string.IsNullOrEmpty(preReleaseLabel))
{
if (!Regex.IsMatch(preReleaseLabel, LabelUnitRegEx)) throw new FormatException(nameof(preReleaseLabel));
PreReleaseLabel = preReleaseLabel;
}
if (!string.IsNullOrEmpty(buildLabel))
{
if (!Regex.IsMatch(buildLabel, LabelUnitRegEx)) throw new FormatException(nameof(buildLabel));
BuildLabel = buildLabel;
}
}
/// <summary>
@@ -410,17 +449,23 @@ namespace System.Management.Automation
/// <param name="patch">The minor version</param>
/// <param name="label">The label for the version</param>
/// <exception cref="PSArgumentException">
/// If <paramref name="major"/>, <paramref name="minor"/>, or <paramref name="patch"/> is less than 0.
/// </exception>
/// <exception cref="PSArgumentNullException">
/// If <paramref name="label"/> is null or an empty string.
/// <exception cref="FormatException">
/// If <paramref name="label"/> don't match 'LabelRegEx'.
/// </exception>
public SemanticVersion(int major, int minor, int patch, string label)
: this(major, minor, patch)
{
if (string.IsNullOrEmpty(label)) throw PSTraceSource.NewArgumentNullException(nameof(label));
// We presume the SymVer :
// 1) major.minor.patch-label
// 2) 'label' starts with letter or digit.
if (!string.IsNullOrEmpty(label))
{
var match = Regex.Match(label, LabelRegEx);
if (!match.Success) throw new FormatException(nameof(label));
Label = label;
PreReleaseLabel = match.Groups["preLabel"].Value;
BuildLabel = match.Groups["buildLabel"].Value;
}
}
/// <summary>
@@ -441,7 +486,9 @@ namespace System.Management.Automation
Major = major;
Minor = minor;
Patch = patch;
Label = null;
// We presume:
// PreReleaseLabel = null;
// BuildLabel = null;
}
/// <summary>
@@ -463,43 +510,66 @@ namespace System.Management.Automation
/// </exception>
public SemanticVersion(int major) : this(major, 0, 0) {}
private const string LabelPropertyName = "PSSemanticVersionLabel";
/// <summary>
/// Construct a <see cref="SemanticVersion"/> from a <see cref="Version"/>,
/// copying the NoteProperty storing the label if the expected property exists.
/// </summary>
/// <param name="version">The version.</param>
/// <exception cref="ArgumentNullException">
/// If <paramref name="version"/> is null.
/// </exception>
/// <exception cref="PSArgumentException">
/// If <paramref name="version.Revision"/> is more than 0.
/// </exception>
public SemanticVersion(Version version)
{
if (version == null) throw PSTraceSource.NewArgumentNullException(nameof(version));
if (version.Revision > 0) throw PSTraceSource.NewArgumentException(nameof(version));
Major = version.Major;
Minor = version.Minor;
Patch = version.Build == -1 ? 0 : version.Build;
var psobj = new PSObject(version);
var labelNote = psobj.Properties[LabelPropertyName];
if (labelNote != null)
var preLabelNote = psobj.Properties[PreLabelPropertyName];
if (preLabelNote != null)
{
Label = labelNote.Value as string;
PreReleaseLabel = preLabelNote.Value as string;
}
var buildLabelNote = psobj.Properties[BuildLabelPropertyName];
if (buildLabelNote != null)
{
BuildLabel = buildLabelNote.Value as string;
}
}
/// <summary>
/// Convert a <see cref="SemanticVersion"/> to a <see cref="Version"/>.
/// If there is a <see cref="Label"/>, it is added as a NoteProperty to the
/// result so that you can round trip back to a <see cref="SemanticVersion"/>
/// without losing the label.
/// If there is a <see cref="PreReleaseLabel"/> or/and a <see cref="BuildLabel"/>,
/// it is added as a NoteProperty to the result so that you can round trip
/// back to a <see cref="SemanticVersion"/> without losing the label.
/// </summary>
/// <param name="semver"></param>
public static implicit operator Version(SemanticVersion semver)
{
PSObject psobj;
var result = new Version(semver.Major, semver.Minor, semver.Patch);
if (!string.IsNullOrEmpty(semver.Label))
if (!string.IsNullOrEmpty(semver.PreReleaseLabel) || !string.IsNullOrEmpty(semver.BuildLabel))
{
var psobj = new PSObject(result);
psobj.Properties.Add(new PSNoteProperty(LabelPropertyName, semver.Label));
psobj = new PSObject(result);
if (!string.IsNullOrEmpty(semver.PreReleaseLabel))
{
psobj.Properties.Add(new PSNoteProperty(PreLabelPropertyName, semver.PreReleaseLabel));
}
if (!string.IsNullOrEmpty(semver.BuildLabel))
{
psobj.Properties.Add(new PSNoteProperty(BuildLabelPropertyName, semver.BuildLabel));
}
psobj.TypeNames.Insert(0, TypeNameForVersionWithLabel);
}
return result;
@@ -521,9 +591,14 @@ namespace System.Management.Automation
public int Patch { get; }
/// <summary>
/// The last component in a SemanticVersion - may be null if not specified.
/// PreReleaseLabel position in the SymVer string 'major.minor.patch-PreReleaseLabel+BuildLabel'.
/// </summary>
public string Label { get; }
public string PreReleaseLabel { get; }
/// <summary>
/// BuildLabel position in the SymVer string 'major.minor.patch-PreReleaseLabel+BuildLabel'.
/// </summary>
public string BuildLabel { get; }
/// <summary>
/// Parse <paramref name="version"/> and return the result if it is a valid <see cref="SemanticVersion"/>, otherwise throws an exception.
@@ -531,12 +606,12 @@ namespace System.Management.Automation
/// <param name="version">The string to parse</param>
/// <returns></returns>
/// <exception cref="PSArgumentException"></exception>
/// <exception cref="ValidationMetadataException"></exception>
/// <exception cref="FormatException"></exception>
/// <exception cref="OverflowException"></exception>
public static SemanticVersion Parse(string version)
{
if (version == null) throw PSTraceSource.NewArgumentNullException(nameof(version));
if (version == String.Empty) throw new FormatException(nameof(version));
var r = new VersionResult();
r.Init(true);
@@ -571,77 +646,151 @@ namespace System.Management.Automation
private static bool TryParseVersion(string version, ref VersionResult result)
{
if (version.EndsWith('-') || version.EndsWith('+') || version.EndsWith('.'))
{
result.SetFailure(ParseFailureKind.FormatException);
return false;
}
string versionSansLabel = null;
var major=0;
var minor=0;
var patch=0;
string preLabel = null;
string buildLabel = null;
// We parse the SymVer 'version' string 'major.minor.patch-PreReleaseLabel+BuildLabel'.
var dashIndex = version.IndexOf('-');
var plusIndex = version.IndexOf('+');
// Empty label?
if (dashIndex == version.Length - 1)
if (dashIndex > plusIndex)
{
result.SetFailure(ParseFailureKind.ArgumentException);
// 'PreReleaseLabel' can contains dashes.
if (plusIndex == -1)
{
// No buildLabel: buildLabel == null
// Format is 'major.minor.patch-PreReleaseLabel'
preLabel = version.Substring(dashIndex+1);
versionSansLabel = version.Substring(0, dashIndex);
}
else
{
// No PreReleaseLabel: preLabel == null
// Format is 'major.minor.patch+BuildLabel'
buildLabel = version.Substring(plusIndex+1);
versionSansLabel = version.Substring(0, plusIndex);
dashIndex = -1;
}
}
else
{
if (dashIndex == -1)
{
// Here dashIndex == plusIndex == -1
// No preLabel - preLabel == null;
// No buildLabel - buildLabel == null;
// Format is 'major.minor.patch'
versionSansLabel = version;
}
else
{
// Format is 'major.minor.patch-PreReleaseLabel+BuildLabel'
preLabel = version.Substring(dashIndex+1, plusIndex-dashIndex-1);
buildLabel = version.Substring(plusIndex+1);
versionSansLabel = version.Substring(0, dashIndex);
}
}
if ((dashIndex != - 1 && String.IsNullOrEmpty(preLabel)) ||
(plusIndex != - 1 && String.IsNullOrEmpty(buildLabel)) ||
String.IsNullOrEmpty(versionSansLabel))
{
// We have dash and no preReleaseLabel or
// we have plus and no buildLabel or
// we have no main version part (versionSansLabel==null)
result.SetFailure(ParseFailureKind.FormatException);
return false;
}
var versionSansLabel = (dashIndex < 0) ? version : version.Substring(0, dashIndex);
string[] parsedComponents = versionSansLabel.Split(Utils.Separators.Dot);
if (parsedComponents.Length > 3)
var match = Regex.Match(versionSansLabel, VersionSansRegEx);
if (!match.Success)
{
result.SetFailure(ParseFailureKind.ArgumentException);
result.SetFailure(ParseFailureKind.FormatException);
return false;
}
int major = 0, minor = 0, patch = 0;
if (!TryParseComponent(parsedComponents[0], "major", ref result, out major))
if (!int.TryParse(match.Groups["major"].Value, out major))
{
result.SetFailure(ParseFailureKind.FormatException);
return false;
}
if (parsedComponents.Length >= 2 && !TryParseComponent(parsedComponents[1], "minor", ref result, out minor))
if (match.Groups["minor"].Success && !int.TryParse(match.Groups["minor"].Value, out minor))
{
result.SetFailure(ParseFailureKind.FormatException);
return false;
}
if (parsedComponents.Length == 3 && !TryParseComponent(parsedComponents[2], "patch", ref result, out patch))
if (match.Groups["patch"].Success && !int.TryParse(match.Groups["patch"].Value, out patch))
{
result.SetFailure(ParseFailureKind.FormatException);
return false;
}
result._parsedVersion = dashIndex < 0
? new SemanticVersion(major, minor, patch)
: new SemanticVersion(major, minor, patch, version.Substring(dashIndex + 1));
return true;
}
private static bool TryParseComponent(string component, string componentName, ref VersionResult result, out int parsedComponent)
{
if (!Int32.TryParse(component, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsedComponent))
if (preLabel != null && !Regex.IsMatch(preLabel, LabelUnitRegEx) ||
(buildLabel != null && !Regex.IsMatch(buildLabel, LabelUnitRegEx)))
{
result.SetFailure(ParseFailureKind.FormatException, component);
return false;
}
if (parsedComponent < 0)
{
result.SetFailure(ParseFailureKind.ArgumentOutOfRangeException, componentName);
result.SetFailure(ParseFailureKind.FormatException);
return false;
}
result._parsedVersion = new SemanticVersion(major, minor, patch, preLabel, buildLabel);
return true;
}
/// <summary>
/// ToString
/// Implement ToString()
/// </summary>
public override string ToString()
{
if (Patch < 0)
if (versionString == null)
{
return string.IsNullOrEmpty(Label)
? StringUtil.Format("{0}.{1}", Major, Minor)
: StringUtil.Format("{0}.{1}-{2}", Major, Minor, Label);
StringBuilder result = new StringBuilder();
result.Append(Major).Append(Utils.Separators.Dot).Append(Minor).Append(Utils.Separators.Dot).Append(Patch);
if (!string.IsNullOrEmpty(PreReleaseLabel))
{
result.Append("-").Append(PreReleaseLabel);
}
if (!string.IsNullOrEmpty(BuildLabel))
{
result.Append("+").Append(BuildLabel);
}
versionString = result.ToString();
}
return string.IsNullOrEmpty(Label)
? StringUtil.Format("{0}.{1}.{2}", Major, Minor, Patch)
: StringUtil.Format("{0}.{1}.{2}-{3}", Major, Minor, Patch, Label);
return versionString;
}
/// <summary>
/// Implement Compare.
/// </summary>
public static int Compare(SemanticVersion versionA, SemanticVersion versionB)
{
if (versionA != null)
{
return versionA.CompareTo(versionB);
}
if (versionB != null)
{
return -1;
}
return 0;
}
/// <summary>
@@ -664,7 +813,8 @@ namespace System.Management.Automation
}
/// <summary>
/// Implement <see cref="IComparable{T}.CompareTo"/>
/// Implement <see cref="IComparable{T}.CompareTo"/>.
/// Meets SymVer 2.0 p.11 http://semver.org/
/// </summary>
public int CompareTo(SemanticVersion value)
{
@@ -680,16 +830,8 @@ namespace System.Management.Automation
if (Patch != value.Patch)
return Patch > value.Patch ? 1 : -1;
if (Label == null)
return value.Label == null ? 0 : 1;
if (value.Label == null)
return -1;
if (!string.Equals(Label, value.Label, StringComparison.Ordinal))
return string.Compare(Label, value.Label, StringComparison.Ordinal);
return 0;
// SymVer 2.0 standard requires to ignore 'BuildLabel' (Build metadata).
return ComparePreLabel(this.PreReleaseLabel, value.PreReleaseLabel);
}
/// <summary>
@@ -705,9 +847,10 @@ namespace System.Management.Automation
/// </summary>
public bool Equals(SemanticVersion other)
{
// SymVer 2.0 standard requires to ignore 'BuildLabel' (Build metadata).
return other != null &&
(Major == other.Major) && (Minor == other.Minor) && (Patch == other.Patch) &&
string.Equals(Label, other.Label, StringComparison.Ordinal);
string.Equals(PreReleaseLabel, other.PreReleaseLabel, StringComparison.Ordinal);
}
/// <summary>
@@ -715,11 +858,7 @@ namespace System.Management.Automation
/// </summary>
public override int GetHashCode()
{
return Utils.CombineHashCodes(
Major.GetHashCode(),
Minor.GetHashCode(),
Patch.GetHashCode(),
Label == null ? 0 : Label.GetHashCode());
return this.ToString().GetHashCode();
}
/// <summary>
@@ -748,8 +887,7 @@ namespace System.Management.Automation
/// </summary>
public static bool operator <(SemanticVersion v1, SemanticVersion v2)
{
if ((object)v1 == null) throw PSTraceSource.NewArgumentException(nameof(v1));
return (v1.CompareTo(v2) < 0);
return (Compare(v1, v2) < 0);
}
/// <summary>
@@ -757,8 +895,7 @@ namespace System.Management.Automation
/// </summary>
public static bool operator <=(SemanticVersion v1, SemanticVersion v2)
{
if ((object)v1 == null) throw PSTraceSource.NewArgumentException(nameof(v1));
return (v1.CompareTo(v2) <= 0);
return (Compare(v1, v2) <= 0);
}
/// <summary>
@@ -766,7 +903,7 @@ namespace System.Management.Automation
/// </summary>
public static bool operator >(SemanticVersion v1, SemanticVersion v2)
{
return (v2 < v1);
return (Compare(v1, v2) > 0);
}
/// <summary>
@@ -774,7 +911,51 @@ namespace System.Management.Automation
/// </summary>
public static bool operator >=(SemanticVersion v1, SemanticVersion v2)
{
return (v2 <= v1);
return (Compare(v1, v2) >= 0);
}
private static int ComparePreLabel(string preLabel1, string preLabel2)
{
// Symver 2.0 standard p.9
// Pre-release versions have a lower precedence than the associated normal version.
// Comparing each dot separated identifier from left to right
// until a difference is found as follows:
// identifiers consisting of only digits are compared numerically
// and identifiers with letters or hyphens are compared lexically in ASCII sort order.
// Numeric identifiers always have lower precedence than non-numeric identifiers.
// A larger set of pre-release fields has a higher precedence than a smaller set,
// if all of the preceding identifiers are equal.
if (String.IsNullOrEmpty(preLabel1)) { return String.IsNullOrEmpty(preLabel2) ? 0 : 1; }
if (String.IsNullOrEmpty(preLabel2)) { return -1; }
var units1 = preLabel1.Split('.');
var units2 = preLabel2.Split('.');
var minLength = units1.Length < units2.Length ? units1.Length : units2.Length;
for (int i = 0; i < minLength; i++)
{
var ac = units1[i];
var bc = units2[i];
int number1, number2;
var isNumber1 = Int32.TryParse(ac, out number1);
var isNumber2 = Int32.TryParse(bc, out number2);
if (isNumber1 && isNumber2)
{
if (number1 != number2) { return number1 < number2 ? -1 : 1; }
}
else
{
if (isNumber1) { return -1; }
if (isNumber2) { return 1; }
int result = String.CompareOrdinal(ac, bc);
if (result != 0) { return result; }
}
}
return units1.Length.CompareTo(units2.Length);
}
internal enum ParseFailureKind
@@ -1982,6 +1982,22 @@ namespace System.Management.Automation.Runspaces
var td251 = new TypeData(@"Deserialized.System.Management.Automation.DebuggerCommandResults", true);
td251.TargetTypeForDeserialization = typeof(Microsoft.PowerShell.DeserializingTypeConverter);
yield return td251;
var td252 = new TypeData(@"System.Version#IncludeLabel", true);
td252.Members.Add("ToString",
new ScriptMethodData(@"ToString", GetScriptBlock(@"
$suffix = """"
if (![String]::IsNullOrEmpty($this.PSSemVerPreReleaseLabel))
{
$suffix = ""-""+$this.PSSemVerPreReleaseLabel
}
if (![String]::IsNullOrEmpty($this.PSSemVerBuildLabel))
{
$suffix += ""+""+$this.PSSemVerBuildLabel
}
""$($this.Major).$($this.Minor).$($this.Build)""+$suffix
")));
yield return td252;
}
}
}
@@ -2,46 +2,56 @@ using namespace System.Management.Automation
using namespace System.Management.Automation.Language
Describe "SemanticVersion api tests" -Tags 'CI' {
Context "constructing valid versions" {
It "string argument constructor" {
$v = [SemanticVersion]::new("1.2.3-alpha")
Context "Constructing valid versions" {
It "String argument constructor" {
$v = [SemanticVersion]::new("1.2.3-Alpha-super.3+BLD.a1-xxx.03")
$v.Major | Should Be 1
$v.Minor | Should Be 2
$v.Patch | Should Be 3
$v.Label | Should Be "alpha"
$v.ToString() | Should Be "1.2.3-alpha"
$v.PreReleaseLabel | Should Be "Alpha-super.3"
$v.BuildLabel | Should Be "BLD.a1-xxx.03"
$v.ToString() | Should Be "1.2.3-Alpha-super.3+BLD.a1-xxx.03"
$v = [SemanticVersion]::new("1.0.0")
$v.Major | Should Be 1
$v.Minor | Should Be 0
$v.Patch | Should Be 0
$v.Label | Should BeNullOrEmpty
$v.PreReleaseLabel | Should BeNullOrEmpty
$v.BuildLabel | Should BeNullOrEmpty
$v.ToString() | Should Be "1.0.0"
$v = [SemanticVersion]::new("3.0")
$v.Major | Should Be 3
$v.Minor | Should Be 0
$v.Patch | Should Be 0
$v.Label | Should BeNullOrEmpty
$v.PreReleaseLabel | Should BeNullOrEmpty
$v.BuildLabel | Should BeNullOrEmpty
$v.ToString() | Should Be "3.0.0"
$v = [SemanticVersion]::new("2")
$v.Major | Should Be 2
$v.Minor | Should Be 0
$v.Patch | Should Be 0
$v.Label | Should BeNullOrEmpty
$v.PreReleaseLabel | Should BeNullOrEmpty
$v.BuildLabel | Should BeNullOrEmpty
$v.ToString() | Should Be "2.0.0"
}
# After the above test, we trust the properties and rely on ToString for validation
# After the above test, we trust the properties and rely on ToString for validation
It "int args constructor" {
It "Int args constructor" {
$v = [SemanticVersion]::new(1, 0, 0)
$v.ToString() | Should Be "1.0.0"
$v = [SemanticVersion]::new(3, 2, 0, "beta.1")
$v.ToString() | Should Be "3.2.0-beta.1"
$v = [SemanticVersion]::new(3, 2, 0, "beta.1+meta")
$v.ToString() | Should Be "3.2.0-beta.1+meta"
$v = [SemanticVersion]::new(3, 2, 0, "beta.1", "meta")
$v.ToString() | Should Be "3.2.0-beta.1+meta"
$v = [SemanticVersion]::new(3, 1)
$v.ToString() | Should Be "3.1.0"
@@ -49,7 +59,7 @@ Describe "SemanticVersion api tests" -Tags 'CI' {
$v.ToString() | Should Be "3.0.0"
}
It "version arg constructor" {
It "Version arg constructor" {
$v = [SemanticVersion]::new([Version]::new(1, 2))
$v.ToString() | Should Be '1.2.0'
@@ -57,37 +67,63 @@ Describe "SemanticVersion api tests" -Tags 'CI' {
$v.ToString() | Should Be '1.2.3'
}
It "semantic version can round trip through version" {
$v1 = [SemanticVersion]::new(3, 2, 1, "prerelease")
It "Can covert to 'Version' type" {
$v1 = [SemanticVersion]::new(3, 2, 1, "prerelease", "meta")
$v2 = [Version]$v1
$v2.GetType() | Should Be "Version"
$v2.PSobject.TypeNames[0] | Should Be "System.Version#IncludeLabel"
$v2.Major | Should Be 3
$v2.Minor | Should Be 2
$v2.Build | Should Be 1
$v2.PSSemVerPreReleaseLabel | Should Be "prerelease"
$v2.PSSemVerBuildLabel | Should Be "meta"
$v2.ToString() | Should Be "3.2.1-prerelease+meta"
}
It "Semantic version can round trip through version" {
$v1 = [SemanticVersion]::new(3, 2, 1, "prerelease", "meta")
$v2 = [SemanticVersion]::new([Version]$v1)
$v2.ToString() | Should Be "3.2.1-prerelease"
$v2.ToString() | Should Be "3.2.1-prerelease+meta"
}
}
Context "Comparisons" {
$v1_0_0 = [SemanticVersion]::new(1, 0, 0)
$v1_1_0 = [SemanticVersion]::new(1, 1, 0)
$v1_1_1 = [SemanticVersion]::new(1, 1, 1)
$v2_1_0 = [SemanticVersion]::new(2, 1, 0)
$v1_0_0_alpha = [SemanticVersion]::new(1, 0, 0, "alpha")
$v1_0_0_beta = [SemanticVersion]::new(1, 0, 0, "beta")
BeforeAll {
$v1_0_0 = [SemanticVersion]::new(1, 0, 0)
$v1_1_0 = [SemanticVersion]::new(1, 1, 0)
$v1_1_1 = [SemanticVersion]::new(1, 1, 1)
$v2_1_0 = [SemanticVersion]::new(2, 1, 0)
$v1_0_0_alpha = [SemanticVersion]::new(1, 0, 0, "alpha.1.1")
$v1_0_0_alpha2 = [SemanticVersion]::new(1, 0, 0, "alpha.1.2")
$v1_0_0_beta = [SemanticVersion]::new(1, 0, 0, "beta")
$v1_0_0_betaBuild = [SemanticVersion]::new(1, 0, 0, "beta", "BUILD")
$testCases = @(
@{ lhs = $v1_0_0; rhs = $v1_1_0 }
@{ lhs = $v1_0_0; rhs = $v1_1_1 }
@{ lhs = $v1_1_0; rhs = $v1_1_1 }
@{ lhs = $v1_0_0; rhs = $v2_1_0 }
@{ lhs = $v1_0_0_alpha; rhs = $v1_0_0_beta }
@{ lhs = $v1_0_0_alpha; rhs = $v1_0_0_alpha2 }
@{ lhs = $v1_0_0_alpha; rhs = $v1_0_0 }
@{ lhs = $v1_0_0_beta; rhs = $v1_0_0 }
@{ lhs = $v2_1_0; rhs = "3.0"}
@{ lhs = "1.5"; rhs = $v2_1_0}
)
}
It "Build meta should be ignored" {
$v1_0_0_beta -eq $v1_0_0_betaBuild | Should Be $true
$v1_0_0_betaBuild -lt $v1_0_0_beta | Should Be $false
$v1_0_0_beta -lt $v1_0_0_betaBuild | Should Be $false
}
$testCases = @(
@{ lhs = $v1_0_0; rhs = $v1_1_0 }
@{ lhs = $v1_0_0; rhs = $v1_1_1 }
@{ lhs = $v1_1_0; rhs = $v1_1_1 }
@{ lhs = $v1_0_0; rhs = $v2_1_0 }
@{ lhs = $v1_0_0_alpha; rhs = $v1_0_0_beta }
@{ lhs = $v1_0_0_alpha; rhs = $v1_0_0 }
@{ lhs = $v1_0_0_beta; rhs = $v1_0_0 }
@{ lhs = $v2_1_0; rhs = "3.0"}
@{ lhs = "1.5"; rhs = $v2_1_0}
)
It "<lhs> less than <rhs>" -TestCases $testCases {
param($lhs, $rhs)
$lhs -lt $rhs | Should Be $true
$rhs -lt $lhs | Should Be $false
}
It "<lhs> less than or equal <rhs>" -TestCases $testCases {
param($lhs, $rhs)
$lhs -le $rhs | Should Be $true
@@ -95,11 +131,13 @@ Describe "SemanticVersion api tests" -Tags 'CI' {
$lhs -le $lhs | Should Be $true
$rhs -le $rhs | Should Be $true
}
It "<lhs> greater than <rhs>" -TestCases $testCases {
param($lhs, $rhs)
$lhs -gt $rhs | Should Be $false
$rhs -gt $lhs | Should Be $true
}
It "<lhs> greater than or equal <rhs>" -TestCases $testCases {
param($lhs, $rhs)
$lhs -ge $rhs | Should Be $false
@@ -108,11 +146,10 @@ Describe "SemanticVersion api tests" -Tags 'CI' {
$rhs -ge $rhs | Should Be $true
}
$testCases = @(
It "Equality <operand>" -TestCases @(
@{ operand = $v1_0_0 }
@{ operand = $v1_0_0_alpha }
)
It "Equality <operand>" -TestCases $testCases {
) {
param($operand)
$operand -eq $operand | Should Be $true
$operand -ne $operand | Should Be $false
@@ -134,84 +171,67 @@ Describe "SemanticVersion api tests" -Tags 'CI' {
}
}
Context "error handling" {
Context "Error handling" {
# The specific errors aren't too useful here, but noted in comments
# so when we pick up a version of Pester that will let us check FullyQualifiedErrorId,
# it's easier to tweak the tests
$testCases = @(
@{ expectedResult = $false; version = $null }
@{ expectedResult = $false; version = [NullString]::Value }
@{ expectedResult = $false; version = "" }
@{ expectedResult = $false; version = "1.0.0-" }
@{ expectedResult = $false; version = "-" }
@{ expectedResult = $false; version = "." }
@{ expectedResult = $false; version = "-alpha" }
@{ expectedResult = $false; version = "1..0" }
@{ expectedResult = $false; version = "1.0.-alpha" }
@{ expectedResult = $false; version = "1.0." }
@{ expectedResult = $false; version = ".0.0" }
)
It "parts of version missing" -TestCases $testCases {
param($version, $expectedResult)
{ [SemanticVersion]::new($version) } | Should Throw # PSArgumentException
{ [SemanticVersion]::Parse($version) } | Should Throw # PSArgumentException
$semVer = $null
[SemanticVersion]::TryParse($_, [ref]$semVer) | Should Be $expectedResult
$semVer | Should Be $null
}
$testCases = @(
@{ expectedResult = $false; version = "-1.0.0" }
@{ expectedResult = $false; version = "1.-1.0" }
@{ expectedResult = $false; version = "1.0.-1" }
)
It "range check of versions: <version>" -TestCases $testCases {
param($version, $expectedResult)
{ [SemanticVersion]::new($version) } | Should Throw # PSArgumentException
{ [SemanticVersion]::Parse($version) } | Should Throw # PSArgumentException
$semVer = $null
[SemanticVersion]::TryParse($_, [ref]$semVer) | Should Be $expectedResult
$semVer | Should Be $null
}
$testCases = @(
@{ expectedResult = $false; version = "aa.0.0" }
@{ expectedResult = $false; version = "1.bb.0" }
@{ expectedResult = $false; version = "1.0.cc" }
)
It "format errors: <version>" -TestCases $testCases {
param($version, $expectedResult)
{ [SemanticVersion]::new($version) } | Should Throw # PSArgumentException
{ [SemanticVersion]::Parse($version) } | Should Throw # PSArgumentException
It "<name>: '<version>'" -TestCases @(
@{ name = "Missing parts: 'null'"; errorId = "PSArgumentNullException";expectedResult = $false; version = $null }
@{ name = "Missing parts: 'NullString'"; errorId = "PSArgumentNullException";expectedResult = $false; version = [NullString]::Value }
@{ name = "Missing parts: 'EmptyString'";errorId = "FormatException"; expectedResult = $false; version = "" }
@{ name = "Missing parts"; errorId = "FormatException"; expectedResult = $false; version = "-" }
@{ name = "Missing parts"; errorId = "FormatException"; expectedResult = $false; version = "." }
@{ name = "Missing parts"; errorId = "FormatException"; expectedResult = $false; version = "+" }
@{ name = "Missing parts"; errorId = "FormatException"; expectedResult = $false; version = "-alpha" }
@{ name = "Missing parts"; errorId = "FormatException"; expectedResult = $false; version = "1..0" }
@{ name = "Missing parts"; errorId = "FormatException"; expectedResult = $false; version = "1.0.-alpha" }
@{ name = "Missing parts"; errorId = "FormatException"; expectedResult = $false; version = "1.0.+alpha" }
@{ name = "Missing parts"; errorId = "FormatException"; expectedResult = $false; version = "1.0.0-alpha+" }
@{ name = "Missing parts"; errorId = "FormatException"; expectedResult = $false; version = "1.0.0-+" }
@{ name = "Missing parts"; errorId = "FormatException"; expectedResult = $false; version = "1.0.0+-" }
@{ name = "Missing parts"; errorId = "FormatException"; expectedResult = $false; version = "1.0.0+" }
@{ name = "Missing parts"; errorId = "FormatException"; expectedResult = $false; version = "1.0.0-" }
@{ name = "Missing parts"; errorId = "FormatException"; expectedResult = $false; version = "1.0.0." }
@{ name = "Missing parts"; errorId = "FormatException"; expectedResult = $false; version = "1.0." }
@{ name = "Missing parts"; errorId = "FormatException"; expectedResult = $false; version = "1.0.." }
@{ name = "Missing parts"; errorId = "FormatException"; expectedResult = $false; version = ".0.0" }
@{ name = "Range check of versions"; errorId = "FormatException"; expectedResult = $false; version = "-1.0.0" }
@{ name = "Range check of versions"; errorId = "FormatException"; expectedResult = $false; version = "1.-1.0" }
@{ name = "Range check of versions"; errorId = "FormatException"; expectedResult = $false; version = "1.0.-1" }
@{ name = "Format errors"; errorId = "FormatException"; expectedResult = $false; version = "aa.0.0" }
@{ name = "Format errors"; errorId = "FormatException"; expectedResult = $false; version = "1.bb.0" }
@{ name = "Format errors"; errorId = "FormatException"; expectedResult = $false; version = "1.0.cc" }
) {
param($version, $expectedResult, $errorId)
{ [SemanticVersion]::new($version) } | ShouldBeErrorId $errorId
if ($version -eq $null) {
# PowerShell convert $null to Empty string
{ [SemanticVersion]::Parse($version) } | ShouldBeErrorId "FormatException"
} else {
{ [SemanticVersion]::Parse($version) } | ShouldBeErrorId $errorId
}
$semVer = $null
[SemanticVersion]::TryParse($_, [ref]$semVer) | Should Be $expectedResult
$semVer | Should Be $null
}
It "Negative version arguments" {
{ [SemanticVersion]::new(-1, 0) } | Should Throw # PSArgumentException
{ [SemanticVersion]::new(1, -1) } | Should Throw # PSArgumentException
{ [SemanticVersion]::new(1, 1, -1) } | Should Throw # PSArgumentException
{ [SemanticVersion]::new(-1, 0) } | ShouldBeErrorId "PSArgumentException"
{ [SemanticVersion]::new(1, -1) } | ShouldBeErrorId "PSArgumentException"
{ [SemanticVersion]::new(1, 1, -1) } | ShouldBeErrorId "PSArgumentException"
}
It "Incompatible version throws" {
It "Incompatible 'Version' throws" {
# Revision isn't supported
{ [SemanticVersion]::new([Version]::new(0, 0, 0, 4)) } | Should Throw # PSArgumentException
{ [SemanticVersion]::new([Version]::new("1.2.3.4")) } | Should Throw # PSArgumentException
{ [SemanticVersion]::new([Version]::new(0, 0, 0, 4)) } | ShouldBeErrorId "PSArgumentException"
{ [SemanticVersion]::new([Version]::new("1.2.3.4")) } | ShouldBeErrorId "PSArgumentException"
}
}
Context "Serialization" {
$testCases = @(
@{ expectedResult = "1.0.0"; semver = [SemanticVersion]::new(1, 0, 0) }
@{ expectedResult = "1.0.1"; semver = [SemanticVersion]::new(1, 0, 1) }
@{ expectedResult = "1.0.0-alpha"; semver = [SemanticVersion]::new(1, 0, 0, "alpha") }
@{ expectedResult = "1.0.0-beta"; semver = [SemanticVersion]::new(1, 0, 0, "beta") }
@{ errorId = "PSArgumentException"; expectedResult = "1.0.0"; semver = [SemanticVersion]::new(1, 0, 0) }
@{ errorId = "PSArgumentException"; expectedResult = "1.0.1"; semver = [SemanticVersion]::new(1, 0, 1) }
@{ errorId = "PSArgumentException"; expectedResult = "1.0.0-alpha"; semver = [SemanticVersion]::new(1, 0, 0, "alpha") }
@{ errorId = "PSArgumentException"; expectedResult = "1.0.0-Alpha-super.3+BLD.a1-xxx.03"; semver = [SemanticVersion]::new(1, 0, 0, "Alpha-super.3+BLD.a1-xxx.03") }
)
It "Can round trip: <semver>" -TestCases $testCases {
param($semver, $expectedResult)