Skip to content

Improves tests of Xtensive.Sorting.TopologicalSorter #354

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
381 changes: 249 additions & 132 deletions Orm/Xtensive.Orm.Tests.Core/Helpers/TopologicalSorterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,145 +16,262 @@

namespace Xtensive.Orm.Tests.Core.Helpers
{
[TestFixture]
public class TopologicalSorterTest
[TestFixture]
public class TopologicalSorterTest
{
[Test, Explicit]
public void PerformanceTest()
{
[Test, Explicit]
public void PerformanceTest()
{
using (TestLog.InfoRegion("No loops")) {
InternalPerformanceTest(10000, 10, false);
InternalPerformanceTest(100, 10, false);
InternalPerformanceTest(1000, 10, false);
InternalPerformanceTest(10000, 10, false);
InternalPerformanceTest(100000, 10, false);
}
TestLog.Info("");
using (TestLog.InfoRegion("With loop removal")) {
InternalPerformanceTest(10000, 10, true);
InternalPerformanceTest(100, 10, true);
InternalPerformanceTest(1000, 10, true);
InternalPerformanceTest(10000, 10, true);
InternalPerformanceTest(100000, 10, true);
}
}
using (TestLog.InfoRegion("No loops")) {
InternalPerformanceTest(10000, 10, false);
InternalPerformanceTest(100, 10, false);
InternalPerformanceTest(1000, 10, false);
InternalPerformanceTest(10000, 10, false);
InternalPerformanceTest(100000, 10, false);
}
TestLog.Info("");
using (TestLog.InfoRegion("With loop removal")) {
InternalPerformanceTest(10000, 10, true);
InternalPerformanceTest(100, 10, true);
InternalPerformanceTest(1000, 10, true);
InternalPerformanceTest(10000, 10, true);
InternalPerformanceTest(100000, 10, true);
}
}

private static void InternalPerformanceTest(int nodeCount, int averageConnectionCount, bool allowLoops)
{
TestLog.Info("Building graph: {0} nodes, {1} connections/node in average.", nodeCount, averageConnectionCount);
var rnd = new Random();
var nodes = new List<Node<int, int>>();
for (int i = 0; i < nodeCount; i++)
nodes.Add(new Node<int, int>(i));
int connectionCount = 0;
foreach (var from in nodes) {
int outgoingConnectionCount = rnd.Next(averageConnectionCount);
for (int i = 0; i < outgoingConnectionCount; i++) {
var to = nodes[rnd.Next(allowLoops ? nodeCount : @from.Item)];
if (from==to)
continue;
var c = new NodeConnection<int, int>(@from, to, connectionCount++);
c.BindToNodes();
}
}

GC.GetTotalMemory(true);
using (new Measurement("Sorting", nodeCount + connectionCount)) {
List<Node<int, int>> removedEdges;
var result = TopologicalSorter.Sort(nodes, out removedEdges);
if (!allowLoops)
Assert.AreEqual(nodeCount, result.Count);
}
GC.GetTotalMemory(true);
}
private static void InternalPerformanceTest(int nodeCount, int averageConnectionCount, bool allowLoops)
{
TestLog.Info("Building graph: {0} nodes, {1} connections/node in average.", nodeCount, averageConnectionCount);
var rnd = new Random();
var nodes = new List<Node<int, int>>();
for (var i = 0; i < nodeCount; i++) {
nodes.Add(new Node<int, int>(i));
}

[Test]
public void SelfReferenceTest()
{
var node = new Node<int, string>(1);
var connection = new NodeConnection<int, string>(node, node, "ConnectionItem");
connection.BindToNodes();

List<NodeConnection<int, string>> removedEdges;
List<int> result = TopologicalSorter.Sort(EnumerableUtils.One(node), out removedEdges);
Assert.AreEqual(1, result.Count);
Assert.AreEqual(node.Item, result[0]);
Assert.AreEqual(1, removedEdges.Count);
Assert.AreEqual(connection, removedEdges[0]);
int connectionCount = 0;
foreach (var from in nodes) {
var outgoingConnectionCount = rnd.Next(averageConnectionCount);
for (var i = 0; i < outgoingConnectionCount; i++) {
var to = nodes[rnd.Next(allowLoops ? nodeCount : @from.Item)];
if (from == to)
continue;
var c = new NodeConnection<int, int>(@from, to, connectionCount++);
c.BindToNodes();
}
}

[Test]
public void RemoveWholeNodeTest()
{
var node1 = new Node<int, string>(1);
var node2 = new Node<int, string>(2);
var connection12_1 = new NodeConnection<int, string>(node1, node2, "ConnectionItem 1->2 1");
connection12_1.BindToNodes();
var connection12_2 = new NodeConnection<int, string>(node1, node2, "ConnectionItem 1->2 2");
connection12_2.BindToNodes();
var connection21_1 = new NodeConnection<int, string>(node2, node1, "ConnectionItem 2->1 1");
connection21_1.BindToNodes();

// Remove edge by edge.

List<NodeConnection<int, string>> removedEdges;
List<int> result = TopologicalSorter.Sort(new[] {node2, node1}, out removedEdges);
Assert.AreEqual(2, result.Count);
Assert.AreEqual(node1.Item, result[0]);
Assert.AreEqual(node2.Item, result[1]);

Assert.AreEqual(1, removedEdges.Count);
Assert.AreEqual(connection21_1, removedEdges[0]);

// Remove whole node
connection12_1.BindToNodes();
connection12_2.BindToNodes();
connection21_1.BindToNodes();

result = TopologicalSorter.Sort(new[] {node2, node1}, out removedEdges, true);
Assert.AreEqual(2, result.Count);
Assert.AreEqual(node1.Item, result[1]);
Assert.AreEqual(node2.Item, result[0]);

Assert.AreEqual(2, removedEdges.Count);
Assert.AreEqual(0, removedEdges.Except(new[] {connection12_1, connection12_2}).Count());
}
_ = GC.GetTotalMemory(true);
using (new Measurement("Sorting", nodeCount + connectionCount)) {
var result = TopologicalSorter.Sort(nodes, out var _);
if (!allowLoops)
Assert.AreEqual(nodeCount, result.Count);
}
_ = GC.GetTotalMemory(true);
}

[Test]
public void CombinedTest()
{
TestSort(new[] {4, 3, 2, 1}, (i1, i2) => !(i1 == 3 || i2 == 3), null, new[] {4, 2, 1});
TestSort(new[] {3, 2, 1}, (i1, i2) => i1 >= i2, new[] {1, 2, 3}, null);
TestSort(new[] {3, 2, 1}, (i1, i2) => true, null, new[] {1, 2, 3});
TestSort(new[] {3, 2, 1}, (i1, i2) => false, new[] {3, 2, 1}, null);
}
[Test]
public void SelfReferenceTest()
{
var node = new Node<int, string>(1);
var connection = new NodeConnection<int, string>(node, node, "ConnectionItem");
connection.BindToNodes();

var result = TopologicalSorter.Sort(EnumerableUtils.One(node), out var removedEdges);
Assert.AreEqual(1, result.Count);
Assert.AreEqual(node.Item, result[0]);
Assert.AreEqual(1, removedEdges.Count);
Assert.AreEqual(connection, removedEdges[0]);
}

[Test]
public void NullNodeCollectionTest()
{
_ = Assert.Throws<ArgumentNullException>(() => TopologicalSorter.Sort((IEnumerable<Node<int, string>>) null, out var _));
_ = Assert.Throws<ArgumentNullException>(() => TopologicalSorter.Sort((IEnumerable<Node<int, string>>) null, out var _, false));
_ = Assert.Throws<ArgumentNullException>(() => TopologicalSorter.Sort((IEnumerable<Node<int, string>>) null, out var _, true));
_ = Assert.Throws<ArgumentNullException>(() => TopologicalSorter.Sort((List<Node<int, int>>) null, out _));
}

[Test]
public void EmptyNodeCollectionTest()
{
_ = TopologicalSorter.Sort(Enumerable.Empty<Node<int, string>>(), out _);
_ = TopologicalSorter.Sort(Enumerable.Empty<Node<int, string>>(), out _, false);
_ = TopologicalSorter.Sort(Enumerable.Empty<Node<int, string>>(), out _, true);
_ = TopologicalSorter.Sort(new List<Node<int, int>>(), out _);
}

[Test]
public void FullCircleTest()
{
var nodes = new List<Node<int, int>>();
for (var i = 0; i < 3; i++) {
nodes.Add(new Node<int, int>(i));
}

var c = new NodeConnection<int, int>(nodes[0], nodes[1], 1);
c.BindToNodes();
c = new NodeConnection<int, int>(nodes[1], nodes[2], 2);
c.BindToNodes();
c = new NodeConnection<int, int>(nodes[2], nodes[0], 3);
c.BindToNodes();

var result = TopologicalSorter.Sort(nodes, out var removedEdges);
Assert.That(result, Is.Null);
}

[Test]
public void RemoveWholeNodeTest()
{
var node1 = new Node<int, string>(1);
var node2 = new Node<int, string>(2);
var connection12_1 = new NodeConnection<int, string>(node1, node2, "ConnectionItem 1->2 1");
connection12_1.BindToNodes();
var connection12_2 = new NodeConnection<int, string>(node1, node2, "ConnectionItem 1->2 2");
connection12_2.BindToNodes();
var connection21_1 = new NodeConnection<int, string>(node2, node1, "ConnectionItem 2->1 1");
connection21_1.BindToNodes();

// Remove edge by edge.
var result = TopologicalSorter.Sort(new[] { node2, node1 }, out var removedEdges);
Assert.AreEqual(2, result.Count);
Assert.AreEqual(node1.Item, result[0]);
Assert.AreEqual(node2.Item, result[1]);

Assert.AreEqual(1, removedEdges.Count);
Assert.AreEqual(connection21_1, removedEdges[0]);

// Remove whole node
connection12_1.BindToNodes();
connection12_2.BindToNodes();
connection21_1.BindToNodes();

result = TopologicalSorter.Sort(new[] { node2, node1 }, out removedEdges, true);
Assert.AreEqual(2, result.Count);
Assert.AreEqual(node1.Item, result[1]);
Assert.AreEqual(node2.Item, result[0]);

Assert.AreEqual(2, removedEdges.Count);
Assert.AreEqual(0, removedEdges.Except(new[] { connection12_1, connection12_2 }).Count());
}

[Test]
public void CombinedTest()
{
TestSortLoopsCheck(new[] { 4, 3, 2, 1 }, (i1, i2) => !(i1 == 3 || i2 == 3), null, new[] { 4, 2, 1 });
TestSortLoopsCheck(new[] { 3, 2, 1 }, (i1, i2) => i1 >= i2, new[] { 3, 2, 1 }, null);
TestSortLoopsCheck(new[] { 3, 2, 1 }, (i1, i2) => true, null, new[] { 1, 2, 3 });
TestSortLoopsCheck(new[] { 3, 2, 1 }, (i1, i2) => false, new[] { 3, 2, 1 }, null);
TestSortLoopsCheck(Array.Empty<int>(), (i1, i2) => true, Array.Empty<int>(), null);
TestSortLoopsCheck(Array.Empty<int>(), (i1, i2) => false, Array.Empty<int>(), null);
_ = Assert.Throws<ArgumentNullException>(() => TestSortLoopsCheck<int>(null, (i1, i2) => true, null, null));
_ = Assert.Throws<ArgumentNullException>(() => TestSortLoopsCheck<int>(null, (i1, i2) => false, null, null));

TestSortNoLoopsCheck(new[] { 4, 3, 2, 1 }, (i1, i2) => !(i1 == 3 || i2 == 3), null);
TestSortNoLoopsCheck(new[] { 3, 2, 1 }, (i1, i2) => i1 >= i2, new[] { 3, 2, 1 });
TestSortNoLoopsCheck(new[] { 3, 2, 1 }, (i1, i2) => true, null);
TestSortNoLoopsCheck(new[] { 3, 2, 1 }, (i1, i2) => false, new[] { 3, 2, 1 });
TestSortNoLoopsCheck(Array.Empty<int>(), (i1, i2) => true, Array.Empty<int>());
TestSortNoLoopsCheck(Array.Empty<int>(), (i1, i2) => false, Array.Empty<int>());
_ = Assert.Throws<ArgumentNullException>(() => TestSortNoLoopsCheck<int>(null, (i1, i2) => true, null));
_ = Assert.Throws<ArgumentNullException>(() => TestSortNoLoopsCheck<int>(null, (i1, i2) => false, null));

TestEdgeRemoval(new[] { 4, 3, 2, 1 }, (i1, i2) => !(i1 == 3 || i2 == 3), new[] { 3, 1, 2, 4 }, new[] { (4, 2), (4, 1), (2, 1) });
TestEdgeRemoval(new[] { 3, 2, 1 }, (i1, i2) => i1 >= i2, new[] { 3, 2, 1 }, null);
TestEdgeRemoval(new[] { 3, 2, 1 }, (i1, i2) => true, new[] { 1, 2, 3 }, new[] { (3, 2), (2, 1), (3, 1) });
TestEdgeRemoval(new[] { 3, 2, 1 }, (i1, i2) => false, new[] { 3, 2, 1 }, null);

TestEdgeRemovalWithNode(new[] { 4, 3, 2, 1 }, (i1, i2) => !(i1 == 3 || i2 == 3), new[] { 3, 1, 2, 4 }, new[] { (4, 2), (4, 1), (2, 1) });
TestEdgeRemovalWithNode(new[] { 3, 2, 1 }, (i1, i2) => i1 >= i2, new[] { 3, 2, 1 }, null);
TestEdgeRemovalWithNode(new[] { 3, 2, 1 }, (i1, i2) => true, new[] { 1, 2, 3 }, new[] { (3, 2), (2, 1), (3, 1) });
TestEdgeRemovalWithNode(new[] { 3, 2, 1 }, (i1, i2) => false, new[] { 3, 2, 1 }, null);
}

private void TestSort<T>(T[] data, Predicate<T, T> connector, T[] expected, T[] loops)
{
List<Node<T, object>> actualLoopNodes;
List<T> actual = TopologicalSorter.Sort(data, connector, out actualLoopNodes);
T[] actualLoops = null;
if (actualLoopNodes != null)
actualLoops = actualLoopNodes
.Where(n => n.OutgoingConnectionCount != 0)
.Select(n => n.Item)
.ToArray();

AssertEx.HasSameElements(expected, actual);
AssertEx.HasSameElements(loops, actualLoops);

List<NodeConnection<T, object>> removedEdges;
List<T> sortWithRemove = TopologicalSorter.Sort(data, connector, out removedEdges);
Assert.AreEqual(sortWithRemove.Count, data.Length);
if (loops == null) {
Assert.AreEqual(sortWithRemove.Count, actual.Count);
for (int i = 0; i < actual.Count; i++) {
Assert.AreEqual(sortWithRemove[i], actual[i]);
}
}
else {
TestLog.Debug("Loops detected");
}
private void TestSortLoopsCheck<T>(T[] data, Predicate<T, T> connector, T[] expected, T[] loops)
{
var actual = TopologicalSorter.Sort(data, connector, out List<Node<T, object>> actualLoopNodes);

if (expected == null)
Assert.That(actual, Is.Null);
else if (data.Length == 0)
Assert.That(actual, Is.Empty);
else
Assert.That(expected.SequenceEqual(actual));

var actualLoops = actualLoopNodes != null
? actualLoopNodes
.Where(n => n.OutgoingConnectionCount != 0)
.Select(n => n.Item)
.ToArray()
: null;

AssertEx.HasSameElements(loops, actualLoops);

var sortWithRemove = TopologicalSorter.Sort(data, connector, out List<NodeConnection<T, object>> removedEdges);
Assert.AreEqual(sortWithRemove.Count, data.Length);

if (loops == null) {
Assert.AreEqual(sortWithRemove.Count, actual.Count);
for (var i = 0; i < actual.Count; i++) {
Assert.AreEqual(sortWithRemove[i], actual[i]);
}
}
else {
TestLog.Debug("Loops detected");
}
}

private void TestSortNoLoopsCheck<T>(T[] data, Predicate<T, T> connector, T[] expected)
{
var actual = TopologicalSorter.Sort(data, connector);

if (expected == null)
Assert.That(actual, Is.Null);
else if (data.Length == 0)
Assert.That(actual, Is.Empty);
else
Assert.That(expected.SequenceEqual(actual));

var sortWithRemove = TopologicalSorter.Sort(data, connector, out List<NodeConnection<T, object>> _);
Assert.AreEqual(sortWithRemove.Count, data.Length);
}

private void TestEdgeRemoval<T>(T[] data, Predicate<T, T> connector, T[] expected, (T source, T target)[] expectedRemovedEdges)
{
var sortWithRemove = TopologicalSorter.Sort(data, connector, out List<NodeConnection<T, object>> removedEdges);
Assert.That(sortWithRemove, Is.Not.Null);
Assert.AreEqual(sortWithRemove.Count, data.Length);
Assert.That(sortWithRemove.SequenceEqual(expected), Is.True);

if (expectedRemovedEdges == null) {
Assert.That(removedEdges, Is.Empty);
}

foreach (var removedEdge in removedEdges) {
var s = removedEdge.Source.Item;
var t = removedEdge.Destination.Item;
(T source, T target) expectedTuple = (s, t);
Assert.That(expectedRemovedEdges.Contains(expectedTuple), Is.True, $"({s} -> {t}) is not represented in expected edges");
}
}

private void TestEdgeRemovalWithNode<T>(T[] data, Predicate<T, T> connector, T[] expected, (T source, T target)[] expectedRemovedEdges)
{
var sortWithRemove = TopologicalSorter.Sort(data, connector, out var removedEdges, true);
Assert.That(sortWithRemove, Is.Not.Null);
Assert.AreEqual(sortWithRemove.Count, data.Length);
Assert.That(sortWithRemove.SequenceEqual(expected), Is.True);

if (expectedRemovedEdges == null) {
Assert.That(removedEdges, Is.Empty);
}

foreach (var removedEdge in removedEdges) {
var s = removedEdge.Source.Item;
var t = removedEdge.Destination.Item;
(T source, T target) expectedTuple = (s, t);
Assert.That(expectedRemovedEdges.Contains(expectedTuple), Is.True, $"({s} -> {t}) is not represented in expected edges");
}
}
}
}
Loading