|
| 1 | +/// Integration tests to make sure all possible combinations of user input |
| 2 | +/// result in the correct output. |
| 3 | +/// This is done by generating all combinations of command line arguments |
| 4 | +/// and options together with their expected output. The real program is called |
| 5 | +/// using the command line parameters and the actual output is compared to the |
| 6 | +/// expected output. |
| 7 | +
|
| 8 | +use assert_cmd::prelude::*; // Add methods on commands |
| 9 | +use predicates::prelude::*; // Used for writing assertions |
| 10 | +use std::fmt; |
| 11 | +use std::process::Command; // Run programs |
| 12 | +use ukebox::chord::ChordQuality; |
| 13 | +use ukebox::Frets; |
| 14 | + |
| 15 | +/// A set of parameters to generate tests for all chords produced |
| 16 | +/// by moving a specific chord shape along the fretboard. |
| 17 | +struct TestConfig { |
| 18 | + chord_quality: ChordQuality, |
| 19 | + /// Start index in the note vector (= root of the chord shape). |
| 20 | + start_index: usize, |
| 21 | + /// Distance to the previous chord shape. |
| 22 | + shape_dist: Frets, |
| 23 | + frets: [Frets; 4], |
| 24 | + note_indices: [usize; 4], |
| 25 | + base_fret: Frets, |
| 26 | + min_fret: Frets, |
| 27 | + lower_min_fret: Frets, |
| 28 | +} |
| 29 | + |
| 30 | +impl TestConfig { |
| 31 | + fn new( |
| 32 | + chord_quality: ChordQuality, |
| 33 | + start_index: usize, |
| 34 | + shape_dist: Frets, |
| 35 | + frets: [Frets; 4], |
| 36 | + note_indices: [usize; 4], |
| 37 | + ) -> Self { |
| 38 | + let min_fret = *frets.iter().min().unwrap(); |
| 39 | + let max_fret = *frets.iter().max().unwrap(); |
| 40 | + |
| 41 | + let base_fret = match max_fret { |
| 42 | + max_fret if max_fret <= 4 => 1, |
| 43 | + _ => min_fret, |
| 44 | + }; |
| 45 | + |
| 46 | + let lower_min_fret = match min_fret { |
| 47 | + fret if fret < shape_dist => 0, |
| 48 | + _ => min_fret - shape_dist, |
| 49 | + }; |
| 50 | + |
| 51 | + Self { |
| 52 | + chord_quality, |
| 53 | + start_index, |
| 54 | + shape_dist, |
| 55 | + frets, |
| 56 | + note_indices, |
| 57 | + base_fret, |
| 58 | + min_fret, |
| 59 | + lower_min_fret, |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + /// Move all frets and notes one fret/semitone higher. |
| 64 | + fn next(&mut self) -> Self { |
| 65 | + let mut frets = self.frets; |
| 66 | + for f in frets.iter_mut() { |
| 67 | + *f += 1; |
| 68 | + } |
| 69 | + |
| 70 | + let mut note_indices = self.note_indices; |
| 71 | + for n in note_indices.iter_mut() { |
| 72 | + *n += 1; |
| 73 | + } |
| 74 | + |
| 75 | + Self::new( |
| 76 | + self.chord_quality, |
| 77 | + self.start_index, |
| 78 | + self.shape_dist, |
| 79 | + frets, |
| 80 | + note_indices, |
| 81 | + ) |
| 82 | + } |
| 83 | + |
| 84 | + fn generate_diagram(&self, title: &str, notes: &[&str]) -> String { |
| 85 | + let mut diagram = title.to_string(); |
| 86 | + let roots = ["G", "C", "E", "A"]; |
| 87 | + |
| 88 | + // Show a symbol for the nut if the chord is played on the lower |
| 89 | + // end of the fretboard. Indicate ongoing strings otherwise. |
| 90 | + let nut = match self.base_fret { |
| 91 | + 1 => "||", |
| 92 | + _ => "-|", |
| 93 | + }; |
| 94 | + |
| 95 | + for i in (0..4).rev() { |
| 96 | + let root = roots[i]; |
| 97 | + let fret = self.frets[i]; |
| 98 | + let note = notes[i]; |
| 99 | + |
| 100 | + // Mark open strings with a special symbol. |
| 101 | + let sym = match fret { |
| 102 | + 0 => "o", |
| 103 | + _ => " ", |
| 104 | + }; |
| 105 | + |
| 106 | + // Create a line representing the string with the fret to be pressed. |
| 107 | + let mut string = "".to_owned(); |
| 108 | + |
| 109 | + for i in self.base_fret..self.base_fret + 4 { |
| 110 | + let c = match fret { |
| 111 | + fret if fret == i => "o", |
| 112 | + _ => "-", |
| 113 | + }; |
| 114 | + |
| 115 | + string.push_str(&format!("-{}-|", c)); |
| 116 | + } |
| 117 | + |
| 118 | + let line = format!("{} {}{}{}- {}", root, sym, nut, string, note); |
| 119 | + diagram.push_str(&format!("{}\n", line)); |
| 120 | + } |
| 121 | + |
| 122 | + // If the fretboard section shown does not include the nut, |
| 123 | + // indicate the number of the first fret shown. |
| 124 | + if self.base_fret > 1 { |
| 125 | + diagram.push_str(&format!(" {}\n", self.base_fret)) |
| 126 | + } |
| 127 | + |
| 128 | + diagram |
| 129 | + } |
| 130 | + |
| 131 | + fn generate_tests_for_chord(&self, index: usize, note_names: &[&str]) -> (String, Vec<Test>) { |
| 132 | + let mut tests = Vec::new(); |
| 133 | + let root = *note_names.iter().cycle().nth(index).unwrap(); |
| 134 | + |
| 135 | + let notes: Vec<&str> = self |
| 136 | + .note_indices |
| 137 | + .iter() |
| 138 | + .map(|j| *note_names.iter().cycle().nth(*j).unwrap()) |
| 139 | + .collect(); |
| 140 | + |
| 141 | + for j in self.lower_min_fret..self.min_fret + 1 { |
| 142 | + let suffix = match self.chord_quality { |
| 143 | + ChordQuality::Major => "", |
| 144 | + ChordQuality::Minor => "m", |
| 145 | + }; |
| 146 | + let chord = format!("{}{}", root, suffix); |
| 147 | + let title = format!("[{} - {} {}]\n\n", chord, root, self.chord_quality); |
| 148 | + let diagram = self.generate_diagram(&title, ¬es); |
| 149 | + let test = Test { |
| 150 | + chord, |
| 151 | + min_fret: j, |
| 152 | + diagram, |
| 153 | + }; |
| 154 | + tests.push(test); |
| 155 | + } |
| 156 | + |
| 157 | + (root.to_string(), tests) |
| 158 | + } |
| 159 | + |
| 160 | + fn generate_tests(&mut self) -> Vec<Test> { |
| 161 | + let note_names = [ |
| 162 | + "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", |
| 163 | + ]; |
| 164 | + let alt_names = [ |
| 165 | + "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B", |
| 166 | + ]; |
| 167 | + |
| 168 | + let mut tests = Vec::new(); |
| 169 | + |
| 170 | + // Move upwards the fretboard using the given chord shape. |
| 171 | + for i in 0..13 { |
| 172 | + let index = self.start_index + i; |
| 173 | + let names = match (index, self.chord_quality) { |
| 174 | + // Bm has F#. |
| 175 | + (11, ChordQuality::Minor) => note_names, |
| 176 | + // All other minor chords have flat notes. |
| 177 | + (_, ChordQuality::Minor) => alt_names, |
| 178 | + (_, _) => note_names, |
| 179 | + }; |
| 180 | + |
| 181 | + let (root, subtests) = self.generate_tests_for_chord(index, &names); |
| 182 | + tests.extend(subtests); |
| 183 | + |
| 184 | + if root.ends_with("#") { |
| 185 | + let (_root, subtests) = self.generate_tests_for_chord(index, &alt_names); |
| 186 | + tests.extend(subtests); |
| 187 | + } |
| 188 | + |
| 189 | + *self = self.next(); |
| 190 | + } |
| 191 | + |
| 192 | + tests |
| 193 | + } |
| 194 | +} |
| 195 | + |
| 196 | +/// A set of command line arguments and options together with the |
| 197 | +/// expected output (chord diagram) to be shown. |
| 198 | +struct Test { |
| 199 | + chord: String, |
| 200 | + min_fret: Frets, |
| 201 | + diagram: String, |
| 202 | +} |
| 203 | + |
| 204 | +impl fmt::Display for Test { |
| 205 | + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 206 | + let s = format!( |
| 207 | + "cargo run -- {} -f {}\n\n{}\n", |
| 208 | + self.chord, self.min_fret, self.diagram |
| 209 | + ); |
| 210 | + |
| 211 | + write!(f, "{}", s) |
| 212 | + } |
| 213 | +} |
| 214 | + |
| 215 | +fn run_tests(test_configs: Vec<TestConfig>) -> Result<(), Box<dyn std::error::Error>> { |
| 216 | + for mut test_config in test_configs { |
| 217 | + for test in test_config.generate_tests() { |
| 218 | + // Run `cargo test -- --nocapture` to print all tests run. |
| 219 | + println!("{}", test); |
| 220 | + |
| 221 | + let mut cmd = Command::main_binary()?; |
| 222 | + cmd.arg(test.chord); |
| 223 | + |
| 224 | + if test.min_fret > 0 { |
| 225 | + cmd.arg("-f").arg(test.min_fret.to_string()); |
| 226 | + } |
| 227 | + |
| 228 | + cmd.assert() |
| 229 | + .success() |
| 230 | + .stdout(predicate::str::contains(test.diagram)); |
| 231 | + } |
| 232 | + } |
| 233 | + |
| 234 | + Ok(()) |
| 235 | +} |
| 236 | + |
| 237 | +#[test] |
| 238 | +fn test_no_args() -> Result<(), Box<dyn std::error::Error>> { |
| 239 | + let mut cmd = Command::main_binary()?; |
| 240 | + cmd.assert().failure().stderr(predicate::str::contains( |
| 241 | + "error: The following required arguments were not provided", |
| 242 | + )); |
| 243 | + |
| 244 | + Ok(()) |
| 245 | +} |
| 246 | + |
| 247 | +#[test] |
| 248 | +fn test_major_chords() -> Result<(), Box<dyn std::error::Error>> { |
| 249 | + let cq = ChordQuality::Major; |
| 250 | + |
| 251 | + let test_configs = vec![ |
| 252 | + TestConfig::new(cq, 0, 1, [0, 0, 0, 3], [7, 0, 4, 0]), |
| 253 | + TestConfig::new(cq, 9, 2, [2, 1, 0, 0], [9, 1, 4, 9]), |
| 254 | + TestConfig::new(cq, 7, 1, [0, 2, 3, 2], [7, 2, 7, 11]), |
| 255 | + TestConfig::new(cq, 5, 1, [2, 0, 1, 0], [9, 0, 5, 9]), |
| 256 | + TestConfig::new(cq, 2, 2, [2, 2, 2, 0], [9, 2, 6, 9]), |
| 257 | + ]; |
| 258 | + |
| 259 | + run_tests(test_configs) |
| 260 | +} |
| 261 | + |
| 262 | +#[test] |
| 263 | +fn test_minor_chords() -> Result<(), Box<dyn std::error::Error>> { |
| 264 | + let cq = ChordQuality::Minor; |
| 265 | + |
| 266 | + let test_configs = vec![ |
| 267 | + TestConfig::new(cq, 0, 1, [0, 3, 3, 3], [7, 3, 7, 0]), |
| 268 | + TestConfig::new(cq, 9, 2, [2, 0, 0, 0], [9, 0, 4, 9]), |
| 269 | + TestConfig::new(cq, 7, 1, [0, 2, 3, 1], [7, 2, 7, 10]), |
| 270 | + TestConfig::new(cq, 5, 1, [1, 0, 1, 3], [8, 0, 5, 0]), |
| 271 | + TestConfig::new(cq, 2, 2, [2, 2, 1, 0], [9, 2, 5, 9]), |
| 272 | + ]; |
| 273 | + |
| 274 | + run_tests(test_configs) |
| 275 | +} |
0 commit comments