1use std::path::Path;
28
29use super::{ErrorKind, ValidationContext};
30
31#[allow(clippy::struct_excessive_bools)]
39#[derive(Debug, Clone, Default)]
40pub struct FileManifest {
41 pub config_json: bool,
43 pub penalties_json: bool,
45 pub stages_json: bool,
47 pub initial_conditions_json: bool,
49 pub post_study_stages_json: bool,
51
52 pub system_buses_json: bool,
54 pub system_lines_json: bool,
56 pub system_hydros_json: bool,
58 pub system_thermals_json: bool,
60 pub system_non_controllable_sources_json: bool,
62 pub system_pumping_stations_json: bool,
64 pub system_energy_contracts_json: bool,
66 pub system_hydro_geometry_parquet: bool,
68 pub system_hydro_production_models_json: bool,
70 pub system_fpha_hyperplanes_parquet: bool,
72 pub system_hydro_energy_productivity_parquet: bool,
74 pub system_tailrace_curves_parquet: bool,
76
77 pub scenarios_inflow_history_parquet: bool,
79 pub scenarios_inflow_seasonal_stats_parquet: bool,
81 pub scenarios_inflow_ar_coefficients_parquet: bool,
83 pub scenarios_inflow_annual_component_parquet: bool,
85 pub scenarios_external_inflow_scenarios_parquet: bool,
87 pub scenarios_external_load_scenarios_parquet: bool,
89 pub scenarios_external_ncs_scenarios_parquet: bool,
91 pub scenarios_load_seasonal_stats_parquet: bool,
93 pub scenarios_load_factors_json: bool,
95 pub scenarios_correlation_json: bool,
97 pub scenarios_non_controllable_factors_json: bool,
99 pub scenarios_non_controllable_stats_parquet: bool,
101
102 pub constraints_thermal_bounds_parquet: bool,
104 pub constraints_hydro_bounds_parquet: bool,
106 pub constraints_line_bounds_parquet: bool,
108 pub constraints_pumping_bounds_parquet: bool,
110 pub constraints_contract_bounds_parquet: bool,
112 pub constraints_generic_constraints_json: bool,
114 pub constraints_generic_constraint_bounds_parquet: bool,
116 pub constraints_generic_parameters_json: bool,
118 pub constraints_penalty_overrides_bus_parquet: bool,
120 pub constraints_penalty_overrides_line_parquet: bool,
122 pub constraints_penalty_overrides_hydro_parquet: bool,
124 pub constraints_penalty_overrides_ncs_parquet: bool,
126 pub constraints_ncs_bounds_parquet: bool,
128 pub constraints_hydro_unit_group_bounds_parquet: bool,
130}
131
132struct FileEntry {
136 relative: &'static str,
138 required: bool,
139}
140
141const FILE_ENTRIES: &[FileEntry] = &[
144 FileEntry {
146 relative: "config.json",
147 required: true,
148 },
149 FileEntry {
150 relative: "penalties.json",
151 required: true,
152 },
153 FileEntry {
154 relative: "stages.json",
155 required: true,
156 },
157 FileEntry {
158 relative: "initial_conditions.json",
159 required: true,
160 },
161 FileEntry {
163 relative: "post_study_stages.json",
164 required: false,
165 },
166 FileEntry {
168 relative: "system/buses.json",
169 required: true,
170 },
171 FileEntry {
172 relative: "system/lines.json",
173 required: true,
174 },
175 FileEntry {
176 relative: "system/hydros.json",
177 required: true,
178 },
179 FileEntry {
180 relative: "system/thermals.json",
181 required: true,
182 },
183 FileEntry {
185 relative: "system/non_controllable_sources.json",
186 required: false,
187 },
188 FileEntry {
189 relative: "system/pumping_stations.json",
190 required: false,
191 },
192 FileEntry {
193 relative: "system/energy_contracts.json",
194 required: false,
195 },
196 FileEntry {
197 relative: "system/hydro_geometry.parquet",
198 required: false,
199 },
200 FileEntry {
201 relative: "system/hydro_production_models.json",
202 required: false,
203 },
204 FileEntry {
205 relative: "system/fpha_hyperplanes.parquet",
206 required: false,
207 },
208 FileEntry {
209 relative: "system/hydro_energy_productivity.parquet",
210 required: false,
211 },
212 FileEntry {
213 relative: "system/tailrace_curves.parquet",
214 required: false,
215 },
216 FileEntry {
218 relative: "scenarios/inflow_history.parquet",
219 required: false,
220 },
221 FileEntry {
222 relative: "scenarios/inflow_seasonal_stats.parquet",
223 required: false,
224 },
225 FileEntry {
226 relative: "scenarios/inflow_ar_coefficients.parquet",
227 required: false,
228 },
229 FileEntry {
230 relative: "scenarios/inflow_annual_component.parquet",
231 required: false,
232 },
233 FileEntry {
234 relative: "scenarios/external_inflow_scenarios.parquet",
235 required: false,
236 },
237 FileEntry {
238 relative: "scenarios/external_load_scenarios.parquet",
239 required: false,
240 },
241 FileEntry {
242 relative: "scenarios/external_ncs_scenarios.parquet",
243 required: false,
244 },
245 FileEntry {
246 relative: "scenarios/load_seasonal_stats.parquet",
247 required: false,
248 },
249 FileEntry {
250 relative: "scenarios/load_factors.json",
251 required: false,
252 },
253 FileEntry {
254 relative: "scenarios/correlation.json",
255 required: false,
256 },
257 FileEntry {
258 relative: "scenarios/non_controllable_factors.json",
259 required: false,
260 },
261 FileEntry {
262 relative: "scenarios/non_controllable_stats.parquet",
263 required: false,
264 },
265 FileEntry {
267 relative: "constraints/thermal_bounds.parquet",
268 required: false,
269 },
270 FileEntry {
271 relative: "constraints/hydro_bounds.parquet",
272 required: false,
273 },
274 FileEntry {
275 relative: "constraints/line_bounds.parquet",
276 required: false,
277 },
278 FileEntry {
279 relative: "constraints/pumping_bounds.parquet",
280 required: false,
281 },
282 FileEntry {
283 relative: "constraints/contract_bounds.parquet",
284 required: false,
285 },
286 FileEntry {
287 relative: "constraints/generic_constraints.json",
288 required: false,
289 },
290 FileEntry {
291 relative: "constraints/generic_constraint_bounds.parquet",
292 required: false,
293 },
294 FileEntry {
295 relative: "constraints/generic_parameters.json",
296 required: false,
297 },
298 FileEntry {
299 relative: "constraints/penalty_overrides_bus.parquet",
300 required: false,
301 },
302 FileEntry {
303 relative: "constraints/penalty_overrides_line.parquet",
304 required: false,
305 },
306 FileEntry {
307 relative: "constraints/penalty_overrides_hydro.parquet",
308 required: false,
309 },
310 FileEntry {
311 relative: "constraints/penalty_overrides_ncs.parquet",
312 required: false,
313 },
314 FileEntry {
315 relative: "constraints/ncs_bounds.parquet",
316 required: false,
317 },
318 FileEntry {
319 relative: "constraints/hydro_unit_group_bounds.parquet",
320 required: false,
321 },
322];
323
324struct RemovedFile {
327 relative: &'static str,
329 replacement: &'static str,
330}
331
332const REMOVED_FILES: &[RemovedFile] = &[
335 RemovedFile {
336 relative: "constraints/exchange_factors.json",
337 replacement: "per-block line capacity is now declared as absolute MW in \
338 constraints/line_bounds.parquet via direct_mw / reverse_mw rows \
339 carrying a block_id (use base_capacity * factor for each block). \
340 Remove the file.",
341 },
342 RemovedFile {
343 relative: "system/scalar_parameters.json",
344 replacement: "scalar parameters are now read from \
345 constraints/generic_parameters.json, beside the constraints that \
346 reference them by @name. Move the file (its contents are unchanged).",
347 },
348];
349
350#[must_use]
359pub fn validate_structure(case_root: &Path, ctx: &mut ValidationContext) -> FileManifest {
360 let mut manifest = FileManifest::default();
361
362 for removed in REMOVED_FILES {
363 if case_root.join(removed.relative).exists() {
364 ctx.add_error(
365 ErrorKind::BusinessRuleViolation,
366 removed.relative,
367 None::<&str>,
368 format!(
369 "{} is no longer read; {}",
370 removed.relative, removed.replacement
371 ),
372 );
373 }
374 }
375
376 for (entry, present) in FILE_ENTRIES.iter().zip(manifest_fields_mut(&mut manifest)) {
377 if case_root.join(entry.relative).exists() {
378 *present = true;
379 } else if entry.required {
380 ctx.add_error(
381 ErrorKind::FileNotFound,
382 entry.relative,
383 None::<&str>,
384 format!(
385 "required file '{}' not found in case directory",
386 entry.relative
387 ),
388 );
389 }
390 }
391
392 manifest
393}
394
395fn manifest_fields_mut(m: &mut FileManifest) -> [&mut bool; 43] {
399 [
400 &mut m.config_json,
402 &mut m.penalties_json,
403 &mut m.stages_json,
404 &mut m.initial_conditions_json,
405 &mut m.post_study_stages_json,
406 &mut m.system_buses_json,
408 &mut m.system_lines_json,
409 &mut m.system_hydros_json,
410 &mut m.system_thermals_json,
411 &mut m.system_non_controllable_sources_json,
413 &mut m.system_pumping_stations_json,
414 &mut m.system_energy_contracts_json,
415 &mut m.system_hydro_geometry_parquet,
416 &mut m.system_hydro_production_models_json,
417 &mut m.system_fpha_hyperplanes_parquet,
418 &mut m.system_hydro_energy_productivity_parquet,
419 &mut m.system_tailrace_curves_parquet,
420 &mut m.scenarios_inflow_history_parquet,
422 &mut m.scenarios_inflow_seasonal_stats_parquet,
423 &mut m.scenarios_inflow_ar_coefficients_parquet,
424 &mut m.scenarios_inflow_annual_component_parquet,
425 &mut m.scenarios_external_inflow_scenarios_parquet,
426 &mut m.scenarios_external_load_scenarios_parquet,
427 &mut m.scenarios_external_ncs_scenarios_parquet,
428 &mut m.scenarios_load_seasonal_stats_parquet,
429 &mut m.scenarios_load_factors_json,
430 &mut m.scenarios_correlation_json,
431 &mut m.scenarios_non_controllable_factors_json,
432 &mut m.scenarios_non_controllable_stats_parquet,
433 &mut m.constraints_thermal_bounds_parquet,
435 &mut m.constraints_hydro_bounds_parquet,
436 &mut m.constraints_line_bounds_parquet,
437 &mut m.constraints_pumping_bounds_parquet,
438 &mut m.constraints_contract_bounds_parquet,
439 &mut m.constraints_generic_constraints_json,
440 &mut m.constraints_generic_constraint_bounds_parquet,
441 &mut m.constraints_generic_parameters_json,
442 &mut m.constraints_penalty_overrides_bus_parquet,
443 &mut m.constraints_penalty_overrides_line_parquet,
444 &mut m.constraints_penalty_overrides_hydro_parquet,
445 &mut m.constraints_penalty_overrides_ncs_parquet,
446 &mut m.constraints_ncs_bounds_parquet,
447 &mut m.constraints_hydro_unit_group_bounds_parquet,
448 ]
449}
450
451#[cfg(test)]
454#[allow(clippy::unwrap_used)]
455mod tests {
456 use super::*;
457 use std::fs;
458 use tempfile::TempDir;
459
460 fn make_case_with_required(dir: &TempDir) {
462 let root = dir.path();
463 fs::create_dir_all(root.join("system")).unwrap();
464 fs::write(root.join("config.json"), b"{}").unwrap();
465 fs::write(root.join("penalties.json"), b"{}").unwrap();
466 fs::write(root.join("stages.json"), b"{}").unwrap();
467 fs::write(root.join("initial_conditions.json"), b"{}").unwrap();
468 fs::write(root.join("system/buses.json"), b"{}").unwrap();
469 fs::write(root.join("system/lines.json"), b"{}").unwrap();
470 fs::write(root.join("system/hydros.json"), b"{}").unwrap();
471 fs::write(root.join("system/thermals.json"), b"{}").unwrap();
472 }
473
474 #[test]
475 fn test_structural_all_required_present() {
476 let dir = TempDir::new().unwrap();
477 make_case_with_required(&dir);
478
479 let mut ctx = ValidationContext::new();
480 let manifest = validate_structure(dir.path(), &mut ctx);
481
482 assert!(
483 !ctx.has_errors(),
484 "should have 0 errors when all required files present, got: {:?}",
485 ctx.errors()
486 );
487
488 assert!(manifest.config_json, "config.json should be present");
489 assert!(manifest.penalties_json, "penalties.json should be present");
490 assert!(manifest.stages_json, "stages.json should be present");
491 assert!(
492 manifest.initial_conditions_json,
493 "initial_conditions.json should be present"
494 );
495 assert!(
496 manifest.system_buses_json,
497 "system/buses.json should be present"
498 );
499 assert!(
500 manifest.system_lines_json,
501 "system/lines.json should be present"
502 );
503 assert!(
504 manifest.system_hydros_json,
505 "system/hydros.json should be present"
506 );
507 assert!(
508 manifest.system_thermals_json,
509 "system/thermals.json should be present"
510 );
511 }
512
513 #[test]
514 fn test_structural_missing_required_hydros() {
515 let dir = TempDir::new().unwrap();
516 make_case_with_required(&dir);
517 fs::remove_file(dir.path().join("system/hydros.json")).unwrap();
518
519 let mut ctx = ValidationContext::new();
520 let manifest = validate_structure(dir.path(), &mut ctx);
521
522 assert!(
523 ctx.has_errors(),
524 "should have at least 1 error when system/hydros.json is missing"
525 );
526 assert_eq!(ctx.errors().len(), 1, "should have exactly 1 error");
527 let entry = &ctx.errors()[0];
528 assert_eq!(
529 entry.kind,
530 ErrorKind::FileNotFound,
531 "error kind should be FileNotFound"
532 );
533 assert!(
534 entry.file.to_string_lossy().contains("hydros.json"),
535 "error file should reference hydros.json, got: {}",
536 entry.file.display()
537 );
538 assert!(
539 !manifest.system_hydros_json,
540 "manifest.system_hydros_json should be false"
541 );
542 }
543
544 #[test]
545 fn test_structural_optional_absent_no_error() {
546 let dir = TempDir::new().unwrap();
547 make_case_with_required(&dir);
548 let mut ctx = ValidationContext::new();
551 let manifest = validate_structure(dir.path(), &mut ctx);
552
553 assert!(
554 !ctx.has_errors(),
555 "absent optional files should not produce errors"
556 );
557
558 assert!(!manifest.system_non_controllable_sources_json);
560 assert!(!manifest.system_hydro_geometry_parquet);
561 assert!(!manifest.scenarios_inflow_history_parquet);
562 assert!(!manifest.constraints_thermal_bounds_parquet);
563 assert!(!manifest.scenarios_correlation_json);
564 }
565
566 #[test]
567 fn test_structural_optional_present_in_manifest() {
568 let dir = TempDir::new().unwrap();
569 make_case_with_required(&dir);
570 let scenarios_dir = dir.path().join("scenarios");
571 fs::create_dir_all(&scenarios_dir).unwrap();
572 fs::write(scenarios_dir.join("correlation.json"), b"{}").unwrap();
573
574 let mut ctx = ValidationContext::new();
575 let manifest = validate_structure(dir.path(), &mut ctx);
576
577 assert!(!ctx.has_errors());
578 assert!(
579 manifest.scenarios_correlation_json,
580 "present optional file should be marked true in manifest"
581 );
582 }
583
584 #[test]
585 fn test_structural_multiple_missing_required() {
586 let dir = TempDir::new().unwrap();
587 fs::create_dir_all(dir.path().join("system")).unwrap();
589
590 let mut ctx = ValidationContext::new();
591 let _manifest = validate_structure(dir.path(), &mut ctx);
592
593 assert!(
594 ctx.has_errors(),
595 "should have errors for all 8 missing required files"
596 );
597 assert_eq!(
598 ctx.errors().len(),
599 8,
600 "should have exactly 8 errors (one per required file), got: {}",
601 ctx.errors().len()
602 );
603 for entry in ctx.errors() {
604 assert_eq!(
605 entry.kind,
606 ErrorKind::FileNotFound,
607 "all errors should be FileNotFound"
608 );
609 }
610 }
611
612 #[test]
613 fn test_structural_manifest_fields_count() {
614 let mut manifest = FileManifest::default();
617 let fields = manifest_fields_mut(&mut manifest);
618 assert_eq!(
619 FILE_ENTRIES.len(),
620 fields.len(),
621 "FILE_ENTRIES and manifest_fields_mut must return the same length"
622 );
623 }
624
625 #[test]
626 fn test_scalar_parameters_json_and_hydro_energy_productivity_present() {
627 let dir = TempDir::new().unwrap();
628 make_case_with_required(&dir);
629 fs::create_dir_all(dir.path().join("constraints")).unwrap();
630 fs::write(
631 dir.path().join("constraints/generic_parameters.json"),
632 b"{\"scalar_parameters\":[]}",
633 )
634 .unwrap();
635 fs::write(
636 dir.path().join("system/hydro_energy_productivity.parquet"),
637 b"",
638 )
639 .unwrap();
640
641 let mut ctx = ValidationContext::new();
642 let manifest = validate_structure(dir.path(), &mut ctx);
643
644 assert!(
645 !ctx.has_errors(),
646 "no errors expected when all required files present"
647 );
648 assert!(
649 manifest.constraints_generic_parameters_json,
650 "constraints_generic_parameters_json should be true"
651 );
652 assert!(
653 manifest.system_hydro_energy_productivity_parquet,
654 "system_hydro_energy_productivity_parquet should be true"
655 );
656 }
657
658 #[test]
659 fn test_scalar_parameters_json_and_hydro_energy_productivity_absent() {
660 let dir = TempDir::new().unwrap();
661 make_case_with_required(&dir);
662 let mut ctx = ValidationContext::new();
665 let manifest = validate_structure(dir.path(), &mut ctx);
666
667 assert!(
668 !ctx.has_errors(),
669 "absent optional files must not produce errors"
670 );
671 assert!(
672 !manifest.constraints_generic_parameters_json,
673 "constraints_generic_parameters_json should be false when file is absent"
674 );
675 assert!(
676 !manifest.system_hydro_energy_productivity_parquet,
677 "system_hydro_energy_productivity_parquet should be false when file is absent"
678 );
679 }
680
681 #[test]
684 fn manifest_detects_scalar_parameters_json_when_present() {
685 let dir = TempDir::new().unwrap();
686 make_case_with_required(&dir);
687 fs::create_dir_all(dir.path().join("constraints")).unwrap();
688 fs::write(
689 dir.path().join("constraints/generic_parameters.json"),
690 b"{\"scalar_parameters\":[]}",
691 )
692 .unwrap();
693
694 let mut ctx = ValidationContext::new();
695 let manifest = validate_structure(dir.path(), &mut ctx);
696
697 assert!(
698 !ctx.has_errors(),
699 "no errors expected when all required files are present"
700 );
701 assert!(
702 manifest.constraints_generic_parameters_json,
703 "constraints_generic_parameters_json must be true when constraints/generic_parameters.json exists"
704 );
705 }
706
707 #[test]
710 fn manifest_reports_absent_when_no_parameter_file() {
711 let dir = TempDir::new().unwrap();
712 make_case_with_required(&dir);
713 let mut ctx = ValidationContext::new();
716 let manifest = validate_structure(dir.path(), &mut ctx);
717
718 assert!(
719 !ctx.has_errors(),
720 "absent optional file must not produce errors"
721 );
722 assert!(
723 !manifest.constraints_generic_parameters_json,
724 "constraints_generic_parameters_json must be false when constraints/generic_parameters.json is absent"
725 );
726 }
727
728 #[test]
729 fn test_manifest_tailrace_curves_present() {
730 let dir = TempDir::new().unwrap();
731 make_case_with_required(&dir);
732 fs::write(dir.path().join("system/tailrace_curves.parquet"), b"").unwrap();
733
734 let mut ctx = ValidationContext::new();
735 let manifest = validate_structure(dir.path(), &mut ctx);
736
737 assert!(
738 !ctx.has_errors(),
739 "present optional file should not produce errors"
740 );
741 assert!(
742 manifest.system_tailrace_curves_parquet,
743 "system_tailrace_curves_parquet should be true when file is present"
744 );
745 }
746
747 #[test]
748 fn test_manifest_tailrace_curves_absent() {
749 let dir = TempDir::new().unwrap();
750 make_case_with_required(&dir);
751 let mut ctx = ValidationContext::new();
754 let manifest = validate_structure(dir.path(), &mut ctx);
755
756 assert!(
757 !ctx.has_errors(),
758 "absent optional file should not produce errors"
759 );
760 assert!(
761 !manifest.system_tailrace_curves_parquet,
762 "system_tailrace_curves_parquet should be false when file is absent"
763 );
764 }
765
766 #[test]
768 fn test_manifest_detects_inflow_annual_component_present() {
769 let dir = TempDir::new().unwrap();
770 make_case_with_required(&dir);
771 let scenarios_dir = dir.path().join("scenarios");
772 fs::create_dir_all(&scenarios_dir).unwrap();
773 fs::write(scenarios_dir.join("inflow_annual_component.parquet"), b"").unwrap();
774
775 let mut ctx = ValidationContext::new();
776 let manifest = validate_structure(dir.path(), &mut ctx);
777
778 assert!(
779 !ctx.has_errors(),
780 "present optional file should not produce errors"
781 );
782 assert!(
783 manifest.scenarios_inflow_annual_component_parquet,
784 "scenarios_inflow_annual_component_parquet should be true when file is present"
785 );
786 }
787
788 #[test]
790 fn test_manifest_inflow_annual_component_absent() {
791 let dir = TempDir::new().unwrap();
792 make_case_with_required(&dir);
793 let mut ctx = ValidationContext::new();
796 let manifest = validate_structure(dir.path(), &mut ctx);
797
798 assert!(
799 !ctx.has_errors(),
800 "absent optional file should not produce errors"
801 );
802 assert!(
803 !manifest.scenarios_inflow_annual_component_parquet,
804 "scenarios_inflow_annual_component_parquet should be false when file is absent"
805 );
806 }
807
808 #[test]
811 fn test_manifest_hydro_unit_group_bounds() {
812 let dir = TempDir::new().unwrap();
813 make_case_with_required(&dir);
814 let constraints_dir = dir.path().join("constraints");
815 fs::create_dir_all(&constraints_dir).unwrap();
816 fs::write(constraints_dir.join("hydro_unit_group_bounds.parquet"), b"").unwrap();
817
818 let mut ctx = ValidationContext::new();
819 let manifest = validate_structure(dir.path(), &mut ctx);
820
821 assert!(
822 !ctx.has_errors(),
823 "present optional file should not produce errors"
824 );
825 assert!(
826 manifest.constraints_hydro_unit_group_bounds_parquet,
827 "constraints_hydro_unit_group_bounds_parquet should be true when file is present"
828 );
829
830 let absent_dir = TempDir::new().unwrap();
831 make_case_with_required(&absent_dir);
832
833 let mut absent_ctx = ValidationContext::new();
834 let absent_manifest = validate_structure(absent_dir.path(), &mut absent_ctx);
835
836 assert!(
837 !absent_ctx.has_errors(),
838 "absent optional file should not produce errors"
839 );
840 assert!(
841 !absent_manifest.constraints_hydro_unit_group_bounds_parquet,
842 "constraints_hydro_unit_group_bounds_parquet should be false when file is absent"
843 );
844 }
845
846 #[test]
847 fn removed_exchange_factors_file_is_rejected() {
848 let dir = TempDir::new().unwrap();
849 make_case_with_required(&dir);
850 let constraints_dir = dir.path().join("constraints");
851 fs::create_dir_all(&constraints_dir).unwrap();
852 fs::write(
855 constraints_dir.join("exchange_factors.json"),
856 b"{\"exchange_factors\": []}",
857 )
858 .unwrap();
859
860 let mut ctx = ValidationContext::new();
861 let _manifest = validate_structure(dir.path(), &mut ctx);
862
863 assert_eq!(
864 ctx.errors().len(),
865 1,
866 "should have exactly 1 error when constraints/exchange_factors.json is present, got: {:?}",
867 ctx.errors()
868 );
869 let entry = ctx.errors()[0];
870 assert_eq!(
871 entry.kind,
872 ErrorKind::BusinessRuleViolation,
873 "removed-file rejection should carry BusinessRuleViolation"
874 );
875 assert!(
876 entry
877 .file
878 .to_string_lossy()
879 .contains("constraints/exchange_factors.json"),
880 "error file should reference constraints/exchange_factors.json, got: {}",
881 entry.file.display()
882 );
883 assert!(
884 entry.message.contains("constraints/exchange_factors.json"),
885 "message should name the removed file, got: {}",
886 entry.message
887 );
888 assert!(
889 entry.message.contains("constraints/line_bounds.parquet"),
890 "message should name the replacement file, got: {}",
891 entry.message
892 );
893 }
894
895 #[test]
896 fn removed_scalar_parameters_file_is_rejected() {
897 let dir = TempDir::new().unwrap();
898 make_case_with_required(&dir);
899 fs::write(
903 dir.path().join("system/scalar_parameters.json"),
904 b"{\"scalar_parameters\": []}",
905 )
906 .unwrap();
907
908 let mut ctx = ValidationContext::new();
909 let _manifest = validate_structure(dir.path(), &mut ctx);
910
911 assert_eq!(
912 ctx.errors().len(),
913 1,
914 "should have exactly 1 error when system/scalar_parameters.json is present, got: {:?}",
915 ctx.errors()
916 );
917 let entry = ctx.errors()[0];
918 assert_eq!(
919 entry.kind,
920 ErrorKind::BusinessRuleViolation,
921 "removed-file rejection should carry BusinessRuleViolation"
922 );
923 assert!(
924 entry
925 .file
926 .to_string_lossy()
927 .contains("system/scalar_parameters.json"),
928 "error file should reference the withdrawn path, got: {}",
929 entry.file.display()
930 );
931 assert!(
932 entry
933 .message
934 .contains("constraints/generic_parameters.json"),
935 "message should name the new path so the old file is rejected loudly, got: {}",
936 entry.message
937 );
938 }
939
940 #[test]
941 fn absent_removed_file_produces_no_finding() {
942 let dir = TempDir::new().unwrap();
943 make_case_with_required(&dir);
944 let mut ctx = ValidationContext::new();
947 let _manifest = validate_structure(dir.path(), &mut ctx);
948
949 assert!(
950 !ctx.has_errors(),
951 "absent removed file should not produce any error, got: {:?}",
952 ctx.errors()
953 );
954 }
955
956 #[test]
957 fn removed_file_check_does_not_disturb_manifest_flags() {
958 let dir = TempDir::new().unwrap();
959 make_case_with_required(&dir);
960 let constraints_dir = dir.path().join("constraints");
961 fs::create_dir_all(&constraints_dir).unwrap();
962 fs::write(constraints_dir.join("exchange_factors.json"), b"{}").unwrap();
963 fs::write(constraints_dir.join("line_bounds.parquet"), b"").unwrap();
964 fs::write(constraints_dir.join("hydro_bounds.parquet"), b"").unwrap();
965 let scenarios_dir = dir.path().join("scenarios");
966 fs::create_dir_all(&scenarios_dir).unwrap();
967 fs::write(scenarios_dir.join("load_factors.json"), b"{}").unwrap();
968
969 let mut ctx = ValidationContext::new();
970 let manifest = validate_structure(dir.path(), &mut ctx);
971
972 assert!(manifest.constraints_line_bounds_parquet);
976 assert!(manifest.constraints_hydro_bounds_parquet);
977 assert!(manifest.scenarios_load_factors_json);
978
979 assert!(!manifest.system_non_controllable_sources_json);
982 assert!(!manifest.system_pumping_stations_json);
983 assert!(!manifest.system_energy_contracts_json);
984 assert!(!manifest.system_hydro_geometry_parquet);
985 assert!(!manifest.system_hydro_production_models_json);
986 assert!(!manifest.system_fpha_hyperplanes_parquet);
987 assert!(!manifest.system_hydro_energy_productivity_parquet);
988 assert!(!manifest.system_tailrace_curves_parquet);
989
990 assert!(!manifest.scenarios_inflow_history_parquet);
991 assert!(!manifest.scenarios_inflow_seasonal_stats_parquet);
992 assert!(!manifest.scenarios_inflow_ar_coefficients_parquet);
993 assert!(!manifest.scenarios_inflow_annual_component_parquet);
994 assert!(!manifest.scenarios_external_inflow_scenarios_parquet);
995 assert!(!manifest.scenarios_external_load_scenarios_parquet);
996 assert!(!manifest.scenarios_external_ncs_scenarios_parquet);
997 assert!(!manifest.scenarios_load_seasonal_stats_parquet);
998 assert!(!manifest.scenarios_correlation_json);
999 assert!(!manifest.scenarios_non_controllable_factors_json);
1000 assert!(!manifest.scenarios_non_controllable_stats_parquet);
1001
1002 assert!(!manifest.constraints_thermal_bounds_parquet);
1003 assert!(!manifest.constraints_pumping_bounds_parquet);
1004 assert!(!manifest.constraints_contract_bounds_parquet);
1005 assert!(!manifest.constraints_generic_constraints_json);
1006 assert!(!manifest.constraints_generic_constraint_bounds_parquet);
1007 assert!(!manifest.constraints_generic_parameters_json);
1008 assert!(!manifest.constraints_penalty_overrides_bus_parquet);
1009 assert!(!manifest.constraints_penalty_overrides_line_parquet);
1010 assert!(!manifest.constraints_penalty_overrides_hydro_parquet);
1011 assert!(!manifest.constraints_penalty_overrides_ncs_parquet);
1012 assert!(!manifest.constraints_ncs_bounds_parquet);
1013 assert!(!manifest.constraints_hydro_unit_group_bounds_parquet);
1014 }
1015
1016 #[test]
1017 fn similarly_named_file_alongside_removed_file_is_not_rejected() {
1018 let dir = TempDir::new().unwrap();
1019 make_case_with_required(&dir);
1020 let constraints_dir = dir.path().join("constraints");
1021 fs::create_dir_all(&constraints_dir).unwrap();
1022 fs::write(constraints_dir.join("exchange_factors_backup.json"), b"{}").unwrap();
1026
1027 let mut ctx = ValidationContext::new();
1028 let _manifest = validate_structure(dir.path(), &mut ctx);
1029
1030 assert!(
1031 !ctx.has_errors(),
1032 "a similarly-named file must not trigger the removed-file rejection, got: {:?}",
1033 ctx.errors()
1034 );
1035 }
1036}