1 module zug.tap;
2 
3 import std.stdio;
4 import std.conv;
5 import std.string;
6 import std.algorithm;
7 import std.process;
8 
9 enum TapDataType {
10     test_result,
11     diagnostic,
12     note
13 };
14 
15 /// needed to keep the diagnostics, the notes, the results etc. in order
16 struct TapData {
17     TapDataType data_type;
18     bool success;
19     string message;
20 }
21 
22 /**
23 struct Tap keeps the test data and gives access to the test methods
24 */
25 struct Tap {
26     private string test_name = "";
27 
28     private bool have_plan = false;
29     private int tests_planned = 0;
30     private TapData[] tests_data;
31     private int tests_count = 0;
32     private int tests_passed = 0;
33     private int tests_failed = 0;
34     private string cache = ""; // cache debug output here ... probably
35     private uint indentation = 0;
36 
37     private ProcessPipes consumer;
38     private bool use_consumer = false;
39 
40     private bool debug_enabled = false;
41     private bool print_messages = true;
42 
43     // skip tests and add them to tests_skipped until true
44     private bool skipping = false;
45     private int tests_skipped;
46     private bool testing_done = false;
47 
48     this(string test_name) {
49         this.test_name = test_name;
50     }
51 
52     void set_consumer(ProcessPipes consumer) {
53         this.consumer = consumer;
54         this.use_consumer = true;
55     }
56 
57 
58     void enable_debugging() { 
59         this.debug_enabled = true;
60     }
61 
62     void disable_debugging() {
63         this.debug_enabled = false;
64     }
65 
66     void enable_consumer() {
67         this.use_consumer = true;
68     }
69 
70     void disable_consumer() {
71         this.use_consumer = false;
72     }
73 
74     void verbose(bool verbose) {
75         this.print_messages = verbose;
76     }
77 
78     bool verbose() {
79         return this.print_messages;
80     }
81 
82     void write(string[] message...) {
83         import std.array : replicate;
84 
85         string[] result;
86         // dfmt off
87         auto indent_string = " ".replicate(this.indentation);
88         // dftm on
89         auto final_message = indent_string ~ message.join(" ");
90         if (this.verbose()) {
91             writeln(final_message);
92         }
93 
94         if (this.use_consumer) {
95             this.consumer.stdin.writeln(final_message);
96         }
97     }
98 
99     void warn(string[] message...) {
100         if (this.debug_enabled) {
101             stderr.writeln(message.join(" "));
102         }
103     }
104 
105     /**
106         set the number of planned tests
107     */
108     void plan(int plan) {
109         this.tests_planned = plan;
110         this.have_plan = true;
111         this.write("1.." ~ to!string(this.tests_planned));
112     }
113 
114     /**
115         get the number of planned tests
116     */
117     int plan() {
118         return this.tests_planned ? this.tests_planned : 0;
119     }
120 
121 
122     /**
123         get the data for the tests ran
124     */
125     TapData[] results() {
126         return this.tests_data;
127     }
128 
129 
130     void add_result(bool success, string message) {
131         this.tests_count++;
132         this.write((success ? "ok" : "not ok"), to!string(this.tests_count), message);
133         this.tests_data ~= TapData(TapDataType.test_result, success, message);
134     }
135 
136     /**
137         Finish testing, do the accounting, print the number of tests ran. Does not take any argument.
138         After this you can run `report()`.
139 
140         Returns `true` if all tests failed, else returns `false`
141     */
142     bool done_testing() {
143 
144         if (this.use_consumer) {
145             this.consumer.stdin.flush();
146             this.consumer.stdin.close();
147             wait(this.consumer.pid);
148             this.disable_consumer();
149         }
150 
151         string[string] summary;
152 
153         foreach (TapData result; this.tests_data) {
154             // not all results are tests results, some are notes or debug or something else
155             if (result.data_type != TapDataType.test_result) {
156                 continue;
157             }
158 
159             if (result.success) {
160                 this.tests_passed++;
161             } else {
162                 this.tests_failed++;
163             }
164         }
165 
166         if (this.have_plan) {
167             if ( this.tests_failed == 0 && this.tests_count == this.tests_planned ) {
168                 this.write("1.." ~ to!string(this.tests_count));
169                 this.testing_done = true;
170                 return true;
171             }
172         } else {
173             this.write("1.." ~ to!string(this.tests_count));
174             if (this.tests_failed == 0) {
175                 this.testing_done = true;
176                 return true;
177             }
178         }
179         return false;
180     }
181 
182     /**
183     prints the detailed info about the test results.
184 
185     */
186     void report() {
187         // dfmt off
188         this.write(
189                 "Test: " ~ this.test_name ~ " = ",
190                 to!string(this.tests_passed), "tests passed;",
191                 to!string(this.tests_failed), "tests failed");
192 
193         this.write(
194                 "Planned:", to!string(this.tests_planned),
195                 "; completed:", to!string(this.tests_count),
196                 "; skipped:", to!string(this.tests_skipped),
197                 "\n\n");
198         // dfmt on
199         if (this.testing_done != true) {
200             this.write("No plan and done_testing not called, something went wrong ... "); 
201         }
202     }
203 
204     /**
205         Print a diagnostic message and add it to the test data.
206 
207         It will be printed regardless of what `verbose` is set to.
208     */
209     void diag(string message) {
210         auto lines = splitLines(message).map!(a => "  #" ~ stripRight(a)).join("\n");
211         auto old_verbose = this.verbose;
212         // diagnostics always get printed
213         this.verbose(true);
214         this.write("  #DIAGNOSTIC: ", "\n", lines);
215         this.verbose(old_verbose);
216         this.tests_data ~= TapData(TapDataType.diagnostic, true, message);
217     }
218 
219     /**
220         Print a note if `verbose` is set to `true`.
221     */
222     void note(string message) {
223         this.write("#NOTE: ", message);
224         this.tests_data ~= TapData(TapDataType.note, true, message);
225     }
226 
227     /**
228         prints "ok" or "not ok" depending if the test succeeds or fails
229 
230         Params:
231             test = delegate, should return a boolean
232             message = string, optional
233     */
234     bool ok(bool delegate() test, string message = "") {
235         if (this.skipping) {
236             this.tests_skipped += 1;
237             return true;
238         }
239 
240         bool result;
241         try {
242             result = test();
243         } catch (Exception e) {
244             result = false;
245             this.diag(e.msg);
246         }
247         this.add_result(result, message);
248         return result;
249     }
250 
251     /**
252         prints "ok" or "not ok" depending if the test succeeds or fails
253 
254         Params:
255             test = boolean
256             message = string, optional
257     */
258     bool ok(bool is_true, string message = "") {
259         if (this.skipping) {
260             this.tests_skipped += 1;
261             return true;
262         }
263 
264         this.add_result(is_true, message);
265         return is_true;
266     }
267 
268     /**
269         write a debugging message to STDERR if `debug_enabled` is `true`
270     */
271     void do_debug(string message) {
272         if (this.debug_enabled) {
273             stderr.writeln("DEBUG: " ~ message);
274         }
275     }
276 
277     /**
278         Sets `skipping` to `true` which will cause tests to be skipped; until you run `resume` no tests will be executed
279     */
280     void skip(string message) {
281         this.skipping = true;
282         this.write("# skipping tests: ", message);
283     }
284 
285     /**
286         Sets `skipping` to `false`: as long as it is `false` the result of the tests will be recorded or the test callbacks will be executed;
287     */
288     void resume(string message) {
289         this.skipping = false;
290         this.write("# resuming tests: ", message);
291     }
292 
293     // alias SubtestCoderef = bool delegate();
294     // bool subtest(string label, SubtestCoderef subtest_callback)
295     // {
296     //     this.write("# subtest: ", label);
297     //     sub_tap.indentation = tap.indentation + 2;
298     //     auto subtest_result = subtest_callback();
299     //     this.ok(subtest_result, label);
300     //     return subtest_result;
301     // }
302 
303      /**
304         prints "ok" or "not ok" depending if the delegate passed in throws an exception or not
305 
306         Params:
307             test = delegate, should return a boolean
308             message = string, optional
309     */
310   
311 }
312 
313 bool it_throws(T)(void delegate() test ) {
314     bool it_throws = false;
315 
316     /*
317         imbricating try/catch to avoid
318         `Error: catch at ... hides catch at ...`
319     */
320     try {
321         try {
322             test();
323         } catch (T e) {
324             it_throws = true;
325         }
326     } catch (Exception e) { // catch everything
327         // maybe we passed Exception
328         static if (is(T == Exception)) {
329             it_throws = true;
330         }
331     }
332     return it_throws;
333 }
334 
335 unittest {
336     {
337         auto tap = Tap("first unittest block");
338         tap.verbose(true);
339         assert(tap.verbose() == true);
340         tap.plan(10);
341         assert(tap.plan() == 10);
342         assert(!tap.ok(false, "should fail"));
343         assert(tap.ok(true, "should pass"));
344         assert(!tap.ok(delegate bool() {
345                 return false;
346             }, "should fail"));
347         assert(tap.ok(delegate bool() {
348                 return true;
349             }, "should pass"));
350         assert(tap.results().length == 4);
351         assert(!tap.done_testing());
352         tap.report();
353     }
354 
355     { // test skipping
356         auto tap = Tap("second unittest block");
357         tap.verbose(true);
358         assert(tap.verbose() == true);
359         assert(tap.ok(true, "should pass"));
360         assert(tap.ok(true, "should pass"));
361         tap.skip("skipping two out of six tests");
362         assert(tap.ok(true, "should pass"));
363         assert(tap.ok(true, "should pass"));
364         tap.resume("skipped two tests out of six, resuming");
365         assert(tap.ok(true, "should pass"));
366         assert(tap.ok(true, "should pass"));
367         assert(tap.results().length == 4);
368         assert(tap.tests_skipped == 2);
369         assert(tap.done_testing());
370         tap.report();
371     }
372 
373     { // test consumer tappy
374         import std.file;
375         import std.process;
376 
377         string path_to_tappy = "/usr/bin/tappy";
378         auto tap = Tap("test with pipe to tappy");
379         tap.verbose(false);
380 
381         if (path_to_tappy.exists && path_to_tappy.isFile) {
382             auto pipe = pipeProcess(path_to_tappy, Redirect.stdin);
383             tap.set_consumer(pipe);
384         } else {
385             tap.verbose(true);
386             tap.skip("tappy not found, skipping the consumer pipe tests");
387         }
388 
389         tap.plan(6);
390         tap.ok(true, "should pass 1");
391         tap.ok(true, "should pass 2");
392         tap.ok(true, "should pass 3");
393         tap.ok(true, "should pass 4");
394         tap.ok(true, "should pass 5");
395         tap.ok(false, "should fail 6");
396         tap.done_testing();
397         assert(tap.tests_passed == 5, "five tests passed");
398         assert(tap.tests_failed == 1, "one test failed");
399         assert(tap.results().length == 6, "six tests ran");
400 
401         // not calling report(), let tappy do the reporting
402         // tap.verbose(true);
403         // tap.report();
404     }
405 
406     // exercise note() and diag() ... TODO how do I test this ? need to capture STDOUT somehow
407     {
408         auto tap = Tap("exercise note() and diag()");
409         tap.verbose(false);
410         tap.plan(1);
411         tap.note("ERROR: this is a note, should not see it now because verbose is false");
412         tap.diag("this is a diagnostic, should see it no matter what verbose is set to");
413         tap.verbose(true);
414         tap.note("this is another note, should see it");
415         tap.ok(true);
416         tap.done_testing();
417     }
418 }