From d160ca98eab3c5de422ed714101f3304f1ba1fce Mon Sep 17 00:00:00 2001 From: glopesdev Date: Mon, 17 Nov 2025 10:47:14 +0000 Subject: [PATCH 1/2] Add optional patch component to HarpVersion ToString and Parse are adapted for backwards compatibility, and allow omitting the floating patch version. --- src/Bonsai.Harp.Tests/TestHarpVersion.cs | 57 ++++++++++++++-- src/Bonsai.Harp/HarpVersion.cs | 87 ++++++++++++++++++------ 2 files changed, 118 insertions(+), 26 deletions(-) diff --git a/src/Bonsai.Harp.Tests/TestHarpVersion.cs b/src/Bonsai.Harp.Tests/TestHarpVersion.cs index 1c71711..039ad91 100644 --- a/src/Bonsai.Harp.Tests/TestHarpVersion.cs +++ b/src/Bonsai.Harp.Tests/TestHarpVersion.cs @@ -64,11 +64,20 @@ public void CompareNullWithNonNullVersion() [TestMethod] public void CompareSpecificVersions_Equal() { - HarpVersion a = new HarpVersion(1, 0); - HarpVersion b = new HarpVersion(1, 0); + HarpVersion a = new HarpVersion(1, 0, 0); + HarpVersion b = new HarpVersion(1, 0, 0); AssertEquals(a, b); } + [TestMethod] + public void ComparePatchSpecificVersions_NotEqual() + { + HarpVersion a = new HarpVersion(1, 0, 0); + HarpVersion b = new HarpVersion(1, 0, 1); + AssertLessThan(a, b); + AssertGreaterThan(b, a); + } + [TestMethod] public void CompareMinorSpecificVersions_NotEqual() { @@ -87,17 +96,39 @@ public void CompareMajorSpecificVersions_NotEqual() AssertGreaterThan(a, b); } + [TestMethod] + public void ComparePatchFloatingVersion_NotEqual() + { + HarpVersion a = new HarpVersion(2, 1, null); + HarpVersion b = new HarpVersion(2, 1, 0); + AssertLessThan(a, b); + AssertGreaterThan(b, a); + Assert.IsTrue(a.Satisfies(b)); + Assert.IsTrue(b.Satisfies(a)); + } + [TestMethod] public void CompareMinorFloatingVersion_NotEqual() { HarpVersion a = new HarpVersion(2, null); - HarpVersion b = new HarpVersion(2, 1); + HarpVersion b = new HarpVersion(2, 1, 0); AssertLessThan(a, b); AssertGreaterThan(b, a); Assert.IsTrue(a.Satisfies(b)); Assert.IsTrue(b.Satisfies(a)); } + [TestMethod] + public void ComparePatchFloatingVersion_NotSatisfies() + { + HarpVersion a = new HarpVersion(2, 2, null); + HarpVersion b = new HarpVersion(2, 1); + AssertLessThan(b, a); + AssertGreaterThan(a, b); + Assert.IsFalse(a.Satisfies(b)); + Assert.IsFalse(b.Satisfies(a)); + } + [TestMethod] public void CompareMinorFloatingVersion_NotSatisfies() { @@ -121,14 +152,24 @@ public void CompareMajorFloatingVersion_NotEqual() } [TestMethod] - public void ParseAndToString_AreReversible() + public void ParseAndToStringSpecificVersion_AreReversible() { - var x = new HarpVersion(2, null); + var x = new HarpVersion(2, 1, 0); var text = x.ToString(); var y = HarpVersion.Parse(text); AssertEquals(x, y); } + [TestMethod] + public void ParseAndToStringPatchFloating_AreReversible() + { + var input = "1.0.x"; + var x = HarpVersion.Parse(input); + Assert.AreEqual("1.0", x.ToString()); + var y = HarpVersion.Parse(x.ToString()); + Assert.AreEqual(x, y); + } + [TestMethod] public void InvalidParseRunawayCharacters_ThrowsException() { @@ -146,5 +187,11 @@ public void InvalidParseWithFloatingMajor_ThrowsException() { Assert.ThrowsExactly(() => HarpVersion.Parse("x.1")); } + + [TestMethod] + public void InvalidParseWithFloatingMinor_ThrowsException() + { + Assert.ThrowsExactly(() => HarpVersion.Parse("1.x.0")); + } } } diff --git a/src/Bonsai.Harp/HarpVersion.cs b/src/Bonsai.Harp/HarpVersion.cs index 3da32b3..6978d3b 100644 --- a/src/Bonsai.Harp/HarpVersion.cs +++ b/src/Bonsai.Harp/HarpVersion.cs @@ -11,7 +11,7 @@ namespace Bonsai.Harp public sealed class HarpVersion : IComparable, IComparable, IEquatable { internal const string FloatingWildcard = "x"; - static readonly Regex VersionRegex = new Regex("^(?x|\\d+)\\.(?x|\\d+)$"); + static readonly Regex VersionRegex = new("^(?x|\\d+)\\.(?x|\\d+)(\\.(?x|\\d+))?$"); /// /// Initializes a new instance of the class with the specified @@ -21,26 +21,62 @@ public sealed class HarpVersion : IComparable, IComparable, IEquata /// /// The optional minor version. If not specified, matches against all minor versions with the same major version. /// + /// + /// Major version is floating and minor version specified, or major and minor version are floating + /// and patch version is specified. + /// public HarpVersion(int? major, int? minor) + : this(major, minor, default) { - if (major == null && minor != null) + } + + /// + /// Initializes a new instance of the class with the specified + /// major and minor version. + /// + /// The optional major version. If not specified, matches against all versions. + /// + /// The optional minor version. If not specified, matches against all minor versions with the same major version. + /// + /// + /// The optional patch version. If not specified, matches against all patch versions with matching major and + /// minor versions. + /// + /// + /// Major version is floating and minor version specified, or minor version is floating + /// and patch version is specified. + /// + public HarpVersion(int? major, int? minor, int? patch) + { + if (minor is not null && major is null) { throw new ArgumentException("Minor version cannot be specified if major version is floating.", nameof(minor)); } + if (patch is not null && minor is null) + { + throw new ArgumentException("Patch version cannot be specified if minor version is floating.", nameof(minor)); + } + Major = major; Minor = minor; + Patch = patch; } /// /// Gets the optional major version. /// - public int? Major { get; private set; } + public int? Major { get; } /// /// Gets the optional minor version. /// - public int? Minor { get; private set; } + public int? Minor { get; } + + /// + /// Gets the optional patch version. + /// + public int? Patch { get; } /// /// Returns whether the specified version matches the current version, taking into account @@ -55,10 +91,13 @@ public bool Satisfies(HarpVersion other) { if (other is null) return false; - var satisfyMajor = !Major.HasValue || Major == other.Major.GetValueOrDefault(Major.Value); + var satisfyMajor = !Major.HasValue || Major == other.Major.GetValueOrDefault(Major.GetValueOrDefault()); if (!satisfyMajor) return false; - return !Minor.HasValue || Minor == other.Minor.GetValueOrDefault(Minor.Value); + var satisfyMinor = !Minor.HasValue || Minor == other.Minor.GetValueOrDefault(Minor.GetValueOrDefault()); + if (!satisfyMinor) return false; + + return !Patch.HasValue || Patch == other.Patch.GetValueOrDefault(Patch.GetValueOrDefault()); } int IComparable.CompareTo(object obj) @@ -84,7 +123,10 @@ public int CompareTo(HarpVersion other) var major = Comparer.Default.Compare(Major, other.Major); if (major != 0) return major; - return Comparer.Default.Compare(Minor, other.Minor); + var minor = Comparer.Default.Compare(Minor, other.Minor); + if (minor != 0) return minor; + + return Comparer.Default.Compare(Patch, other.Patch); } /// @@ -97,8 +139,7 @@ public int CompareTo(HarpVersion other) /// public override bool Equals(object obj) { - if (obj is HarpVersion version) return Equals(version); - else return false; + return obj is HarpVersion version && Equals(version); } /// @@ -112,7 +153,7 @@ public override bool Equals(object obj) public bool Equals(HarpVersion other) { if (other is null) return false; - return Major == other.Major && Minor == other.Minor; + return Major == other.Major && Minor == other.Minor && Patch == other.Patch; } /// @@ -124,7 +165,7 @@ public bool Equals(HarpVersion other) /// public override int GetHashCode() { - return 29863 * Major.GetHashCode() + 1723 * Minor.GetHashCode(); + return 29863 * Major.GetHashCode() + 1723 * Minor.GetHashCode() + 6917 * Patch.GetHashCode(); } /// @@ -155,7 +196,7 @@ public override int GetHashCode() /// public static bool operator !=(HarpVersion lhs, HarpVersion rhs) { - if (lhs is null) return !(rhs is null); + if (lhs is null) return rhs is not null; else return !lhs.Equals(rhs); } @@ -171,7 +212,7 @@ public override int GetHashCode() /// public static bool operator <(HarpVersion lhs, HarpVersion rhs) { - if (lhs is null) return !(rhs is null); + if (lhs is null) return rhs is not null; else return lhs.CompareTo(rhs) < 0; } @@ -255,13 +296,15 @@ public static bool TryParse(string version, out HarpVersion value) { if (version == null) throw new ArgumentNullException(nameof(version)); var match = VersionRegex.Match(version); - if (match.Success && match.Groups.Count == 3) + if (match.Success && match.Groups.Count == 5) { - var major = match.Groups[1].Value; - var minor = match.Groups[2].Value; + var major = match.Groups[2].Value; + var minor = match.Groups[3].Value; + var patch = match.Groups[4].Value; value = new HarpVersion( - major == FloatingWildcard ? (int?)null : int.Parse(major), - minor == FloatingWildcard ? (int?)null : int.Parse(minor)); + major == FloatingWildcard ? null : int.Parse(major), + minor == FloatingWildcard ? null : int.Parse(minor), + string.IsNullOrEmpty(patch) || patch == FloatingWildcard ? null : int.Parse(patch)); return true; } else @@ -279,9 +322,11 @@ public static bool TryParse(string version, out HarpVersion value) /// public override string ToString() { - var major = Major.HasValue ? Major.Value.ToString(CultureInfo.InvariantCulture) : FloatingWildcard; - var minor = Minor.HasValue ? Minor.Value.ToString(CultureInfo.InvariantCulture) : FloatingWildcard; - return $"{major}.{minor}"; + var major = Major.HasValue ? Major.GetValueOrDefault().ToString(CultureInfo.InvariantCulture) : FloatingWildcard; + var minor = Minor.HasValue ? Minor.GetValueOrDefault().ToString(CultureInfo.InvariantCulture) : FloatingWildcard; + return Patch.HasValue + ? $"{major}.{minor}.{Patch.GetValueOrDefault().ToString(CultureInfo.InvariantCulture)}" + : $"{major}.{minor}"; } } } From 53ae54cacfcc94e65f23cda4ade340e89af5fb64 Mon Sep 17 00:00:00 2001 From: glopesdev Date: Mon, 17 Nov 2025 12:52:13 +0000 Subject: [PATCH 2/2] Refactor firmware metadata CoreVersion is renamed to ProtocolVersion for compliance and an optional patch component is allowed on all version strings. --- src/Bonsai.Harp.Tests/TestFirmwareMetadata.cs | 21 +++++++- src/Bonsai.Harp/FirmwareMetadata.cs | 48 ++++++++++++------- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/Bonsai.Harp.Tests/TestFirmwareMetadata.cs b/src/Bonsai.Harp.Tests/TestFirmwareMetadata.cs index 09ddae0..08648de 100644 --- a/src/Bonsai.Harp.Tests/TestFirmwareMetadata.cs +++ b/src/Bonsai.Harp.Tests/TestFirmwareMetadata.cs @@ -8,8 +8,25 @@ public class TestFirmwareMetadata [TestMethod] public void ParseAndToString_AreReversible() { - var x = new FirmwareMetadata("Behavior", new HarpVersion(2, 5), new HarpVersion(1, 6), new HarpVersion(1, 2), 0); - var y = FirmwareMetadata.Parse(x.ToString()); + var x = new FirmwareMetadata("Behavior", + firmwareVersion: new HarpVersion(2, 5, 2), + protocolVersion: new HarpVersion(1, 9, 0), + hardwareVersion: new HarpVersion(1, 2), + assemblyVersion: 0); + var text = x.ToString(); + var y = FirmwareMetadata.Parse(text); + Assert.IsTrue(x.Equals(y)); + } + + [TestMethod] + public void ParseAndToStringPatchFloating_AreReversible() + { + var x = new FirmwareMetadata("Behavior", + firmwareVersion: new HarpVersion(2, 5), + protocolVersion: new HarpVersion(1, 6), + hardwareVersion: new HarpVersion(1, 2)); + var text = x.ToString(); + var y = FirmwareMetadata.Parse(text); Assert.IsTrue(x.Equals(y)); } } diff --git a/src/Bonsai.Harp/FirmwareMetadata.cs b/src/Bonsai.Harp/FirmwareMetadata.cs index ab88ec3..d025f89 100644 --- a/src/Bonsai.Harp/FirmwareMetadata.cs +++ b/src/Bonsai.Harp/FirmwareMetadata.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Globalization; using System.Text.RegularExpressions; @@ -10,7 +11,7 @@ namespace Bonsai.Harp /// public sealed class FirmwareMetadata : IEquatable { - static readonly Regex MetadataRegex = new Regex("^(?\\w+)-fw(?\\d+\\.\\d+)-harp(?\\d+\\.\\d+)-hw(?(?:x|\\d+)\\.(?:x|\\d+))-ass(?x|\\d+)(?:-preview(?\\d+))?$"); + static readonly Regex MetadataRegex = new("^(?\\w+)-fw(?\\d+\\.\\d+(\\.\\d+)?)-harp(?\\d+\\.\\d+(\\.\\d+)?)-hw(?\\d+\\.\\d+(\\.\\d+)?)-ass(?x|\\d+)(?:-preview(?\\d+))?$"); /// /// Initializes a new instance of the class with the @@ -18,21 +19,21 @@ public sealed class FirmwareMetadata : IEquatable /// /// The unique identifier of the device type on which the firmware should be installed. /// The version of the firmware contained in the device or hex file. - /// The version of the Harp core implemented by the firmware. + /// The version of the Harp core implemented by the firmware. /// The hardware version of the device, or range of hardware versions supported by the firmware. /// The board assembly version of the device, or range of assembly versions supported by the firmware. /// The optional prerelease number, for preview versions of the firmware. public FirmwareMetadata( string deviceName, HarpVersion firmwareVersion, - HarpVersion coreVersion, + HarpVersion protocolVersion, HarpVersion hardwareVersion, int? assemblyVersion = default, int? prereleaseVersion = default) { DeviceName = deviceName ?? throw new ArgumentNullException(nameof(deviceName)); FirmwareVersion = firmwareVersion ?? throw new ArgumentNullException(nameof(firmwareVersion)); - CoreVersion = coreVersion ?? throw new ArgumentNullException(nameof(coreVersion)); + ProtocolVersion = protocolVersion ?? throw new ArgumentNullException(nameof(protocolVersion)); HardwareVersion = hardwareVersion ?? throw new ArgumentNullException(nameof(hardwareVersion)); AssemblyVersion = assemblyVersion; PrereleaseVersion = prereleaseVersion; @@ -49,9 +50,20 @@ public FirmwareMetadata( public HarpVersion FirmwareVersion { get; } /// - /// Gets the version of the Harp core implemented by the firmware. + /// Gets the version of the Harp protocol implemented by the firmware. /// - public HarpVersion CoreVersion { get; } + public HarpVersion ProtocolVersion { get; } + + /// + /// Gets the version of the Harp protocol implemented by the firmware. + /// + /// + /// This property is obsolete. Use instead. + /// + [Obsolete] + [Browsable(false)] + [EditorBrowsable(EditorBrowsableState.Never)] + public HarpVersion CoreVersion => ProtocolVersion; /// /// Gets the hardware version of the device, or range of hardware versions supported by the firmware. @@ -113,7 +125,7 @@ public bool Equals(FirmwareMetadata other) if (other is null) return false; return DeviceName == other.DeviceName && FirmwareVersion.Equals(other.FirmwareVersion) && - CoreVersion.Equals(other.CoreVersion) && + ProtocolVersion.Equals(other.ProtocolVersion) && HardwareVersion.Equals(other.HardwareVersion) && AssemblyVersion == other.AssemblyVersion && PrereleaseVersion == other.PrereleaseVersion; @@ -130,7 +142,7 @@ public override int GetHashCode() { return 17 * DeviceName.GetHashCode() + 8971 * FirmwareVersion.GetHashCode() + - 2803 * CoreVersion.GetHashCode() + + 2803 * ProtocolVersion.GetHashCode() + 691 * HardwareVersion.GetHashCode() + 1409 * AssemblyVersion.GetHashCode() + 2333 * PrereleaseVersion.GetHashCode(); @@ -164,7 +176,7 @@ public override int GetHashCode() /// public static bool operator !=(FirmwareMetadata lhs, FirmwareMetadata rhs) { - if (lhs is null) return !(rhs is null); + if (lhs is null) return rhs is not null; else return !lhs.Equals(rhs); } @@ -200,15 +212,15 @@ public static bool TryParse(string input, out FirmwareMetadata metadata) { if (input == null) throw new ArgumentNullException(nameof(input)); var match = MetadataRegex.Match(input); - if (match.Success && match.Groups.Count == 7) + if (match.Success && match.Groups.Count == 10) { - var deviceName = match.Groups[1].Value; - var firmwareVersion = HarpVersion.Parse(match.Groups[2].Value); - var coreVersion = HarpVersion.Parse(match.Groups[3].Value); - var hardwareVersion = HarpVersion.Parse(match.Groups[4].Value); - var assemblyVersion = match.Groups[5].Value == HarpVersion.FloatingWildcard ? (int?)null : int.Parse(match.Groups[5].Value); - var prereleaseVersion = string.IsNullOrEmpty(match.Groups[6].Value) ? (int?)null : int.Parse(match.Groups[6].Value); - metadata = new FirmwareMetadata(deviceName, firmwareVersion, coreVersion, hardwareVersion, assemblyVersion, prereleaseVersion); + var deviceName = match.Groups[4].Value; + var firmwareVersion = HarpVersion.Parse(match.Groups[5].Value); + var protocolVersion = HarpVersion.Parse(match.Groups[6].Value); + var hardwareVersion = HarpVersion.Parse(match.Groups[7].Value); + var assemblyVersion = match.Groups[8].Value == HarpVersion.FloatingWildcard ? (int?)null : int.Parse(match.Groups[8].Value); + var prereleaseVersion = string.IsNullOrEmpty(match.Groups[9].Value) ? (int?)null : int.Parse(match.Groups[9].Value); + metadata = new FirmwareMetadata(deviceName, firmwareVersion, protocolVersion, hardwareVersion, assemblyVersion, prereleaseVersion); return true; } else @@ -228,7 +240,7 @@ public override string ToString() { var prerelease = PrereleaseVersion.HasValue ? $"-preview{PrereleaseVersion.Value}" : string.Empty; var assemblyNumber = AssemblyVersion.HasValue ? AssemblyVersion.Value.ToString(CultureInfo.InvariantCulture) : HarpVersion.FloatingWildcard; - return $"{DeviceName}-fw{FirmwareVersion}-harp{CoreVersion}-hw{HardwareVersion}-ass{assemblyNumber}{prerelease}"; + return $"{DeviceName}-fw{FirmwareVersion}-harp{ProtocolVersion}-hw{HardwareVersion}-ass{assemblyNumber}{prerelease}"; } } }