A NuGet List item parser – Part 2: Including Pre-release packages

In Part 1 I reorganised my NuGet source synchronization tool’s code to make it testable, with the specific aim of allowing pre-release packages to be synchronized. In particular, the affected code was isolated into this function:

function Parse-PackageItem {
	param(
		[string]$Package
	)
	$idVer = $Package.Split(' ')
	if ($idVer.Length -eq 2) {
		$id = $idVer[0]
		[string]$ver = $idVer[1]
		$parts = $ver.Split('.')
		if ($parts.Length -eq 3) {
			[int]$major = $parts[0]
			[int]$minor = $parts[1]
			[int]$patch = $parts[2]
			new-object -TypeName PSCustomObject -Property @{
				package = $Package
				id = $id
				version = $ver
				major = $major
				minor = $minor
				patch = $patch
			}
		}
	}
}

This is a challenge because the pre-release part of the version starts with a hyphen, followed by a series of dot separated identifiers after that. Each identifier consists of letters, digits and hyphens.

A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers immediately following the patch version. Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. Numeric identifiers MUST NOT include leading zeroes. Pre-release versions have a lower precedence than the associated normal version. A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. Examples: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92.

https://semver.org/#spec-item-9

For our purposes, the pre-release label is whatever follows the hyphen.

Creating the first tests

This is the Pester test that we created previously:

if (Get-Module SyncNuGetRepos -All) {
	Remove-Module SyncNuGetRepos
}
Import-Module "$PSScriptRoot\..\bin\Debug\SyncNuGetRepos\SyncNuGetRepos.psm1" -WarningAction SilentlyContinue
Describe "Parse-PackageItem" {
	Context "Exists" {
		It "Runs" {
			Parse-PackageItem
		}
	}
}

First I add a test for the non-pre-release case. This would exist if TDD was done from the beginning.

Context "Non pre-release Item parts" {
		$parts = Parse-PackageItem -Package 'package 1.0.123'
	It "Given 'package 1.0.123', <property> should be <value>" -TestCases @(
		@{ property = 'id'; value = 'package' }
		@{ property = 'major'; value = '1' }
		@{ property = 'minor'; value = '0' }
		@{ property = 'patch'; value = '123' }
		@{ property = 'prerelease'; value = '' }
	) {
		param ($property, $value)
		"$($parts.$property)" | should -Be $value
	}
}
  Context Non pre-release Item parts
    [+] Given 'package 1.0.123', 'id' should be 'package' 69ms
    [+] Given 'package 1.0.123', 'major' should be '1' 23ms
    [+] Given 'package 1.0.123', 'minor' should be '0' 14ms
    [+] Given 'package 1.0.123', 'patch' should be '123' 15ms
    [+] Given 'package 1.0.123', 'prerelease' should be <empty> 14ms

The tests pass.

The good thing is that we can easily modify this test for the other scenarios we want to test. For the next test I am adding a simple pre-release label:

	Context "Simple pre-release Item parts" {
			$parts = Parse-PackageItem -Package 'package 1.0.123-PreRelease'
		It "Given 'package 1.0.123', <property> should be <value>" -TestCases @(
			@{ property = 'id'; value = 'package' }
			@{ property = 'major'; value = '1' }
			@{ property = 'minor'; value = '0' }
			@{ property = 'patch'; value = '123' }
			@{ property = 'prerelease'; value = 'PreRelease' }
		) {
			param ($property, $value)
			"$($parts.$property)" | should -Be $value
		}
	}

As expected, this failed:

  Context Simple pre-release Item parts
    [-] Error occurred in Context block 173ms
      FormatException: Input string was not in a correct format.
      PSInvalidCastException: Cannot convert value "123-PreRelease" to type "System.Int32". Error: "Input string was not in a correct format."
      RuntimeException: Cannot convert value "123-PreRelease" to type "System.Int32". Error: "Input string was not in a correct format."
      at Parse-PackageItem, C:\VSTS\ContinuousIntegration\SyncNuGetRepos\SyncNuGetRepos\bin\Debug\SyncNuGetRepos\SyncNuGetRepos.psm1: line 109
      at <ScriptBlock>, C:\VSTS\ContinuousIntegration\SyncNuGetRepos\SyncNuGetRepos\Tests\Parse-PackageItem.tests.ps1: line 25
      at DescribeImpl, C:\Program Files\WindowsPowerShell\Modules\Pester\4.3.1\Functions\Describe.ps1: line 161

The immediate error was the integer conversion, suggesting that the patch should be fixed. Instead, the pre-release label modifies the entire version, not just the patch. I change

		[string]$ver = $idVer[1]
		$parts = $ver.Split('.')

becomes

		[string]$ver = $idVer[1]
		$verPreRel = $ver.Split('-')
		[string]$verParts = $verPreRel[0]
		[string]$preRel = ''
		if ($verParts.Length -eq 2) {
			$preRel = $verPreRel[1]
		}
		$parts = $verParts.Split('.')

and I assign $preRel to the prerelease property:

function Parse-PackageItem {
	param(
		[string]$Package
	)
	$idVer = $Package.Split(' ')
	if ($idVer.Length -eq 2) {
		$id = $idVer[0]
		[string]$ver = $idVer[1]
		$verPreRel = $ver.Split('-')
		[string]$verParts = $verPreRel[0]
		[string]$preRel = ''
		if ($verPreRel.Length -eq 2) {
			$preRel = $verPreRel[1]
		}
		$parts = $verParts.Split('.')
		if ($parts.Length -eq 3) {
			[int]$major = $parts[0]
			[int]$minor = $parts[1]
			[int]$patch = $parts[2]
			new-object -TypeName PSCustomObject -Property @{
				package = $Package
				id = $id
				version = $ver
				major = $major
				minor = $minor
				patch = $patch
				prerelease = $preRel
			}
		}
	}
}

I’m cheating a little. The test failed, and I had to fix a typo.

The next test verifies that the pre-release part can consist of dot-separtated identifiers:

	Context "Dot-separated identifier pre-release Item parts" {
			$parts = Parse-PackageItem -Package 'package 1.0.123-Pre.Release'
		It "Given 'package 1.0.123', <property> should be <value>" -TestCases @(
			@{ property = 'id'; value = 'package' }
			@{ property = 'major'; value = '1' }
			@{ property = 'minor'; value = '0' }
			@{ property = 'patch'; value = '123' }
			@{ property = 'prerelease'; value = 'Pre.Release' }
		) {
			param ($property, $value)
			"$($parts.$property)" | should -Be $value
		}
	}

The test passes. The next test is for the pre-release part containing a hyphen:

	Context "Hyphened identifier pre-release Item parts" {
			$parts = Parse-PackageItem -Package 'package 1.0.123-Pre-Release'
		It "Given 'package 1.0.123', <property> should be <value>" -TestCases @(
			@{ property = 'id'; value = 'package' }
			@{ property = 'major'; value = '1' }
			@{ property = 'minor'; value = '0' }
			@{ property = 'patch'; value = '123' }
			@{ property = 'prerelease'; value = 'Pre-Release' }
		) {
			param ($property, $value)
			"$($parts.$property)" | should -Be $value
		}
	}

As expected, this failed:

    [-] Given 'package 1.0.123', 'prerelease' should be 'Pre-Release' 13ms
      Expected strings to be the same, but they were different.
      Expected length: 11
      Actual length:   0
      Strings differ at index 0.
      Expected: 'Pre-Release'
      But was:  ''
      -----------^
      60: 			"$($parts.$property)" | should -Be $value
      at <ScriptBlock>, C:\VSTS\ContinuousIntegration\SyncNuGetRepos\SyncNuGetRepos\Tests\Parse-PackageItem.tests.ps1: line 60

The value was empty because of $ver.Split(‘-‘) returning more than two parts. I check the String.Split documentation and see that I can make this $ver.Split(‘-‘, 2). After I make the change, the test passes.

Now my function looks like this:

function Parse-PackageItem {
	param(
		[string]$Package
	)
	$idVer = $Package.Split(' ')
	if ($idVer.Length -eq 2) {
		$id = $idVer[0]
		[string]$ver = $idVer[1]
		$verPreRel = $ver.Split('-', 2)
		[string]$verParts = $verPreRel[0]
		[string]$preRel = ''
		if ($verPreRel.Length -eq 2) {
			$preRel = $verPreRel[1]
		}
		$parts = $verParts.Split('.')
		if ($parts.Length -eq 3) {
			[int]$major = $parts[0]
			[int]$minor = $parts[1]
			[int]$patch = $parts[2]
			new-object -TypeName PSCustomObject -Property @{
				package = $Package
				id = $id
				version = $ver
				major = $major
				minor = $minor
				patch = $patch
				prerelease = $preRel
			}
		}
	}
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s