Skip to content

Commit 0391daf

Browse files
rdiachenkoromani
authored andcommitted
Issue #106: enforce pitest
1 parent a0ae57d commit 0391daf

File tree

5 files changed

+1630
-0
lines changed

5 files changed

+1630
-0
lines changed

.ci/pitest-survival-check.groovy

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
import groovy.io.FileType
2+
import groovy.transform.EqualsAndHashCode
3+
import groovy.transform.Immutable
4+
import groovy.xml.XmlUtil
5+
6+
int exitCode = checkPitestReport()
7+
System.exit(exitCode)
8+
9+
/**
10+
* Check the generated pitest report. Parse the surviving and suppressed mutations and compare
11+
* them.
12+
*
13+
* @return {@code 0} if pitest report is as expected, {@code 1} otherwise
14+
*/
15+
private static int checkPitestReport() {
16+
final XmlParser xmlParser = new XmlParser()
17+
File mutationReportFile = null
18+
final String suppressedMutationFileUri = ".${File.separator}config${File.separator}" +
19+
"pitest-suppressions.xml"
20+
21+
final File pitReports =
22+
new File(".${File.separator}target${File.separator}pit-reports")
23+
24+
if (!pitReports.exists()) {
25+
throw new IllegalStateException(
26+
"Pitest report directory does not exist, generate pitest report first")
27+
}
28+
29+
pitReports.eachFileRecurse(FileType.FILES) {
30+
if (it.name == 'mutations.xml') {
31+
mutationReportFile = it
32+
}
33+
}
34+
final Node mutationReportNode = xmlParser.parse(mutationReportFile)
35+
final Set<Mutation> survivingMutations = getSurvivingMutations(mutationReportNode)
36+
37+
final File suppressionFile = new File(suppressedMutationFileUri)
38+
Set<Mutation> suppressedMutations = new TreeSet<>()
39+
if (suppressionFile.exists()) {
40+
final Node suppressedMutationNode = xmlParser.parse(suppressedMutationFileUri)
41+
suppressedMutations = getSuppressedMutations(suppressedMutationNode)
42+
}
43+
44+
if (survivingMutations.isEmpty()) {
45+
if (suppressionFile.exists()) {
46+
suppressionFile.delete()
47+
}
48+
}
49+
else {
50+
final StringBuilder suppressionFileContent = new StringBuilder(1024)
51+
suppressionFileContent.append(
52+
'<?xml version="1.0" encoding="UTF-8"?>\n<suppressedMutations>\n')
53+
54+
survivingMutations.each {
55+
suppressionFileContent.append(it.toXmlString())
56+
}
57+
suppressionFileContent.append('</suppressedMutations>\n')
58+
59+
if (!suppressionFile.exists()) {
60+
suppressionFile.createNewFile()
61+
}
62+
suppressionFile.write(suppressionFileContent.toString())
63+
}
64+
65+
return printComparisonToConsole(survivingMutations, suppressedMutations)
66+
}
67+
68+
/**
69+
* Get the surviving mutations. All child nodes of the main {@code mutations} node
70+
* are parsed.
71+
*
72+
* @param mainNode the main {@code mutations} node
73+
* @return A set of surviving mutations
74+
*/
75+
private static Set<Mutation> getSurvivingMutations(Node mainNode) {
76+
77+
final List<Node> children = mainNode.children()
78+
final Set<Mutation> survivingMutations = new TreeSet<>()
79+
80+
children.each { node ->
81+
final Node mutationNode = node as Node
82+
83+
final String mutationStatus = mutationNode.attribute("status")
84+
85+
if (mutationStatus == "SURVIVED" || mutationStatus == "NO_COVERAGE") {
86+
survivingMutations.add(getMutation(mutationNode))
87+
}
88+
}
89+
return survivingMutations
90+
}
91+
92+
/**
93+
* Get the suppressed mutations. All child nodes of the main {@code suppressedMutations} node
94+
* are parsed.
95+
*
96+
* @param mainNode the main {@code suppressedMutations} node
97+
* @return A set of suppressed mutations
98+
*/
99+
private static Set<Mutation> getSuppressedMutations(Node mainNode) {
100+
final List<Node> children = mainNode.children()
101+
final Set<Mutation> suppressedMutations = new TreeSet<>()
102+
103+
children.each { node ->
104+
final Node mutationNode = node as Node
105+
suppressedMutations.add(getMutation(mutationNode))
106+
}
107+
return suppressedMutations
108+
}
109+
110+
/**
111+
* Construct the {@link Mutation} object from the {@code mutation} XML node.
112+
* The {@code mutations.xml} file is parsed to get the {@code mutationNode}.
113+
*
114+
* @param mutationNode the {@code mutation} XML node
115+
* @return {@link Mutation} object represented by the {@code mutation} XML node
116+
*/
117+
private static Mutation getMutation(Node mutationNode) {
118+
final List childNodes = mutationNode.children()
119+
120+
String sourceFile = null
121+
String mutatedClass = null
122+
String mutatedMethod = null
123+
String mutator = null
124+
String lineContent = null
125+
String description = null
126+
String mutationClassPackage = null
127+
int lineNumber = 0
128+
childNodes.each {
129+
final Node childNode = it as Node
130+
final String text = childNode.name()
131+
132+
final String childNodeText = XmlUtil.escapeXml(childNode.text())
133+
switch (text) {
134+
case "sourceFile":
135+
sourceFile = childNodeText
136+
break
137+
case "mutatedClass":
138+
mutatedClass = childNodeText
139+
mutationClassPackage = mutatedClass.split("[A-Z]")[0]
140+
break
141+
case "mutatedMethod":
142+
mutatedMethod = childNodeText
143+
break
144+
case "mutator":
145+
mutator = childNodeText
146+
break
147+
case "description":
148+
description = childNodeText
149+
break
150+
case "lineNumber":
151+
lineNumber = Integer.parseInt(childNodeText)
152+
break
153+
case "lineContent":
154+
lineContent = childNodeText
155+
break
156+
}
157+
}
158+
if (lineContent == null) {
159+
final String mutationFileName = mutationClassPackage + sourceFile
160+
final String startingPath =
161+
".${File.separator}src${File.separator}main${File.separator}java${File.separator}"
162+
final String javaExtension = ".java"
163+
final String mutationFilePath = startingPath + mutationFileName
164+
.substring(0, mutationFileName.length() - javaExtension.length())
165+
.replace(".", File.separator) + javaExtension
166+
167+
final File file = new File(mutationFilePath)
168+
lineContent = XmlUtil.escapeXml(file.readLines().get(lineNumber - 1).trim())
169+
}
170+
if (lineNumber == 0) {
171+
lineNumber = -1
172+
}
173+
174+
final String unstableAttributeValue = mutationNode.attribute("unstable")
175+
final boolean isUnstable = Boolean.parseBoolean(unstableAttributeValue)
176+
177+
return new Mutation(sourceFile, mutatedClass, mutatedMethod, mutator, description,
178+
lineContent, lineNumber, isUnstable)
179+
}
180+
181+
/**
182+
* Compare surviving and suppressed mutations. The comparison passes successfully (i.e. returns 0)
183+
* when:
184+
* <ol>
185+
* <li>Surviving and suppressed mutations are equal.</li>
186+
* <li>There are extra suppressed mutations but they are unstable
187+
* i.e. {@code unstable="true"}.</li>
188+
* </ol>
189+
* The comparison fails (i.e. returns 1) when:
190+
* <ol>
191+
* <li>Surviving mutations are not present in the suppressed list.</li>
192+
* <li>There are mutations in the suppression list that are not there is surviving list.</li>
193+
* </ol>
194+
*
195+
* @param survivingMutations A set of surviving mutations
196+
* @param suppressedMutations A set of suppressed mutations
197+
* @return {@code 0} if comparison passes successfully
198+
*/
199+
private static int printComparisonToConsole(Set<Mutation> survivingMutations,
200+
Set<Mutation> suppressedMutations) {
201+
final Set<Mutation> survivingUnsuppressedMutations =
202+
setDifference(survivingMutations, suppressedMutations)
203+
final Set<Mutation> extraSuppressions =
204+
setDifference(suppressedMutations, survivingMutations)
205+
206+
final int exitCode
207+
if (survivingMutations == suppressedMutations) {
208+
exitCode = 0
209+
println 'No new surviving mutation(s) found.'
210+
}
211+
else if (survivingUnsuppressedMutations.isEmpty()
212+
&& hasOnlyUnstableMutations(extraSuppressions)) {
213+
exitCode = 0
214+
println 'No new surviving mutation(s) found.'
215+
}
216+
else {
217+
if (!survivingUnsuppressedMutations.isEmpty()) {
218+
println "New surviving mutation(s) found:"
219+
survivingUnsuppressedMutations.each {
220+
println it
221+
}
222+
}
223+
if (!extraSuppressions.isEmpty()
224+
&& extraSuppressions.any { !it.isUnstable() }) {
225+
println "\nUnnecessary suppressed mutation(s) found and should be removed:"
226+
extraSuppressions.each {
227+
if (!it.isUnstable()) {
228+
println it
229+
}
230+
}
231+
}
232+
exitCode = 1
233+
}
234+
return exitCode
235+
}
236+
237+
/**
238+
* Whether a set has only unstable mutations.
239+
*
240+
* @param mutations A set of mutations
241+
* @return {@code true} if a set has only unstable mutations
242+
*/
243+
private static boolean hasOnlyUnstableMutations(Set<Mutation> mutations) {
244+
return mutations.every { it.isUnstable() }
245+
}
246+
247+
/**
248+
* Determine the difference between 2 sets. The result is {@code setOne - setTwo}.
249+
*
250+
* @param setOne The first set in the difference
251+
* @param setTwo The second set in the difference
252+
* @return {@code setOne - setTwo}
253+
*/
254+
private static Set<Mutation> setDifference(final Set<Mutation> setOne,
255+
final Set<Mutation> setTwo) {
256+
final Set<Mutation> result = new TreeSet<>(setOne)
257+
result.removeIf { mutation -> setTwo.contains(mutation) }
258+
return result
259+
}
260+
261+
/**
262+
* A class to represent the XML {@code mutation} node.
263+
*/
264+
@EqualsAndHashCode(excludes = ["lineNumber", "unstable"])
265+
@Immutable
266+
class Mutation implements Comparable<Mutation> {
267+
268+
/**
269+
* Mutation nodes present in suppressions file do not have a {@code lineNumber}.
270+
* The {@code lineNumber} is set to {@code -1} for such mutations.
271+
*/
272+
private static final int LINE_NUMBER_NOT_PRESENT_VALUE = -1
273+
274+
String sourceFile
275+
String mutatedClass
276+
String mutatedMethod
277+
String mutator
278+
String description
279+
String lineContent
280+
int lineNumber
281+
boolean unstable
282+
283+
@Override
284+
String toString() {
285+
String toString = """
286+
Source File: "${getSourceFile()}"
287+
Class: "${getMutatedClass()}"
288+
Method: "${getMutatedMethod()}"
289+
Line Contents: "${getLineContent()}"
290+
Mutator: "${getMutator()}"
291+
Description: "${getDescription()}"
292+
""".stripIndent()
293+
if (getLineNumber() != LINE_NUMBER_NOT_PRESENT_VALUE) {
294+
toString += 'Line Number: ' + getLineNumber()
295+
}
296+
return toString
297+
}
298+
299+
@Override
300+
int compareTo(Mutation other) {
301+
int i = getSourceFile() <=> other.getSourceFile()
302+
if (i != 0) {
303+
return i
304+
}
305+
306+
i = getMutatedClass() <=> other.getMutatedClass()
307+
if (i != 0) {
308+
return i
309+
}
310+
311+
i = getMutatedMethod() <=> other.getMutatedMethod()
312+
if (i != 0) {
313+
return i
314+
}
315+
316+
i = getLineContent() <=> other.getLineContent()
317+
if (i != 0) {
318+
return i
319+
}
320+
321+
i = getMutator() <=> other.getMutator()
322+
if (i != 0) {
323+
return i
324+
}
325+
326+
return getDescription() <=> other.getDescription()
327+
}
328+
329+
/**
330+
* XML format of the mutation.
331+
*
332+
* @return XML format of the mutation
333+
*/
334+
String toXmlString() {
335+
return """
336+
<mutation unstable="${isUnstable()}">
337+
<sourceFile>${getSourceFile()}</sourceFile>
338+
<mutatedClass>${getMutatedClass()}</mutatedClass>
339+
<mutatedMethod>${getMutatedMethod()}</mutatedMethod>
340+
<mutator>${getMutator()}</mutator>
341+
<description>${getDescription()}</description>
342+
<lineContent>${getLineContent()}</lineContent>
343+
</mutation>
344+
""".stripIndent(10)
345+
}
346+
347+
}

.ci/pitest.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/bash
2+
# Attention, there is no "-x" to avoid problems on CircleCI
3+
set -e
4+
5+
echo "Generation of pitest report:"
6+
echo "./mvnw -e --no-transfer-progress -Ppitest clean test-compile org.pitest:pitest-maven:mutationCoverage"
7+
set +e
8+
./mvnw -e --no-transfer-progress -Ppitest clean test-compile org.pitest:pitest-maven:mutationCoverage
9+
EXIT_CODE=$?
10+
set -e
11+
echo "Execution of comparison of suppressed mutations survivals and current survivals:"
12+
echo "groovy .ci/pitest-survival-check.groovy"
13+
groovy .ci/pitest-survival-check.groovy
14+
exit $EXIT_CODE

0 commit comments

Comments
 (0)