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 * Wrapper for the PHPUnit XML configuration file.
13 *
14 * Example XML configuration file:
15 * <code>
16 * <?xml version="1.0" encoding="utf-8" ?>
17 *
18 * <phpunit backupGlobals="true"
19 *          backupStaticAttributes="false"
20 *          bootstrap="/path/to/bootstrap.php"
21 *          cacheTokens="false"
22 *          columns="80"
23 *          colors="false"
24 *          stderr="false"
25 *          convertErrorsToExceptions="true"
26 *          convertNoticesToExceptions="true"
27 *          convertWarningsToExceptions="true"
28 *          forceCoversAnnotation="false"
29 *          processIsolation="false"
30 *          stopOnError="false"
31 *          stopOnFailure="false"
32 *          stopOnWarning="false"
33 *          stopOnIncomplete="false"
34 *          stopOnRisky="false"
35 *          stopOnSkipped="false"
36 *          failOnWarning="false"
37 *          failOnRisky="false"
38 *          extensionsDirectory="tools/phpunit.d"
39 *          printerClass="PHPUnit_TextUI_ResultPrinter"
40 *          testSuiteLoaderClass="PHPUnit_Runner_StandardTestSuiteLoader"
41 *          beStrictAboutChangesToGlobalState="false"
42 *          beStrictAboutCoversAnnotation="false"
43 *          beStrictAboutOutputDuringTests="false"
44 *          beStrictAboutResourceUsageDuringSmallTests="false"
45 *          beStrictAboutTestsThatDoNotTestAnything="false"
46 *          beStrictAboutTodoAnnotatedTests="false"
47 *          checkForUnintentionallyCoveredCode="false"
48 *          enforceTimeLimit="false"
49 *          timeoutForSmallTests="1"
50 *          timeoutForMediumTests="10"
51 *          timeoutForLargeTests="60"
52 *          verbose="false"
53 *          reverseDefectList="false"
54 *          registerMockObjectsFromTestArgumentsRecursively="false">
55 *   <testsuites>
56 *     <testsuite name="My Test Suite">
57 *       <directory suffix="Test.php" phpVersion="5.3.0" phpVersionOperator=">=">/path/to/files</directory>
58 *       <file phpVersion="5.3.0" phpVersionOperator=">=">/path/to/MyTest.php</file>
59 *       <exclude>/path/to/files/exclude</exclude>
60 *     </testsuite>
61 *   </testsuites>
62 *
63 *   <groups>
64 *     <include>
65 *       <group>name</group>
66 *     </include>
67 *     <exclude>
68 *       <group>name</group>
69 *     </exclude>
70 *   </groups>
71 *
72 *   <testdoxGroups>
73 *     <include>
74 *       <group>name</group>
75 *     </include>
76 *     <exclude>
77 *       <group>name</group>
78 *     </exclude>
79 *   </testdoxGroups>
80 *
81 *   <filter>
82 *     <whitelist addUncoveredFilesFromWhitelist="true"
83 *                processUncoveredFilesFromWhitelist="false">
84 *       <directory suffix=".php">/path/to/files</directory>
85 *       <file>/path/to/file</file>
86 *       <exclude>
87 *         <directory suffix=".php">/path/to/files</directory>
88 *         <file>/path/to/file</file>
89 *       </exclude>
90 *     </whitelist>
91 *   </filter>
92 *
93 *   <listeners>
94 *     <listener class="MyListener" file="/optional/path/to/MyListener.php">
95 *       <arguments>
96 *         <array>
97 *           <element key="0">
98 *             <string>Sebastian</string>
99 *           </element>
100 *         </array>
101 *         <integer>22</integer>
102 *         <string>April</string>
103 *         <double>19.78</double>
104 *         <null/>
105 *         <object class="stdClass"/>
106 *         <file>MyRelativeFile.php</file>
107 *         <directory>MyRelativeDir</directory>
108 *       </arguments>
109 *     </listener>
110 *   </listeners>
111 *
112 *   <logging>
113 *     <log type="coverage-html" target="/tmp/report" lowUpperBound="50" highLowerBound="90"/>
114 *     <log type="coverage-clover" target="/tmp/clover.xml"/>
115 *     <log type="coverage-crap4j" target="/tmp/crap.xml" threshold="30"/>
116 *     <log type="json" target="/tmp/logfile.json"/>
117 *     <log type="plain" target="/tmp/logfile.txt"/>
118 *     <log type="tap" target="/tmp/logfile.tap"/>
119 *     <log type="teamcity" target="/tmp/logfile.txt"/>
120 *     <log type="junit" target="/tmp/logfile.xml" logIncompleteSkipped="false"/>
121 *     <log type="testdox-html" target="/tmp/testdox.html"/>
122 *     <log type="testdox-text" target="/tmp/testdox.txt"/>
123 *     <log type="testdox-xml" target="/tmp/testdox.xml"/>
124 *   </logging>
125 *
126 *   <php>
127 *     <includePath>.</includePath>
128 *     <ini name="foo" value="bar"/>
129 *     <const name="foo" value="bar"/>
130 *     <var name="foo" value="bar"/>
131 *     <env name="foo" value="bar"/>
132 *     <post name="foo" value="bar"/>
133 *     <get name="foo" value="bar"/>
134 *     <cookie name="foo" value="bar"/>
135 *     <server name="foo" value="bar"/>
136 *     <files name="foo" value="bar"/>
137 *     <request name="foo" value="bar"/>
138 *   </php>
139 * </phpunit>
140 * </code>
141 */
142class PHPUnit_Util_Configuration
143{
144    private static $instances = [];
145
146    protected $document;
147    protected $xpath;
148    protected $filename;
149
150    /**
151     * Loads a PHPUnit configuration file.
152     *
153     * @param string $filename
154     */
155    protected function __construct($filename)
156    {
157        $this->filename = $filename;
158        $this->document = PHPUnit_Util_XML::loadFile($filename, false, true, true);
159        $this->xpath    = new DOMXPath($this->document);
160    }
161
162    final private function __clone()
163    {
164    }
165
166    /**
167     * Returns a PHPUnit configuration object.
168     *
169     * @param string $filename
170     *
171     * @return PHPUnit_Util_Configuration
172     */
173    public static function getInstance($filename)
174    {
175        $realpath = realpath($filename);
176
177        if ($realpath === false) {
178            throw new PHPUnit_Framework_Exception(
179                sprintf(
180                    'Could not read "%s".',
181                    $filename
182                )
183            );
184        }
185
186        if (!isset(self::$instances[$realpath])) {
187            self::$instances[$realpath] = new self($realpath);
188        }
189
190        return self::$instances[$realpath];
191    }
192
193    /**
194     * Returns the realpath to the configuration file.
195     *
196     * @return string
197     */
198    public function getFilename()
199    {
200        return $this->filename;
201    }
202
203    /**
204     * Returns the configuration for SUT filtering.
205     *
206     * @return array
207     */
208    public function getFilterConfiguration()
209    {
210        $addUncoveredFilesFromWhitelist     = true;
211        $processUncoveredFilesFromWhitelist = false;
212
213        $tmp = $this->xpath->query('filter/whitelist');
214
215        if ($tmp->length == 1) {
216            if ($tmp->item(0)->hasAttribute('addUncoveredFilesFromWhitelist')) {
217                $addUncoveredFilesFromWhitelist = $this->getBoolean(
218                    (string) $tmp->item(0)->getAttribute(
219                        'addUncoveredFilesFromWhitelist'
220                    ),
221                    true
222                );
223            }
224
225            if ($tmp->item(0)->hasAttribute('processUncoveredFilesFromWhitelist')) {
226                $processUncoveredFilesFromWhitelist = $this->getBoolean(
227                    (string) $tmp->item(0)->getAttribute(
228                        'processUncoveredFilesFromWhitelist'
229                    ),
230                    false
231                );
232            }
233        }
234
235        return [
236          'whitelist' => [
237            'addUncoveredFilesFromWhitelist'     => $addUncoveredFilesFromWhitelist,
238            'processUncoveredFilesFromWhitelist' => $processUncoveredFilesFromWhitelist,
239            'include'                            => [
240              'directory' => $this->readFilterDirectories(
241                  'filter/whitelist/directory'
242              ),
243              'file' => $this->readFilterFiles(
244                  'filter/whitelist/file'
245              )
246            ],
247            'exclude' => [
248              'directory' => $this->readFilterDirectories(
249                  'filter/whitelist/exclude/directory'
250              ),
251              'file' => $this->readFilterFiles(
252                  'filter/whitelist/exclude/file'
253              )
254            ]
255          ]
256        ];
257    }
258
259    /**
260     * Returns the configuration for groups.
261     *
262     * @return array
263     */
264    public function getGroupConfiguration()
265    {
266        return $this->parseGroupConfiguration('groups');
267    }
268
269    /**
270     * Returns the configuration for testdox groups.
271     *
272     * @return array
273     */
274    public function getTestdoxGroupConfiguration()
275    {
276        return $this->parseGroupConfiguration('testdoxGroups');
277    }
278
279    /**
280     * @param string $root
281     *
282     * @return array
283     */
284    private function parseGroupConfiguration($root)
285    {
286        $groups = [
287            'include' => [],
288            'exclude' => []
289        ];
290
291        foreach ($this->xpath->query($root . '/include/group') as $group) {
292            $groups['include'][] = (string) $group->textContent;
293        }
294
295        foreach ($this->xpath->query($root . '/exclude/group') as $group) {
296            $groups['exclude'][] = (string) $group->textContent;
297        }
298
299        return $groups;
300    }
301
302    /**
303     * Returns the configuration for listeners.
304     *
305     * @return array
306     */
307    public function getListenerConfiguration()
308    {
309        $result = [];
310
311        foreach ($this->xpath->query('listeners/listener') as $listener) {
312            $class     = (string) $listener->getAttribute('class');
313            $file      = '';
314            $arguments = [];
315
316            if ($listener->getAttribute('file')) {
317                $file = $this->toAbsolutePath(
318                    (string) $listener->getAttribute('file'),
319                    true
320                );
321            }
322
323            foreach ($listener->childNodes as $node) {
324                if ($node instanceof DOMElement && $node->tagName == 'arguments') {
325                    foreach ($node->childNodes as $argument) {
326                        if ($argument instanceof DOMElement) {
327                            if ($argument->tagName == 'file' ||
328                            $argument->tagName == 'directory') {
329                                $arguments[] = $this->toAbsolutePath((string) $argument->textContent);
330                            } else {
331                                $arguments[] = PHPUnit_Util_XML::xmlToVariable($argument);
332                            }
333                        }
334                    }
335                }
336            }
337
338            $result[] = [
339              'class'     => $class,
340              'file'      => $file,
341              'arguments' => $arguments
342            ];
343        }
344
345        return $result;
346    }
347
348    /**
349     * Returns the logging configuration.
350     *
351     * @return array
352     */
353    public function getLoggingConfiguration()
354    {
355        $result = [];
356
357        foreach ($this->xpath->query('logging/log') as $log) {
358            $type   = (string) $log->getAttribute('type');
359            $target = (string) $log->getAttribute('target');
360
361            if (!$target) {
362                continue;
363            }
364
365            $target = $this->toAbsolutePath($target);
366
367            if ($type == 'coverage-html') {
368                if ($log->hasAttribute('lowUpperBound')) {
369                    $result['lowUpperBound'] = $this->getInteger(
370                        (string) $log->getAttribute('lowUpperBound'),
371                        50
372                    );
373                }
374
375                if ($log->hasAttribute('highLowerBound')) {
376                    $result['highLowerBound'] = $this->getInteger(
377                        (string) $log->getAttribute('highLowerBound'),
378                        90
379                    );
380                }
381            } elseif ($type == 'coverage-crap4j') {
382                if ($log->hasAttribute('threshold')) {
383                    $result['crap4jThreshold'] = $this->getInteger(
384                        (string) $log->getAttribute('threshold'),
385                        30
386                    );
387                }
388            } elseif ($type == 'junit') {
389                if ($log->hasAttribute('logIncompleteSkipped')) {
390                    $result['logIncompleteSkipped'] = $this->getBoolean(
391                        (string) $log->getAttribute('logIncompleteSkipped'),
392                        false
393                    );
394                }
395            } elseif ($type == 'coverage-text') {
396                if ($log->hasAttribute('showUncoveredFiles')) {
397                    $result['coverageTextShowUncoveredFiles'] = $this->getBoolean(
398                        (string) $log->getAttribute('showUncoveredFiles'),
399                        false
400                    );
401                }
402                if ($log->hasAttribute('showOnlySummary')) {
403                    $result['coverageTextShowOnlySummary'] = $this->getBoolean(
404                        (string) $log->getAttribute('showOnlySummary'),
405                        false
406                    );
407                }
408            }
409
410            $result[$type] = $target;
411        }
412
413        return $result;
414    }
415
416    /**
417     * Returns the PHP configuration.
418     *
419     * @return array
420     */
421    public function getPHPConfiguration()
422    {
423        $result = [
424          'include_path' => [],
425          'ini'          => [],
426          'const'        => [],
427          'var'          => [],
428          'env'          => [],
429          'post'         => [],
430          'get'          => [],
431          'cookie'       => [],
432          'server'       => [],
433          'files'        => [],
434          'request'      => []
435        ];
436
437        foreach ($this->xpath->query('php/includePath') as $includePath) {
438            $path = (string) $includePath->textContent;
439            if ($path) {
440                $result['include_path'][] = $this->toAbsolutePath($path);
441            }
442        }
443
444        foreach ($this->xpath->query('php/ini') as $ini) {
445            $name  = (string) $ini->getAttribute('name');
446            $value = (string) $ini->getAttribute('value');
447
448            $result['ini'][$name] = $value;
449        }
450
451        foreach ($this->xpath->query('php/const') as $const) {
452            $name  = (string) $const->getAttribute('name');
453            $value = (string) $const->getAttribute('value');
454
455            $result['const'][$name] = $this->getBoolean($value, $value);
456        }
457
458        foreach (['var', 'env', 'post', 'get', 'cookie', 'server', 'files', 'request'] as $array) {
459            foreach ($this->xpath->query('php/' . $array) as $var) {
460                $name  = (string) $var->getAttribute('name');
461                $value = (string) $var->getAttribute('value');
462
463                $result[$array][$name] = $this->getBoolean($value, $value);
464            }
465        }
466
467        return $result;
468    }
469
470    /**
471     * Handles the PHP configuration.
472     */
473    public function handlePHPConfiguration()
474    {
475        $configuration = $this->getPHPConfiguration();
476
477        if (! empty($configuration['include_path'])) {
478            ini_set(
479                'include_path',
480                implode(PATH_SEPARATOR, $configuration['include_path']) .
481                PATH_SEPARATOR .
482                ini_get('include_path')
483            );
484        }
485
486        foreach ($configuration['ini'] as $name => $value) {
487            if (defined($value)) {
488                $value = constant($value);
489            }
490
491            ini_set($name, $value);
492        }
493
494        foreach ($configuration['const'] as $name => $value) {
495            if (!defined($name)) {
496                define($name, $value);
497            }
498        }
499
500        foreach (['var', 'post', 'get', 'cookie', 'server', 'files', 'request'] as $array) {
501            // See https://github.com/sebastianbergmann/phpunit/issues/277
502            switch ($array) {
503                case 'var':
504                    $target = &$GLOBALS;
505                    break;
506
507                case 'server':
508                    $target = &$_SERVER;
509                    break;
510
511                default:
512                    $target = &$GLOBALS['_' . strtoupper($array)];
513                    break;
514            }
515
516            foreach ($configuration[$array] as $name => $value) {
517                $target[$name] = $value;
518            }
519        }
520
521        foreach ($configuration['env'] as $name => $value) {
522            if (false === getenv($name)) {
523                putenv("{$name}={$value}");
524            }
525            if (!isset($_ENV[$name])) {
526                $_ENV[$name] = $value;
527            }
528        }
529    }
530
531    /**
532     * Returns the PHPUnit configuration.
533     *
534     * @return array
535     */
536    public function getPHPUnitConfiguration()
537    {
538        $result = [];
539        $root   = $this->document->documentElement;
540
541        if ($root->hasAttribute('cacheTokens')) {
542            $result['cacheTokens'] = $this->getBoolean(
543                (string) $root->getAttribute('cacheTokens'),
544                false
545            );
546        }
547
548        if ($root->hasAttribute('columns')) {
549            $columns = (string) $root->getAttribute('columns');
550
551            if ($columns == 'max') {
552                $result['columns'] = 'max';
553            } else {
554                $result['columns'] = $this->getInteger($columns, 80);
555            }
556        }
557
558        if ($root->hasAttribute('colors')) {
559            /* only allow boolean for compatibility with previous versions
560              'always' only allowed from command line */
561            if ($this->getBoolean($root->getAttribute('colors'), false)) {
562                $result['colors'] = PHPUnit_TextUI_ResultPrinter::COLOR_AUTO;
563            } else {
564                $result['colors'] = PHPUnit_TextUI_ResultPrinter::COLOR_NEVER;
565            }
566        }
567
568        /*
569         * Issue #657
570         */
571        if ($root->hasAttribute('stderr')) {
572            $result['stderr'] = $this->getBoolean(
573                (string) $root->getAttribute('stderr'),
574                false
575            );
576        }
577
578        if ($root->hasAttribute('backupGlobals')) {
579            $result['backupGlobals'] = $this->getBoolean(
580                (string) $root->getAttribute('backupGlobals'),
581                true
582            );
583        }
584
585        if ($root->hasAttribute('backupStaticAttributes')) {
586            $result['backupStaticAttributes'] = $this->getBoolean(
587                (string) $root->getAttribute('backupStaticAttributes'),
588                false
589            );
590        }
591
592        if ($root->getAttribute('bootstrap')) {
593            $result['bootstrap'] = $this->toAbsolutePath(
594                (string) $root->getAttribute('bootstrap')
595            );
596        }
597
598        if ($root->hasAttribute('convertErrorsToExceptions')) {
599            $result['convertErrorsToExceptions'] = $this->getBoolean(
600                (string) $root->getAttribute('convertErrorsToExceptions'),
601                true
602            );
603        }
604
605        if ($root->hasAttribute('convertNoticesToExceptions')) {
606            $result['convertNoticesToExceptions'] = $this->getBoolean(
607                (string) $root->getAttribute('convertNoticesToExceptions'),
608                true
609            );
610        }
611
612        if ($root->hasAttribute('convertWarningsToExceptions')) {
613            $result['convertWarningsToExceptions'] = $this->getBoolean(
614                (string) $root->getAttribute('convertWarningsToExceptions'),
615                true
616            );
617        }
618
619        if ($root->hasAttribute('forceCoversAnnotation')) {
620            $result['forceCoversAnnotation'] = $this->getBoolean(
621                (string) $root->getAttribute('forceCoversAnnotation'),
622                false
623            );
624        }
625
626        if ($root->hasAttribute('disableCodeCoverageIgnore')) {
627            $result['disableCodeCoverageIgnore'] = $this->getBoolean(
628                (string) $root->getAttribute('disableCodeCoverageIgnore'),
629                false
630            );
631        }
632
633        if ($root->hasAttribute('processIsolation')) {
634            $result['processIsolation'] = $this->getBoolean(
635                (string) $root->getAttribute('processIsolation'),
636                false
637            );
638        }
639
640        if ($root->hasAttribute('stopOnError')) {
641            $result['stopOnError'] = $this->getBoolean(
642                (string) $root->getAttribute('stopOnError'),
643                false
644            );
645        }
646
647        if ($root->hasAttribute('stopOnFailure')) {
648            $result['stopOnFailure'] = $this->getBoolean(
649                (string) $root->getAttribute('stopOnFailure'),
650                false
651            );
652        }
653
654        if ($root->hasAttribute('stopOnWarning')) {
655            $result['stopOnWarning'] = $this->getBoolean(
656                (string) $root->getAttribute('stopOnWarning'),
657                false
658            );
659        }
660
661        if ($root->hasAttribute('stopOnIncomplete')) {
662            $result['stopOnIncomplete'] = $this->getBoolean(
663                (string) $root->getAttribute('stopOnIncomplete'),
664                false
665            );
666        }
667
668        if ($root->hasAttribute('stopOnRisky')) {
669            $result['stopOnRisky'] = $this->getBoolean(
670                (string) $root->getAttribute('stopOnRisky'),
671                false
672            );
673        }
674
675        if ($root->hasAttribute('stopOnSkipped')) {
676            $result['stopOnSkipped'] = $this->getBoolean(
677                (string) $root->getAttribute('stopOnSkipped'),
678                false
679            );
680        }
681
682        if ($root->hasAttribute('failOnWarning')) {
683            $result['failOnWarning'] = $this->getBoolean(
684                (string) $root->getAttribute('failOnWarning'),
685                false
686            );
687        }
688
689        if ($root->hasAttribute('failOnRisky')) {
690            $result['failOnRisky'] = $this->getBoolean(
691                (string) $root->getAttribute('failOnRisky'),
692                false
693            );
694        }
695
696        if ($root->hasAttribute('testSuiteLoaderClass')) {
697            $result['testSuiteLoaderClass'] = (string) $root->getAttribute(
698                'testSuiteLoaderClass'
699            );
700        }
701
702        if ($root->getAttribute('testSuiteLoaderFile')) {
703            $result['testSuiteLoaderFile'] = $this->toAbsolutePath(
704                (string) $root->getAttribute('testSuiteLoaderFile')
705            );
706        }
707
708        if ($root->hasAttribute('printerClass')) {
709            $result['printerClass'] = (string) $root->getAttribute(
710                'printerClass'
711            );
712        }
713
714        if ($root->getAttribute('printerFile')) {
715            $result['printerFile'] = $this->toAbsolutePath(
716                (string) $root->getAttribute('printerFile')
717            );
718        }
719
720        if ($root->hasAttribute('beStrictAboutChangesToGlobalState')) {
721            $result['beStrictAboutChangesToGlobalState'] = $this->getBoolean(
722                (string) $root->getAttribute('beStrictAboutChangesToGlobalState'),
723                false
724            );
725        }
726
727        if ($root->hasAttribute('beStrictAboutOutputDuringTests')) {
728            $result['disallowTestOutput'] = $this->getBoolean(
729                (string) $root->getAttribute('beStrictAboutOutputDuringTests'),
730                false
731            );
732        }
733
734        if ($root->hasAttribute('beStrictAboutResourceUsageDuringSmallTests')) {
735            $result['beStrictAboutResourceUsageDuringSmallTests'] = $this->getBoolean(
736                (string) $root->getAttribute('beStrictAboutResourceUsageDuringSmallTests'),
737                false
738            );
739        }
740
741        if ($root->hasAttribute('beStrictAboutTestsThatDoNotTestAnything')) {
742            $result['reportUselessTests'] = $this->getBoolean(
743                (string) $root->getAttribute('beStrictAboutTestsThatDoNotTestAnything'),
744                false
745            );
746        }
747
748        if ($root->hasAttribute('beStrictAboutTodoAnnotatedTests')) {
749            $result['disallowTodoAnnotatedTests'] = $this->getBoolean(
750                (string) $root->getAttribute('beStrictAboutTodoAnnotatedTests'),
751                false
752            );
753        }
754
755        if ($root->hasAttribute('beStrictAboutCoversAnnotation')) {
756            $result['strictCoverage'] = $this->getBoolean(
757                (string) $root->getAttribute('beStrictAboutCoversAnnotation'),
758                false
759            );
760        } elseif ($root->hasAttribute('checkForUnintentionallyCoveredCode')) {
761            $result['strictCoverage'] = $this->getBoolean(
762                (string) $root->getAttribute('checkForUnintentionallyCoveredCode'),
763                false
764            );
765
766            $result['deprecatedCheckForUnintentionallyCoveredCodeSettingUsed'] = true;
767        }
768
769        if ($root->hasAttribute('enforceTimeLimit')) {
770            $result['enforceTimeLimit'] = $this->getBoolean(
771                (string) $root->getAttribute('enforceTimeLimit'),
772                false
773            );
774        }
775
776        if ($root->hasAttribute('timeoutForSmallTests')) {
777            $result['timeoutForSmallTests'] = $this->getInteger(
778                (string) $root->getAttribute('timeoutForSmallTests'),
779                1
780            );
781        }
782
783        if ($root->hasAttribute('timeoutForMediumTests')) {
784            $result['timeoutForMediumTests'] = $this->getInteger(
785                (string) $root->getAttribute('timeoutForMediumTests'),
786                10
787            );
788        }
789
790        if ($root->hasAttribute('timeoutForLargeTests')) {
791            $result['timeoutForLargeTests'] = $this->getInteger(
792                (string) $root->getAttribute('timeoutForLargeTests'),
793                60
794            );
795        }
796
797        if ($root->hasAttribute('reverseDefectList')) {
798            $result['reverseDefectList'] = $this->getBoolean(
799                (string) $root->getAttribute('reverseDefectList'),
800                false
801            );
802        }
803
804        if ($root->hasAttribute('verbose')) {
805            $result['verbose'] = $this->getBoolean(
806                (string) $root->getAttribute('verbose'),
807                false
808            );
809        }
810
811        if ($root->hasAttribute('registerMockObjectsFromTestArgumentsRecursively')) {
812            $result['registerMockObjectsFromTestArgumentsRecursively'] = $this->getBoolean(
813                (string) $root->getAttribute('registerMockObjectsFromTestArgumentsRecursively'),
814                false
815            );
816        }
817
818        if ($root->hasAttribute('extensionsDirectory')) {
819            $result['extensionsDirectory'] = $this->toAbsolutePath(
820                    (string) $root->getAttribute(
821                        'extensionsDirectory'
822                    )
823            );
824        }
825
826        return $result;
827    }
828
829    /**
830     * Returns the test suite configuration.
831     *
832     * @return PHPUnit_Framework_TestSuite
833     */
834    public function getTestSuiteConfiguration($testSuiteFilter = null)
835    {
836        $testSuiteNodes = $this->xpath->query('testsuites/testsuite');
837
838        if ($testSuiteNodes->length == 0) {
839            $testSuiteNodes = $this->xpath->query('testsuite');
840        }
841
842        if ($testSuiteNodes->length == 1) {
843            return $this->getTestSuite($testSuiteNodes->item(0), $testSuiteFilter);
844        }
845
846        if ($testSuiteNodes->length > 1) {
847            $suite = new PHPUnit_Framework_TestSuite;
848
849            foreach ($testSuiteNodes as $testSuiteNode) {
850                $suite->addTestSuite(
851                    $this->getTestSuite($testSuiteNode, $testSuiteFilter)
852                );
853            }
854
855            return $suite;
856        }
857    }
858
859    /**
860     * Returns the test suite names from the configuration.
861     *
862     * @return array
863     */
864    public function getTestSuiteNames()
865    {
866        $names = [];
867        $nodes = $this->xpath->query('*/testsuite');
868        foreach ($nodes as $node) {
869            $names[] = $node->getAttribute('name');
870        }
871
872        return $names;
873    }
874
875    /**
876     * @param DOMElement $testSuiteNode
877     *
878     * @return PHPUnit_Framework_TestSuite
879     */
880    protected function getTestSuite(DOMElement $testSuiteNode, $testSuiteFilter = null)
881    {
882        if ($testSuiteNode->hasAttribute('name')) {
883            $suite = new PHPUnit_Framework_TestSuite(
884                (string) $testSuiteNode->getAttribute('name')
885            );
886        } else {
887            $suite = new PHPUnit_Framework_TestSuite;
888        }
889
890        $exclude = [];
891
892        foreach ($testSuiteNode->getElementsByTagName('exclude') as $excludeNode) {
893            $excludeFile = (string) $excludeNode->textContent;
894            if ($excludeFile) {
895                $exclude[] = $this->toAbsolutePath($excludeFile);
896            }
897        }
898
899        $fileIteratorFacade = new File_Iterator_Facade;
900
901        foreach ($testSuiteNode->getElementsByTagName('directory') as $directoryNode) {
902            if ($testSuiteFilter && $directoryNode->parentNode->getAttribute('name') != $testSuiteFilter) {
903                continue;
904            }
905
906            $directory = (string) $directoryNode->textContent;
907
908            if (empty($directory)) {
909                continue;
910            }
911
912            if ($directoryNode->hasAttribute('phpVersion')) {
913                $phpVersion = (string) $directoryNode->getAttribute('phpVersion');
914            } else {
915                $phpVersion = PHP_VERSION;
916            }
917
918            if ($directoryNode->hasAttribute('phpVersionOperator')) {
919                $phpVersionOperator = (string) $directoryNode->getAttribute('phpVersionOperator');
920            } else {
921                $phpVersionOperator = '>=';
922            }
923
924            if (!version_compare(PHP_VERSION, $phpVersion, $phpVersionOperator)) {
925                continue;
926            }
927
928            if ($directoryNode->hasAttribute('prefix')) {
929                $prefix = (string) $directoryNode->getAttribute('prefix');
930            } else {
931                $prefix = '';
932            }
933
934            if ($directoryNode->hasAttribute('suffix')) {
935                $suffix = (string) $directoryNode->getAttribute('suffix');
936            } else {
937                $suffix = 'Test.php';
938            }
939
940            $files = $fileIteratorFacade->getFilesAsArray(
941                $this->toAbsolutePath($directory),
942                $suffix,
943                $prefix,
944                $exclude
945            );
946            $suite->addTestFiles($files);
947        }
948
949        foreach ($testSuiteNode->getElementsByTagName('file') as $fileNode) {
950            if ($testSuiteFilter && $fileNode->parentNode->getAttribute('name') != $testSuiteFilter) {
951                continue;
952            }
953
954            $file = (string) $fileNode->textContent;
955
956            if (empty($file)) {
957                continue;
958            }
959
960            // Get the absolute path to the file
961            $file = $fileIteratorFacade->getFilesAsArray(
962                $this->toAbsolutePath($file)
963            );
964
965            if (!isset($file[0])) {
966                continue;
967            }
968
969            $file = $file[0];
970
971            if ($fileNode->hasAttribute('phpVersion')) {
972                $phpVersion = (string) $fileNode->getAttribute('phpVersion');
973            } else {
974                $phpVersion = PHP_VERSION;
975            }
976
977            if ($fileNode->hasAttribute('phpVersionOperator')) {
978                $phpVersionOperator = (string) $fileNode->getAttribute('phpVersionOperator');
979            } else {
980                $phpVersionOperator = '>=';
981            }
982
983            if (!version_compare(PHP_VERSION, $phpVersion, $phpVersionOperator)) {
984                continue;
985            }
986
987            $suite->addTestFile($file);
988        }
989
990        return $suite;
991    }
992
993    /**
994     * @param string $value
995     * @param bool   $default
996     *
997     * @return bool
998     */
999    protected function getBoolean($value, $default)
1000    {
1001        if (strtolower($value) == 'false') {
1002            return false;
1003        } elseif (strtolower($value) == 'true') {
1004            return true;
1005        }
1006
1007        return $default;
1008    }
1009
1010    /**
1011     * @param string $value
1012     * @param bool   $default
1013     *
1014     * @return bool
1015     */
1016    protected function getInteger($value, $default)
1017    {
1018        if (is_numeric($value)) {
1019            return (int) $value;
1020        }
1021
1022        return $default;
1023    }
1024
1025    /**
1026     * @param string $query
1027     *
1028     * @return array
1029     */
1030    protected function readFilterDirectories($query)
1031    {
1032        $directories = [];
1033
1034        foreach ($this->xpath->query($query) as $directory) {
1035            $directoryPath = (string) $directory->textContent;
1036
1037            if (!$directoryPath) {
1038                continue;
1039            }
1040
1041            if ($directory->hasAttribute('prefix')) {
1042                $prefix = (string) $directory->getAttribute('prefix');
1043            } else {
1044                $prefix = '';
1045            }
1046
1047            if ($directory->hasAttribute('suffix')) {
1048                $suffix = (string) $directory->getAttribute('suffix');
1049            } else {
1050                $suffix = '.php';
1051            }
1052
1053            if ($directory->hasAttribute('group')) {
1054                $group = (string) $directory->getAttribute('group');
1055            } else {
1056                $group = 'DEFAULT';
1057            }
1058
1059            $directories[] = [
1060              'path'   => $this->toAbsolutePath($directoryPath),
1061              'prefix' => $prefix,
1062              'suffix' => $suffix,
1063              'group'  => $group
1064            ];
1065        }
1066
1067        return $directories;
1068    }
1069
1070    /**
1071     * @param string $query
1072     *
1073     * @return array
1074     */
1075    protected function readFilterFiles($query)
1076    {
1077        $files = [];
1078
1079        foreach ($this->xpath->query($query) as $file) {
1080            $filePath = (string) $file->textContent;
1081
1082            if ($filePath) {
1083                $files[] = $this->toAbsolutePath($filePath);
1084            }
1085        }
1086
1087        return $files;
1088    }
1089
1090    /**
1091     * @param string $path
1092     * @param bool   $useIncludePath
1093     *
1094     * @return string
1095     */
1096    protected function toAbsolutePath($path, $useIncludePath = false)
1097    {
1098        $path = trim($path);
1099
1100        if ($path[0] === '/') {
1101            return $path;
1102        }
1103
1104        // Matches the following on Windows:
1105        //  - \\NetworkComputer\Path
1106        //  - \\.\D:
1107        //  - \\.\c:
1108        //  - C:\Windows
1109        //  - C:\windows
1110        //  - C:/windows
1111        //  - c:/windows
1112        if (defined('PHP_WINDOWS_VERSION_BUILD') &&
1113            ($path[0] === '\\' ||
1114            (strlen($path) >= 3 && preg_match('#^[A-Z]\:[/\\\]#i', substr($path, 0, 3))))) {
1115            return $path;
1116        }
1117
1118        // Stream
1119        if (strpos($path, '://') !== false) {
1120            return $path;
1121        }
1122
1123        $file = dirname($this->filename) . DIRECTORY_SEPARATOR . $path;
1124
1125        if ($useIncludePath && !file_exists($file)) {
1126            $includePathFile = stream_resolve_include_path($path);
1127
1128            if ($includePathFile) {
1129                $file = $includePathFile;
1130            }
1131        }
1132
1133        return $file;
1134    }
1135}
1136