1<?php
2/*
3 * This file is part of the php-code-coverage package.
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
11namespace SebastianBergmann\CodeCoverage\Report\Xml;
12
13use SebastianBergmann\CodeCoverage\CodeCoverage;
14use SebastianBergmann\CodeCoverage\Node\AbstractNode;
15use SebastianBergmann\CodeCoverage\Node\Directory as DirectoryNode;
16use SebastianBergmann\CodeCoverage\Node\File as FileNode;
17use SebastianBergmann\CodeCoverage\RuntimeException;
18use SebastianBergmann\CodeCoverage\Version;
19use SebastianBergmann\Environment\Runtime;
20
21class Facade
22{
23    /**
24     * @var string
25     */
26    private $target;
27
28    /**
29     * @var Project
30     */
31    private $project;
32
33    /**
34     * @var string
35     */
36    private $phpUnitVersion;
37
38    /**
39     * @param string $version
40     */
41    public function __construct($version)
42    {
43        $this->phpUnitVersion = $version;
44    }
45
46    /**
47     * @param CodeCoverage $coverage
48     * @param string       $target
49     *
50     * @throws RuntimeException
51     */
52    public function process(CodeCoverage $coverage, $target)
53    {
54        if (substr($target, -1, 1) != DIRECTORY_SEPARATOR) {
55            $target .= DIRECTORY_SEPARATOR;
56        }
57
58        $this->target = $target;
59        $this->initTargetDirectory($target);
60
61        $report = $coverage->getReport();
62
63        $this->project = new Project(
64            $coverage->getReport()->getName()
65        );
66
67        $this->setBuildInformation();
68        $this->processTests($coverage->getTests());
69        $this->processDirectory($report, $this->project);
70
71        $this->saveDocument($this->project->asDom(), 'index');
72    }
73
74    private function setBuildInformation()
75    {
76        $buildNode = $this->project->getBuildInformation();
77        $buildNode->setRuntimeInformation(new Runtime());
78        $buildNode->setBuildTime(\DateTime::createFromFormat('U', $_SERVER['REQUEST_TIME']));
79        $buildNode->setGeneratorVersions($this->phpUnitVersion, Version::id());
80    }
81
82    /**
83     * @param string $directory
84     */
85    protected function initTargetDirectory($directory)
86    {
87        if (file_exists($directory)) {
88            if (!is_dir($directory)) {
89                throw new RuntimeException(
90                    "'$directory' exists but is not a directory."
91                );
92            }
93
94            if (!is_writable($directory)) {
95                throw new RuntimeException(
96                    "'$directory' exists but is not writable."
97                );
98            }
99        } elseif (!@mkdir($directory, 0777, true)) {
100            throw new RuntimeException(
101                "'$directory' could not be created."
102            );
103        }
104    }
105
106    private function processDirectory(DirectoryNode $directory, Node $context)
107    {
108        $dirname = $directory->getName();
109        if ($this->project->getProjectSourceDirectory() === $dirname) {
110            $dirname = '/';
111        }
112        $dirObject = $context->addDirectory($dirname);
113
114        $this->setTotals($directory, $dirObject->getTotals());
115
116        foreach ($directory->getDirectories() as $node) {
117            $this->processDirectory($node, $dirObject);
118        }
119
120        foreach ($directory->getFiles() as $node) {
121            $this->processFile($node, $dirObject);
122        }
123    }
124
125    private function processFile(FileNode $file, Directory $context)
126    {
127        $fileObject = $context->addFile(
128            $file->getName(),
129            $file->getId() . '.xml'
130        );
131
132        $this->setTotals($file, $fileObject->getTotals());
133
134        $path = substr(
135            $file->getPath(),
136            strlen($this->project->getProjectSourceDirectory())
137        );
138        $fileReport = new Report($path);
139
140        $this->setTotals($file, $fileReport->getTotals());
141
142        foreach ($file->getClassesAndTraits() as $unit) {
143            $this->processUnit($unit, $fileReport);
144        }
145
146        foreach ($file->getFunctions() as $function) {
147            $this->processFunction($function, $fileReport);
148        }
149
150        foreach ($file->getCoverageData() as $line => $tests) {
151            if (!is_array($tests) || count($tests) === 0) {
152                continue;
153            }
154
155            $coverage = $fileReport->getLineCoverage($line);
156
157            foreach ($tests as $test) {
158                $coverage->addTest($test);
159            }
160
161            $coverage->finalize();
162        }
163
164        $fileReport->getSource()->setSourceCode(
165            file_get_contents($file->getPath())
166        );
167
168        $this->saveDocument($fileReport->asDom(), $file->getId());
169    }
170
171    private function processUnit($unit, Report $report)
172    {
173        if (isset($unit['className'])) {
174            $unitObject = $report->getClassObject($unit['className']);
175        } else {
176            $unitObject = $report->getTraitObject($unit['traitName']);
177        }
178
179        $unitObject->setLines(
180            $unit['startLine'],
181            $unit['executableLines'],
182            $unit['executedLines']
183        );
184
185        $unitObject->setCrap($unit['crap']);
186
187        $unitObject->setPackage(
188            $unit['package']['fullPackage'],
189            $unit['package']['package'],
190            $unit['package']['subpackage'],
191            $unit['package']['category']
192        );
193
194        $unitObject->setNamespace($unit['package']['namespace']);
195
196        foreach ($unit['methods'] as $method) {
197            $methodObject = $unitObject->addMethod($method['methodName']);
198            $methodObject->setSignature($method['signature']);
199            $methodObject->setLines($method['startLine'], $method['endLine']);
200            $methodObject->setCrap($method['crap']);
201            $methodObject->setTotals(
202                $method['executableLines'],
203                $method['executedLines'],
204                $method['coverage']
205            );
206        }
207    }
208
209    private function processFunction($function, Report $report)
210    {
211        $functionObject = $report->getFunctionObject($function['functionName']);
212
213        $functionObject->setSignature($function['signature']);
214        $functionObject->setLines($function['startLine']);
215        $functionObject->setCrap($function['crap']);
216        $functionObject->setTotals($function['executableLines'], $function['executedLines'], $function['coverage']);
217    }
218
219    private function processTests(array $tests)
220    {
221        $testsObject = $this->project->getTests();
222
223        foreach ($tests as $test => $result) {
224            if ($test == 'UNCOVERED_FILES_FROM_WHITELIST') {
225                continue;
226            }
227
228            $testsObject->addTest($test, $result);
229        }
230    }
231
232    private function setTotals(AbstractNode $node, Totals $totals)
233    {
234        $loc = $node->getLinesOfCode();
235
236        $totals->setNumLines(
237            $loc['loc'],
238            $loc['cloc'],
239            $loc['ncloc'],
240            $node->getNumExecutableLines(),
241            $node->getNumExecutedLines()
242        );
243
244        $totals->setNumClasses(
245            $node->getNumClasses(),
246            $node->getNumTestedClasses()
247        );
248
249        $totals->setNumTraits(
250            $node->getNumTraits(),
251            $node->getNumTestedTraits()
252        );
253
254        $totals->setNumMethods(
255            $node->getNumMethods(),
256            $node->getNumTestedMethods()
257        );
258
259        $totals->setNumFunctions(
260            $node->getNumFunctions(),
261            $node->getNumTestedFunctions()
262        );
263    }
264
265    /**
266     * @return string
267     */
268    protected function getTargetDirectory()
269    {
270        return $this->target;
271    }
272
273    protected function saveDocument(\DOMDocument $document, $name)
274    {
275        $filename = sprintf('%s/%s.xml', $this->getTargetDirectory(), $name);
276
277        $document->formatOutput       = true;
278        $document->preserveWhiteSpace = false;
279        $this->initTargetDirectory(dirname($filename));
280
281        $document->save($filename);
282    }
283}
284