Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Distro.Alpine;
|
||||
using StellaOps.Concelier.Connector.Distro.Alpine.Configuration;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Concelier.Storage.Advisories;
|
||||
using StellaOps.Concelier.Testing;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.Alpine.Tests;
|
||||
|
||||
[Collection(ConcelierFixtureCollection.Name)]
|
||||
public sealed class AlpineConnectorTests
|
||||
{
|
||||
private static readonly Uri SecDbUri = new("https://secdb.alpinelinux.org/v3.20/main.json");
|
||||
private readonly ConcelierPostgresFixture _fixture;
|
||||
|
||||
public AlpineConnectorTests(ConcelierPostgresFixture fixture)
|
||||
{
|
||||
_fixture = fixture;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchParseMap_StoresAdvisoriesAndUpdatesCursor()
|
||||
{
|
||||
await using var harness = await BuildHarnessAsync();
|
||||
|
||||
harness.Handler.AddJsonResponse(SecDbUri, BuildMinimalSecDb());
|
||||
|
||||
var connector = harness.ServiceProvider.GetRequiredService<AlpineConnector>();
|
||||
await connector.FetchAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.ParseAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
await connector.MapAsync(harness.ServiceProvider, CancellationToken.None);
|
||||
|
||||
var advisoryStore = harness.ServiceProvider.GetRequiredService<IAdvisoryStore>();
|
||||
var advisories = await advisoryStore.GetRecentAsync(10, CancellationToken.None);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
|
||||
var advisory = advisories.Single(item => item.AdvisoryKey == "alpine/cve-2021-36159");
|
||||
var package = Assert.Single(advisory.AffectedPackages);
|
||||
Assert.Equal(AffectedPackageTypes.Apk, package.Type);
|
||||
Assert.Equal("apk-tools", package.Identifier);
|
||||
Assert.Equal("v3.20/main", package.Platform);
|
||||
|
||||
var range = Assert.Single(package.VersionRanges);
|
||||
Assert.Equal("apk", range.RangeKind);
|
||||
Assert.Equal("2.12.6-r0", range.FixedVersion);
|
||||
|
||||
var stateRepository = harness.ServiceProvider.GetRequiredService<ISourceStateRepository>();
|
||||
var state = await stateRepository.TryGetAsync(AlpineConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
Assert.True(state!.Cursor.TryGetValue("pendingDocuments", out var pendingDocs)
|
||||
&& pendingDocs.AsDocumentArray.Count == 0);
|
||||
Assert.True(state.Cursor.TryGetValue("pendingMappings", out var pendingMappings)
|
||||
&& pendingMappings.AsDocumentArray.Count == 0);
|
||||
|
||||
harness.Handler.AssertNoPendingResponses();
|
||||
}
|
||||
|
||||
private async Task<ConnectorTestHarness> BuildHarnessAsync()
|
||||
{
|
||||
var initialTime = new DateTimeOffset(2025, 12, 22, 0, 0, 0, TimeSpan.Zero);
|
||||
var harness = new ConnectorTestHarness(_fixture, initialTime, AlpineOptions.HttpClientName);
|
||||
await harness.EnsureServiceProviderAsync(services =>
|
||||
{
|
||||
services.AddAlpineConnector(options =>
|
||||
{
|
||||
options.BaseUri = new Uri("https://secdb.alpinelinux.org/");
|
||||
options.Releases = new[] { "v3.20" };
|
||||
options.Repositories = new[] { "main" };
|
||||
options.MaxDocumentsPerFetch = 1;
|
||||
options.FetchTimeout = TimeSpan.FromSeconds(5);
|
||||
options.RequestDelay = TimeSpan.Zero;
|
||||
options.UserAgent = "StellaOps.Tests.Alpine/1.0";
|
||||
});
|
||||
});
|
||||
|
||||
return harness;
|
||||
}
|
||||
|
||||
private static string BuildMinimalSecDb()
|
||||
=> "{\"distroversion\":\"v3.20\",\"reponame\":\"main\",\"urlprefix\":\"https://dl-cdn.alpinelinux.org/alpine\",\"packages\":[{\"pkg\":{\"name\":\"apk-tools\",\"secfixes\":{\"2.12.6-r0\":[\"CVE-2021-36159\"]}}},{\"pkg\":{\"name\":\"busybox\",\"secfixes\":{\"1.36.1-r29\":[\"CVE-2023-42364\"]}}}]}";
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.Core.Jobs;
|
||||
using StellaOps.Concelier.Connector.Common.Http;
|
||||
using StellaOps.Concelier.Connector.Distro.Alpine;
|
||||
using StellaOps.Concelier.Connector.Distro.Alpine.Configuration;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.Alpine.Tests;
|
||||
|
||||
public sealed class AlpineDependencyInjectionRoutineTests
|
||||
{
|
||||
[Fact]
|
||||
public void Register_ConfiguresOptionsAndScheduler()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddOptions();
|
||||
services.AddSourceCommon();
|
||||
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["concelier:sources:alpine:baseUri"] = "https://secdb.alpinelinux.org/",
|
||||
["concelier:sources:alpine:releases:0"] = "v3.20",
|
||||
["concelier:sources:alpine:repositories:0"] = "main",
|
||||
["concelier:sources:alpine:maxDocumentsPerFetch"] = "5",
|
||||
["concelier:sources:alpine:fetchTimeout"] = "00:00:30",
|
||||
["concelier:sources:alpine:requestDelay"] = "00:00:00.100",
|
||||
["concelier:sources:alpine:userAgent"] = "StellaOps.Tests.Alpine/1.0"
|
||||
})
|
||||
.Build();
|
||||
|
||||
var routine = new AlpineDependencyInjectionRoutine();
|
||||
routine.Register(services, configuration);
|
||||
services.Configure<JobSchedulerOptions>(_ => { });
|
||||
|
||||
using var provider = services.BuildServiceProvider(validateScopes: true);
|
||||
|
||||
var options = provider.GetRequiredService<IOptions<AlpineOptions>>().Value;
|
||||
Assert.Equal(new Uri("https://secdb.alpinelinux.org/"), options.BaseUri);
|
||||
Assert.Equal(new[] { "v3.20" }, options.Releases);
|
||||
Assert.Equal(new[] { "main" }, options.Repositories);
|
||||
Assert.Equal(5, options.MaxDocumentsPerFetch);
|
||||
Assert.Equal(TimeSpan.FromSeconds(30), options.FetchTimeout);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(100), options.RequestDelay);
|
||||
Assert.Equal("StellaOps.Tests.Alpine/1.0", options.UserAgent);
|
||||
|
||||
var schedulerOptions = provider.GetRequiredService<IOptions<JobSchedulerOptions>>().Value;
|
||||
Assert.True(schedulerOptions.Definitions.TryGetValue(AlpineJobKinds.Fetch, out var fetchDefinition));
|
||||
Assert.True(schedulerOptions.Definitions.TryGetValue(AlpineJobKinds.Parse, out var parseDefinition));
|
||||
Assert.True(schedulerOptions.Definitions.TryGetValue(AlpineJobKinds.Map, out var mapDefinition));
|
||||
|
||||
Assert.Equal(typeof(AlpineFetchJob), fetchDefinition.JobType);
|
||||
Assert.Equal(TimeSpan.FromMinutes(5), fetchDefinition.Timeout);
|
||||
Assert.Equal(TimeSpan.FromMinutes(4), fetchDefinition.LeaseDuration);
|
||||
Assert.Equal("*/30 * * * *", fetchDefinition.CronExpression);
|
||||
Assert.True(fetchDefinition.Enabled);
|
||||
|
||||
Assert.Equal(typeof(AlpineParseJob), parseDefinition.JobType);
|
||||
Assert.Equal(TimeSpan.FromMinutes(6), parseDefinition.Timeout);
|
||||
Assert.Equal(TimeSpan.FromMinutes(4), parseDefinition.LeaseDuration);
|
||||
Assert.Equal("7,37 * * * *", parseDefinition.CronExpression);
|
||||
Assert.True(parseDefinition.Enabled);
|
||||
|
||||
Assert.Equal(typeof(AlpineMapJob), mapDefinition.JobType);
|
||||
Assert.Equal(TimeSpan.FromMinutes(8), mapDefinition.Timeout);
|
||||
Assert.Equal(TimeSpan.FromMinutes(4), mapDefinition.LeaseDuration);
|
||||
Assert.Equal("12,42 * * * *", mapDefinition.CronExpression);
|
||||
Assert.True(mapDefinition.Enabled);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using StellaOps.Concelier.Connector.Distro.Alpine.Dto;
|
||||
using StellaOps.Concelier.Connector.Distro.Alpine.Internal;
|
||||
using StellaOps.Concelier.Merge.Comparers;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.Alpine.Tests;
|
||||
|
||||
internal static class AlpineFixtureReader
|
||||
{
|
||||
private static readonly StringComparer NameComparer = StringComparer.OrdinalIgnoreCase;
|
||||
|
||||
public static AlpineSecDbDto LoadDto(string filename)
|
||||
=> AlpineSecDbParser.Parse(ReadFixture(filename));
|
||||
|
||||
public static AlpineSecDbDto FilterPackages(
|
||||
AlpineSecDbDto dto,
|
||||
IReadOnlyCollection<string> packageNames,
|
||||
int maxVersionsPerPackage = 0)
|
||||
{
|
||||
if (packageNames is null || packageNames.Count == 0)
|
||||
{
|
||||
return dto;
|
||||
}
|
||||
|
||||
var allowed = new HashSet<string>(
|
||||
packageNames.Where(static name => !string.IsNullOrWhiteSpace(name))
|
||||
.Select(static name => name.Trim()),
|
||||
NameComparer);
|
||||
|
||||
var packages = dto.Packages
|
||||
.Where(pkg => allowed.Contains(pkg.Name))
|
||||
.Select(pkg => pkg with { Secfixes = TrimSecfixes(pkg.Secfixes, maxVersionsPerPackage) })
|
||||
.OrderBy(pkg => pkg.Name, NameComparer)
|
||||
.ToList();
|
||||
|
||||
return dto with { Packages = packages };
|
||||
}
|
||||
|
||||
public static string NormalizeSnapshot(string value)
|
||||
=> value.Replace("\r\n", "\n", StringComparison.Ordinal).TrimEnd();
|
||||
|
||||
public static string ReadFixture(string filename)
|
||||
{
|
||||
var path = ResolveFixturePath(filename);
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
|
||||
public static string GetWritableFixturePath(string filename)
|
||||
=> Path.Combine(GetProjectRoot(), "Source", "Distro", "Alpine", "Fixtures", filename);
|
||||
|
||||
private static string ResolveFixturePath(string filename)
|
||||
{
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(AppContext.BaseDirectory, "Source", "Distro", "Alpine", "Fixtures", filename),
|
||||
Path.Combine(AppContext.BaseDirectory, "Fixtures", filename),
|
||||
Path.Combine(GetProjectRoot(), "Source", "Distro", "Alpine", "Fixtures", filename),
|
||||
};
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
throw new FileNotFoundException($"Fixture '{filename}' not found.", filename);
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string[]> TrimSecfixes(
|
||||
IReadOnlyDictionary<string, string[]> secfixes,
|
||||
int maxVersions)
|
||||
{
|
||||
if (secfixes is null || secfixes.Count == 0)
|
||||
{
|
||||
return new Dictionary<string, string[]>(NameComparer);
|
||||
}
|
||||
|
||||
if (maxVersions <= 0 || secfixes.Count <= maxVersions)
|
||||
{
|
||||
return new Dictionary<string, string[]>(secfixes, NameComparer);
|
||||
}
|
||||
|
||||
var comparer = Comparer<string>.Create((left, right) => ApkVersionComparer.Instance.Compare(left, right));
|
||||
var orderedKeys = secfixes.Keys.OrderBy(static key => key, comparer).ToList();
|
||||
var skip = Math.Max(0, orderedKeys.Count - maxVersions);
|
||||
var trimmed = new Dictionary<string, string[]>(NameComparer);
|
||||
for (var i = skip; i < orderedKeys.Count; i++)
|
||||
{
|
||||
var key = orderedKeys[i];
|
||||
trimmed[key] = secfixes[key];
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
private static string GetProjectRoot()
|
||||
=> Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", ".."));
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Distro.Alpine;
|
||||
using StellaOps.Concelier.Connector.Distro.Alpine.Dto;
|
||||
using StellaOps.Concelier.Connector.Distro.Alpine.Internal;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.Alpine.Tests;
|
||||
|
||||
public sealed class AlpineMapperTests
|
||||
{
|
||||
[Fact]
|
||||
public void Map_BuildsApkAdvisoriesWithRanges()
|
||||
{
|
||||
var dto = new AlpineSecDbDto(
|
||||
DistroVersion: "v3.20",
|
||||
RepoName: "main",
|
||||
UrlPrefix: "https://dl-cdn.alpinelinux.org/alpine",
|
||||
Packages: new[]
|
||||
{
|
||||
new AlpinePackageDto(
|
||||
"apk-tools",
|
||||
new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["2.12.6-r0"] = new[] { "CVE-2021-36159" }
|
||||
}),
|
||||
new AlpinePackageDto(
|
||||
"busybox",
|
||||
new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["1.36.1-r29"] = new[] { "CVE-2023-42364" }
|
||||
})
|
||||
});
|
||||
|
||||
var recordedAt = new DateTimeOffset(2025, 12, 22, 0, 0, 0, TimeSpan.Zero);
|
||||
var document = BuildDocument("https://secdb.alpinelinux.org/v3.20/main.json", recordedAt);
|
||||
|
||||
var advisories = AlpineMapper.Map(dto, document, recordedAt);
|
||||
Assert.Equal(2, advisories.Count);
|
||||
|
||||
var apkToolsAdvisory = advisories.Single(advisory => advisory.AdvisoryKey == "alpine/cve-2021-36159");
|
||||
Assert.Contains("CVE-2021-36159", apkToolsAdvisory.Aliases);
|
||||
|
||||
var apkPackage = Assert.Single(apkToolsAdvisory.AffectedPackages);
|
||||
Assert.Equal(AffectedPackageTypes.Apk, apkPackage.Type);
|
||||
Assert.Equal("apk-tools", apkPackage.Identifier);
|
||||
Assert.Equal("v3.20/main", apkPackage.Platform);
|
||||
|
||||
var range = Assert.Single(apkPackage.VersionRanges);
|
||||
Assert.Equal("apk", range.RangeKind);
|
||||
Assert.Equal("2.12.6-r0", range.FixedVersion);
|
||||
Assert.Equal("fixed:2.12.6-r0", range.RangeExpression);
|
||||
Assert.NotNull(range.Primitives?.VendorExtensions);
|
||||
Assert.Equal("v3.20", range.Primitives!.VendorExtensions["alpine.distroversion"]);
|
||||
Assert.Equal("main", range.Primitives.VendorExtensions["alpine.repo"]);
|
||||
}
|
||||
|
||||
private static DocumentRecord BuildDocument(string uri, DateTimeOffset recordedAt)
|
||||
{
|
||||
return new DocumentRecord(
|
||||
Guid.NewGuid(),
|
||||
AlpineConnectorPlugin.SourceName,
|
||||
uri,
|
||||
recordedAt,
|
||||
new string('0', 64),
|
||||
DocumentStatuses.Mapped,
|
||||
"application/json",
|
||||
Headers: null,
|
||||
Metadata: null,
|
||||
Etag: null,
|
||||
LastModified: recordedAt,
|
||||
PayloadId: null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Linq;
|
||||
using StellaOps.Concelier.Connector.Distro.Alpine.Dto;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.Alpine.Tests;
|
||||
|
||||
public sealed class AlpineSecDbParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void Parse_SecDbFixture_ExtractsPackagesAndMetadata()
|
||||
{
|
||||
var dto = AlpineFixtureReader.LoadDto("v3.20-main.json");
|
||||
|
||||
Assert.Equal("v3.20", dto.DistroVersion);
|
||||
Assert.Equal("main", dto.RepoName);
|
||||
Assert.Equal("https://dl-cdn.alpinelinux.org/alpine", dto.UrlPrefix);
|
||||
Assert.NotEmpty(dto.Packages);
|
||||
|
||||
var apkTools = dto.Packages.Single(pkg => pkg.Name == "apk-tools");
|
||||
Assert.True(apkTools.Secfixes.ContainsKey("2.12.6-r0"));
|
||||
Assert.Contains("CVE-2021-36159", apkTools.Secfixes["2.12.6-r0"]);
|
||||
|
||||
var busybox = dto.Packages.Single(pkg => pkg.Name == "busybox");
|
||||
Assert.True(busybox.Secfixes.Keys.Any());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using StellaOps.Concelier.Documents;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Distro.Alpine;
|
||||
using StellaOps.Concelier.Connector.Distro.Alpine.Dto;
|
||||
using StellaOps.Concelier.Connector.Distro.Alpine.Internal;
|
||||
using StellaOps.Concelier.Models;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Distro.Alpine.Tests;
|
||||
|
||||
public sealed class AlpineSnapshotTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("v3.18-main.json", "alpine-v3.18-main.snapshot.json", "2025-12-22T00:00:00Z")]
|
||||
[InlineData("v3.19-main.json", "alpine-v3.19-main.snapshot.json", "2025-12-22T00:10:00Z")]
|
||||
[InlineData("v3.20-main.json", "alpine-v3.20-main.snapshot.json", "2025-12-22T00:20:00Z")]
|
||||
public void Snapshot_FixturesMatchGolden(string fixtureFile, string snapshotFile, string recordedAt)
|
||||
{
|
||||
var dto = AlpineFixtureReader.LoadDto(fixtureFile);
|
||||
var filtered = AlpineFixtureReader.FilterPackages(
|
||||
dto,
|
||||
new[] { "apk-tools", "busybox", "zlib" },
|
||||
maxVersionsPerPackage: 2);
|
||||
|
||||
var recorded = DateTimeOffset.Parse(recordedAt);
|
||||
var document = BuildDocument(filtered, recorded);
|
||||
|
||||
var advisories = AlpineMapper.Map(filtered, document, recorded);
|
||||
var ordered = advisories
|
||||
.OrderBy(advisory => advisory.AdvisoryKey, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
var snapshot = AlpineFixtureReader.NormalizeSnapshot(SnapshotSerializer.ToSnapshot(ordered));
|
||||
var snapshotPath = AlpineFixtureReader.GetWritableFixturePath(snapshotFile);
|
||||
|
||||
if (ShouldUpdateGoldens() || !File.Exists(snapshotPath))
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(snapshotPath)!);
|
||||
File.WriteAllText(snapshotPath, snapshot);
|
||||
return;
|
||||
}
|
||||
|
||||
var expected = AlpineFixtureReader.NormalizeSnapshot(File.ReadAllText(snapshotPath));
|
||||
Assert.Equal(expected, snapshot);
|
||||
}
|
||||
|
||||
private static DocumentRecord BuildDocument(AlpineSecDbDto dto, DateTimeOffset recordedAt)
|
||||
{
|
||||
var uri = new Uri(new Uri("https://secdb.alpinelinux.org/"), $"{dto.DistroVersion}/{dto.RepoName}.json");
|
||||
return new DocumentRecord(
|
||||
Guid.NewGuid(),
|
||||
AlpineConnectorPlugin.SourceName,
|
||||
uri.ToString(),
|
||||
recordedAt,
|
||||
new string('0', 64),
|
||||
DocumentStatuses.Mapped,
|
||||
"application/json",
|
||||
Headers: null,
|
||||
Metadata: null,
|
||||
Etag: null,
|
||||
LastModified: recordedAt,
|
||||
PayloadId: null);
|
||||
}
|
||||
|
||||
private static bool ShouldUpdateGoldens()
|
||||
=> IsTruthy(Environment.GetEnvironmentVariable("UPDATE_GOLDENS"))
|
||||
|| IsTruthy(Environment.GetEnvironmentVariable("DOTNET_TEST_UPDATE_GOLDENS"));
|
||||
|
||||
private static bool IsTruthy(string? value)
|
||||
=> !string.IsNullOrWhiteSpace(value)
|
||||
&& (string.Equals(value, "1", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,20 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Distro.Alpine/StellaOps.Concelier.Connector.Distro.Alpine.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Merge/StellaOps.Concelier.Merge.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Testing/StellaOps.Concelier.Testing.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="Source\Distro\Alpine\Fixtures\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,349 @@
|
||||
using System.Globalization;
|
||||
using System.IO.Compression;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Concelier.Connector.Common;
|
||||
using StellaOps.Concelier.Connector.Common.Fetch;
|
||||
using StellaOps.Concelier.Connector.Common.Testing;
|
||||
using StellaOps.Concelier.Connector.Epss;
|
||||
using StellaOps.Concelier.Connector.Epss.Configuration;
|
||||
using StellaOps.Concelier.Connector.Epss.Internal;
|
||||
using StellaOps.Concelier.Documents;
|
||||
using StellaOps.Concelier.Storage;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Scanner.Storage.Epss;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Connector.Epss.Tests;
|
||||
|
||||
public sealed class EpssConnectorTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task FetchAsync_StoresDocument_OnSuccess()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
var date = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
var fileName = $"epss_scores-{date:yyyy-MM-dd}.csv.gz";
|
||||
var uri = new Uri(options.BaseUri, fileName);
|
||||
var payload = BuildSampleGzip(date);
|
||||
|
||||
var handler = new CannedHttpMessageHandler();
|
||||
handler.AddResponse(uri, () =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new ByteArrayContent(payload)
|
||||
};
|
||||
response.Headers.ETag = new EntityTagHeaderValue("\"epss-etag\"");
|
||||
response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/gzip");
|
||||
return response;
|
||||
});
|
||||
|
||||
var documentStore = new InMemoryDocumentStore();
|
||||
var dtoStore = new InMemoryDtoStore();
|
||||
var stateRepository = new InMemorySourceStateRepository();
|
||||
var connector = CreateConnector(handler, documentStore, dtoStore, stateRepository, options);
|
||||
|
||||
await connector.FetchAsync(new ServiceCollection().BuildServiceProvider(), CancellationToken.None);
|
||||
|
||||
var record = await documentStore.FindBySourceAndUriAsync(EpssConnectorPlugin.SourceName, uri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(record);
|
||||
Assert.Equal(DocumentStatuses.PendingParse, record!.Status);
|
||||
|
||||
var state = await stateRepository.TryGetAsync(EpssConnectorPlugin.SourceName, CancellationToken.None);
|
||||
Assert.NotNull(state);
|
||||
var cursor = EpssCursor.FromDocument(state!.Cursor);
|
||||
Assert.Contains(record.Id, cursor.PendingDocuments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FetchAsync_ReturnsNotModified_OnEtagMatch()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
var date = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
var fileName = $"epss_scores-{date:yyyy-MM-dd}.csv.gz";
|
||||
var uri = new Uri(options.BaseUri, fileName);
|
||||
|
||||
var documentStore = new InMemoryDocumentStore();
|
||||
var dtoStore = new InMemoryDtoStore();
|
||||
var stateRepository = new InMemorySourceStateRepository();
|
||||
|
||||
var existing = new DocumentRecord(
|
||||
Guid.NewGuid(),
|
||||
EpssConnectorPlugin.SourceName,
|
||||
uri.ToString(),
|
||||
DateTimeOffset.UtcNow,
|
||||
"sha256-previous",
|
||||
DocumentStatuses.Mapped,
|
||||
"application/gzip",
|
||||
Headers: null,
|
||||
Metadata: null,
|
||||
Etag: "\"epss-etag\"",
|
||||
LastModified: DateTimeOffset.UtcNow,
|
||||
PayloadId: null,
|
||||
ExpiresAt: null,
|
||||
Payload: null);
|
||||
|
||||
await documentStore.UpsertAsync(existing, CancellationToken.None);
|
||||
await stateRepository.UpdateCursorAsync(
|
||||
EpssConnectorPlugin.SourceName,
|
||||
EpssCursor.Empty with { ETag = "\"epss-etag\"" }.ToDocumentObject(),
|
||||
DateTimeOffset.UtcNow,
|
||||
CancellationToken.None);
|
||||
|
||||
var handler = new CannedHttpMessageHandler();
|
||||
handler.AddResponse(uri, () => new HttpResponseMessage(HttpStatusCode.NotModified));
|
||||
|
||||
var connector = CreateConnector(handler, documentStore, dtoStore, stateRepository, options);
|
||||
|
||||
await connector.FetchAsync(new ServiceCollection().BuildServiceProvider(), CancellationToken.None);
|
||||
|
||||
var record = await documentStore.FindBySourceAndUriAsync(EpssConnectorPlugin.SourceName, uri.ToString(), CancellationToken.None);
|
||||
Assert.NotNull(record);
|
||||
Assert.Equal("\"epss-etag\"", record!.Etag);
|
||||
|
||||
var state = await stateRepository.TryGetAsync(EpssConnectorPlugin.SourceName, CancellationToken.None);
|
||||
var cursor = EpssCursor.FromDocument(state!.Cursor);
|
||||
Assert.Empty(cursor.PendingDocuments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParseAsync_CreatesDto_AndUpdatesStatus()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
var date = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
var fileName = $"epss_scores-{date:yyyy-MM-dd}.csv.gz";
|
||||
var uri = new Uri(options.BaseUri, fileName);
|
||||
var payload = BuildSampleGzip(date);
|
||||
|
||||
var documentStore = new InMemoryDocumentStore();
|
||||
var dtoStore = new InMemoryDtoStore();
|
||||
var stateRepository = new InMemorySourceStateRepository();
|
||||
|
||||
var recordId = Guid.NewGuid();
|
||||
var rawStorage = new RawDocumentStorage(documentStore);
|
||||
await rawStorage.UploadAsync(EpssConnectorPlugin.SourceName, uri.ToString(), payload, "application/gzip", CancellationToken.None, recordId);
|
||||
|
||||
var document = new DocumentRecord(
|
||||
recordId,
|
||||
EpssConnectorPlugin.SourceName,
|
||||
uri.ToString(),
|
||||
DateTimeOffset.UtcNow,
|
||||
"sha256-test",
|
||||
DocumentStatuses.PendingParse,
|
||||
"application/gzip",
|
||||
Headers: null,
|
||||
Metadata: new Dictionary<string, string> { ["epss.date"] = date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) },
|
||||
Etag: null,
|
||||
LastModified: null,
|
||||
PayloadId: recordId,
|
||||
ExpiresAt: null,
|
||||
Payload: payload);
|
||||
|
||||
await documentStore.UpsertAsync(document, CancellationToken.None);
|
||||
await stateRepository.UpdateCursorAsync(
|
||||
EpssConnectorPlugin.SourceName,
|
||||
EpssCursor.Empty with { PendingDocuments = new[] { recordId } }.ToDocumentObject(),
|
||||
DateTimeOffset.UtcNow,
|
||||
CancellationToken.None);
|
||||
|
||||
var connector = CreateConnector(rawStorage, documentStore, dtoStore, stateRepository, options);
|
||||
|
||||
await connector.ParseAsync(new ServiceCollection().BuildServiceProvider(), CancellationToken.None);
|
||||
|
||||
var dto = await dtoStore.FindByDocumentIdAsync(recordId, CancellationToken.None);
|
||||
Assert.NotNull(dto);
|
||||
|
||||
var updated = await documentStore.FindAsync(recordId, CancellationToken.None);
|
||||
Assert.Equal(DocumentStatuses.PendingMap, updated!.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MapAsync_MarksDocumentMapped()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
var date = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
var fileName = $"epss_scores-{date:yyyy-MM-dd}.csv.gz";
|
||||
var uri = new Uri(options.BaseUri, fileName);
|
||||
var payload = BuildSampleGzip(date);
|
||||
|
||||
var documentStore = new InMemoryDocumentStore();
|
||||
var dtoStore = new InMemoryDtoStore();
|
||||
var stateRepository = new InMemorySourceStateRepository();
|
||||
|
||||
var recordId = Guid.NewGuid();
|
||||
var rawStorage = new RawDocumentStorage(documentStore);
|
||||
await rawStorage.UploadAsync(EpssConnectorPlugin.SourceName, uri.ToString(), payload, "application/gzip", CancellationToken.None, recordId);
|
||||
|
||||
var document = new DocumentRecord(
|
||||
recordId,
|
||||
EpssConnectorPlugin.SourceName,
|
||||
uri.ToString(),
|
||||
DateTimeOffset.UtcNow,
|
||||
"sha256-test",
|
||||
DocumentStatuses.PendingMap,
|
||||
"application/gzip",
|
||||
Headers: null,
|
||||
Metadata: new Dictionary<string, string> { ["epss.date"] = date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) },
|
||||
Etag: null,
|
||||
LastModified: null,
|
||||
PayloadId: recordId,
|
||||
ExpiresAt: null,
|
||||
Payload: payload);
|
||||
|
||||
await documentStore.UpsertAsync(document, CancellationToken.None);
|
||||
|
||||
var dtoPayload = new DocumentObject
|
||||
{
|
||||
["modelVersion"] = $"v{date:yyyy.MM.dd}",
|
||||
["publishedDate"] = date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
|
||||
["rowCount"] = 2,
|
||||
["contentHash"] = "sha256:placeholder"
|
||||
};
|
||||
|
||||
await dtoStore.UpsertAsync(new DtoRecord(
|
||||
Guid.NewGuid(),
|
||||
recordId,
|
||||
EpssConnectorPlugin.SourceName,
|
||||
"epss.snapshot.v1",
|
||||
dtoPayload,
|
||||
DateTimeOffset.UtcNow), CancellationToken.None);
|
||||
|
||||
await stateRepository.UpdateCursorAsync(
|
||||
EpssConnectorPlugin.SourceName,
|
||||
EpssCursor.Empty with { PendingMappings = new[] { recordId } }.ToDocumentObject(),
|
||||
DateTimeOffset.UtcNow,
|
||||
CancellationToken.None);
|
||||
|
||||
var connector = CreateConnector(rawStorage, documentStore, dtoStore, stateRepository, options);
|
||||
|
||||
await connector.MapAsync(new ServiceCollection().BuildServiceProvider(), CancellationToken.None);
|
||||
|
||||
var updated = await documentStore.FindAsync(recordId, CancellationToken.None);
|
||||
Assert.Equal(DocumentStatuses.Mapped, updated!.Status);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0.75, EpssBand.Critical)]
|
||||
[InlineData(0.55, EpssBand.High)]
|
||||
[InlineData(0.25, EpssBand.Medium)]
|
||||
[InlineData(0.05, EpssBand.Low)]
|
||||
public void ToObservation_AssignsBand(double score, EpssBand expected)
|
||||
{
|
||||
var row = new EpssScoreRow("CVE-2025-0001", score, 0.5);
|
||||
|
||||
var observation = EpssMapper.ToObservation(row, "v2025.12.21", new DateOnly(2025, 12, 21));
|
||||
|
||||
Assert.Equal(expected, observation.Band);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EpssCursor_Empty_UsesMinValue()
|
||||
{
|
||||
var cursor = EpssCursor.Empty;
|
||||
|
||||
Assert.Equal(DateTimeOffset.MinValue, cursor.UpdatedAt);
|
||||
Assert.Empty(cursor.PendingDocuments);
|
||||
Assert.Empty(cursor.PendingMappings);
|
||||
}
|
||||
|
||||
private static EpssOptions CreateOptions()
|
||||
=> new()
|
||||
{
|
||||
BaseUri = new Uri("https://epss.example/"),
|
||||
FetchCurrent = true,
|
||||
CatchUpDays = 0,
|
||||
HttpTimeout = TimeSpan.FromSeconds(10),
|
||||
MaxRetries = 0,
|
||||
AirgapMode = false
|
||||
};
|
||||
|
||||
private static EpssConnector CreateConnector(
|
||||
CannedHttpMessageHandler handler,
|
||||
IDocumentStore documentStore,
|
||||
IDtoStore dtoStore,
|
||||
ISourceStateRepository stateRepository,
|
||||
EpssOptions options)
|
||||
{
|
||||
var client = handler.CreateClient();
|
||||
var factory = new SingleClientFactory(client);
|
||||
var rawStorage = new RawDocumentStorage(documentStore);
|
||||
var diagnostics = new EpssDiagnostics();
|
||||
var hash = DefaultCryptoHash.CreateForTests();
|
||||
return new EpssConnector(
|
||||
factory,
|
||||
rawStorage,
|
||||
documentStore,
|
||||
dtoStore,
|
||||
stateRepository,
|
||||
Options.Create(options),
|
||||
diagnostics,
|
||||
hash,
|
||||
TimeProvider.System,
|
||||
NullLogger<EpssConnector>.Instance);
|
||||
}
|
||||
|
||||
private static EpssConnector CreateConnector(
|
||||
RawDocumentStorage rawStorage,
|
||||
IDocumentStore documentStore,
|
||||
IDtoStore dtoStore,
|
||||
ISourceStateRepository stateRepository,
|
||||
EpssOptions options)
|
||||
{
|
||||
var client = new HttpClient();
|
||||
var factory = new SingleClientFactory(client);
|
||||
var diagnostics = new EpssDiagnostics();
|
||||
var hash = DefaultCryptoHash.CreateForTests();
|
||||
return new EpssConnector(
|
||||
factory,
|
||||
rawStorage,
|
||||
documentStore,
|
||||
dtoStore,
|
||||
stateRepository,
|
||||
Options.Create(options),
|
||||
diagnostics,
|
||||
hash,
|
||||
TimeProvider.System,
|
||||
NullLogger<EpssConnector>.Instance);
|
||||
}
|
||||
|
||||
private static byte[] BuildSampleGzip(DateOnly date)
|
||||
{
|
||||
var modelVersion = $"v{date:yyyy.MM.dd}";
|
||||
var lines = new[]
|
||||
{
|
||||
$"# model {modelVersion}",
|
||||
$"# date {date:yyyy-MM-dd}",
|
||||
"cve,epss,percentile",
|
||||
"CVE-2024-0001,0.42,0.91",
|
||||
"CVE-2024-0002,0.82,0.99"
|
||||
};
|
||||
|
||||
using var output = new MemoryStream();
|
||||
using (var gzip = new GZipStream(output, CompressionLevel.Optimal, leaveOpen: true))
|
||||
using (var writer = new StreamWriter(gzip, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), leaveOpen: true))
|
||||
{
|
||||
foreach (var line in lines)
|
||||
{
|
||||
writer.WriteLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private sealed class SingleClientFactory : IHttpClientFactory
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
|
||||
public SingleClientFactory(HttpClient client)
|
||||
=> _client = client;
|
||||
|
||||
public HttpClient CreateClient(string name) => _client;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Common/StellaOps.Concelier.Connector.Common.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Connector.Epss/StellaOps.Concelier.Connector.Epss.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Cryptography/StellaOps.Cryptography.csproj" />
|
||||
<ProjectReference Include="../../../Scanner/__Libraries/StellaOps.Scanner.Storage/StellaOps.Scanner.Storage.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DotNet.Testcontainers.Builders;
|
||||
using DotNet.Testcontainers.Containers;
|
||||
using StellaOps.Concelier.Merge.Comparers;
|
||||
using StellaOps.Concelier.Normalization.Distro;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Integration.Tests;
|
||||
|
||||
public sealed class DistroVersionCrossCheckTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task CrossCheck_InstalledVersionsMatchComparers()
|
||||
{
|
||||
var fixtures = LoadFixtures();
|
||||
var groups = fixtures
|
||||
.GroupBy(fixture => fixture.Image, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
await using var container = new ContainerBuilder()
|
||||
.WithImage(group.Key)
|
||||
.WithCommand("sh", "-c", "sleep 3600")
|
||||
.Build();
|
||||
|
||||
await container.StartAsync();
|
||||
|
||||
foreach (var fixture in group)
|
||||
{
|
||||
var installed = await GetInstalledVersionAsync(container, fixture, CancellationToken.None);
|
||||
var actual = CompareVersions(fixture, installed);
|
||||
Assert.Equal(fixture.ExpectedComparison, actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> GetInstalledVersionAsync(
|
||||
IContainer container,
|
||||
DistroVersionFixture fixture,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var output = fixture.Distro switch
|
||||
{
|
||||
"rpm" => await RunCommandAsync(container,
|
||||
$"rpm -q --qf '%{{NAME}}-%{{EPOCHNUM}}:%{{VERSION}}-%{{RELEASE}}.%{{ARCH}}' {fixture.Package}", ct),
|
||||
"deb" => await RunCommandAsync(container,
|
||||
$"dpkg-query -W -f='${{Version}}' {fixture.Package}", ct),
|
||||
"apk" => await RunCommandAsync(container, $"apk info -v {fixture.Package}", ct),
|
||||
_ => throw new InvalidOperationException($"Unsupported distro: {fixture.Distro}")
|
||||
};
|
||||
|
||||
return fixture.Distro switch
|
||||
{
|
||||
"apk" => ExtractApkVersion(fixture.Package, output),
|
||||
_ => output.Trim()
|
||||
};
|
||||
}
|
||||
|
||||
private static int CompareVersions(DistroVersionFixture fixture, string installedVersion)
|
||||
{
|
||||
return fixture.Distro switch
|
||||
{
|
||||
"rpm" => Math.Sign(CompareRpm(installedVersion, fixture.FixedVersion)),
|
||||
"deb" => Math.Sign(DebianEvrComparer.Instance.Compare(installedVersion, fixture.FixedVersion)),
|
||||
"apk" => Math.Sign(ApkVersionComparer.Instance.Compare(installedVersion, fixture.FixedVersion)),
|
||||
_ => throw new InvalidOperationException($"Unsupported distro: {fixture.Distro}")
|
||||
};
|
||||
}
|
||||
|
||||
private static int CompareRpm(string installed, string fixedEvr)
|
||||
{
|
||||
if (!Nevra.TryParse(installed, out var nevra) || nevra is null)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to parse NEVRA '{installed}'.");
|
||||
}
|
||||
|
||||
var fixedNevra = $"{nevra.Name}-{fixedEvr}";
|
||||
if (!string.IsNullOrWhiteSpace(nevra.Architecture))
|
||||
{
|
||||
fixedNevra = $"{fixedNevra}.{nevra.Architecture}";
|
||||
}
|
||||
|
||||
return NevraComparer.Instance.Compare(installed, fixedNevra);
|
||||
}
|
||||
|
||||
private static async Task<string> RunCommandAsync(IContainer container, string command, CancellationToken ct)
|
||||
{
|
||||
var result = await container.ExecAsync(new[] { "sh", "-c", command }, ct);
|
||||
if (result.ExitCode != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Command failed ({result.ExitCode}): {command}\n{result.Stderr}");
|
||||
}
|
||||
|
||||
return result.Stdout.Trim();
|
||||
}
|
||||
|
||||
private static string ExtractApkVersion(string package, string output)
|
||||
{
|
||||
var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var prefix = package + "-";
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
if (trimmed.StartsWith(prefix, StringComparison.Ordinal))
|
||||
{
|
||||
return trimmed[prefix.Length..];
|
||||
}
|
||||
}
|
||||
|
||||
return lines.Length > 0 ? lines[0].Trim() : string.Empty;
|
||||
}
|
||||
|
||||
private static List<DistroVersionFixture> LoadFixtures()
|
||||
{
|
||||
var path = ResolveFixturePath("distro-version-crosscheck.json");
|
||||
var payload = File.ReadAllText(path);
|
||||
var fixtures = JsonSerializer.Deserialize<List<DistroVersionFixture>>(payload, JsonOptions)
|
||||
?? new List<DistroVersionFixture>();
|
||||
return fixtures;
|
||||
}
|
||||
|
||||
private static string ResolveFixturePath(string filename)
|
||||
{
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(AppContext.BaseDirectory, "Fixtures", filename),
|
||||
Path.Combine(GetProjectRoot(), "Fixtures", filename)
|
||||
};
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
throw new FileNotFoundException($"Fixture '{filename}' not found.", filename);
|
||||
}
|
||||
|
||||
private static string GetProjectRoot()
|
||||
=> Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", ".."));
|
||||
|
||||
private sealed record DistroVersionFixture(
|
||||
string Image,
|
||||
string Distro,
|
||||
string Package,
|
||||
string FixedVersion,
|
||||
int ExpectedComparison,
|
||||
string? Note);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
[
|
||||
{
|
||||
"image": "registry.access.redhat.com/ubi9:latest",
|
||||
"distro": "rpm",
|
||||
"package": "glibc",
|
||||
"fixedVersion": "0:0-0",
|
||||
"expectedComparison": 1,
|
||||
"note": "baseline floor"
|
||||
},
|
||||
{
|
||||
"image": "registry.access.redhat.com/ubi9:latest",
|
||||
"distro": "rpm",
|
||||
"package": "rpm",
|
||||
"fixedVersion": "0:0-0",
|
||||
"expectedComparison": 1,
|
||||
"note": "baseline floor"
|
||||
},
|
||||
{
|
||||
"image": "registry.access.redhat.com/ubi9:latest",
|
||||
"distro": "rpm",
|
||||
"package": "openssl-libs",
|
||||
"fixedVersion": "0:0-0",
|
||||
"expectedComparison": 1,
|
||||
"note": "baseline floor"
|
||||
},
|
||||
{
|
||||
"image": "debian:12-slim",
|
||||
"distro": "deb",
|
||||
"package": "dpkg",
|
||||
"fixedVersion": "0",
|
||||
"expectedComparison": 1,
|
||||
"note": "baseline floor"
|
||||
},
|
||||
{
|
||||
"image": "debian:12-slim",
|
||||
"distro": "deb",
|
||||
"package": "libc6",
|
||||
"fixedVersion": "0",
|
||||
"expectedComparison": 1,
|
||||
"note": "baseline floor"
|
||||
},
|
||||
{
|
||||
"image": "debian:12-slim",
|
||||
"distro": "deb",
|
||||
"package": "base-files",
|
||||
"fixedVersion": "0",
|
||||
"expectedComparison": 1,
|
||||
"note": "baseline floor"
|
||||
},
|
||||
{
|
||||
"image": "ubuntu:22.04",
|
||||
"distro": "deb",
|
||||
"package": "dpkg",
|
||||
"fixedVersion": "0",
|
||||
"expectedComparison": 1,
|
||||
"note": "baseline floor"
|
||||
},
|
||||
{
|
||||
"image": "ubuntu:22.04",
|
||||
"distro": "deb",
|
||||
"package": "libc6",
|
||||
"fixedVersion": "0",
|
||||
"expectedComparison": 1,
|
||||
"note": "baseline floor"
|
||||
},
|
||||
{
|
||||
"image": "ubuntu:22.04",
|
||||
"distro": "deb",
|
||||
"package": "base-files",
|
||||
"fixedVersion": "0",
|
||||
"expectedComparison": 1,
|
||||
"note": "baseline floor"
|
||||
},
|
||||
{
|
||||
"image": "alpine:3.20",
|
||||
"distro": "apk",
|
||||
"package": "apk-tools",
|
||||
"fixedVersion": "0-r0",
|
||||
"expectedComparison": 1,
|
||||
"note": "baseline floor"
|
||||
},
|
||||
{
|
||||
"image": "alpine:3.20",
|
||||
"distro": "apk",
|
||||
"package": "busybox",
|
||||
"fixedVersion": "0-r0",
|
||||
"expectedComparison": 1,
|
||||
"note": "baseline floor"
|
||||
},
|
||||
{
|
||||
"image": "alpine:3.20",
|
||||
"distro": "apk",
|
||||
"package": "zlib",
|
||||
"fixedVersion": "0-r0",
|
||||
"expectedComparison": 1,
|
||||
"note": "baseline floor"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,39 @@
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Integration.Tests;
|
||||
|
||||
internal static class IntegrationTestSettings
|
||||
{
|
||||
public static bool IsEnabled
|
||||
{
|
||||
get
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable("STELLAOPS_INTEGRATION_TESTS");
|
||||
return string.Equals(value, "1", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class IntegrationFactAttribute : FactAttribute
|
||||
{
|
||||
public IntegrationFactAttribute()
|
||||
{
|
||||
if (!IntegrationTestSettings.IsEnabled)
|
||||
{
|
||||
Skip = "Integration tests disabled. Set STELLAOPS_INTEGRATION_TESTS=true to enable.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class IntegrationTheoryAttribute : TheoryAttribute
|
||||
{
|
||||
public IntegrationTheoryAttribute()
|
||||
{
|
||||
if (!IntegrationTestSettings.IsEnabled)
|
||||
{
|
||||
Skip = "Integration tests disabled. Set STELLAOPS_INTEGRATION_TESTS=true to enable.";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Concelier.Merge\StellaOps.Concelier.Merge.csproj" />
|
||||
<ProjectReference Include="..\..\__Libraries\StellaOps.Concelier.Normalization\StellaOps.Concelier.Normalization.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Testcontainers" Version="4.4.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="Fixtures\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,76 @@
|
||||
using StellaOps.Concelier.Merge.Comparers;
|
||||
using StellaOps.Concelier.Normalization.Distro;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class ApkVersionComparerTests
|
||||
{
|
||||
public static TheoryData<string, string, int, string> ComparisonCases => BuildComparisonCases();
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ComparisonCases))]
|
||||
public void Compare_ApkVersions_ReturnsExpectedOrder(string left, string right, int expected, string note)
|
||||
{
|
||||
var actual = Math.Sign(ApkVersionComparer.Instance.Compare(left, right));
|
||||
Assert.True(expected == actual, $"[{note}] '{left}' vs '{right}': expected {expected}, got {actual}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compare_ParsesApkVersionComponents()
|
||||
{
|
||||
var result = ApkVersionComparer.Instance.Compare(
|
||||
ApkVersion.Parse("3.1.4-r0"),
|
||||
ApkVersion.Parse("3.1.3-r2"));
|
||||
|
||||
Assert.True(result > 0);
|
||||
}
|
||||
|
||||
private static TheoryData<string, string, int, string> BuildComparisonCases()
|
||||
{
|
||||
var data = new TheoryData<string, string, int, string>();
|
||||
|
||||
// Suffix ordering.
|
||||
data.Add("1.0_alpha", "1.0_beta", -1, "suffix ordering: alpha < beta");
|
||||
data.Add("1.0_beta", "1.0_pre", -1, "suffix ordering: beta < pre");
|
||||
data.Add("1.0_pre", "1.0_rc", -1, "suffix ordering: pre < rc");
|
||||
data.Add("1.0_rc", "1.0", -1, "suffix ordering: rc < none");
|
||||
data.Add("1.0", "1.0_p", -1, "suffix ordering: none < p");
|
||||
data.Add("1.0_alpha", "1.0_p", -1, "suffix ordering: alpha < p");
|
||||
|
||||
// Suffix numeric ordering.
|
||||
data.Add("1.0_alpha1", "1.0_alpha2", -1, "suffix numeric ordering");
|
||||
data.Add("1.0_alpha2", "1.0_alpha10", -1, "suffix numeric ordering");
|
||||
data.Add("1.0_pre1", "1.0_pre2", -1, "suffix numeric ordering");
|
||||
data.Add("1.0_rc1", "1.0_rc2", -1, "suffix numeric ordering");
|
||||
data.Add("1.0_beta1", "1.0_beta01", 0, "suffix numeric leading zeros ignored");
|
||||
|
||||
// Numeric ordering in version.
|
||||
data.Add("1.2.3", "1.2.10", -1, "numeric segment ordering");
|
||||
data.Add("1.10.0", "1.2.9", 1, "numeric segment ordering");
|
||||
data.Add("2.0", "1.9", 1, "major version ordering");
|
||||
data.Add("1.02", "1.2", 0, "leading zeros ignored");
|
||||
data.Add("1.2.03", "1.2.3", 0, "leading zeros ignored");
|
||||
|
||||
// Alpha segment ordering.
|
||||
data.Add("1.2.3a", "1.2.3b", -1, "alpha segment ordering");
|
||||
data.Add("1.2.3a", "1.2.3", 1, "alpha sorts after empty");
|
||||
data.Add("1.2.3", "1.2.3a", -1, "empty sorts before alpha");
|
||||
data.Add("1.2.3aa", "1.2.3b", -1, "alpha lexical ordering");
|
||||
|
||||
// Package release ordering.
|
||||
data.Add("1.2.3-r0", "1.2.3-r1", -1, "pkgrel ordering");
|
||||
data.Add("1.2.3-r1", "1.2.3-r2", -1, "pkgrel ordering");
|
||||
data.Add("1.2.3-r2", "1.2.3-r10", -1, "pkgrel numeric ordering");
|
||||
data.Add("1.2.3-r10", "1.2.3-r2", 1, "pkgrel numeric ordering");
|
||||
data.Add("1.2.3", "1.2.3-r0", -1, "implicit release sorts before explicit r0");
|
||||
|
||||
// Combined ordering.
|
||||
data.Add("1.2.3_p1-r0", "1.2.3_p1-r1", -1, "pkgrel ordering after suffix");
|
||||
data.Add("1.2.3_rc1-r1", "1.2.3-r0", -1, "rc sorts before release even with higher pkgrel");
|
||||
data.Add("1.2.3_p1-r0", "1.2.3-r9", 1, "patch suffix sorts after release");
|
||||
data.Add("1.2.3_pre2-r3", "1.2.3_pre10-r1", -1, "suffix numeric ordering beats pkgrel");
|
||||
data.Add("1.2.3", "1.2.3", 0, "exact match");
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -81,4 +81,70 @@ public sealed class DebianEvrComparerTests
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, int, string> ComparisonCases => BuildComparisonCases();
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ComparisonCases))]
|
||||
public void Compare_DebianEvr_ReturnsExpectedOrder(string left, string right, int expected, string note)
|
||||
{
|
||||
var actual = Math.Sign(DebianEvrComparer.Instance.Compare(left, right));
|
||||
Assert.True(expected == actual, $"[{note}] '{left}' vs '{right}': expected {expected}, got {actual}");
|
||||
}
|
||||
|
||||
private static TheoryData<string, string, int, string> BuildComparisonCases()
|
||||
{
|
||||
var data = new TheoryData<string, string, int, string>();
|
||||
|
||||
// Epoch precedence.
|
||||
data.Add("0:1.0-1", "1:1.0-1", -1, "epoch precedence: 0 < 1");
|
||||
data.Add("1:1.0-1", "0:9.9-9", 1, "epoch precedence: 1 > 0");
|
||||
data.Add("2:0.1-1", "1:9.9-9", 1, "epoch precedence: 2 > 1");
|
||||
data.Add("3:1.0-1", "4:0.1-1", -1, "epoch precedence: 3 < 4");
|
||||
data.Add("5:2.0-1", "4:9.9-9", 1, "epoch precedence: 5 > 4");
|
||||
data.Add("1:2.0-1", "2:1.0-1", -1, "epoch precedence: 1 < 2");
|
||||
|
||||
// Numeric ordering in upstream version.
|
||||
for (var i = 1; i <= 12; i++)
|
||||
{
|
||||
data.Add($"0:1.{i}-1", $"0:1.{i + 1}-1", -1, "numeric segment ordering");
|
||||
}
|
||||
|
||||
data.Add("0:1.09-1", "0:1.9-1", 0, "leading zeros ignored");
|
||||
data.Add("0:2.001-1", "0:2.1-1", 0, "leading zeros ignored");
|
||||
|
||||
// Tilde pre-releases.
|
||||
data.Add("0:1.0~alpha1-1", "0:1.0~alpha2-1", -1, "tilde pre-release ordering");
|
||||
data.Add("0:1.0~rc1-1", "0:1.0-1", -1, "tilde sorts before release");
|
||||
data.Add("0:1.0~~-1", "0:1.0~-1", -1, "double tilde sorts earlier");
|
||||
data.Add("0:2.0~beta-1", "0:2.0~rc-1", -1, "tilde alpha ordering");
|
||||
data.Add("0:1.0~rc1-1", "0:1.0~rc2-1", -1, "tilde rc ordering");
|
||||
data.Add("0:1.0~rc-1", "0:1.0~rc-2", -1, "revision breaks tilde ties");
|
||||
|
||||
// Debian revision ordering.
|
||||
for (var i = 1; i <= 10; i++)
|
||||
{
|
||||
data.Add($"0:1.0-{i}", $"0:1.0-{i + 1}", -1, "revision numeric ordering");
|
||||
}
|
||||
|
||||
data.Add("0:1.0-1", "0:1.0-1ubuntu0.1", -1, "ubuntu security backport");
|
||||
data.Add("0:1.0-1ubuntu0.1", "0:1.0-1ubuntu0.2", -1, "ubuntu incremental backport");
|
||||
data.Add("0:1.0-1ubuntu1", "0:1.0-1ubuntu2", -1, "ubuntu delta update");
|
||||
data.Add("0:1.0-1build1", "0:1.0-1build2", -1, "ubuntu rebuild");
|
||||
data.Add("0:1.0-1+deb12u1", "0:1.0-1+deb12u2", -1, "debian stable update");
|
||||
data.Add("0:1.0-1ubuntu0.2", "0:1.0-1ubuntu1", -1, "ubuntu ordering baseline");
|
||||
data.Add("0:1.0-1ubuntu1", "0:1.0-1ubuntu1.1", -1, "ubuntu dotted revision");
|
||||
data.Add("0:1.0-1ubuntu1.1", "0:1.0-1ubuntu1.2", -1, "ubuntu dotted revision ordering");
|
||||
data.Add("0:1.0-1+deb12u1", "0:1.0-1ubuntu1", -1, "debian update before ubuntu delta");
|
||||
data.Add("0:1.0-1ubuntu2", "0:1.0-1ubuntu10", -1, "ubuntu numeric ordering");
|
||||
|
||||
// Native package handling.
|
||||
data.Add("0:1.0", "0:1.0-1", -1, "native package sorts before revisioned");
|
||||
data.Add("0:1.0", "0:1.0+deb12u1", -1, "native package sorts before debian update");
|
||||
data.Add("1:1.0", "1:1.0-1", -1, "native package sorts before revisioned");
|
||||
data.Add("0:2.0", "0:2.0-0", -1, "native package sorts before zero revision");
|
||||
data.Add("0:2.0-0", "0:2.0-1", -1, "zero revision sorts before higher revision");
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
param(
|
||||
[string]$Configuration = "Debug"
|
||||
)
|
||||
|
||||
$root = Resolve-Path (Join-Path $PSScriptRoot "..\\..\\..\\..")
|
||||
$mergePath = Join-Path $root "__Libraries\\StellaOps.Concelier.Merge\\bin\\$Configuration\\net10.0\\StellaOps.Concelier.Merge.dll"
|
||||
$normPath = Join-Path $root "__Libraries\\StellaOps.Concelier.Normalization\\bin\\$Configuration\\net10.0\\StellaOps.Concelier.Normalization.dll"
|
||||
|
||||
if (-not (Test-Path $mergePath)) {
|
||||
throw "Build StellaOps.Concelier.Merge first. Missing: $mergePath"
|
||||
}
|
||||
|
||||
[System.Reflection.Assembly]::LoadFrom($mergePath) | Out-Null
|
||||
if (Test-Path $normPath) {
|
||||
[System.Reflection.Assembly]::LoadFrom($normPath) | Out-Null
|
||||
}
|
||||
|
||||
$nevraComparer = [StellaOps.Concelier.Merge.Comparers.NevraComparer]::Instance
|
||||
$debComparer = [StellaOps.Concelier.Merge.Comparers.DebianEvrComparer]::Instance
|
||||
$apkComparer = [StellaOps.Concelier.Merge.Comparers.ApkVersionComparer]::Instance
|
||||
|
||||
$rpmVersions = @(
|
||||
'kernel-0:4.18.0-80.el8.x86_64',
|
||||
'kernel-1:4.18.0-80.el8.x86_64',
|
||||
'kernel-0:4.18.11-80.el8.x86_64',
|
||||
'pkg-0:1.0-1.el9.noarch',
|
||||
'pkg-0:1.0-1.el9.x86_64',
|
||||
'pkg-0:1.0-2.el9.x86_64',
|
||||
'pkg-0:1.0-10.el9.x86_64',
|
||||
'pkg-0:1.0~rc1-1.el9.x86_64',
|
||||
'pkg-0:1.0-1.fc35.x86_64',
|
||||
'pkg-0:1.0-1.fc36.x86_64',
|
||||
'openssl-1:1.1.1k-7.el8.x86_64',
|
||||
'openssl-3:1.1.1k-7.el8.x86_64',
|
||||
'podman-1:4.5.0-1.el9.x86_64',
|
||||
'podman-2:4.4.0-1.el9.x86_64',
|
||||
'glibc-4:2.36-9.el9.x86_64',
|
||||
'glibc-5:2.36-8.el9.x86_64'
|
||||
)
|
||||
|
||||
$debVersions = @(
|
||||
'0:1.0-1',
|
||||
'1.0-1',
|
||||
'1.0-1ubuntu0.1',
|
||||
'1.0-1ubuntu0.2',
|
||||
'1.0-1ubuntu1',
|
||||
'1.0-1ubuntu2',
|
||||
'1.0-1+deb12u1',
|
||||
'1.0-1+deb12u2',
|
||||
'1:1.1.1n-0+deb11u2',
|
||||
'1:1.1.1n-0+deb11u5',
|
||||
'2.0~beta1-1',
|
||||
'2.0~rc1-1',
|
||||
'2.0-1',
|
||||
'1.2.3-1',
|
||||
'1.2.10-1',
|
||||
'3.0.0-1'
|
||||
)
|
||||
|
||||
$apkVersions = @(
|
||||
'1.0_alpha1-r0',
|
||||
'1.0_beta1-r0',
|
||||
'1.0_pre1-r0',
|
||||
'1.0_rc1-r0',
|
||||
'1.0-r0',
|
||||
'1.0_p1-r0',
|
||||
'1.2.3-r0',
|
||||
'1.2.10-r0',
|
||||
'2.0-r0',
|
||||
'1.2.3a-r0',
|
||||
'1.2.3b-r0',
|
||||
'1.2.3-r1',
|
||||
'1.2.3-r2',
|
||||
'1.2.3_p1-r1',
|
||||
'3.9.1-r0',
|
||||
'3.1.1-r0'
|
||||
)
|
||||
|
||||
function New-Cases {
|
||||
param(
|
||||
[string[]]$Versions,
|
||||
[string]$Distro,
|
||||
$Comparer
|
||||
)
|
||||
|
||||
$cases = New-Object System.Collections.Generic.List[object]
|
||||
for ($i = 0; $i -lt $Versions.Count; $i++) {
|
||||
for ($j = $i + 1; $j -lt $Versions.Count; $j++) {
|
||||
$left = $Versions[$i]
|
||||
$right = $Versions[$j]
|
||||
$expected = [Math]::Sign($Comparer.Compare($left, $right))
|
||||
$cases.Add([ordered]@{
|
||||
left = $left
|
||||
right = $right
|
||||
expected = $expected
|
||||
distro = $Distro
|
||||
note = "pairwise"
|
||||
})
|
||||
}
|
||||
}
|
||||
return $cases
|
||||
}
|
||||
|
||||
function Write-GoldenFile {
|
||||
param(
|
||||
[string]$Path,
|
||||
$Cases
|
||||
)
|
||||
$lines = foreach ($case in $Cases) {
|
||||
$case | ConvertTo-Json -Compress
|
||||
}
|
||||
Set-Content -Path $Path -Value ($lines -join "`n") -Encoding ascii
|
||||
}
|
||||
|
||||
$rpmCases = New-Cases -Versions $rpmVersions -Distro "rpm" -Comparer $nevraComparer
|
||||
$debCases = New-Cases -Versions $debVersions -Distro "deb" -Comparer $debComparer
|
||||
$apkCases = New-Cases -Versions $apkVersions -Distro "apk" -Comparer $apkComparer
|
||||
|
||||
Write-GoldenFile -Path (Join-Path $PSScriptRoot "rpm_version_comparison.golden.ndjson") -Cases $rpmCases
|
||||
Write-GoldenFile -Path (Join-Path $PSScriptRoot "deb_version_comparison.golden.ndjson") -Cases $debCases
|
||||
Write-GoldenFile -Path (Join-Path $PSScriptRoot "apk_version_comparison.golden.ndjson") -Cases $apkCases
|
||||
@@ -0,0 +1,28 @@
|
||||
# Distro Version Comparison Goldens
|
||||
|
||||
Golden files store pairwise version comparison results in NDJSON to guard
|
||||
regressions in distro-specific comparers (RPM, Debian, Alpine APK).
|
||||
|
||||
## Format
|
||||
Each line is a single JSON object:
|
||||
|
||||
```
|
||||
{"left":"0:1.0-1.el8","right":"1:0.1-1.el8","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
```
|
||||
|
||||
Fields:
|
||||
- left/right: version strings as understood by the target comparer.
|
||||
- expected: comparison result (-1, 0, 1) after Math.Sign.
|
||||
- distro: rpm | deb | apk.
|
||||
- note: optional human note or generation hint.
|
||||
|
||||
## Updating goldens
|
||||
1) Build the comparers:
|
||||
`dotnet build ..\..\..\..\__Libraries\StellaOps.Concelier.Merge\StellaOps.Concelier.Merge.csproj`
|
||||
2) Regenerate:
|
||||
`pwsh .\GenerateGoldenComparisons.ps1`
|
||||
|
||||
Files:
|
||||
- rpm_version_comparison.golden.ndjson
|
||||
- deb_version_comparison.golden.ndjson
|
||||
- apk_version_comparison.golden.ndjson
|
||||
@@ -0,0 +1,120 @@
|
||||
{"left":"1.0_alpha1-r0","right":"1.0_beta1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_alpha1-r0","right":"1.0_pre1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_alpha1-r0","right":"1.0_rc1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_alpha1-r0","right":"1.0-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_alpha1-r0","right":"1.0_p1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_alpha1-r0","right":"1.2.3-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_alpha1-r0","right":"1.2.10-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_alpha1-r0","right":"2.0-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_alpha1-r0","right":"1.2.3a-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_alpha1-r0","right":"1.2.3b-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_alpha1-r0","right":"1.2.3-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_alpha1-r0","right":"1.2.3-r2","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_alpha1-r0","right":"1.2.3_p1-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_alpha1-r0","right":"3.9.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_alpha1-r0","right":"3.1.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_beta1-r0","right":"1.0_pre1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_beta1-r0","right":"1.0_rc1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_beta1-r0","right":"1.0-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_beta1-r0","right":"1.0_p1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_beta1-r0","right":"1.2.3-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_beta1-r0","right":"1.2.10-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_beta1-r0","right":"2.0-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_beta1-r0","right":"1.2.3a-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_beta1-r0","right":"1.2.3b-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_beta1-r0","right":"1.2.3-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_beta1-r0","right":"1.2.3-r2","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_beta1-r0","right":"1.2.3_p1-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_beta1-r0","right":"3.9.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_beta1-r0","right":"3.1.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_pre1-r0","right":"1.0_rc1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_pre1-r0","right":"1.0-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_pre1-r0","right":"1.0_p1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_pre1-r0","right":"1.2.3-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_pre1-r0","right":"1.2.10-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_pre1-r0","right":"2.0-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_pre1-r0","right":"1.2.3a-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_pre1-r0","right":"1.2.3b-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_pre1-r0","right":"1.2.3-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_pre1-r0","right":"1.2.3-r2","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_pre1-r0","right":"1.2.3_p1-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_pre1-r0","right":"3.9.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_pre1-r0","right":"3.1.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_rc1-r0","right":"1.0-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_rc1-r0","right":"1.0_p1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_rc1-r0","right":"1.2.3-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_rc1-r0","right":"1.2.10-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_rc1-r0","right":"2.0-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_rc1-r0","right":"1.2.3a-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_rc1-r0","right":"1.2.3b-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_rc1-r0","right":"1.2.3-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_rc1-r0","right":"1.2.3-r2","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_rc1-r0","right":"1.2.3_p1-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_rc1-r0","right":"3.9.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_rc1-r0","right":"3.1.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0-r0","right":"1.0_p1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0-r0","right":"1.2.3-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0-r0","right":"1.2.10-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0-r0","right":"2.0-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0-r0","right":"1.2.3a-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0-r0","right":"1.2.3b-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0-r0","right":"1.2.3-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0-r0","right":"1.2.3-r2","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0-r0","right":"1.2.3_p1-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0-r0","right":"3.9.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0-r0","right":"3.1.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_p1-r0","right":"1.2.3-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_p1-r0","right":"1.2.10-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_p1-r0","right":"2.0-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_p1-r0","right":"1.2.3a-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_p1-r0","right":"1.2.3b-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_p1-r0","right":"1.2.3-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_p1-r0","right":"1.2.3-r2","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_p1-r0","right":"1.2.3_p1-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_p1-r0","right":"3.9.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.0_p1-r0","right":"3.1.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3-r0","right":"1.2.10-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3-r0","right":"2.0-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3-r0","right":"1.2.3a-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3-r0","right":"1.2.3b-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3-r0","right":"1.2.3-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3-r0","right":"1.2.3-r2","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3-r0","right":"1.2.3_p1-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3-r0","right":"3.9.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3-r0","right":"3.1.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.10-r0","right":"2.0-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.10-r0","right":"1.2.3a-r0","expected":1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.10-r0","right":"1.2.3b-r0","expected":1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.10-r0","right":"1.2.3-r1","expected":1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.10-r0","right":"1.2.3-r2","expected":1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.10-r0","right":"1.2.3_p1-r1","expected":1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.10-r0","right":"3.9.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.10-r0","right":"3.1.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"2.0-r0","right":"1.2.3a-r0","expected":1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"2.0-r0","right":"1.2.3b-r0","expected":1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"2.0-r0","right":"1.2.3-r1","expected":1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"2.0-r0","right":"1.2.3-r2","expected":1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"2.0-r0","right":"1.2.3_p1-r1","expected":1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"2.0-r0","right":"3.9.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"2.0-r0","right":"3.1.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3a-r0","right":"1.2.3b-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3a-r0","right":"1.2.3-r1","expected":1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3a-r0","right":"1.2.3-r2","expected":1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3a-r0","right":"1.2.3_p1-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3a-r0","right":"3.9.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3a-r0","right":"3.1.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3b-r0","right":"1.2.3-r1","expected":1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3b-r0","right":"1.2.3-r2","expected":1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3b-r0","right":"1.2.3_p1-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3b-r0","right":"3.9.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3b-r0","right":"3.1.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3-r1","right":"1.2.3-r2","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3-r1","right":"1.2.3_p1-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3-r1","right":"3.9.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3-r1","right":"3.1.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3-r2","right":"1.2.3_p1-r1","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3-r2","right":"3.9.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3-r2","right":"3.1.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3_p1-r1","right":"3.9.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"1.2.3_p1-r1","right":"3.1.1-r0","expected":-1,"distro":"apk","note":"pairwise"}
|
||||
{"left":"3.9.1-r0","right":"3.1.1-r0","expected":1,"distro":"apk","note":"pairwise"}
|
||||
@@ -0,0 +1,120 @@
|
||||
{"left":"0:1.0-1","right":"1.0-1","expected":0,"distro":"deb","note":"pairwise"}
|
||||
{"left":"0:1.0-1","right":"1.0-1ubuntu0.1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"0:1.0-1","right":"1.0-1ubuntu0.2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"0:1.0-1","right":"1.0-1ubuntu1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"0:1.0-1","right":"1.0-1ubuntu2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"0:1.0-1","right":"1.0-1\u002Bdeb12u1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"0:1.0-1","right":"1.0-1\u002Bdeb12u2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"0:1.0-1","right":"1:1.1.1n-0\u002Bdeb11u2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"0:1.0-1","right":"1:1.1.1n-0\u002Bdeb11u5","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"0:1.0-1","right":"2.0~beta1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"0:1.0-1","right":"2.0~rc1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"0:1.0-1","right":"2.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"0:1.0-1","right":"1.2.3-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"0:1.0-1","right":"1.2.10-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"0:1.0-1","right":"3.0.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1","right":"1.0-1ubuntu0.1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1","right":"1.0-1ubuntu0.2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1","right":"1.0-1ubuntu1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1","right":"1.0-1ubuntu2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1","right":"1.0-1\u002Bdeb12u1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1","right":"1.0-1\u002Bdeb12u2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1","right":"1:1.1.1n-0\u002Bdeb11u2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1","right":"1:1.1.1n-0\u002Bdeb11u5","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1","right":"2.0~beta1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1","right":"2.0~rc1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1","right":"2.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1","right":"1.2.3-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1","right":"1.2.10-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1","right":"3.0.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.1","right":"1.0-1ubuntu0.2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.1","right":"1.0-1ubuntu1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.1","right":"1.0-1ubuntu2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.1","right":"1.0-1\u002Bdeb12u1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.1","right":"1.0-1\u002Bdeb12u2","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.1","right":"1:1.1.1n-0\u002Bdeb11u2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.1","right":"1:1.1.1n-0\u002Bdeb11u5","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.1","right":"2.0~beta1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.1","right":"2.0~rc1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.1","right":"2.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.1","right":"1.2.3-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.1","right":"1.2.10-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.1","right":"3.0.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.2","right":"1.0-1ubuntu1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.2","right":"1.0-1ubuntu2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.2","right":"1.0-1\u002Bdeb12u1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.2","right":"1.0-1\u002Bdeb12u2","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.2","right":"1:1.1.1n-0\u002Bdeb11u2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.2","right":"1:1.1.1n-0\u002Bdeb11u5","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.2","right":"2.0~beta1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.2","right":"2.0~rc1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.2","right":"2.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.2","right":"1.2.3-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.2","right":"1.2.10-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu0.2","right":"3.0.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu1","right":"1.0-1ubuntu2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu1","right":"1.0-1\u002Bdeb12u1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu1","right":"1.0-1\u002Bdeb12u2","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu1","right":"1:1.1.1n-0\u002Bdeb11u2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu1","right":"1:1.1.1n-0\u002Bdeb11u5","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu1","right":"2.0~beta1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu1","right":"2.0~rc1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu1","right":"2.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu1","right":"1.2.3-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu1","right":"1.2.10-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu1","right":"3.0.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu2","right":"1.0-1\u002Bdeb12u1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu2","right":"1.0-1\u002Bdeb12u2","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu2","right":"1:1.1.1n-0\u002Bdeb11u2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu2","right":"1:1.1.1n-0\u002Bdeb11u5","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu2","right":"2.0~beta1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu2","right":"2.0~rc1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu2","right":"2.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu2","right":"1.2.3-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu2","right":"1.2.10-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1ubuntu2","right":"3.0.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u1","right":"1.0-1\u002Bdeb12u2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u1","right":"1:1.1.1n-0\u002Bdeb11u2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u1","right":"1:1.1.1n-0\u002Bdeb11u5","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u1","right":"2.0~beta1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u1","right":"2.0~rc1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u1","right":"2.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u1","right":"1.2.3-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u1","right":"1.2.10-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u1","right":"3.0.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u2","right":"1:1.1.1n-0\u002Bdeb11u2","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u2","right":"1:1.1.1n-0\u002Bdeb11u5","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u2","right":"2.0~beta1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u2","right":"2.0~rc1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u2","right":"2.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u2","right":"1.2.3-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u2","right":"1.2.10-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.0-1\u002Bdeb12u2","right":"3.0.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1:1.1.1n-0\u002Bdeb11u2","right":"1:1.1.1n-0\u002Bdeb11u5","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1:1.1.1n-0\u002Bdeb11u2","right":"2.0~beta1-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1:1.1.1n-0\u002Bdeb11u2","right":"2.0~rc1-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1:1.1.1n-0\u002Bdeb11u2","right":"2.0-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1:1.1.1n-0\u002Bdeb11u2","right":"1.2.3-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1:1.1.1n-0\u002Bdeb11u2","right":"1.2.10-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1:1.1.1n-0\u002Bdeb11u2","right":"3.0.0-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1:1.1.1n-0\u002Bdeb11u5","right":"2.0~beta1-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1:1.1.1n-0\u002Bdeb11u5","right":"2.0~rc1-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1:1.1.1n-0\u002Bdeb11u5","right":"2.0-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1:1.1.1n-0\u002Bdeb11u5","right":"1.2.3-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1:1.1.1n-0\u002Bdeb11u5","right":"1.2.10-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1:1.1.1n-0\u002Bdeb11u5","right":"3.0.0-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"2.0~beta1-1","right":"2.0~rc1-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"2.0~beta1-1","right":"2.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"2.0~beta1-1","right":"1.2.3-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"2.0~beta1-1","right":"1.2.10-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"2.0~beta1-1","right":"3.0.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"2.0~rc1-1","right":"2.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"2.0~rc1-1","right":"1.2.3-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"2.0~rc1-1","right":"1.2.10-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"2.0~rc1-1","right":"3.0.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"2.0-1","right":"1.2.3-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"2.0-1","right":"1.2.10-1","expected":1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"2.0-1","right":"3.0.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.2.3-1","right":"1.2.10-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.2.3-1","right":"3.0.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
{"left":"1.2.10-1","right":"3.0.0-1","expected":-1,"distro":"deb","note":"pairwise"}
|
||||
@@ -0,0 +1,120 @@
|
||||
{"left":"kernel-0:4.18.0-80.el8.x86_64","right":"kernel-1:4.18.0-80.el8.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.0-80.el8.x86_64","right":"kernel-0:4.18.11-80.el8.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.0-80.el8.x86_64","right":"pkg-0:1.0-1.el9.noarch","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.0-80.el8.x86_64","right":"pkg-0:1.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.0-80.el8.x86_64","right":"pkg-0:1.0-2.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.0-80.el8.x86_64","right":"pkg-0:1.0-10.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.0-80.el8.x86_64","right":"pkg-0:1.0~rc1-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.0-80.el8.x86_64","right":"pkg-0:1.0-1.fc35.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.0-80.el8.x86_64","right":"pkg-0:1.0-1.fc36.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.0-80.el8.x86_64","right":"openssl-1:1.1.1k-7.el8.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.0-80.el8.x86_64","right":"openssl-3:1.1.1k-7.el8.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.0-80.el8.x86_64","right":"podman-1:4.5.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.0-80.el8.x86_64","right":"podman-2:4.4.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.0-80.el8.x86_64","right":"glibc-4:2.36-9.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.0-80.el8.x86_64","right":"glibc-5:2.36-8.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-1:4.18.0-80.el8.x86_64","right":"kernel-0:4.18.11-80.el8.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-1:4.18.0-80.el8.x86_64","right":"pkg-0:1.0-1.el9.noarch","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-1:4.18.0-80.el8.x86_64","right":"pkg-0:1.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-1:4.18.0-80.el8.x86_64","right":"pkg-0:1.0-2.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-1:4.18.0-80.el8.x86_64","right":"pkg-0:1.0-10.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-1:4.18.0-80.el8.x86_64","right":"pkg-0:1.0~rc1-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-1:4.18.0-80.el8.x86_64","right":"pkg-0:1.0-1.fc35.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-1:4.18.0-80.el8.x86_64","right":"pkg-0:1.0-1.fc36.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-1:4.18.0-80.el8.x86_64","right":"openssl-1:1.1.1k-7.el8.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-1:4.18.0-80.el8.x86_64","right":"openssl-3:1.1.1k-7.el8.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-1:4.18.0-80.el8.x86_64","right":"podman-1:4.5.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-1:4.18.0-80.el8.x86_64","right":"podman-2:4.4.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-1:4.18.0-80.el8.x86_64","right":"glibc-4:2.36-9.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-1:4.18.0-80.el8.x86_64","right":"glibc-5:2.36-8.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.11-80.el8.x86_64","right":"pkg-0:1.0-1.el9.noarch","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.11-80.el8.x86_64","right":"pkg-0:1.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.11-80.el8.x86_64","right":"pkg-0:1.0-2.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.11-80.el8.x86_64","right":"pkg-0:1.0-10.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.11-80.el8.x86_64","right":"pkg-0:1.0~rc1-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.11-80.el8.x86_64","right":"pkg-0:1.0-1.fc35.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.11-80.el8.x86_64","right":"pkg-0:1.0-1.fc36.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.11-80.el8.x86_64","right":"openssl-1:1.1.1k-7.el8.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.11-80.el8.x86_64","right":"openssl-3:1.1.1k-7.el8.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.11-80.el8.x86_64","right":"podman-1:4.5.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.11-80.el8.x86_64","right":"podman-2:4.4.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.11-80.el8.x86_64","right":"glibc-4:2.36-9.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"kernel-0:4.18.11-80.el8.x86_64","right":"glibc-5:2.36-8.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.noarch","right":"pkg-0:1.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.noarch","right":"pkg-0:1.0-2.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.noarch","right":"pkg-0:1.0-10.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.noarch","right":"pkg-0:1.0~rc1-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.noarch","right":"pkg-0:1.0-1.fc35.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.noarch","right":"pkg-0:1.0-1.fc36.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.noarch","right":"openssl-1:1.1.1k-7.el8.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.noarch","right":"openssl-3:1.1.1k-7.el8.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.noarch","right":"podman-1:4.5.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.noarch","right":"podman-2:4.4.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.noarch","right":"glibc-4:2.36-9.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.noarch","right":"glibc-5:2.36-8.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.x86_64","right":"pkg-0:1.0-2.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.x86_64","right":"pkg-0:1.0-10.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.x86_64","right":"pkg-0:1.0~rc1-1.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.x86_64","right":"pkg-0:1.0-1.fc35.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.x86_64","right":"pkg-0:1.0-1.fc36.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.x86_64","right":"openssl-1:1.1.1k-7.el8.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.x86_64","right":"openssl-3:1.1.1k-7.el8.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.x86_64","right":"podman-1:4.5.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.x86_64","right":"podman-2:4.4.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.x86_64","right":"glibc-4:2.36-9.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.el9.x86_64","right":"glibc-5:2.36-8.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-2.el9.x86_64","right":"pkg-0:1.0-10.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-2.el9.x86_64","right":"pkg-0:1.0~rc1-1.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-2.el9.x86_64","right":"pkg-0:1.0-1.fc35.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-2.el9.x86_64","right":"pkg-0:1.0-1.fc36.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-2.el9.x86_64","right":"openssl-1:1.1.1k-7.el8.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-2.el9.x86_64","right":"openssl-3:1.1.1k-7.el8.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-2.el9.x86_64","right":"podman-1:4.5.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-2.el9.x86_64","right":"podman-2:4.4.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-2.el9.x86_64","right":"glibc-4:2.36-9.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-2.el9.x86_64","right":"glibc-5:2.36-8.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-10.el9.x86_64","right":"pkg-0:1.0~rc1-1.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-10.el9.x86_64","right":"pkg-0:1.0-1.fc35.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-10.el9.x86_64","right":"pkg-0:1.0-1.fc36.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-10.el9.x86_64","right":"openssl-1:1.1.1k-7.el8.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-10.el9.x86_64","right":"openssl-3:1.1.1k-7.el8.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-10.el9.x86_64","right":"podman-1:4.5.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-10.el9.x86_64","right":"podman-2:4.4.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-10.el9.x86_64","right":"glibc-4:2.36-9.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-10.el9.x86_64","right":"glibc-5:2.36-8.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0~rc1-1.el9.x86_64","right":"pkg-0:1.0-1.fc35.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0~rc1-1.el9.x86_64","right":"pkg-0:1.0-1.fc36.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0~rc1-1.el9.x86_64","right":"openssl-1:1.1.1k-7.el8.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0~rc1-1.el9.x86_64","right":"openssl-3:1.1.1k-7.el8.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0~rc1-1.el9.x86_64","right":"podman-1:4.5.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0~rc1-1.el9.x86_64","right":"podman-2:4.4.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0~rc1-1.el9.x86_64","right":"glibc-4:2.36-9.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0~rc1-1.el9.x86_64","right":"glibc-5:2.36-8.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.fc35.x86_64","right":"pkg-0:1.0-1.fc36.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.fc35.x86_64","right":"openssl-1:1.1.1k-7.el8.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.fc35.x86_64","right":"openssl-3:1.1.1k-7.el8.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.fc35.x86_64","right":"podman-1:4.5.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.fc35.x86_64","right":"podman-2:4.4.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.fc35.x86_64","right":"glibc-4:2.36-9.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.fc35.x86_64","right":"glibc-5:2.36-8.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.fc36.x86_64","right":"openssl-1:1.1.1k-7.el8.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.fc36.x86_64","right":"openssl-3:1.1.1k-7.el8.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.fc36.x86_64","right":"podman-1:4.5.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.fc36.x86_64","right":"podman-2:4.4.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.fc36.x86_64","right":"glibc-4:2.36-9.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"pkg-0:1.0-1.fc36.x86_64","right":"glibc-5:2.36-8.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"openssl-1:1.1.1k-7.el8.x86_64","right":"openssl-3:1.1.1k-7.el8.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"openssl-1:1.1.1k-7.el8.x86_64","right":"podman-1:4.5.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"openssl-1:1.1.1k-7.el8.x86_64","right":"podman-2:4.4.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"openssl-1:1.1.1k-7.el8.x86_64","right":"glibc-4:2.36-9.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"openssl-1:1.1.1k-7.el8.x86_64","right":"glibc-5:2.36-8.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"openssl-3:1.1.1k-7.el8.x86_64","right":"podman-1:4.5.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"openssl-3:1.1.1k-7.el8.x86_64","right":"podman-2:4.4.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"openssl-3:1.1.1k-7.el8.x86_64","right":"glibc-4:2.36-9.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"openssl-3:1.1.1k-7.el8.x86_64","right":"glibc-5:2.36-8.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"podman-1:4.5.0-1.el9.x86_64","right":"podman-2:4.4.0-1.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"podman-1:4.5.0-1.el9.x86_64","right":"glibc-4:2.36-9.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"podman-1:4.5.0-1.el9.x86_64","right":"glibc-5:2.36-8.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"podman-2:4.4.0-1.el9.x86_64","right":"glibc-4:2.36-9.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"podman-2:4.4.0-1.el9.x86_64","right":"glibc-5:2.36-8.el9.x86_64","expected":1,"distro":"rpm","note":"pairwise"}
|
||||
{"left":"glibc-4:2.36-9.el9.x86_64","right":"glibc-5:2.36-8.el9.x86_64","expected":-1,"distro":"rpm","note":"pairwise"}
|
||||
@@ -0,0 +1,213 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text;
|
||||
using StellaOps.Concelier.Merge.Comparers;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Concelier.Merge.Tests;
|
||||
|
||||
public sealed class GoldenVersionComparisonTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[InlineData("rpm_version_comparison.golden.ndjson", "rpm")]
|
||||
[InlineData("deb_version_comparison.golden.ndjson", "deb")]
|
||||
[InlineData("apk_version_comparison.golden.ndjson", "apk")]
|
||||
public void GoldenFiles_MatchComparers(string fileName, string distro)
|
||||
{
|
||||
if (ShouldUpdateGoldens())
|
||||
{
|
||||
WriteGoldens(fileName, distro);
|
||||
return;
|
||||
}
|
||||
|
||||
var cases = LoadCases(fileName);
|
||||
Assert.True(cases.Count >= 100, $"Expected at least 100 cases in {fileName}.");
|
||||
|
||||
var failures = new List<string>();
|
||||
foreach (var testCase in cases)
|
||||
{
|
||||
var actual = distro switch
|
||||
{
|
||||
"rpm" => Math.Sign(NevraComparer.Instance.Compare(testCase.Left, testCase.Right)),
|
||||
"deb" => Math.Sign(DebianEvrComparer.Instance.Compare(testCase.Left, testCase.Right)),
|
||||
"apk" => Math.Sign(ApkVersionComparer.Instance.Compare(testCase.Left, testCase.Right)),
|
||||
_ => throw new InvalidOperationException($"Unsupported distro: {distro}")
|
||||
};
|
||||
|
||||
if (actual != testCase.Expected)
|
||||
{
|
||||
failures.Add($"FAIL {distro}: {testCase.Left} vs {testCase.Right} expected {testCase.Expected} got {actual} ({testCase.Note})");
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Empty(failures);
|
||||
}
|
||||
|
||||
private static List<GoldenComparisonCase> LoadCases(string fileName)
|
||||
{
|
||||
var path = ResolveGoldenPath(fileName);
|
||||
var lines = File.ReadAllLines(path);
|
||||
var cases = new List<GoldenComparisonCase>(lines.Length);
|
||||
|
||||
foreach (var line in lines.Where(static l => !string.IsNullOrWhiteSpace(l)))
|
||||
{
|
||||
var testCase = JsonSerializer.Deserialize<GoldenComparisonCase>(line, JsonOptions);
|
||||
if (testCase is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
cases.Add(testCase);
|
||||
}
|
||||
|
||||
return cases;
|
||||
}
|
||||
|
||||
private static string ResolveGoldenPath(string fileName)
|
||||
{
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(AppContext.BaseDirectory, "Fixtures", "Golden", fileName),
|
||||
Path.Combine(GetProjectRoot(), "Fixtures", "Golden", fileName)
|
||||
};
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
throw new FileNotFoundException($"Golden file '{fileName}' not found.", fileName);
|
||||
}
|
||||
|
||||
private static string GetProjectRoot()
|
||||
=> Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", ".."));
|
||||
|
||||
private static void WriteGoldens(string fileName, string distro)
|
||||
{
|
||||
var cases = BuildCases(distro);
|
||||
var path = Path.Combine(GetProjectRoot(), "Fixtures", "Golden", fileName);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
|
||||
var lines = cases.Select(testCase => JsonSerializer.Serialize(testCase, JsonOptions));
|
||||
File.WriteAllText(path, string.Join('\n', lines), Encoding.ASCII);
|
||||
}
|
||||
|
||||
private static List<GoldenComparisonCase> BuildCases(string distro)
|
||||
{
|
||||
string[] versions;
|
||||
Func<string, string, int> comparer;
|
||||
|
||||
switch (distro)
|
||||
{
|
||||
case "rpm":
|
||||
versions =
|
||||
[
|
||||
"kernel-0:4.18.0-80.el8.x86_64",
|
||||
"kernel-1:4.18.0-80.el8.x86_64",
|
||||
"kernel-0:4.18.11-80.el8.x86_64",
|
||||
"pkg-0:1.0-1.el9.noarch",
|
||||
"pkg-0:1.0-1.el9.x86_64",
|
||||
"pkg-0:1.0-2.el9.x86_64",
|
||||
"pkg-0:1.0-10.el9.x86_64",
|
||||
"pkg-0:1.0~rc1-1.el9.x86_64",
|
||||
"pkg-0:1.0-1.fc35.x86_64",
|
||||
"pkg-0:1.0-1.fc36.x86_64",
|
||||
"openssl-1:1.1.1k-7.el8.x86_64",
|
||||
"openssl-3:1.1.1k-7.el8.x86_64",
|
||||
"podman-1:4.5.0-1.el9.x86_64",
|
||||
"podman-2:4.4.0-1.el9.x86_64",
|
||||
"glibc-4:2.36-9.el9.x86_64",
|
||||
"glibc-5:2.36-8.el9.x86_64"
|
||||
];
|
||||
comparer = (left, right) => Math.Sign(NevraComparer.Instance.Compare(left, right));
|
||||
break;
|
||||
case "deb":
|
||||
versions =
|
||||
[
|
||||
"0:1.0-1",
|
||||
"1.0-1",
|
||||
"1.0-1ubuntu0.1",
|
||||
"1.0-1ubuntu0.2",
|
||||
"1.0-1ubuntu1",
|
||||
"1.0-1ubuntu2",
|
||||
"1.0-1+deb12u1",
|
||||
"1.0-1+deb12u2",
|
||||
"1:1.1.1n-0+deb11u2",
|
||||
"1:1.1.1n-0+deb11u5",
|
||||
"2.0~beta1-1",
|
||||
"2.0~rc1-1",
|
||||
"2.0-1",
|
||||
"1.2.3-1",
|
||||
"1.2.10-1",
|
||||
"3.0.0-1"
|
||||
];
|
||||
comparer = (left, right) => Math.Sign(DebianEvrComparer.Instance.Compare(left, right));
|
||||
break;
|
||||
case "apk":
|
||||
versions =
|
||||
[
|
||||
"1.0_alpha1-r0",
|
||||
"1.0_beta1-r0",
|
||||
"1.0_pre1-r0",
|
||||
"1.0_rc1-r0",
|
||||
"1.0-r0",
|
||||
"1.0_p1-r0",
|
||||
"1.2.3-r0",
|
||||
"1.2.10-r0",
|
||||
"2.0-r0",
|
||||
"1.2.3a-r0",
|
||||
"1.2.3b-r0",
|
||||
"1.2.3-r1",
|
||||
"1.2.3-r2",
|
||||
"1.2.3_p1-r1",
|
||||
"3.9.1-r0",
|
||||
"3.1.1-r0"
|
||||
];
|
||||
comparer = (left, right) => Math.Sign(ApkVersionComparer.Instance.Compare(left, right));
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unsupported distro: {distro}");
|
||||
}
|
||||
|
||||
var cases = new List<GoldenComparisonCase>(versions.Length * versions.Length);
|
||||
for (var i = 0; i < versions.Length; i++)
|
||||
{
|
||||
for (var j = i + 1; j < versions.Length; j++)
|
||||
{
|
||||
var left = versions[i];
|
||||
var right = versions[j];
|
||||
cases.Add(new GoldenComparisonCase(left, right, comparer(left, right), distro, "pairwise"));
|
||||
}
|
||||
}
|
||||
|
||||
return cases;
|
||||
}
|
||||
|
||||
private static bool ShouldUpdateGoldens()
|
||||
=> IsTruthy(Environment.GetEnvironmentVariable("UPDATE_GOLDENS"))
|
||||
|| IsTruthy(Environment.GetEnvironmentVariable("DOTNET_TEST_UPDATE_GOLDENS"));
|
||||
|
||||
private static bool IsTruthy(string? value)
|
||||
=> !string.IsNullOrWhiteSpace(value)
|
||||
&& (string.Equals(value, "1", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private sealed record GoldenComparisonCase(
|
||||
string Left,
|
||||
string Right,
|
||||
int Expected,
|
||||
string? Distro,
|
||||
string? Note);
|
||||
}
|
||||
@@ -105,4 +105,81 @@ public sealed class NevraComparerTests
|
||||
var actual = Math.Sign(NevraComparer.Instance.Compare(left, right));
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string, int, string> ComparisonCases => BuildComparisonCases();
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ComparisonCases))]
|
||||
public void Compare_NevraVersions_ReturnsExpectedOrder(string left, string right, int expected, string note)
|
||||
{
|
||||
var actual = Math.Sign(NevraComparer.Instance.Compare(left, right));
|
||||
Assert.True(expected == actual, $"[{note}] '{left}' vs '{right}': expected {expected}, got {actual}");
|
||||
}
|
||||
|
||||
private static TheoryData<string, string, int, string> BuildComparisonCases()
|
||||
{
|
||||
var data = new TheoryData<string, string, int, string>();
|
||||
|
||||
// Epoch precedence.
|
||||
data.Add("kernel-0:4.18.0-80.el8.x86_64", "kernel-1:4.18.0-80.el8.x86_64", -1, "epoch precedence: 0 < 1");
|
||||
data.Add("kernel-2:4.18.0-80.el8.x86_64", "kernel-1:9.9.9-1.el8.x86_64", 1, "epoch precedence: 2 > 1");
|
||||
data.Add("openssl-1:1.1.1k-7.el8.x86_64", "openssl-3:1.1.1k-7.el8.x86_64", -1, "epoch precedence: 1 < 3");
|
||||
data.Add("bash-10:5.1.0-1.el9.x86_64", "bash-9:9.9.9-1.el9.x86_64", 1, "epoch precedence: 10 > 9");
|
||||
data.Add("podman-1:4.5.0-1.el9.x86_64", "podman-2:4.4.0-1.el9.x86_64", -1, "epoch precedence: 1 < 2");
|
||||
data.Add("glibc-5:2.36-8.el9.x86_64", "glibc-4:2.36-9.el9.x86_64", 1, "epoch precedence: 5 > 4");
|
||||
|
||||
// Numeric ordering.
|
||||
for (var i = 1; i <= 10; i++)
|
||||
{
|
||||
data.Add($"pkg-0:1.{i}-1.el9.x86_64", $"pkg-0:1.{i + 1}-1.el9.x86_64", -1, "numeric segment ordering");
|
||||
}
|
||||
|
||||
data.Add("pkg-0:1.9-1.el9.x86_64", "pkg-0:1.10-1.el9.x86_64", -1, "numeric length ordering");
|
||||
data.Add("pkg-0:1.02-1.el9.x86_64", "pkg-0:1.2-1.el9.x86_64", 0, "leading zeros ignored");
|
||||
data.Add("pkg-0:1.002-1.el9.x86_64", "pkg-0:1.2-1.el9.x86_64", 0, "leading zeros ignored");
|
||||
|
||||
// Alpha ordering.
|
||||
data.Add("pkg-0:1.0a-1.el9.x86_64", "pkg-0:1.0b-1.el9.x86_64", -1, "alpha segment ordering");
|
||||
data.Add("pkg-0:1.0aa-1.el9.x86_64", "pkg-0:1.0b-1.el9.x86_64", -1, "alpha length ordering");
|
||||
data.Add("pkg-0:1.0b-1.el9.x86_64", "pkg-0:1.0aa-1.el9.x86_64", 1, "alpha segment ordering");
|
||||
data.Add("pkg-0:1.0a-1.el9.x86_64", "pkg-0:1.0-1.el9.x86_64", 1, "alpha sorts after empty");
|
||||
data.Add("pkg-0:1.0-1.el9.x86_64", "pkg-0:1.0a-1.el9.x86_64", -1, "empty sorts before alpha");
|
||||
data.Add("pkg-0:1.0z-1.el9.x86_64", "pkg-0:1.0aa-1.el9.x86_64", 1, "alpha lexical ordering");
|
||||
|
||||
// Tilde pre-releases.
|
||||
data.Add("pkg-0:1.0~rc1-1.el9.x86_64", "pkg-0:1.0-1.el9.x86_64", -1, "tilde sorts before release");
|
||||
data.Add("pkg-0:1.0~rc1-1.el9.x86_64", "pkg-0:1.0~rc2-1.el9.x86_64", -1, "tilde rc ordering");
|
||||
data.Add("pkg-0:1.0~~-1.el9.x86_64", "pkg-0:1.0~-1.el9.x86_64", -1, "double tilde sorts earlier");
|
||||
data.Add("pkg-0:1.0~beta-1.el9.x86_64", "pkg-0:1.0~rc-1.el9.x86_64", -1, "tilde alpha segment ordering");
|
||||
data.Add("pkg-0:1.0~rc-1.el9.x86_64", "pkg-0:1.0~rc-1.el9.x86_64", 0, "tilde equivalence");
|
||||
data.Add("pkg-0:1.0~rc-1.el9.x86_64", "pkg-0:1.0~rc-2.el9.x86_64", -1, "release breaks tilde ties");
|
||||
|
||||
// Release qualifiers and backports.
|
||||
data.Add("pkg-0:1.0-1.el8.x86_64", "pkg-0:1.0-1.el9.x86_64", -1, "release qualifier el8 < el9");
|
||||
data.Add("pkg-0:1.0-1.el8.x86_64", "pkg-0:1.0-1.el8_5.x86_64", -1, "backport suffix ordering");
|
||||
data.Add("pkg-0:1.0-1.el8_5.x86_64", "pkg-0:1.0-1.el8_5.1.x86_64", -1, "incremental backport");
|
||||
data.Add("pkg-0:1.0-1.el8_5.1.x86_64", "pkg-0:1.0-2.el8.x86_64", -1, "release increments beat base");
|
||||
data.Add("pkg-0:1.0-2.el8.x86_64", "pkg-0:1.0-10.el8.x86_64", -1, "release numeric ordering");
|
||||
data.Add("pkg-0:1.0-10.el8.x86_64", "pkg-0:1.0-2.el8.x86_64", 1, "release numeric ordering");
|
||||
data.Add("pkg-0:1.0-1.fc35.x86_64", "pkg-0:1.0-1.fc36.x86_64", -1, "release qualifier fc35 < fc36");
|
||||
data.Add("pkg-0:1.0-1.el8_5.x86_64", "pkg-0:1.0-1.el8_5.0.x86_64", -1, "zero suffix still sorts later");
|
||||
data.Add("pkg-0:1.0-1.el8_5.0.x86_64", "pkg-0:1.0-1.el8_5.x86_64", 1, "zero suffix still sorts later");
|
||||
data.Add("pkg-0:1.0-1.el8_5.1.x86_64", "pkg-0:1.0-1.el8_5.2.x86_64", -1, "backport numeric ordering");
|
||||
|
||||
// Architecture ordering.
|
||||
data.Add("pkg-0:1.0-1.el9.noarch", "pkg-0:1.0-1.el9.x86_64", -1, "architecture lexical ordering");
|
||||
data.Add("pkg-0:1.0-1.el9.aarch64", "pkg-0:1.0-1.el9.x86_64", -1, "architecture lexical ordering");
|
||||
data.Add("pkg-0:1.0-1.el9.ppc64le", "pkg-0:1.0-1.el9.ppc64", 1, "architecture lexical ordering");
|
||||
data.Add("pkg-0:1.0-1.el9.s390x", "pkg-0:1.0-1.el9.s390", 1, "architecture lexical ordering");
|
||||
data.Add("pkg-0:1.0-1.el9.arm64", "pkg-0:1.0-1.el9.aarch64", 1, "architecture lexical ordering");
|
||||
|
||||
// Package name ordering.
|
||||
data.Add("aaa-0:1.0-1.el9.x86_64", "bbb-0:1.0-1.el9.x86_64", -1, "name lexical ordering");
|
||||
data.Add("openssl-0:1.0-1.el9.x86_64", "openssl-libs-0:1.0-1.el9.x86_64", -1, "name lexical ordering");
|
||||
data.Add("zlib-0:1.0-1.el9.x86_64", "bzip2-0:1.0-1.el9.x86_64", 1, "name lexical ordering");
|
||||
data.Add("kernel-0:1.0-1.el9.x86_64", "kernel-core-0:1.0-1.el9.x86_64", -1, "name lexical ordering");
|
||||
data.Add("glibc-0:1.0-1.el9.x86_64", "glibc-devel-0:1.0-1.el9.x86_64", -1, "name lexical ordering");
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Concelier Merge Tests
|
||||
|
||||
This project verifies distro version comparison logic and merge rules.
|
||||
|
||||
## Layout
|
||||
- Comparer unit tests: `*.Tests.cs` in this project (RPM, Debian, APK).
|
||||
- Golden fixtures: `Fixtures/Golden/*.golden.ndjson`.
|
||||
- Integration cross-checks: `src/Concelier/__Tests/StellaOps.Concelier.Integration.Tests`.
|
||||
|
||||
## Golden files
|
||||
Golden files capture pairwise comparison results in NDJSON.
|
||||
See `Fixtures/Golden/README.md` for format and regeneration steps.
|
||||
|
||||
## Integration tests
|
||||
Cross-check tests compare container-installed versions against fixed
|
||||
versions using the same comparers. They require Docker/Testcontainers.
|
||||
|
||||
Enable with:
|
||||
`$env:STELLAOPS_INTEGRATION_TESTS = "true"`
|
||||
|
||||
Run (from repo root):
|
||||
`dotnet test src/Concelier/__Tests/StellaOps.Concelier.Integration.Tests/StellaOps.Concelier.Integration.Tests.csproj`
|
||||
@@ -10,4 +10,9 @@
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
|
||||
<ProjectReference Include="../../__Libraries/StellaOps.Concelier.Normalization/StellaOps.Concelier.Normalization.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
<ItemGroup>
|
||||
<None Update="Fixtures\Golden\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Concelier Merge Comparator Test Tasks
|
||||
|
||||
Local status mirror for `docs/implplan/SPRINT_2000_0003_0002_distro_version_tests.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| T1 | DONE | NEVRA comparison corpus expanded. |
|
||||
| T2 | DONE | Debian EVR comparison corpus expanded. |
|
||||
| T3 | DOING | Golden NDJSON fixtures + regression runner. |
|
||||
| T4 | TODO | Testcontainers real-image cross-checks. |
|
||||
| T5 | TODO | Test corpus README. |
|
||||
|
||||
Last synced: 2025-12-22 (UTC).
|
||||
@@ -71,4 +71,26 @@ public sealed class AffectedVersionRangeExtensionsTests
|
||||
|
||||
Assert.Null(rule);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToNormalizedVersionRule_FallsBackForApkRange()
|
||||
{
|
||||
var range = new AffectedVersionRange(
|
||||
rangeKind: "apk",
|
||||
introducedVersion: null,
|
||||
fixedVersion: "3.1.4-r0",
|
||||
lastAffectedVersion: null,
|
||||
rangeExpression: "fixed:3.1.4-r0",
|
||||
provenance: AdvisoryProvenance.Empty,
|
||||
primitives: null);
|
||||
|
||||
var rule = range.ToNormalizedVersionRule("alpine:v3.20/main");
|
||||
|
||||
Assert.NotNull(rule);
|
||||
Assert.Equal(NormalizedVersionSchemes.Apk, rule!.Scheme);
|
||||
Assert.Equal(NormalizedVersionRuleTypes.LessThan, rule.Type);
|
||||
Assert.Equal("3.1.4-r0", rule.Max);
|
||||
Assert.False(rule.MaxInclusive);
|
||||
Assert.Equal("alpine:v3.20/main", rule.Notes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using StellaOps.Concelier.Normalization.Distro;
|
||||
|
||||
namespace StellaOps.Concelier.Normalization.Tests;
|
||||
|
||||
public sealed class ApkVersionParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToCanonicalString_RoundTripsExplicitPkgRel()
|
||||
{
|
||||
var parsed = ApkVersion.Parse(" 3.1.4-r0 ");
|
||||
|
||||
Assert.Equal("3.1.4-r0", parsed.Original);
|
||||
Assert.Equal("3.1.4-r0", parsed.ToCanonicalString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToCanonicalString_SuppressesImplicitPkgRel()
|
||||
{
|
||||
var parsed = ApkVersion.Parse("1.2.3_alpha");
|
||||
|
||||
Assert.Equal("1.2.3_alpha", parsed.ToCanonicalString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryParse_TracksExplicitRelease()
|
||||
{
|
||||
var success = ApkVersion.TryParse("2.0.1-r5", out var parsed);
|
||||
|
||||
Assert.True(success);
|
||||
Assert.NotNull(parsed);
|
||||
Assert.True(parsed!.HasExplicitPkgRel);
|
||||
Assert.Equal(5, parsed.PkgRel);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user