diff --git a/src/classes/public/PSSemVer.ps1 b/src/classes/public/PSSemVer.ps1 index 4fdd855..433b4ad 100644 --- a/src/classes/public/PSSemVer.ps1 +++ b/src/classes/public/PSSemVer.ps1 @@ -172,38 +172,45 @@ } [int] CompareTo([Object]$other) { - if (-not $other -is [PSSemVer]) { + # A null comparand sorts before any value, per the IComparable convention. + if ($null -eq $other) { + return 1 + } + # PowerShell converts the right-hand operand for -lt/-gt and Sort-Object, but a direct + # CompareTo call can pass anything, so convert here as well and reject what cannot be a version. + $comparand = $other -as [PSSemVer] + if ($null -eq $comparand) { throw [ArgumentException]::new('The argument must be of type PSSemVer') } - if ($this.Major -lt $other.Major) { + if ($this.Major -lt $comparand.Major) { return -1 } - if ($this.Major -gt $other.Major) { + if ($this.Major -gt $comparand.Major) { return 1 } - if ($this.Minor -lt $other.Minor) { + if ($this.Minor -lt $comparand.Minor) { return -1 } - if ($this.Minor -gt $other.Minor) { + if ($this.Minor -gt $comparand.Minor) { return 1 } - if ($this.Patch -lt $other.Patch) { + if ($this.Patch -lt $comparand.Patch) { return -1 } - if ($this.Patch -gt $other.Patch) { + if ($this.Patch -gt $comparand.Patch) { return 1 } - if ([string]::IsNullOrEmpty($this.Prerelease) -and [string]::IsNullOrEmpty($other.Prerelease)) { + if ([string]::IsNullOrEmpty($this.Prerelease) -and [string]::IsNullOrEmpty($comparand.Prerelease)) { return 0 } if ([string]::IsNullOrEmpty($this.Prerelease)) { return 1 } - if ([string]::IsNullOrEmpty($other.Prerelease)) { + if ([string]::IsNullOrEmpty($comparand.Prerelease)) { return -1 } $thisPrereleaseArray = ($this.Prerelease -split '\.') - $otherPrereleaseArray = ($other.Prerelease -split '\.') + $otherPrereleaseArray = ($comparand.Prerelease -split '\.') for ($i = 0; $i -lt [Math]::Max($thisPrereleaseArray.Length, $otherPrereleaseArray.Length); $i++) { if ($i -ge $thisPrereleaseArray.Length) { return -1 @@ -230,22 +237,29 @@ } [bool] Equals([Object]$other) { - if (-not $other -is [PSSemVer]) { + # PowerShell does not convert the right-hand operand for -eq, so an unconverted value + # arrives here. Convert it so -eq against a version string keeps working, and treat + # anything that is not a version as simply not equal rather than comparing absent properties. + if ($null -eq $other) { + return $false + } + $comparand = $other -as [PSSemVer] + if ($null -eq $comparand) { return $false } - if ($this.Major -ne $other.Major) { + if ($this.Major -ne $comparand.Major) { return $false } - if ($this.Minor -ne $other.Minor) { + if ($this.Minor -ne $comparand.Minor) { return $false } - if ($this.Patch -ne $other.Patch) { + if ($this.Patch -ne $comparand.Patch) { return $false } - if ($this.Prerelease -ne $other.Prerelease) { + if ($this.Prerelease -ne $comparand.Prerelease) { return $false } - if ($this.BuildMetadata -ne $other.BuildMetadata) { + if ($this.BuildMetadata -ne $comparand.BuildMetadata) { return $false } return $true diff --git a/tests/PSSemVer.Tests.ps1 b/tests/PSSemVer.Tests.ps1 index 995a1e6..c06c22c 100644 --- a/tests/PSSemVer.Tests.ps1 +++ b/tests/PSSemVer.Tests.ps1 @@ -311,6 +311,50 @@ Describe 'PSSemVer' { } Describe 'Class: Comparison' { + Context 'Type guard' { + It 'CompareTo throws on a value that cannot be a version' { + { ([PSSemVer]'1.0.0').CompareTo([guid]::NewGuid()) } | Should -Throw + } + It 'CompareTo throws on a hashtable that has no version properties' { + { ([PSSemVer]'1.0.0').CompareTo(@{ Foo = 'bar' }) } | Should -Throw + } + It 'CompareTo accepts a hashtable that describes a version' { + # PowerShell converts a property-shaped hashtable to a class instance. That is a real + # conversion, unlike the old behaviour of reading absent properties off any object. + ([PSSemVer]'1.0.0').CompareTo(@{ Major = 9; Minor = 0; Patch = 0 }) | Should -BeLessThan 0 + } + It 'CompareTo throws on a garbage string' { + { ([PSSemVer]'1.0.0').CompareTo('not-a-version') } | Should -Throw + } + It 'CompareTo sorts $null before any version' { + ([PSSemVer]'1.0.0').CompareTo($null) | Should -Be 1 + } + It 'CompareTo still accepts a convertible version string' { + ([PSSemVer]'1.0.0').CompareTo('1.0.1') | Should -BeLessThan 0 + } + It 'Equals returns false for a value that cannot be a version' { + ([PSSemVer]'1.0.0').Equals([guid]::NewGuid()) | Should -BeFalse + } + It 'Equals returns false for a garbage string' { + ([PSSemVer]'1.0.0').Equals('not-a-version') | Should -BeFalse + } + It 'Equals returns false for $null' { + ([PSSemVer]'1.0.0').Equals($null) | Should -BeFalse + } + It '-eq against an equal version string still returns true' { + [PSSemVer]'1.0.0' -eq '1.0.0' | Should -BeTrue + } + It '-eq against a different version string returns false' { + [PSSemVer]'1.0.0' -eq '1.0.1' | Should -BeFalse + } + It '-lt against a version string still compares' { + [PSSemVer]'1.0.0' -lt '1.0.1' | Should -BeTrue + } + It 'Sort-Object still orders PSSemVer objects' { + $sorted = @([PSSemVer]'1.0.0', [PSSemVer]'0.9.0') | Sort-Object + $sorted[0].ToString() | Should -Be '0.9.0' + } + } It "'1.2.3' < '1.2.4'" { $PSSemVer1 = [PSSemVer]::Parse('1.2.3') $PSSemVer2 = [PSSemVer]::Parse('1.2.4')