• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 //! Various utility functions used in tests.
2 
3 // This file is included directly into integration tests in the
4 // `tests/` directory. These tests are compiled without access to the
5 // rest of the `pdl` crate. To make this work, avoid `use crate::`
6 // statements below.
7 
8 use quote::quote;
9 use std::fs;
10 use std::io::Write;
11 use std::path::Path;
12 use std::process::{Command, Stdio};
13 use tempfile::NamedTempFile;
14 
15 /// Search for a binary in `$PATH` or as a sibling to the current
16 /// executable (typically the test binary).
find_binary(name: &str) -> Result<std::path::PathBuf, String>17 pub fn find_binary(name: &str) -> Result<std::path::PathBuf, String> {
18     let mut current_exe = std::env::current_exe().unwrap();
19     current_exe.pop();
20     let paths = std::env::var_os("PATH").unwrap();
21     for mut path in std::iter::once(current_exe.clone()).chain(std::env::split_paths(&paths)) {
22         path.push(name);
23         if path.exists() {
24             return Ok(path);
25         }
26     }
27 
28     Err(format!(
29         "could not find '{}' in the directory of the binary ({}) or in $PATH ({})",
30         name,
31         current_exe.to_string_lossy(),
32         paths.to_string_lossy(),
33     ))
34 }
35 
36 /// Run `input` through `rustfmt`.
37 ///
38 /// # Panics
39 ///
40 /// Panics if `rustfmt` cannot be found in the same directory as the
41 /// test executable or if it returns a non-zero exit code.
rustfmt(input: &str) -> String42 pub fn rustfmt(input: &str) -> String {
43     let rustfmt_path = find_binary("rustfmt").expect("cannot find rustfmt");
44     let mut rustfmt = Command::new(&rustfmt_path)
45         .stdin(Stdio::piped())
46         .stdout(Stdio::piped())
47         .spawn()
48         .unwrap_or_else(|_| panic!("failed to start {:?}", &rustfmt_path));
49 
50     let mut stdin = rustfmt.stdin.take().unwrap();
51     // Owned copy which we can move into the writing thread.
52     let input = String::from(input);
53     std::thread::spawn(move || {
54         stdin.write_all(input.as_bytes()).expect("could not write to stdin");
55     });
56 
57     let output = rustfmt.wait_with_output().expect("error executing rustfmt");
58     assert!(output.status.success(), "rustfmt failed: {}", output.status);
59     String::from_utf8(output.stdout).expect("rustfmt output was not UTF-8")
60 }
61 
62 /// Find the unified diff between two strings using `diff`.
63 ///
64 /// # Panics
65 ///
66 /// Panics if `diff` cannot be found on `$PATH` or if it returns an
67 /// error.
diff(left_label: &str, left: &str, right_label: &str, right: &str) -> String68 pub fn diff(left_label: &str, left: &str, right_label: &str, right: &str) -> String {
69     let mut temp_left = NamedTempFile::new().unwrap();
70     temp_left.write_all(left.as_bytes()).unwrap();
71     let mut temp_right = NamedTempFile::new().unwrap();
72     temp_right.write_all(right.as_bytes()).unwrap();
73 
74     // We expect `diff` to be available on PATH.
75     let output = Command::new("diff")
76         .arg("--unified")
77         .arg("--color=always")
78         .arg("--label")
79         .arg(left_label)
80         .arg("--label")
81         .arg(right_label)
82         .arg(temp_left.path())
83         .arg(temp_right.path())
84         .output()
85         .expect("failed to run diff");
86     let diff_trouble_exit_code = 2; // from diff(1)
87     assert_ne!(
88         output.status.code().unwrap(),
89         diff_trouble_exit_code,
90         "diff failed: {}",
91         output.status
92     );
93     String::from_utf8(output.stdout).expect("diff output was not UTF-8")
94 }
95 
96 /// Compare two strings and output a diff if they are not equal.
97 #[track_caller]
assert_eq_with_diff(left_label: &str, left: &str, right_label: &str, right: &str)98 pub fn assert_eq_with_diff(left_label: &str, left: &str, right_label: &str, right: &str) {
99     assert!(
100         left == right,
101         "texts did not match, diff:\n{}\n",
102         diff(left_label, left, right_label, right)
103     );
104 }
105 
106 // Assert that an expression equals the given expression.
107 //
108 // Both expressions are wrapped in a `main` function (so we can format
109 // it with `rustfmt`) and a diff is be shown if they differ.
110 #[track_caller]
assert_expr_eq(left: proc_macro2::TokenStream, right: proc_macro2::TokenStream)111 pub fn assert_expr_eq(left: proc_macro2::TokenStream, right: proc_macro2::TokenStream) {
112     let left = quote! {
113         fn main() { #left }
114     };
115     let right = quote! {
116         fn main() { #right }
117     };
118     assert_eq_with_diff("left", &rustfmt(&left.to_string()), "right", &rustfmt(&right.to_string()));
119 }
120 
121 /// Check that `haystack` contains `needle`.
122 ///
123 /// Panic with a nice message if not.
124 #[track_caller]
assert_contains(haystack: &str, needle: &str)125 pub fn assert_contains(haystack: &str, needle: &str) {
126     assert!(haystack.contains(needle), "Could not find {:?} in {:?}", needle, haystack);
127 }
128 
129 /// Compare a string with a snapshot file.
130 ///
131 /// The `snapshot_path` is relative to the current working directory
132 /// of the test binary. This depends on how you execute the tests:
133 ///
134 /// * When using `atest`: The current working directory is a random
135 ///   temporary directory. You need to ensure that the snapshot file
136 ///   is installed into this directory. You do this by adding the
137 ///   snapshot to the `data` attribute of your test rule
138 ///
139 /// * When using Cargo: The current working directory is set to
140 ///   `CARGO_MANIFEST_DIR`, which is where the `Cargo.toml` file is
141 ///   found.
142 ///
143 /// If you run the test with Cargo and the `UPDATE_SNAPSHOTS`
144 /// environment variable is set, then the `actual_content` will be
145 /// written to `snapshot_path`. Otherwise the content is compared and
146 /// a panic is triggered if they differ.
147 #[track_caller]
assert_snapshot_eq<P: AsRef<Path>>(snapshot_path: P, actual_content: &str)148 pub fn assert_snapshot_eq<P: AsRef<Path>>(snapshot_path: P, actual_content: &str) {
149     let snapshot = snapshot_path.as_ref();
150     let snapshot_content = fs::read(snapshot).unwrap_or_else(|err| {
151         panic!("Could not read snapshot from {}: {}", snapshot.display(), err)
152     });
153     let snapshot_content = String::from_utf8(snapshot_content).expect("Snapshot was not UTF-8");
154 
155     // Normal comparison if UPDATE_SNAPSHOTS is unset.
156     if std::env::var("UPDATE_SNAPSHOTS").is_err() {
157         return assert_eq_with_diff(
158             snapshot.to_str().unwrap(),
159             &snapshot_content,
160             "actual",
161             actual_content,
162         );
163     }
164 
165     // Bail out if we are not using Cargo.
166     if std::env::var("CARGO_MANIFEST_DIR").is_err() {
167         panic!("Please unset UPDATE_SNAPSHOTS if you are not using Cargo");
168     }
169 
170     if actual_content != snapshot_content {
171         eprintln!(
172             "Updating snapshot {}: {} -> {} bytes",
173             snapshot.display(),
174             snapshot_content.len(),
175             actual_content.len()
176         );
177         fs::write(&snapshot_path, actual_content).unwrap_or_else(|err| {
178             panic!("Could not write snapshot to {}: {}", snapshot.display(), err)
179         });
180     }
181 }
182 
183 #[cfg(test)]
184 mod tests {
185     use super::*;
186 
187     #[test]
test_diff_labels_with_special_chars()188     fn test_diff_labels_with_special_chars() {
189         // Check that special characters in labels are passed
190         // correctly to diff.
191         let patch = diff("left 'file'", "foo\nbar\n", "right ~file!", "foo\nnew line\nbar\n");
192         assert_contains(&patch, "left 'file'");
193         assert_contains(&patch, "right ~file!");
194     }
195 
196     #[test]
197     #[should_panic]
test_assert_eq_with_diff_on_diff()198     fn test_assert_eq_with_diff_on_diff() {
199         // We use identical labels to check that we haven't
200         // accidentally mixed up the labels with the file content.
201         assert_eq_with_diff("", "foo\nbar\n", "", "foo\nnew line\nbar\n");
202     }
203 
204     #[test]
test_assert_eq_with_diff_on_eq()205     fn test_assert_eq_with_diff_on_eq() {
206         // No panic when there is no diff.
207         assert_eq_with_diff("left", "foo\nbar\n", "right", "foo\nbar\n");
208     }
209 }
210