1 <?php
2 /*
3  * This file is part of PHPUnit.
4  *
5  * (c) Sebastian Bergmann <sebastian@phpunit.de>
6  *
7  * For the full copyright and license information, please view the LICENSE
8  * file that was distributed with this source code.
9  */
10 
11 /**
12  * A TestListener that generates a logfile of the test execution in XML markup.
13  *
14  * The XML markup used is the same as the one that is used by the JUnit Ant task.
15  */
16 class PHPUnit_Util_Log_JUnit extends PHPUnit_Util_Printer implements PHPUnit_Framework_TestListener
17 {
18     /**
19      * @var DOMDocument
20      */
21     protected $document;
22 
23     /**
24      * @var DOMElement
25      */
26     protected $root;
27 
28     /**
29      * @var bool
30      */
31     protected $logIncompleteSkipped = false;
32 
33     /**
34      * @var bool
35      */
36     protected $writeDocument = true;
37 
38     /**
39      * @var DOMElement[]
40      */
41     protected $testSuites = [];
42 
43     /**
44      * @var int[]
45      */
46     protected $testSuiteTests = [0];
47 
48     /**
49      * @var int[]
50      */
51     protected $testSuiteAssertions = [0];
52 
53     /**
54      * @var int[]
55      */
56     protected $testSuiteErrors = [0];
57 
58     /**
59      * @var int[]
60      */
61     protected $testSuiteFailures = [0];
62 
63     /**
64      * @var int[]
65      */
66     protected $testSuiteTimes = [0];
67 
68     /**
69      * @var int
70      */
71     protected $testSuiteLevel = 0;
72 
73     /**
74      * @var DOMElement
75      */
76     protected $currentTestCase = null;
77 
78     /**
79      * @var bool
80      */
81     protected $attachCurrentTestCase = true;
82 
83     /**
84      * Constructor.
85      *
86      * @param mixed $out
87      * @param bool  $logIncompleteSkipped
88      */
89     public function __construct($out = null, $logIncompleteSkipped = false)
90     {
91         $this->document               = new DOMDocument('1.0', 'UTF-8');
92         $this->document->formatOutput = true;
93 
94         $this->root = $this->document->createElement('testsuites');
95         $this->document->appendChild($this->root);
96 
97         parent::__construct($out);
98 
99         $this->logIncompleteSkipped = $logIncompleteSkipped;
100     }
101 
102     /**
103      * Flush buffer and close output.
104      */
105     public function flush()
106     {
107         if ($this->writeDocument === true) {
108             $this->write($this->getXML());
109         }
110 
111         parent::flush();
112     }
113 
114     /**
115      * An error occurred.
116      *
117      * @param PHPUnit_Framework_Test $test
118      * @param Exception              $e
119      * @param float                  $time
120      */
121     public function addError(PHPUnit_Framework_Test $test, Exception $e, $time)
122     {
123         $this->doAddFault($test, $e, $time, 'error');
124         $this->testSuiteErrors[$this->testSuiteLevel]++;
125     }
126 
127     /**
128      * A warning occurred.
129      *
130      * @param PHPUnit_Framework_Test    $test
131      * @param PHPUnit_Framework_Warning $e
132      * @param float                     $time
133      */
134     public function addWarning(PHPUnit_Framework_Test $test, PHPUnit_Framework_Warning $e, $time)
135     {
136         if (!$this->logIncompleteSkipped) {
137             return;
138         }
139 
140         $this->doAddFault($test, $e, $time, 'warning');
141         $this->testSuiteFailures[$this->testSuiteLevel]++;
142     }
143 
144     /**
145      * A failure occurred.
146      *
147      * @param PHPUnit_Framework_Test                 $test
148      * @param PHPUnit_Framework_AssertionFailedError $e
149      * @param float                                  $time
150      */
151     public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time)
152     {
153         $this->doAddFault($test, $e, $time, 'failure');
154         $this->testSuiteFailures[$this->testSuiteLevel]++;
155     }
156 
157     /**
158      * Incomplete test.
159      *
160      * @param PHPUnit_Framework_Test $test
161      * @param Exception              $e
162      * @param float                  $time
163      */
164     public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time)
165     {
166         if ($this->logIncompleteSkipped && $this->currentTestCase !== null) {
167             $error = $this->document->createElement(
168                 'error',
169                 PHPUnit_Util_XML::prepareString(
170                     "Incomplete Test\n" .
171                     PHPUnit_Util_Filter::getFilteredStacktrace($e)
172                 )
173             );
174 
175             $error->setAttribute('type', get_class($e));
176 
177             $this->currentTestCase->appendChild($error);
178 
179             $this->testSuiteErrors[$this->testSuiteLevel]++;
180         } else {
181             $this->attachCurrentTestCase = false;
182         }
183     }
184 
185     /**
186      * Risky test.
187      *
188      * @param PHPUnit_Framework_Test $test
189      * @param Exception              $e
190      * @param float                  $time
191      */
192     public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time)
193     {
194         if ($this->logIncompleteSkipped && $this->currentTestCase !== null) {
195             $error = $this->document->createElement(
196                 'error',
197                 PHPUnit_Util_XML::prepareString(
198                     "Risky Test\n" .
199                     PHPUnit_Util_Filter::getFilteredStacktrace($e)
200                 )
201             );
202 
203             $error->setAttribute('type', get_class($e));
204 
205             $this->currentTestCase->appendChild($error);
206 
207             $this->testSuiteErrors[$this->testSuiteLevel]++;
208         } else {
209             $this->attachCurrentTestCase = false;
210         }
211     }
212 
213     /**
214      * Skipped test.
215      *
216      * @param PHPUnit_Framework_Test $test
217      * @param Exception              $e
218      * @param float                  $time
219      */
220     public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time)
221     {
222         if ($this->logIncompleteSkipped && $this->currentTestCase !== null) {
223             $error = $this->document->createElement(
224                 'error',
225                 PHPUnit_Util_XML::prepareString(
226                     "Skipped Test\n" .
227                     PHPUnit_Util_Filter::getFilteredStacktrace($e)
228                 )
229             );
230 
231             $error->setAttribute('type', get_class($e));
232 
233             $this->currentTestCase->appendChild($error);
234 
235             $this->testSuiteErrors[$this->testSuiteLevel]++;
236         } else {
237             $this->attachCurrentTestCase = false;
238         }
239     }
240 
241     /**
242      * A testsuite started.
243      *
244      * @param PHPUnit_Framework_TestSuite $suite
245      */
246     public function startTestSuite(PHPUnit_Framework_TestSuite $suite)
247     {
248         $testSuite = $this->document->createElement('testsuite');
249         $testSuite->setAttribute('name', $suite->getName());
250 
251         if (class_exists($suite->getName(), false)) {
252             try {
253                 $class = new ReflectionClass($suite->getName());
254 
255                 $testSuite->setAttribute('file', $class->getFileName());
256             } catch (ReflectionException $e) {
257             }
258         }
259 
260         if ($this->testSuiteLevel > 0) {
261             $this->testSuites[$this->testSuiteLevel]->appendChild($testSuite);
262         } else {
263             $this->root->appendChild($testSuite);
264         }
265 
266         $this->testSuiteLevel++;
267         $this->testSuites[$this->testSuiteLevel]          = $testSuite;
268         $this->testSuiteTests[$this->testSuiteLevel]      = 0;
269         $this->testSuiteAssertions[$this->testSuiteLevel] = 0;
270         $this->testSuiteErrors[$this->testSuiteLevel]     = 0;
271         $this->testSuiteFailures[$this->testSuiteLevel]   = 0;
272         $this->testSuiteTimes[$this->testSuiteLevel]      = 0;
273     }
274 
275     /**
276      * A testsuite ended.
277      *
278      * @param PHPUnit_Framework_TestSuite $suite
279      */
280     public function endTestSuite(PHPUnit_Framework_TestSuite $suite)
281     {
282         $this->testSuites[$this->testSuiteLevel]->setAttribute(
283             'tests',
284             $this->testSuiteTests[$this->testSuiteLevel]
285         );
286 
287         $this->testSuites[$this->testSuiteLevel]->setAttribute(
288             'assertions',
289             $this->testSuiteAssertions[$this->testSuiteLevel]
290         );
291 
292         $this->testSuites[$this->testSuiteLevel]->setAttribute(
293             'failures',
294             $this->testSuiteFailures[$this->testSuiteLevel]
295         );
296 
297         $this->testSuites[$this->testSuiteLevel]->setAttribute(
298             'errors',
299             $this->testSuiteErrors[$this->testSuiteLevel]
300         );
301 
302         $this->testSuites[$this->testSuiteLevel]->setAttribute(
303             'time',
304             sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel])
305         );
306 
307         if ($this->testSuiteLevel > 1) {
308             $this->testSuiteTests[$this->testSuiteLevel - 1]      += $this->testSuiteTests[$this->testSuiteLevel];
309             $this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel];
310             $this->testSuiteErrors[$this->testSuiteLevel - 1]     += $this->testSuiteErrors[$this->testSuiteLevel];
311             $this->testSuiteFailures[$this->testSuiteLevel - 1]   += $this->testSuiteFailures[$this->testSuiteLevel];
312             $this->testSuiteTimes[$this->testSuiteLevel - 1]      += $this->testSuiteTimes[$this->testSuiteLevel];
313         }
314 
315         $this->testSuiteLevel--;
316     }
317 
318     /**
319      * A test started.
320      *
321      * @param PHPUnit_Framework_Test $test
322      */
323     public function startTest(PHPUnit_Framework_Test $test)
324     {
325         $testCase = $this->document->createElement('testcase');
326         $testCase->setAttribute('name', $test->getName());
327 
328         if ($test instanceof PHPUnit_Framework_TestCase) {
329             $class      = new ReflectionClass($test);
330             $methodName = $test->getName();
331 
332             if ($class->hasMethod($methodName)) {
333                 $method = $class->getMethod($test->getName());
334 
335                 $testCase->setAttribute('class', $class->getName());
336                 $testCase->setAttribute('file', $class->getFileName());
337                 $testCase->setAttribute('line', $method->getStartLine());
338             }
339         }
340 
341         $this->currentTestCase = $testCase;
342     }
343 
344     /**
345      * A test ended.
346      *
347      * @param PHPUnit_Framework_Test $test
348      * @param float                  $time
349      */
350     public function endTest(PHPUnit_Framework_Test $test, $time)
351     {
352         if ($this->attachCurrentTestCase) {
353             if ($test instanceof PHPUnit_Framework_TestCase) {
354                 $numAssertions = $test->getNumAssertions();
355                 $this->testSuiteAssertions[$this->testSuiteLevel] += $numAssertions;
356 
357                 $this->currentTestCase->setAttribute(
358                     'assertions',
359                     $numAssertions
360                 );
361             }
362 
363             $this->currentTestCase->setAttribute(
364                 'time',
365                 sprintf('%F', $time)
366             );
367 
368             $this->testSuites[$this->testSuiteLevel]->appendChild(
369                 $this->currentTestCase
370             );
371 
372             $this->testSuiteTests[$this->testSuiteLevel]++;
373             $this->testSuiteTimes[$this->testSuiteLevel] += $time;
374 
375             if (method_exists($test, 'hasOutput') && $test->hasOutput()) {
376                 $systemOut = $this->document->createElement('system-out');
377                 $systemOut->appendChild(
378                     $this->document->createTextNode($test->getActualOutput())
379                 );
380                 $this->currentTestCase->appendChild($systemOut);
381             }
382         }
383 
384         $this->attachCurrentTestCase = true;
385         $this->currentTestCase       = null;
386     }
387 
388     /**
389      * Returns the XML as a string.
390      *
391      * @return string
392      */
393     public function getXML()
394     {
395         return $this->document->saveXML();
396     }
397 
398     /**
399      * Enables or disables the writing of the document
400      * in flush().
401      *
402      * This is a "hack" needed for the integration of
403      * PHPUnit with Phing.
404      *
405      * @return string
406      */
407     public function setWriteDocument($flag)
408     {
409         if (is_bool($flag)) {
410             $this->writeDocument = $flag;
411         }
412     }
413 
414     /**
415      * Method which generalizes addError() and addFailure()
416      *
417      * @param PHPUnit_Framework_Test $test
418      * @param Exception              $e
419      * @param float                  $time
420      * @param string                 $type
421      */
422     private function doAddFault(PHPUnit_Framework_Test $test, Exception $e, $time, $type)
423     {
424         if ($this->currentTestCase === null) {
425             return;
426         }
427 
428         if ($test instanceof PHPUnit_Framework_SelfDescribing) {
429             $buffer = $test->toString() . PHP_EOL;
430         } else {
431             $buffer = '';
432         }
433 
434         $buffer .= PHPUnit_Framework_TestFailure::exceptionToString($e) . PHP_EOL .
435                    PHPUnit_Util_Filter::getFilteredStacktrace($e);
436 
437         $fault = $this->document->createElement(
438             $type,
439             PHPUnit_Util_XML::prepareString($buffer)
440         );
441 
442         if ($e instanceof PHPUnit_Framework_ExceptionWrapper) {
443             $fault->setAttribute('type', $e->getClassName());
444         } else {
445             $fault->setAttribute('type', get_class($e));
446         }
447 
448         $this->currentTestCase->appendChild($fault);
449     }
450 }
451