Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
69 / 69
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
CloverXml
100.00% covered (success)
100.00%
69 / 69
100.00% covered (success)
100.00%
4 / 4
27
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setRounding
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFiles
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 handleFileNode
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
1 / 1
17
1<?php
2/**
3 * Copyright (C) 2018 Kunal Mehta <legoktm@debian.org>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 */
18
19namespace Wikimedia\CloverDiff;
20
21use InvalidArgumentException;
22use SimpleXMLElement;
23
24/**
25 * Represents a clover.xml file
26 */
27class CloverXml {
28
29    /**
30     * Count percentage covered
31     */
32    public const PERCENTAGE = 1;
33
34    /**
35     * Return (un)covered lines
36     */
37    public const LINES = 2;
38
39    /**
40     * Count coverage status of classes and functions
41     */
42    public const METHODS = 3;
43
44    /**
45     * @var string
46     */
47    private string $fname;
48
49    /**
50     * @var SimpleXMLElement
51     */
52    private SimpleXMLElement $xml;
53
54    /**
55     * Whether to round or not
56     * @var bool
57     */
58    private bool $rounding = true;
59
60    /**
61     * @param string $fname Filename
62     *
63     * @throws InvalidArgumentException
64     */
65    public function __construct( string $fname ) {
66        if ( !file_exists( $fname ) ) {
67            throw new InvalidArgumentException( "$fname doesn't exist" );
68        }
69        $this->fname = $fname;
70        $this->xml = new SimpleXMLElement( file_get_contents( $fname ) );
71    }
72
73    /**
74     * Enable/disable rounding abilities
75     *
76     * @param bool $rounding
77     */
78    public function setRounding( bool $rounding ): void {
79        $this->rounding = $rounding;
80    }
81
82    /**
83     * @param int $mode
84     *
85     * @return array
86     */
87    public function getFiles( int $mode = self::PERCENTAGE ): array {
88        $files = [];
89        $commonPath = null;
90        foreach ( $this->xml->project->children() as $node ) {
91            if ( $node->getName() === 'package' ) {
92                // If there's a common namespace I think, PHPUnit will
93                // put everything under a package subnode.
94                foreach ( $node->children() as $subNode ) {
95                    if ( $subNode->getName() === 'file' ) {
96                        $files += $this->handleFileNode( $subNode, $commonPath, $mode );
97                    }
98                }
99            } elseif ( $node->getName() === 'file' ) {
100                $files += $this->handleFileNode( $node, $commonPath, $mode );
101            }
102            // TODO: else?
103        }
104
105        // Now strip common path from everything...
106        $sanePathFiles = [];
107        foreach ( $files as $path => $info ) {
108            // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal
109            $newPath = str_replace( $commonPath, '', $path );
110            $sanePathFiles[$newPath] = $info;
111        }
112
113        return $sanePathFiles;
114    }
115
116    /**
117     * @param SimpleXMLElement $node
118     * @param string|null &$commonPath
119     * @param int $mode
120     *
121     * @return array[]|float[]|int[]
122     */
123    private function handleFileNode( SimpleXMLElement $node, ?string &$commonPath, int $mode ): array {
124        $coveredLines = 0;
125        $totalLines = 0;
126        $lines = [];
127        $mStats = [];
128        $mCovered = 0;
129        $mTotal = 0;
130        $class = null;
131        $method = null;
132        foreach ( $node->children() as $child ) {
133            if ( $child->getName() === 'class' ) {
134                $class = $child['name'];
135                if ( $child['namespace'] != 'global'
136                    // PHPUnit 6 includes the namespace in the class name
137                    // in addition to the namespace attribute
138                    && strpos( $class, (string)$child['namespace'] ) !== 0
139                ) {
140                    $class = "{$child['namespace']}\\$class";
141                }
142                continue;
143            }
144            if ( $child->getName() !== 'line' ) {
145                continue;
146            }
147            if ( $child['type'] == 'method' ) {
148                if ( $method !== null ) {
149                    // @phan-suppress-next-line PhanDivisionByZero
150                    $mStats[$method] = $mCovered / $mTotal * 100;
151                    $mCovered = 0;
152                    $mTotal = 0;
153                }
154                // @phan-suppress-next-line PhanTypeSuspiciousStringExpression
155                $method = "$class::{$child['name']}";
156            }
157            $totalLines++;
158            $mTotal++;
159            $lineCovered = (int)$child['count'];
160            if ( $lineCovered ) {
161                // If count > 0 then it's covered
162                $coveredLines++;
163                $mCovered++;
164            }
165            $lines[(int)$child['num']] = $lineCovered;
166        }
167        $path = (string)$node['name'];
168        if ( $totalLines === 0 ) {
169            // Don't ever divide by 0
170            $covered = 0;
171        } else {
172            $covered = $coveredLines / $totalLines * 100;
173            // Do some rounding
174            if ( $this->rounding ) {
175                if ( $totalLines < 500 ) {
176                    $covered = round( $covered );
177                } elseif ( $totalLines < 1000 ) {
178                    $covered = round( $covered, 1 );
179                } else {
180                    $covered = round( $covered, 2 );
181                }
182            }
183        }
184        if ( $commonPath === null ) {
185            $commonPath = $path;
186        } else {
187            while ( strpos( $path, $commonPath ) === false ) {
188                $commonPath = dirname( $commonPath ) . '/';
189            }
190        }
191
192        if ( $mode === self::LINES ) {
193            $ret = $lines;
194        } elseif ( $mode === self::METHODS ) {
195            $ret = $mStats;
196        } else {
197            $ret = $covered;
198        }
199        return [ $path => $ret ];
200    }
201
202}