4
4
5
5
namespace GrumPHP \Task ;
6
6
7
+ use GrumPHP \Exception \FileNotFoundException ;
7
8
use GrumPHP \Runner \TaskResult ;
8
9
use GrumPHP \Runner \TaskResultInterface ;
9
10
use GrumPHP \Task \Config \ConfigOptionsResolver ;
15
16
use GrumPHP \Util \Filesystem ;
16
17
use SimpleXMLElement ;
17
18
use SplFileInfo ;
19
+ use Symfony \Component \OptionsResolver \Options ;
18
20
use Symfony \Component \OptionsResolver \OptionsResolver ;
21
+ use VeeWee \Xml \Dom \Document ;
19
22
20
23
class CloverCoverage implements TaskInterface
21
24
{
25
+ public const MERGE_STRATEGY_COMBINE = 'combine ' ;
26
+ public const MERGE_STRATEGY_MERGE = 'merge ' ;
27
+
22
28
/**
23
29
* @var Filesystem
24
30
*/
@@ -53,16 +59,41 @@ public static function getConfigurableOptions(): ConfigOptionsResolver
53
59
$ resolver = new OptionsResolver ();
54
60
55
61
$ resolver ->setDefined ('clover_file ' );
56
- $ resolver ->setDefined ('level ' );
62
+ $ resolver ->setDefined ('minimum_level ' );
63
+ $ resolver ->setDefined ('target_level ' );
64
+ $ resolver ->setDefined ('merge_strategy ' );
57
65
58
- $ resolver ->addAllowedTypes ('clover_file ' , ['string ' ]);
59
- $ resolver ->addAllowedTypes ('level ' , ['int ' , 'float ' ]);
66
+ $ resolver ->setRequired ('clover_file ' );
67
+
68
+ $ resolver ->addAllowedTypes ('clover_file ' , ['string ' , 'string[] ' ]);
69
+ $ resolver ->addAllowedTypes ('minimum_level ' , ['int ' , 'float ' ]);
70
+ $ resolver ->addAllowedTypes ('target_level ' , ['int ' , 'float ' , 'null ' ]);
71
+
72
+ $ resolver ->addAllowedTypes ('merge_strategy ' , ['string ' ]);
73
+ $ resolver ->setAllowedValues ('merge_strategy ' , [
74
+ self ::MERGE_STRATEGY_COMBINE ,
75
+ self ::MERGE_STRATEGY_MERGE ,
76
+ ]);
60
77
61
78
$ resolver ->setDefaults ([
62
- 'level ' => 100 ,
79
+ 'minimum_level ' => 100 ,
80
+ 'target_level ' => null ,
81
+ 'merge_strategy ' => self ::MERGE_STRATEGY_MERGE ,
63
82
]);
64
83
65
- $ resolver ->setRequired ('clover_file ' );
84
+ // @deprecated : Can be removed on 3.0.0
85
+ $ resolver ->setDefined ('level ' );
86
+ $ resolver ->setDeprecated (
87
+ 'level ' ,
88
+ 'grumphp ' ,
89
+ '2.8.0 ' ,
90
+ 'The level has been deprecated and will be removed in 3.0.0. Use minimum_level instead. '
91
+ );
92
+ $ resolver ->addAllowedTypes ('level ' , ['int ' , 'float ' ]);
93
+ $ resolver ->setDefault ('minimum_level ' , function (Options $ options ): int |float {
94
+ return (float ) ($ options ['level ' ] ?? 100 );
95
+ });
96
+ // @deprecated : end
66
97
67
98
return ConfigOptionsResolver::fromOptionsResolver ($ resolver );
68
99
}
@@ -81,41 +112,96 @@ public function canRunInContext(ContextInterface $context): bool
81
112
public function run (ContextInterface $ context ): TaskResultInterface
82
113
{
83
114
$ configuration = $ this ->getConfig ()->getOptions ();
84
- $ percentage = round (min (100 , max (0 , (float ) $ configuration ['level ' ])), 2 );
85
- $ cloverFile = (string ) $ configuration ['clover_file ' ];
86
-
87
- if (!$ this ->filesystem ->exists ($ cloverFile )) {
88
- return TaskResult::createFailed ($ this , $ context , 'Invalid input file provided ' );
115
+ $ clamp = static fn (float $ value ): float => round (min (100 , max (0 , $ value )), 2 );
116
+ $ minimumLevel = $ clamp ((float ) $ configuration ['minimum_level ' ]);
117
+ $ targetLevel = $ configuration ['target_level ' ] ? $ clamp ((float ) $ configuration ['target_level ' ]) : null ;
118
+ /** @var list<string> $cloverFiles */
119
+ $ cloverFiles = (array ) $ configuration ['clover_file ' ];
120
+ /** @var CloverCoverage::MERGE_STRATEGY_* $mergeStrategy */
121
+ $ mergeStrategy = (string ) $ configuration ['merge_strategy ' ];
122
+
123
+ if (!count ($ cloverFiles )) {
124
+ return TaskResult::createFailed ($ this , $ context , 'No clover file(s) provided ' );
89
125
}
90
126
91
- if (! $ percentage ) {
127
+ if ($ minimumLevel === 0.0 ) {
92
128
return TaskResult::createFailed (
93
129
$ this ,
94
130
$ context ,
95
- 'An integer checked percentage must be given as second parameter '
131
+ 'You must provide a positive minimum level between 1-100 for code coverage. '
96
132
);
97
133
}
98
134
99
- $ xml = new SimpleXMLElement ($ this ->filesystem ->readFromFileInfo (new SplFileInfo ($ cloverFile )));
100
- $ totalElements = (int ) current ($ xml ->xpath ('/coverage/project/metrics/@elements ' ) ?? []);
101
- $ checkedElements = (int ) current ($ xml ->xpath ('/coverage/project/metrics/@coveredelements ' ) ?? []);
135
+ try {
136
+ [
137
+ 'totalElements ' => $ totalElements ,
138
+ 'checkedElements ' => $ checkedElements
139
+ ] = $ this ->parseTotals ($ cloverFiles , $ mergeStrategy );
140
+ } catch (FileNotFoundException $ exception ) {
141
+ return TaskResult::createFailed ($ this , $ context , $ exception ->getMessage ());
142
+ }
102
143
103
144
if (0 === $ totalElements ) {
104
145
return TaskResult::createSkipped ($ this , $ context );
105
146
}
106
147
107
148
$ coverage = round (($ checkedElements / $ totalElements ) * 100 , 2 );
108
149
109
- if ($ coverage < $ percentage ) {
150
+ if ($ coverage < $ minimumLevel ) {
110
151
$ message = sprintf (
111
152
'Code coverage is %1$d%%, which is below the accepted %2$d%% ' .PHP_EOL ,
112
153
$ coverage ,
113
- $ percentage
154
+ $ minimumLevel
114
155
);
115
156
116
157
return TaskResult::createFailed ($ this , $ context , $ message );
117
158
}
118
159
160
+ if ($ targetLevel !== null && $ coverage < $ targetLevel ) {
161
+ $ message = sprintf (
162
+ 'Code coverage is %1$d%%, which is below the target %2$d%% ' .PHP_EOL ,
163
+ $ coverage ,
164
+ $ targetLevel
165
+ );
166
+
167
+ return TaskResult::createNonBlockingFailed ($ this , $ context , $ message );
168
+ }
169
+
119
170
return TaskResult::createPassed ($ this , $ context );
120
171
}
172
+
173
+ /**
174
+ * @param list<string> $coverageFiles
175
+ * @param CloverCoverage::MERGE_STRATEGY_* $mergeStrategy
176
+ * @return array{'totalElements': int, 'checkedElements': int}
177
+ *
178
+ * @throws FileNotFoundException
179
+ */
180
+ private function parseTotals (array $ coverageFiles , string $ mergeStrategy ): array
181
+ {
182
+ $ result = [
183
+ 'totalElements ' => 0 ,
184
+ 'checkedElements ' => 0 ,
185
+ ];
186
+
187
+ foreach ($ coverageFiles as $ file ) {
188
+ if (!$ this ->filesystem ->exists ($ file )) {
189
+ throw new FileNotFoundException ($ file );
190
+ }
191
+
192
+ $ xml = new SimpleXMLElement ($ this ->filesystem ->readFromFileInfo (new SplFileInfo ($ file )));
193
+ $ totalElements = (int ) current ($ xml ->xpath ('/coverage/project/metrics/@elements ' ) ?? []);
194
+ $ checkedElements = (int ) current ($ xml ->xpath ('/coverage/project/metrics/@coveredelements ' ) ?? []);
195
+
196
+ $ result = [
197
+ 'totalElements ' => match ($ mergeStrategy ) {
198
+ self ::MERGE_STRATEGY_COMBINE => $ result ['totalElements ' ] + $ totalElements ,
199
+ self ::MERGE_STRATEGY_MERGE => $ result ['totalElements ' ] ?: $ totalElements ,
200
+ },
201
+ 'checkedElements ' => $ result ['checkedElements ' ] + $ checkedElements
202
+ ];
203
+ }
204
+
205
+ return $ result ;
206
+ }
121
207
}
0 commit comments