Add ElkSharp hybrid routing and edge refinement tests
- Hybrid iterative routing parity tests (deterministic replay, Sugiyama stability, no regression in violation counts) - Edge refinement tests for hybrid mode - Document processing scenario updates for rendering changes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,11 +16,54 @@ public partial class DocumentProcessingWorkflowRenderingTests
|
||||
{
|
||||
var graph = BuildDocumentProcessingWorkflowGraph();
|
||||
var engine = new ElkSharpWorkflowRenderLayoutEngine();
|
||||
var outputDir = Path.Combine(
|
||||
Path.GetDirectoryName(typeof(DocumentProcessingWorkflowRenderingTests).Assembly.Location)!,
|
||||
"TestResults", "workflow-renderings", DateTime.Today.ToString("yyyyMMdd"), "DocumentProcessingWorkflow", "layout-only");
|
||||
Directory.CreateDirectory(outputDir);
|
||||
var captureDiagnostics = string.Equals(
|
||||
Environment.GetEnvironmentVariable("STELLAOPS_ELKSHARP_CAPTURE_LAYOUT_DIAGNOSTICS"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
var progressLogPath = Path.Combine(outputDir, "elksharp.progress.log");
|
||||
if (File.Exists(progressLogPath))
|
||||
{
|
||||
File.Delete(progressLogPath);
|
||||
}
|
||||
|
||||
var diagnosticsPath = Path.Combine(outputDir, "elksharp.refinement-diagnostics.json");
|
||||
if (File.Exists(diagnosticsPath))
|
||||
{
|
||||
File.Delete(diagnosticsPath);
|
||||
}
|
||||
|
||||
using var diagnosticsCapture = captureDiagnostics
|
||||
? ElkLayoutDiagnostics.BeginCapture()
|
||||
: null;
|
||||
if (diagnosticsCapture is not null)
|
||||
{
|
||||
diagnosticsCapture.Diagnostics.ProgressLogPath = progressLogPath;
|
||||
diagnosticsCapture.Diagnostics.SnapshotPath = diagnosticsPath;
|
||||
}
|
||||
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
var layout = await engine.LayoutAsync(graph, new WorkflowRenderLayoutRequest
|
||||
{
|
||||
Direction = WorkflowRenderLayoutDirection.LeftToRight,
|
||||
});
|
||||
stopwatch.Stop();
|
||||
|
||||
if (diagnosticsCapture is not null)
|
||||
{
|
||||
await File.WriteAllTextAsync(
|
||||
diagnosticsPath,
|
||||
JsonSerializer.Serialize(diagnosticsCapture.Diagnostics, new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
TestContext.Out.WriteLine($"Layout-only wall time: {stopwatch.Elapsed.TotalSeconds:F2}s");
|
||||
if (diagnosticsCapture is not null)
|
||||
{
|
||||
TestContext.Out.WriteLine($"Layout-only diagnostics: {diagnosticsPath}");
|
||||
TestContext.Out.WriteLine($"Layout-only progress log: {progressLogPath}");
|
||||
}
|
||||
|
||||
Assert.That(layout.Nodes.Count, Is.EqualTo(24));
|
||||
Assert.That(layout.Edges.Count, Is.EqualTo(36));
|
||||
@@ -926,4 +969,4 @@ public partial class DocumentProcessingWorkflowRenderingTests
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
using StellaOps.ElkSharp;
|
||||
|
||||
namespace StellaOps.Workflow.Renderer.Tests;
|
||||
|
||||
[TestFixture]
|
||||
public partial class ElkSharpEdgeRefinementTests
|
||||
{
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public async Task LayoutAsync_WhenHybridDeterministicModeRenderedTwice_ShouldProduceDeterministicGeometry()
|
||||
{
|
||||
var graph = BuildElkSharpStressGraph();
|
||||
var engine = new ElkSharpLayeredLayoutEngine();
|
||||
var options = CreateHybridOptions();
|
||||
|
||||
var first = await engine.LayoutAsync(graph, options);
|
||||
var second = await engine.LayoutAsync(graph, options);
|
||||
|
||||
JsonSerializer.Serialize(first).Should().Be(JsonSerializer.Serialize(second));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public async Task LayoutAsync_WhenHybridDeterministicModeEnabled_ShouldKeepNodeGeometryStableAgainstLegacy()
|
||||
{
|
||||
var graph = BuildElkSharpStressGraph();
|
||||
var engine = new ElkSharpLayeredLayoutEngine();
|
||||
|
||||
var legacy = await engine.LayoutAsync(graph, CreateLegacyOptions());
|
||||
var hybrid = await engine.LayoutAsync(graph, CreateHybridOptions());
|
||||
|
||||
JsonSerializer.Serialize(legacy.Nodes.OrderBy(node => node.Id, StringComparer.Ordinal).ToArray())
|
||||
.Should()
|
||||
.Be(JsonSerializer.Serialize(hybrid.Nodes.OrderBy(node => node.Id, StringComparer.Ordinal).ToArray()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Property("Intent", "Operational")]
|
||||
public async Task LayoutAsync_WhenHybridDeterministicModeEnabled_ShouldNotIncreasePrimaryViolationsAgainstLegacy()
|
||||
{
|
||||
var graph = BuildElkSharpStressGraph();
|
||||
var engine = new ElkSharpLayeredLayoutEngine();
|
||||
|
||||
var legacy = await engine.LayoutAsync(graph, CreateLegacyOptions());
|
||||
var hybrid = await engine.LayoutAsync(graph, CreateHybridOptions());
|
||||
|
||||
CountPrimaryViolations(hybrid).Should().BeLessThanOrEqualTo(CountPrimaryViolations(legacy));
|
||||
}
|
||||
|
||||
private static ElkLayoutOptions CreateLegacyOptions()
|
||||
{
|
||||
return new ElkLayoutOptions
|
||||
{
|
||||
Direction = ElkLayoutDirection.LeftToRight,
|
||||
Effort = ElkLayoutEffort.Best,
|
||||
OrderingIterations = 18,
|
||||
PlacementIterations = 10,
|
||||
IterativeRouting = new IterativeRoutingOptions
|
||||
{
|
||||
Mode = IterativeRoutingMode.LegacyMultiStrategy,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static ElkLayoutOptions CreateHybridOptions()
|
||||
{
|
||||
return new ElkLayoutOptions
|
||||
{
|
||||
Direction = ElkLayoutDirection.LeftToRight,
|
||||
Effort = ElkLayoutEffort.Best,
|
||||
OrderingIterations = 18,
|
||||
PlacementIterations = 10,
|
||||
IterativeRouting = new IterativeRoutingOptions
|
||||
{
|
||||
Mode = IterativeRoutingMode.HybridDeterministic,
|
||||
MaxRepairWaves = 4,
|
||||
MaxParallelRepairBuilds = 4,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static int CountPrimaryViolations(ElkLayoutResult layout)
|
||||
{
|
||||
var edges = layout.Edges.ToArray();
|
||||
var nodes = layout.Nodes.ToArray();
|
||||
|
||||
return ElkEdgeRouterHighway.DetectRemainingBrokenHighways(edges, nodes).Count
|
||||
+ ElkEdgeRoutingScoring.CountRepeatCollectorCorridorViolations(edges, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountRepeatCollectorNodeClearanceViolations(edges, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountTargetApproachJoinViolations(edges, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountTargetApproachBacktrackingViolations(edges, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountSharedLaneViolations(edges, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountBoundarySlotViolations(edges, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountBelowGraphViolations(edges, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountUnderNodeViolations(edges, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountLongDiagonalViolations(edges, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountBadBoundaryAngles(edges, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountGatewaySourceExitViolations(edges, nodes)
|
||||
+ ElkEdgeRoutingScoring.CountExcessiveDetourViolations(edges, nodes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
using StellaOps.ElkSharp;
|
||||
using StellaOps.Workflow.Abstractions;
|
||||
using StellaOps.Workflow.Renderer.ElkSharp;
|
||||
|
||||
namespace StellaOps.Workflow.Renderer.Tests;
|
||||
|
||||
[TestFixture]
|
||||
public class ElkSharpWorkflowRenderLayoutEngineIterativeRoutingTests
|
||||
{
|
||||
[Test]
|
||||
public async Task LayoutAsync_WhenLeftToRightBest_ShouldEnableHybridDeterministicIterativeRouting()
|
||||
{
|
||||
var captureEngine = new CaptureElkLayoutEngine();
|
||||
var engine = new ElkSharpWorkflowRenderLayoutEngine(captureEngine);
|
||||
|
||||
await engine.LayoutAsync(
|
||||
BuildGraph(),
|
||||
new WorkflowRenderLayoutRequest
|
||||
{
|
||||
Direction = WorkflowRenderLayoutDirection.LeftToRight,
|
||||
Effort = WorkflowRenderLayoutEffort.Best,
|
||||
});
|
||||
|
||||
captureEngine.LastOptions.Should().NotBeNull();
|
||||
captureEngine.LastOptions!.Direction.Should().Be(ElkLayoutDirection.LeftToRight);
|
||||
captureEngine.LastOptions.Effort.Should().Be(ElkLayoutEffort.Best);
|
||||
captureEngine.LastOptions.IterativeRouting.Should().NotBeNull();
|
||||
captureEngine.LastOptions.IterativeRouting!.Enabled.Should().BeTrue();
|
||||
captureEngine.LastOptions.IterativeRouting.Mode.Should().Be(IterativeRoutingMode.HybridDeterministic);
|
||||
captureEngine.LastOptions.IterativeRouting.MaxRepairWaves.Should().Be(1);
|
||||
captureEngine.LastOptions.IterativeRouting.MaxParallelRepairBuilds.Should().Be(Math.Max(1, Environment.ProcessorCount));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task LayoutAsync_WhenTopToBottomBest_ShouldNotForceHybridIterativeRouting()
|
||||
{
|
||||
var captureEngine = new CaptureElkLayoutEngine();
|
||||
var engine = new ElkSharpWorkflowRenderLayoutEngine(captureEngine);
|
||||
|
||||
await engine.LayoutAsync(
|
||||
BuildGraph(),
|
||||
new WorkflowRenderLayoutRequest
|
||||
{
|
||||
Direction = WorkflowRenderLayoutDirection.TopToBottom,
|
||||
Effort = WorkflowRenderLayoutEffort.Best,
|
||||
});
|
||||
|
||||
captureEngine.LastOptions.Should().NotBeNull();
|
||||
captureEngine.LastOptions!.Direction.Should().Be(ElkLayoutDirection.TopToBottom);
|
||||
captureEngine.LastOptions.IterativeRouting.Should().BeNull();
|
||||
}
|
||||
|
||||
private static WorkflowRenderGraph BuildGraph()
|
||||
{
|
||||
return new WorkflowRenderGraph
|
||||
{
|
||||
Id = "capture",
|
||||
Nodes =
|
||||
[
|
||||
new WorkflowRenderNode
|
||||
{
|
||||
Id = "start",
|
||||
Label = "Start",
|
||||
Kind = "Start",
|
||||
},
|
||||
],
|
||||
Edges = [],
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class CaptureElkLayoutEngine : IElkLayoutEngine
|
||||
{
|
||||
public ElkLayoutOptions? LastOptions { get; private set; }
|
||||
|
||||
public Task<ElkLayoutResult> LayoutAsync(
|
||||
ElkGraph graph,
|
||||
ElkLayoutOptions? options = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
LastOptions = options;
|
||||
return Task.FromResult(new ElkLayoutResult
|
||||
{
|
||||
GraphId = graph.Id,
|
||||
Nodes = graph.Nodes.Select(node => new ElkPositionedNode
|
||||
{
|
||||
Id = node.Id,
|
||||
Label = node.Label,
|
||||
Kind = node.Kind,
|
||||
X = 0d,
|
||||
Y = 0d,
|
||||
Width = node.Width,
|
||||
Height = node.Height,
|
||||
}).ToArray(),
|
||||
Edges = [],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user