sprints work
This commit is contained in:
@@ -0,0 +1,599 @@
|
||||
// <copyright file="ReachabilityIndexIntegrationTests.cs" company="StellaOps">
|
||||
// Copyright (c) StellaOps. Licensed under the AGPL-3.0-or-later.
|
||||
// </copyright>
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Reachability.Core.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for <see cref="ReachabilityIndex"/> verifying end-to-end
|
||||
/// behavior with mock adapters that simulate real data sources.
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class ReachabilityIndexIntegrationTests : IDisposable
|
||||
{
|
||||
private readonly ServiceProvider _serviceProvider;
|
||||
private readonly IReachabilityIndex _reachabilityIndex;
|
||||
private readonly MockReachGraphAdapter _reachGraphAdapter;
|
||||
private readonly MockSignalsAdapter _signalsAdapter;
|
||||
private readonly FakeTimeProvider _timeProvider;
|
||||
|
||||
public ReachabilityIndexIntegrationTests()
|
||||
{
|
||||
_reachGraphAdapter = new MockReachGraphAdapter();
|
||||
_signalsAdapter = new MockSignalsAdapter();
|
||||
_timeProvider = new FakeTimeProvider(DateTimeOffset.Parse("2026-01-10T10:00:00Z"));
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton<IReachGraphAdapter>(_reachGraphAdapter);
|
||||
services.AddSingleton<ISignalsAdapter>(_signalsAdapter);
|
||||
services.AddSingleton<TimeProvider>(_timeProvider);
|
||||
services.AddSingleton<IReachabilityIndex, ReachabilityIndex>();
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
_reachabilityIndex = _serviceProvider.GetRequiredService<IReachabilityIndex>();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_serviceProvider.Dispose();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Static Query Tests
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task QueryStaticAsync_ReturnsReachableResult_WhenSymbolInGraph()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = new SymbolRef("pkg:npm/lodash@4.17.21", "lodash.merge", "JavaScript");
|
||||
var artifactDigest = "sha256:abc123";
|
||||
|
||||
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
|
||||
pathCount: 2,
|
||||
shortestPath: 3,
|
||||
entrypoints: ["server.handleRequest", "api.process"]);
|
||||
|
||||
// Act
|
||||
var result = await _reachabilityIndex.QueryStaticAsync(symbol, artifactDigest, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.IsReachable.Should().BeTrue();
|
||||
result.PathCount.Should().Be(2);
|
||||
result.ShortestPathLength.Should().Be(3);
|
||||
result.Entrypoints.Should().HaveCount(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryStaticAsync_ReturnsUnreachable_WhenSymbolNotInGraph()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = new SymbolRef("pkg:npm/unused@1.0.0", "unused.func", "JavaScript");
|
||||
var artifactDigest = "sha256:abc123";
|
||||
|
||||
_reachGraphAdapter.SetupUnreachableSymbol(symbol, artifactDigest);
|
||||
|
||||
// Act
|
||||
var result = await _reachabilityIndex.QueryStaticAsync(symbol, artifactDigest, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.IsReachable.Should().BeFalse();
|
||||
result.PathCount.Should().Be(0);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Runtime Query Tests
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task QueryRuntimeAsync_ReturnsObserved_WhenSignalExists()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = new SymbolRef("pkg:npm/express@4.18.0", "express.Router", "JavaScript");
|
||||
var artifactDigest = "sha256:def456";
|
||||
var observationWindow = TimeSpan.FromDays(7);
|
||||
|
||||
_signalsAdapter.SetupObservedSymbol(symbol, artifactDigest,
|
||||
hitCount: 42,
|
||||
firstSeen: _timeProvider.GetUtcNow().AddDays(-5),
|
||||
lastSeen: _timeProvider.GetUtcNow().AddMinutes(-30));
|
||||
|
||||
// Act
|
||||
var result = await _reachabilityIndex.QueryRuntimeAsync(
|
||||
symbol, artifactDigest, observationWindow, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.WasObserved.Should().BeTrue();
|
||||
result.HitCount.Should().Be(42);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryRuntimeAsync_ReturnsNotObserved_WhenNoSignals()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = new SymbolRef("pkg:npm/dead-code@1.0.0", "deadCode.func", "JavaScript");
|
||||
var artifactDigest = "sha256:def456";
|
||||
var observationWindow = TimeSpan.FromDays(7);
|
||||
|
||||
_signalsAdapter.SetupNotObservedSymbol(symbol, artifactDigest);
|
||||
|
||||
// Act
|
||||
var result = await _reachabilityIndex.QueryRuntimeAsync(
|
||||
symbol, artifactDigest, observationWindow, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.WasObserved.Should().BeFalse();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Hybrid Query Tests
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task QueryHybridAsync_CombinesStaticAndRuntime()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = new SymbolRef("pkg:npm/active@1.0.0", "active.process", "JavaScript");
|
||||
var artifactDigest = "sha256:hybrid123";
|
||||
|
||||
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
|
||||
pathCount: 1, shortestPath: 2, entrypoints: ["main"]);
|
||||
|
||||
_signalsAdapter.SetupObservedSymbol(symbol, artifactDigest,
|
||||
hitCount: 100,
|
||||
firstSeen: _timeProvider.GetUtcNow().AddDays(-7),
|
||||
lastSeen: _timeProvider.GetUtcNow().AddMinutes(-5));
|
||||
|
||||
var options = new HybridQueryOptions
|
||||
{
|
||||
IncludeStatic = true,
|
||||
IncludeRuntime = true,
|
||||
ObservationWindow = TimeSpan.FromDays(7)
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _reachabilityIndex.QueryHybridAsync(
|
||||
symbol, artifactDigest, options, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.LatticeState.Should().Be(LatticeState.RuntimeObserved);
|
||||
result.StaticResult.Should().NotBeNull();
|
||||
result.RuntimeResult.Should().NotBeNull();
|
||||
result.Confidence.Should().BeGreaterOrEqualTo(0.8);
|
||||
result.Verdict.VexStatus.Should().Be("affected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryHybridAsync_StaticReachableButNotObserved_ReturnsStaticReachable()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = new SymbolRef("pkg:npm/potential@1.0.0", "potential.risk", "JavaScript");
|
||||
var artifactDigest = "sha256:hybrid456";
|
||||
|
||||
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
|
||||
pathCount: 1, shortestPath: 2, entrypoints: ["startup"]);
|
||||
|
||||
_signalsAdapter.SetupNotObservedSymbol(symbol, artifactDigest);
|
||||
|
||||
var options = new HybridQueryOptions
|
||||
{
|
||||
IncludeStatic = true,
|
||||
IncludeRuntime = true,
|
||||
ObservationWindow = TimeSpan.FromDays(7)
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _reachabilityIndex.QueryHybridAsync(
|
||||
symbol, artifactDigest, options, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.LatticeState.Should().Be(LatticeState.StaticReachable);
|
||||
result.StaticResult!.IsReachable.Should().BeTrue();
|
||||
result.RuntimeResult!.WasObserved.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryHybridAsync_NotReachableAndNotObserved_ReturnsNotAffected()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = new SymbolRef("pkg:npm/safe@1.0.0", "safe.unused", "JavaScript");
|
||||
var artifactDigest = "sha256:safe789";
|
||||
|
||||
_reachGraphAdapter.SetupUnreachableSymbol(symbol, artifactDigest);
|
||||
_signalsAdapter.SetupNotObservedSymbol(symbol, artifactDigest);
|
||||
|
||||
var options = new HybridQueryOptions
|
||||
{
|
||||
IncludeStatic = true,
|
||||
IncludeRuntime = true,
|
||||
ObservationWindow = TimeSpan.FromDays(30)
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _reachabilityIndex.QueryHybridAsync(
|
||||
symbol, artifactDigest, options, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.LatticeState.Should().BeOneOf(
|
||||
LatticeState.StaticUnreachable,
|
||||
LatticeState.RuntimeNotObserved);
|
||||
result.Verdict.VexStatus.Should().Be("not_affected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryHybridAsync_StaticOnlyMode()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = new SymbolRef("pkg:npm/static-only@1.0.0", "staticOnly.func", "JavaScript");
|
||||
var artifactDigest = "sha256:static";
|
||||
|
||||
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
|
||||
pathCount: 1, shortestPath: 1, entrypoints: ["app"]);
|
||||
|
||||
var options = new HybridQueryOptions
|
||||
{
|
||||
IncludeStatic = true,
|
||||
IncludeRuntime = false,
|
||||
ObservationWindow = TimeSpan.Zero
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _reachabilityIndex.QueryHybridAsync(
|
||||
symbol, artifactDigest, options, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.StaticResult.Should().NotBeNull();
|
||||
result.RuntimeResult.Should().BeNull();
|
||||
result.LatticeState.Should().Be(LatticeState.StaticReachable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryHybridAsync_RuntimeOnlyMode()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = new SymbolRef("pkg:npm/runtime-only@1.0.0", "runtimeOnly.func", "JavaScript");
|
||||
var artifactDigest = "sha256:runtime";
|
||||
|
||||
_signalsAdapter.SetupObservedSymbol(symbol, artifactDigest,
|
||||
hitCount: 50,
|
||||
firstSeen: _timeProvider.GetUtcNow().AddHours(-1),
|
||||
lastSeen: _timeProvider.GetUtcNow());
|
||||
|
||||
var options = new HybridQueryOptions
|
||||
{
|
||||
IncludeStatic = false,
|
||||
IncludeRuntime = true,
|
||||
ObservationWindow = TimeSpan.FromHours(24)
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _reachabilityIndex.QueryHybridAsync(
|
||||
symbol, artifactDigest, options, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.StaticResult.Should().BeNull();
|
||||
result.RuntimeResult.Should().NotBeNull();
|
||||
result.LatticeState.Should().Be(LatticeState.RuntimeObserved);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Evidence URI Integration Tests
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task QueryHybridAsync_GeneratesValidEvidenceBundle()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = new SymbolRef("pkg:npm/uri-test@1.0.0", "uriTest.check", "JavaScript");
|
||||
var artifactDigest = "sha256:uri123";
|
||||
|
||||
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
|
||||
pathCount: 1, shortestPath: 2, entrypoints: ["main"]);
|
||||
|
||||
_signalsAdapter.SetupObservedSymbol(symbol, artifactDigest,
|
||||
hitCount: 5,
|
||||
firstSeen: _timeProvider.GetUtcNow().AddHours(-1),
|
||||
lastSeen: _timeProvider.GetUtcNow());
|
||||
|
||||
var options = new HybridQueryOptions
|
||||
{
|
||||
IncludeStatic = true,
|
||||
IncludeRuntime = true,
|
||||
ObservationWindow = TimeSpan.FromHours(24)
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _reachabilityIndex.QueryHybridAsync(
|
||||
symbol, artifactDigest, options, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Evidence.Should().NotBeNull();
|
||||
result.Evidence.Uris.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Content Digest Tests (Determinism)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task QueryHybridAsync_SameInput_ProducesSameContentDigest()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = new SymbolRef("pkg:npm/deterministic@1.0.0", "det.func", "JavaScript");
|
||||
var artifactDigest = "sha256:det123";
|
||||
|
||||
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
|
||||
pathCount: 1, shortestPath: 1, entrypoints: ["entry"]);
|
||||
_signalsAdapter.SetupNotObservedSymbol(symbol, artifactDigest);
|
||||
|
||||
var options = new HybridQueryOptions
|
||||
{
|
||||
IncludeStatic = true,
|
||||
IncludeRuntime = true,
|
||||
ObservationWindow = TimeSpan.FromDays(1)
|
||||
};
|
||||
|
||||
// Act
|
||||
var result1 = await _reachabilityIndex.QueryHybridAsync(
|
||||
symbol, artifactDigest, options, CancellationToken.None);
|
||||
var result2 = await _reachabilityIndex.QueryHybridAsync(
|
||||
symbol, artifactDigest, options, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result1.ContentDigest.Should().Be(result2.ContentDigest);
|
||||
result1.ContentDigest.Should().StartWith("sha256:");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Cancellation Tests
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task QueryHybridAsync_ThrowsOnCancellation()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = new SymbolRef("pkg:npm/cancel@1.0.0", "cancel.func", "JavaScript");
|
||||
var artifactDigest = "sha256:cancel";
|
||||
var cts = new CancellationTokenSource();
|
||||
await cts.CancelAsync();
|
||||
|
||||
var options = new HybridQueryOptions
|
||||
{
|
||||
IncludeStatic = true,
|
||||
IncludeRuntime = true,
|
||||
ObservationWindow = TimeSpan.FromHours(1)
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(() =>
|
||||
_reachabilityIndex.QueryHybridAsync(symbol, artifactDigest, options, cts.Token));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Edge Cases
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public async Task QueryStaticAsync_HandlesSpecialCharactersInSymbol()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = new SymbolRef(
|
||||
"pkg:npm/%40scope%2Fpackage@1.0.0",
|
||||
"SomeClass<T>.Method(string, int)",
|
||||
"CSharp");
|
||||
var artifactDigest = "sha256:special";
|
||||
|
||||
_reachGraphAdapter.SetupReachableSymbol(symbol, artifactDigest,
|
||||
pathCount: 1, shortestPath: 1, entrypoints: ["entry"]);
|
||||
|
||||
// Act
|
||||
var result = await _reachabilityIndex.QueryStaticAsync(symbol, artifactDigest, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.IsReachable.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task QueryHybridAsync_HandlesEmptyOptions()
|
||||
{
|
||||
// Arrange
|
||||
var symbol = new SymbolRef("pkg:npm/empty@1.0.0", "empty.func", "JavaScript");
|
||||
var artifactDigest = "sha256:empty";
|
||||
|
||||
var options = new HybridQueryOptions
|
||||
{
|
||||
IncludeStatic = false,
|
||||
IncludeRuntime = false,
|
||||
ObservationWindow = TimeSpan.Zero
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _reachabilityIndex.QueryHybridAsync(
|
||||
symbol, artifactDigest, options, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.LatticeState.Should().Be(LatticeState.Unknown);
|
||||
result.StaticResult.Should().BeNull();
|
||||
result.RuntimeResult.Should().BeNull();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Mock Adapters
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Mock implementation of <see cref="IReachGraphAdapter"/> for testing.
|
||||
/// </summary>
|
||||
internal sealed class MockReachGraphAdapter : IReachGraphAdapter
|
||||
{
|
||||
private readonly Dictionary<string, StaticReachabilityResult> _results = new();
|
||||
private readonly FakeTimeProvider _timeProvider = new(DateTimeOffset.UtcNow);
|
||||
|
||||
public void SetupReachableSymbol(
|
||||
SymbolRef symbol,
|
||||
string artifactDigest,
|
||||
int pathCount,
|
||||
int shortestPath,
|
||||
string[] entrypoints)
|
||||
{
|
||||
var key = MakeKey(symbol, artifactDigest);
|
||||
_results[key] = new StaticReachabilityResult
|
||||
{
|
||||
Symbol = symbol,
|
||||
ArtifactDigest = artifactDigest,
|
||||
IsReachable = true,
|
||||
PathCount = pathCount,
|
||||
ShortestPathLength = shortestPath,
|
||||
Entrypoints = entrypoints.ToImmutableArray(),
|
||||
AnalyzedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
public void SetupUnreachableSymbol(SymbolRef symbol, string artifactDigest)
|
||||
{
|
||||
var key = MakeKey(symbol, artifactDigest);
|
||||
_results[key] = new StaticReachabilityResult
|
||||
{
|
||||
Symbol = symbol,
|
||||
ArtifactDigest = artifactDigest,
|
||||
IsReachable = false,
|
||||
PathCount = 0,
|
||||
AnalyzedAt = _timeProvider.GetUtcNow()
|
||||
};
|
||||
}
|
||||
|
||||
public Task<StaticReachabilityResult> QueryAsync(SymbolRef symbol, string artifactDigest, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var key = MakeKey(symbol, artifactDigest);
|
||||
|
||||
if (_results.TryGetValue(key, out var result))
|
||||
{
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
return Task.FromResult(new StaticReachabilityResult
|
||||
{
|
||||
Symbol = symbol,
|
||||
ArtifactDigest = artifactDigest,
|
||||
IsReachable = false,
|
||||
PathCount = 0,
|
||||
AnalyzedAt = _timeProvider.GetUtcNow()
|
||||
});
|
||||
}
|
||||
|
||||
private static string MakeKey(SymbolRef symbol, string artifactDigest)
|
||||
=> $"{symbol.Purl}:{symbol.Symbol}:{artifactDigest}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock implementation of <see cref="ISignalsAdapter"/> for testing.
|
||||
/// </summary>
|
||||
internal sealed class MockSignalsAdapter : ISignalsAdapter
|
||||
{
|
||||
private readonly Dictionary<string, (long hitCount, DateTimeOffset firstSeen, DateTimeOffset lastSeen)> _observations = new();
|
||||
private readonly FakeTimeProvider _timeProvider = new(DateTimeOffset.UtcNow);
|
||||
|
||||
public void SetupObservedSymbol(
|
||||
SymbolRef symbol,
|
||||
string artifactDigest,
|
||||
long hitCount,
|
||||
DateTimeOffset firstSeen,
|
||||
DateTimeOffset lastSeen)
|
||||
{
|
||||
var key = MakeKey(symbol, artifactDigest);
|
||||
_observations[key] = (hitCount, firstSeen, lastSeen);
|
||||
}
|
||||
|
||||
public void SetupNotObservedSymbol(SymbolRef symbol, string artifactDigest)
|
||||
{
|
||||
// Don't add to dictionary - will return not observed
|
||||
}
|
||||
|
||||
public Task<RuntimeReachabilityResult> QueryAsync(
|
||||
SymbolRef symbol,
|
||||
string artifactDigest,
|
||||
TimeSpan observationWindow,
|
||||
string? tenantId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
var key = MakeKey(symbol, artifactDigest);
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var windowStart = now - observationWindow;
|
||||
|
||||
if (_observations.TryGetValue(key, out var obs) && obs.lastSeen >= windowStart)
|
||||
{
|
||||
return Task.FromResult(new RuntimeReachabilityResult
|
||||
{
|
||||
Symbol = symbol,
|
||||
ArtifactDigest = artifactDigest,
|
||||
WasObserved = true,
|
||||
ObservationWindow = observationWindow,
|
||||
WindowStart = windowStart,
|
||||
WindowEnd = now,
|
||||
HitCount = obs.hitCount,
|
||||
FirstSeen = obs.firstSeen,
|
||||
LastSeen = obs.lastSeen
|
||||
});
|
||||
}
|
||||
|
||||
return Task.FromResult(new RuntimeReachabilityResult
|
||||
{
|
||||
Symbol = symbol,
|
||||
ArtifactDigest = artifactDigest,
|
||||
WasObserved = false,
|
||||
ObservationWindow = observationWindow,
|
||||
WindowStart = windowStart,
|
||||
WindowEnd = now,
|
||||
HitCount = 0
|
||||
});
|
||||
}
|
||||
|
||||
private static string MakeKey(SymbolRef symbol, string artifactDigest)
|
||||
=> $"{symbol.Purl}:{symbol.Symbol}:{artifactDigest}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fake <see cref="TimeProvider"/> for deterministic time in tests.
|
||||
/// </summary>
|
||||
internal sealed class FakeTimeProvider : TimeProvider
|
||||
{
|
||||
private DateTimeOffset _now;
|
||||
|
||||
public FakeTimeProvider(DateTimeOffset initialTime)
|
||||
{
|
||||
_now = initialTime;
|
||||
}
|
||||
|
||||
public override DateTimeOffset GetUtcNow() => _now;
|
||||
|
||||
public void Advance(TimeSpan duration)
|
||||
{
|
||||
_now = _now.Add(duration);
|
||||
}
|
||||
|
||||
public void SetTime(DateTimeOffset time)
|
||||
{
|
||||
_now = time;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user