diff --git a/core/src/main/kotlin/fr/sncf/osrd/api/pathfinding/PathfindingBlockRequest.kt b/core/src/main/kotlin/fr/sncf/osrd/api/pathfinding/PathfindingBlockRequest.kt index c307505cf98..32a60f976bd 100644 --- a/core/src/main/kotlin/fr/sncf/osrd/api/pathfinding/PathfindingBlockRequest.kt +++ b/core/src/main/kotlin/fr/sncf/osrd/api/pathfinding/PathfindingBlockRequest.kt @@ -25,6 +25,9 @@ class PathfindingBlockRequest( // One set of location by step, each step must be reached in order @Json(name = "path_items") val pathItems: List, + + // Set of authorized track section ids, empty set means no restriction + @Json(name = "allowed_track_sections") val allowedTrackSections: Set = emptySet(), ) val pathfindingRequestAdapter: JsonAdapter = diff --git a/core/src/main/kotlin/fr/sncf/osrd/api/pathfinding/PathfindingBlocksEndpoint.kt b/core/src/main/kotlin/fr/sncf/osrd/api/pathfinding/PathfindingBlocksEndpoint.kt index e9a19c9fcb2..4eabf5e3eaa 100644 --- a/core/src/main/kotlin/fr/sncf/osrd/api/pathfinding/PathfindingBlocksEndpoint.kt +++ b/core/src/main/kotlin/fr/sncf/osrd/api/pathfinding/PathfindingBlocksEndpoint.kt @@ -4,6 +4,7 @@ import fr.sncf.osrd.api.ExceptionHandler import fr.sncf.osrd.api.FullInfra import fr.sncf.osrd.api.InfraProvider import fr.sncf.osrd.api.TrackLocation +import fr.sncf.osrd.api.stdcm.parseTrackSectionIds import fr.sncf.osrd.cli.Request import fr.sncf.osrd.cli.Response import fr.sncf.osrd.cli.RsJson @@ -121,6 +122,7 @@ fun runPathfinding(infra: FullInfra, request: PathfindingBlockRequest): Pathfind targets.add(ExplorerStep(allStarts, canBacktrack = step.canBacktrack)) } if (targets.size < 2) throw NoPathFoundException(NotEnoughPathItems()) + val allowedTrackSectionIds = parseTrackSectionIds(infra, request.allowedTrackSections) val constraints = initConstraintsFromRSProps( infra, @@ -128,6 +130,7 @@ fun runPathfinding(infra: FullInfra, request: PathfindingBlockRequest): Pathfind request.rollingStockLoadingGauge, request.rollingStockSupportedElectrifications, request.rollingStockSupportedSignalingSystems, + allowedTrackSectionIds, ) // Compute the paths from the entry waypoint to the exit waypoint diff --git a/core/src/main/kotlin/fr/sncf/osrd/api/stdcm/STDCMEndpoint.kt b/core/src/main/kotlin/fr/sncf/osrd/api/stdcm/STDCMEndpoint.kt index 500707a20a2..0b8092a9567 100644 --- a/core/src/main/kotlin/fr/sncf/osrd/api/stdcm/STDCMEndpoint.kt +++ b/core/src/main/kotlin/fr/sncf/osrd/api/stdcm/STDCMEndpoint.kt @@ -598,8 +598,11 @@ private fun parseSimulationScheduleItems( ) } -fun parseTrackSectionIds(infra: FullInfra, trackSectionName: Set?): Set? { - return trackSectionName?.mapNotNull { infra.rawInfra.getTrackSectionFromName(it) }?.toSet() +fun parseTrackSectionIds(infra: FullInfra, trackSectionNames: Set?): Set? { + return trackSectionNames + ?.mapNotNull { infra.rawInfra.getTrackSectionFromName(it) } + ?.toSet() + ?.takeIf { it.isNotEmpty() } } private fun progressCallback( diff --git a/core/src/test/kotlin/fr/sncf/osrd/pathfinding/PathfindingTest.kt b/core/src/test/kotlin/fr/sncf/osrd/pathfinding/PathfindingTest.kt index f6e20029ea0..238b772346d 100644 --- a/core/src/test/kotlin/fr/sncf/osrd/pathfinding/PathfindingTest.kt +++ b/core/src/test/kotlin/fr/sncf/osrd/pathfinding/PathfindingTest.kt @@ -20,6 +20,7 @@ import fr.sncf.osrd.utils.units.Distance import fr.sncf.osrd.utils.units.Offset import fr.sncf.osrd.utils.units.OffsetRange import fr.sncf.osrd.utils.units.meters +import java.util.stream.Stream import kotlin.test.assertEquals import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy @@ -27,6 +28,9 @@ import org.assertj.core.api.AssertionsForClassTypes import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource fun getPathfindingBlockRequest( rs: RollingStock, @@ -610,6 +614,60 @@ class PathfindingTest : ApiTest() { } } +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class PathfindingAllowedTrackSectionsTest { + // tiny_infra path: foo_b -> foo_to_bar -> bar_a + private val infra = Helpers.fullInfraFromFile("tiny_infra/infra.json") + private val waypointsStart = listOf(TrackLocation("ne.micro.foo_b", Offset(50.meters))) + private val waypointsEnd = listOf(TrackLocation("ne.micro.bar_a", Offset(100.meters))) + private val pathItems = listOf(PathItem(waypointsStart, false), PathItem(waypointsEnd, false)) + + private fun makeRequest(allowedTrackSections: Set = emptySet()) = + PathfindingBlockRequest( + rollingStockLoadingGauge = TestTrains.REALISTIC_FAST_TRAIN.loadingGaugeType, + rollingStockIsThermal = TestTrains.REALISTIC_FAST_TRAIN.isThermal, + rollingStockSupportedElectrifications = + TestTrains.REALISTIC_FAST_TRAIN.modeNames.filterNot { it == "thermal" }, + rollingStockSupportedSignalingSystems = + TestTrains.REALISTIC_FAST_TRAIN.supportedSignalingSystems.toList(), + rollingStockMaximumSpeed = TestTrains.REALISTIC_FAST_TRAIN.maxSpeed, + rollingStockLength = TestTrains.REALISTIC_FAST_TRAIN.length, + timeout = null, + infra = "tiny_infra/infra.json", + expectedVersion = 1, + pathItems = pathItems, + allowedTrackSections = allowedTrackSections, + ) + + private fun successCases(): Stream = + Stream.of( + Arguments.of(emptySet()), + Arguments.of(setOf("ne.micro.foo_b", "ne.micro.foo_to_bar", "ne.micro.bar_a")), + ) + + @ParameterizedTest + @MethodSource("successCases") + fun allowedTrackSectionsSucceeds(allowedTrackSections: Set) { + checkPathfindingSuccess( + runPathfinding(infra, makeRequest(allowedTrackSections)), + 10250.meters, + ) + } + + @Test + fun allowedTrackSectionsFailsWhenRequiredTrackExcluded() { + // omit foo_to_bar + assertThatThrownBy { + runPathfinding(infra, makeRequest(setOf("ne.micro.foo_b", "ne.micro.bar_a"))) + } + .isExactlyInstanceOf(NoPathFoundException::class.java) + .satisfies({ e -> + assertThat((e as NoPathFoundException).response) + .isExactlyInstanceOf(NotFoundInBlocks::class.java) + }) + } +} + class PathfindingStopsAtEndOfBlock : ApiTest() { fun callPathfindingEndpoint( rs: RollingStock,