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:
master
2026-03-30 17:25:28 +03:00
parent 9c79b00598
commit 5722d36c0e
3 changed files with 251 additions and 1 deletions

View File

@@ -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
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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 = [],
});
}
}
}