Passed
Push — main ( e5a85d...619edc )
by Michiel
07:04
created

PHPUnitReportTask   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 269
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 33
eloc 91
dl 0
loc 269
ccs 0
cts 91
cp 0
rs 9.76
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A fixDocument() 0 28 6
A transform() 0 39 5
A setStyleDir() 0 3 1
A getStyleSheet() 0 25 5
A setFormat() 0 3 1
A setInFile() 0 3 1
B handleChildren() 0 35 8
A setUseSortTable() 0 3 1
A main() 0 11 2
A init() 0 4 2
A setToDir() 0 3 1
1
<?php
2
3
/**
4
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
6
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
7
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
8
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
10
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
11
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
12
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
13
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
14
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
 *
16
 * This software consists of voluntary contributions made by many individuals
17
 * and is licensed under the LGPL. For more information please see
18
 * <http://phing.info>.
19
 */
20
21
namespace Phing\Task\Ext\PhpUnit;
22
23
use DOMDocument;
24
use Phing\Exception\BuildException;
25
use Phing\Io\ExtendedFileStream;
26
use Phing\Io\FileWriter;
27
use Phing\Io\IOException;
28
use Phing\Io\File;
29
use Phing\Phing;
30
use Phing\Task;
31
use Phing\Task\System\Condition\OsCondition;
32
use XSLTProcessor;
33
34
/**
35
 * Transform a PHPUnit xml report using XSLT.
36
 * This transformation generates an html report in either framed or non-framed
37
 * style. The non-framed style is convenient to have a concise report via mail,
38
 * the framed report is much more convenient if you want to browse into
39
 * different packages or testcases since it is a Javadoc like report.
40
 *
41
 * @author  Michiel Rook <[email protected]>
42
 * @package phing.tasks.ext.phpunit
43
 * @since   2.1.0
44
 */
45
class PHPUnitReportTask extends Task
46
{
47
    private $format = "noframes";
48
    private $styleDir = "";
49
50
    /**
51
     * @var File
52
     */
53
    private $toDir;
54
55
    /**
56
     * Whether to use the sorttable JavaScript library, defaults to false
57
     * See {@link http://www.kryogenix.org/code/browser/sorttable/)}
58
     *
59
     * @var boolean
60
     */
61
    private $useSortTable = false;
62
63
    /**
64
     * the directory where the results XML can be found
65
     */
66
    private $inFile = "testsuites.xml";
67
68
    /**
69
     * Set the filename of the XML results file to use.
70
     *
71
     * @param  File $inFile
72
     * @return void
73
     */
74
    public function setInFile(File $inFile)
75
    {
76
        $this->inFile = $inFile;
77
    }
78
79
    /**
80
     * Set the format of the generated report. Must be noframes or frames.
81
     *
82
     * @param  $format
83
     * @return void
84
     */
85
    public function setFormat($format)
86
    {
87
        $this->format = $format;
88
    }
89
90
    /**
91
     * Set the directory where the stylesheets are located.
92
     *
93
     * @param  $styleDir
94
     * @return void
95
     */
96
    public function setStyleDir($styleDir)
97
    {
98
        $this->styleDir = $styleDir;
99
    }
100
101
    /**
102
     * Set the directory where the files resulting from the
103
     * transformation should be written to.
104
     *
105
     * @param  File $toDir
106
     * @return void
107
     */
108
    public function setToDir(File $toDir)
109
    {
110
        $this->toDir = $toDir;
111
    }
112
113
    /**
114
     * Sets whether to use the sorttable JavaScript library, defaults to false
115
     * See {@link http://www.kryogenix.org/code/browser/sorttable/)}
116
     *
117
     * @param  boolean $useSortTable
118
     * @return void
119
     */
120
    public function setUseSortTable($useSortTable)
121
    {
122
        $this->useSortTable = (bool) $useSortTable;
123
    }
124
125
    /**
126
     * Returns the path to the XSL stylesheet
127
     *
128
     * @return File
129
     * @throws IOException
130
     */
131
    protected function getStyleSheet()
132
    {
133
        $xslname = "phpunit-" . $this->format . ".xsl";
134
135
        if ($this->styleDir) {
136
            $file = new File($this->styleDir, $xslname);
137
        } else {
138
            $path = Phing::getResourcePath("phing/etc/$xslname");
139
140
            if ($path === null) {
141
                $path = Phing::getResourcePath("etc/$xslname");
142
143
                if ($path === null) {
144
                    throw new BuildException("Could not find $xslname in resource path");
145
                }
146
            }
147
148
            $file = new File($path);
149
        }
150
151
        if (!$file->exists()) {
152
            throw new BuildException("Could not find file " . $file->getPath());
153
        }
154
155
        return $file;
156
    }
157
158
    /**
159
     * Transforms the DOM document
160
     *
161
     * @param DOMDocument $document
162
     * @throws BuildException
163
     * @throws IOException
164
     */
165
    protected function transform(\DOMDocument $document)
166
    {
167
        if (!$this->toDir->exists()) {
168
            throw new BuildException("Directory '" . $this->toDir . "' does not exist");
169
        }
170
171
        $xslfile = $this->getStyleSheet();
172
173
        $xsl = new \DOMDocument();
174
        $xsl->load($xslfile->getAbsolutePath());
175
176
        $proc = new XSLTProcessor();
177
        if (defined('XSL_SECPREF_WRITE_FILE')) {
178
            $proc->setSecurityPrefs(XSL_SECPREF_WRITE_FILE | XSL_SECPREF_CREATE_DIRECTORY);
179
        }
180
        $proc->registerPHPFunctions('nl2br');
181
        $proc->importStylesheet($xsl);
182
        $proc->setParameter('', 'output.sorttable', (string) $this->useSortTable);
183
184
        if ($this->format === "noframes") {
185
            $writer = new FileWriter(new File($this->toDir, "phpunit-noframes.html"));
186
            $writer->write($proc->transformToXml($document));
187
            $writer->close();
188
        } else {
189
            ExtendedFileStream::registerStream();
190
191
            $toDir = (string) $this->toDir;
192
193
            // urlencode() the path if we're on Windows
194
            if (OsCondition::isOS(OsCondition::FAMILY_WINDOWS)) {
195
                $toDir = urlencode($toDir);
196
            }
197
198
            // no output for the framed report
199
            // it's all done by extension...
200
            $proc->setParameter('', 'output.dir', $toDir);
201
            $proc->transformToXml($document);
202
203
            ExtendedFileStream::unregisterStream();
204
        }
205
    }
206
207
    /**
208
     * Fixes DOM document tree:
209
     *   - adds package="default" to 'testsuite' elements without
210
     *     package attribute
211
     *   - removes outer 'testsuite' container(s)
212
     *
213
     * @param \DOMDocument $document
214
     */
215
    protected function fixDocument(\DOMDocument $document)
216
    {
217
        $rootElement = $document->firstChild;
218
219
        $xp = new \DOMXPath($document);
220
221
        $nodes = $xp->query("/testsuites/testsuite/testsuite/testsuite");
222
223
        if ($nodes->length === 0) {
224
            $nodes = $xp->query("/testsuites/testsuite");
225
226
            foreach ($nodes as $node) {
227
                $children = $xp->query("./testsuite", $node);
228
229
                if ($children->length) {
230
                    $this->handleChildren($rootElement, $children);
231
                    $rootElement->removeChild($node);
0 ignored issues
show
Bug introduced by
The method removeChild() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

231
                    $rootElement->/** @scrutinizer ignore-call */ 
232
                                  removeChild($node);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
232
                }
233
            }
234
        } else {
235
            $nodes = $xp->query("/testsuites/testsuite/testsuite");
236
237
            foreach ($nodes as $node) {
238
                $children = $xp->query("./testsuite", $node);
239
240
                if ($children->length) {
241
                    $this->handleChildren($rootElement, $children);
242
                    $rootElement->firstChild->removeChild($node);
0 ignored issues
show
Bug introduced by
The method removeChild() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

242
                    $rootElement->firstChild->/** @scrutinizer ignore-call */ 
243
                                              removeChild($node);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
243
                }
244
            }
245
        }
246
    }
247
248
    private function handleChildren($rootElement, $children)
249
    {
250
        /**
251
         * @var $child \DOMElement
252
         */
253
        foreach ($children as $child) {
254
            $rootElement->appendChild($child);
255
256
            if ($child->hasAttribute('package')) {
257
                continue;
258
            }
259
260
            if ($child->hasAttribute('namespace')) {
261
                $child->setAttribute('package', $child->getAttribute('namespace'));
262
                continue;
263
            }
264
265
            $package = 'default';
266
            try {
267
                $refClass = new \ReflectionClass($child->getAttribute('name'));
268
269
                if (preg_match('/@package\s+(.*)\r?\n/m', $refClass->getDocComment(), $matches)) {
270
                    $package = end($matches);
271
                } elseif (method_exists($refClass, 'getNamespaceName')) {
272
                    $namespace = $refClass->getNamespaceName();
273
274
                    if ($namespace !== '') {
275
                        $package = $namespace;
276
                    }
277
                }
278
            } catch (\ReflectionException $e) {
279
                // do nothing
280
            }
281
282
            $child->setAttribute('package', trim($package));
283
        }
284
    }
285
286
    /**
287
     * Initialize the task
288
     *
289
     * @throws \Phing\Exception\BuildException
290
     */
291
    public function init()
292
    {
293
        if (!class_exists('XSLTProcessor')) {
294
            throw new BuildException("PHPUnitReportTask requires the XSL extension");
295
        }
296
    }
297
298
    /**
299
     * The main entry point
300
     *
301
     * @throws BuildException
302
     */
303
    public function main()
304
    {
305
        $testSuitesDoc = new \DOMDocument();
306
        $testSuitesDoc->load((string) $this->inFile);
307
308
        $this->fixDocument($testSuitesDoc);
309
310
        try {
311
            $this->transform($testSuitesDoc);
312
        } catch (IOException $e) {
313
            throw new BuildException('Transformation failed.', $e);
314
        }
315
    }
316
}
317