Skip to content

Commit 108b246

Browse files
authored
Add integration tests (#22)
* Update unit tests * Add dependencies for integration tests * Add integration tests * Small change by cargo fmt * Test several values for min-fret per chord shape * Turn frets and note_indices into function arguments * Add struct TestConfig * Add TestConfig.generate_tests() * Move generate_diagram() to TestConfig * Run multiple TestConfig instances * Add function run_tests() * Add ChordQuality * Add tests for minor chords * Add comments
1 parent b391c28 commit 108b246

File tree

4 files changed

+292
-15
lines changed

4 files changed

+292
-15
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,7 @@ regex = "1"
1111
structopt = "0.3"
1212

1313
[dev-dependencies]
14+
assert_cmd = "0.10"
1415
indoc = "0.3"
16+
predicates = "1"
1517
rstest = "0.3"

src/diagram/chord_diagram.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::chord::Chord;
22
use crate::diagram::StringDiagram;
3-
use crate::note::Note;
43
use crate::diagram::CHART_WIDTH;
4+
use crate::note::Note;
55
use crate::FretPattern;
66
use crate::Frets;
77
use crate::NotePattern;
@@ -89,10 +89,10 @@ mod tests {
8989
indoc!("
9090
[C - C major]
9191
92-
A ||---+---+-●-+---+ C
93-
E ||---+---+---+---+ E
94-
C ||---+---+---+---+ C
95-
G ||---+---+---+---+ G
92+
A ||---|---|-o-|---|- C
93+
E o||---|---|---|---|- E
94+
C o||---|---|---|---|- C
95+
G o||---|---|---|---|- G
9696
"),
9797
),
9898
case(
@@ -101,10 +101,10 @@ mod tests {
101101
indoc!("
102102
[C - C major]
103103
104-
A -+-●-+---+---+---+ C
105-
E -+-●-+---+---+---+ G
106-
C -+---+-●-+---+---+ E
107-
G -+---+---+-●-+---+ C
104+
A -|-o-|---|---|---|- C
105+
E -|-o-|---|---|---|- G
106+
C -|---|-o-|---|---|- E
107+
G -|---|---|-o-|---|- C
108108
3
109109
")
110110
),

src/diagram/string_diagram.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use crate::note::Note;
21
use crate::diagram::CHART_WIDTH;
2+
use crate::note::Note;
33
use crate::Frets;
44
use std::fmt;
55

@@ -65,11 +65,11 @@ mod tests {
6565
fret,
6666
note_name,
6767
diagram,
68-
case("C", 1, 0, "C", "C ||---+---+---+---+ C"),
69-
case("C", 1, 4, "E", "C ||---+---+---+-●-+ E"),
70-
case("C", 1, 2, "D", "C ||---+-●-+---+---+ D"),
71-
case("G", 1, 4, "B", "G ||---+---+---+-●-+ B"),
72-
case("C", 5, 7, "G", "C -+---+---+-●-+---+ G")
68+
case("C", 1, 0, "C", "C o||---|---|---|---|- C"),
69+
case("C", 1, 4, "E", "C ||---|---|---|-o-|- E"),
70+
case("C", 1, 2, "D", "C ||---|-o-|---|---|- D"),
71+
case("G", 1, 4, "B", "G ||---|---|---|-o-|- B"),
72+
case("C", 5, 7, "G", "C -|---|---|-o-|---|- G")
7373
)]
7474
fn test_format_line(
7575
root_name: &str,

tests/ukebox.rs

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
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, &notes);
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

Comments
 (0)