Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

editoast: add consist params validation #10332

Merged
merged 1 commit into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions editoast/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4825,6 +4825,8 @@ components:
- $ref: '#/components/schemas/EditoastSpriteErrorsFileNotFound'
- $ref: '#/components/schemas/EditoastSpriteErrorsUnknownSignalingSystem'
- $ref: '#/components/schemas/EditoastStdcmErrorInfraNotFound'
- $ref: '#/components/schemas/EditoastStdcmErrorInvalidConsistLength'
- $ref: '#/components/schemas/EditoastStdcmErrorInvalidConsistMass'
- $ref: '#/components/schemas/EditoastStdcmErrorInvalidPathItems'
- $ref: '#/components/schemas/EditoastStdcmErrorRollingStockNotFound'
- $ref: '#/components/schemas/EditoastStdcmErrorTimetableNotFound'
Expand Down Expand Up @@ -6018,6 +6020,54 @@ components:
type: string
enum:
- editoast:stdcm_v2:InfraNotFound
EditoastStdcmErrorInvalidConsistLength:
type: object
required:
- type
- status
- message
properties:
context:
type: object
required:
- message
properties:
message:
type: string
message:
type: string
status:
type: integer
enum:
- 400
type:
type: string
enum:
- editoast:stdcm_v2:InvalidConsistLength
EditoastStdcmErrorInvalidConsistMass:
type: object
required:
- type
- status
- message
properties:
context:
type: object
required:
- message
properties:
message:
type: string
message:
type: string
status:
type: integer
enum:
- 400
type:
type: string
enum:
- editoast:stdcm_v2:InvalidConsistMass
EditoastStdcmErrorInvalidPathItems:
type: object
required:
Expand Down
180 changes: 169 additions & 11 deletions editoast/src/views/timetable/stdcm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ use tracing::Instrument;
use tracing_opentelemetry::OpenTelemetrySpanExt;
use utoipa::IntoParams;
use utoipa::ToSchema;
use validator::Validate;

use crate::core::conflict_detection::Conflict;
use crate::core::conflict_detection::TrainRequirements;
Expand Down Expand Up @@ -100,6 +99,10 @@ enum StdcmError {
TrainSimulationFail,
#[error("Path items are invalid")]
InvalidPathItems { items: Vec<InvalidPathItem> },
#[error("Invalid consist mass: {message}")]
InvalidConsistMass { message: String },
#[error("Invalid consist length: {message}")]
InvalidConsistLength { message: String },
Comment on lines +102 to +105
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would have been great to have numerical values instead of a generic message string :/

Because of that, 1) the full error message is split in several places and 2) in tests you have to assert_eq on strings instead of numerical values which are more stable.

}

#[derive(Debug, Default, Clone, Serialize, Deserialize, IntoParams, ToSchema)]
Expand Down Expand Up @@ -156,7 +159,6 @@ async fn stdcm(

let trace_id = Some(trace_id).filter(|trace_id| *trace_id != TraceId::INVALID);

stdcm_request.validate()?;
let mut conn = db_pool.get().await?;

let timetable_id = id;
Expand All @@ -178,14 +180,18 @@ async fn stdcm(
.await?
.into();

let towed_rolling_stock = stdcm_request
.get_towed_rolling_stock(&mut conn)
.await?
.map(From::from);

stdcm_request.validate_consist(&rolling_stock, &towed_rolling_stock)?;

let physics_consist_parameters = PhysicsConsistParameters {
max_speed: stdcm_request.max_speed,
total_length: stdcm_request.total_length,
total_mass: stdcm_request.total_mass,
towed_rolling_stock: stdcm_request
.get_towed_rolling_stock(&mut conn)
.await?
.map(From::from),
towed_rolling_stock,
traction_engine: rolling_stock,
};

Expand Down Expand Up @@ -524,6 +530,10 @@ mod tests {
use rstest::rstest;
use serde_json::json;
use std::str::FromStr;
use uom::si::length::meter;
use uom::si::length::Length;
use uom::si::mass::kilogram;
use uom::si::quantities::Mass;
use uuid::Uuid;

use crate::core::conflict_detection::Conflict;
Expand All @@ -535,6 +545,7 @@ mod tests {
use crate::core::simulation::PhysicsConsist;
use crate::core::simulation::ReportTrain;
use crate::core::simulation::SpeedLimitProperties;
use crate::error::InternalError;
use crate::models::fixtures::create_fast_rolling_stock;
use crate::models::fixtures::create_simple_rolling_stock;
use crate::models::fixtures::create_small_infra;
Expand All @@ -551,7 +562,12 @@ mod tests {

use super::*;

fn get_stdcm_payload(rolling_stock_id: i64, work_schedule_group_id: Option<i64>) -> Request {
fn get_stdcm_payload(
rolling_stock_id: i64,
work_schedule_group_id: Option<i64>,
total_mass: Option<f64>,
total_length: Option<f64>,
) -> Request {
Request {
start_time: Some(
DateTime::from_str("2024-01-01T10:00:00Z").expect("Failed to parse datetime"),
Expand Down Expand Up @@ -601,8 +617,8 @@ mod tests {
time_gap_before: 35000,
time_gap_after: 35000,
margin: Some(MarginValue::MinPer100Km(4.5)),
total_mass: None,
total_length: None,
total_mass: total_mass.map(Mass::new::<kilogram>),
total_length: total_length.map(Length::new::<meter>),
max_speed: None,
loading_gauge_type: None,
}
Expand Down Expand Up @@ -883,7 +899,147 @@ mod tests {

let request = app
.post(format!("/timetable/{}/stdcm?infra={}", timetable.id, small_infra.id).as_str())
.json(&get_stdcm_payload(rolling_stock.id, None));
.json(&get_stdcm_payload(rolling_stock.id, None, None, None));

let stdcm_response: StdcmResponse =
app.fetch(request).assert_status(StatusCode::OK).json_into();

if let PathfindingResult::Success(path) =
PathfindingResult::Success(pathfinding_result_success())
{
assert_eq!(
stdcm_response,
StdcmResponse::Success {
simulation: simulation_response(),
path,
departure_time: DateTime::from_str("2024-01-02T00:00:00Z")
.expect("Failed to parse datetime")
}
);
}
}

#[rstest]
async fn stdcm_request_mass_validation() {
let db_pool = DbConnectionPoolV2::for_tests();
let mut core = core_mocking_client();
core.stub("/v2/stdcm")
.method(reqwest::Method::POST)
.response(StatusCode::OK)
.json(crate::core::stdcm::Response::Success {
simulation: simulation_response(),
path: pathfinding_result_success(),
departure_time: DateTime::from_str("2024-01-02T00:00:00Z")
.expect("Failed to parse datetime"),
})
.finish();

let app = TestAppBuilder::new()
.db_pool(db_pool.clone())
.core_client(core.into())
.build();
let small_infra = create_small_infra(&mut db_pool.get_ok()).await;
let timetable = create_timetable(&mut db_pool.get_ok()).await;
let rolling_stock =
create_fast_rolling_stock(&mut db_pool.get_ok(), &Uuid::new_v4().to_string()).await;

let total_mass = Some(80_000.0);
let request = app
.post(format!("/timetable/{}/stdcm?infra={}", timetable.id, small_infra.id).as_str())
.json(&get_stdcm_payload(rolling_stock.id, None, total_mass, None));

let stdcm_response: InternalError = app
.fetch(request)
.assert_status(StatusCode::BAD_REQUEST)
.json_into();

assert_eq!(
stdcm_response.message,
"Invalid consist mass: The total mass must be greater than the sum of the rolling stock masses (900000 kilograms)"
.to_owned()
);
}

#[rstest]
async fn stdcm_request_length_validation() {
let db_pool = DbConnectionPoolV2::for_tests();
let mut core = core_mocking_client();
core.stub("/v2/stdcm")
.method(reqwest::Method::POST)
.response(StatusCode::OK)
.json(crate::core::stdcm::Response::Success {
simulation: simulation_response(),
path: pathfinding_result_success(),
departure_time: DateTime::from_str("2024-01-02T00:00:00Z")
.expect("Failed to parse datetime"),
})
.finish();

let app = TestAppBuilder::new()
.db_pool(db_pool.clone())
.core_client(core.into())
.build();
let small_infra = create_small_infra(&mut db_pool.get_ok()).await;
let timetable = create_timetable(&mut db_pool.get_ok()).await;
let rolling_stock =
create_fast_rolling_stock(&mut db_pool.get_ok(), &Uuid::new_v4().to_string()).await;

let total_length = Some(300.0);
let request = app
.post(format!("/timetable/{}/stdcm?infra={}", timetable.id, small_infra.id).as_str())
.json(&get_stdcm_payload(
rolling_stock.id,
None,
None,
total_length,
));

let stdcm_response: InternalError = app
.fetch(request)
.assert_status(StatusCode::BAD_REQUEST)
.json_into();

assert_eq!(
stdcm_response.message,
"Invalid consist length: The total length must be greater than the sum of the rolling stock lengths (400 meters)"
.to_owned()
);
}

#[rstest]
async fn stdcm_request_validation_success() {
let db_pool = DbConnectionPoolV2::for_tests();
let mut core = core_mocking_client();
core.stub("/v2/stdcm")
.method(reqwest::Method::POST)
.response(StatusCode::OK)
.json(crate::core::stdcm::Response::Success {
simulation: simulation_response(),
path: pathfinding_result_success(),
departure_time: DateTime::from_str("2024-01-02T00:00:00Z")
.expect("Failed to parse datetime"),
})
.finish();

let app = TestAppBuilder::new()
.db_pool(db_pool.clone())
.core_client(core.into())
.build();
let small_infra = create_small_infra(&mut db_pool.get_ok()).await;
let timetable = create_timetable(&mut db_pool.get_ok()).await;
let rolling_stock =
create_fast_rolling_stock(&mut db_pool.get_ok(), &Uuid::new_v4().to_string()).await;

let total_length = Some(410.0);
let total_mass = Some(910_000.0);
let request = app
.post(format!("/timetable/{}/stdcm?infra={}", timetable.id, small_infra.id).as_str())
.json(&get_stdcm_payload(
rolling_stock.id,
None,
total_mass,
total_length,
));

let stdcm_response: StdcmResponse =
app.fetch(request).assert_status(StatusCode::OK).json_into();
Expand Down Expand Up @@ -931,7 +1087,7 @@ mod tests {

let request = app
.post(format!("/timetable/{}/stdcm?infra={}", timetable.id, small_infra.id).as_str())
.json(&get_stdcm_payload(rolling_stock.id, None));
.json(&get_stdcm_payload(rolling_stock.id, None, None, None));

let stdcm_response: StdcmResponse =
app.fetch(request).assert_status(StatusCode::OK).json_into();
Expand Down Expand Up @@ -1002,6 +1158,8 @@ mod tests {
.json(&get_stdcm_payload(
rolling_stock.id,
Some(work_schedule_group.id),
None,
None,
));

let stdcm_response: StdcmResponse =
Expand Down
Loading
Loading