use assert_cmd::prelude::*; use predicates::prelude::*; use rstest::rstest;
use std::fmt;
use std::process::Command; use std::str::FromStr;
use ukebox::chord::ChordType;
use ukebox::chord::FretID;
use ukebox::chord::Tuning;
use ukebox::note::Note;
use ukebox::note::Semitones;
struct TestConfig {
chord_type: ChordType,
tuning: Tuning,
start_index: usize,
shape_dist: Semitones,
frets: [FretID; 4],
base_fret: FretID,
min_fret: FretID,
lower_min_fret: FretID,
}
impl TestConfig {
fn new(
chord_type: ChordType,
start_index: usize,
shape_dist: Semitones,
frets: [FretID; 4],
tuning: Tuning,
) -> Self {
let min_fret = *frets.iter().min().unwrap();
let max_fret = *frets.iter().max().unwrap();
let base_fret = match max_fret {
max_fret if max_fret <= 4 => 1,
_ => min_fret,
};
let lower_min_fret = match min_fret {
fret if fret < shape_dist => 0,
_ => min_fret - shape_dist,
};
Self {
chord_type,
tuning,
start_index,
shape_dist,
frets,
base_fret,
min_fret,
lower_min_fret,
}
}
fn next(&mut self) -> Self {
let mut frets = self.frets;
for f in frets.iter_mut() {
*f += 1;
}
Self::new(
self.chord_type,
self.start_index,
self.shape_dist,
frets,
self.tuning,
)
}
fn generate_diagram(&self, title: &str, notes: &[&str]) -> String {
let mut diagram = title.to_string();
let roots = ["G", "C", "E", "A"];
let interval = self.tuning.get_interval();
let nut = match self.base_fret {
1 => "||",
_ => "-|",
};
let root_width = self.tuning.get_root_width();
for i in (0..4).rev() {
let root = Note::from_str(roots[i]).unwrap() + interval;
let fret = self.frets[i];
let note = notes[i];
let sym = match fret {
0 => "o",
_ => " ",
};
let mut string = "".to_owned();
for i in self.base_fret..self.base_fret + 4 {
let c = match fret {
fret if fret == i => "o",
_ => "-",
};
string.push_str(&format!("-{}-|", c));
}
let root_str = format!("{:width$}", root.to_string(), width = root_width);
let line = format!("{} {}{}{}- {}", root_str, sym, nut, string, note);
diagram.push_str(&format!("{}\n", line));
}
if self.base_fret > 1 {
diagram.push_str(&format!(
"{:width$}\n",
self.base_fret,
width = root_width + 6
));
}
diagram
}
fn generate_tests_for_chord(&self, index: usize, note_names: &[&str]) -> (String, Vec<Test>) {
use ChordType::*;
let mut tests = Vec::new();
let root = *note_names.iter().cycle().nth(index).unwrap();
let semitones = self.tuning.get_semitones() as usize;
let roots = [7, 0, 4, 9];
let notes: Vec<&str> = roots
.iter()
.zip(self.frets.iter())
.map(|(root, fret)| {
*note_names
.iter()
.cycle()
.nth(*root as usize + *fret as usize + semitones)
.unwrap()
})
.collect();
for j in self.lower_min_fret..self.min_fret + 1 {
let suffix = match self.chord_type {
Major => "",
Minor => "m",
SuspendedSecond => "sus2",
SuspendedFourth => "sus4",
Augmented => "aug",
Diminished => "dim",
DominantSeventh => "7",
MinorSeventh => "m7",
MajorSeventh => "maj7",
MinorMajorSeventh => "mMaj7",
AugmentedSeventh => "aug7",
AugmentedMajorSeventh => "augMaj7",
DiminishedSeventh => "dim7",
HalfDiminishedSeventh => "m7b5",
};
let chord = format!("{}{}", root, suffix);
let title = format!("[{} - {} {}]\n\n", chord, root, self.chord_type);
let diagram = self.generate_diagram(&title, ¬es);
let test = Test {
chord,
tuning: self.tuning,
min_fret: j,
diagram,
};
tests.push(test);
}
(root.to_string(), tests)
}
fn generate_tests(&mut self) -> Vec<Test> {
use ChordType::*;
let note_names = [
"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
];
let alt_names = [
"C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B",
];
let rename = |name_list: [&'static str; 12], index, name| {
let mut names = name_list;
names[index] = name;
names
};
let mut tests = Vec::new();
let start_index = self.start_index + self.tuning.get_semitones() as usize;
for i in 0..13 {
let index = start_index + i;
let names = match (index % 12, self.chord_type) {
(11, Minor) => note_names,
(_, Minor) => alt_names,
(_, Diminished) => alt_names,
(5, SuspendedFourth) => alt_names,
(0, DominantSeventh) => alt_names,
(5, DominantSeventh) => alt_names,
(0, MinorSeventh) => alt_names,
(5, MinorSeventh) => alt_names,
(7, MinorSeventh) => alt_names,
(0, MinorMajorSeventh) => alt_names,
(5, MinorMajorSeventh) => alt_names,
(7, MinorMajorSeventh) => rename(note_names, 10, "Bb"),
(0, AugmentedSeventh) => rename(note_names, 10, "Bb"),
(5, AugmentedSeventh) => rename(note_names, 3, "Eb"),
(1, DiminishedSeventh) => rename(note_names, 10, "Bb"),
(6, DiminishedSeventh) => rename(note_names, 3, "Eb"),
(_, DiminishedSeventh) => alt_names,
(_, HalfDiminishedSeventh) => alt_names,
(_, _) => note_names,
};
let (root, subtests) = self.generate_tests_for_chord(index, &names);
tests.extend(subtests);
if root.ends_with("#") {
let names = match (index % 12, self.chord_type) {
(10, Augmented) => rename(alt_names, 6, "F#"),
(10, AugmentedSeventh) => rename(alt_names, 6, "F#"),
(10, AugmentedMajorSeventh) => rename(alt_names, 6, "F#"),
(_, _) => alt_names,
};
let (_root, subtests) = self.generate_tests_for_chord(index, &names);
tests.extend(subtests);
}
*self = self.next();
}
tests
}
}
struct Test {
chord: String,
tuning: Tuning,
min_fret: FretID,
diagram: String,
}
impl fmt::Display for Test {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = format!(
"cargo run -- {} -t {} -f {}\n\n{}\n",
self.chord, self.tuning, self.min_fret, self.diagram
);
write!(f, "{}", s)
}
}
fn run_tests(test_configs: Vec<TestConfig>) -> Result<(), Box<dyn std::error::Error>> {
for mut test_config in test_configs {
for test in test_config.generate_tests() {
println!("{}", test);
let mut cmd = Command::cargo_bin("ukebox")?;
cmd.arg(test.chord);
cmd.arg("-t").arg(test.tuning.to_string());
if test.min_fret > 0 {
cmd.arg("-f").arg(test.min_fret.to_string());
}
cmd.assert()
.success()
.stdout(predicate::str::contains(test.diagram));
}
}
Ok(())
}
#[test]
fn test_no_args() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("ukebox")?;
cmd.assert().failure().stderr(predicate::str::contains(
"error: The following required arguments were not provided",
));
Ok(())
}
#[test]
fn test_unknown_chord() -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = Command::cargo_bin("ukebox")?;
cmd.arg("blafoo");
cmd.assert().failure().stderr(predicate::str::contains(
"error: Invalid value for '<chord>': Could not parse chord name \"blafoo\"",
));
Ok(())
}
#[rstest(
tuning,
case::c_tuning(Tuning::C),
case::d_tuning(Tuning::D),
case::g_tuning(Tuning::G)
)]
fn test_major_chords(tuning: Tuning) -> Result<(), Box<dyn std::error::Error>> {
let ct = ChordType::Major;
let test_configs = vec![
TestConfig::new(ct, 0, 1, [0, 0, 0, 3], tuning),
TestConfig::new(ct, 9, 2, [2, 1, 0, 0], tuning),
TestConfig::new(ct, 7, 1, [0, 2, 3, 2], tuning),
TestConfig::new(ct, 5, 1, [2, 0, 1, 0], tuning),
TestConfig::new(ct, 2, 2, [2, 2, 2, 0], tuning),
];
run_tests(test_configs)
}
#[rstest(
tuning,
case::c_tuning(Tuning::C),
case::d_tuning(Tuning::D),
case::g_tuning(Tuning::G)
)]
fn test_minor_chords(tuning: Tuning) -> Result<(), Box<dyn std::error::Error>> {
let ct = ChordType::Minor;
let test_configs = vec![
TestConfig::new(ct, 0, 1, [0, 3, 3, 3], tuning),
TestConfig::new(ct, 9, 2, [2, 0, 0, 0], tuning),
TestConfig::new(ct, 7, 1, [0, 2, 3, 1], tuning),
TestConfig::new(ct, 5, 1, [1, 0, 1, 3], tuning),
TestConfig::new(ct, 2, 2, [2, 2, 1, 0], tuning),
];
run_tests(test_configs)
}
#[rstest(
tuning,
case::c_tuning(Tuning::C),
case::d_tuning(Tuning::D),
case::g_tuning(Tuning::G)
)]
fn test_suspended_second_chords(tuning: Tuning) -> Result<(), Box<dyn std::error::Error>> {
let ct = ChordType::SuspendedSecond;
let test_configs = vec![
TestConfig::new(ct, 0, 1, [0, 2, 3, 3], tuning),
TestConfig::new(ct, 10, 1, [3, 0, 1, 1], tuning),
TestConfig::new(ct, 7, 1, [0, 2, 3, 0], tuning),
TestConfig::new(ct, 5, 1, [0, 0, 1, 3], tuning),
TestConfig::new(ct, 2, 2, [2, 2, 0, 0], tuning),
];
run_tests(test_configs)
}
#[rstest(
tuning,
case::c_tuning(Tuning::C),
case::d_tuning(Tuning::D),
case::g_tuning(Tuning::G)
)]
fn test_suspended_fourth_chords(tuning: Tuning) -> Result<(), Box<dyn std::error::Error>> {
let ct = ChordType::SuspendedFourth;
let test_configs = vec![
TestConfig::new(ct, 0, 1, [0, 0, 1, 3], tuning),
TestConfig::new(ct, 9, 2, [2, 2, 0, 0], tuning),
TestConfig::new(ct, 7, 1, [0, 2, 3, 3], tuning),
TestConfig::new(ct, 5, 1, [3, 0, 1, 1], tuning),
TestConfig::new(ct, 2, 2, [0, 2, 3, 0], tuning),
];
run_tests(test_configs)
}
#[rstest(
tuning,
case::c_tuning(Tuning::C),
case::d_tuning(Tuning::D),
case::g_tuning(Tuning::G)
)]
fn test_augmented_chords(tuning: Tuning) -> Result<(), Box<dyn std::error::Error>> {
let ct = ChordType::Augmented;
let test_configs = vec![
TestConfig::new(ct, 0, 0, [1, 0, 0, 3], tuning),
TestConfig::new(ct, 9, 2, [2, 1, 1, 0], tuning),
TestConfig::new(ct, 8, 0, [1, 0, 0, 3], tuning),
TestConfig::new(ct, 7, 0, [0, 3, 3, 2], tuning),
TestConfig::new(ct, 5, 1, [2, 1, 1, 0], tuning),
TestConfig::new(ct, 4, 0, [1, 0, 0, 3], tuning),
TestConfig::new(ct, 1, 2, [2, 1, 1, 0], tuning),
];
run_tests(test_configs)
}
#[rstest(
tuning,
case::c_tuning(Tuning::C),
case::d_tuning(Tuning::D),
case::g_tuning(Tuning::G)
)]
fn test_diminished_chords(tuning: Tuning) -> Result<(), Box<dyn std::error::Error>> {
let ct = ChordType::Diminished;
let test_configs = vec![
TestConfig::new(ct, 2, 2, [7, 5, 4, 5], tuning),
TestConfig::new(ct, 10, 2, [3, 1, 0, 1], tuning),
TestConfig::new(ct, 7, 1, [0, 1, 3, 1], tuning),
TestConfig::new(ct, 6, 0, [2, 0, 2, 0], tuning),
TestConfig::new(ct, 3, 2, [2, 3, 2, 0], tuning),
];
run_tests(test_configs)
}
#[rstest(
tuning,
case::c_tuning(Tuning::C),
case::d_tuning(Tuning::D),
case::g_tuning(Tuning::G)
)]
fn test_dominant_seventh_chords(tuning: Tuning) -> Result<(), Box<dyn std::error::Error>> {
let ct = ChordType::DominantSeventh;
let test_configs = vec![
TestConfig::new(ct, 0, 1, [0, 0, 0, 1], tuning),
TestConfig::new(ct, 9, 2, [0, 1, 0, 0], tuning),
TestConfig::new(ct, 7, 1, [0, 2, 1, 2], tuning),
TestConfig::new(ct, 4, 1, [1, 2, 0, 2], tuning),
];
run_tests(test_configs)
}
#[rstest(
tuning,
case::c_tuning(Tuning::C),
case::d_tuning(Tuning::D),
case::g_tuning(Tuning::G)
)]
fn test_minor_seventh_chords(tuning: Tuning) -> Result<(), Box<dyn std::error::Error>> {
let ct = ChordType::MinorSeventh;
let test_configs = vec![
TestConfig::new(ct, 1, 0, [1, 1, 0, 2], tuning),
TestConfig::new(ct, 9, 2, [0, 0, 0, 0], tuning),
TestConfig::new(ct, 7, 1, [0, 2, 1, 1], tuning),
TestConfig::new(ct, 4, 1, [0, 2, 0, 2], tuning),
];
run_tests(test_configs)
}
#[rstest(
tuning,
case::c_tuning(Tuning::C),
case::d_tuning(Tuning::D),
case::g_tuning(Tuning::G)
)]
fn test_major_seventh_chords(tuning: Tuning) -> Result<(), Box<dyn std::error::Error>> {
let ct = ChordType::MajorSeventh;
let test_configs = vec![
TestConfig::new(ct, 0, 1, [0, 0, 0, 2], tuning),
TestConfig::new(ct, 10, 1, [3, 2, 1, 0], tuning),
TestConfig::new(ct, 9, 0, [1, 1, 0, 0], tuning),
TestConfig::new(ct, 7, 1, [0, 2, 2, 2], tuning),
TestConfig::new(ct, 4, 1, [1, 3, 0, 2], tuning),
];
run_tests(test_configs)
}
#[rstest(
tuning,
case::c_tuning(Tuning::C),
case::d_tuning(Tuning::D),
case::g_tuning(Tuning::G)
)]
fn test_minor_major_seventh_chords(tuning: Tuning) -> Result<(), Box<dyn std::error::Error>> {
let ct = ChordType::MinorMajorSeventh;
let test_configs = vec![
TestConfig::new(ct, 1, 0, [1, 1, 0, 3], tuning),
TestConfig::new(ct, 10, 2, [3, 1, 1, 0], tuning),
TestConfig::new(ct, 9, 0, [1, 0, 0, 0], tuning),
TestConfig::new(ct, 7, 1, [0, 2, 2, 1], tuning),
TestConfig::new(ct, 4, 2, [0, 3, 0, 2], tuning),
];
run_tests(test_configs)
}
#[rstest(
tuning,
case::c_tuning(Tuning::C),
case::d_tuning(Tuning::D),
case::g_tuning(Tuning::G)
)]
fn test_augmented_seventh_chords(tuning: Tuning) -> Result<(), Box<dyn std::error::Error>> {
let ct = ChordType::AugmentedSeventh;
let test_configs = vec![
TestConfig::new(ct, 0, 1, [1, 0, 0, 1], tuning),
TestConfig::new(ct, 9, 2, [0, 1, 1, 0], tuning),
TestConfig::new(ct, 7, 1, [0, 3, 1, 2], tuning),
TestConfig::new(ct, 4, 1, [1, 2, 0, 3], tuning),
];
run_tests(test_configs)
}
#[rstest(
tuning,
case::c_tuning(Tuning::C),
case::d_tuning(Tuning::D),
case::g_tuning(Tuning::G)
)]
fn test_augmented_major_seventh_chords(tuning: Tuning) -> Result<(), Box<dyn std::error::Error>> {
let ct = ChordType::AugmentedMajorSeventh;
let test_configs = vec![
TestConfig::new(ct, 0, 1, [1, 0, 0, 2], tuning),
TestConfig::new(ct, 10, 1, [3, 2, 2, 0], tuning),
TestConfig::new(ct, 9, 0, [1, 1, 1, 0], tuning),
TestConfig::new(ct, 7, 1, [0, 3, 2, 2], tuning),
TestConfig::new(ct, 4, 2, [1, 3, 0, 3], tuning),
];
run_tests(test_configs)
}
#[rstest(
tuning,
case::c_tuning(Tuning::C),
case::d_tuning(Tuning::D),
case::g_tuning(Tuning::G)
)]
fn test_diminished_seventh_chords(tuning: Tuning) -> Result<(), Box<dyn std::error::Error>> {
let ct = ChordType::DiminishedSeventh;
let test_configs = vec![
TestConfig::new(ct, 1, 2, [0, 1, 0, 1], tuning),
TestConfig::new(ct, 10, 2, [0, 1, 0, 1], tuning),
TestConfig::new(ct, 7, 2, [0, 1, 0, 1], tuning),
TestConfig::new(ct, 4, 2, [0, 1, 0, 1], tuning),
];
run_tests(test_configs)
}
#[rstest(
tuning,
case::c_tuning(Tuning::C),
case::d_tuning(Tuning::D),
case::g_tuning(Tuning::G)
)]
fn test_half_diminished_seventh_chords(tuning: Tuning) -> Result<(), Box<dyn std::error::Error>> {
let ct = ChordType::HalfDiminishedSeventh;
let test_configs = vec![
TestConfig::new(ct, 1, 2, [0, 1, 0, 2], tuning),
TestConfig::new(ct, 10, 2, [1, 1, 0, 1], tuning),
TestConfig::new(ct, 7, 2, [0, 1, 1, 1], tuning),
TestConfig::new(ct, 4, 2, [0, 2, 0, 1], tuning),
];
run_tests(test_configs)
}