feat: add initial project setup
All checks were successful
default / dotnet-default-workflow (push) Successful in 1m2s
release / dotnet-release-workflow (push) Successful in 1m6s

This commit is contained in:
Louis Seubert 2026-04-05 08:23:42 +02:00
commit ed1e31314d
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
32 changed files with 3397 additions and 0 deletions

View file

@ -0,0 +1,15 @@
[*.{cs,vb}]
# disable CA1822: Mark members as static
# -> TUnit requiring instance methods for test cases
dotnet_diagnostic.CA1822.severity = none
# disable CA1707: Identifiers should not contain underscores
dotnet_diagnostic.CA1707.severity = none
# disable IDE0060: Remove unused parameter
dotnet_diagnostic.IDE0060.severity = none
# disable IDE0005: Unnecessary using directive
dotnet_diagnostic.IDE0005.severity = none
# disable IDE0390: Method can be made synchronous
dotnet_diagnostic.IDE0390.severity = none
# disable IDE0391: Method can be made synchronous
dotnet_diagnostic.IDE0391.severity = none

View file

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TUnit" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\semver\Geekeey.SemVer.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,115 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.SemVer.Tests;
internal sealed class SemanticVersionComparerTests
{
[Test]
[Arguments("1.0.0", "2.0.0", -1)]
[Arguments("2.0.0", "1.0.0", 1)]
[Arguments("1.0.0", "1.1.0", -1)]
[Arguments("1.1.0", "1.0.0", 1)]
[Arguments("1.0.0", "1.0.1", -1)]
[Arguments("1.0.1", "1.0.0", 1)]
[Arguments("1.0.0-alpha", "1.0.0", -1)]
[Arguments("1.0.0", "1.0.0-alpha", 1)]
[Arguments("1.0.0-alpha", "1.0.0-alpha.1", -1)]
[Arguments("1.0.0-alpha.1", "1.0.0-alpha.beta", -1)]
[Arguments("1.0.0-alpha.beta", "1.0.0-beta", -1)]
[Arguments("1.0.0-beta", "1.0.0-beta.2", -1)]
[Arguments("1.0.0-beta.2", "1.0.0-beta.11", -1)]
[Arguments("1.0.0-beta.11", "1.0.0-rc.1", -1)]
[Arguments("1.0.0-rc.1", "1.0.0", -1)]
[Arguments("1.0.0-alpha.5", "1.0.0-alpha.10", -1)]
[Arguments("1.0.0-alpha.10", "1.0.0-alpha.5", 1)]
[Arguments("1.0.0-alpha.beta", "1.0.0-alpha.5", 1)]
[Arguments("1.0.0-alpha.5", "1.0.0-alpha.beta", -1)]
[Arguments("1.0.0-alpha.1.2", "1.0.0-alpha.1.2.3", -1)]
[Arguments("1.0.0-alpha.1.2.3", "1.0.0-alpha.1.2", 1)]
[Arguments("1.0.0-alpha-b", "1.0.0-alpha-a", 1)]
[Arguments("1.0.0-0.3.7", "1.0.0-x.7.z.92", -1)]
[Arguments("1.0.0-x.7.z.92", "1.0.0-0.3.7", 1)]
[Arguments("1.0.0+build.1", "1.0.0+build.2", 0)]
[Arguments("1.0.0-alpha+build.1", "1.0.0-alpha+build.2", 0)]
public async Task I_can_compare_by_precedence(string v1Str, string v2Str, int expected)
{
var v1 = SemanticVersion.Parse(v1Str);
var v2 = SemanticVersion.Parse(v2Str);
var result = SemanticVersionComparer.Priority.Compare(v1, v2);
await Assert.That(Math.Sign(result)).IsEqualTo(expected);
}
[Test]
[Arguments("1.0.0+build.1", "1.0.0+build.2", 0)]
[Arguments("1.0.0-alpha+build.1", "1.0.0-alpha+build.2", 0)]
public async Task I_can_confirm_precedence_ignores_metadata(string v1Str, string v2Str, int expected)
{
var v1 = SemanticVersion.Parse(v1Str);
var v2 = SemanticVersion.Parse(v2Str);
var result = SemanticVersionComparer.Priority.Compare(v1, v2);
await Assert.That(result).IsEqualTo(expected);
}
[Test]
[Arguments("1.0.0", "1.0.0+build.1", -1)]
[Arguments("1.0.0+build.1", "1.0.0", 1)]
[Arguments("1.0.0+build.1", "1.0.0+build.2", -1)]
[Arguments("1.0.0+build.1.1", "1.0.0+build.1", 1)]
[Arguments("1.0.0+a.b", "1.0.0+a.a", 1)]
[Arguments("1.0.0+a.a", "1.0.0+a.b", -1)]
[Arguments("1.0.0+a.b", "1.0.0+a.b.c", -1)]
[Arguments("1.0.0+a.b.c", "1.0.0+a.b", 1)]
[Arguments("1.0.0+1.2.3", "1.0.0+1.2.4", -1)]
[Arguments("1.0.0+1.2.4", "1.0.0+1.2.3", 1)]
[Arguments("1.0.0+01", "1.0.0+1", -1)]
public async Task I_can_compare_by_sort_order(string v1Str, string v2Str, int expected)
{
var v1 = SemanticVersion.Parse(v1Str);
var v2 = SemanticVersion.Parse(v2Str);
var result = SemanticVersionComparer.SortOrder.Compare(v1, v2);
await Assert.That(Math.Sign(result)).IsEqualTo(expected);
}
[Test]
public async Task I_can_confirm_precedence_equality_ignores_metadata()
{
var v1 = SemanticVersion.Parse("1.0.0-alpha+build.1");
var v2 = SemanticVersion.Parse("1.0.0-alpha+build.2");
await Assert.That(SemanticVersionComparer.Priority.Equals(v1, v2)).IsTrue();
await Assert.That(SemanticVersionComparer.Priority.GetHashCode(v1)).IsEqualTo(SemanticVersionComparer.Priority.GetHashCode(v2));
}
[Test]
public async Task I_can_confirm_sort_order_equality_includes_metadata()
{
var v1 = SemanticVersion.Parse("1.0.0-alpha+build.1");
var v2 = SemanticVersion.Parse("1.0.0-alpha+build.2");
await Assert.That(SemanticVersionComparer.SortOrder.Equals(v1, v2)).IsFalse();
await Assert.That(SemanticVersionComparer.SortOrder.GetHashCode(v1)).IsNotEqualTo(SemanticVersionComparer.SortOrder.GetHashCode(v2));
}
[Test]
public async Task I_can_confirm_equality_for_identical_versions()
{
var v1 = SemanticVersion.Parse("1.0.0-alpha.1");
var v2 = SemanticVersion.Parse("1.0.0-alpha.1");
await Assert.That(SemanticVersionComparer.Priority.Equals(v1, v2)).IsTrue();
await Assert.That(SemanticVersionComparer.SortOrder.Equals(v1, v2)).IsTrue();
await Assert.That(SemanticVersionComparer.Priority.GetHashCode(v1)).IsEqualTo(SemanticVersionComparer.Priority.GetHashCode(v2));
await Assert.That(SemanticVersionComparer.SortOrder.GetHashCode(v1)).IsEqualTo(SemanticVersionComparer.SortOrder.GetHashCode(v2));
}
[Test]
public async Task I_can_confirm_equality_for_different_versions()
{
var v1 = SemanticVersion.Parse("1.0.0-alpha.1");
var v2 = SemanticVersion.Parse("1.0.0-alpha.2");
await Assert.That(SemanticVersionComparer.Priority.Equals(v1, v2)).IsFalse();
await Assert.That(SemanticVersionComparer.SortOrder.Equals(v1, v2)).IsFalse();
}
}

View file

@ -0,0 +1,261 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
namespace Geekeey.SemVer.Tests;
internal sealed class SemanticVersionRangeTests
{
[Test]
[Arguments("1.2.3", "1.2.3", true)]
[Arguments("1.2.3", "1.2.4", false)]
[Arguments(">1.2.3", "1.2.4", true)]
[Arguments(">1.2.3", "1.2.3", false)]
[Arguments(">=1.2.3", "1.2.3", true)]
[Arguments("<1.2.3", "1.2.2", true)]
[Arguments("<=1.2.3", "1.2.3", true)]
public async Task I_can_satisfy_npm_basic_ranges(string range, string version, bool expected)
{
var r = SemanticVersionRange.Parse(range);
var v = SemanticVersion.Parse(version);
await Assert.That(r.Contains(v)).IsEqualTo(expected);
}
[Test]
[Arguments("1.2.3 - 2.3.4", "1.2.3", true)]
[Arguments("1.2.3 - 2.3.4", "2.3.4", true)]
[Arguments("1.2.3 - 2.3.4", "1.2.2", false)]
[Arguments("1.2.3 - 2.3.4", "2.3.5", false)]
public async Task I_can_satisfy_npm_hyphen_ranges(string range, string version, bool expected)
{
var r = SemanticVersionRange.Parse(range);
var v = SemanticVersion.Parse(version);
await Assert.That(r.Contains(v)).IsEqualTo(expected);
}
[Test]
[Arguments("^1.2.3", "1.2.3", true)]
[Arguments("^1.2.3", "1.9.9", true)]
[Arguments("^1.2.3", "2.0.0", false)]
[Arguments("^0.2.3", "0.2.3", true)]
[Arguments("^0.2.3", "0.2.4", true)]
[Arguments("^0.2.3", "0.3.0", false)]
[Arguments("^0.0.3", "0.0.3", true)]
[Arguments("^0.0.3", "0.0.4", false)]
public async Task I_can_satisfy_npm_caret_ranges(string range, string version, bool expected)
{
var r = SemanticVersionRange.Parse(range);
var v = SemanticVersion.Parse(version);
await Assert.That(r.Contains(v)).IsEqualTo(expected);
}
[Test]
[Arguments("~1.2.3", "1.2.3", true)]
[Arguments("~1.2.3", "1.2.9", true)]
[Arguments("~1.2.3", "1.3.0", false)]
[Arguments("~1.2", "1.2.0", true)]
[Arguments("~1.2", "1.2.9", true)]
[Arguments("~1.2", "1.3.0", false)]
[Arguments("~1", "1.0.0", true)]
[Arguments("~1", "1.9.9", true)]
[Arguments("~1", "2.0.0", false)]
public async Task I_can_satisfy_npm_tilde_ranges(string range, string version, bool expected)
{
var r = SemanticVersionRange.Parse(range);
var v = SemanticVersion.Parse(version);
await Assert.That(r.Contains(v)).IsEqualTo(expected);
}
[Test]
[Arguments("1.2.x", "1.2.0", true)]
[Arguments("1.2.x", "1.2.9", true)]
[Arguments("1.2.x", "1.3.0", false)]
[Arguments("1.x", "1.0.0", true)]
[Arguments("1.x", "1.9.9", true)]
[Arguments("1.x", "2.0.0", false)]
[Arguments("*", "0.0.0", true)]
[Arguments("*", "9.9.9", true)]
public async Task I_can_satisfy_npm_wildcard_ranges(string range, string version, bool expected)
{
var r = SemanticVersionRange.Parse(range);
var v = SemanticVersion.Parse(version);
await Assert.That(r.Contains(v)).IsEqualTo(expected);
}
[Test]
public async Task I_can_parse_exact_version_ranges_via_public_api()
{
var range = SemanticVersionRange.Parse("=1.2.3");
await Assert.That(range.Contains(SemanticVersion.Parse("1.2.3"))).IsTrue();
await Assert.That(range.Contains(SemanticVersion.Parse("1.2.4"))).IsFalse();
await Assert.That(range.ToString("n", null)).IsEqualTo("1.2.3");
}
[Test]
[Arguments(">1.2.3 || <1.0.0", "1.2.4", true)]
[Arguments(">1.2.3 || <1.0.0", "0.9.9", true)]
[Arguments(">1.2.3 || <1.0.0", "1.1.0", false)]
[Arguments(">1.0.0 <=3.2.6 || >=1.0.0 <6.0.0", "2.0.0", true)]
[Arguments(">1.0.0 <=3.2.6 || >=1.0.0 <6.0.0", "5.0.0", true)]
[Arguments(">1.0.0 <=3.2.6 || >=1.0.0 <6.0.0", "7.0.0", false)]
public async Task I_can_satisfy_npm_or_ranges(string range, string version, bool expected)
{
var r = SemanticVersionRange.Parse(range);
var v = SemanticVersion.Parse(version);
await Assert.That(r.Contains(v)).IsEqualTo(expected);
}
[Test]
[Arguments("^6.2.3-alpha.1", "6.2.3-alpha.1", true)]
[Arguments("^6.2.3-alpha.1", "6.2.3-beta", true)]
[Arguments("^6.2.3-alpha.1", "6.2.2", false)]
[Arguments("^6.2.3-alpha.1", "7.0.0", false)]
public async Task I_can_satisfy_complex_npm_ranges(string range, string version, bool expected)
{
var r = SemanticVersionRange.Parse(range);
var v = SemanticVersion.Parse(version);
await Assert.That(r.Contains(v)).IsEqualTo(expected);
}
[Test]
public async Task I_can_handle_two_constraints_without_combining_them()
{
var range = SemanticVersionRange.Parse(">=1.0.0 !=2.0.0");
await Assert.That(range.Contains(SemanticVersion.Parse("1.5.0"))).IsTrue();
await Assert.That(range.Contains(SemanticVersion.Parse("2.0.0"))).IsFalse();
await Assert.That(range.ToString("n", null)).IsEqualTo(">=1.0.0 !=2.0.0");
}
[Test]
[Arguments("^5.*", "5.1.1", "6.1.0", ">=5.0.0 <6.0.0")]
[Arguments("5.*", "5.1.1", "6.1.0", ">=5.0.0 <6.0.0")]
[Arguments(">=5.*", "5.1.1", "6.1.0", ">=5.0.0 <6.0.0")]
[Arguments(">5.*", "5.0.1", "5.0.0", ">5.0.0 <6.0.0")]
[Arguments("<=5.*", "5.9.9", "6.0.0", "<6.0.0")]
[Arguments("<5.*", "4.9.9", "5.0.0", "<5.0.0")]
public async Task I_can_handle_ranges_with_wildcard(string range, string inside, string outside, string expected)
{
var r = SemanticVersionRange.Parse(range);
await Assert.That(r.Contains(SemanticVersion.Parse(inside))).IsTrue();
await Assert.That(r.Contains(SemanticVersion.Parse(outside))).IsFalse();
await Assert.That(r.ToString("n", null)).IsEqualTo(expected);
}
[Test]
[Arguments("[1.2.3]", "1.2.3", true)]
[Arguments("[1.2.3]", "1.2.4", false)]
[Arguments("[1.2.3, 1.4.0)", "1.2.3", true)]
[Arguments("[1.2.3, 1.4.0)", "1.3.9", true)]
[Arguments("[1.2.3, 1.4.0)", "1.4.0", false)]
[Arguments("(1.2.3, 1.4.0]", "1.2.3", false)]
[Arguments("(1.2.3, 1.4.0]", "1.4.0", true)]
[Arguments("[1.2.3,)", "1.2.3", true)]
[Arguments("[1.2.3,)", "9.9.9", true)]
[Arguments("(,1.4.0]", "1.4.0", true)]
[Arguments("(,1.4.0]", "0.0.0", true)]
[Arguments("(,)", "0.0.0", true)]
[Arguments("(,)", "9.9.9", true)]
public async Task I_can_satisfy_maven_ranges(string range, string version, bool expected)
{
var r = SemanticVersionRange.Parse(range);
var v = SemanticVersion.Parse(version);
await Assert.That(r.Contains(v)).IsEqualTo(expected);
}
[Test]
[Arguments("[1.2,1.3],[1.5,)", "1.2.0", true)]
[Arguments("[1.2,1.3],[1.5,)", "1.4.0", false)]
[Arguments("[1.2,1.3],[1.5,)", "1.5.0", true)]
public async Task I_can_satisfy_maven_or_ranges(string range, string version, bool expected)
{
var r = SemanticVersionRange.Parse(range);
var v = SemanticVersion.Parse(version);
await Assert.That(r.Contains(v)).IsEqualTo(expected);
}
[Test]
[Arguments("^1.2.3", "1.2.3-alpha", false)]
[Arguments("^1.2.3-alpha", "1.2.3-beta", true)]
[Arguments("^1.2.3-alpha", "1.2.4", true)]
[Arguments("^1.2.3-alpha", "1.3.0-alpha", true)]
public async Task I_can_satisfy_prerelease_rules(string range, string version, bool expected)
{
var r = SemanticVersionRange.Parse(range);
var v = SemanticVersion.Parse(version);
await Assert.That(r.Contains(v)).IsEqualTo(expected);
}
[Test]
[Arguments(">=1.0.0 !2.0.0")]
[Arguments("1.2.3 - ")]
[Arguments("!=1.x")]
public async Task I_can_not_parse_invalid_ranges(string range)
{
await Assert.That(() => SemanticVersionRange.Parse(range))
.Throws<FormatException>();
}
[Test]
public async Task I_can_serialize_to_json()
{
var r = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
var json = System.Text.Json.JsonSerializer.Serialize(r);
await Assert.That(json).IsEqualTo("\"^1.2.3\"");
}
[Test]
public async Task I_can_deserialize_from_json()
{
var json = "\"^1.2.3\"";
var r = System.Text.Json.JsonSerializer.Deserialize<SemanticVersionRange>(json);
await Assert.That(r.ToString()).IsEqualTo("^1.2.3");
await Assert.That(r.Contains(new SemanticVersion(1, 2, 4))).IsTrue();
}
[Test]
public async Task I_use_npm_short_format_by_default()
{
var value = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
await Assert.That(value.ToString()).IsEqualTo("^1.2.3");
}
[Test]
[Arguments("^1.2.3", "m", "[1.2.3,2.0.0)")]
[Arguments("~1.2.3", "m", "[1.2.3,1.3.0)")]
[Arguments("1.2.3", "m", "[1.2.3]")]
[Arguments("[1.2.3,2.0.0)", "n", ">=1.2.3 <2.0.0")]
[Arguments("[1.2.3,2.0.0)", "ns", "^1.2.3")]
[Arguments("[1.2.3,1.3.0)", "ns", "~1.2.3")]
[Arguments("[1.2,1.3],[1.5,)", "n", ">=1.2.0 <=1.3.0 || >=1.5.0")]
[Arguments("*", "m", "(,)")]
[Arguments("(,)", "ns", "*")]
public async Task I_can_convert_range_formats(string range, string format, string expected)
{
var value = SemanticVersionRange.Parse(range);
await Assert.That(value.ToString(format, null)).IsEqualTo(expected);
}
[Test]
public async Task I_can_format_ranges_to_chars()
{
var value = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
var destination = new char[32];
var success = value.TryFormat(destination, out var charsWritten, "ns", null);
await Assert.That(success).IsTrue();
await Assert.That(new string(destination[..charsWritten])).IsEqualTo("^1.2.3");
}
[Test]
public async Task I_fail_formatting_when_the_tentative_short_form_overflows()
{
var value = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
var destination = new char[5];
var success = value.TryFormat(destination, out var charsWritten, "ns", null);
await Assert.That(success).IsFalse();
await Assert.That(charsWritten).IsEqualTo(0);
}
}

View file

@ -0,0 +1,225 @@
// Copyright (c) The Geekeey Authors
// SPDX-License-Identifier: EUPL-1.2
using System.Text.Encodings.Web;
using System.Text.Json;
namespace Geekeey.SemVer.Tests;
internal sealed class SemanticVersionTests
{
private static readonly JsonSerializerOptions RelaxedOptions = new()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
[Test]
[Arguments(0, 0, 0, 0)]
[Arguments(1, 1, 1, 0)]
[Arguments(1, 2, 0, 0)]
[Arguments(45, 2, 4, 0)]
public async Task I_can_create_semver_from_version(int major, int minor, int build, int revision)
{
var version = new Version(major, minor, build, revision);
var v = new SemanticVersion(version);
await Assert.That(v.Major).IsEqualTo((ulong)major);
await Assert.That(v.Minor).IsEqualTo((ulong)minor);
await Assert.That(v.Patch).IsEqualTo((ulong)build);
await Assert.That(v.Prerelease).IsNull();
await Assert.That(v.Metadata).IsNull();
}
[Test]
[Arguments(1UL)]
[Arguments(0UL)]
public async Task I_can_create_semver_with_major(ulong major)
{
var v = new SemanticVersion(major);
await Assert.That(v.Major).IsEqualTo(major);
await Assert.That(v.Minor).IsEqualTo(0UL);
await Assert.That(v.Patch).IsEqualTo(0UL);
await Assert.That(v.Prerelease).IsNull();
await Assert.That(v.Metadata).IsNull();
}
[Test]
[Arguments(1UL, 2UL)]
public async Task I_can_create_semver_with_major_and_minor(ulong major, ulong minor)
{
var v = new SemanticVersion(major, minor);
await Assert.That(v.Major).IsEqualTo(major);
await Assert.That(v.Minor).IsEqualTo(minor);
await Assert.That(v.Patch).IsEqualTo(0UL);
await Assert.That(v.Prerelease).IsNull();
await Assert.That(v.Metadata).IsNull();
}
[Test]
[Arguments(1UL, 2UL, 3UL)]
public async Task I_can_create_semver_with_major_minor_and_patch(ulong major, ulong minor, ulong patch)
{
var v = new SemanticVersion(major, minor, patch);
await Assert.That(v.Major).IsEqualTo(major);
await Assert.That(v.Minor).IsEqualTo(minor);
await Assert.That(v.Patch).IsEqualTo(patch);
await Assert.That(v.Prerelease).IsNull();
await Assert.That(v.Metadata).IsNull();
}
[Test]
[Arguments(1UL, 2UL, 3UL, "alpha", "build.1")]
[Arguments(1UL, 2UL, 3UL, null, null)]
public async Task I_can_create_semver_with_all_components(ulong major, ulong minor, ulong patch, string? prerelease, string? metadata)
{
var v = new SemanticVersion(major, minor, patch, prerelease, metadata);
await Assert.That(v.Major).IsEqualTo(major);
await Assert.That(v.Minor).IsEqualTo(minor);
await Assert.That(v.Patch).IsEqualTo(patch);
await Assert.That(v.Prerelease).IsEqualTo(prerelease);
await Assert.That(v.Metadata).IsEqualTo(metadata);
}
[Test]
public async Task I_can_compare_identical_versions()
{
var v1 = new SemanticVersion(1, 2, 3);
var v2 = new SemanticVersion(1, 2, 3);
await Assert.That(v1.CompareTo(v2)).IsEqualTo(0);
}
[Test]
public async Task I_can_compare_with_objects()
{
var v1 = new SemanticVersion(1, 2, 3);
object v2 = new SemanticVersion(1, 2, 3);
await Assert.That(v1.CompareTo(v2)).IsEqualTo(0);
await Assert.That(v1.CompareTo(null)).IsEqualTo(1);
await Assert.That(v1.CompareTo("not a version")).IsEqualTo(1);
}
[Test]
public async Task I_can_confirm_operators_work()
{
var v1 = new SemanticVersion(1, 2, 3);
var v2 = new SemanticVersion(2, 0, 0);
await Assert.That(v1 < v2).IsTrue();
await Assert.That(v1 <= v2).IsTrue();
await Assert.That(v1 > v2).IsFalse();
await Assert.That(v1 >= v2).IsFalse();
}
[Test]
public async Task I_can_parse_from_string()
{
var result = SemanticVersion.Parse("1.2.3");
await Assert.That(result.Major).IsEqualTo(1UL);
await Assert.That(result.Minor).IsEqualTo(2UL);
await Assert.That(result.Patch).IsEqualTo(3UL);
}
[Test]
public async Task I_can_try_parse_from_string()
{
var success = SemanticVersion.TryParse("1.2.3-alpha+build.1", out var result);
await Assert.That(success).IsTrue();
await Assert.That(result.Major).IsEqualTo(1UL);
await Assert.That(result.Minor).IsEqualTo(2UL);
await Assert.That(result.Patch).IsEqualTo(3UL);
await Assert.That(result.Prerelease).IsEqualTo("alpha");
await Assert.That(result.Metadata).IsEqualTo("build.1");
}
[Test]
[Arguments(1, 2, 3, null, null, "1.2.3")]
[Arguments(1, 2, 3, "alpha", null, "1.2.3-alpha")]
[Arguments(1, 2, 3, "alpha", "build.1", "1.2.3-alpha+build.1")]
public async Task I_can_get_string_representation(ulong major, ulong minor, ulong patch, string? pre, string? meta, string expected)
{
var v = new SemanticVersion(major, minor, patch, pre, meta);
// Note: format "f" is needed for full version including metadata based on code
var format = string.IsNullOrEmpty(meta) ? (string.IsNullOrEmpty(pre) ? null : "s") : "f";
await Assert.That(v.ToString(format, null)).IsEqualTo(expected);
}
[Test]
public async Task I_can_format_to_chars()
{
var v = new SemanticVersion(1, 2, 3, "beta", "456");
var dest = new char[32];
var success = v.TryFormat(dest, out var charsWritten, "f", null);
await Assert.That(success).IsTrue();
await Assert.That(new string(dest[..charsWritten])).IsEqualTo("1.2.3-beta+456");
}
[Test]
[Arguments(1, 2, 3, null, null, "\"1.2.3\"")]
[Arguments(1, 2, 3, "alpha", null, "\"1.2.3-alpha\"")]
[Arguments(1, 2, 3, "alpha", "build.1", "\"1.2.3-alpha+build.1\"")]
public async Task I_can_serialize_to_json(ulong major, ulong minor, ulong patch, string? pre, string? meta, string expectedJson)
{
var v = new SemanticVersion(major, minor, patch, pre, meta);
var json = JsonSerializer.Serialize(v, RelaxedOptions);
await Assert.That(json).IsEqualTo(expectedJson);
}
[Test]
public async Task I_can_deserialize_from_json()
{
var json = "\"1.2.3-beta+789\"";
var v = JsonSerializer.Deserialize<SemanticVersion>(json);
await Assert.That(v.Major).IsEqualTo(1UL);
await Assert.That(v.Minor).IsEqualTo(2UL);
await Assert.That(v.Patch).IsEqualTo(3UL);
await Assert.That(v.Prerelease).IsEqualTo("beta");
await Assert.That(v.Metadata).IsEqualTo("789");
}
[Test]
public async Task I_can_handle_invalid_json_token()
{
var json = "123"; // Number instead of string
await Assert.That(() => JsonSerializer.Deserialize<SemanticVersion>(json))
.Throws<JsonException>()
.WithMessage("Expected string");
}
[Test]
public async Task I_can_serialize_as_part_of_object()
{
var obj = new { Version = new SemanticVersion(1, 0, 0, "rc.1", "metadata") };
var json = JsonSerializer.Serialize(obj, RelaxedOptions);
await Assert.That(json).IsEqualTo(/*lang=json,strict*/ "{\"Version\":\"1.0.0-rc.1+metadata\"}");
}
[Test]
public async Task I_can_deserialize_null()
{
var json = "null";
var v = JsonSerializer.Deserialize<SemanticVersion>(json);
await Assert.That(v).IsEqualTo(default(SemanticVersion));
}
[Test]
public async Task I_can_deserialize_empty_string()
{
var json = "\"\"";
await Assert.That(() => JsonSerializer.Deserialize<SemanticVersion>(json))
.Throws<JsonException>();
}
}