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