Skip to content

Commit 53edc28

Browse files
bedaHovorkaclaude
andcommitted
Implement BFS-based path connectivity validation in XMLContextFactoryTest
Add parseXML_rudyUjezd_createsValidContext test that verifies railway network connectivity using breadth-first search algorithm. The existPath helper function now properly validates that paths can be created between InOut nodes by checking actual graph connectivity through TrackSections, not just node existence. This fixes the issue where disconnected graph components would incorrectly pass validation. The test ensures all InOuts on opposite ends of the rudyUjezd station are reachable through the track infrastructure. Changes: - Add BFS traversal algorithm to existPath() - Add rudyUjezd.xml test fixture validation - Fix map iteration for Java Map compatibility - Use TrackSection.ends() to find connected nodes All 659 tests pass (629 passing, 30 skipped). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent bd549ff commit 53edc28

2 files changed

Lines changed: 182 additions & 0 deletions

File tree

src/test/kotlin/cz/vutbr/fit/interlockSim/xml/XMLContextFactoryTest.kt

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@ import assertk.assertions.isNotNull
1919
import assertk.assertions.isSameInstanceAs
2020
import assertk.assertions.isTrue
2121
import cz.vutbr.fit.interlockSim.context.ContextCreationException
22+
import cz.vutbr.fit.interlockSim.context.DefaultContext
2223
import cz.vutbr.fit.interlockSim.objects.cells.InOut
2324
import cz.vutbr.fit.interlockSim.objects.cells.RailSemaphore
2425
import cz.vutbr.fit.interlockSim.objects.cells.RailSwitch
26+
import cz.vutbr.fit.interlockSim.objects.tracks.TrackSection
2527
import cz.vutbr.fit.interlockSim.testutil.exists
28+
import cz.vutbr.fit.interlockSim.util.Point
2629
import cz.vutbr.fit.interlockSim.testutil.isFile
2730
import cz.vutbr.fit.interlockSim.testutil.withMessage
2831
import org.junit.jupiter.api.*
@@ -206,6 +209,121 @@ class XMLContextFactoryTest {
206209
assertThat(cellA2).isNotNull().isInstanceOf(InOut::class)
207210
assertThat(cellB2).isNotNull().isInstanceOf(InOut::class)
208211
}
212+
213+
@Test
214+
fun parseXML_rudyUjezd_createsValidContext() {
215+
val xml = getFixtureStream("rudyUjezd.xml")
216+
217+
val context = factory.createContext(xml)
218+
219+
assertThat(context).isNotNull()
220+
val grid = context.getRailWayNetGrid()
221+
// Check grid size (from rudyUjezd.xml: X=100, Y=100)
222+
assertThat(grid.getCols()).isEqualTo(100)
223+
assertThat(grid.getRows()).isEqualTo(100)
224+
// Check presence of at least one InOut, RailSwitch, and RailSemaphore
225+
var hasInOut = false
226+
var hasSwitch = false
227+
var hasSemaphore = false
228+
for (entry in grid) {
229+
when (entry.value) {
230+
is InOut -> hasInOut = true
231+
is RailSwitch -> hasSwitch = true
232+
is RailSemaphore -> hasSemaphore = true
233+
}
234+
}
235+
236+
// in-outs on first end:
237+
// <InOut X="37" Y="32" SpatialType="HORIZONTAL" orientation="true" name="" />
238+
val f1 = context.getRailWayNetGrid().getCellAt(37, 32)
239+
// <InOut X="37" Y="31" SpatialType="HORIZONTAL" orientation="true" name="" />
240+
val f2 = context.getRailWayNetGrid().getCellAt(37, 31)
241+
242+
// in-outs on second end:
243+
// <InOut X="5" Y="31" SpatialType="HORIZONTAL" orientation="false" name="" />
244+
val s1 = context.getRailWayNetGrid().getCellAt(5, 31)
245+
// <InOut X="5" Y="32" SpatialType="HORIZONTAL" orientation="false" name="" />
246+
val s2 = context.getRailWayNetGrid().getCellAt(5, 32)
247+
248+
assertThat(f1).isNotNull().isInstanceOf(InOut::class)
249+
assertThat(f2).isNotNull().isInstanceOf(InOut::class)
250+
assertThat(s1).isNotNull().isInstanceOf(InOut::class)
251+
assertThat(s2).isNotNull().isInstanceOf(InOut::class)
252+
253+
// from each end, there are switches and semaphores leading into the station area and must exist path to each InOut on the other side
254+
assertThat(existPath(f1 as InOut, s1 as InOut, context)).isTrue()
255+
assertThat(existPath(f1, s2 as InOut, context)).isTrue()
256+
assertThat(existPath(f2 as InOut, s1, context)).isTrue()
257+
assertThat(existPath(f2, s2, context)).isTrue()
258+
// and back
259+
assertThat(existPath(s1, f1, context)).isTrue()
260+
assertThat(existPath(s1, f2, context)).isTrue()
261+
assertThat(existPath(s2, f1, context)).isTrue()
262+
assertThat(existPath(s2, f2, context)).isTrue()
263+
264+
265+
assertThat(hasInOut).withMessage("Should contain at least one InOut").isTrue()
266+
assertThat(hasSwitch).withMessage("Should contain at least one RailSwitch").isTrue()
267+
assertThat(hasSemaphore).withMessage("Should contain at least one RailSemaphore").isTrue()
268+
}
269+
270+
/**
271+
* Checks if a path exists (or can be created) between two InOuts in the railway network.
272+
* Uses BFS to traverse the track graph and verify connectivity.
273+
*/
274+
private fun existPath(
275+
from: InOut,
276+
to: InOut,
277+
context: DefaultContext
278+
) : Boolean {
279+
// Get grid locations for both InOuts
280+
val fromLoc = context.getRailWayNetGrid().getLocation(from) ?: return false
281+
val toLoc = context.getRailWayNetGrid().getLocation(to) ?: return false
282+
283+
// If they're the same location, path exists trivially
284+
if (fromLoc == toLoc) return true
285+
286+
// BFS on the track graph
287+
val graph = context.getGraph()
288+
val visited = mutableSetOf<Point>()
289+
val queue = mutableListOf(fromLoc)
290+
291+
while (queue.isNotEmpty()) {
292+
val current = queue.removeFirst()
293+
294+
// Skip if already visited
295+
if (current in visited) continue
296+
visited.add(current)
297+
298+
// Check if we reached the destination
299+
if (current == toLoc) return true
300+
301+
// Get all track blocks connected to this location
302+
val edges = graph.assignedEdges(current)
303+
304+
// For each track block, find the other end and add it to the queue
305+
for (entry in edges.entrySet()) {
306+
val trackBlock = entry.value
307+
308+
// TrackBlocks should be TrackSections which have ends()
309+
if (trackBlock is TrackSection) {
310+
val ends = trackBlock.ends()
311+
// Get grid locations of both ends
312+
for (pathSeparator in ends) {
313+
val endLocation = context.getRailWayNetGrid().getLocation(pathSeparator)
314+
// Add the other end (not current) to the queue
315+
if (endLocation != null && endLocation != current && endLocation !in visited) {
316+
queue.add(endLocation)
317+
}
318+
}
319+
}
320+
}
321+
}
322+
323+
// No path found
324+
return false
325+
}
326+
209327
}
210328

211329
@Nested
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?xml version="1.0"?>
2+
<!DOCTYPE net>
3+
<net X="100" Y="100" >
4+
<RailSwitch X="33" Y="32" SpatialType="HORIZONTAL" Type="SIMPLE_LEFT_TRUE" />
5+
<RailSwitch X="34" Y="32" SpatialType="HORIZONTAL" Type="SIMPLE_LEFT_FALSE" />
6+
<RailSwitch X="35" Y="32" SpatialType="HORIZONTAL" Type="SIMPLE_RIGHT_TRUE" />
7+
<RailSwitch X="8" Y="31" SpatialType="HORIZONTAL" Type="SIMPLE_LEFT_TRUE" />
8+
<RailSemaphore X="36" Y="32" SpatialType="HORIZONTAL" orientation="true" />
9+
<InOut X="37" Y="32" SpatialType="HORIZONTAL" orientation="true" name="" />
10+
<RailSwitch X="9" Y="31" SpatialType="HORIZONTAL" Type="SIMPLE_LEFT_FALSE" />
11+
<RailSemaphore X="11" Y="31" SpatialType="HORIZONTAL" orientation="true" />
12+
<RailSemaphore X="31" Y="31" SpatialType="HORIZONTAL" orientation="false" />
13+
<RailSemaphore X="11" Y="30" SpatialType="HORIZONTAL" orientation="true" />
14+
<InOut X="5" Y="31" SpatialType="HORIZONTAL" orientation="false" name="" />
15+
<RailSemaphore X="6" Y="31" SpatialType="HORIZONTAL" orientation="false" />
16+
<RailSwitch X="7" Y="31" SpatialType="HORIZONTAL" Type="SIMPLE_RIGHT_FALSE" />
17+
<RailSemaphore X="31" Y="30" SpatialType="HORIZONTAL" orientation="false" />
18+
<InOut X="5" Y="32" SpatialType="HORIZONTAL" orientation="false" name="" />
19+
<RailSemaphore X="6" Y="32" SpatialType="HORIZONTAL" orientation="false" />
20+
<RailSwitch X="7" Y="32" SpatialType="HORIZONTAL" Type="SIMPLE_LEFT_FALSE" />
21+
<RailSwitch X="35" Y="31" SpatialType="HORIZONTAL" Type="SIMPLE_LEFT_TRUE" />
22+
<RailSwitch X="34" Y="31" SpatialType="HORIZONTAL" Type="SIMPLE_RIGHT_FALSE" />
23+
<RailSwitch X="8" Y="32" SpatialType="HORIZONTAL" Type="SIMPLE_RIGHT_TRUE" />
24+
<RailSwitch X="33" Y="31" SpatialType="HORIZONTAL" Type="SIMPLE_RIGHT_TRUE" />
25+
<RailSwitch X="9" Y="32" SpatialType="HORIZONTAL" Type="SIMPLE_RIGHT_FALSE" />
26+
<RailSemaphore X="36" Y="31" SpatialType="HORIZONTAL" orientation="true" />
27+
<InOut X="37" Y="31" SpatialType="HORIZONTAL" orientation="true" name="" />
28+
<RailSemaphore X="11" Y="32" SpatialType="HORIZONTAL" orientation="true" />
29+
<RailSemaphore X="11" Y="33" SpatialType="HORIZONTAL" orientation="true" />
30+
<RailSemaphore X="31" Y="33" SpatialType="HORIZONTAL" orientation="false" />
31+
<RailSemaphore X="31" Y="32" SpatialType="HORIZONTAL" orientation="false" />
32+
<SimpleTrackBlock fromX="35" fromY="31" toX="34" toY="31" fromSegment="A" toSegment="F" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
33+
<SimpleTrackBlock fromX="34" fromY="31" toX="33" toY="31" fromSegment="A" toSegment="F" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
34+
<SimpleTrackBlock fromX="34" fromY="32" toX="33" toY="32" fromSegment="A" toSegment="F" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
35+
<SimpleTrackBlock fromX="35" fromY="32" toX="34" toY="32" fromSegment="A" toSegment="F" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
36+
<SimpleTrackBlock fromX="8" fromY="32" toX="9" toY="32" fromSegment="F" toSegment="A" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
37+
<SimpleTrackBlock fromX="36" fromY="31" toX="35" toY="31" fromSegment="A" toSegment="F" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
38+
<SimpleTrackBlock fromX="36" fromY="32" toX="35" toY="32" fromSegment="A" toSegment="F" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
39+
<SimpleTrackBlock fromX="8" fromY="31" toX="9" toY="31" fromSegment="F" toSegment="A" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
40+
<SimpleTrackBlock fromX="37" fromY="32" toX="36" toY="32" fromSegment="A" toSegment="F" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
41+
<SimpleTrackBlock fromX="37" fromY="31" toX="36" toY="31" fromSegment="A" toSegment="F" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
42+
<SimpleTrackBlock fromX="9" fromY="32" toX="11" toY="32" fromSegment="F" toSegment="A" length="100.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
43+
<SimpleTrackBlock fromX="9" fromY="31" toX="11" toY="31" fromSegment="F" toSegment="A" length="100.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
44+
<SimpleTrackBlock fromX="9" fromY="32" toX="11" toY="33" fromSegment="G" toSegment="A" length="100.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
45+
<SimpleTrackBlock fromX="11" fromY="33" toX="31" toY="33" fromSegment="F" toSegment="A" length="100.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
46+
<SimpleTrackBlock fromX="8" fromY="32" toX="7" toY="31" fromSegment="B" toSegment="G" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
47+
<SimpleTrackBlock fromX="8" fromY="31" toX="7" toY="32" fromSegment="D" toSegment="E" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
48+
<SimpleTrackBlock fromX="33" fromY="31" toX="31" toY="30" fromSegment="B" toSegment="F" length="100.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
49+
<SimpleTrackBlock fromX="11" fromY="30" toX="31" toY="30" fromSegment="F" toSegment="A" length="100.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
50+
<SimpleTrackBlock fromX="35" fromY="31" toX="34" toY="32" fromSegment="D" toSegment="E" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
51+
<SimpleTrackBlock fromX="35" fromY="32" toX="34" toY="31" fromSegment="B" toSegment="G" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
52+
<SimpleTrackBlock fromX="31" fromY="31" toX="11" toY="31" fromSegment="A" toSegment="F" length="100.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
53+
<SimpleTrackBlock fromX="11" fromY="32" toX="31" toY="32" fromSegment="F" toSegment="A" length="100.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
54+
<SimpleTrackBlock fromX="9" fromY="31" toX="11" toY="30" fromSegment="E" toSegment="A" length="100.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
55+
<SimpleTrackBlock fromX="5" fromY="32" toX="6" toY="32" fromSegment="F" toSegment="A" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
56+
<SimpleTrackBlock fromX="5" fromY="31" toX="6" toY="31" fromSegment="F" toSegment="A" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
57+
<SimpleTrackBlock fromX="33" fromY="32" toX="31" toY="33" fromSegment="D" toSegment="F" length="100.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
58+
<SimpleTrackBlock fromX="6" fromY="32" toX="7" toY="32" fromSegment="F" toSegment="A" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
59+
<SimpleTrackBlock fromX="6" fromY="31" toX="7" toY="31" fromSegment="F" toSegment="A" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
60+
<SimpleTrackBlock fromX="7" fromY="32" toX="8" toY="32" fromSegment="F" toSegment="A" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
61+
<SimpleTrackBlock fromX="8" fromY="31" toX="7" toY="31" fromSegment="A" toSegment="F" length="5.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
62+
<SimpleTrackBlock fromX="31" fromY="31" toX="33" toY="31" fromSegment="F" toSegment="A" length="100.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
63+
<SimpleTrackBlock fromX="31" fromY="32" toX="33" toY="32" fromSegment="F" toSegment="A" length="100.0" maxSpeedfrom="24.0" maxSpeedto="24.0" />
64+
</net>

0 commit comments

Comments
 (0)