Skip to content

Commit 086240a

Browse files
kmer2016SharglutDev
authored andcommitted
front, editoast: invalid effort curve if speed has duplicates or if effort/speed have less than 2 coordinates.
front: prevent user to send duplicates coordinates from effortCurve spreadsheet editoast: ensure speed values are unique and strictly increasing front: speed and effort values must be strictly positive, and speed values must be unique editoast: use json as input in all tests of rollin_stock editoast: refacto. + throw an error if curves has less than 2 values front: refacto. test and correct invalidity curve condition front: filter curve before check validity front: display invalid curve front: improve curve identifier front: fix translate front: improve error message style
1 parent a362aa6 commit 086240a

File tree

7 files changed

+216
-37
lines changed

7 files changed

+216
-37
lines changed

editoast/src/schema/rolling_stock/mod.rs

+54-9
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,30 @@ impl<'de> Deserialize<'de> for EffortCurve {
180180
));
181181
}
182182

183+
if inner.max_efforts.len() < 2 {
184+
return Err(serde::de::Error::custom(
185+
"effort curve should have at least 2 points.",
186+
));
187+
}
188+
189+
if inner.max_efforts.iter().any(|&x| x < 0.0) {
190+
return Err(serde::de::Error::custom(
191+
"max_efforts values must be equal or greater than 0.",
192+
));
193+
};
194+
195+
if inner.speeds.iter().any(|&x| x < 0.0) {
196+
return Err(serde::de::Error::custom(
197+
"speeds values must be equal or greater than 0.",
198+
));
199+
};
200+
201+
if inner.speeds.windows(2).any(|window| window[0] >= window[1]) {
202+
return Err(serde::de::Error::custom(
203+
"speeds values must be strictly increasing.",
204+
));
205+
}
206+
183207
Ok(EffortCurve {
184208
speeds: inner.speeds,
185209
max_efforts: inner.max_efforts,
@@ -279,20 +303,41 @@ pub enum SignalingSystem {
279303
#[cfg(test)]
280304
mod tests {
281305
use crate::schema::rolling_stock::EffortCurve;
306+
use serde_json::{from_value, json};
282307

283308
#[test]
284309
fn test_de_effort_curve_valid() {
285-
assert!(serde_json::from_str::<EffortCurve>(
286-
r#"{ "speeds": [0, 1], "max_efforts": [0, 2] }"#
287-
)
288-
.is_ok());
310+
let curve = json!({ "speeds": [0, 1], "max_efforts": [0, 2] });
311+
assert!(from_value::<EffortCurve>(curve).is_ok());
312+
}
313+
314+
#[test]
315+
fn test_effort_curve_invalid_due_to_single_point() {
316+
let curve = json!({ "speeds": [0], "max_efforts": [0] });
317+
assert!(from_value::<EffortCurve>(curve).is_err());
318+
}
319+
320+
#[test]
321+
fn test_de_effort_curve_invalid_due_to_mismatched_lengths() {
322+
let curve = json!({ "speeds": [0, 1], "max_efforts": [] });
323+
assert!(from_value::<EffortCurve>(curve).is_err());
324+
}
325+
326+
#[test]
327+
fn test_de_effort_curve_invalid_due_to_negative_max_efforts() {
328+
let curve = json!({ "speeds": [0, 1, 2], "max_efforts": [5, 4, -3] });
329+
assert!(from_value::<EffortCurve>(curve).is_err());
330+
}
331+
332+
#[test]
333+
fn test_de_effort_curve_invalid_due_to_negative_speeds() {
334+
let curve = json!({ "speeds": [-1, 0, 1], "max_efforts": [5, 4, 3] });
335+
assert!(from_value::<EffortCurve>(curve).is_err());
289336
}
290337

291338
#[test]
292-
fn test_de_effort_curve_unvalid() {
293-
assert!(
294-
serde_json::from_str::<EffortCurve>(r#"{ "speeds": [0, 1], "max_efforts": [] }"#)
295-
.is_err()
296-
);
339+
fn test_de_effort_curve_invalid_due_to_unordered_speeds() {
340+
let curve = json!({ "speeds": [0, 2, 1], "max_efforts": [5, 4, 3] });
341+
assert!(from_value::<EffortCurve>(curve).is_err());
297342
}
298343
}

front/public/locales/en/rollingstock.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@
6969
"rollingStockDuplicateName": "Rolling stock of the same name already exists.",
7070
"rollingStockNotAdded": "Rolling stock not added",
7171
"rollingStockNotUpdated": "Rolling stock not updated",
72-
"rollingStockNotDeleted": "Rolling stock not deleted"
72+
"rollingStockNotDeleted": "Rolling stock not deleted",
73+
"invalidEffortCurves": "Invalid curves: {{invalidEffortCurves}}. Each curve must have at least 2 combinations of speed / effort values (with no duplication of speed values). Combinations with at least one empty field are ignored."
7374
},
7475
"name": "Name",
7576
"no": "No",

front/public/locales/fr/rollingstock.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@
7171
"rollingStockDuplicateName": "Un matériel roulant du même nom existe déjà.",
7272
"rollingStockNotAdded": "Le matériel roulant n'a pas été ajouté.",
7373
"rollingStockNotUpdated": "Le matériel roulant n'a pas été mis à jour.",
74-
"rollingStockNotDeleted": "Le matériel roulant n'a pas été supprimé."
74+
"rollingStockNotDeleted": "Le matériel roulant n'a pas été supprimé.",
75+
"invalidEffortCurves": "Courbes invalides : {{invalidEffortCurves}}. Chaque courbe doit posséder au moins 2 combinaisons de valeurs vitesse / effort (sans doublon sur les valeurs de vitesse). Les combinaisons ayant au moins un champs vide sont ignorées."
7576
},
7677
"name": "Nom",
7778
"no": "Non",

front/src/modules/rollingStock/components/RollingStockEditor/RollingStockEditorForm.tsx

+22-17
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,8 @@ const RollingStockEditorForm = ({
184184
return;
185185
}
186186

187-
const { invalidFields, validRollingStockForm } = checkRollingStockFormValidity(
188-
data,
189-
effortCurves
190-
);
187+
const { invalidFields, validRollingStockForm, invalidEffortCurves } =
188+
checkRollingStockFormValidity(data, effortCurves, t);
191189
if (invalidFields.length) {
192190
setRollingStockValues(validRollingStockForm);
193191
setErrorMessage(
@@ -196,18 +194,27 @@ const RollingStockEditorForm = ({
196194
count: invalidFields.length,
197195
})
198196
);
199-
} else {
200-
setErrorMessage('');
201-
const payload = rollingStockEditorQueryArg(validRollingStockForm, effortCurves!);
202-
openModal(
203-
<RollingStockEditorFormModal
204-
setAddOrEditState={setAddOrEditState}
205-
request={isAdding ? addNewRollingstock(payload) : updateRollingStock(payload)}
206-
mainText={t('confirmUpdateRollingStock')}
207-
buttonText={t('translation:common.yes')}
208-
/>
197+
198+
return;
199+
}
200+
201+
if (invalidEffortCurves.length > 0) {
202+
setErrorMessage(
203+
t('messages.invalidEffortCurves', { invalidEffortCurves: invalidEffortCurves.join(', ') })
209204
);
205+
return;
210206
}
207+
208+
setErrorMessage('');
209+
const payload = rollingStockEditorQueryArg(validRollingStockForm, effortCurves!);
210+
openModal(
211+
<RollingStockEditorFormModal
212+
setAddOrEditState={setAddOrEditState}
213+
request={isAdding ? addNewRollingstock(payload) : updateRollingStock(payload)}
214+
mainText={t('confirmUpdateRollingStock')}
215+
buttonText={t('translation:common.yes')}
216+
/>
217+
);
211218
};
212219

213220
const cancel = () => {
@@ -297,9 +304,7 @@ const RollingStockEditorForm = ({
297304
<Tabs pills fullWidth tabs={[tabRollingStockDetails, tabRollingStockCurves]} />
298305
<div className="d-flex justify-content-end">
299306
<div className="d-flex flex-column justify-content-end">
300-
{errorMessage && (
301-
<p className="text-danger mb-1 ml-auto error-message text-wrap">{errorMessage}</p>
302-
)}
307+
{errorMessage && <p className="text-danger mb-1 p-3">{errorMessage}</p>}
303308
<div className="d-flex justify-content-end">
304309
<button
305310
type="button"

front/src/modules/rollingStock/helpers/__tests__/utils.spec.ts

+94-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ import { checkRollingStockFormValidity, makeEffortCurve } from 'modules/rollingS
22

33
import type { EffortCurves } from 'common/api/osrdEditoastApi';
44
import type { EffortCurveForms, RollingStockParametersValues } from 'modules/rollingStock/types';
5+
import { TFunction } from 'i18next';
6+
7+
function setupEffortCurve(tractionMode: string, max_efforts: number[], speeds: number[]) {
8+
const curves = makeEffortCurve(tractionMode);
9+
curves.curves[0].curve.max_efforts = max_efforts;
10+
curves.curves[0].curve.speeds = speeds;
11+
return curves;
12+
}
13+
const tMock: TFunction = (key: string) => key;
514

615
describe('checkRollingStockFormValidity', () => {
716
describe('Non electric stock', () => {
@@ -39,8 +48,9 @@ describe('checkRollingStockFormValidity', () => {
3948
rollingResistanceB: 0,
4049
rollingResistanceC: 0,
4150
},
51+
invalidEffortCurves: [],
4252
};
43-
const result = checkRollingStockFormValidity(rsForm, effortCurves);
53+
const result = checkRollingStockFormValidity(rsForm, effortCurves, tMock);
4454
expect(result).toEqual(expected);
4555
});
4656
});
@@ -89,8 +99,90 @@ describe('checkRollingStockFormValidity', () => {
8999
raisePantographTime: 15,
90100
basePowerClass: null,
91101
},
102+
invalidEffortCurves: ['comfortTypes.STANDARD > 15000 > unspecified > unspecified'],
103+
};
104+
const result = checkRollingStockFormValidity(rsForm, effortCurves, tMock);
105+
expect(result).toEqual(expected);
106+
});
107+
});
108+
describe('Invalid curve', () => {
109+
const rsForm = {
110+
name: 'auietsrn',
111+
mass: 155,
112+
maxSpeed: 10000,
113+
loadingGauge: 'G1',
114+
basePowerClass: null,
115+
} as RollingStockParametersValues;
116+
const expected = {
117+
invalidFields: [
118+
'length',
119+
'startupAcceleration',
120+
'comfortAcceleration',
121+
'startupTime',
122+
'gammaValue',
123+
'inertiaCoefficient',
124+
'rollingResistanceA',
125+
'rollingResistanceB',
126+
'rollingResistanceC',
127+
'electricalPowerStartupTime',
128+
'raisePantographTime',
129+
],
130+
validRollingStockForm: {
131+
name: 'auietsrn',
132+
length: 0,
133+
mass: 155,
134+
maxSpeed: 10000,
135+
startupAcceleration: 0,
136+
comfortAcceleration: 0.01,
137+
startupTime: 0,
138+
gammaValue: 0.01,
139+
inertiaCoefficient: 1,
140+
rollingResistanceA: 0,
141+
rollingResistanceB: 0,
142+
rollingResistanceC: 0,
143+
loadingGauge: 'G1',
144+
electricalPowerStartupTime: 5,
145+
raisePantographTime: 15,
146+
basePowerClass: null,
147+
},
148+
invalidEffortCurves: [],
149+
};
150+
it('should return invalidEffortCurves as true when any curve contains fewer than two values', () => {
151+
const effortCurves: EffortCurveForms = {
152+
'15000': setupEffortCurve('1500', [1], [10]),
153+
'1000': setupEffortCurve('1000', [3, 4, 3], [30, 40, 50]),
92154
};
93-
const result = checkRollingStockFormValidity(rsForm, effortCurves);
155+
156+
const result = checkRollingStockFormValidity(rsForm, effortCurves, tMock);
157+
expect(result).toEqual({
158+
...expected,
159+
invalidEffortCurves: ['comfortTypes.STANDARD > 15000 > unspecified > unspecified'],
160+
});
161+
});
162+
163+
it('should return invalidEffortCurves as true when any curve includes duplicate speed values', () => {
164+
const effortCurves: EffortCurveForms = {
165+
'15000': setupEffortCurve('1500', [1, 2, 1], [10, 20, 10]),
166+
'1000': setupEffortCurve('1000', [3, 4, 3], [30, 40, 30]),
167+
};
168+
169+
const result = checkRollingStockFormValidity(rsForm, effortCurves, tMock);
170+
expect(result).toEqual({
171+
...expected,
172+
invalidEffortCurves: [
173+
'comfortTypes.STANDARD > 1000 > unspecified > unspecified',
174+
'comfortTypes.STANDARD > 15000 > unspecified > unspecified',
175+
],
176+
});
177+
});
178+
179+
it('should return invalidEffortCurves as false when all curves contain unique speed values and at least two values each', () => {
180+
const effortCurves: EffortCurveForms = {
181+
'15000': setupEffortCurve('1500', [1, 2, 1], [10, 20, 30]),
182+
'1000': setupEffortCurve('1000', [3, 4, 3], [30, 40, 50]),
183+
};
184+
185+
const result = checkRollingStockFormValidity(rsForm, effortCurves, tMock);
94186
expect(result).toEqual(expected);
95187
});
96188
});

front/src/modules/rollingStock/helpers/utils.ts

+42-2
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,13 @@ type Conditions = Record<string, (effortCurves: EffortCurveForms | null) => bool
176176

177177
export const checkRollingStockFormValidity = (
178178
rollingStockForm: RollingStockParametersValues,
179-
effortCurves: EffortCurveForms | null
180-
): { invalidFields: string[]; validRollingStockForm: RollingStockParametersValidValues } => {
179+
effortCurves: EffortCurveForms | null,
180+
t: TFunction
181+
): {
182+
invalidFields: string[];
183+
validRollingStockForm: RollingStockParametersValidValues;
184+
invalidEffortCurves: string[];
185+
} => {
181186
const conditions = RS_SCHEMA_PROPERTIES.reduce<Conditions>((acc, val) => {
182187
if (val.condition) {
183188
return { ...acc, [val.title]: val.condition };
@@ -196,15 +201,50 @@ export const checkRollingStockFormValidity = (
196201
return false;
197202
});
198203

204+
let invalidEffortCurves: string[] = [];
205+
Object.entries(effortCurves || {}).forEach(([mode, { curves }]) => {
206+
curves.forEach(
207+
({ curve, cond: { comfort, electrical_profile_level, power_restriction_code } }) => {
208+
const filteredCurve = filterUndefinedValueInCurve(curve);
209+
210+
if (
211+
filteredCurve.max_efforts.length < 2 ||
212+
filteredCurve.speeds.length < 2 ||
213+
new Set(filteredCurve.speeds).size !== filteredCurve.speeds.length
214+
) {
215+
const formattedComfort = formatCurveCondition(comfort, t, 'comfortTypes');
216+
const formattedElecProfile = formatCurveCondition(electrical_profile_level, t);
217+
const formattedResCode = formatCurveCondition(power_restriction_code, t);
218+
219+
invalidEffortCurves = [
220+
...invalidEffortCurves,
221+
`${formattedComfort} > ${t(mode)} > ${formattedElecProfile} > ${formattedResCode}`,
222+
];
223+
}
224+
}
225+
);
226+
});
227+
199228
return {
200229
invalidFields,
201230
validRollingStockForm: {
202231
...pick(RS_REQUIRED_FIELDS, invalidFields),
203232
...omit(rollingStockForm, invalidFields),
204233
} as RollingStockParametersValidValues,
234+
invalidEffortCurves,
205235
};
206236
};
207237

238+
const formatCurveCondition = (
239+
conditionValue: string | null,
240+
t: TFunction,
241+
translationCategory?: string
242+
): string => {
243+
if (conditionValue === null) return t('unspecified');
244+
if (translationCategory) return t(getTranslationKey(translationCategory, conditionValue));
245+
return t(conditionValue);
246+
};
247+
208248
export const createEmptyCurve = (
209249
comfort: RollingStockComfortType,
210250
electricalProfile: string | null = null,

front/src/styles/scss/applications/rollingStockEditor/_rollingStockForm.scss

-5
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,6 @@
6262
}
6363
}
6464

65-
.error-message {
66-
padding: 20px 0;
67-
max-width: 80%;
68-
}
69-
7065
.rollingstock-card-body {
7166
height: 18rem;
7267
}

0 commit comments

Comments
 (0)