## Summary Advanced Search results came back in a different order every search, whether or not a sort was chosen. Follow-up to #2625, where it was noticed while testing the property test. ## Why Results arrive from the search workers in whatever order they finished. Each sort the gump offers compares a single key and `Array.Sort` is not stable, so equal keys, which is most rows under type or map, landed in a different order every run; with no sort chosen the list was arrival order outright. The range sort also answered 0 for any two results off the viewer's map, so those shuffled as well. ## What changed - The five comparers share one base class whose `Compare` applies the chosen key and then a fixed tie-break, serial ascending regardless of direction. The direction is about the key; a fixed order among equal keys is what keeps two searches identical. - The collected result list is put in serial order before the gump sees it, so an unsorted search is deterministic too. That sort runs on the thread-pool work item that already assembles the list and reads only the result records. - No change to which key each sort uses or to what ascending and descending mean. ## Test plan - [x] New `AdvancedSearchResultOrderTests`: six equal-keyed results offered in two arrival orders to every comparer in both directions, asserting the same sequence and serial order; the range comparer on off-map results; reverse flips the key but not the tie-break. - [x] `UOContent.Tests`: 873 passed, 0 failed - [x] `Server.Tests`: 869 passed, 0 failed - [ ] In game: run the same search twice with each sort and with none; the lists match.
132 lines
3.9 KiB
C#
132 lines
3.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Server;
|
|
using Server.Engines.AdvancedSearch;
|
|
using Server.Items;
|
|
using Xunit;
|
|
|
|
namespace UOContent.Tests;
|
|
|
|
// Every sort must give the same sequence from any arrival order: key first, then serial.
|
|
[Collection("Sequential UOContent Tests")]
|
|
public class AdvancedSearchResultOrderTests : IDisposable
|
|
{
|
|
private readonly List<Item> _items = [];
|
|
|
|
public void Dispose()
|
|
{
|
|
for (var i = 0; i < _items.Count; i++)
|
|
{
|
|
_items[i].Delete();
|
|
}
|
|
|
|
_items.Clear();
|
|
}
|
|
|
|
private AdvancedSearchResult Result(Item item, string name = "same", Map map = null)
|
|
{
|
|
_items.Add(item);
|
|
return new AdvancedSearchResult(name, item.GetType(), item.Location, map ?? Map.Felucca, null) { Entity = item };
|
|
}
|
|
|
|
private AdvancedSearchResult[] EqualKeyed()
|
|
{
|
|
var results = new AdvancedSearchResult[6];
|
|
|
|
for (var i = 0; i < results.Length; i++)
|
|
{
|
|
results[i] = Result(new Item(0x1));
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private static AdvancedSearchResult[] Shuffled(AdvancedSearchResult[] inOrder, params int[] permutation)
|
|
{
|
|
var shuffled = new AdvancedSearchResult[inOrder.Length];
|
|
|
|
for (var i = 0; i < permutation.Length; i++)
|
|
{
|
|
shuffled[i] = inOrder[permutation[i]];
|
|
}
|
|
|
|
return shuffled;
|
|
}
|
|
|
|
private static void AssertSameSequenceFromAnyArrival(
|
|
AdvancedSearchResult[] inOrder,
|
|
IComparer<AdvancedSearchResult> comparer
|
|
)
|
|
{
|
|
var first = Shuffled(inOrder, 3, 0, 5, 1, 4, 2);
|
|
var second = Shuffled(inOrder, 5, 4, 3, 2, 1, 0);
|
|
|
|
Array.Sort(first, comparer);
|
|
Array.Sort(second, comparer);
|
|
|
|
Assert.Equal(first, second);
|
|
}
|
|
|
|
public static IEnumerable<object[]> Comparers()
|
|
{
|
|
yield return [AdvancedSearchResultSerialComparer.Instance];
|
|
yield return [AdvancedSearchResultTypeComparer.Instance];
|
|
yield return [AdvancedSearchResultTypeComparer.InstanceReverse];
|
|
yield return [AdvancedSearchResultNameComparer.Instance];
|
|
yield return [AdvancedSearchResultNameComparer.InstanceReverse];
|
|
yield return [AdvancedSearchResultMapComparer.Instance];
|
|
yield return [AdvancedSearchResultMapComparer.InstanceReverse];
|
|
yield return [AdvancedSearchResultSelectedComparer.Instance];
|
|
yield return [AdvancedSearchResultSelectedComparer.InstanceReverse];
|
|
}
|
|
|
|
[Theory]
|
|
[MemberData(nameof(Comparers))]
|
|
public void EqualKeysOrderBySerialFromAnyArrivalOrder(IComparer<AdvancedSearchResult> comparer)
|
|
{
|
|
var inOrder = EqualKeyed();
|
|
|
|
AssertSameSequenceFromAnyArrival(inOrder, comparer);
|
|
|
|
var sorted = Shuffled(inOrder, 3, 0, 5, 1, 4, 2);
|
|
Array.Sort(sorted, comparer);
|
|
|
|
Assert.Equal(inOrder, sorted);
|
|
}
|
|
|
|
// Off-map results compare equal on range.
|
|
[Fact]
|
|
public void RangeComparerOrdersOffMapResultsBySerial()
|
|
{
|
|
var from = new Mobile();
|
|
from.MoveToWorld(new Point3D(1000, 1000, 0), Map.Trammel);
|
|
|
|
try
|
|
{
|
|
var inOrder = EqualKeyed();
|
|
|
|
AssertSameSequenceFromAnyArrival(inOrder, new AdvancedSearchRangeComparer(from));
|
|
AssertSameSequenceFromAnyArrival(inOrder, new AdvancedSearchRangeComparer(from, true));
|
|
}
|
|
finally
|
|
{
|
|
from.Delete();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ReverseFlipsTheKeyNotTheTieBreak()
|
|
{
|
|
var a1 = Result(new Item(0x1), "alpha");
|
|
var a2 = Result(new Item(0x1), "alpha");
|
|
var b1 = Result(new Item(0x1), "beta");
|
|
|
|
var results = new[] { b1, a2, a1 };
|
|
|
|
Array.Sort(results, AdvancedSearchResultNameComparer.Instance);
|
|
Assert.Equal([a1, a2, b1], results);
|
|
|
|
Array.Sort(results, AdvancedSearchResultNameComparer.InstanceReverse);
|
|
Assert.Equal([b1, a1, a2], results);
|
|
}
|
|
}
|