stabilizaiton work - projects rework for maintenanceability and ui livening

This commit is contained in:
master
2026-02-03 23:40:04 +02:00
parent 074ce117ba
commit 557feefdc3
3305 changed files with 186813 additions and 107843 deletions

View File

@@ -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");
}
}

View File

@@ -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();
}
}

View File

@@ -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 },
};
}

View File

@@ -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);
}
}

View File

@@ -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:"));
}
}

View File

@@ -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
}

View File

@@ -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);
}
}

View File

@@ -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;
});
}
}

View File

@@ -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;
});
}
}

View File

@@ -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();
}
}

View File

@@ -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;
});
}
}

View File

@@ -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;
});
}
}

View File

@@ -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;
});
}
}

View File

@@ -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) &lt;= 0 and Compare(y, z) &lt;= 0 then Compare(x, z) &lt;= 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 &lt;= y and y &lt;= z then x &lt;= 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 &lt;= y and y &lt;= z then x &lt;= 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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;
});
}
}

View File

@@ -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;
});
}
}

View File

@@ -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();
}
}

View File

@@ -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 },
};
}

View File

@@ -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);
}
}

View File

@@ -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"));
}
}

View File

@@ -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
}