using XFEEDToolBox.Core.Galaxy; using XFEEDToolBox.Core.Models; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public class GalaxyCellStreamingTests { private static GalaxyCellQueryPlan Plan(double z = 5, Coordinates? origin = null) { var centre = new Coordinates(5, 5, z); return new(GalaxyCamera.Create(centre, 1600, 900, 0, 0, 4), GalaxyOriginalCatalogueLod.Create(centre, 1, 500, .001, 1, 0), origin ?? new(0, 0, 0)); } private static GalacticSystem Star(ulong cell, Coordinates? position = null) => new("Fixture " + cell, position ?? new(9, 9, 9), (long)(cell | (1UL << (44 - 3 * (int)(cell & 7))))); private static GalaxyRegionResult Receipt(IReadOnlyList cells, IReadOnlyList? stars = null) => new(stars ?? cells.Select(c => Star(c)).ToArray(), (int)cells.Min(c => c & 7), cells.Count, cells.Count, false, true) { Scope = "requested-cells", RequestedCells = cells.Count, Origin = new(0, 0, 0) }; private static async Task> Collect(GalaxyCellStreamingSession session, GalaxyCellQueryPlan plan, Func, IReadOnlyList, CancellationToken, Task> query) { var updates = new List(); await foreach (var update in session.StreamAsync(plan, query)) updates.Add(update); return updates; } [Test] public void ReceiptProvesOnlyTheExactRequestedMassCells() { var actual = Receipt(new ulong[] { 7 }); Check.That(GalaxyCellStreamingSession.CoversRequestedCells(actual, 1)); Check.That(!actual.IsFullyCovered, "A requested mass-seven cell is not evidence of a whole all-mass region."); Check.That(!GalaxyCellStreamingSession.CoversRequestedCells(actual, 2)); Check.That(!GalaxyCellStreamingSession.CoversRequestedCells(actual with { Scope = "region" }, 1)); Check.That(!GalaxyCellStreamingSession.CoversRequestedCells(actual with { RequestedCells = -1 }, 1)); Check.That(!GalaxyCellStreamingSession.CoversRequestedCells(actual with { FailedCells = 1 }, 1)); } [Test] public async Task CappedBatchesSplitCellListsWithoutClippingOrDuplicatingEdgeStars() { using var session = new GalaxyCellStreamingSession(); var plan = Plan(); Task Query(Coordinates centre, IReadOnlyList radii, IReadOnlyList cells, CancellationToken token) { var result = Receipt(cells); return Task.FromResult(cells.Count > 1 ? result with { Systems = [Star(cells[0])], Truncated = true, Complete = false } : result); } var updates = await Collect(session, plan, Query); var rows = updates.SelectMany(u => u.Systems).ToArray(); Check.That(updates[^1].CoverageComplete && updates[^1].RefinedBatches > 0); Check.Equal(plan.Cells.Count, rows.Length); Check.Equal(rows.Length, rows.Select(s => s.SystemAddress).Distinct().Count()); Check.That(rows.Any(s => s.Position.DistanceTo(plan.Descriptor.Centre) > plan.Descriptor.InnerRadius)); var repeated = await Collect(session, plan, (_, _, _, _) => throw new Exception("Complete cell should be cached")); Check.That(repeated[^1].CoverageComplete && repeated.Any(u => u.FromCache)); } [Test] public async Task UnknownAndFailedSingleCellsRemainRetryableInsteadOfBecomingEmptyCoverage() { using var session = new GalaxyCellStreamingSession(new() { BatchSize = 1 }); var plan = Plan(); var first = await Collect(session, plan, (_, _, cells, _) => Task.FromResult(new GalaxyRegionResult([], (int)(cells[0] & 7), 1, 1){Origin=new(0,0,0)})); Check.That(!first[^1].CoverageComplete && first[^1].UnverifiedCells == plan.Cells.Count); var retried = 0; var second = await Collect(session, plan, (_, _, cells, _) => { retried++; return Task.FromResult(Receipt(cells, [])); }); Check.That(second[^1].CoverageComplete && retried == plan.Cells.Count); } [Test] public async Task OffRequestAndConflictingRowsCannotPoisonTheCellCache() { using var session = new GalaxyCellStreamingSession(); var plan = Plan(); var rejected = false; try { await Collect(session, plan, (_, _, cells, _) => Task.FromResult(Receipt(cells, [Star(GalaxyCellQueryPlan.Encode(0, 10, 10, 10))]))); } catch (InvalidDataException) { rejected = true; } Check.That(rejected && session.CachedCells == 0); rejected = false; try { await Collect(session, plan, (_, _, cells, _) => Task.FromResult(Receipt(cells) with { Origin = new(1, 2, 3) })); } catch (InvalidDataException) { rejected = true; } Check.That(rejected && session.CachedCells == 0); rejected = false; try { await Collect(session, plan, (_, _, cells, _) => Task.FromResult(Receipt(cells, [Star(cells[0]), Star(cells[0], new(2, 3, 4))]))); } catch (InvalidDataException) { rejected = true; } Check.That(rejected && session.CachedCells == 0); } [Test] public async Task LatestDisjointViewCancelsOldWorkAndNeverPublishesItsRows() { using var session = new GalaxyCellStreamingSession(); var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); async Task Old(Coordinates centre, IReadOnlyList radii, IReadOnlyList cells, CancellationToken token) { started.SetResult(); await Task.Delay(Timeout.Infinite, token); return Receipt(cells); } await using var old = session.StreamAsync(Plan(), Old).GetAsyncEnumerator(); var oldMove = old.MoveNextAsync().AsTask(); await started.Task.WaitAsync(TimeSpan.FromSeconds(3)); var latest = await Collect(session, Plan(10005), (_, _, cells, _) => Task.FromResult(Receipt(cells, []))); var cancelled = false; try { await oldMove; } catch (OperationCanceledException) { cancelled = true; } Check.That(cancelled && latest[^1].CoverageComplete && latest.All(u => u.Systems.Count == 0)); } [Test] public async Task UsefulInflightCellsCanFinishForTheNewViewWithoutStalePublication() { using var session = new GalaxyCellStreamingSession(); var plan = Plan(); var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); async Task Query(Coordinates centre, IReadOnlyList radii, IReadOnlyList cells, CancellationToken token) { started.SetResult(); await release.Task.WaitAsync(token); return Receipt(cells); } await using var old = session.StreamAsync(plan, Query).GetAsyncEnumerator(); var oldMove = old.MoveNextAsync().AsTask(); await started.Task.WaitAsync(TimeSpan.FromSeconds(3)); var latestTask = Collect(session, plan, (_, _, _, _) => throw new Exception("Useful accepted cells should be reused")); release.SetResult(); var latest = await latestTask; var cancelled = false; try { await oldMove; } catch (OperationCanceledException) { cancelled = true; } Check.That(cancelled && latest[^1].CoverageComplete && latest.Any(u => u.FromCache && u.Systems.Count > 0)); } [Test] public async Task CoordinateOriginIsPartOfTheCacheIdentityAndEvictionRemainsBounded() { using var session = new GalaxyCellStreamingSession(new() { MaximumCachedCells = 2, MaximumCachedSystems = 2 }); await Collect(session, Plan(), (_, _, cells, _) => Task.FromResult(Receipt(cells))); Check.That(session.CachedCells <= 2); var queried = 0; await Collect(session, Plan(origin: new(100, 100, 100)), (_, _, cells, _) => { queried++; return Task.FromResult(Receipt(cells) with {Origin=new(100,100,100)}); }); Check.That(queried > 0 && session.CachedCells <= 2); } [Test] public async Task PartlyOverlappingPanKeepsUsefulAcceptedCellsAndQueriesOnlyTheNewOnes() { using var session = new GalaxyCellStreamingSession(); var first = Plan(); var next = Plan(15); var oldIds = first.Cells.Select(c => c.Address).ToHashSet(); var newIds = next.Cells.Select(c => c.Address).ToHashSet(); Check.That(oldIds.Overlaps(newIds) && !oldIds.SetEquals(newIds)); var started = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); async Task Original(Coordinates centre, IReadOnlyList radii, IReadOnlyList cells, CancellationToken token) { started.SetResult(); await release.Task.WaitAsync(token); return Receipt(cells); } await using var old = session.StreamAsync(first, Original).GetAsyncEnumerator(); var oldMove = old.MoveNextAsync().AsTask(); await started.Task.WaitAsync(TimeSpan.FromSeconds(3)); var queried = new List(); var latestTask = Collect(session, next, (_, _, cells, _) => { queried.AddRange(cells); return Task.FromResult(Receipt(cells)); }); release.SetResult(); var latest = await latestTask; var cancelled = false; try { await oldMove; } catch (OperationCanceledException) { cancelled = true; } Check.That(cancelled && latest[^1].CoverageComplete && latest.Any(u => u.FromCache)); Check.That(queried.ToHashSet().SetEquals(newIds.Except(oldIds))); } }