Forge
csharpdeeb25a2
1using CascadeIDE.Cockpit.Graph;
2using CascadeIDE.Cockpit.Graph.Layout;
3using CascadeIDE.Models;
4using CascadeIDE.Services;
5using CascadeIDE.Features.WorkspaceNavigation.Application;
6using CascadeIDE.ViewModels;
7using Xunit;
8
9namespace CascadeIDE.Tests;
10
11public sealed class ControlFlowGraphLayoutEngineTests
12{
13 [Fact]
14 public void Layout_PlacesFlowTopToBottom_ByDepth()
15 {
16 var engine = new ControlFlowGraphLayoutEngine();
17 var doc = new GraphDocument
18 {
19 AnchorPath = @"D:\w\A.cs",
20 Nodes =
21 [
22 new GraphNode
23 {
24 Id = "n0",
25 Path = @"D:\w\A.cs",
26 Kind = "anchor",
27 Label = "A.cs"
28 },
29 new GraphNode
30 {
31 Id = "n1",
32 Path = @"D:\w\A.cs",
33 Kind = "call_step",
34 Label = "B"
35 },
36 new GraphNode
37 {
38 Id = "n2",
39 Path = @"D:\w\A.cs",
40 Kind = "call_step",
41 Label = "C"
42 }
43 ],
44 Edges =
45 [
46 new GraphEdge { FromId = "n0", ToId = "n1", Kind = "Call" },
47 new GraphEdge { FromId = "n1", ToId = "n2", Kind = "Call" }
48 ]
49 };
50
51 // Высота ≥ ширина — автоматический выбор оси вертикальный поток сверху вниз (широко-низкая панель дала бы горизонталь).
52 var scene = engine.Layout(doc, 280, 300);
53 Assert.Equal(3, scene.Nodes.Count);
54 Assert.Equal(GraphControlFlowMainAxis.Vertical, scene.ControlFlowMainAxis);
55
56 var n0 = scene.Nodes.Single(n => n.Id == "n0");
57 var n1 = scene.Nodes.Single(n => n.Id == "n1");
58 var n2 = scene.Nodes.Single(n => n.Id == "n2");
59
60 Assert.True(n0.IsAnchor);
61 Assert.True(n0.Center.Y < n1.Center.Y);
62 Assert.True(n1.Center.Y < n2.Center.Y);
63 Assert.Equal(140, n0.Center.X, 0.5);
64 }
65
66 [Fact]
67 public void Layout_ShowsEdgeStyleLegend_WhenGraphHasNonSolidEdges()
68 {
69 var engine = new ControlFlowGraphLayoutEngine();
70 var doc = new GraphDocument
71 {
72 AnchorPath = @"D:\w\A.cs",
73 Nodes =
74 [
75 new GraphNode
76 {
77 Id = "n0",
78 Path = @"D:\w\A.cs",
79 Kind = "anchor",
80 Label = "A.cs"
81 },
82 new GraphNode
83 {
84 Id = "n1",
85 Path = @"D:\w\A.cs",
86 Kind = "call_step",
87 Label = "B"
88 }
89 ],
90 Edges =
91 [
92 new GraphEdge { FromId = "n0", ToId = "n1", Kind = "ConditionalCall" }
93 ]
94 };
95
96 var scene = engine.Layout(doc, 400, 200);
97 Assert.True(scene.ShowLegendEdgeStyleKey);
98 }
99
100 [Fact]
101 public void Layout_PlacesSameDepthBranchesSideBySide()
102 {
103 var engine = new ControlFlowGraphLayoutEngine();
104 var doc = new GraphDocument
105 {
106 AnchorPath = @"D:\w\A.cs",
107 Nodes =
108 [
109 new GraphNode
110 {
111 Id = "n0",
112 Path = @"D:\w\A.cs",
113 Kind = "anchor",
114 Label = "A.cs"
115 },
116 new GraphNode
117 {
118 Id = "n1",
119 Path = @"D:\w\A.cs",
120 Kind = "call_step",
121 Label = "LeftBranch"
122 },
123 new GraphNode
124 {
125 Id = "n2",
126 Path = @"D:\w\A.cs",
127 Kind = "call_step",
128 Label = "RightBranch"
129 }
130 ],
131 Edges =
132 [
133 new GraphEdge { FromId = "n0", ToId = "n1", Kind = "ConditionalCall" },
134 new GraphEdge { FromId = "n0", ToId = "n2", Kind = "ConditionalCall" }
135 ]
136 };
137
138 // Вертикальный основной поток: ветки одного уровня — одинаковый Y, разведение по X.
139 var scene = engine.Layout(doc, 220, 280);
140 Assert.Equal(GraphControlFlowMainAxis.Vertical, scene.ControlFlowMainAxis);
141 var n1 = scene.Nodes.Single(n => n.Id == "n1");
142 var n2 = scene.Nodes.Single(n => n.Id == "n2");
143 Assert.Equal(n1.Center.Y, n2.Center.Y, 0.5);
144 Assert.True(n1.Center.X < n2.Center.X);
145 }
146
147 [Fact]
148 public void Layout_NarrowSlot_ShortensLabelsAndSetsAdaptiveSideFont()
149 {
150 var engine = new ControlFlowGraphLayoutEngine();
151 var doc = new GraphDocument
152 {
153 AnchorPath = @"D:\w\A.cs",
154 Nodes =
155 [
156 new GraphNode
157 {
158 Id = "n0",
159 Path = @"D:\w\A.cs",
160 Kind = "anchor",
161 Label = "A.cs"
162 },
163 new GraphNode
164 {
165 Id = "n1",
166 Path = @"D:\w\A.cs",
167 Kind = "call_step",
168 Label = new string('M', 40)
169 }
170 ],
171 Edges = [new GraphEdge { FromId = "n0", ToId = "n1", Kind = "Call" }]
172 };
173
174 var scene = engine.Layout(doc, 118, 200);
175 Assert.NotNull(scene.SideLabelFontSizePx);
176 Assert.InRange(scene.SideLabelFontSizePx!.Value, GraphRenderInvariants.CompactSideLabelFontSizeFloor, GraphRenderInvariants.MaxSideLabelFontSize);
177 var n1 = scene.Nodes.Single(n => n.Id == "n1");
178 Assert.True(n1.Label.Length < 40);
179 Assert.EndsWith("…", n1.Label);
180 }
181
182 [Fact]
183 public void Layout_WideSlot_AllowsLongerLabelsThanNarrow()
184 {
185 var narrow = GraphControlFlowLayoutMetrics.ResolveLabelCharBudget(118);
186 var wide = GraphControlFlowLayoutMetrics.ResolveLabelCharBudget(360);
187 Assert.True(wide > narrow);
188 }
189
190 [Fact]
191 public void Layout_LegendColumnLeft_StartsAtMaxInkRightPlusGap_NotReadableBandRight()
192 {
193 var engine = new ControlFlowGraphLayoutEngine();
194 var doc = new GraphDocument
195 {
196 AnchorPath = @"D:\w\A.cs",
197 Nodes =
198 [
199 new GraphNode { Id = "n0", Path = @"D:\w\A.cs", Kind = "anchor", Label = "A" },
200 new GraphNode
201 {
202 Id = "n1",
203 Path = @"D:\w\A.cs",
204 Kind = "condition_step",
205 Label = "IF",
206 LegendIndex = 1,
207 LegendText = "x > 0"
208 }
209 ],
210 Edges = [new GraphEdge { FromId = "n0", ToId = "n1", Kind = "Call" }]
211 };
212 const double viewportW = 400;
213 // Выше модель горизонтальной полосы: легенда справа фиксируется от max(Center.X+R), без привязки к концу reading band по X.
214 var scene = engine.Layout(doc, viewportW, 260);
215 Assert.True(scene.UseLegendColumn);
216 var inkSl = GraphControlFlowLayoutMetrics.BesideLegendInkSlack;
217 var minCl = Math.Max(
218 GraphControlFlowLayoutMetrics.LegendGap,
219 GraphControlFlowLayoutMetrics.LegendBesideMinClearance);
220 var inkR = 0.0;
221 foreach (var n in scene.Nodes)
222 inkR = Math.Max(inkR, n.Center.X + n.Radius + inkSl);
223 var expected = inkR + minCl;
224 Assert.Equal(expected, scene.LegendColumnLeft, 0.6);
225 }
226
227 [Fact]
228 public void Layout_NarrowWidth_PlacesLegendBlockBelowGraph_FullWidthText()
229 {
230 var engine = new ControlFlowGraphLayoutEngine();
231 var doc = new GraphDocument
232 {
233 AnchorPath = @"D:\w\A.cs",
234 Nodes =
235 [
236 new GraphNode { Id = "n0", Path = @"D:\w\A.cs", Kind = "anchor", Label = "A" },
237 new GraphNode
238 {
239 Id = "n1",
240 Path = @"D:\w\A.cs",
241 Kind = "condition_step",
242 Label = "IF",
243 LegendIndex = 1,
244 LegendText = "pred"
245 }
246 ],
247 Edges = [new GraphEdge { FromId = "n0", ToId = "n1", Kind = "Call" }]
248 };
249 // Мало места справа от «чернила» — блок легенды уходит вниз на полную ширину.
250 var scene = engine.Layout(doc, 130, 200);
251 Assert.True(scene.UseLegendColumn);
252 Assert.Equal(GraphLegendBlockPlacement.BelowGraph, scene.LegendPlacement);
253 Assert.True(scene.LegendBlockTopY > 0);
254 Assert.Equal(GraphControlFlowLayoutMetrics.SidePadding, scene.LegendColumnLeft, 0.01);
255 }
256
257 [Fact]
258 public void Layout_WideViewport_LinearChain_SelectsHorizontalMainAxis()
259 {
260 var engine = new ControlFlowGraphLayoutEngine();
261 GraphNode Node(string id, string label) =>
262 new()
263 {
264 Id = id,
265 Path = @"D:\w\Chain.cs",
266 Kind = "call_step",
267 Label = label
268 };
269
270 var edges = new List<GraphEdge>();
271 var nodes = new List<GraphNode>
272 {
273 new()
274 {
275 Id = "n0",
276 Path = @"D:\w\Chain.cs",
277 Kind = "anchor",
278 Label = "Anchor"
279 }
280 };
281 for (var i = 1; i <= 6; i++)
282 {
283 nodes.Add(Node($"n{i}", $"s{i}"));
284 edges.Add(new GraphEdge { FromId = $"n{i - 1}", ToId = $"n{i}", Kind = "Call" });
285 }
286
287 var doc = new GraphDocument
288 {
289 AnchorPath = @"D:\w\Chain.cs",
290 Nodes = nodes,
291 Edges = edges
292 };
293
294 var scene = engine.Layout(doc, 640, 130);
295 Assert.Equal(GraphControlFlowMainAxis.Horizontal, scene.ControlFlowMainAxis);
296 for (var i = 0; i < 6; i++)
297 {
298 var a = scene.Nodes.Single(n => n.Id == $"n{i}");
299 var b = scene.Nodes.Single(n => n.Id == $"n{i + 1}");
300 Assert.True(a.Center.X < b.Center.X);
301 }
302 }
303
304 [Fact]
305 public void Layout_TallViewport_LinearChain_SelectsVerticalMainAxis()
306 {
307 var engine = new ControlFlowGraphLayoutEngine();
308 GraphNode Node(string id, string label) =>
309 new()
310 {
311 Id = id,
312 Path = @"D:\w\Chain.cs",
313 Kind = "call_step",
314 Label = label
315 };
316
317 var edges = new List<GraphEdge>();
318 var nodes = new List<GraphNode>
319 {
320 new()
321 {
322 Id = "n0",
323 Path = @"D:\w\Chain.cs",
324 Kind = "anchor",
325 Label = "Anchor"
326 }
327 };
328 for (var i = 1; i <= 6; i++)
329 {
330 nodes.Add(Node($"n{i}", $"s{i}"));
331 edges.Add(new GraphEdge { FromId = $"n{i - 1}", ToId = $"n{i}", Kind = "Call" });
332 }
333
334 var doc = new GraphDocument
335 {
336 AnchorPath = @"D:\w\Chain.cs",
337 Nodes = nodes,
338 Edges = edges
339 };
340
341 var scene = engine.Layout(doc, 200, 420);
342 Assert.Equal(GraphControlFlowMainAxis.Vertical, scene.ControlFlowMainAxis);
343 for (var i = 0; i < 6; i++)
344 {
345 var a = scene.Nodes.Single(n => n.Id == $"n{i}");
346 var b = scene.Nodes.Single(n => n.Id == $"n{i + 1}");
347 Assert.True(a.Center.Y < b.Center.Y);
348 }
349 }
350
351 [Fact]
352 public void ChooseMainAxis_PicksAxisBySlackAndAspect()
353 {
354 Assert.Equal(
355 GraphControlFlowMainAxis.Horizontal,
356 GraphControlFlowLayoutMetrics.ChooseMainAxis(graphWidth: 520, heightForLayout: 118, levelCount: 6, maxNodesOnAnyLevel: 2));
357
358 Assert.Equal(
359 GraphControlFlowMainAxis.Vertical,
360 GraphControlFlowLayoutMetrics.ChooseMainAxis(graphWidth: 178, heightForLayout: 380, levelCount: 6, maxNodesOnAnyLevel: 2));
361 }
362
363 [Fact]
364 public void Layout_SettingsOverride_Vertical_OverridesWideShortHeuristic()
365 {
366 var engine = new ControlFlowGraphLayoutEngine();
367 GraphNode Node(string id, string label) =>
368 new()
369 {
370 Id = id,
371 Path = @"D:\w\Chain.cs",
372 Kind = "call_step",
373 Label = label
374 };
375
376 var edges = new List<GraphEdge>();
377 var nodes = new List<GraphNode>
378 {
379 new()
380 {
381 Id = "n0",
382 Path = @"D:\w\Chain.cs",
383 Kind = "anchor",
384 Label = "Anchor"
385 }
386 };
387 for (var i = 1; i <= 6; i++)
388 {
389 nodes.Add(Node($"n{i}", $"s{i}"));
390 edges.Add(new GraphEdge { FromId = $"n{i - 1}", ToId = $"n{i}", Kind = "Call" });
391 }
392
393 var doc = new GraphDocument
394 {
395 AnchorPath = @"D:\w\Chain.cs",
396 Nodes = nodes,
397 Edges = edges
398 };
399
400 var scene = engine.Layout(
401 doc,
402 640,
403 130,
404 CodeNavigationMapDetailLevel.Normal,
405 GraphControlFlowMainAxis.Vertical);
406 Assert.Equal(GraphControlFlowMainAxis.Vertical, scene.ControlFlowMainAxis);
407 Assert.True(scene.Nodes.Single(n => n.Id == "n0").Center.Y < scene.Nodes.Single(n => n.Id == "n1").Center.Y);
408 }
409}
410
View only · write via MCP/CIDE