dprint_development/
spec_helpers.rs

1use anyhow::Result;
2use console::Style;
3use file_test_runner::RunOptions;
4use file_test_runner::SubTestResult;
5use file_test_runner::TestResult;
6use file_test_runner::collection::CollectOptions;
7use similar::ChangeTag;
8use similar::TextDiff;
9use std::fmt::Display;
10use std::fs;
11use std::panic::AssertUnwindSafe;
12use std::panic::catch_unwind;
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 Display for DiffFailedMessage<'_> {
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  /// Set to true to overwrite the failing tests with the actual result.
55  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::default(),
78    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 { duration: None, output }
129          } else {
130            TestResult::Passed { duration: None }
131          },
132        });
133      }
134
135      TestResult::SubTests { duration: None, 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| -> Result<Option<String>, String> {
148      match catch_unwind(AssertUnwindSafe(|| format_text(&spec_file_path_buf, file_text, &spec.config))) {
149        Ok(Ok(formatted)) => Ok(formatted),
150        Ok(Err(err)) => Err(format!("Formatter error: {:#}", err)),
151        Err(panic_info) => {
152          let panic_msg = panic_info
153            .downcast_ref::<String>()
154            .map(|s| s.as_str())
155            .or_else(|| panic_info.downcast_ref::<&str>().copied())
156            .unwrap_or("unknown panic");
157          Err(format!("Formatter panicked: {}", panic_msg))
158        }
159      }
160    };
161
162    if spec.is_trace {
163      let trace_json = get_trace_json(&spec_file_path_buf, &spec.file_text, &spec.config);
164      handle_trace(spec, &trace_json);
165      None
166    } else {
167      let result = match format(&spec.file_text) {
168        Ok(formatted) => formatted.unwrap_or_else(|| spec.file_text.to_string()),
169        Err(err_msg) => {
170          return Some(FailedTestResult {
171            expected: spec.expected_text.clone(),
172            actual: format!("{}\n\nInput:\n{}", err_msg, spec.file_text),
173            actual_second: None,
174            message: spec.message.clone(),
175          });
176        }
177      };
178
179      if result != spec.expected_text {
180        if run_spec_options.fix_failures {
181          // very rough, but good enough
182          let file_text = fs::read_to_string(test_file_path).expect("Expected to read the file.");
183          let file_text = file_text.replace(&spec.expected_text, &result);
184          fs::write(test_file_path, file_text).expect("Expected to write to file.");
185          None
186        } else {
187          Some(FailedTestResult {
188            expected: spec.expected_text.clone(),
189            actual: result,
190            actual_second: None,
191            message: spec.message.clone(),
192          })
193        }
194      } else if run_spec_options.format_twice && !spec.skip_format_twice {
195        // ensure no changes when formatting twice
196        let twice_result = match format(&result) {
197          Ok(formatted) => formatted.unwrap_or_else(|| result.to_string()),
198          Err(err_msg) => {
199            return Some(FailedTestResult {
200              expected: spec.expected_text.clone(),
201              actual: result,
202              actual_second: Some(format!("ERROR on second format: {}", err_msg)),
203              message: spec.message.clone(),
204            });
205          }
206        };
207        if twice_result != spec.expected_text {
208          Some(FailedTestResult {
209            expected: spec.expected_text.clone(),
210            actual: result,
211            actual_second: Some(twice_result),
212            message: spec.message.clone(),
213          })
214        } else {
215          None
216        }
217      } else {
218        None
219      }
220    }
221  }
222
223  fn handle_trace(spec: &Spec, trace_json: &str) {
224    let app_js_text = include_str!("../trace_analyzer/app.js");
225    let app_css_text = include_str!("../trace_analyzer/app.css");
226    let html_file = r#"<!DOCTYPE html>
227<html lang="en">
228<head>
229    <meta charset="utf-8">
230    <meta name="viewport" content="width=device-width">
231    <title><!-- title --></title>
232    <script src="https://d3js.org/d3.v5.min.js"></script>
233    <script src="https://d3js.org/d3-quadtree.v1.min.js"></script>
234    <script src="https://d3js.org/d3-timer.v1.min.js"></script>
235    <script src="https://d3js.org/d3-force.v2.min.js"></script>
236    <script src="https://d3js.org/d3-color.v2.min.js"></script>
237    <script src="https://d3js.org/d3-dispatch.v2.min.js"></script>
238    <script src="https://d3js.org/d3-ease.v2.min.js"></script>
239    <script src="https://d3js.org/d3-interpolate.v2.min.js"></script>
240    <script src="https://d3js.org/d3-selection.v2.min.js"></script>
241    <script src="https://d3js.org/d3-timer.v2.min.js"></script>
242    <script src="https://d3js.org/d3-transition.v2.min.js"></script>
243    <script src="https://d3js.org/d3-drag.v2.min.js"></script>
244    <script src="https://d3js.org/d3-zoom.v2.min.js"></script>
245    <script type="text/javascript">
246    <!-- script -->
247    </script>
248    <style>
249    <!-- style -->
250    </style>
251</head>
252<body onload="onLoad()">
253</body>
254</html>"#;
255    let mut script = format!("const rawTraceResult = {};\n", trace_json);
256    script.push_str(&format!("const specMessage = \"{}\";\n", spec.message.replace('"', "\\\"")));
257    script.push_str(app_js_text);
258    let html_file = html_file
259      .replace("<!-- script -->", &script)
260      .replace("<!-- title -->", &format!("Trace - {}", spec.message))
261      .replace("<!-- style -->", app_css_text);
262    let temp_file_path = std::env::temp_dir().join("dprint-core-trace.html");
263    fs::write(&temp_file_path, html_file).unwrap();
264    let url = format!("file://{}", temp_file_path.to_string_lossy().replace('\\', "/"));
265    panic!("\n==============\nTrace output ready! Please open your browser to: {}\n==============\n", url);
266  }
267
268  #[cfg(not(debug_assertions))]
269  fn assert_spec_not_only_or_trace(spec: &Spec) {
270    if spec.is_trace {
271      panic!("Cannot run 'trace' spec in release mode: {}", spec.message);
272    }
273
274    if spec.is_only {
275      panic!("Cannot run 'only' spec in release mode: {}", spec.message);
276    }
277  }
278
279  #[cfg(not(debug_assertions))]
280  fn assert_not_fix_failures(run_spec_options: &RunSpecsOptions) {
281    if run_spec_options.fix_failures {
282      panic!("Cannot have 'fix_failures' as `true` in release mode.");
283    }
284  }
285}