1 /++
2   A module containing the SpecReporter
3 
4   This is an example of how this reporter looks
5   <script type="text/javascript" src="https://asciinema.org/a/9z1tolgn7x55v41i3mm3wlkum.js" id="asciicast-9z1tolgn7x55v41i3mm3wlkum" async></script>
6 
7   Copyright: © 2017 Szabo Bogdan
8   License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
9   Authors: Szabo Bogdan
10 +/
11 module trial.reporters.spec;
12 
13 import std.stdio;
14 import std.array;
15 import std.conv;
16 import std.datetime;
17 import std..string;
18 import std.algorithm;
19 
20 import trial.interfaces;
21 import trial.settings;
22 import trial.reporters.writer;
23 
24 /// A structure containing the glyphs used for the spec reporter
25 struct SpecGlyphs {
26   version(Windows) {
27     ///
28     string ok = "+";
29   } else {
30     ///
31     string ok = "✓";
32   }
33 
34   string pending = "-";
35 }
36 
37 ///
38 string specGlyphsToCode(SpecGlyphs glyphs) {
39   return "SpecGlyphs(`" ~ glyphs.ok ~ "`)";
40 }
41 
42 /// This is the default reporter. The "spec" reporter outputs a hierarchical view nested just as the test cases are.
43 class SpecReporter : ITestCaseLifecycleListener
44 {
45   enum Type
46   {
47     none,
48     success,
49     step,
50     pending,
51     failure,
52     testBegin,
53     testEnd,
54     emptyLine,
55     danger,
56     warning
57   }
58 
59   protected
60   {
61     int failedTests = 0;
62     string lastSuiteName;
63 
64     ReportWriter writer;
65   }
66 
67   private
68   {
69     Settings settings;
70   }
71 
72   this()
73   {
74     writer = defaultWriter;
75   }
76 
77   this(Settings settings)
78   {
79     writer = defaultWriter;
80     this.settings = settings;
81   }
82 
83   this(ReportWriter writer)
84   {
85     this.writer = writer;
86   }
87 
88   private
89   {
90     string indentation(size_t cnt) pure
91     {
92       return "  ".replicate(cnt);
93     }
94   }
95 
96   void write(Type t)(string text = "", size_t spaces = 0)
97   {
98     writer.write(indentation(spaces));
99 
100     switch (t)
101     {
102     case Type.emptyLine:
103       writer.writeln("");
104       break;
105 
106     case Type.success:
107       writer.write(settings.glyphs.spec.ok, ReportWriter.Context.success);
108       writer.write(" " ~ text, ReportWriter.Context.inactive);
109       break;
110 
111     case Type.pending:
112       writer.write(settings.glyphs.spec.pending, ReportWriter.Context.info);
113       writer.write(" " ~ text, ReportWriter.Context.inactive);
114       break;
115 
116     case Type.failure:
117       writer.write(failedTests.to!string ~ ") " ~ text,
118           ReportWriter.Context.danger);
119       break;
120 
121     case Type.danger:
122       writer.write(text, ReportWriter.Context.danger);
123       break;
124 
125     case Type.warning:
126       writer.write(text, ReportWriter.Context.warning);
127       break;
128 
129     default:
130       writer.write(text);
131     }
132   }
133 
134   void begin(string suite, ref TestResult test)
135   {
136   }
137 
138   protected auto printSuite(string suite) {
139     size_t indents = 1;
140 
141     auto oldPieces = lastSuiteName.split(".");
142     auto pieces = suite.split(".");
143     lastSuiteName = suite;
144 
145     auto prefix = oldPieces.commonPrefix(pieces).array.length;
146 
147     write!(Type.emptyLine)();
148     indents += prefix;
149 
150     foreach (piece; pieces[prefix .. $])
151     {
152       write!(Type.none)(piece, indents);
153       write!(Type.emptyLine)();
154       indents++;
155     }
156 
157     return indents;
158   }
159 
160   void end(string suite, ref TestResult test)
161   {
162     size_t indents = 1;
163 
164     if (suite != lastSuiteName)
165     {
166       indents = printSuite(suite);
167     }
168     else
169     {
170       indents = suite.count('.') + 2;
171     }
172 
173     if (test.status == TestResult.Status.success)
174     {
175       write!(Type.success)(test.name, indents);
176     }
177 
178     if (test.status == TestResult.Status.pending)
179     {
180       write!(Type.pending)(test.name, indents);
181     }
182 
183     if (test.status == TestResult.Status.failure)
184     {
185       write!(Type.failure)(test.name, indents);
186       failedTests++;
187     }
188 
189     auto timeDiff = (test.end - test.begin).total!"msecs";
190 
191     if(timeDiff >= settings.warningTestDuration && timeDiff < settings.dangerTestDuration) {
192       write!(Type.warning)(" (" ~ timeDiff.to!string ~ "ms)", 0);
193     }
194 
195     if(timeDiff >= settings.dangerTestDuration) {
196       write!(Type.danger)(" (" ~ timeDiff.to!string ~ "ms)", 0);
197     }
198 
199     write!(Type.emptyLine);
200 
201     indents--;
202   }
203 }
204 
205 version (unittest)
206 {
207   version(Have_fluent_asserts) {
208     import fluent.asserts;
209   }
210 }
211 
212 @("it should print a success test")
213 unittest
214 {
215   auto writer = new BufferedWriter;
216   auto reporter = new SpecReporter(writer);
217 
218   auto suite = SuiteResult("some suite");
219   auto test = new TestResult("some test");
220   test.status = TestResult.Status.success;
221 
222   reporter.begin("some suite", test);
223   reporter.end("some suite", test);
224 
225   writer.buffer.should.equal("\n  some suite" ~ "\n    ✓ some test\n");
226 }
227 
228 @("it should print two success tests")
229 unittest
230 {
231   auto writer = new BufferedWriter;
232   auto reporter = new SpecReporter(writer);
233 
234   auto suite = SuiteResult("some suite");
235   auto test1 = new TestResult("some test");
236   test1.status = TestResult.Status.success;
237 
238   auto test2 = new TestResult("other test");
239   test2.status = TestResult.Status.success;
240 
241   reporter.begin("some suite", test1);
242   reporter.end("some suite", test1);
243 
244   reporter.begin("some suite", test2);
245   reporter.end("some suite", test2);
246 
247   writer.buffer.should.contain("\n  some suite\n");
248   writer.buffer.should.contain("\n    ✓ some test\n");
249   writer.buffer.should.contain("\n    ✓ other test\n");
250 }
251 
252 @("it should print a failing test")
253 unittest
254 {
255   auto writer = new BufferedWriter;
256   auto reporter = new SpecReporter(writer);
257 
258   auto suite = SuiteResult("some suite");
259   auto test = new TestResult("some test");
260 
261   test.status = TestResult.Status.failure;
262 
263   reporter.begin("some suite", test);
264   reporter.end("some suite", test);
265 
266   writer.buffer.should.equal("\n  some suite" ~ "\n    0) some test\n");
267 }
268 
269 @("it should print a pending test")
270 unittest
271 {
272   auto writer = new BufferedWriter;
273   auto reporter = new SpecReporter(writer);
274 
275   auto suite = SuiteResult("some suite");
276   auto test = new TestResult("some test");
277 
278   test.status = TestResult.Status.pending;
279 
280   reporter.begin("some suite", test);
281   reporter.end("some suite", test);
282 
283   writer.buffer.should.equal("\n  some suite" ~ "\n    - some test\n");
284 }
285 
286 @("it should split suites by dot")
287 unittest
288 {
289   auto writer = new BufferedWriter;
290   auto reporter = new SpecReporter(writer);
291 
292   auto suite = SuiteResult("some.suite");
293   auto test = new TestResult("some test");
294 
295   test.status = TestResult.Status.failure;
296   test.throwable = new Exception("Random failure");
297 
298   reporter.end("some.suite", test);
299   reporter.end("some.suite", test);
300 
301   writer.buffer.should.equal(
302       "\n" ~ "  some\n" ~ "    suite\n" ~ "      0) some test\n" ~ "      1) some test\n");
303 }
304 
305 @("it should omit the common suite names")
306 unittest
307 {
308   auto writer = new BufferedWriter;
309   auto reporter = new SpecReporter(writer);
310 
311   auto suite = SuiteResult("some.suite");
312   auto test = new TestResult("some test");
313 
314   test.status = TestResult.Status.failure;
315   test.throwable = new Exception("Random failure");
316 
317   reporter.end("some.suite", test);
318   reporter.end("some.other", test);
319 
320   writer.buffer.should.equal(
321       "\n" ~ "  some\n" ~ "    suite\n" ~ "      0) some test\n\n" ~ "    other\n"
322       ~ "      1) some test\n");
323 }
324 
325 /// it should print the test duration
326 unittest
327 {
328   auto writer = new BufferedWriter;
329   auto reporter = new SpecReporter(writer);
330 
331   auto suite = SuiteResult("some.suite");
332   auto test = new TestResult("some test");
333 
334   test.status = TestResult.Status.success;
335   test.end = Clock.currTime;
336   test.begin = test.end - 1.seconds;
337 
338   test.throwable = new Exception("Random failure");
339 
340   reporter.end("some.suite", test);
341 
342   writer.buffer.should.equal(
343       "\n" ~ "  some\n" ~ "    suite\n" ~ "      ✓ some test (1000ms)\n");
344 }