stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests;
|
||||
|
||||
public partial class DebianVersionComparerTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_OpenSSL_DebianBackport_CorrectlyIdentifiesVulnerable()
|
||||
{
|
||||
// Installed version vs fixed version from DSA
|
||||
var installed = "1:1.1.1k-1+deb11u1";
|
||||
var fixedVersion = "1:1.1.1k-1+deb11u2";
|
||||
|
||||
var result = _comparer.CompareWithProof(installed, fixedVersion);
|
||||
|
||||
result.IsLessThan.Should().BeTrue("installed version should be less than fixed");
|
||||
result.IsGreaterThanOrEqual.Should().BeFalse("installed is VULNERABLE");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_OpenSSL_DebianBackport_CorrectlyIdentifiesFixed()
|
||||
{
|
||||
// Installed version >= fixed version
|
||||
var installed = "1:1.1.1k-1+deb11u2";
|
||||
var fixedVersion = "1:1.1.1k-1+deb11u2";
|
||||
|
||||
var result = _comparer.CompareWithProof(installed, fixedVersion);
|
||||
|
||||
result.IsGreaterThanOrEqual.Should().BeTrue("installed version is FIXED");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_UbuntuSecurityBackport_CorrectlyIdentifies()
|
||||
{
|
||||
// Ubuntu security backport pattern
|
||||
var installed = "1.0-1ubuntu0.1";
|
||||
var fixedVersion = "1.0-1ubuntu0.2";
|
||||
|
||||
var result = _comparer.CompareWithProof(installed, fixedVersion);
|
||||
|
||||
result.IsLessThan.Should().BeTrue("installed is older security backport");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests;
|
||||
|
||||
public partial class DebianVersionComparerTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(DebianComparisonCases))]
|
||||
public void Compare_DebianVersions_ReturnsExpectedOrder(string left, string right, int expected)
|
||||
{
|
||||
var result = Math.Sign(_comparer.Compare(left, right));
|
||||
result.Should().Be(expected, because: $"comparing '{left}' with '{right}'");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_SameVersion_ReturnsZero()
|
||||
{
|
||||
_comparer.Compare("1:1.1.1k-1+deb11u1", "1:1.1.1k-1+deb11u1").Should().Be(0);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_NullLeft_ReturnsNegative()
|
||||
{
|
||||
_comparer.Compare(null, "1.0-1").Should().BeNegative();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_NullRight_ReturnsPositive()
|
||||
{
|
||||
_comparer.Compare("1.0-1", null).Should().BePositive();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests;
|
||||
|
||||
public partial class DebianVersionComparerTests
|
||||
{
|
||||
public static TheoryData<string, string, int> DebianComparisonCases => new()
|
||||
{
|
||||
// Epoch precedence
|
||||
{ "0:1.0-1", "1:0.1-1", -1 },
|
||||
{ "1:1.0-1", "0:9.9-9", 1 },
|
||||
{ "1.0-1", "0:1.0-1", 0 }, // Missing epoch = 0
|
||||
{ "2:1.0-1", "1:9.9-9", 1 },
|
||||
|
||||
// Upstream version ordering
|
||||
{ "1.9-1", "1.10-1", -1 },
|
||||
{ "1.02-1", "1.2-1", 0 }, // Leading zeros ignored
|
||||
{ "1.0-1", "1.0.1-1", -1 },
|
||||
|
||||
// Tilde pre-releases
|
||||
{ "1.0~rc1-1", "1.0-1", -1 },
|
||||
{ "1.0~alpha-1", "1.0~beta-1", -1 },
|
||||
{ "2.0~rc1", "2.0", -1 },
|
||||
{ "1.0~-1", "1.0-1", -1 },
|
||||
|
||||
// Debian revision
|
||||
{ "1.0-1", "1.0-2", -1 },
|
||||
{ "1.0-1ubuntu1", "1.0-1ubuntu2", -1 },
|
||||
{ "1.0-1+deb11u1", "1.0-1+deb11u2", -1 },
|
||||
|
||||
// Ubuntu backport patterns
|
||||
{ "1.0-1", "1.0-1ubuntu0.1", -1 },
|
||||
{ "1.0-1ubuntu0.1", "1.0-1ubuntu0.2", -1 },
|
||||
{ "1.0-1build1", "1.0-1build2", -1 },
|
||||
|
||||
// Native packages (no revision)
|
||||
{ "1.0", "1.0-1", -1 },
|
||||
{ "1.1", "1.0-99", 1 },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests;
|
||||
|
||||
public partial class DebianVersionComparerTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("1:1.2-1", "0:9.9-9", 1)] // Epoch jump
|
||||
[InlineData("2.0~rc1", "2.0", -1)] // Tilde pre-release
|
||||
[InlineData("1.2-3+deb12u1", "1.2-3+deb12u2", -1)] // Debian stable update
|
||||
[InlineData("1.2-3ubuntu0.1", "1.2-3", 1)] // Ubuntu backport
|
||||
public void Compare_EdgeCases_ReturnsExpectedOrder(string left, string right, int expected)
|
||||
{
|
||||
var result = Math.Sign(_comparer.Compare(left, right));
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit;
|
||||
using StellaOps.VersionComparison;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests;
|
||||
|
||||
public partial class DebianVersionComparerTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_EpochDifference_ReturnsEpochProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("0:1.0-1", "1:0.1-1");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.Comparator.Should().Be(ComparatorType.Dpkg);
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Epoch:") && line.Contains("left is older"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_SameEpochDifferentVersion_ReturnsVersionProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("1:1.1.1k-1", "1:1.1.1l-1");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Epoch:") && line.Contains("equal"));
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Upstream version:") && line.Contains("left is older"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_SameVersionDifferentRevision_ReturnsRevisionProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("1:1.1.1k-1+deb11u1", "1:1.1.1k-1+deb11u2");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Upstream version:") && line.Contains("equal"));
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Debian revision:") && line.Contains("left is older"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_EqualVersions_ReturnsEqualProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("1:1.1.1k-1+deb11u1", "1:1.1.1k-1+deb11u1");
|
||||
|
||||
result.Comparison.Should().Be(0);
|
||||
result.IsEqual.Should().BeTrue();
|
||||
result.ProofLines.Should().AllSatisfy(line => line.Should().Contain("equal"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_TildePreRelease_ReturnsCorrectProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("2.0~rc1-1", "2.0-1");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Upstream version:") && line.Contains("left is older"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_NativePackage_HandlesEmptyRevision()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("1.0", "1.0-1");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Debian revision:"));
|
||||
}
|
||||
}
|
||||
@@ -1,207 +1,8 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.VersionComparison;
|
||||
using StellaOps.VersionComparison.Comparers;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.VersionComparison.Tests;
|
||||
|
||||
public class DebianVersionComparerTests
|
||||
public partial class DebianVersionComparerTests
|
||||
{
|
||||
private readonly DebianVersionComparer _comparer = DebianVersionComparer.Instance;
|
||||
|
||||
#region Basic Comparison Tests
|
||||
|
||||
public static TheoryData<string, string, int> DebianComparisonCases => new()
|
||||
{
|
||||
// Epoch precedence
|
||||
{ "0:1.0-1", "1:0.1-1", -1 },
|
||||
{ "1:1.0-1", "0:9.9-9", 1 },
|
||||
{ "1.0-1", "0:1.0-1", 0 }, // Missing epoch = 0
|
||||
{ "2:1.0-1", "1:9.9-9", 1 },
|
||||
|
||||
// Upstream version ordering
|
||||
{ "1.9-1", "1.10-1", -1 },
|
||||
{ "1.02-1", "1.2-1", 0 }, // Leading zeros ignored
|
||||
{ "1.0-1", "1.0.1-1", -1 },
|
||||
|
||||
// Tilde pre-releases
|
||||
{ "1.0~rc1-1", "1.0-1", -1 },
|
||||
{ "1.0~alpha-1", "1.0~beta-1", -1 },
|
||||
{ "2.0~rc1", "2.0", -1 },
|
||||
{ "1.0~-1", "1.0-1", -1 },
|
||||
|
||||
// Debian revision
|
||||
{ "1.0-1", "1.0-2", -1 },
|
||||
{ "1.0-1ubuntu1", "1.0-1ubuntu2", -1 },
|
||||
{ "1.0-1+deb11u1", "1.0-1+deb11u2", -1 },
|
||||
|
||||
// Ubuntu backport patterns
|
||||
{ "1.0-1", "1.0-1ubuntu0.1", -1 },
|
||||
{ "1.0-1ubuntu0.1", "1.0-1ubuntu0.2", -1 },
|
||||
{ "1.0-1build1", "1.0-1build2", -1 },
|
||||
|
||||
// Native packages (no revision)
|
||||
{ "1.0", "1.0-1", -1 },
|
||||
{ "1.1", "1.0-99", 1 },
|
||||
};
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(DebianComparisonCases))]
|
||||
public void Compare_DebianVersions_ReturnsExpectedOrder(string left, string right, int expected)
|
||||
{
|
||||
var result = Math.Sign(_comparer.Compare(left, right));
|
||||
result.Should().Be(expected, because: $"comparing '{left}' with '{right}'");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_SameVersion_ReturnsZero()
|
||||
{
|
||||
_comparer.Compare("1:1.1.1k-1+deb11u1", "1:1.1.1k-1+deb11u1").Should().Be(0);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_NullLeft_ReturnsNegative()
|
||||
{
|
||||
_comparer.Compare(null, "1.0-1").Should().BeNegative();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_NullRight_ReturnsPositive()
|
||||
{
|
||||
_comparer.Compare("1.0-1", null).Should().BePositive();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Proof Line Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_EpochDifference_ReturnsEpochProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("0:1.0-1", "1:0.1-1");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.Comparator.Should().Be(ComparatorType.Dpkg);
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Epoch:") && line.Contains("left is older"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_SameEpochDifferentVersion_ReturnsVersionProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("1:1.1.1k-1", "1:1.1.1l-1");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Epoch:") && line.Contains("equal"));
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Upstream version:") && line.Contains("left is older"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_SameVersionDifferentRevision_ReturnsRevisionProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("1:1.1.1k-1+deb11u1", "1:1.1.1k-1+deb11u2");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Upstream version:") && line.Contains("equal"));
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Debian revision:") && line.Contains("left is older"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_EqualVersions_ReturnsEqualProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("1:1.1.1k-1+deb11u1", "1:1.1.1k-1+deb11u1");
|
||||
|
||||
result.Comparison.Should().Be(0);
|
||||
result.IsEqual.Should().BeTrue();
|
||||
result.ProofLines.Should().AllSatisfy(line => line.Should().Contain("equal"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_TildePreRelease_ReturnsCorrectProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("2.0~rc1-1", "2.0-1");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Upstream version:") && line.Contains("left is older"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_NativePackage_HandlesEmptyRevision()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("1.0", "1.0-1");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Debian revision:"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("1:1.2-1", "0:9.9-9", 1)] // Epoch jump
|
||||
[InlineData("2.0~rc1", "2.0", -1)] // Tilde pre-release
|
||||
[InlineData("1.2-3+deb12u1", "1.2-3+deb12u2", -1)] // Debian stable update
|
||||
[InlineData("1.2-3ubuntu0.1", "1.2-3", 1)] // Ubuntu backport
|
||||
public void Compare_EdgeCases_ReturnsExpectedOrder(string left, string right, int expected)
|
||||
{
|
||||
var result = Math.Sign(_comparer.Compare(left, right));
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Real-World Advisory Scenarios
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_OpenSSL_DebianBackport_CorrectlyIdentifiesVulnerable()
|
||||
{
|
||||
// Installed version vs fixed version from DSA
|
||||
var installed = "1:1.1.1k-1+deb11u1";
|
||||
var fixedVersion = "1:1.1.1k-1+deb11u2";
|
||||
|
||||
var result = _comparer.CompareWithProof(installed, fixedVersion);
|
||||
|
||||
result.IsLessThan.Should().BeTrue("installed version should be less than fixed");
|
||||
result.IsGreaterThanOrEqual.Should().BeFalse("installed is VULNERABLE");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_OpenSSL_DebianBackport_CorrectlyIdentifiesFixed()
|
||||
{
|
||||
// Installed version >= fixed version
|
||||
var installed = "1:1.1.1k-1+deb11u2";
|
||||
var fixedVersion = "1:1.1.1k-1+deb11u2";
|
||||
|
||||
var result = _comparer.CompareWithProof(installed, fixedVersion);
|
||||
|
||||
result.IsGreaterThanOrEqual.Should().BeTrue("installed version is FIXED");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_UbuntuSecurityBackport_CorrectlyIdentifies()
|
||||
{
|
||||
// Ubuntu security backport pattern
|
||||
var installed = "1.0-1ubuntu0.1";
|
||||
var fixedVersion = "1.0-1ubuntu0.2";
|
||||
|
||||
var result = _comparer.CompareWithProof(installed, fixedVersion);
|
||||
|
||||
result.IsLessThan.Should().BeTrue("installed is older security backport");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using FluentAssertions;
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using StellaOps.VersionComparison.Comparers;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests.Properties;
|
||||
|
||||
public partial class VersionComparisonPropertyTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Null handling: null is less than any valid version.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_NullIsLessThanAnyVersion()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
RpmVersionArb(),
|
||||
version => ((IComparer<string>)RpmVersionComparer.Instance).Compare(null, version) < 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Null handling: any valid version is greater than null.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_AnyVersionGreaterThanNull()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
RpmVersionArb(),
|
||||
version => ((IComparer<string>)RpmVersionComparer.Instance).Compare(version, null) > 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Null handling: null equals null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RpmComparer_NullEqualsNull()
|
||||
{
|
||||
((IComparer<string>)RpmVersionComparer.Instance).Compare(null, null).Should().Be(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Null handling: null is less than any valid Debian version.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_NullIsLessThanAnyVersion()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
DebianVersionArb(),
|
||||
version => ((IComparer<string>)DebianVersionComparer.Instance).Compare(null, version) < 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Null handling: any valid Debian version is greater than null.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_AnyVersionGreaterThanNull()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
DebianVersionArb(),
|
||||
version => ((IComparer<string>)DebianVersionComparer.Instance).Compare(version, null) > 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Null handling: null equals null for Debian.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DebianComparer_NullEqualsNull()
|
||||
{
|
||||
((IComparer<string>)DebianVersionComparer.Instance).Compare(null, null).Should().Be(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using StellaOps.VersionComparison.Comparers;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests.Properties;
|
||||
|
||||
public partial class VersionComparisonPropertyTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Monotonicity: Incrementing epoch always results in newer version.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_EpochMonotonicity()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Choose(0, 10).ToArbitrary(),
|
||||
Gen.Elements("1.0", "2.5", "1.0.1", "10.20.30").ToArbitrary(),
|
||||
Gen.Elements("1", "1ubuntu1", "2build1").ToArbitrary(),
|
||||
(epoch, version, revision) =>
|
||||
{
|
||||
var lower = $"{epoch}:{version}-{revision}";
|
||||
var higher = $"{epoch + 1}:{version}-{revision}";
|
||||
return DebianVersionComparer.Instance.Compare(lower, higher) < 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Boundary: Tilde pre-release always sorts before release.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_TildePreReleaseBehavior()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Elements("1.0", "2.0", "3.5.1").ToArbitrary(),
|
||||
Gen.Elements("alpha", "beta", "rc1", "rc2").ToArbitrary(),
|
||||
(version, prerelease) =>
|
||||
{
|
||||
var preReleaseVersion = $"{version}~{prerelease}-1";
|
||||
var releaseVersion = $"{version}-1";
|
||||
return DebianVersionComparer.Instance.Compare(preReleaseVersion, releaseVersion) < 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Proof lines are non-empty for valid comparisons.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_ProofLinesNonEmpty()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
DebianVersionArb(),
|
||||
DebianVersionArb(),
|
||||
(x, y) =>
|
||||
{
|
||||
var result = DebianVersionComparer.Instance.CompareWithProof(x, y);
|
||||
return result.ProofLines.Length > 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using StellaOps.VersionComparison.Comparers;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests.Properties;
|
||||
|
||||
public partial class VersionComparisonPropertyTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Reflexivity: Any version equals itself.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_Reflexivity()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
DebianVersionArb(),
|
||||
version => DebianVersionComparer.Instance.Compare(version, version) == 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Anti-symmetry: Compare(x, y) == -Compare(y, x).
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_AntiSymmetry()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
DebianVersionArb(),
|
||||
DebianVersionArb(),
|
||||
(x, y) =>
|
||||
{
|
||||
var cmpXY = Math.Sign(DebianVersionComparer.Instance.Compare(x, y));
|
||||
var cmpYX = Math.Sign(DebianVersionComparer.Instance.Compare(y, x));
|
||||
return cmpXY == -cmpYX;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transitivity: if x <= y and y <= z then x <= z.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_Transitivity()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
DebianVersionArb(),
|
||||
DebianVersionArb(),
|
||||
DebianVersionArb(),
|
||||
(x, y, z) =>
|
||||
{
|
||||
var comparer = DebianVersionComparer.Instance;
|
||||
var cmpXY = comparer.Compare(x, y);
|
||||
var cmpYZ = comparer.Compare(y, z);
|
||||
var cmpXZ = comparer.Compare(x, z);
|
||||
|
||||
if (cmpXY <= 0 && cmpYZ <= 0)
|
||||
{
|
||||
return cmpXZ <= 0;
|
||||
}
|
||||
|
||||
if (cmpXY >= 0 && cmpYZ >= 0)
|
||||
{
|
||||
return cmpXZ >= 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consistency: Same input produces same result (determinism).
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_IsDeterministic()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
DebianVersionArb(),
|
||||
DebianVersionArb(),
|
||||
(x, y) =>
|
||||
{
|
||||
var result1 = DebianVersionComparer.Instance.Compare(x, y);
|
||||
var result2 = DebianVersionComparer.Instance.Compare(x, y);
|
||||
return result1 == result2;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests.Properties;
|
||||
|
||||
public partial class VersionComparisonPropertyTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Generator for valid RPM version strings.
|
||||
/// </summary>
|
||||
private static Arbitrary<string> RpmVersionArb()
|
||||
{
|
||||
var epochGen = Gen.Frequency(
|
||||
(3, Gen.Constant<string>("")),
|
||||
(1, Gen.Choose(0, 5).Select(e => $"{e}:")));
|
||||
|
||||
var versionGen = Gen.Elements(
|
||||
"1.0", "1.1", "1.2", "2.0", "2.1",
|
||||
"1.0.0", "1.0.1", "1.1.0", "2.0.0",
|
||||
"1.9", "1.10", "1.99", "1.100",
|
||||
"1.0~alpha", "1.0~beta", "1.0~rc1",
|
||||
"10.20.30", "0.1", "0.0.1");
|
||||
|
||||
var releaseGen = Gen.Elements(
|
||||
"1", "2", "3",
|
||||
"1.el8", "1.el8_5", "1.el9",
|
||||
"1.fc38", "1.fc39",
|
||||
"1ubuntu1", "1build1");
|
||||
|
||||
return (from epoch in epochGen
|
||||
from version in versionGen
|
||||
from release in releaseGen
|
||||
select $"{epoch}{version}-{release}").ToArbitrary();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generator for valid Debian version strings.
|
||||
/// </summary>
|
||||
private static Arbitrary<string> DebianVersionArb()
|
||||
{
|
||||
var epochGen = Gen.Frequency(
|
||||
(3, Gen.Constant<string>("")),
|
||||
(1, Gen.Choose(0, 5).Select(e => $"{e}:")));
|
||||
|
||||
var versionGen = Gen.Elements(
|
||||
"1.0", "1.1", "1.2", "2.0", "2.1",
|
||||
"1.0.0", "1.0.1", "1.1.0", "2.0.0",
|
||||
"1.9", "1.10", "1.99", "1.100",
|
||||
"1.0~alpha", "1.0~beta", "1.0~rc1",
|
||||
"10.20.30", "0.1", "0.0.1",
|
||||
"1.0+dfsg", "1.0+really1.1");
|
||||
|
||||
var revisionGen = Gen.Elements(
|
||||
"1", "2", "3",
|
||||
"1ubuntu1", "1ubuntu2",
|
||||
"1build1", "1build2",
|
||||
"1+deb11u1", "1+deb12u1");
|
||||
|
||||
return (from epoch in epochGen
|
||||
from version in versionGen
|
||||
from revision in revisionGen
|
||||
select $"{epoch}{version}-{revision}").ToArbitrary();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using StellaOps.VersionComparison.Comparers;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests.Properties;
|
||||
|
||||
public partial class VersionComparisonPropertyTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Leading zeros in numeric segments are ignored for RPM.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property RpmComparer_LeadingZerosIgnored()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Choose(1, 99).ToArbitrary(),
|
||||
Gen.Choose(1, 99).ToArbitrary(),
|
||||
(major, minor) =>
|
||||
{
|
||||
var withLeading = $"0{major}.0{minor}-1";
|
||||
var withoutLeading = $"{major}.{minor}-1";
|
||||
return RpmVersionComparer.Instance.Compare(withLeading, withoutLeading) == 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Leading zeros in numeric segments are ignored for Debian.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property DebianComparer_LeadingZerosIgnored()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Choose(1, 99).ToArbitrary(),
|
||||
Gen.Choose(1, 99).ToArbitrary(),
|
||||
(major, minor) =>
|
||||
{
|
||||
var withLeading = $"0{major}.0{minor}-1";
|
||||
var withoutLeading = $"{major}.{minor}-1";
|
||||
return DebianVersionComparer.Instance.Compare(withLeading, withoutLeading) == 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Numeric segments sort numerically, not lexicographically (9 < 10).
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property RpmComparer_NumericSegmentsNotLexicographic()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Choose(1, 9).ToArbitrary(),
|
||||
single =>
|
||||
{
|
||||
var singleDigit = $"1.{single}-1";
|
||||
var doubleDigit = $"1.{single + 10}-1";
|
||||
return RpmVersionComparer.Instance.Compare(singleDigit, doubleDigit) < 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Numeric segments sort numerically for Debian (9 < 10).
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property DebianComparer_NumericSegmentsNotLexicographic()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Choose(1, 9).ToArbitrary(),
|
||||
single =>
|
||||
{
|
||||
var singleDigit = $"1.{single}-1";
|
||||
var doubleDigit = $"1.{single + 10}-1";
|
||||
return DebianVersionComparer.Instance.Compare(singleDigit, doubleDigit) < 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using StellaOps.VersionComparison.Comparers;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests.Properties;
|
||||
|
||||
public partial class VersionComparisonPropertyTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Monotonicity: Incrementing epoch always results in newer version.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_EpochMonotonicity()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Choose(0, 10).ToArbitrary(),
|
||||
Gen.Elements("1.0", "2.5", "1.0.1", "10.20.30").ToArbitrary(),
|
||||
Gen.Elements("1", "2.el8", "3.fc38").ToArbitrary(),
|
||||
(epoch, version, release) =>
|
||||
{
|
||||
var lower = $"{epoch}:{version}-{release}";
|
||||
var higher = $"{epoch + 1}:{version}-{release}";
|
||||
return RpmVersionComparer.Instance.Compare(lower, higher) < 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Monotonicity: Major version increments are always newer.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_MajorVersionMonotonicity()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Choose(1, 100).ToArbitrary(),
|
||||
Gen.Choose(0, 99).ToArbitrary(),
|
||||
Gen.Choose(0, 99).ToArbitrary(),
|
||||
(major, minor, patch) =>
|
||||
{
|
||||
var lower = $"{major}.{minor}.{patch}-1";
|
||||
var higher = $"{major + 1}.{minor}.{patch}-1";
|
||||
return RpmVersionComparer.Instance.Compare(lower, higher) < 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Boundary: Tilde pre-release always sorts before release.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_TildePreReleaseBehavior()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Elements("1.0", "2.0", "3.5.1").ToArbitrary(),
|
||||
Gen.Elements("alpha", "beta", "rc1", "rc2").ToArbitrary(),
|
||||
(version, prerelease) =>
|
||||
{
|
||||
var preReleaseVersion = $"{version}~{prerelease}-1";
|
||||
var releaseVersion = $"{version}-1";
|
||||
return RpmVersionComparer.Instance.Compare(preReleaseVersion, releaseVersion) < 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Proof lines are non-empty for valid comparisons.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_ProofLinesNonEmpty()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
RpmVersionArb(),
|
||||
RpmVersionArb(),
|
||||
(x, y) =>
|
||||
{
|
||||
var result = RpmVersionComparer.Instance.CompareWithProof(x, y);
|
||||
return result.ProofLines.Length > 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using StellaOps.VersionComparison.Comparers;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests.Properties;
|
||||
|
||||
public partial class VersionComparisonPropertyTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Reflexivity: Any version equals itself.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_Reflexivity()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
RpmVersionArb(),
|
||||
version => RpmVersionComparer.Instance.Compare(version, version) == 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Anti-symmetry: Compare(x, y) == -Compare(y, x).
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_AntiSymmetry()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
RpmVersionArb(),
|
||||
RpmVersionArb(),
|
||||
(x, y) =>
|
||||
{
|
||||
var cmpXY = Math.Sign(RpmVersionComparer.Instance.Compare(x, y));
|
||||
var cmpYX = Math.Sign(RpmVersionComparer.Instance.Compare(y, x));
|
||||
return cmpXY == -cmpYX;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transitivity: if x <= y and y <= z then x <= z.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_Transitivity()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
RpmVersionArb(),
|
||||
RpmVersionArb(),
|
||||
RpmVersionArb(),
|
||||
(x, y, z) =>
|
||||
{
|
||||
var comparer = RpmVersionComparer.Instance;
|
||||
var cmpXY = comparer.Compare(x, y);
|
||||
var cmpYZ = comparer.Compare(y, z);
|
||||
var cmpXZ = comparer.Compare(x, z);
|
||||
|
||||
if (cmpXY <= 0 && cmpYZ <= 0)
|
||||
{
|
||||
return cmpXZ <= 0;
|
||||
}
|
||||
|
||||
if (cmpXY >= 0 && cmpYZ >= 0)
|
||||
{
|
||||
return cmpXZ >= 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consistency: Same input produces same result (determinism).
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_IsDeterministic()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
RpmVersionArb(),
|
||||
RpmVersionArb(),
|
||||
(x, y) =>
|
||||
{
|
||||
var result1 = RpmVersionComparer.Instance.Compare(x, y);
|
||||
var result2 = RpmVersionComparer.Instance.Compare(x, y);
|
||||
return result1 == result2;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,8 @@
|
||||
// Description: Property-based tests for version comparers verifying
|
||||
// monotonicity, transitivity, reflexivity, and boundary behavior.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using StellaOps.VersionComparison.Comparers;
|
||||
using StellaOps.VersionComparison.Models;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
using FluentAssertions;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests.Properties;
|
||||
|
||||
@@ -21,583 +15,10 @@ namespace StellaOps.VersionComparison.Tests.Properties;
|
||||
/// Verifies mathematical properties required for correct sorting:
|
||||
/// - Reflexivity: Compare(x, x) == 0
|
||||
/// - Anti-symmetry: Compare(x, y) == -Compare(y, x)
|
||||
/// - Transitivity: if Compare(x, y) <= 0 and Compare(y, z) <= 0 then Compare(x, z) <= 0
|
||||
/// - Transitivity: if Compare(x, y) <= 0 and Compare(y, z) <= 0 then Compare(x, z) <= 0
|
||||
/// </summary>
|
||||
[Trait("Category", "Property")]
|
||||
public class VersionComparisonPropertyTests
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public partial class VersionComparisonPropertyTests
|
||||
{
|
||||
#region RPM Version Property Tests
|
||||
|
||||
/// <summary>
|
||||
/// Reflexivity: Any version equals itself.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_Reflexivity()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
RpmVersionArb(),
|
||||
version => RpmVersionComparer.Instance.Compare(version, version) == 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Anti-symmetry: Compare(x, y) == -Compare(y, x).
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_AntiSymmetry()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
RpmVersionArb(),
|
||||
RpmVersionArb(),
|
||||
(x, y) =>
|
||||
{
|
||||
var cmpXY = Math.Sign(RpmVersionComparer.Instance.Compare(x, y));
|
||||
var cmpYX = Math.Sign(RpmVersionComparer.Instance.Compare(y, x));
|
||||
return cmpXY == -cmpYX;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transitivity: if x <= y and y <= z then x <= z.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_Transitivity()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
RpmVersionArb(),
|
||||
RpmVersionArb(),
|
||||
RpmVersionArb(),
|
||||
(x, y, z) =>
|
||||
{
|
||||
var comparer = RpmVersionComparer.Instance;
|
||||
var cmpXY = comparer.Compare(x, y);
|
||||
var cmpYZ = comparer.Compare(y, z);
|
||||
var cmpXZ = comparer.Compare(x, z);
|
||||
|
||||
// If x <= y and y <= z, then x <= z
|
||||
if (cmpXY <= 0 && cmpYZ <= 0)
|
||||
{
|
||||
return cmpXZ <= 0;
|
||||
}
|
||||
|
||||
// If x >= y and y >= z, then x >= z
|
||||
if (cmpXY >= 0 && cmpYZ >= 0)
|
||||
{
|
||||
return cmpXZ >= 0;
|
||||
}
|
||||
|
||||
return true; // No constraint for mixed cases
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Monotonicity: Incrementing epoch always results in newer version.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_EpochMonotonicity()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Choose(0, 10).ToArbitrary(),
|
||||
Gen.Elements("1.0", "2.5", "1.0.1", "10.20.30").ToArbitrary(),
|
||||
Gen.Elements("1", "2.el8", "3.fc38").ToArbitrary(),
|
||||
(epoch, version, release) =>
|
||||
{
|
||||
var lower = $"{epoch}:{version}-{release}";
|
||||
var higher = $"{epoch + 1}:{version}-{release}";
|
||||
return RpmVersionComparer.Instance.Compare(lower, higher) < 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Monotonicity: Major version increments are always newer.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_MajorVersionMonotonicity()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Choose(1, 100).ToArbitrary(),
|
||||
Gen.Choose(0, 99).ToArbitrary(),
|
||||
Gen.Choose(0, 99).ToArbitrary(),
|
||||
(major, minor, patch) =>
|
||||
{
|
||||
var lower = $"{major}.{minor}.{patch}-1";
|
||||
var higher = $"{major + 1}.{minor}.{patch}-1";
|
||||
return RpmVersionComparer.Instance.Compare(lower, higher) < 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Boundary: Tilde pre-release always sorts before release.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_TildePreReleaseBehavior()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Elements("1.0", "2.0", "3.5.1").ToArbitrary(),
|
||||
Gen.Elements("alpha", "beta", "rc1", "rc2").ToArbitrary(),
|
||||
(version, prerelease) =>
|
||||
{
|
||||
var preReleaseVersion = $"{version}~{prerelease}-1";
|
||||
var releaseVersion = $"{version}-1";
|
||||
return RpmVersionComparer.Instance.Compare(preReleaseVersion, releaseVersion) < 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consistency: Same input produces same result (determinism).
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_IsDeterministic()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
RpmVersionArb(),
|
||||
RpmVersionArb(),
|
||||
(x, y) =>
|
||||
{
|
||||
var result1 = RpmVersionComparer.Instance.Compare(x, y);
|
||||
var result2 = RpmVersionComparer.Instance.Compare(x, y);
|
||||
return result1 == result2;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Proof lines are non-empty for valid comparisons.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_ProofLinesNonEmpty()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
RpmVersionArb(),
|
||||
RpmVersionArb(),
|
||||
(x, y) =>
|
||||
{
|
||||
var result = RpmVersionComparer.Instance.CompareWithProof(x, y);
|
||||
return result.ProofLines.Length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Debian Version Property Tests
|
||||
|
||||
/// <summary>
|
||||
/// Reflexivity: Any version equals itself.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_Reflexivity()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
DebianVersionArb(),
|
||||
version => DebianVersionComparer.Instance.Compare(version, version) == 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Anti-symmetry: Compare(x, y) == -Compare(y, x).
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_AntiSymmetry()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
DebianVersionArb(),
|
||||
DebianVersionArb(),
|
||||
(x, y) =>
|
||||
{
|
||||
var cmpXY = Math.Sign(DebianVersionComparer.Instance.Compare(x, y));
|
||||
var cmpYX = Math.Sign(DebianVersionComparer.Instance.Compare(y, x));
|
||||
return cmpXY == -cmpYX;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transitivity: if x <= y and y <= z then x <= z.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_Transitivity()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
DebianVersionArb(),
|
||||
DebianVersionArb(),
|
||||
DebianVersionArb(),
|
||||
(x, y, z) =>
|
||||
{
|
||||
var comparer = DebianVersionComparer.Instance;
|
||||
var cmpXY = comparer.Compare(x, y);
|
||||
var cmpYZ = comparer.Compare(y, z);
|
||||
var cmpXZ = comparer.Compare(x, z);
|
||||
|
||||
if (cmpXY <= 0 && cmpYZ <= 0)
|
||||
{
|
||||
return cmpXZ <= 0;
|
||||
}
|
||||
|
||||
if (cmpXY >= 0 && cmpYZ >= 0)
|
||||
{
|
||||
return cmpXZ >= 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Monotonicity: Incrementing epoch always results in newer version.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_EpochMonotonicity()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Choose(0, 10).ToArbitrary(),
|
||||
Gen.Elements("1.0", "2.5", "1.0.1", "10.20.30").ToArbitrary(),
|
||||
Gen.Elements("1", "1ubuntu1", "2build1").ToArbitrary(),
|
||||
(epoch, version, revision) =>
|
||||
{
|
||||
var lower = $"{epoch}:{version}-{revision}";
|
||||
var higher = $"{epoch + 1}:{version}-{revision}";
|
||||
return DebianVersionComparer.Instance.Compare(lower, higher) < 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Boundary: Tilde pre-release always sorts before release.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_TildePreReleaseBehavior()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Elements("1.0", "2.0", "3.5.1").ToArbitrary(),
|
||||
Gen.Elements("alpha", "beta", "rc1", "rc2").ToArbitrary(),
|
||||
(version, prerelease) =>
|
||||
{
|
||||
var preReleaseVersion = $"{version}~{prerelease}-1";
|
||||
var releaseVersion = $"{version}-1";
|
||||
return DebianVersionComparer.Instance.Compare(preReleaseVersion, releaseVersion) < 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consistency: Same input produces same result (determinism).
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_IsDeterministic()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
DebianVersionArb(),
|
||||
DebianVersionArb(),
|
||||
(x, y) =>
|
||||
{
|
||||
var result1 = DebianVersionComparer.Instance.Compare(x, y);
|
||||
var result2 = DebianVersionComparer.Instance.Compare(x, y);
|
||||
return result1 == result2;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Proof lines are non-empty for valid comparisons.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_ProofLinesNonEmpty()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
DebianVersionArb(),
|
||||
DebianVersionArb(),
|
||||
(x, y) =>
|
||||
{
|
||||
var result = DebianVersionComparer.Instance.CompareWithProof(x, y);
|
||||
return result.ProofLines.Length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Boundary Behavior Tests
|
||||
|
||||
/// <summary>
|
||||
/// Null handling: null is less than any valid version.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_NullIsLessThanAnyVersion()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
RpmVersionArb(),
|
||||
version => ((IComparer<string>)RpmVersionComparer.Instance).Compare(null, version) < 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Null handling: any valid version is greater than null.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property RpmComparer_AnyVersionGreaterThanNull()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
RpmVersionArb(),
|
||||
version => ((IComparer<string>)RpmVersionComparer.Instance).Compare(version, null) > 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Null handling: null equals null.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RpmComparer_NullEqualsNull()
|
||||
{
|
||||
((IComparer<string>)RpmVersionComparer.Instance).Compare(null, null).Should().Be(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Null handling: null is less than any valid Debian version.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_NullIsLessThanAnyVersion()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
DebianVersionArb(),
|
||||
version => ((IComparer<string>)DebianVersionComparer.Instance).Compare(null, version) < 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Null handling: any valid Debian version is greater than null.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 100)]
|
||||
public Property DebianComparer_AnyVersionGreaterThanNull()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
DebianVersionArb(),
|
||||
version => ((IComparer<string>)DebianVersionComparer.Instance).Compare(version, null) > 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Null handling: null equals null for Debian.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DebianComparer_NullEqualsNull()
|
||||
{
|
||||
((IComparer<string>)DebianVersionComparer.Instance).Compare(null, null).Should().Be(0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Version Ordering Tests
|
||||
|
||||
/// <summary>
|
||||
/// Leading zeros in numeric segments are ignored for RPM.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property RpmComparer_LeadingZerosIgnored()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Choose(1, 99).ToArbitrary(),
|
||||
Gen.Choose(1, 99).ToArbitrary(),
|
||||
(major, minor) =>
|
||||
{
|
||||
var withLeading = $"0{major}.0{minor}-1";
|
||||
var withoutLeading = $"{major}.{minor}-1";
|
||||
return RpmVersionComparer.Instance.Compare(withLeading, withoutLeading) == 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Leading zeros in numeric segments are ignored for Debian.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property DebianComparer_LeadingZerosIgnored()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Choose(1, 99).ToArbitrary(),
|
||||
Gen.Choose(1, 99).ToArbitrary(),
|
||||
(major, minor) =>
|
||||
{
|
||||
var withLeading = $"0{major}.0{minor}-1";
|
||||
var withoutLeading = $"{major}.{minor}-1";
|
||||
return DebianVersionComparer.Instance.Compare(withLeading, withoutLeading) == 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Numeric segments sort numerically, not lexicographically (9 < 10).
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property RpmComparer_NumericSegmentsNotLexicographic()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Choose(1, 9).ToArbitrary(),
|
||||
(single) =>
|
||||
{
|
||||
var singleDigit = $"1.{single}-1";
|
||||
var doubleDigit = $"1.{single + 10}-1";
|
||||
return RpmVersionComparer.Instance.Compare(singleDigit, doubleDigit) < 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Numeric segments sort numerically for Debian (9 < 10).
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property DebianComparer_NumericSegmentsNotLexicographic()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Choose(1, 9).ToArbitrary(),
|
||||
(single) =>
|
||||
{
|
||||
var singleDigit = $"1.{single}-1";
|
||||
var doubleDigit = $"1.{single + 10}-1";
|
||||
return DebianVersionComparer.Instance.Compare(singleDigit, doubleDigit) < 0;
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Generators
|
||||
|
||||
/// <summary>
|
||||
/// Generator for valid RPM version strings.
|
||||
/// </summary>
|
||||
private static Arbitrary<string> RpmVersionArb()
|
||||
{
|
||||
var epochGen = Gen.Frequency(
|
||||
(3, Gen.Constant<string>("")),
|
||||
(1, Gen.Choose(0, 5).Select(e => $"{e}:")));
|
||||
|
||||
var versionGen = Gen.Elements(
|
||||
"1.0", "1.1", "1.2", "2.0", "2.1",
|
||||
"1.0.0", "1.0.1", "1.1.0", "2.0.0",
|
||||
"1.9", "1.10", "1.99", "1.100",
|
||||
"1.0~alpha", "1.0~beta", "1.0~rc1",
|
||||
"10.20.30", "0.1", "0.0.1");
|
||||
|
||||
var releaseGen = Gen.Elements(
|
||||
"1", "2", "3",
|
||||
"1.el8", "1.el8_5", "1.el9",
|
||||
"1.fc38", "1.fc39",
|
||||
"1ubuntu1", "1build1");
|
||||
|
||||
return (from epoch in epochGen
|
||||
from version in versionGen
|
||||
from release in releaseGen
|
||||
select $"{epoch}{version}-{release}").ToArbitrary();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generator for valid Debian version strings.
|
||||
/// </summary>
|
||||
private static Arbitrary<string> DebianVersionArb()
|
||||
{
|
||||
var epochGen = Gen.Frequency(
|
||||
(3, Gen.Constant<string>("")),
|
||||
(1, Gen.Choose(0, 5).Select(e => $"{e}:")));
|
||||
|
||||
var versionGen = Gen.Elements(
|
||||
"1.0", "1.1", "1.2", "2.0", "2.1",
|
||||
"1.0.0", "1.0.1", "1.1.0", "2.0.0",
|
||||
"1.9", "1.10", "1.99", "1.100",
|
||||
"1.0~alpha", "1.0~beta", "1.0~rc1",
|
||||
"10.20.30", "0.1", "0.0.1",
|
||||
"1.0+dfsg", "1.0+really1.1");
|
||||
|
||||
var revisionGen = Gen.Elements(
|
||||
"1", "2", "3",
|
||||
"1ubuntu1", "1ubuntu2",
|
||||
"1build1", "1build2",
|
||||
"1+deb11u1", "1+deb12u1");
|
||||
|
||||
return (from epoch in epochGen
|
||||
from version in versionGen
|
||||
from revision in revisionGen
|
||||
select $"{epoch}{version}-{revision}").ToArbitrary();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Property tests for version range matching and resolution.
|
||||
/// Verifies range containment, boundary conditions, and semantic consistency.
|
||||
/// </summary>
|
||||
[Trait("Category", "Property")]
|
||||
public class VersionRangePropertyTests
|
||||
{
|
||||
/// <summary>
|
||||
/// For any version v, range "[v,v]" contains exactly v.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property ExactVersionRange_ContainsOnlyExactVersion()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Elements("1.0-1", "2.0-1", "1.0.1-1", "3.0~rc1-1").ToArbitrary(),
|
||||
(string version) =>
|
||||
{
|
||||
// Exact range should match
|
||||
var comparer = RpmVersionComparer.Instance;
|
||||
return ((IComparer<string>)comparer).Compare(version, version) == 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For versions a < b, range [a, b] contains both endpoints.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property ClosedRange_ContainsBothEndpoints()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Elements(
|
||||
("1.0-1", "2.0-1"),
|
||||
("1.0-1", "1.1-1"),
|
||||
("1.0~alpha-1", "1.0-1"),
|
||||
("0:1.0-1", "1:1.0-1")).ToArbitrary(),
|
||||
((string lower, string upper) range) =>
|
||||
{
|
||||
var (lower, upper) = range;
|
||||
var comparer = (IComparer<string>)RpmVersionComparer.Instance;
|
||||
|
||||
// Lower is contained: lower >= lower && lower <= upper
|
||||
var lowerContained = comparer.Compare(lower, lower) >= 0 &&
|
||||
comparer.Compare(lower, upper) <= 0;
|
||||
|
||||
// Upper is contained: upper >= lower && upper <= upper
|
||||
var upperContained = comparer.Compare(upper, lower) >= 0 &&
|
||||
comparer.Compare(upper, upper) <= 0;
|
||||
|
||||
return lowerContained && upperContained;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For versions a < b, range (a, b) excludes both endpoints.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property OpenRange_ExcludesBothEndpoints()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Elements(
|
||||
("1.0-1", "2.0-1"),
|
||||
("1.0-1", "1.1-1"),
|
||||
("0:1.0-1", "1:1.0-1")).ToArbitrary(),
|
||||
((string lower, string upper) range) =>
|
||||
{
|
||||
var (lower, upper) = range;
|
||||
var comparer = (IComparer<string>)RpmVersionComparer.Instance;
|
||||
|
||||
// In open range (a, b): version must be strictly > a and strictly < b
|
||||
// Since we're testing with just endpoints, they should NOT be in the open range
|
||||
var lowerInOpen = comparer.Compare(lower, lower) > 0; // false
|
||||
var upperInOpen = comparer.Compare(upper, upper) < 0; // false
|
||||
|
||||
return !lowerInOpen && !upperInOpen;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Version ordering is consistent with range containment.
|
||||
/// If a is in range [lower, upper] and b > upper, then a < b.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property RangeContainment_ConsistentWithOrdering()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Elements("1.0-1", "1.5-1").ToArbitrary(),
|
||||
Gen.Elements("2.0-1", "3.0-1").ToArbitrary(),
|
||||
(string inRange, string outOfRange) =>
|
||||
{
|
||||
var comparer = (IComparer<string>)RpmVersionComparer.Instance;
|
||||
// If inRange < outOfRange, this should hold
|
||||
return comparer.Compare(inRange, outOfRange) < 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
using FsCheck;
|
||||
using FsCheck.Fluent;
|
||||
using FsCheck.Xunit;
|
||||
using StellaOps.TestKit;
|
||||
using StellaOps.VersionComparison.Comparers;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests.Properties;
|
||||
|
||||
/// <summary>
|
||||
/// Property tests for version range matching and resolution.
|
||||
/// Verifies range containment, boundary conditions, and semantic consistency.
|
||||
/// </summary>
|
||||
[Trait("Category", "Property")]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public class VersionRangePropertyTests
|
||||
{
|
||||
/// <summary>
|
||||
/// For any version v, range "[v,v]" contains exactly v.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property ExactVersionRange_ContainsOnlyExactVersion()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Elements("1.0-1", "2.0-1", "1.0.1-1", "3.0~rc1-1").ToArbitrary(),
|
||||
(string version) =>
|
||||
{
|
||||
var comparer = RpmVersionComparer.Instance;
|
||||
return ((IComparer<string>)comparer).Compare(version, version) == 0;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For versions a < b, range [a, b] contains both endpoints.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property ClosedRange_ContainsBothEndpoints()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Elements(
|
||||
("1.0-1", "2.0-1"),
|
||||
("1.0-1", "1.1-1"),
|
||||
("1.0~alpha-1", "1.0-1"),
|
||||
("0:1.0-1", "1:1.0-1")).ToArbitrary(),
|
||||
((string lower, string upper) range) =>
|
||||
{
|
||||
var (lower, upper) = range;
|
||||
var comparer = (IComparer<string>)RpmVersionComparer.Instance;
|
||||
|
||||
var lowerContained = comparer.Compare(lower, lower) >= 0 &&
|
||||
comparer.Compare(lower, upper) <= 0;
|
||||
|
||||
var upperContained = comparer.Compare(upper, lower) >= 0 &&
|
||||
comparer.Compare(upper, upper) <= 0;
|
||||
|
||||
return lowerContained && upperContained;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For versions a < b, range (a, b) excludes both endpoints.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property OpenRange_ExcludesBothEndpoints()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Elements(
|
||||
("1.0-1", "2.0-1"),
|
||||
("1.0-1", "1.1-1"),
|
||||
("0:1.0-1", "1:1.0-1")).ToArbitrary(),
|
||||
((string lower, string upper) range) =>
|
||||
{
|
||||
var (lower, upper) = range;
|
||||
var comparer = (IComparer<string>)RpmVersionComparer.Instance;
|
||||
|
||||
var lowerInOpen = comparer.Compare(lower, lower) > 0;
|
||||
var upperInOpen = comparer.Compare(upper, upper) < 0;
|
||||
|
||||
return !lowerInOpen && !upperInOpen;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Version ordering is consistent with range containment.
|
||||
/// If a is in range [lower, upper] and b > upper, then a < b.
|
||||
/// </summary>
|
||||
[Property(MaxTest = 50)]
|
||||
public Property RangeContainment_ConsistentWithOrdering()
|
||||
{
|
||||
return Prop.ForAll(
|
||||
Gen.Elements("1.0-1", "1.5-1").ToArbitrary(),
|
||||
Gen.Elements("2.0-1", "3.0-1").ToArbitrary(),
|
||||
(string inRange, string outOfRange) =>
|
||||
{
|
||||
var comparer = (IComparer<string>)RpmVersionComparer.Instance;
|
||||
return comparer.Compare(inRange, outOfRange) < 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests;
|
||||
|
||||
public partial class RpmVersionComparerTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(RpmComparisonCases))]
|
||||
public void Compare_RpmVersions_ReturnsExpectedOrder(string left, string right, int expected)
|
||||
{
|
||||
var result = Math.Sign(_comparer.Compare(left, right));
|
||||
result.Should().Be(expected, because: $"comparing '{left}' with '{right}'");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_SameVersion_ReturnsZero()
|
||||
{
|
||||
_comparer.Compare("1.0-1.el8", "1.0-1.el8").Should().Be(0);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_NullLeft_ReturnsNegative()
|
||||
{
|
||||
_comparer.Compare(null, "1.0-1").Should().BeNegative();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_NullRight_ReturnsPositive()
|
||||
{
|
||||
_comparer.Compare("1.0-1", null).Should().BePositive();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests;
|
||||
|
||||
public partial class RpmVersionComparerTests
|
||||
{
|
||||
public static TheoryData<string, string, int> RpmComparisonCases => new()
|
||||
{
|
||||
// Epoch precedence
|
||||
{ "0:1.0-1", "1:0.1-1", -1 },
|
||||
{ "1:1.0-1", "0:9.9-9", 1 },
|
||||
{ "1.0-1", "0:1.0-1", 0 }, // Missing epoch = 0
|
||||
{ "2:1.0-1", "1:9.9-9", 1 },
|
||||
|
||||
// Numeric version ordering
|
||||
{ "1.9-1", "1.10-1", -1 },
|
||||
{ "1.02-1", "1.2-1", 0 }, // Leading zeros ignored
|
||||
{ "1.0-1", "1.0.1-1", -1 },
|
||||
{ "2.0-1", "1.999-1", 1 },
|
||||
|
||||
// Tilde pre-releases
|
||||
{ "1.0~rc1-1", "1.0-1", -1 }, // Tilde sorts before release
|
||||
{ "1.0~alpha-1", "1.0~beta-1", -1 },
|
||||
{ "1.0~-1", "1.0-1", -1 },
|
||||
{ "1.0~~-1", "1.0~-1", -1 }, // Double tilde < single
|
||||
|
||||
// Release qualifiers (RHEL backports)
|
||||
{ "1.0-1.el8", "1.0-1.el8_5", -1 }, // Base < security update
|
||||
{ "1.0-1.el8_5", "1.0-1.el8_5.1", -1 },
|
||||
{ "1.0-1.el8", "1.0-1.el9", -1 }, // el8 < el9
|
||||
{ "1.0-1.el9_2", "1.0-1.el9_3", -1 },
|
||||
|
||||
// Alpha suffix ordering (1.0a > 1.0 because "a" is additional segment)
|
||||
{ "1.0a-1", "1.0-1", 1 }, // 1.0a > 1.0 (alpha suffix adds to version)
|
||||
{ "1.0-1", "1.0a-1", -1 },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests;
|
||||
|
||||
public partial class RpmVersionComparerTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("1:1.2-1", "0:9.9-9", 1)] // Epoch jump
|
||||
[InlineData("2.0~rc1", "2.0", -1)] // Tilde pre-release
|
||||
[InlineData("1.2-3.el9_2", "1.2-3.el9_3", -1)] // Release qualifier
|
||||
[InlineData("1.2-3ubuntu0.1", "1.2-3", 1)] // Ubuntu rebuild
|
||||
public void Compare_EdgeCases_ReturnsExpectedOrder(string left, string right, int expected)
|
||||
{
|
||||
var result = Math.Sign(_comparer.Compare(left, right));
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.TestKit;
|
||||
using StellaOps.VersionComparison;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.VersionComparison.Tests;
|
||||
|
||||
public partial class RpmVersionComparerTests
|
||||
{
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_EpochDifference_ReturnsEpochProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("0:1.0-1", "1:0.1-1");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.Comparator.Should().Be(ComparatorType.RpmEvr);
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Epoch:") && line.Contains("left is older"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_SameEpochDifferentVersion_ReturnsVersionProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("1:1.0-1", "1:2.0-1");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Epoch:") && line.Contains("equal"));
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Version:") && line.Contains("left is older"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_SameVersionDifferentRelease_ReturnsReleaseProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("1.0-1.el8", "1.0-1.el8_5");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Release:") && line.Contains("left is older"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_EqualVersions_ReturnsEqualProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("1.0-1.el8", "1.0-1.el8");
|
||||
|
||||
result.Comparison.Should().Be(0);
|
||||
result.IsEqual.Should().BeTrue();
|
||||
result.ProofLines.Should().AllSatisfy(line => line.Should().Contain("equal"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_TildePreRelease_ReturnsCorrectProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("1.0~rc1-1", "1.0-1");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Version:") && line.Contains("left is older"));
|
||||
}
|
||||
}
|
||||
@@ -1,149 +1,8 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.VersionComparison;
|
||||
using StellaOps.VersionComparison.Comparers;
|
||||
|
||||
using StellaOps.TestKit;
|
||||
namespace StellaOps.VersionComparison.Tests;
|
||||
|
||||
public class RpmVersionComparerTests
|
||||
public partial class RpmVersionComparerTests
|
||||
{
|
||||
private readonly RpmVersionComparer _comparer = RpmVersionComparer.Instance;
|
||||
|
||||
#region Basic Comparison Tests
|
||||
|
||||
public static TheoryData<string, string, int> RpmComparisonCases => new()
|
||||
{
|
||||
// Epoch precedence
|
||||
{ "0:1.0-1", "1:0.1-1", -1 },
|
||||
{ "1:1.0-1", "0:9.9-9", 1 },
|
||||
{ "1.0-1", "0:1.0-1", 0 }, // Missing epoch = 0
|
||||
{ "2:1.0-1", "1:9.9-9", 1 },
|
||||
|
||||
// Numeric version ordering
|
||||
{ "1.9-1", "1.10-1", -1 },
|
||||
{ "1.02-1", "1.2-1", 0 }, // Leading zeros ignored
|
||||
{ "1.0-1", "1.0.1-1", -1 },
|
||||
{ "2.0-1", "1.999-1", 1 },
|
||||
|
||||
// Tilde pre-releases
|
||||
{ "1.0~rc1-1", "1.0-1", -1 }, // Tilde sorts before release
|
||||
{ "1.0~alpha-1", "1.0~beta-1", -1 },
|
||||
{ "1.0~-1", "1.0-1", -1 },
|
||||
{ "1.0~~-1", "1.0~-1", -1 }, // Double tilde < single
|
||||
|
||||
// Release qualifiers (RHEL backports)
|
||||
{ "1.0-1.el8", "1.0-1.el8_5", -1 }, // Base < security update
|
||||
{ "1.0-1.el8_5", "1.0-1.el8_5.1", -1 },
|
||||
{ "1.0-1.el8", "1.0-1.el9", -1 }, // el8 < el9
|
||||
{ "1.0-1.el9_2", "1.0-1.el9_3", -1 },
|
||||
|
||||
// Alpha suffix ordering (1.0a > 1.0 because 'a' is additional segment)
|
||||
{ "1.0a-1", "1.0-1", 1 }, // 1.0a > 1.0 (alpha suffix adds to version)
|
||||
{ "1.0-1", "1.0a-1", -1 },
|
||||
};
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[MemberData(nameof(RpmComparisonCases))]
|
||||
public void Compare_RpmVersions_ReturnsExpectedOrder(string left, string right, int expected)
|
||||
{
|
||||
var result = Math.Sign(_comparer.Compare(left, right));
|
||||
result.Should().Be(expected, because: $"comparing '{left}' with '{right}'");
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_SameVersion_ReturnsZero()
|
||||
{
|
||||
_comparer.Compare("1.0-1.el8", "1.0-1.el8").Should().Be(0);
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_NullLeft_ReturnsNegative()
|
||||
{
|
||||
_comparer.Compare(null, "1.0-1").Should().BeNegative();
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void Compare_NullRight_ReturnsPositive()
|
||||
{
|
||||
_comparer.Compare("1.0-1", null).Should().BePositive();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Proof Line Tests
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_EpochDifference_ReturnsEpochProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("0:1.0-1", "1:0.1-1");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.Comparator.Should().Be(ComparatorType.RpmEvr);
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Epoch:") && line.Contains("left is older"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_SameEpochDifferentVersion_ReturnsVersionProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("1:1.0-1", "1:2.0-1");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Epoch:") && line.Contains("equal"));
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Version:") && line.Contains("left is older"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_SameVersionDifferentRelease_ReturnsReleaseProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("1.0-1.el8", "1.0-1.el8_5");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Release:") && line.Contains("left is older"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_EqualVersions_ReturnsEqualProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("1.0-1.el8", "1.0-1.el8");
|
||||
|
||||
result.Comparison.Should().Be(0);
|
||||
result.IsEqual.Should().BeTrue();
|
||||
result.ProofLines.Should().AllSatisfy(line => line.Should().Contain("equal"));
|
||||
}
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Fact]
|
||||
public void CompareWithProof_TildePreRelease_ReturnsCorrectProof()
|
||||
{
|
||||
var result = _comparer.CompareWithProof("1.0~rc1-1", "1.0-1");
|
||||
|
||||
result.Comparison.Should().BeNegative();
|
||||
result.ProofLines.Should().Contain(line => line.Contains("Version:") && line.Contains("left is older"));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Cases
|
||||
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
[Theory]
|
||||
[InlineData("1:1.2-1", "0:9.9-9", 1)] // Epoch jump
|
||||
[InlineData("2.0~rc1", "2.0", -1)] // Tilde pre-release
|
||||
[InlineData("1.2-3.el9_2", "1.2-3.el9_3", -1)] // Release qualifier
|
||||
[InlineData("1.2-3ubuntu0.1", "1.2-3", 1)] // Ubuntu rebuild
|
||||
public void Compare_EdgeCases_ReturnsExpectedOrder(string left, string right, int expected)
|
||||
{
|
||||
var result = Math.Sign(_comparer.Compare(left, right));
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user