Files
soundcraft/crates/midi/tests/it/ops.rs
T
aiongg e8abd44712 CI: one test binary per crate, nextest, line tables only (#985)
* Build: line tables only for workspace crates, no debug info for dependencies

Full debug info made most of the dev and test target size and link time. Line tables keep file and line numbers in backtraces; dependency frames lose them.

* Tests: build each crate's integration tests as one binary

Each tests/*.rs file was its own test binary: 98 binaries, each linking the crate and its dependencies. The files are now modules of tests/it/main.rs, one binary per crate, which cuts link time and target size. Filter by module: `cargo test -p <crate> --test it <module>::`.

Binaries with their own main (harness = false) in clap-host and vst3-host stay separate, as do the ui-egui tests that switch the process-wide Light/Dark palette and recovery.rs, whose autosave locks leak into child processes other tests start.

* Xtask: run tests with nextest, doctests apart, and no examples in the test build

cargo xtask ci runs the tests with cargo nextest when it is installed, else cargo test with a notice, and doctests with cargo test --doc. The test build leaves out examples (--lib --bins --tests); clippy --all-targets still compiles them. Test binaries with harness = false run under cargo test: their own main cannot answer nextest's --list. clippy and the wasm check keep going past a failing crate. cargo xtask test runs the test step alone.
2026-10-23 14:00:38 -04:00

218 lines
8.5 KiB
Rust

use soundcraft_midi::ops::*;
use soundcraft_midi::*;
fn n(pitch: u8, start: i64, length: i64) -> Note {
Note { pitch, velocity: 100, release_velocity: 64, channel: 0, start, length }
}
fn starts(notes: &[Note]) -> Vec<i64> {
notes.iter().map(|n| n.start).collect()
}
#[test]
fn quantize_full_strength() {
let mut notes = vec![n(60, 10, 200), n(60, 230, 100), n(60, 365, 50), n(60, -20, 40)];
quantize(&mut notes, &QuantizeOptions { grid_ticks: 240, ..Default::default() });
assert_eq!(starts(&notes), vec![0, 240, 480, 0]);
// Durations preserved by default.
assert_eq!(notes.iter().map(|n| n.length).collect::<Vec<_>>(), vec![200, 100, 50, 40]);
}
#[test]
fn quantize_half_strength() {
let mut notes = vec![n(60, 20, 100), n(60, 220, 100)];
quantize(&mut notes, &QuantizeOptions { grid_ticks: 240, strength: 0.5, ..Default::default() });
assert_eq!(starts(&notes), vec![10, 230]);
}
#[test]
fn quantize_swing() {
// 100% swing on a 1/8 (480) grid moves odd lines by 480/3 = 160 (triplet feel).
let mut notes = vec![n(60, 0, 10), n(60, 470, 10), n(60, 650, 10), n(60, 950, 10)];
quantize(&mut notes, &QuantizeOptions { grid_ticks: 480, swing: 1.0, ..Default::default() });
assert_eq!(starts(&notes), vec![0, 640, 640, 960]);
let mut notes = vec![n(60, 480, 10)];
quantize(&mut notes, &QuantizeOptions { grid_ticks: 480, swing: 0.5, ..Default::default() });
assert_eq!(starts(&notes), vec![560]);
}
#[test]
fn quantize_include_exclude() {
let opts = QuantizeOptions { grid_ticks: 240, include_within: 0.5, ..Default::default() };
// Half grid = 120; include within 60 ticks.
let mut notes = vec![n(60, 50, 10), n(60, 100, 10)];
quantize(&mut notes, &opts);
assert_eq!(starts(&notes), vec![0, 100]);
let opts = QuantizeOptions { grid_ticks: 240, exclude_within: 0.25, ..Default::default() };
// Exclude closer than 30 ticks.
let mut notes = vec![n(60, 20, 10), n(60, 50, 10)];
quantize(&mut notes, &opts);
assert_eq!(starts(&notes), vec![20, 0]);
}
#[test]
fn quantize_ends_and_duration() {
let mut notes = vec![n(60, 10, 220)];
quantize(&mut notes, &QuantizeOptions { grid_ticks: 240, quantize_ends: true, ..Default::default() });
assert_eq!((notes[0].start, notes[0].length), (0, 240));
let mut notes = vec![n(60, 10, 220)];
quantize(&mut notes, &QuantizeOptions { grid_ticks: 240, preserve_duration: false, ..Default::default() });
assert_eq!((notes[0].start, notes[0].length), (0, 230));
// Ends quantized onto the start line still keep a length of at least 1.
let mut notes = vec![n(60, 10, 5)];
quantize(&mut notes, &QuantizeOptions { grid_ticks: 240, quantize_ends: true, ..Default::default() });
assert_eq!(notes[0].length, 1);
}
#[test]
fn quantize_random_is_deterministic_and_bounded() {
let opts = QuantizeOptions { grid_ticks: 240, random: 0.5, ..Default::default() };
let mut a: Vec<Note> = (0..50).map(|i| n(60, i * 240 + 7, 10)).collect();
let mut b = a.clone();
quantize(&mut a, &opts);
quantize(&mut b, &opts);
assert_eq!(a, b);
for (i, note) in a.iter().enumerate() {
assert!((note.start - i as i64 * 240).abs() <= 60);
}
let mut c: Vec<Note> = (0..50).map(|i| n(60, i * 240 + 7, 10)).collect();
quantize_with_seed(&mut c, &opts, 42);
assert_ne!(a, c);
}
#[test]
fn quantize_degenerate_options() {
let mut notes = vec![n(60, 17, 10)];
quantize(&mut notes, &QuantizeOptions { grid_ticks: 0, ..Default::default() });
quantize(&mut notes, &QuantizeOptions { grid_ticks: -5, ..Default::default() });
assert_eq!(notes[0].start, 17);
quantize(&mut notes, &QuantizeOptions { grid_ticks: 240, strength: f32::NAN, swing: f32::INFINITY, ..Default::default() });
assert_eq!(notes[0].start, 17);
let mut extreme = vec![n(60, i64::MAX - 10, 1), n(60, i64::MIN + 10, 1)];
quantize(&mut extreme, &QuantizeOptions { grid_ticks: i64::MAX, random: 1.0, ..Default::default() });
}
#[test]
fn transpose_clamps() {
let mut notes = vec![n(0, 0, 1), n(60, 0, 1), n(127, 0, 1)];
transpose(&mut notes, 12);
assert_eq!(notes.iter().map(|n| n.pitch).collect::<Vec<_>>(), vec![12, 72, 127]);
transpose(&mut notes, -100);
assert_eq!(notes.iter().map(|n| n.pitch).collect::<Vec<_>>(), vec![0, 0, 27]);
transpose(&mut notes, i32::MAX);
assert!(notes.iter().all(|n| n.pitch == 127));
}
#[test]
fn velocity_ops() {
let base = vec![n(60, 0, 1), n(60, 100, 1), n(60, 200, 1)];
let vels = |v: &[Note]| v.iter().map(|n| n.velocity).collect::<Vec<_>>();
let mut v = base.clone();
change_velocity(&mut v, VelocityOp::Set(0));
assert_eq!(vels(&v), vec![1, 1, 1]);
change_velocity(&mut v, VelocityOp::Set(200));
assert_eq!(vels(&v), vec![127, 127, 127]);
let mut v = base.clone();
change_velocity(&mut v, VelocityOp::Add(50));
assert_eq!(vels(&v), vec![127; 3]);
change_velocity(&mut v, VelocityOp::Add(-500));
assert_eq!(vels(&v), vec![1; 3]);
let mut v = base.clone();
change_velocity(&mut v, VelocityOp::Scale(0.5));
assert_eq!(vels(&v), vec![50; 3]);
change_velocity(&mut v, VelocityOp::Scale(f32::NAN));
assert_eq!(vels(&v), vec![50; 3]);
let mut v = vec![n(60, 0, 1), n(60, 0, 1)];
v[0].velocity = 10;
v[1].velocity = 120;
change_velocity(&mut v, VelocityOp::Limit { min: 100, max: 30 });
assert_eq!(vels(&v), vec![30, 100]);
let mut v = base.clone();
change_velocity(&mut v, VelocityOp::Ramp { from: 20, to: 120 });
assert_eq!(vels(&v), vec![20, 70, 120]);
let mut a = base.clone();
let mut b = base.clone();
change_velocity(&mut a, VelocityOp::Humanize(10));
change_velocity(&mut b, VelocityOp::Humanize(10));
assert_eq!(a, b);
assert!(a.iter().all(|n| (90..=110).contains(&n.velocity)));
}
#[test]
fn duration_ops() {
let lens = |v: &[Note]| v.iter().map(|n| n.length).collect::<Vec<_>>();
let base = vec![n(60, 0, 100), n(62, 480, 100), n(64, 480, 50), n(60, 960, 100)];
let mut v = base.clone();
change_duration(&mut v, DurationOp::Set(0));
assert_eq!(lens(&v), vec![1; 4]);
let mut v = base.clone();
change_duration(&mut v, DurationOp::Add(-75));
assert_eq!(lens(&v), vec![25, 25, 1, 25]);
let mut v = base.clone();
change_duration(&mut v, DurationOp::Scale(2.0));
assert_eq!(lens(&v), vec![200, 200, 100, 200]);
let mut v = base.clone();
change_duration(&mut v, DurationOp::Legato { gap: 10 });
assert_eq!(lens(&v), vec![470, 470, 470, 100]);
let mut v = vec![n(60, 0, 500), n(60, 200, 500), n(61, 100, 500), Note { channel: 1, ..n(60, 100, 500) }];
change_duration(&mut v, DurationOp::RemoveOverlap);
assert_eq!(lens(&v), vec![200, 500, 500, 500]);
}
#[test]
fn split_by_pitch() {
let notes = vec![n(36, 0, 1), n(60, 1, 1), n(72, 2, 1), n(48, 3, 1)];
let (inside, outside) = split_notes(&notes, 60, 40);
assert_eq!(inside.iter().map(|n| n.pitch).collect::<Vec<_>>(), vec![60, 48]);
assert_eq!(outside.iter().map(|n| n.pitch).collect::<Vec<_>>(), vec![36, 72]);
}
#[test]
fn sequence_helpers() {
let mut seq = Sequence {
notes: vec![n(64, 100, 50), n(60, 100, 10), n(60, 100, 30), n(60, 0, 10), Note { channel: 2, ..n(60, 0, 10) }],
ctrls: vec![
CtrlEvent { tick: 500, channel: 0, kind: CtrlKind::Cc(1), value: 1 },
CtrlEvent { tick: 5, channel: 0, kind: CtrlKind::Cc(1), value: 2 },
CtrlEvent { tick: 500, channel: 0, kind: CtrlKind::Cc(1), value: 3 },
CtrlEvent { tick: 500, channel: 0, kind: CtrlKind::Cc(2), value: 4 },
],
};
assert_eq!(seq.end_tick(), 500);
seq.sort();
assert_eq!(starts(&seq.notes), vec![0, 0, 100, 100, 100]);
assert_eq!(seq.notes[2].pitch, 60);
assert_eq!(seq.notes_in(0, 100).count(), 2);
assert_eq!(seq.notes_overlapping(105, 106).count(), 3);
let removed = seq.remove_duplicates();
assert_eq!(removed, 2);
assert_eq!(seq.notes.len(), 4);
let kept = seq.notes.iter().find(|n| n.start == 100 && n.pitch == 60).unwrap();
assert_eq!(kept.length, 30);
assert_eq!(seq.ctrls.iter().map(|c| c.value).collect::<Vec<_>>(), vec![2, 3, 4]);
assert_eq!(seq.remove_duplicates(), 0);
assert_eq!(Sequence::default().end_tick(), 0);
}
#[test]
fn sequence_serde_round_trip() {
let seq = Sequence { notes: vec![n(60, 0, 1)], ctrls: vec![CtrlEvent { tick: 1, channel: 0, kind: CtrlKind::PolyPressure(60), value: 9 }] };
let json = serde_json::to_string(&seq).unwrap();
assert_eq!(serde_json::from_str::<Sequence>(&json).unwrap(), seq);
}