1use clap::arg_enum;
2use colored::*;
3use diff;
4use failure::{bail, Error};
5use failure_derive::Fail;
6use itertools::*;
7use junit_report::{Duration, Report, TestCase, TestSuite};
8use serde::{Deserialize, Serialize};
9use serde_json as json;
10use std::path::PathBuf;
11use std::process::Command;
12use std::str::from_utf8;
13use std::time;
14use termize;
15
16const PASSED: &str = "TEST RUN PASSED";
17const FAILED: &str = "TEST RUN FAILED";
18const ARROW_DOWN: char = '▼';
19const ARROW_UP: char = '▲';
20
21#[derive(Debug, Fail)]
22enum NixTestError {
23 #[fail(display = "There was a problem running your nix tests.\n\n {}", msg)]
24 Running { msg: String },
25}
26
27pub fn run(test_file: PathBuf) -> Result<TestResult, Error> {
30 let run_test_nix = include_str!("./runTest.nix");
31 let out = Command::new("sh")
32 .arg("-c")
33 .arg(format!(
34 "nix-instantiate \
35 --json --eval --strict \
36 -E '{run_test_nix}' \
37 --arg testFile {test_file:#?}",
38 test_file = test_file.canonicalize()?,
39 run_test_nix = run_test_nix
40 ))
41 .output()
42 .map_err(|msg| NixTestError::Running {
43 msg: msg.to_string(),
44 })?;
45 if out.status.success() {
46 Ok(json::from_str(from_utf8(&out.stdout)?)?)
47 } else {
48 bail!(NixTestError::Running {
49 msg: from_utf8(&out.stderr)?.to_string()
50 })
51 }
52}
53
54arg_enum! {
55 #[derive(PartialEq, Debug)]
56 pub enum Reporter {
58 Human,
59 Json,
60 Junit
61 }
62}
63
64#[derive(Serialize, Deserialize, Debug)]
66#[serde(rename_all = "camelCase")]
67pub struct TestResult {
68 passed: Vec<PassedTest>,
69 failed: Vec<FailedTest>,
70}
71
72impl TestResult {
73 pub fn successful(&self) -> bool {
75 self.failed.is_empty()
76 }
77
78 pub fn format(&self, now: time::Duration, reporter: Reporter) -> Result<String, Error> {
80 match reporter {
81 Reporter::Json => Ok(self.json()),
82 Reporter::Human => self.human(now),
83 Reporter::Junit => self.junit(),
84 }
85 }
86
87 fn json(&self) -> String {
88 json::to_string(&self).unwrap()
89 }
90
91 fn human(&self, now: time::Duration) -> Result<String, Error> {
92 Ok(format!(
93 "
94 {failed_tests}
95 {status}
96
97 {durationLabel} {duration} ms
98 {passedLabel} {passed_count}
99 {failedLabel} {failed_count}
100
101",
102 status = self.status().underline(),
103 durationLabel = "Duration:".dimmed(),
104 passedLabel = "Passed:".dimmed(),
105 failedLabel = "Failed:".dimmed(),
106 duration = now.as_millis(),
107 passed_count = self.passed.len(),
108 failed_count = self.failed.len(),
109 failed_tests = self.failed_to_human()?
110 ))
111 }
112
113 fn failed_to_human(&self) -> Result<String, Error> {
114 let mut failed_tests = String::new();
115 for test in &self.failed {
116 failed_tests.push_str(&test.human()?);
117 failed_tests.push('\n');
118 }
119 Ok(failed_tests)
120 }
121
122 fn junit(&self) -> Result<String, Error> {
123 let mut report = Report::new();
124 let mut test_suite = TestSuite::new("nix tests"); test_suite.add_testcases(self.to_testcases());
126 report.add_testsuite(test_suite);
127 let mut out: Vec<u8> = Vec::new();
128 report.write_xml(&mut out)?;
129 Ok(from_utf8(&out)?.to_string())
130 }
131
132 fn to_testcases(&self) -> Vec<TestCase> {
133 let mut testcases = vec![];
134 for test in &self.passed {
135 testcases.push(test.junit());
136 }
137 for test in &self.failed {
138 testcases.push(test.junit());
139 }
140 testcases
141 }
142
143 fn status(&self) -> ColoredString {
144 if self.successful() {
145 PASSED.green()
146 } else {
147 FAILED.red()
148 }
149 }
150}
151
152#[test]
153fn status_passed_test() {
154 assert_eq!(
155 TestResult {
156 passed: vec![],
157 failed: vec![]
158 }
159 .status(),
160 PASSED.green()
161 )
162}
163
164#[test]
165fn status_failed_test() {
166 assert_eq!(
167 TestResult {
168 passed: vec![],
169 failed: vec![FailedTest {
170 expected: "".to_string(),
171 result: "".to_string(),
172 failed_test: "".to_string()
173 }]
174 }
175 .status(),
176 FAILED.red()
177 )
178}
179
180trait JunitTest {
181 fn junit(&self) -> TestCase;
182 fn format_result(&self) -> String;
183}
184
185#[derive(Serialize, Deserialize, Debug)]
186#[serde(rename_all = "camelCase")]
187struct FailedTest {
188 expected: String,
189 failed_test: String,
190 result: String,
191}
192
193impl FailedTest {
194 fn human(&self) -> Result<String, Error> {
195 let result_diff = render_diff(ARROW_UP, &self.result, &self.expected);
196 let expected_diff = render_diff(ARROW_DOWN, &self.expected, &self.result);
197 let indent = 8;
198 Ok(format!(
199 "
200 {name}
201
202 {result}
203 ╷
204 │ Expect.equal
205 ╵
206 {expected}
207 ",
208 name = ("✗ ".to_owned() + &self.failed_test).red(),
209 result = with_diff(&self.result, &result_diff, indent),
210 expected = with_diff(&expected_diff, &self.expected, indent)
211 )
212 .to_string())
213 }
214}
215
216fn render_diff(symbol: char, left: &str, right: &str) -> String {
217 let mut rendered = String::new();
218 for diff in diff::chars(left, right) {
219 match diff {
220 diff::Result::Left(_) => rendered.push(symbol),
221 diff::Result::Right(_) => (),
222 diff::Result::Both(_, _) => rendered.push(' '),
223 }
224 }
225 rendered
226}
227
228fn with_diff(first: &str, second: &str, indent: usize) -> String {
229 let width = termize::dimensions().map(|t| t.1).unwrap_or(80);
230 sub_strings(first, width - indent)
231 .into_iter()
232 .interleave(sub_strings(second, width - indent))
233 .join(&format!("\n{}", " ".repeat(indent)))
234}
235
236fn sub_strings(source: &str, sub_size: usize) -> Vec<String> {
237 source
238 .chars()
239 .chunks(sub_size)
240 .into_iter()
241 .map(Iterator::collect)
242 .collect::<Vec<_>>()
243}
244
245impl JunitTest for FailedTest {
246 fn junit(&self) -> TestCase {
247 TestCase::failure(
248 &self.failed_test,
249 Duration::zero(),
250 "Equals",
251 &self.format_result(),
252 )
253 }
254 fn format_result(&self) -> String {
255 format!(
256 "Actual: {result} Expected: {expected}",
257 result = self.result,
258 expected = self.expected
259 )
260 }
261}
262
263#[derive(Serialize, Deserialize, Debug)]
264#[serde(rename_all = "camelCase")]
265struct PassedTest {
266 passed_test: String,
267}
268
269impl JunitTest for PassedTest {
270 fn junit(&self) -> TestCase {
271 TestCase::success(&self.passed_test, Duration::zero())
272 }
273 fn format_result(&self) -> String {
274 String::new()
275 }
276}