1use super::gm::instrument_range;
2use super::notation::GuitarTechnique;
3use super::score::{
4 InstrumentDefinition, InstrumentRange, NotationSpannerKind, NoteAddr, PercussionInstrument,
5 Score, ScoreView, StaffKind,
6};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub enum ValidationError {
13 EmptyScore,
15 PartWithoutStaves { part: usize },
17 StaffWithoutMeasures { part: usize, staff: usize },
19 MeasureCountMismatch {
21 part: usize,
22 staff: usize,
23 expected: usize,
24 found: usize,
25 },
26 InvalidTimeSignature {
28 part: usize,
29 staff: usize,
30 measure: usize,
31 numerator: u8,
32 denominator: u8,
33 },
34 BeatCount {
36 part: usize,
37 staff: usize,
38 measure: usize,
39 voice: usize,
40 expected_beats: f64,
41 found_beats: f64,
42 },
43 OutOfRange {
45 part_index: usize,
46 staff_index: usize,
47 measure_index: usize,
48 note_index: usize,
49 pitch_midi: u8,
50 instrument_range: (u8, u8),
51 },
52 InvalidTablature {
54 part: usize,
55 staff: usize,
56 reason: TablatureValidationReason,
57 },
58 InvalidStaffPresentation {
60 part: usize,
61 staff: usize,
62 reason: StaffPresentationValidationReason,
63 },
64 InvalidInstrumentDefinition {
66 part: usize,
67 reason: InstrumentDefinitionValidationReason,
68 },
69 InvalidPercussionInstrument {
71 part: usize,
72 instrument: usize,
73 id: String,
74 reason: PercussionInstrumentValidationReason,
75 },
76 InvalidScoreView {
77 index: usize,
78 id: String,
79 reason: ScoreViewValidationReason,
80 },
81 InvalidScoreStyleOverride {
83 property: super::score::ViewStyleProperty,
84 value: f32,
85 },
86 InvalidObjectStyleOverride {
88 index: usize,
89 reason: ObjectStyleValidationReason,
90 },
91 TabPositionOutOfRange {
93 part: usize,
94 staff: usize,
95 measure: usize,
96 voice: usize,
97 note: usize,
98 string: u8,
99 lines: u8,
100 },
101 MicrotoneOutOfRange {
103 part: usize,
104 staff: usize,
105 measure: usize,
106 voice: usize,
107 note: usize,
108 pitch: usize,
109 microtone_cents: i16,
110 },
111 InvalidGuitarBendCurve {
112 part: usize,
113 staff: usize,
114 measure: usize,
115 voice: usize,
116 note: usize,
117 reason: GuitarBendCurveValidationReason,
118 },
119 InvalidHarmonyRange {
121 part: usize,
122 staff: usize,
123 measure: usize,
124 voice: usize,
125 note: usize,
126 end: NoteAddr,
127 },
128 InvalidSpannerId { index: usize, id: String },
130 DuplicateSpannerId {
132 first: usize,
133 duplicate: usize,
134 id: String,
135 },
136 InvalidSpannerEndpoint {
138 index: usize,
139 id: String,
140 kind: NotationSpannerKind,
141 endpoint: SpannerEndpoint,
142 address: NoteAddr,
143 },
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
148pub enum SpannerEndpoint {
149 Start,
150 End,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub enum TablatureValidationReason {
155 InvalidLineCount {
156 lines: u8,
157 },
158 TooManyTunings {
159 tuning_count: usize,
160 lines: u8,
161 },
162 TuningOutOfMidiRange {
163 index: usize,
164 midi: i16,
165 },
166 ChangeWithoutBase {
167 measure: usize,
168 },
169 ChangeLineCountMismatch {
170 measure: usize,
171 base_lines: u8,
172 changed_lines: u8,
173 },
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub enum StaffPresentationValidationReason {
178 InvalidLineCount { lines: u8 },
179 InvalidLineDistance { line_distance: f32 },
180 TablatureWithoutConfig,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub enum InstrumentDefinitionValidationReason {
185 EmptyId,
186 InvalidStaffCount {
187 staff_count: u8,
188 },
189 InvalidMidiChannel {
190 midi_channel: u8,
191 },
192 InvalidRange {
193 kind: InstrumentRangeKind,
194 range: InstrumentRange,
195 },
196}
197
198#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
199pub enum InstrumentRangeKind {
200 Written,
201 Sounding,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
205pub enum PercussionInstrumentValidationReason {
206 EmptyId,
207 DuplicateId { first: usize },
208 InvalidStaffPosition { staff_position: i8 },
209 InvalidPreferredVoice { preferred_voice: u8 },
210 InvalidTechnique { technique: String },
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub enum ScoreViewValidationReason {
215 EmptyIdOrName,
216 DuplicateId {
217 first: usize,
218 },
219 EmptyPartSelection,
220 InvalidPart {
221 part: usize,
222 },
223 DuplicatePart {
224 part: usize,
225 },
226 InvalidStaff {
227 part: usize,
228 staff: usize,
229 },
230 HiddenStaffOutsideSelection {
231 part: usize,
232 staff: usize,
233 },
234 DuplicateStaffKindOverride {
235 part: usize,
236 staff: usize,
237 },
238 StaffKindOverrideOutsideSelection {
239 part: usize,
240 staff: usize,
241 },
242 TablatureOverrideWithoutConfig {
243 part: usize,
244 staff: usize,
245 },
246 InvalidMeasuresPerRow,
247 InvalidTypedStyleOverride {
248 property: super::score::ViewStyleProperty,
249 value: f32,
250 },
251 BreakOutOfRange {
252 measure: usize,
253 },
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub enum ObjectStyleValidationReason {
258 InvalidValue {
259 property: super::score::ViewStyleProperty,
260 value: f32,
261 },
262 MissingTarget,
263 InvalidProvenance,
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub enum GuitarBendCurveValidationReason {
268 TooManyPoints { count: usize },
269 RequiresBendTechnique,
270 RequiresStartAtZero,
271 RequiresEndAtFullDuration,
272 PositionsNotStrictlyIncreasing,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
277pub enum ValidationWarning {
278 IncompleteBar {
280 part: usize,
281 staff: usize,
282 measure: usize,
283 expected_beats: f64,
284 actual_beats: f64,
285 },
286 OverlappingVolta { part: usize, staff: usize },
288 EmptyPart { part: usize },
290 DuplicateRehearsalMark { mark: String },
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct ValidationReport {
297 pub errors: Vec<ValidationError>,
298 pub warnings: Vec<ValidationWarning>,
299}
300
301impl ValidationReport {
302 pub fn is_valid(&self) -> bool {
304 self.errors.is_empty()
305 }
306}
307
308pub fn validate(score: &Score) -> ValidationReport {
318 let mut errors = Vec::new();
319 let mut warnings = Vec::new();
320
321 let mut rehearsal_counts: HashMap<String, usize> = HashMap::new();
322
323 if score.parts.is_empty() {
324 errors.push(ValidationError::EmptyScore);
325 }
326
327 for override_ in &score.style_overrides {
328 if !valid_typed_style_value(override_.value) {
329 errors.push(ValidationError::InvalidScoreStyleOverride {
330 property: override_.property,
331 value: override_.value,
332 });
333 }
334 }
335 for (index, override_) in score.object_style_overrides.iter().enumerate() {
336 let reason = if !valid_typed_style_value(override_.value) {
337 Some(ObjectStyleValidationReason::InvalidValue {
338 property: override_.property,
339 value: override_.value,
340 })
341 } else if !object_style_target_exists(score, &override_.target) {
342 Some(ObjectStyleValidationReason::MissingTarget)
343 } else if override_.provenance.as_ref().is_some_and(|provenance| {
344 provenance.format.trim().is_empty()
345 || provenance.format.len() > 64
346 || provenance.source_location.trim().is_empty()
347 || provenance.source_location.len() > 2048
348 }) {
349 Some(ObjectStyleValidationReason::InvalidProvenance)
350 } else {
351 None
352 };
353 if let Some(reason) = reason {
354 errors.push(ValidationError::InvalidObjectStyleOverride { index, reason });
355 }
356 }
357
358 let mut view_ids: HashMap<String, usize> = HashMap::new();
359 for (index, view) in score.views.iter().enumerate() {
360 validate_score_view(index, view, score, &mut view_ids, &mut errors);
361 }
362
363 let mut spanner_ids: HashMap<&str, usize> = HashMap::new();
364 for (index, spanner) in score.spanners.iter().enumerate() {
365 if spanner.id.trim().is_empty() {
366 errors.push(ValidationError::InvalidSpannerId {
367 index,
368 id: spanner.id.clone(),
369 });
370 } else if let Some(first) = spanner_ids.insert(spanner.id.as_str(), index) {
371 errors.push(ValidationError::DuplicateSpannerId {
372 first,
373 duplicate: index,
374 id: spanner.id.clone(),
375 });
376 }
377 for (endpoint, address) in [
378 (SpannerEndpoint::Start, &spanner.start),
379 (SpannerEndpoint::End, &spanner.end),
380 ] {
381 if !note_exists(score, address) {
382 errors.push(ValidationError::InvalidSpannerEndpoint {
383 index,
384 id: spanner.id.clone(),
385 kind: spanner.kind.clone(),
386 endpoint,
387 address: address.clone(),
388 });
389 }
390 }
391 }
392
393 for (pi, part) in score.parts.iter().enumerate() {
394 let range = instrument_range(part.midi_program);
395 let is_percussion = part.midi_channel == 9;
396 let mut part_has_notes = false;
397
398 if part.staves.is_empty() {
399 errors.push(ValidationError::PartWithoutStaves { part: pi });
400 continue;
401 }
402
403 if let Some(definition) = &part.instrument {
404 validate_instrument_definition(pi, definition, &mut errors);
405 }
406 validate_percussion_kit(pi, &part.percussion_instruments, &mut errors);
407
408 let expected_measure_count = part.staves[0].measures.len();
409
410 for (si, staff) in part.staves.iter().enumerate() {
411 if staff.measures.is_empty() {
412 errors.push(ValidationError::StaffWithoutMeasures {
413 part: pi,
414 staff: si,
415 });
416 continue;
417 }
418 if staff.measures.len() != expected_measure_count {
419 errors.push(ValidationError::MeasureCountMismatch {
420 part: pi,
421 staff: si,
422 expected: expected_measure_count,
423 found: staff.measures.len(),
424 });
425 }
426
427 if !(1..=64).contains(&staff.presentation.lines) {
428 errors.push(ValidationError::InvalidStaffPresentation {
429 part: pi,
430 staff: si,
431 reason: StaffPresentationValidationReason::InvalidLineCount {
432 lines: staff.presentation.lines,
433 },
434 });
435 }
436 if !staff.presentation.line_distance.is_finite()
437 || !(0.1..=16.0).contains(&staff.presentation.line_distance)
438 {
439 errors.push(ValidationError::InvalidStaffPresentation {
440 part: pi,
441 staff: si,
442 reason: StaffPresentationValidationReason::InvalidLineDistance {
443 line_distance: staff.presentation.line_distance,
444 },
445 });
446 }
447 if staff.presentation.kind == StaffKind::Tablature && staff.tablature.is_none() {
448 errors.push(ValidationError::InvalidStaffPresentation {
449 part: pi,
450 staff: si,
451 reason: StaffPresentationValidationReason::TablatureWithoutConfig,
452 });
453 }
454
455 if let Some(tab) = &staff.tablature {
456 if !(1..=64).contains(&tab.lines) {
457 errors.push(ValidationError::InvalidTablature {
458 part: pi,
459 staff: si,
460 reason: TablatureValidationReason::InvalidLineCount { lines: tab.lines },
461 });
462 } else if tab.tuning_midi.len() > usize::from(tab.lines) {
463 errors.push(ValidationError::InvalidTablature {
464 part: pi,
465 staff: si,
466 reason: TablatureValidationReason::TooManyTunings {
467 tuning_count: tab.tuning_midi.len(),
468 lines: tab.lines,
469 },
470 });
471 }
472 for (index, &midi) in tab.tuning_midi.iter().enumerate() {
473 if !(0..=127).contains(&midi) {
474 errors.push(ValidationError::InvalidTablature {
475 part: pi,
476 staff: si,
477 reason: TablatureValidationReason::TuningOutOfMidiRange { index, midi },
478 });
479 }
480 }
481 }
482
483 let mut current_ts = score.settings.time_signature.clone();
484 let mut volta_numbers_seen: Vec<u8> = Vec::new();
485
486 for (mi, measure) in staff.measures.iter().enumerate() {
487 if let Some(change) = &measure.tablature_change {
488 match &staff.tablature {
489 None => errors.push(ValidationError::InvalidTablature {
490 part: pi,
491 staff: si,
492 reason: TablatureValidationReason::ChangeWithoutBase { measure: mi },
493 }),
494 Some(base) if base.lines != change.lines => {
495 errors.push(ValidationError::InvalidTablature {
496 part: pi,
497 staff: si,
498 reason: TablatureValidationReason::ChangeLineCountMismatch {
499 measure: mi,
500 base_lines: base.lines,
501 changed_lines: change.lines,
502 },
503 });
504 }
505 Some(_) => {}
506 }
507 if !(1..=64).contains(&change.lines) {
508 errors.push(ValidationError::InvalidTablature {
509 part: pi,
510 staff: si,
511 reason: TablatureValidationReason::InvalidLineCount {
512 lines: change.lines,
513 },
514 });
515 } else if change.tuning_midi.len() > usize::from(change.lines) {
516 errors.push(ValidationError::InvalidTablature {
517 part: pi,
518 staff: si,
519 reason: TablatureValidationReason::TooManyTunings {
520 tuning_count: change.tuning_midi.len(),
521 lines: change.lines,
522 },
523 });
524 }
525 for (index, &midi) in change.tuning_midi.iter().enumerate() {
526 if !(0..=127).contains(&midi) {
527 errors.push(ValidationError::InvalidTablature {
528 part: pi,
529 staff: si,
530 reason: TablatureValidationReason::TuningOutOfMidiRange {
531 index,
532 midi,
533 },
534 });
535 }
536 }
537 }
538 if let Some(ts) = &measure.time_sig {
539 current_ts = ts.clone();
540 }
541 if !valid_time_signature(¤t_ts) {
542 errors.push(ValidationError::InvalidTimeSignature {
543 part: pi,
544 staff: si,
545 measure: mi,
546 numerator: current_ts.numerator,
547 denominator: current_ts.denominator,
548 });
549 continue;
550 }
551 if measure.multi_rest_count.is_some() {
552 continue;
553 }
554
555 if let Some(ref mark) = measure.rehearsal {
557 let entry = rehearsal_counts.entry(mark.clone()).or_insert(0);
558 *entry += 1;
559 }
560
561 if let Some(ref volta) = measure.volta {
563 if volta_numbers_seen.contains(&volta.number) {
564 warnings.push(ValidationWarning::OverlappingVolta {
565 part: pi,
566 staff: si,
567 });
568 } else {
569 volta_numbers_seen.push(volta.number);
570 }
571 }
572
573 let expected = current_ts.total_beats();
574 for (vi, voice) in measure.voices.iter().enumerate() {
575 if voice.is_empty() {
576 continue;
577 }
578 let non_rest_count: usize = voice.iter().filter(|n| !n.is_rest).count();
579 if non_rest_count > 0 {
580 part_has_notes = true;
581 }
582 let total: f64 = voice.iter().map(|n| n.beats()).sum();
583 if total > expected + 0.02 {
584 errors.push(ValidationError::BeatCount {
585 part: pi,
586 staff: si,
587 measure: mi,
588 voice: vi,
589 expected_beats: expected,
590 found_beats: total,
591 });
592 } else if total < expected - 0.02 && non_rest_count > 0 {
593 warnings.push(ValidationWarning::IncompleteBar {
594 part: pi,
595 staff: si,
596 measure: mi,
597 expected_beats: expected,
598 actual_beats: total,
599 });
600 }
601
602 for (ni, note) in voice.iter().enumerate() {
603 if note.is_rest || note.is_grace {
604 continue;
605 }
606 if let Some(chord) = ¬e.chord_symbol
607 && let Some(end) = &chord.range_end
608 && !note_exists(score, end)
609 {
610 errors.push(ValidationError::InvalidHarmonyRange {
611 part: pi,
612 staff: si,
613 measure: mi,
614 voice: vi,
615 note: ni,
616 end: end.clone(),
617 });
618 }
619 for (pitch_index, pitch) in note.pitches.iter().enumerate() {
620 if !(-99..=99).contains(&pitch.microtone_cents) {
621 errors.push(ValidationError::MicrotoneOutOfRange {
622 part: pi,
623 staff: si,
624 measure: mi,
625 voice: vi,
626 note: ni,
627 pitch: pitch_index,
628 microtone_cents: pitch.microtone_cents,
629 });
630 }
631 }
632 if !note.guitar_bend_curve.is_empty() {
633 let reason = if note.guitar_bend_curve.len() > 32 {
634 Some(GuitarBendCurveValidationReason::TooManyPoints {
635 count: note.guitar_bend_curve.len(),
636 })
637 } else if note.guitar_technique != Some(GuitarTechnique::Bend) {
638 Some(GuitarBendCurveValidationReason::RequiresBendTechnique)
639 } else if note
640 .guitar_bend_curve
641 .first()
642 .map(|point| point.position_per_mille)
643 != Some(0)
644 {
645 Some(GuitarBendCurveValidationReason::RequiresStartAtZero)
646 } else if note
647 .guitar_bend_curve
648 .last()
649 .map(|point| point.position_per_mille)
650 != Some(1000)
651 {
652 Some(GuitarBendCurveValidationReason::RequiresEndAtFullDuration)
653 } else if note.guitar_bend_curve.windows(2).any(|points| {
654 points[0].position_per_mille >= points[1].position_per_mille
655 }) {
656 Some(
657 GuitarBendCurveValidationReason::PositionsNotStrictlyIncreasing,
658 )
659 } else {
660 None
661 };
662 if let Some(reason) = reason {
663 errors.push(ValidationError::InvalidGuitarBendCurve {
664 part: pi,
665 staff: si,
666 measure: mi,
667 voice: vi,
668 note: ni,
669 reason,
670 });
671 }
672 }
673 }
674
675 if !is_percussion {
676 let transpose = staff.transpose_semitones;
677 for (ni, note) in voice.iter().enumerate() {
678 if note.is_rest || note.is_grace {
679 continue;
680 }
681 if let Some(tab) = &staff.tablature {
682 let positions =
683 note.tab_position.iter().chain(note.tab_positions.iter());
684 for position in positions {
685 if position.string == 0 || position.string > tab.lines {
686 errors.push(ValidationError::TabPositionOutOfRange {
687 part: pi,
688 staff: si,
689 measure: mi,
690 voice: vi,
691 note: ni,
692 string: position.string,
693 lines: tab.lines,
694 });
695 }
696 }
697 }
698 for pitch in ¬e.pitches {
699 let midi = (pitch.to_midi() + transpose as i16).clamp(0, 127) as u8;
700 if midi < range.0 || midi > range.1 {
701 errors.push(ValidationError::OutOfRange {
702 part_index: pi,
703 staff_index: si,
704 measure_index: mi,
705 note_index: ni,
706 pitch_midi: midi,
707 instrument_range: range,
708 });
709 }
710 }
711 }
712 }
713 }
714 }
715 }
716
717 if !part_has_notes {
718 warnings.push(ValidationWarning::EmptyPart { part: pi });
719 }
720 }
721
722 for (mark, count) in &rehearsal_counts {
723 if *count > 1 {
724 warnings.push(ValidationWarning::DuplicateRehearsalMark { mark: mark.clone() });
725 }
726 }
727
728 ValidationReport { errors, warnings }
729}
730
731fn validate_instrument_definition(
732 part: usize,
733 definition: &InstrumentDefinition,
734 errors: &mut Vec<ValidationError>,
735) {
736 if definition.id.trim().is_empty() {
737 errors.push(ValidationError::InvalidInstrumentDefinition {
738 part,
739 reason: InstrumentDefinitionValidationReason::EmptyId,
740 });
741 }
742 if !(1..=64).contains(&definition.staff_count) {
743 errors.push(ValidationError::InvalidInstrumentDefinition {
744 part,
745 reason: InstrumentDefinitionValidationReason::InvalidStaffCount {
746 staff_count: definition.staff_count,
747 },
748 });
749 }
750 if definition.midi_channel > 15 {
751 errors.push(ValidationError::InvalidInstrumentDefinition {
752 part,
753 reason: InstrumentDefinitionValidationReason::InvalidMidiChannel {
754 midi_channel: definition.midi_channel,
755 },
756 });
757 }
758 for (kind, range) in [
759 (InstrumentRangeKind::Written, definition.written_range),
760 (InstrumentRangeKind::Sounding, definition.sounding_range),
761 ] {
762 if let Some(range) = range
763 && range.lowest > range.highest
764 {
765 errors.push(ValidationError::InvalidInstrumentDefinition {
766 part,
767 reason: InstrumentDefinitionValidationReason::InvalidRange { kind, range },
768 });
769 }
770 }
771}
772
773fn validate_percussion_kit(
774 part: usize,
775 instruments: &[PercussionInstrument],
776 errors: &mut Vec<ValidationError>,
777) {
778 let mut ids: HashMap<&str, usize> = HashMap::new();
779 for (instrument_index, instrument) in instruments.iter().enumerate() {
780 let invalid = |reason| ValidationError::InvalidPercussionInstrument {
781 part,
782 instrument: instrument_index,
783 id: instrument.id.clone(),
784 reason,
785 };
786 if instrument.id.trim().is_empty() {
787 errors.push(invalid(PercussionInstrumentValidationReason::EmptyId));
788 } else if let Some(first) = ids.insert(instrument.id.as_str(), instrument_index) {
789 errors.push(invalid(PercussionInstrumentValidationReason::DuplicateId {
790 first,
791 }));
792 }
793 if let Some(staff_position) = instrument.staff_position
794 && !(-32..=32).contains(&staff_position)
795 {
796 errors.push(invalid(
797 PercussionInstrumentValidationReason::InvalidStaffPosition { staff_position },
798 ));
799 }
800 if let Some(preferred_voice) = instrument.preferred_voice
801 && !(1..=4).contains(&preferred_voice)
802 {
803 errors.push(invalid(
804 PercussionInstrumentValidationReason::InvalidPreferredVoice { preferred_voice },
805 ));
806 }
807 for technique in &instrument.techniques {
808 if technique.trim().is_empty() || technique.len() > 128 {
809 errors.push(invalid(
810 PercussionInstrumentValidationReason::InvalidTechnique {
811 technique: technique.clone(),
812 },
813 ));
814 }
815 }
816 }
817}
818
819fn validate_score_view(
820 index: usize,
821 view: &ScoreView,
822 score: &Score,
823 ids: &mut HashMap<String, usize>,
824 errors: &mut Vec<ValidationError>,
825) {
826 let invalid = |reason| ValidationError::InvalidScoreView {
827 index,
828 id: view.id.clone(),
829 reason,
830 };
831 if view.id.trim().is_empty() || view.name.trim().is_empty() {
832 errors.push(invalid(ScoreViewValidationReason::EmptyIdOrName));
833 } else if let Some(first) = ids.insert(view.id.clone(), index) {
834 errors.push(invalid(ScoreViewValidationReason::DuplicateId { first }));
835 }
836 if view.parts.is_empty() {
837 errors.push(invalid(ScoreViewValidationReason::EmptyPartSelection));
838 return;
839 }
840 let mut selected = vec![false; score.parts.len()];
841 for &part_index in &view.parts {
842 if part_index >= score.parts.len() {
843 errors.push(invalid(ScoreViewValidationReason::InvalidPart {
844 part: part_index,
845 }));
846 } else if std::mem::replace(&mut selected[part_index], true) {
847 errors.push(invalid(ScoreViewValidationReason::DuplicatePart {
848 part: part_index,
849 }));
850 }
851 }
852 if view.layout.measures_per_row.is_some_and(|value| value == 0) {
853 errors.push(invalid(ScoreViewValidationReason::InvalidMeasuresPerRow));
854 }
855 for override_ in &view.layout.typed_style_overrides {
856 if !valid_typed_style_value(override_.value) {
857 errors.push(invalid(
858 ScoreViewValidationReason::InvalidTypedStyleOverride {
859 property: override_.property,
860 value: override_.value,
861 },
862 ));
863 }
864 }
865 for reference in &view.layout.hidden_staves {
866 let Some(part) = score.parts.get(reference.part) else {
867 errors.push(invalid(ScoreViewValidationReason::InvalidPart {
868 part: reference.part,
869 }));
870 continue;
871 };
872 if reference.staff >= part.staves.len() {
873 errors.push(invalid(ScoreViewValidationReason::InvalidStaff {
874 part: reference.part,
875 staff: reference.staff,
876 }));
877 } else if !selected[reference.part] {
878 errors.push(invalid(
879 ScoreViewValidationReason::HiddenStaffOutsideSelection {
880 part: reference.part,
881 staff: reference.staff,
882 },
883 ));
884 }
885 }
886 let mut overridden = HashMap::new();
887 for override_ in &view.staff_kind_overrides {
888 let reference = override_.staff;
889 let Some(part) = score.parts.get(reference.part) else {
890 errors.push(invalid(ScoreViewValidationReason::InvalidPart {
891 part: reference.part,
892 }));
893 continue;
894 };
895 if reference.staff >= part.staves.len() {
896 errors.push(invalid(ScoreViewValidationReason::InvalidStaff {
897 part: reference.part,
898 staff: reference.staff,
899 }));
900 } else if !selected[reference.part] {
901 errors.push(invalid(
902 ScoreViewValidationReason::StaffKindOverrideOutsideSelection {
903 part: reference.part,
904 staff: reference.staff,
905 },
906 ));
907 } else if overridden
908 .insert((reference.part, reference.staff), ())
909 .is_some()
910 {
911 errors.push(invalid(
912 ScoreViewValidationReason::DuplicateStaffKindOverride {
913 part: reference.part,
914 staff: reference.staff,
915 },
916 ));
917 } else if override_.kind == StaffKind::Tablature
918 && part.staves[reference.staff].tablature.is_none()
919 {
920 errors.push(invalid(
921 ScoreViewValidationReason::TablatureOverrideWithoutConfig {
922 part: reference.part,
923 staff: reference.staff,
924 },
925 ));
926 }
927 }
928 let measure_count = score.measure_count();
929 for &measure in view
930 .layout
931 .system_breaks
932 .iter()
933 .chain(view.layout.page_breaks.iter())
934 {
935 if measure >= measure_count {
936 errors.push(invalid(ScoreViewValidationReason::BreakOutOfRange {
937 measure,
938 }));
939 }
940 }
941}
942
943fn valid_typed_style_value(value: f32) -> bool {
944 value.is_finite() && (0.05..=64.0).contains(&value)
945}
946
947fn object_style_target_exists(score: &Score, target: &super::score::ObjectStyleTarget) -> bool {
948 use super::score::ObjectStyleTarget;
949 match target {
950 ObjectStyleTarget::ScoreText { text_index } => *text_index < score.texts.len(),
951 ObjectStyleTarget::MeasureText {
952 part,
953 staff,
954 measure,
955 text_index,
956 } => score
957 .parts
958 .get(*part)
959 .and_then(|part| part.staves.get(*staff))
960 .and_then(|staff| staff.measures.get(*measure))
961 .is_some_and(|measure| *text_index < measure.texts.len()),
962 ObjectStyleTarget::Note { address } => score
963 .parts
964 .get(address.part)
965 .and_then(|part| part.staves.get(address.staff))
966 .and_then(|staff| staff.measures.get(address.measure))
967 .and_then(|measure| measure.voices.get(address.voice))
968 .is_some_and(|voice| address.note < voice.len()),
969 }
970}
971
972fn valid_time_signature(time: &super::notation::TimeSignature) -> bool {
973 time.numerator > 0 && matches!(time.denominator, 1 | 2 | 4 | 8 | 16 | 32 | 64)
974}
975
976fn note_exists(score: &Score, address: &NoteAddr) -> bool {
977 score
978 .parts
979 .get(address.part)
980 .and_then(|part| part.staves.get(address.staff))
981 .and_then(|staff| staff.measures.get(address.measure))
982 .and_then(|measure| measure.voices.get(address.voice))
983 .and_then(|voice| voice.get(address.note))
984 .is_some()
985}
986
987#[cfg(test)]
988mod tests {
989 use super::*;
990 use crate::model::{
991 duration::Duration,
992 notation::ChordSymbol,
993 pitch::{Pitch, Step},
994 score::{Note, NoteAddr, Score},
995 };
996
997 #[test]
998 fn validate_clean_score_returns_empty_errors() {
999 let score = Score::new("T", 120, 4, 4, 0, 1);
1000 assert!(validate(&score).errors.is_empty());
1001 }
1002
1003 #[test]
1004 fn validate_empty_score_returns_structural_error() {
1005 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1006 score.parts.clear();
1007 let report = validate(&score);
1008 assert!(
1009 report
1010 .errors
1011 .iter()
1012 .any(|error| matches!(error, ValidationError::EmptyScore))
1013 );
1014 }
1015
1016 #[test]
1017 fn validate_detects_missing_staves_and_measures() {
1018 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1019 score.parts[0].staves.clear();
1020 let report = validate(&score);
1021 assert!(
1022 report
1023 .errors
1024 .iter()
1025 .any(|error| matches!(error, ValidationError::PartWithoutStaves { part: 0 }))
1026 );
1027
1028 score.parts[0].staves.push(crate::model::score::Staff::new(
1029 crate::model::notation::Clef::Treble,
1030 ));
1031 let report = validate(&score);
1032 assert!(report.errors.iter().any(|error| matches!(
1033 error,
1034 ValidationError::StaffWithoutMeasures { part: 0, staff: 0 }
1035 )));
1036 }
1037
1038 #[test]
1039 fn validate_detects_staff_measure_count_mismatch() {
1040 let mut score = Score::template(crate::model::score::ScoreTemplate::Piano);
1041 score.parts[0].staves[1].measures.pop();
1042 let report = validate(&score);
1043 assert!(report.errors.iter().any(|error| matches!(
1044 error,
1045 ValidationError::MeasureCountMismatch {
1046 part: 0,
1047 staff: 1,
1048 expected: 4,
1049 found: 3
1050 }
1051 )));
1052 }
1053
1054 #[test]
1055 fn validate_detects_invalid_time_signature() {
1056 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1057 score.parts[0].staves[0].measures[0].time_sig =
1058 Some(crate::model::notation::TimeSignature {
1059 numerator: 0,
1060 denominator: 3,
1061 });
1062 let report = validate(&score);
1063 assert!(report.errors.iter().any(|error| matches!(
1064 error,
1065 ValidationError::InvalidTimeSignature {
1066 part: 0,
1067 staff: 0,
1068 measure: 0,
1069 numerator: 0,
1070 denominator: 3
1071 }
1072 )));
1073 }
1074
1075 #[test]
1076 fn validate_overfull_measure_returns_error() {
1077 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1078 score.parts[0].staves[0].measures[0].voices[0]
1079 .push(Note::new(Pitch::new(Step::C, 4), Duration::Quarter));
1080 let report = validate(&score);
1081 assert!(!report.errors.is_empty());
1082 assert!(matches!(
1083 report.errors[0],
1084 ValidationError::BeatCount {
1085 measure: 0,
1086 voice: 0,
1087 ..
1088 }
1089 ));
1090 }
1091
1092 #[test]
1093 fn validate_skips_multi_rest() {
1094 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1095 score.parts[0].staves[0].measures[0].multi_rest_count = Some(4);
1096 score.parts[0].staves[0].measures[0].voices[0].clear();
1097 assert!(validate(&score).errors.is_empty());
1098 }
1099
1100 #[test]
1101 fn validate_rejects_tablature_view_override_without_tablature_configuration() {
1102 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1103 score
1104 .views
1105 .push(ScoreView::linked_tablature_staff("tab", "Tab", 0, 0));
1106
1107 assert!(validate(&score).errors.iter().any(|error| matches!(
1108 error,
1109 ValidationError::InvalidScoreView {
1110 reason: ScoreViewValidationReason::TablatureOverrideWithoutConfig {
1111 part: 0,
1112 staff: 0,
1113 },
1114 ..
1115 }
1116 )));
1117 }
1118
1119 #[test]
1120 fn validate_rejects_non_finite_typed_view_style_override() {
1121 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1122 let mut view = ScoreView::linked_part("part", "Part", 0);
1123 view.layout
1124 .typed_style_overrides
1125 .push(super::super::score::ViewStyleOverride {
1126 property: super::super::score::ViewStyleProperty::TextScale,
1127 value: f32::NAN,
1128 });
1129 score.views.push(view);
1130 assert!(validate(&score).errors.iter().any(|error| matches!(
1131 error,
1132 ValidationError::InvalidScoreView {
1133 reason: ScoreViewValidationReason::InvalidTypedStyleOverride { .. },
1134 ..
1135 }
1136 )));
1137 }
1138
1139 #[test]
1140 fn validate_rejects_out_of_range_score_style_override() {
1141 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1142 score
1143 .style_overrides
1144 .push(super::super::score::ViewStyleOverride {
1145 property: super::super::score::ViewStyleProperty::StaffSpace,
1146 value: 0.01,
1147 });
1148
1149 assert!(validate(&score).errors.iter().any(|error| matches!(
1150 error,
1151 ValidationError::InvalidScoreStyleOverride {
1152 property: super::super::score::ViewStyleProperty::StaffSpace,
1153 value,
1154 } if (*value - 0.01).abs() < f32::EPSILON
1155 )));
1156 }
1157
1158 #[test]
1159 fn validate_rejects_object_style_override_with_missing_target() {
1160 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1161 score
1162 .object_style_overrides
1163 .push(super::super::score::ObjectStyleOverride {
1164 target: super::super::score::ObjectStyleTarget::ScoreText { text_index: 0 },
1165 property: super::super::score::ViewStyleProperty::TextScale,
1166 value: 1.1,
1167 provenance: None,
1168 });
1169 assert!(validate(&score).errors.iter().any(|error| matches!(
1170 error,
1171 ValidationError::InvalidObjectStyleOverride {
1172 reason: ObjectStyleValidationReason::MissingTarget,
1173 ..
1174 }
1175 )));
1176 }
1177
1178 #[test]
1179 fn validate_rejects_non_normalized_guitar_bend_curve() {
1180 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1181 score.parts[0].staves[0].measures[0].voices[0] =
1182 vec![Note::new(Pitch::new(Step::E, 4), Duration::Whole)];
1183 let note = &mut score.parts[0].staves[0].measures[0].voices[0][0];
1184 note.guitar_technique = Some(GuitarTechnique::Bend);
1185 note.guitar_bend_curve = vec![
1186 crate::GuitarBendPoint {
1187 position_per_mille: 100,
1188 alter_cents: 0,
1189 },
1190 crate::GuitarBendPoint {
1191 position_per_mille: 1000,
1192 alter_cents: 200,
1193 },
1194 ];
1195 assert!(validate(&score).errors.iter().any(|error| matches!(
1196 error,
1197 ValidationError::InvalidGuitarBendCurve {
1198 reason: GuitarBendCurveValidationReason::RequiresStartAtZero,
1199 ..
1200 }
1201 )));
1202 }
1203
1204 #[test]
1205 fn validate_rejects_harmony_range_to_missing_note() {
1206 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1207 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Whole);
1208 note.chord_symbol = Some(ChordSymbol {
1209 root: "C".to_owned(),
1210 kind: "major".to_owned(),
1211 bass: None,
1212 placement: None,
1213 extender: true,
1214 harmonic_degree: None,
1215 harmony_function: None,
1216 harmony_type: None,
1217 chord_ref: None,
1218 range_end: Some(NoteAddr {
1219 part: 0,
1220 staff: 0,
1221 measure: 0,
1222 voice: 0,
1223 note: 9,
1224 }),
1225 degrees: Vec::new(),
1226 });
1227 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
1228 assert!(validate(&score).errors.iter().any(|error| matches!(
1229 error,
1230 ValidationError::InvalidHarmonyRange { note: 0, end, .. }
1231 if end.note == 9
1232 )));
1233 }
1234
1235 #[test]
1236 fn validate_out_of_range_pitch_detected() {
1237 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1239 score.parts[0].midi_program = 0;
1240 score.parts[0].staves[0].measures[0].voices[0] =
1241 vec![Note::new(Pitch::new(Step::C, 9), Duration::Whole)];
1242 let report = validate(&score);
1243 assert!(report.errors.iter().any(
1244 |e| matches!(e, ValidationError::OutOfRange { pitch_midi, .. } if *pitch_midi == 120)
1245 ));
1246 }
1247
1248 #[test]
1249 fn validate_percussion_channel_skips_range_check() {
1250 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1252 score.parts[0].midi_channel = 9;
1253 score.parts[0].midi_program = 0;
1254 score.parts[0].staves[0].measures[0].voices[0] =
1255 vec![Note::new(Pitch::new(Step::C, 9), Duration::Whole)];
1256 let report = validate(&score);
1257 assert!(
1258 !report
1259 .errors
1260 .iter()
1261 .any(|e| matches!(e, ValidationError::OutOfRange { .. }))
1262 );
1263 }
1264
1265 #[test]
1266 fn validate_in_range_pitch_ok() {
1267 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1269 score.parts[0].midi_program = 0;
1270 score.parts[0].staves[0].measures[0].voices[0] =
1271 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
1272 assert!(
1273 !validate(&score)
1274 .errors
1275 .iter()
1276 .any(|e| matches!(e, ValidationError::OutOfRange { .. }))
1277 );
1278 }
1279
1280 #[test]
1281 fn validate_rejects_deserialized_microtone_out_of_range() {
1282 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1283 let mut note = Note::new(Pitch::new(Step::C, 4), Duration::Whole);
1284 note.pitches[0].microtone_cents = 100;
1285 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
1286 let report = validate(&score);
1287 assert!(report.errors.iter().any(|error| matches!(
1288 error,
1289 ValidationError::MicrotoneOutOfRange {
1290 microtone_cents: 100,
1291 ..
1292 }
1293 )));
1294 }
1295
1296 #[test]
1297 fn validate_rejects_invalid_and_duplicate_typed_spanners() {
1298 let mut score = Score::new("T", 120, 4, 4, 0, 1);
1299 score.parts[0].staves[0].measures[0].voices[0] =
1300 vec![Note::new(Pitch::new(Step::C, 4), Duration::Whole)];
1301 let address = NoteAddr {
1302 part: 0,
1303 staff: 0,
1304 measure: 0,
1305 voice: 0,
1306 note: 0,
1307 };
1308 score.spanners = vec![
1309 super::super::score::NotationSpanner {
1310 id: String::new(),
1311 kind: NotationSpannerKind::Slur,
1312 start: address.clone(),
1313 end: address.clone(),
1314 number: Some(1),
1315 line_type: None,
1316 text: None,
1317 placement: None,
1318 ottava_size: None,
1319 ottava_type: None,
1320 },
1321 super::super::score::NotationSpanner {
1322 id: "duplicate".to_string(),
1323 kind: NotationSpannerKind::Pedal,
1324 start: address.clone(),
1325 end: NoteAddr { note: 9, ..address },
1326 number: Some(2),
1327 line_type: None,
1328 text: None,
1329 placement: None,
1330 ottava_size: None,
1331 ottava_type: None,
1332 },
1333 super::super::score::NotationSpanner {
1334 id: "duplicate".to_string(),
1335 kind: NotationSpannerKind::Ottava,
1336 start: NoteAddr {
1337 part: 9,
1338 staff: 0,
1339 measure: 0,
1340 voice: 0,
1341 note: 0,
1342 },
1343 end: NoteAddr {
1344 part: 0,
1345 staff: 0,
1346 measure: 0,
1347 voice: 0,
1348 note: 0,
1349 },
1350 number: None,
1351 line_type: None,
1352 text: None,
1353 placement: None,
1354 ottava_size: Some(8),
1355 ottava_type: None,
1356 },
1357 ];
1358
1359 let report = validate(&score);
1360 assert!(
1361 report
1362 .errors
1363 .iter()
1364 .any(|error| matches!(error, ValidationError::InvalidSpannerId { index: 0, .. }))
1365 );
1366 assert!(report.errors.iter().any(|error| matches!(
1367 error,
1368 ValidationError::DuplicateSpannerId {
1369 first: 1,
1370 duplicate: 2,
1371 ..
1372 }
1373 )));
1374 assert_eq!(
1375 report
1376 .errors
1377 .iter()
1378 .filter(|error| matches!(error, ValidationError::InvalidSpannerEndpoint { .. }))
1379 .count(),
1380 2
1381 );
1382 }
1383
1384 #[test]
1385 fn validate_rejects_invalid_tablature_metadata_and_positions() {
1386 let mut score = Score::new("Tab", 120, 4, 4, 0, 1);
1387 score.parts[0].staves[0].tablature = Some(super::super::notation::TablatureConfig {
1388 lines: 6,
1389 tuning_midi: vec![64, 59, 55, 50, 45, 40, 35],
1390 capo: 0,
1391 });
1392 let mut note = Note::new(Pitch::new(Step::E, 4), Duration::Whole);
1393 note.tab_position = Some(super::super::notation::TabPosition { string: 7, fret: 0 });
1394 note.tab_positions = vec![super::super::notation::TabPosition { string: 8, fret: 3 }];
1395 score.parts[0].staves[0].measures[0].voices[0] = vec![note];
1396
1397 let report = validate(&score);
1398 assert!(report.errors.iter().any(|error| matches!(
1399 error,
1400 ValidationError::InvalidTablature {
1401 reason: TablatureValidationReason::TooManyTunings { .. },
1402 ..
1403 }
1404 )));
1405 assert!(
1406 report
1407 .errors
1408 .iter()
1409 .any(|error| matches!(error, ValidationError::TabPositionOutOfRange { .. }))
1410 );
1411 assert_eq!(
1412 report
1413 .errors
1414 .iter()
1415 .filter(|error| matches!(error, ValidationError::TabPositionOutOfRange { .. }))
1416 .count(),
1417 2
1418 );
1419 }
1420
1421 #[test]
1422 fn validate_rejects_invalid_staff_presentation() {
1423 let mut score = Score::new("Presentation", 120, 4, 4, 0, 1);
1424 let presentation = &mut score.parts[0].staves[0].presentation;
1425 presentation.kind = StaffKind::Tablature;
1426 presentation.lines = 0;
1427 presentation.line_distance = f32::NAN;
1428
1429 let report = validate(&score);
1430 assert!(report.errors.iter().any(|error| matches!(
1431 error,
1432 ValidationError::InvalidStaffPresentation {
1433 reason: StaffPresentationValidationReason::InvalidLineCount { lines: 0 },
1434 ..
1435 }
1436 )));
1437 assert!(report.errors.iter().any(|error| matches!(
1438 error,
1439 ValidationError::InvalidStaffPresentation {
1440 reason: StaffPresentationValidationReason::InvalidLineDistance { .. },
1441 ..
1442 }
1443 )));
1444 assert!(report.errors.iter().any(|error| matches!(
1445 error,
1446 ValidationError::InvalidStaffPresentation {
1447 reason: StaffPresentationValidationReason::TablatureWithoutConfig,
1448 ..
1449 }
1450 )));
1451 }
1452
1453 #[test]
1454 fn validate_rejects_tablature_change_without_matching_base_geometry() {
1455 let mut score = Score::new("Tab change", 120, 4, 4, 0, 1);
1456 score.parts[0].staves[0].measures[0].tablature_change =
1457 Some(super::super::notation::TablatureConfig {
1458 lines: 6,
1459 tuning_midi: vec![40, 45, 50, 55, 59, 64],
1460 capo: 2,
1461 });
1462 let report = validate(&score);
1463 assert!(report.errors.iter().any(|error| matches!(
1464 error,
1465 ValidationError::InvalidTablature {
1466 reason: TablatureValidationReason::ChangeWithoutBase { measure: 0 },
1467 ..
1468 }
1469 )));
1470
1471 score.parts[0].staves[0].tablature = Some(super::super::notation::TablatureConfig {
1472 lines: 6,
1473 tuning_midi: vec![40, 45, 50, 55, 59, 64],
1474 capo: 0,
1475 });
1476 score.parts[0].staves[0].measures[0]
1477 .tablature_change
1478 .as_mut()
1479 .expect("change exists")
1480 .lines = 7;
1481 let report = validate(&score);
1482 assert!(report.errors.iter().any(|error| matches!(
1483 error,
1484 ValidationError::InvalidTablature {
1485 reason: TablatureValidationReason::ChangeLineCountMismatch {
1486 measure: 0,
1487 base_lines: 6,
1488 changed_lines: 7
1489 },
1490 ..
1491 }
1492 )));
1493 }
1494
1495 #[test]
1496 fn validate_rejects_invalid_percussion_kit_entries() {
1497 let mut score = Score::new("Kit", 120, 4, 4, 0, 1);
1498 score.parts[0].percussion_instruments = vec![
1499 PercussionInstrument {
1500 id: "snare".to_string(),
1501 name: None,
1502 midi_unpitched: Some(38),
1503 staff_position: Some(40),
1504 notehead: None,
1505 preferred_voice: Some(5),
1506 techniques: vec!["".to_string()],
1507 },
1508 PercussionInstrument {
1509 id: "snare".to_string(),
1510 name: None,
1511 midi_unpitched: Some(38),
1512 staff_position: None,
1513 notehead: None,
1514 preferred_voice: None,
1515 techniques: Vec::new(),
1516 },
1517 ];
1518
1519 let report = validate(&score);
1520 assert!(report.errors.iter().any(|error| matches!(
1521 error,
1522 ValidationError::InvalidPercussionInstrument {
1523 reason: PercussionInstrumentValidationReason::DuplicateId { first: 0 },
1524 ..
1525 }
1526 )));
1527 assert!(report.errors.iter().any(|error| matches!(
1528 error,
1529 ValidationError::InvalidPercussionInstrument {
1530 reason: PercussionInstrumentValidationReason::InvalidStaffPosition {
1531 staff_position: 40
1532 },
1533 ..
1534 }
1535 )));
1536 assert!(report.errors.iter().any(|error| matches!(
1537 error,
1538 ValidationError::InvalidPercussionInstrument {
1539 reason: PercussionInstrumentValidationReason::InvalidPreferredVoice {
1540 preferred_voice: 5
1541 },
1542 ..
1543 }
1544 )));
1545 }
1546
1547 #[test]
1548 fn validate_rejects_invalid_instrument_definition() {
1549 let mut score = Score::new("Instrument", 120, 4, 4, 0, 1);
1550 score.parts[0].instrument = Some(InstrumentDefinition {
1551 id: String::new(),
1552 name: "Broken".to_string(),
1553 short_name: String::new(),
1554 family: None,
1555 transpose_semitones: 0,
1556 written_range: Some(InstrumentRange {
1557 lowest: 80,
1558 highest: 40,
1559 }),
1560 sounding_range: None,
1561 default_clefs: Vec::new(),
1562 staff_count: 0,
1563 staff_kind: StaffKind::Standard,
1564 midi_channel: 16,
1565 midi_program: 0,
1566 percussion_map_id: None,
1567 });
1568
1569 let report = validate(&score);
1570 assert!(report.errors.iter().any(|error| matches!(
1571 error,
1572 ValidationError::InvalidInstrumentDefinition {
1573 reason: InstrumentDefinitionValidationReason::EmptyId,
1574 ..
1575 }
1576 )));
1577 assert!(report.errors.iter().any(|error| matches!(
1578 error,
1579 ValidationError::InvalidInstrumentDefinition {
1580 reason: InstrumentDefinitionValidationReason::InvalidStaffCount { staff_count: 0 },
1581 ..
1582 }
1583 )));
1584 assert!(report.errors.iter().any(|error| matches!(
1585 error,
1586 ValidationError::InvalidInstrumentDefinition {
1587 reason: InstrumentDefinitionValidationReason::InvalidRange {
1588 kind: InstrumentRangeKind::Written,
1589 ..
1590 },
1591 ..
1592 }
1593 )));
1594 }
1595
1596 #[test]
1597 fn validate_empty_part_warning() {
1598 let score = Score::new("T", 120, 4, 4, 0, 1);
1599 let report = validate(&score);
1600 assert!(
1601 report
1602 .warnings
1603 .iter()
1604 .any(|w| matches!(w, ValidationWarning::EmptyPart { part: 0 }))
1605 );
1606 }
1607
1608 #[test]
1609 fn validate_duplicate_rehearsal_mark_warning() {
1610 use crate::model::score::Score;
1611 let mut score = Score::new("T", 120, 4, 4, 0, 2);
1612 score.parts[0].staves[0].measures[0].rehearsal = Some("A".to_string());
1613 score.parts[0].staves[0].measures[1].rehearsal = Some("A".to_string());
1614 let report = validate(&score);
1615 assert!(report.warnings.iter().any(
1616 |w| matches!(w, ValidationWarning::DuplicateRehearsalMark { mark } if mark == "A")
1617 ));
1618 }
1619}