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 * @license GPL-3.0-or-later
5 */
6
7namespace Wikimedia\CloverDiff;
8
9use InvalidArgumentException;
10use SimpleXMLElement;
11
12/**
13 * Represents a clover.xml file
14 */
15class CloverXml {
16
17    /**
18     * Count percentage covered
19     */
20    public const PERCENTAGE = 1;
21
22    /**
23     * Return (un)covered lines
24     */
25    public const LINES = 2;
26
27    /**
28     * Count coverage status of classes and functions
29     */
30    public const METHODS = 3;
31
32    /**
33     * @var string
34     */
35    private string $fname;
36
37    /**
38     * @var SimpleXMLElement
39     */
40    private SimpleXMLElement $xml;
41
42    /**
43     * Whether to round or not
44     * @var bool
45     */
46    private bool $rounding = true;
47
48    /**
49     * @param string $fname Filename
50     *
51     * @throws InvalidArgumentException
52     */
53    public function __construct( string $fname ) {
54        if ( !file_exists( $fname ) ) {
55            throw new InvalidArgumentException( "$fname doesn't exist" );
56        }
57        $this->fname = $fname;
58        $this->xml = new SimpleXMLElement( file_get_contents( $fname ) );
59    }
60
61    /**
62     * Enable/disable rounding abilities
63     *
64     * @param bool $rounding
65     */
66    public function setRounding( bool $rounding ): void {
67        $this->rounding = $rounding;
68    }
69
70    /**
71     * @param int $mode
72     *
73     * @return array
74     */
75    public function getFiles( int $mode = self::PERCENTAGE ): array {
76        $files = [];
77        $commonPath = null;
78        foreach ( $this->xml->project->children() as $node ) {
79            if ( $node->getName() === 'package' ) {
80                // If there's a common namespace I think, PHPUnit will
81                // put everything under a package subnode.
82                foreach ( $node->children() as $subNode ) {
83                    if ( $subNode->getName() === 'file' ) {
84                        $files += $this->handleFileNode( $subNode, $commonPath, $mode );
85                    }
86                }
87            } elseif ( $node->getName() === 'file' ) {
88                $files += $this->handleFileNode( $node, $commonPath, $mode );
89            }
90            // TODO: else?
91        }
92
93        // Now strip common path from everything...
94        $sanePathFiles = [];
95        foreach ( $files as $path => $info ) {
96            // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal
97            $newPath = str_replace( $commonPath, '', $path );
98            $sanePathFiles[$newPath] = $info;
99        }
100
101        return $sanePathFiles;
102    }
103
104    /**
105     * @param SimpleXMLElement $node
106     * @param string|null &$commonPath
107     * @param int $mode
108     *
109     * @return array[]|float[]|int[]
110     */
111    private function handleFileNode( SimpleXMLElement $node, ?string &$commonPath, int $mode ): array {
112        $coveredLines = 0;
113        $totalLines = 0;
114        $lines = [];
115        $mStats = [];
116        $mCovered = 0;
117        $mTotal = 0;
118        $class = null;
119        $method = null;
120        foreach ( $node->children() as $child ) {
121            if ( $child->getName() === 'class' ) {
122                $class = $child['name'];
123                if ( $child['namespace'] != 'global'
124                    // PHPUnit 6 includes the namespace in the class name
125                    // in addition to the namespace attribute
126                    && strpos( $class, (string)$child['namespace'] ) !== 0
127                ) {
128                    $class = "{$child['namespace']}\\$class";
129                }
130                continue;
131            }
132            if ( $child->getName() !== 'line' ) {
133                continue;
134            }
135            if ( $child['type'] == 'method' ) {
136                if ( $method !== null ) {
137                    // @phan-suppress-next-line PhanDivisionByZero
138                    $mStats[$method] = $mCovered / $mTotal * 100;
139                    $mCovered = 0;
140                    $mTotal = 0;
141                }
142                // @phan-suppress-next-line PhanTypeSuspiciousStringExpression
143                $method = "$class::{$child['name']}";
144            }
145            $totalLines++;
146            $mTotal++;
147            $lineCovered = (int)$child['count'];
148            if ( $lineCovered ) {
149                // If count > 0 then it's covered
150                $coveredLines++;
151                $mCovered++;
152            }
153            $lines[(int)$child['num']] = $lineCovered;
154        }
155        $path = (string)$node['name'];
156        if ( $totalLines === 0 ) {
157            // Don't ever divide by 0
158            $covered = 0;
159        } else {
160            $covered = $coveredLines / $totalLines * 100;
161            // Do some rounding
162            if ( $this->rounding ) {
163                if ( $totalLines < 500 ) {
164                    $covered = round( $covered );
165                } elseif ( $totalLines < 1000 ) {
166                    $covered = round( $covered, 1 );
167                } else {
168                    $covered = round( $covered, 2 );
169                }
170            }
171        }
172        if ( $commonPath === null ) {
173            $commonPath = $path;
174        } else {
175            while ( strpos( $path, $commonPath ) === false ) {
176                $commonPath = dirname( $commonPath ) . '/';
177            }
178        }
179
180        if ( $mode === self::LINES ) {
181            $ret = $lines;
182        } elseif ( $mode === self::METHODS ) {
183            $ret = $mStats;
184        } else {
185            $ret = $covered;
186        }
187        return [ $path => $ret ];
188    }
189
190}