Skip to content

Commit bc988ba

Browse files
authored
feat(sim): validate train length against available track distance on construction (Issue #60)
Add pre-construction validation in Train that checks the train's length against the shortest topological path between its origin and destination InOuts. Throws SimulationException if no path exists or the train is too long to fit on the network. - Uses TopologyNavigator.findAllTopologicalPaths() to enumerate all routes - Skips validation when InOuts are not registered in the context (test mocks) - TrainLengthValidationTest: 8 tests covering valid/invalid lengths and multi-path networks (vyhybna.xml fixture) Fixes #60
1 parent f1ffcdb commit bc988ba

4 files changed

Lines changed: 416 additions & 2 deletions

File tree

core/src/commonMain/kotlin/cz/vutbr/fit/interlockSim/sim/Train.kt

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import cz.vutbr.fit.interlockSim.context.SimulationContext.ReportType
1313
import cz.vutbr.fit.interlockSim.context.SimulationEnvironment
1414
import cz.vutbr.fit.interlockSim.context.navigation.PathResult
1515
import cz.vutbr.fit.interlockSim.context.navigation.TrainNavigationService
16+
import cz.vutbr.fit.interlockSim.exceptions.SimulationException
1617
import cz.vutbr.fit.interlockSim.exceptions.requireSimulation
1718
import cz.vutbr.fit.interlockSim.exceptions.requireSimulationNotNull
1819
import cz.vutbr.fit.interlockSim.objects.cells.DynamicInOut
@@ -804,6 +805,7 @@ class Train :
804805
* Create train
805806
* @param env The simulation environment
806807
* @param timetable Train timetable
808+
* @throws SimulationException if train length exceeds track distance between InOuts
807809
*/
808810
constructor(env: SimulationEnvironment?, timetable: Timetable?) {
809811
this.env = requireSimulationNotNull(env) { "env must not be null" }
@@ -815,8 +817,93 @@ class Train :
815817
val inName = validatedTimetable.getIn().name
816818
val outName = validatedTimetable.getOut().name
817819
trainNavService = env.getTrainNavigationService()
820+
821+
// Issue #60: Validate train length against track distance between InOuts
822+
validateTrainLength(env, validatedTimetable, this.length)
823+
818824
logger.debug { "Train $number created: from $inName to $outName, length $length" }
819825
}
826+
827+
/**
828+
* Validates that train length does not exceed the shortest track distance between InOuts.
829+
*
830+
* **Issue #60: Track Length Validation**
831+
* - Calculates shortest path distance between origin and destination InOuts
832+
* - Ensures train can physically fit on the track
833+
* - Prevents runtime simulation errors from track being too short
834+
*
835+
* **Implementation:**
836+
* - Uses TopologyNavigator to find all possible paths
837+
* - Calculates total track distance for each path
838+
* - Validates train length against shortest available path
839+
* - Gracefully handles test mocks by catching exceptions
840+
*
841+
* @param env Simulation environment providing topology navigator
842+
* @param timetable Train timetable with origin and destination InOuts
843+
* @param trainLength Length of the train in meters
844+
* @throws SimulationException if train length exceeds shortest track distance
845+
* @since 2026-02-06 (Issue #60)
846+
*/
847+
private fun validateTrainLength(
848+
env: SimulationEnvironment,
849+
timetable: Timetable,
850+
trainLength: Double
851+
) {
852+
val inOut = timetable.getIn()
853+
val outOut = timetable.getOut()
854+
855+
// Skip validation if either InOut is not registered in this context (e.g. mock objects in tests)
856+
val contextInOuts = env.getInOuts()
857+
if (!contextInOuts.contains(inOut) || !contextInOuts.contains(outOut)) {
858+
logger.trace { "Train length validation skipped: InOuts not registered in context (likely test mock)" }
859+
return
860+
}
861+
862+
try {
863+
val topologyNavigator = env.getTopologyNavigator()
864+
865+
// Find all topologically possible paths between InOuts
866+
val paths = topologyNavigator.findAllTopologicalPaths(
867+
start = inOut,
868+
target = outOut,
869+
maxDepth = 100
870+
)
871+
872+
requireSimulation(paths.isNotEmpty()) {
873+
"Train length validation failed: No route exists between " +
874+
"InOut '${inOut.name}' and InOut '${outOut.name}'. " +
875+
"Railway network must provide at least one path between entry and exit points."
876+
}
877+
878+
// Calculate distance for each path and find the shortest using idiomatic Kotlin
879+
val shortestPathDistance = paths.minOf { path ->
880+
path.sumOf { section -> section.length() }
881+
}
882+
883+
// Validate train length against shortest path
884+
requireSimulation(trainLength <= shortestPathDistance) {
885+
"Train length ($trainLength m) exceeds track distance ($shortestPathDistance m) " +
886+
"between InOut '${inOut.name}' and InOut '${outOut.name}'. " +
887+
"Minimum track length required: $trainLength m, available: $shortestPathDistance m. " +
888+
"Reduce train length or increase track distance to resolve this issue."
889+
}
890+
891+
logger.debug {
892+
"Train length validation passed: train=$trainLength m, " +
893+
"shortest path=$shortestPathDistance m (${inOut.name}${outOut.name})"
894+
}
895+
} catch (e: SimulationException) {
896+
// Rethrow validation failures (train too long, no route, etc.)
897+
throw e
898+
} catch (e: Exception) {
899+
// Any other exception indicates an unexpected failure in topology/navigation logic.
900+
// Log at WARN and rethrow so that validation is not silently bypassed.
901+
logger.warn(e) {
902+
"Train length validation failed due to unexpected error; simulation will be aborted: ${e.message}"
903+
}
904+
throw e
905+
}
906+
}
820907

821908
override fun distanceToSemaphore(): Double =
822909
if (pathToSemaphore == null) 0.0 else pathToSemaphore!!.length() - front.getPosition()

core/src/jvmMain/kotlin/cz/vutbr/fit/interlockSim/xml/XMLContextFactory.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ import java.lang.reflect.InvocationTargetException
3939
*/
4040
class XMLContextFactory : EditingContextFactory {
4141

42-
// TODO: Validate track length >= train length - see issue #60 (relates to Goals 3 & 4)
42+
// Issue #60: Track length validation implemented in Train constructor
43+
// Validation occurs at train creation time when both train length and topology are available
4344

4445
private val xmlValidator = XmlSchemaValidator()
4546

core/src/jvmTest/kotlin/cz/vutbr/fit/interlockSim/sim/GeneratorTest.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,12 @@ class GeneratorTest : KoinTestBase() {
6666
@BeforeEach
6767
fun setUp() {
6868
// Create a context with at least 2 InOuts for Generator to use
69+
// Track is 200m to accommodate trains up to 150m used in collection management tests
6970
val contextBuilder =
7071
get<TestContextBuilder>()
7172
.withInOut("IN1", 0, 0, isEntry = true)
72-
.withInOut("OUT1", 1, 0, isEntry = false)
73+
.withInOut("OUT1", 5, 0, isEntry = false)
74+
.withConnection(0, 0, 5, 0, 200.0, 80.0)
7375
val delegateContext = contextBuilder.buildSimulationContext()
7476
mockContext = MockSimulationContext(delegateContext)
7577
}

0 commit comments

Comments
 (0)