use assert_cmd::Command;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
fn bin() -> Command {
Command::new(assert_cmd::cargo::cargo_bin!("bpm-finder-tools"))
}
fn run(args: &[&str]) -> (bool, String, String) {
let output = bin().args(args).output().unwrap();
(
output.status.success(),
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
fn temp_wav_path(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
std::env::temp_dir().join(format!("{name}-{nanos}.wav"))
}
fn write_click_track_wav(path: &Path, bpm: f64, sample_rate: u32, duration_seconds: f64) {
let total_samples = (sample_rate as f64 * duration_seconds) as usize;
let mut pcm_samples = vec![0i16; total_samples];
let interval = (sample_rate as f64 * 60.0 / bpm) as usize;
let pulse_len = 512usize;
let mut beat_start = 0usize;
while beat_start < total_samples {
for offset in 0..pulse_len {
let index = beat_start + offset;
if index >= total_samples {
break;
}
let decay = 1.0 - offset as f32 / pulse_len as f32;
pcm_samples[index] = (decay * i16::MAX as f32 * 0.8) as i16;
}
beat_start += interval;
}
let bytes_per_sample = 2u16;
let channels = 1u16;
let block_align = channels * bytes_per_sample;
let byte_rate = sample_rate * block_align as u32;
let data_size = (pcm_samples.len() * bytes_per_sample as usize) as u32;
let riff_size = 36 + data_size;
let mut wav = Vec::with_capacity((44 + data_size) as usize);
wav.extend_from_slice(b"RIFF");
wav.extend_from_slice(&riff_size.to_le_bytes());
wav.extend_from_slice(b"WAVE");
wav.extend_from_slice(b"fmt ");
wav.extend_from_slice(&16u32.to_le_bytes());
wav.extend_from_slice(&1u16.to_le_bytes());
wav.extend_from_slice(&channels.to_le_bytes());
wav.extend_from_slice(&sample_rate.to_le_bytes());
wav.extend_from_slice(&byte_rate.to_le_bytes());
wav.extend_from_slice(&block_align.to_le_bytes());
wav.extend_from_slice(&(bytes_per_sample * 8).to_le_bytes());
wav.extend_from_slice(b"data");
wav.extend_from_slice(&data_size.to_le_bytes());
for sample in pcm_samples {
wav.extend_from_slice(&sample.to_le_bytes());
}
fs::write(path, wav).unwrap();
}
#[test]
fn file_command_detects_bpm_from_wav() {
let path = temp_wav_path("bpm-finder-tools");
write_click_track_wav(&path, 120.0, 44_100, 8.0);
let path_string = path.to_string_lossy().into_owned();
let (success, stdout, stderr) = run(&["file", &path_string]);
let _ = fs::remove_file(&path);
assert!(success, "{stderr}");
assert!(stdout.contains("Detected BPM: 120"));
assert!(stdout.contains("Normalized BPM: 120"));
}
#[test]
fn tap_command_outputs_expected_values() {
let (success, stdout, _) = run(&["tap", "500", "480", "495", "505"]);
assert!(success);
assert!(stdout.contains("Average interval: 495 ms"));
assert!(stdout.contains("Exact BPM: 121.212"));
assert!(stdout.contains("Rounded BPM: 121"));
}
#[test]
fn ms_command_outputs_delay_table() {
let (success, stdout, _) = run(&["ms", "128"]);
assert!(success);
assert!(stdout.contains("quarter"));
assert!(stdout.contains("468.75"));
assert!(stdout.contains("dotted quarter"));
}
#[test]
fn bpm_command_outputs_exact_and_rounded_bpm() {
let (success, stdout, _) = run(&["bpm", "500"]);
assert!(success);
assert!(stdout.contains("Exact BPM: 120"));
assert!(stdout.contains("Rounded BPM: 120"));
}
#[test]
fn normalize_command_outputs_normalized_bpm() {
let (success, stdout, _) = run(&["normalize", "72", "--min", "90", "--max", "180"]);
assert!(success);
assert!(stdout.contains("Normalized BPM: 144"));
assert!(stdout.contains("Already in range: no"));
}
#[test]
fn tap_command_fails_with_too_few_values() {
let (success, _, stderr) = run(&["tap", "500"]);
assert!(!success);
assert!(stderr.contains("tap requires at least two interval values in milliseconds"));
}
#[test]
fn normalize_command_fails_for_invalid_range() {
let (success, _, stderr) = run(&["normalize", "120", "--min", "180", "--max", "90"]);
assert!(!success);
assert!(stderr.contains("min BPM must be lower than max BPM"));
}
#[test]
fn ms_command_fails_for_invalid_number() {
let (success, _, stderr) = run(&["ms", "fast"]);
assert!(!success);
assert!(stderr.contains("invalid BPM value: fast"));
}