1use anyhow::Result;
2use console::Style;
3use file_test_runner::collection::CollectOptions;
4use file_test_runner::RunOptions;
5use file_test_runner::SubTestResult;
6use file_test_runner::TestResult;
7use similar::ChangeTag;
8use similar::TextDiff;
9use std::fmt::Display;
10use std::fs;
11use std::panic::catch_unwind;
12use std::panic::AssertUnwindSafe;
13use std::path::Path;
14use std::path::PathBuf;
15use std::sync::Arc;
16
17use super::*;
18
19struct FailedTestResult {
20 expected: String,
21 actual: String,
22 actual_second: Option<String>,
23 message: String,
24}
25
26struct DiffFailedMessage<'a> {
27 expected: &'a str,
28 actual: &'a str,
29}
30
31impl<'a> Display for DiffFailedMessage<'a> {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 let diff = TextDiff::from_lines(self.expected, self.actual);
34
35 for op in diff.ops() {
36 for change in diff.iter_changes(op) {
37 let (sign, style) = match change.tag() {
38 ChangeTag::Delete => ("-", Style::new().green()),
39 ChangeTag::Insert => ("+", Style::new().red()),
40 ChangeTag::Equal => (" ", Style::new()),
41 };
42 write!(f, "{}{}", style.apply_to(sign).bold(), style.apply_to(change),)?;
43 }
44 }
45 Ok(())
46 }
47}
48
49type FormatTextFunc = dyn (Fn(&Path, &str, &SpecConfigMap) -> Result<Option<String>>) + Send + Sync;
50type GetTraceJsonFunc = dyn (Fn(&Path, &str, &SpecConfigMap) -> String) + Send + Sync;
51
52#[derive(Debug, Clone)]
53pub struct RunSpecsOptions {
54 pub fix_failures: bool,
56 pub format_twice: bool,
57}
58
59pub fn run_specs(
60 directory_path: &Path,
61 parse_spec_options: &ParseSpecOptions,
62 run_spec_options: &RunSpecsOptions,
63 format_text: Arc<FormatTextFunc>,
64 get_trace_json: Arc<GetTraceJsonFunc>,
65) {
66 #[cfg(not(debug_assertions))]
67 assert_not_fix_failures(run_spec_options);
68
69 let parse_spec_options = parse_spec_options.clone();
70 let run_spec_options = run_spec_options.clone();
71 file_test_runner::collect_and_run_tests(
72 CollectOptions {
73 base: directory_path.to_path_buf(),
74 filter_override: None,
75 strategy: Box::new(file_test_runner::collection::strategies::TestPerFileCollectionStrategy { file_pattern: None }),
76 },
77 RunOptions { parallel: true },
78 Arc::new(move |test| {
79 let file_text = test.read_to_string().unwrap();
80 let specs = parse_specs(file_text, &parse_spec_options);
81 let specs = if specs.iter().any(|s| s.is_only) {
82 specs.into_iter().filter(|s| s.is_only).collect()
83 } else {
84 specs
85 };
86 let mut sub_tests = Vec::new();
87 for spec in specs {
88 #[cfg(not(debug_assertions))]
89 assert_spec_not_only_or_trace(&spec);
90
91 if spec.skip {
92 sub_tests.push(SubTestResult {
93 name: spec.message.clone(),
94 result: TestResult::Ignored,
95 });
96 continue;
97 }
98
99 let test_file_path = &test.path;
100 let maybe_failed_result = run_spec(&spec, test_file_path, &run_spec_options, &format_text, &get_trace_json);
101
102 sub_tests.push(SubTestResult {
103 name: spec.message.clone(),
104 result: if let Some(failed_test) = maybe_failed_result {
105 let mut output = Vec::<u8>::new();
106 let mut failed_message = format!(
107 "Failed: {} ({})\nExpected: `{:?}`,\nActual: `{:?}`,`,\nDiff:\n{}",
108 failed_test.message,
109 test_file_path.display(),
110 failed_test.expected,
111 failed_test.actual,
112 DiffFailedMessage {
113 actual: &failed_test.actual,
114 expected: &failed_test.expected
115 }
116 );
117 if let Some(actual_second) = &failed_test.actual_second {
118 failed_message.push_str(&format!(
119 "\nTwice: `{:?}`,\nTwice diff:\n{}",
120 actual_second,
121 DiffFailedMessage {
122 actual: actual_second,
123 expected: &failed_test.actual,
124 }
125 ));
126 }
127 output.extend(failed_message.as_bytes());
128 TestResult::Failed { output }
129 } else {
130 TestResult::Passed
131 },
132 });
133 }
134
135 TestResult::SubTests(sub_tests)
136 }),
137 );
138
139 fn run_spec(
140 spec: &Spec,
141 test_file_path: &Path,
142 run_spec_options: &RunSpecsOptions,
143 format_text: &Arc<FormatTextFunc>,
144 get_trace_json: &Arc<GetTraceJsonFunc>,
145 ) -> Option<FailedTestResult> {
146 let spec_file_path_buf = PathBuf::from(&spec.file_name);
147 let format = |file_text: &str| {
148 let result = catch_unwind(AssertUnwindSafe(|| format_text(&spec_file_path_buf, file_text, &spec.config)));
149 if result.is_err() {
150 eprintln!("Panic in spec '{}' in {}\n", spec.message, test_file_path.display());
151 }
152 let result = result.unwrap();
153 result.unwrap_or_else(|err| panic!("Could not parse spec '{}' in {}\nMessage: {:#}", spec.message, test_file_path.display(), err,))
154 };
155
156 if spec.is_trace {
157 let trace_json = get_trace_json(&spec_file_path_buf, &spec.file_text, &spec.config);
158 handle_trace(spec, &trace_json);
159 None
160 } else {
161 let result = format(&spec.file_text).unwrap_or_else(|| spec.file_text.to_string());
162 if result != spec.expected_text {
163 if run_spec_options.fix_failures {
164 let file_text = fs::read_to_string(test_file_path).expect("Expected to read the file.");
166 let file_text = file_text.replace(&spec.expected_text, &result);
167 fs::write(test_file_path, file_text).expect("Expected to write to file.");
168 None
169 } else {
170 Some(FailedTestResult {
171 expected: spec.expected_text.clone(),
172 actual: result,
173 actual_second: None,
174 message: spec.message.clone(),
175 })
176 }
177 } else if run_spec_options.format_twice && !spec.skip_format_twice {
178 let twice_result = format(&result).unwrap_or_else(|| result.to_string());
180 if twice_result != spec.expected_text {
181 Some(FailedTestResult {
182 expected: spec.expected_text.clone(),
183 actual: result,
184 actual_second: Some(twice_result),
185 message: spec.message.clone(),
186 })
187 } else {
188 None
189 }
190 } else {
191 None
192 }
193 }
194 }
195
196 fn handle_trace(spec: &Spec, trace_json: &str) {
197 let app_js_text = include_str!("../trace_analyzer/app.js");
198 let app_css_text = include_str!("../trace_analyzer/app.css");
199 let html_file = r#"<!DOCTYPE html>
200<html lang="en">
201<head>
202 <meta charset="utf-8">
203 <meta name="viewport" content="width=device-width">
204 <title><!-- title --></title>
205 <script src="https://d3js.org/d3.v5.min.js"></script>
206 <script src="https://d3js.org/d3-quadtree.v1.min.js"></script>
207 <script src="https://d3js.org/d3-timer.v1.min.js"></script>
208 <script src="https://d3js.org/d3-force.v2.min.js"></script>
209 <script src="https://d3js.org/d3-color.v2.min.js"></script>
210 <script src="https://d3js.org/d3-dispatch.v2.min.js"></script>
211 <script src="https://d3js.org/d3-ease.v2.min.js"></script>
212 <script src="https://d3js.org/d3-interpolate.v2.min.js"></script>
213 <script src="https://d3js.org/d3-selection.v2.min.js"></script>
214 <script src="https://d3js.org/d3-timer.v2.min.js"></script>
215 <script src="https://d3js.org/d3-transition.v2.min.js"></script>
216 <script src="https://d3js.org/d3-drag.v2.min.js"></script>
217 <script src="https://d3js.org/d3-zoom.v2.min.js"></script>
218 <script type="text/javascript">
219 <!-- script -->
220 </script>
221 <style>
222 <!-- style -->
223 </style>
224</head>
225<body onload="onLoad()">
226</body>
227</html>"#;
228 let mut script = format!("const rawTraceResult = {};\n", trace_json);
229 script.push_str(&format!("const specMessage = \"{}\";\n", spec.message.replace('"', "\\\"")));
230 script.push_str(app_js_text);
231 let html_file = html_file
232 .replace("<!-- script -->", &script)
233 .replace("<!-- title -->", &format!("Trace - {}", spec.message))
234 .replace("<!-- style -->", app_css_text);
235 let temp_file_path = std::env::temp_dir().join("dprint-core-trace.html");
236 fs::write(&temp_file_path, html_file).unwrap();
237 let url = format!("file://{}", temp_file_path.to_string_lossy().replace('\\', "/"));
238 panic!("\n==============\nTrace output ready! Please open your browser to: {}\n==============\n", url);
239 }
240
241 #[cfg(not(debug_assertions))]
242 fn assert_spec_not_only_or_trace(spec: &Spec) {
243 if spec.is_trace {
244 panic!("Cannot run 'trace' spec in release mode: {}", spec.message);
245 }
246
247 if spec.is_only {
248 panic!("Cannot run 'only' spec in release mode: {}", spec.message);
249 }
250 }
251
252 #[cfg(not(debug_assertions))]
253 fn assert_not_fix_failures(run_spec_options: &RunSpecsOptions) {
254 if run_spec_options.fix_failures {
255 panic!("Cannot have 'fix_failures' as `true` in release mode.");
256 }
257 }
258}