Add determinism tests for verdict artifact generation and update SHA256 sums script

- Implemented comprehensive tests for verdict artifact generation to ensure deterministic outputs across various scenarios, including identical inputs, parallel execution, and change ordering.
- Created helper methods for generating sample verdict inputs and computing canonical hashes.
- Added tests to validate the stability of canonical hashes, proof spine ordering, and summary statistics.
- Introduced a new PowerShell script to update SHA256 sums for files, ensuring accurate hash generation and file integrity checks.
This commit is contained in:
StellaOps Bot
2025-12-24 02:17:34 +02:00
parent e59921374e
commit 7503c19b8f
390 changed files with 37389 additions and 5380 deletions

View File

@@ -0,0 +1,602 @@
// -----------------------------------------------------------------------------
// VersionComparisonPropertyTests.cs
// Sprint: SPRINT_5100_0009_0001 (Scanner Tests)
// Task: SCANNER-5100-001 - Property tests for version/range resolution
// Description: Property-based tests for version comparers verifying
// monotonicity, transitivity, reflexivity, and boundary behavior.
// -----------------------------------------------------------------------------
using FsCheck;
using FsCheck.Xunit;
using StellaOps.VersionComparison.Comparers;
using StellaOps.VersionComparison.Models;
using Xunit;
using FluentAssertions;
namespace StellaOps.VersionComparison.Tests.Properties;
/// <summary>
/// Property-based tests for version comparers.
/// 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
/// </summary>
[Trait("Category", "Property")]
public 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(
Tuple.Create(3, Gen.Constant("")),
Tuple.Create(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(
Tuple.Create(3, Gen.Constant("")),
Tuple.Create(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

@@ -10,6 +10,8 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="FsCheck" Version="2.16.6" />
<PackageReference Include="FsCheck.Xunit" Version="2.16.6" />
</ItemGroup>
<ItemGroup>