From 1b6a4f9b4600079e829d5884f768563ed7dea5e5 Mon Sep 17 00:00:00 2001
From: Sean Hall <r.sean.hall@gmail.com>
Date: Sun, 18 Oct 2020 22:38:21 -0500
Subject: Add CompareVersions engine method and expose verutil in Mba.Core.

---
 src/WixToolset.Mba.Core/Engine.cs                  |   6 +
 src/WixToolset.Mba.Core/IBootstrapperEngine.cs     |   6 +
 src/WixToolset.Mba.Core/IEngine.cs                 |   3 +
 src/WixToolset.Mba.Core/VerUtil.cs                 | 126 +++++++++++++++++++++
 src/WixToolset.Mba.Core/VerUtilVersion.cs          |  63 +++++++++++
 .../VerUtilVersionReleaseLabel.cs                  |  22 ++++
 src/balutil/BalBootstrapperEngine.cpp              |  26 +++++
 src/balutil/inc/IBootstrapperApplication.h         |   2 +-
 src/balutil/inc/IBootstrapperEngine.h              |   6 +
 src/bextutil/BextBundleExtensionEngine.cpp         |  26 +++++
 src/bextutil/inc/IBundleExtensionEngine.h          |   6 +
 src/mbanative/mbanative.def                        |   6 +
 src/mbanative/precomp.h                            |   1 +
 .../BaseBootstrapperApplicationFactoryFixture.cs   |   2 +-
 src/test/WixToolsetTest.Mba.Core/VerUtilFixture.cs |  93 +++++++++++++++
 15 files changed, 392 insertions(+), 2 deletions(-)
 create mode 100644 src/WixToolset.Mba.Core/VerUtil.cs
 create mode 100644 src/WixToolset.Mba.Core/VerUtilVersion.cs
 create mode 100644 src/WixToolset.Mba.Core/VerUtilVersionReleaseLabel.cs
 create mode 100644 src/test/WixToolsetTest.Mba.Core/VerUtilFixture.cs

(limited to 'src')

diff --git a/src/WixToolset.Mba.Core/Engine.cs b/src/WixToolset.Mba.Core/Engine.cs
index e3ad097a..c6570f9d 100644
--- a/src/WixToolset.Mba.Core/Engine.cs
+++ b/src/WixToolset.Mba.Core/Engine.cs
@@ -49,6 +49,12 @@ namespace WixToolset.Mba.Core
             this.engine.CloseSplashScreen();
         }
 
+        public int CompareVersions(string version1, string version2)
+        {
+            this.engine.CompareVersions(version1, version2, out var result);
+            return result;
+        }
+
         public bool ContainsVariable(string name)
         {
             int capacity = 0;
diff --git a/src/WixToolset.Mba.Core/IBootstrapperEngine.cs b/src/WixToolset.Mba.Core/IBootstrapperEngine.cs
index 584e0f6d..5cc42071 100644
--- a/src/WixToolset.Mba.Core/IBootstrapperEngine.cs
+++ b/src/WixToolset.Mba.Core/IBootstrapperEngine.cs
@@ -145,6 +145,12 @@ namespace WixToolset.Mba.Core
             [MarshalAs(UnmanagedType.LPWStr)] string wzArguments,
             [MarshalAs(UnmanagedType.U4)] int dwWaitForInputIdleTimeout
             );
+
+        void CompareVersions(
+            [MarshalAs(UnmanagedType.LPWStr)] string wzVersion1,
+            [MarshalAs(UnmanagedType.LPWStr)] string wzVersion2,
+            [MarshalAs(UnmanagedType.I4)] out int pnResult
+            );
     }
 
     /// <summary>
diff --git a/src/WixToolset.Mba.Core/IEngine.cs b/src/WixToolset.Mba.Core/IEngine.cs
index b486e51d..8403352d 100644
--- a/src/WixToolset.Mba.Core/IEngine.cs
+++ b/src/WixToolset.Mba.Core/IEngine.cs
@@ -25,6 +25,9 @@ namespace WixToolset.Mba.Core
         /// </summary>
         void CloseSplashScreen();
 
+        /// <returns>0 if equal, 1 if version1 &gt; version2, -1 if version1 &lt; version2</returns>
+        int CompareVersions(string version1, string version2);
+
         /// <summary>
         /// Checks if a variable exists in the engine.
         /// </summary>
diff --git a/src/WixToolset.Mba.Core/VerUtil.cs b/src/WixToolset.Mba.Core/VerUtil.cs
new file mode 100644
index 00000000..dcc9dee2
--- /dev/null
+++ b/src/WixToolset.Mba.Core/VerUtil.cs
@@ -0,0 +1,126 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Mba.Core
+{
+    using System;
+    using System.Runtime.InteropServices;
+    using System.Text;
+
+    public static class VerUtil
+    {
+        [DllImport("mbanative.dll", ExactSpelling = true, PreserveSig = false)]
+        internal static extern int VerCompareParsedVersions(
+            VersionHandle pVersion1,
+            VersionHandle pVersion2
+            );
+
+        [DllImport("mbanative.dll", ExactSpelling = true, PreserveSig = false)]
+        internal static extern int VerCompareStringVersions(
+            [MarshalAs(UnmanagedType.LPWStr)] string wzVersion1,
+            [MarshalAs(UnmanagedType.LPWStr)] string wzVersion2,
+            [MarshalAs(UnmanagedType.Bool)] bool fStrict
+            );
+
+        [DllImport("mbanative.dll", ExactSpelling = true, PreserveSig = false)]
+        internal static extern VersionHandle VerCopyVersion(
+            VersionHandle pSource
+            );
+
+        [DllImport("mbanative.dll", ExactSpelling = true)]
+        internal static extern void VerFreeVersion(
+            IntPtr pVersion
+            );
+
+        [DllImport("mbanative.dll", ExactSpelling = true, PreserveSig = false)]
+        internal static extern VersionHandle VerParseVersion(
+            [MarshalAs(UnmanagedType.LPWStr)] string wzVersion,
+            [MarshalAs(UnmanagedType.U4)] uint cchValue,
+            [MarshalAs(UnmanagedType.Bool)] bool fStrict
+            );
+
+        [DllImport("mbanative.dll", ExactSpelling = true, PreserveSig = false)]
+        internal static extern VersionHandle VerVersionFromQword(
+            [MarshalAs(UnmanagedType.I8)] long qwVersion
+            );
+
+        [StructLayout(LayoutKind.Sequential)]
+        internal struct VersionReleaseLabelStruct
+        {
+            public bool fNumeric;
+            public uint dwValue;
+            public IntPtr cchLabelOffset;
+            public int cchLabel;
+        }
+
+        [StructLayout(LayoutKind.Sequential)]
+        internal struct VersionStruct
+        {
+            public IntPtr sczVersion;
+            public uint dwMajor;
+            public uint dwMinor;
+            public uint dwPatch;
+            public uint dwRevision;
+            public int cReleaseLabels;
+            public IntPtr rgReleaseLabels;
+            public IntPtr cchMetadataOffset;
+            public bool fInvalid;
+        }
+
+        internal static string VersionStringFromOffset(IntPtr wzVersion, IntPtr cchOffset, int? cchLength = null)
+        {
+            var offset = cchOffset.ToInt64() * UnicodeEncoding.CharSize;
+            var wz = new IntPtr(wzVersion.ToInt64() + offset);
+            if (cchLength.HasValue)
+            {
+                return Marshal.PtrToStringUni(wz, (int)cchLength);
+            }
+            else
+            {
+                return Marshal.PtrToStringUni(wz);
+            }
+        }
+
+        internal sealed class VersionHandle : SafeHandle
+        {
+            public VersionHandle() : base(IntPtr.Zero, true) { }
+
+            public override bool IsInvalid => false;
+
+            protected override bool ReleaseHandle()
+            {
+                VerFreeVersion(this.handle);
+                return true;
+            }
+        }
+
+        /// <returns>0 if equal, 1 if version1 &gt; version2, -1 if version1 &lt; version2</returns>
+        public static int CompareParsedVersions(VerUtilVersion version1, VerUtilVersion version2)
+        {
+            return VerCompareParsedVersions(version1.GetHandle(), version2.GetHandle());
+        }
+
+        /// <returns>0 if equal, 1 if version1 &gt; version2, -1 if version1 &lt; version2</returns>
+        public static int CompareStringVersions(string version1, string version2, bool strict)
+        {
+            return VerCompareStringVersions(version1, version2, strict);
+        }
+
+        public static VerUtilVersion CopyVersion(VerUtilVersion version)
+        {
+            var handle = VerCopyVersion(version.GetHandle());
+            return new VerUtilVersion(handle);
+        }
+
+        public static VerUtilVersion ParseVersion(string version, bool strict)
+        {
+            var handle = VerParseVersion(version, 0, strict);
+            return new VerUtilVersion(handle);
+        }
+
+        public static VerUtilVersion VersionFromQword(long version)
+        {
+            var handle = VerVersionFromQword(version);
+            return new VerUtilVersion(handle);
+        }
+    }
+}
diff --git a/src/WixToolset.Mba.Core/VerUtilVersion.cs b/src/WixToolset.Mba.Core/VerUtilVersion.cs
new file mode 100644
index 00000000..2b509a21
--- /dev/null
+++ b/src/WixToolset.Mba.Core/VerUtilVersion.cs
@@ -0,0 +1,63 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Mba.Core
+{
+    using System;
+    using System.Runtime.InteropServices;
+
+    public sealed class VerUtilVersion : IDisposable
+    {
+        internal VerUtilVersion(VerUtil.VersionHandle handle)
+        {
+            this.Handle = handle;
+
+            var pVersion = handle.DangerousGetHandle();
+            var version = (VerUtil.VersionStruct)Marshal.PtrToStructure(pVersion, typeof(VerUtil.VersionStruct));
+            this.Version = Marshal.PtrToStringUni(version.sczVersion);
+            this.Major = version.dwMajor;
+            this.Minor = version.dwMinor;
+            this.Patch = version.dwPatch;
+            this.Revision = version.dwRevision;
+            this.ReleaseLabels = new VerUtilVersionReleaseLabel[version.cReleaseLabels];
+            this.Metadata = VerUtil.VersionStringFromOffset(version.sczVersion, version.cchMetadataOffset);
+            this.IsInvalid = version.fInvalid;
+
+            for (var i = 0; i < version.cReleaseLabels; ++i)
+            {
+                var offset = i * Marshal.SizeOf(typeof(VerUtil.VersionReleaseLabelStruct));
+                var pReleaseLabel = new IntPtr(version.rgReleaseLabels.ToInt64() + offset);
+                this.ReleaseLabels[i] = new VerUtilVersionReleaseLabel(pReleaseLabel, version.sczVersion);
+            }
+        }
+
+        public string Version { get; private set; }
+        public uint Major { get; private set; }
+        public uint Minor { get; private set; }
+        public uint Patch { get; private set; }
+        public uint Revision { get; private set; }
+        public VerUtilVersionReleaseLabel[] ReleaseLabels { get; private set; }
+        public string Metadata { get; private set; }
+        public bool IsInvalid { get; private set; }
+
+        public void Dispose()
+        {
+            if (this.Handle != null)
+            {
+                this.Handle.Dispose();
+                this.Handle = null;
+            }
+        }
+
+        private VerUtil.VersionHandle Handle { get; set; }
+
+        internal VerUtil.VersionHandle GetHandle()
+        {
+            if (this.Handle == null)
+            {
+                throw new ObjectDisposedException(this.Version);
+            }
+
+            return this.Handle;
+        }
+    }
+}
diff --git a/src/WixToolset.Mba.Core/VerUtilVersionReleaseLabel.cs b/src/WixToolset.Mba.Core/VerUtilVersionReleaseLabel.cs
new file mode 100644
index 00000000..84ff3b36
--- /dev/null
+++ b/src/WixToolset.Mba.Core/VerUtilVersionReleaseLabel.cs
@@ -0,0 +1,22 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Mba.Core
+{
+    using System;
+    using System.Runtime.InteropServices;
+
+    public sealed class VerUtilVersionReleaseLabel
+    {
+        internal VerUtilVersionReleaseLabel(IntPtr pReleaseLabel, IntPtr wzVersion)
+        {
+            var releaseLabel = (VerUtil.VersionReleaseLabelStruct)Marshal.PtrToStructure(pReleaseLabel, typeof(VerUtil.VersionReleaseLabelStruct));
+            this.IsNumeric = releaseLabel.fNumeric;
+            this.Value = releaseLabel.dwValue;
+            this.Label = VerUtil.VersionStringFromOffset(wzVersion, releaseLabel.cchLabelOffset, releaseLabel.cchLabel);
+        }
+
+        public bool IsNumeric { get; private set; }
+        public uint Value { get; private set; }
+        public string Label { get; private set; }
+    }
+}
diff --git a/src/balutil/BalBootstrapperEngine.cpp b/src/balutil/BalBootstrapperEngine.cpp
index b68ea7c2..8a78b127 100644
--- a/src/balutil/BalBootstrapperEngine.cpp
+++ b/src/balutil/BalBootstrapperEngine.cpp
@@ -535,6 +535,32 @@ public: // IBootstrapperEngine
         return m_pfnBAEngineProc(BOOTSTRAPPER_ENGINE_MESSAGE_LAUNCHAPPROVEDEXE, &args, &results, m_pvBAEngineProcContext);
     }
 
+    virtual STDMETHODIMP CompareVersions(
+        __in_z LPCWSTR wzVersion1,
+        __in_z LPCWSTR wzVersion2,
+        __out int* pnResult
+        )
+    {
+        HRESULT hr = S_OK;
+        BAENGINE_COMPAREVERSIONS_ARGS args = { };
+        BAENGINE_COMPAREVERSIONS_RESULTS results = { };
+
+        ExitOnNull(pnResult, hr, E_INVALIDARG, "pnResult is required");
+
+        args.cbSize = sizeof(args);
+        args.wzVersion1 = wzVersion1;
+        args.wzVersion2 = wzVersion2;
+
+        results.cbSize = sizeof(results);
+
+        hr = m_pfnBAEngineProc(BOOTSTRAPPER_ENGINE_MESSAGE_COMPAREVERSIONS, &args, &results, m_pvBAEngineProcContext);
+
+        *pnResult = results.nResult;
+
+    LExit:
+        return hr;
+    }
+
 public: // IMarshal
     virtual STDMETHODIMP GetUnmarshalClass(
         __in REFIID /*riid*/,
diff --git a/src/balutil/inc/IBootstrapperApplication.h b/src/balutil/inc/IBootstrapperApplication.h
index 9cc19120..75ea7bc9 100644
--- a/src/balutil/inc/IBootstrapperApplication.h
+++ b/src/balutil/inc/IBootstrapperApplication.h
@@ -396,7 +396,7 @@ DECLARE_INTERFACE_IID_(IBootstrapperApplication, IUnknown, "53C31D56-49C0-426B-A
         __inout BOOL* pfCancel
         ) = 0;
 
-    // OnExecuteBegin - called when the engine begins executing a package.
+    // OnExecutePackageBegin - called when the engine begins executing a package.
     //
     STDMETHOD(OnExecutePackageBegin)(
         __in_z LPCWSTR wzPackageId,
diff --git a/src/balutil/inc/IBootstrapperEngine.h b/src/balutil/inc/IBootstrapperEngine.h
index cf055102..cd89a9f0 100644
--- a/src/balutil/inc/IBootstrapperEngine.h
+++ b/src/balutil/inc/IBootstrapperEngine.h
@@ -127,4 +127,10 @@ DECLARE_INTERFACE_IID_(IBootstrapperEngine, IUnknown, "6480D616-27A0-44D7-905B-8
         __in_z_opt LPCWSTR wzArguments,
         __in DWORD dwWaitForInputIdleTimeout
         ) = 0;
+
+    STDMETHOD(CompareVersions)(
+        __in_z LPCWSTR wzVersion1,
+        __in_z LPCWSTR wzVersion2,
+        __out int* pnResult
+        ) = 0;
 };
diff --git a/src/bextutil/BextBundleExtensionEngine.cpp b/src/bextutil/BextBundleExtensionEngine.cpp
index 983782a9..9906a7f5 100644
--- a/src/bextutil/BextBundleExtensionEngine.cpp
+++ b/src/bextutil/BextBundleExtensionEngine.cpp
@@ -280,6 +280,32 @@ public: // IBundleExtensionEngine
         return m_pfnBundleExtensionEngineProc(BUNDLE_EXTENSION_ENGINE_MESSAGE_SETVARIABLEVERSION, &args, &results, m_pvBundleExtensionEngineProcContext);
     }
 
+    virtual STDMETHODIMP CompareVersions(
+        __in_z LPCWSTR wzVersion1,
+        __in_z LPCWSTR wzVersion2,
+        __out int* pnResult
+        )
+    {
+        HRESULT hr = S_OK;
+        BUNDLE_EXTENSION_ENGINE_COMPAREVERSIONS_ARGS args = { };
+        BUNDLE_EXTENSION_ENGINE_COMPAREVERSIONS_RESULTS results = { };
+
+        ExitOnNull(pnResult, hr, E_INVALIDARG, "pnResult is required");
+
+        args.cbSize = sizeof(args);
+        args.wzVersion1 = wzVersion1;
+        args.wzVersion2 = wzVersion2;
+
+        results.cbSize = sizeof(results);
+
+        hr = m_pfnBundleExtensionEngineProc(BUNDLE_EXTENSION_ENGINE_MESSAGE_COMPAREVERSIONS, &args, &results, m_pvBundleExtensionEngineProcContext);
+
+        *pnResult = results.nResult;
+
+    LExit:
+        return hr;
+    }
+
 public:
     CBextBundleExtensionEngine(
         __in PFN_BUNDLE_EXTENSION_ENGINE_PROC pfnBundleExtensionEngineProc,
diff --git a/src/bextutil/inc/IBundleExtensionEngine.h b/src/bextutil/inc/IBundleExtensionEngine.h
index 77ea2770..a96e3812 100644
--- a/src/bextutil/inc/IBundleExtensionEngine.h
+++ b/src/bextutil/inc/IBundleExtensionEngine.h
@@ -58,4 +58,10 @@ DECLARE_INTERFACE_IID_(IBundleExtensionEngine, IUnknown, "9D027A39-F6B6-42CC-973
         __in_z LPCWSTR wzVariable,
         __in_z_opt LPCWSTR wzValue
         ) = 0;
+
+    STDMETHOD(CompareVersions)(
+        __in_z LPCWSTR wzVersion1,
+        __in_z LPCWSTR wzVersion2,
+        __out int* pnResult
+        ) = 0;
 };
diff --git a/src/mbanative/mbanative.def b/src/mbanative/mbanative.def
index bd366ac7..28e923b6 100644
--- a/src/mbanative/mbanative.def
+++ b/src/mbanative/mbanative.def
@@ -4,3 +4,9 @@
 EXPORTS
     InitializeFromCreateArgs
     StoreBAInCreateResults
+    VerCompareParsedVersions
+    VerCompareStringVersions
+    VerCopyVersion
+    VerFreeVersion
+    VerParseVersion
+    VerVersionFromQword
diff --git a/src/mbanative/precomp.h b/src/mbanative/precomp.h
index d19b4695..2e2f3ff8 100644
--- a/src/mbanative/precomp.h
+++ b/src/mbanative/precomp.h
@@ -6,6 +6,7 @@
 #include <msiquery.h>
 
 #include <dutil.h>
+#include <verutil.h>
 
 #include <BootstrapperEngine.h>
 #include <BootstrapperApplication.h>
diff --git a/src/test/WixToolsetTest.Mba.Core/BaseBootstrapperApplicationFactoryFixture.cs b/src/test/WixToolsetTest.Mba.Core/BaseBootstrapperApplicationFactoryFixture.cs
index 12483ddf..7fe0a405 100644
--- a/src/test/WixToolsetTest.Mba.Core/BaseBootstrapperApplicationFactoryFixture.cs
+++ b/src/test/WixToolsetTest.Mba.Core/BaseBootstrapperApplicationFactoryFixture.cs
@@ -1,6 +1,6 @@
 // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
 
-namespace WixToolsetTest.Util
+namespace WixToolsetTest.Mba.Core
 {
     using System;
     using System.Runtime.InteropServices;
diff --git a/src/test/WixToolsetTest.Mba.Core/VerUtilFixture.cs b/src/test/WixToolsetTest.Mba.Core/VerUtilFixture.cs
new file mode 100644
index 00000000..44142e3d
--- /dev/null
+++ b/src/test/WixToolsetTest.Mba.Core/VerUtilFixture.cs
@@ -0,0 +1,93 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolsetTest.Mba.Core
+{
+    using System;
+    using WixToolset.Mba.Core;
+    using Xunit;
+
+    public class VerUtilFixture
+    {
+        [Fact]
+        public void CanCompareStringVersions()
+        {
+            var version1 = "1.2.3.4+abcd";
+            var version2 = "1.2.3.4+zyxw";
+
+            Assert.Equal(0, VerUtil.CompareStringVersions(version1, version2, strict: false));
+        }
+
+        [Fact]
+        public void CanCopyVersion()
+        {
+            var version = "1.2.3.4-5.6.7.8.9.0";
+
+            VerUtilVersion copiedVersion = null;
+            try
+            {
+                using (var parsedVersion = VerUtil.ParseVersion(version, strict: true))
+                {
+                    copiedVersion = VerUtil.CopyVersion(parsedVersion);
+                }
+
+                using (var secondVersion = VerUtil.ParseVersion(version, strict: true))
+                {
+                    Assert.Equal(0, VerUtil.CompareParsedVersions(copiedVersion, secondVersion));
+                }
+            }
+            finally
+            {
+                copiedVersion?.Dispose();
+            }
+        }
+
+        [Fact]
+        public void CanCreateFromQword()
+        {
+            var version = new Version(100, 200, 300, 400);
+            var qwVersion = Engine.VersionToLong(version);
+
+            using var parsedVersion = VerUtil.VersionFromQword(qwVersion);
+            Assert.Equal("100.200.300.400", parsedVersion.Version);
+            Assert.Equal(100u, parsedVersion.Major);
+            Assert.Equal(200u, parsedVersion.Minor);
+            Assert.Equal(300u, parsedVersion.Patch);
+            Assert.Equal(400u, parsedVersion.Revision);
+            Assert.Empty(parsedVersion.ReleaseLabels);
+            Assert.Equal("", parsedVersion.Metadata);
+            Assert.False(parsedVersion.IsInvalid);
+        }
+
+        [Fact]
+        public void CanParseVersion()
+        {
+            var version = "1.2.3.4-a.b.c.d.5.+abc123";
+
+            using var parsedVersion = VerUtil.ParseVersion(version, strict: false);
+            Assert.Equal(version, parsedVersion.Version);
+            Assert.Equal(1u, parsedVersion.Major);
+            Assert.Equal(2u, parsedVersion.Minor);
+            Assert.Equal(3u, parsedVersion.Patch);
+            Assert.Equal(4u, parsedVersion.Revision);
+            Assert.Equal(5, parsedVersion.ReleaseLabels.Length);
+            Assert.Equal("+abc123", parsedVersion.Metadata);
+            Assert.True(parsedVersion.IsInvalid);
+
+            Assert.Equal("a", parsedVersion.ReleaseLabels[0].Label);
+            Assert.False(parsedVersion.ReleaseLabels[0].IsNumeric);
+
+            Assert.Equal("b", parsedVersion.ReleaseLabels[1].Label);
+            Assert.False(parsedVersion.ReleaseLabels[1].IsNumeric);
+
+            Assert.Equal("c", parsedVersion.ReleaseLabels[2].Label);
+            Assert.False(parsedVersion.ReleaseLabels[2].IsNumeric);
+
+            Assert.Equal("d", parsedVersion.ReleaseLabels[3].Label);
+            Assert.False(parsedVersion.ReleaseLabels[3].IsNumeric);
+
+            Assert.Equal("5", parsedVersion.ReleaseLabels[4].Label);
+            Assert.True(parsedVersion.ReleaseLabels[4].IsNumeric);
+            Assert.Equal(5u, parsedVersion.ReleaseLabels[4].Value);
+        }
+    }
+}
-- 
cgit v1.2.3-55-g6feb