1 /++
2   A module containing the single threaded runner
3 
4   Copyright: © 2017 Szabo Bogdan
5   License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6   Authors: Szabo Bogdan
7 +/
8 module trial.executor.single;
9 
10 public import trial.interfaces;
11 import trial.runner;
12 import std.datetime;
13 import trial.step;
14 import trial.stackresult;
15 
16 /**
17 The default test executor runs test in sequential order in a single thread
18 */
19 class DefaultExecutor : ITestExecutor, IStepLifecycleListener, IAttachmentListener
20 {
21   private
22   {
23     SuiteResult suiteResult;
24     TestResult testResult;
25     StepResult currentStep;
26     StepResult[] stepStack;
27   }
28 
29   this() {
30     suiteResult = SuiteResult("unknown");
31   }
32 
33   /// Called when an attachment is ready
34   void attach(ref const Attachment attachment) {
35     if(currentStep is null) {
36       suiteResult.attachments ~= Attachment(attachment.name, attachment.file, attachment.mime);
37       return;
38     }
39 
40     currentStep.attachments ~= Attachment(attachment.name, attachment.file, attachment.mime);
41   }
42 
43   /// Add the step result and update the other listeners on every step
44   void begin(string suite, string test, ref StepResult step)
45   {
46     currentStep.steps ~= step;
47     stepStack ~= currentStep;
48     currentStep = step;
49     LifeCycleListeners.instance.update();
50   }
51 
52   /// Update the other listeners on every step
53   void end(string suite, string test, ref StepResult step)
54   {
55     currentStep = stepStack[stepStack.length - 1];
56     stepStack = stepStack[0 .. $ - 1];
57     LifeCycleListeners.instance.update();
58   }
59 
60   /// It does nothing
61   SuiteResult[] beginExecution(ref const(TestCase)[])
62   {
63     return [];
64   }
65 
66   /// Return the result for the last executed suite
67   SuiteResult[] endExecution()
68   {
69     if (suiteResult.begin == SysTime.fromUnixTime(0))
70     {
71       return [];
72     }
73 
74     LifeCycleListeners.instance.update();
75     LifeCycleListeners.instance.end(suiteResult);
76     return [ suiteResult ];
77   }
78 
79   protected
80   {
81     /// Run a test case
82     void runTest(ref const(TestCase) testCase, TestResult testResult) {
83       try
84       {
85         testCase.func();
86         testResult.status = TestResult.Status.success;
87       }
88       catch (PendingTestException) 
89       {
90         testResult.status = TestResult.Status.pending;
91       }
92       catch (Throwable t)
93       {
94         testResult.status = TestResult.Status.failure;
95         testResult.throwable = t.toTestException;
96       }
97     }
98 
99     /// Convert a test case to a test result
100     void createTestResult(const(TestCase) testCase)
101     {
102       testResult = testCase.toTestResult;
103       testResult.begin = Clock.currTime;
104 
105       testResult.status = TestResult.Status.started;
106       currentStep = testResult;
107 
108       stepStack = [];
109 
110       Step.suite = testCase.suiteName;
111       Step.test = testCase.name;
112 
113       LifeCycleListeners.instance.begin(testCase.suiteName, testResult);
114 
115       runTest(testCase, testResult);
116 
117       testResult.end = Clock.currTime;
118 
119       LifeCycleListeners.instance.end(testCase.suiteName, testResult);
120     }
121   }
122 
123   /// Execute a test case
124   SuiteResult[] execute(ref const(TestCase) testCase)
125   {
126     SuiteResult[] result;
127     LifeCycleListeners.instance.update();
128 
129     if (suiteResult.name != testCase.suiteName)
130     {
131       if (suiteResult.begin != SysTime.fromUnixTime(0))
132       {
133         suiteResult.end = Clock.currTime;
134         LifeCycleListeners.instance.end(suiteResult);
135         result = [suiteResult];
136       }
137 
138       suiteResult = SuiteResult(testCase.suiteName, Clock.currTime, Clock.currTime);
139       LifeCycleListeners.instance.begin(suiteResult);
140     }
141 
142     createTestResult(testCase);
143     suiteResult.tests ~= testResult;
144     currentStep = null;
145     LifeCycleListeners.instance.update();
146 
147     return result;
148   }
149 }
150 
151 version(unittest) {
152   version(Have_fluent_asserts) {
153     import fluent.asserts;
154   }
155 }
156 
157 /// Executing a test case that throws a PendingTestException should mark the test result
158 /// as pending instead of a failure
159 unittest {
160   auto old = LifeCycleListeners.instance;
161   LifeCycleListeners.instance = new LifeCycleListeners;
162   LifeCycleListeners.instance.add(new DefaultExecutor);
163 
164   scope (exit) {
165     LifeCycleListeners.instance = old;
166   }
167 
168   void test() {
169     throw new PendingTestException();
170   }
171 
172   auto testCase = const TestCase("Some.Suite", "test name", &test, []);
173   auto result = [testCase].runTests;
174 
175   result.length.should.equal(1);
176   result[0].tests[0].status.should.equal(TestResult.Status.pending);
177 }
178 
179 
180 /// Executing a test case should set the right begin and end times
181 unittest {
182   import core.thread;
183   auto old = LifeCycleListeners.instance;
184   LifeCycleListeners.instance = new LifeCycleListeners;
185   LifeCycleListeners.instance.add(new DefaultExecutor);
186 
187   scope (exit) {
188     LifeCycleListeners.instance = old;
189   }
190 
191   void test() {
192     Thread.sleep(1.msecs);
193   }
194 
195   auto testCase = const TestCase("Some.Suite", "test name", &test, []);
196 
197   auto begin = Clock.currTime;
198   auto result = [ testCase ].runTests;
199   auto testResult = result[0].tests[0];
200 
201   testResult.begin.should.be.greaterThan(begin);
202   testResult.end.should.be.greaterThan(begin + 1.msecs);
203 }