Skip to content

Commit 4fca97b

Browse files
1.23.0: write JUnit XML using DOM API
Remove the problematic Jackson dependency: it had to be kept compatible with MPS but older versions contained vulnerabilities that would fail customers' security checks.
1 parent 928c2b5 commit 4fca97b

File tree

10 files changed

+587
-370
lines changed

10 files changed

+587
-370
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
66
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## 1.23.0
9+
10+
- `modelcheck`: Dependency on Jackson XML removed, XML is now serialized using the DOM API. As a result, the format has
11+
slightly changed (different attribute order, inclusion of the XML prolog) but remains compatible.
12+
813
## 1.22.2
914

1015
- Upgrade to project-loader 3.0.2 to get the latest bug fix.

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
org.gradle.parallel=true
22

3-
version.backend=1.22.2
3+
version.backend=1.23.0
44
version.project-loader=3.0.2
55

66
# A comma-separated list of MPS releases or prereleases to test against.

modelcheck/build.gradle.kts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ mpsZips {
2121
}
2222

2323
dependencies {
24-
implementation(kotlin("test"))
25-
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.13.+")
26-
27-
testImplementation(kotlin("test"))
2824
testImplementation("org.xmlunit:xmlunit-core:2.6.+")
25+
26+
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
27+
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
28+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
2929
}
3030

3131
tasks.test {
Lines changed: 9 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package de.itemis.mps.gradle.junit
22

3-
import com.fasterxml.jackson.annotation.JsonInclude
4-
import com.fasterxml.jackson.dataformat.xml.annotation.*
5-
6-
public data class Skipped(@field:JacksonXmlText() val content: String)
3+
public data class Skipped(val content: String)
74

85
/**
96
* Indicates that the test errored. An errored test is one that had an unanticipated problem. e.g., an unchecked throwable; or a problem with the implementation of the test. Contains as a text node relevant data for the error, e.g., a stack trace
@@ -12,14 +9,13 @@ public data class Error(
129
/**
1310
* The error message. e.g., if a java exception is thrown, the return value of getMessage()
1411
*/
15-
@field:JacksonXmlProperty(isAttribute = true)
1612
val message: String,
1713
/**
1814
* The type of error that occured. e.g., if a java execption is thrown the full class name of the exception.
1915
*/
20-
@field:JacksonXmlProperty(isAttribute = true)
2116
val type: String,
22-
@field:JacksonXmlText() val content: String? = null)
17+
val content: String? = null
18+
)
2319

2420
/**
2521
* Indicates that the test failed. A failure is a test which the code has explicitly failed by using the mechanisms for that purpose. e.g., via an assertEquals. Contains as a text node relevant data for the failure, e.g., a stack trace
@@ -28,137 +24,94 @@ public data class Failure(
2824
/**
2925
* The message specified in the assert
3026
*/
31-
@field:JacksonXmlProperty(isAttribute = true)
3227
val message: String,
3328
/**
3429
* The type of the assert.
3530
*/
36-
@field:JacksonXmlProperty(isAttribute = true)
3731
val type: String,
38-
@field:JacksonXmlText() val content: String? = null)
32+
val content: String? = null
33+
)
3934

40-
public data class SystemOut(@field:JacksonXmlText()
41-
val content: String)
35+
public data class SystemOut(val content: String)
4236

43-
public data class SystemErr(@field:JacksonXmlText()
44-
val content: String)
37+
public data class SystemErr(val content: String)
4538

4639

4740
public data class Testcase(
4841
/**
4942
* Name of the test method
5043
*/
51-
@field:JacksonXmlProperty(isAttribute = true)
5244
val name: String,
5345
/**
5446
* Full class name for the class the test method is in.
5547
*/
56-
@field:JacksonXmlProperty(isAttribute = true)
5748
val classname: String,
5849
/**
5950
* Time taken (in seconds) to execute the test
6051
*/
61-
@field:JacksonXmlProperty(isAttribute = true)
6252
val time: Int,
63-
@field:JacksonXmlElementWrapper(useWrapping = false)
64-
@field:JacksonXmlProperty(localName = "skipped")
65-
@field:JsonInclude(JsonInclude.Include.NON_NULL)
6653
val skipped: Skipped? = null,
67-
@field:JacksonXmlElementWrapper(useWrapping = false)
68-
@field:JacksonXmlProperty(localName = "error")
69-
@field:JsonInclude(JsonInclude.Include.NON_NULL)
7054
val error: Error? = null,
71-
@field:JacksonXmlElementWrapper(useWrapping = false)
72-
@field:JacksonXmlProperty(localName = "failure")
73-
@field:JsonInclude(JsonInclude.Include.NON_NULL)
7455
val failure: Failure? = null
7556
)
7657

77-
@JacksonXmlRootElement(localName = "testsuite")
7858
public data class Testsuite(
7959
/**
8060
* Full class name of the test for non-aggregated testsuite documents. Class name without the package for aggregated testsuites documents
8161
*/
82-
@field:JacksonXmlProperty(isAttribute = true)
8362
val name: String,
8463
/**
8564
* The total number of tests in the suite
8665
*/
87-
@field:JacksonXmlProperty(isAttribute = true)
8866
val tests: Int,
8967
/**
9068
* The total number of tests in the suite that errored. An errored test is one that had an unanticipated problem. e.g., an unchecked throwable; or a problem with the implementation of the test.
9169
*/
92-
@field:JacksonXmlProperty(isAttribute = true)
9370
val errors: Int = 0,
9471
/**
9572
* The total number of tests in the suite that failed. A failure is a test which the code has explicitly failed by using the mechanisms for that purpose. e.g., via an assertEquals
9673
*/
97-
@field:JacksonXmlProperty(isAttribute = true)
9874
val failures: Int = 0,
9975
/**
10076
* Only required if contained in a testsuites list
10177
* Starts at '0' for the first testsuite and is incremented by 1 for each following testsuite
10278
*/
103-
@field:JacksonXmlProperty(isAttribute = true)
10479
val id: Int? = null,
10580
/**
10681
* Derived from testsuite/@name in the non-aggregated documents
10782
*/
108-
@field:JacksonXmlProperty(localName = "package", isAttribute = true)
10983
val pkg: String? = null,
11084
/**
11185
* The total number of ignored or skipped tests in the suite.
11286
*/
113-
@field:JacksonXmlProperty(isAttribute = true)
11487
val skipped: Int? = null,
11588
/**
11689
* Time taken (in seconds) to execute the tests in the suite
11790
*/
118-
@field:JacksonXmlProperty(isAttribute = true)
11991
val time: Int = 0,
12092
/**
12193
* when the test was executed. Timezone may not be specified.
12294
*/
123-
@field:JacksonXmlProperty(isAttribute = true)
12495
val timestamp: String,
12596
/**
12697
* Host on which the tests were executed. 'localhost' should be used if the hostname cannot be determined.
12798
*/
128-
@field:JacksonXmlProperty(isAttribute = true)
12999
val hostname: String = "localhost",
130100
/**
131101
* Properties (e.g., environment settings) set during test execution
132102
*/
133-
@field:JacksonXmlElementWrapper(useWrapping = true)
134-
@field:JacksonXmlProperty(localName = "properties")
135103
val properties: List<Property> = emptyList(),
136-
@field:JacksonXmlElementWrapper(useWrapping = false)
137-
@field:JacksonXmlProperty(localName = "testcase")
138104
val testcases: List<Testcase>,
139105
/**
140106
* Data that was written to standard out while the test was executed
141107
*/
142-
@field:JacksonXmlElementWrapper(useWrapping = false)
143-
@field:JacksonXmlProperty(localName = "system-out")
144108
val systemOut: SystemOut = SystemOut(""),
145109
/**
146110
* Data that was written to standard error while the test was executed
147111
*/
148-
@field:JacksonXmlElementWrapper(useWrapping = false)
149-
@field:JacksonXmlProperty(localName = "system-err")
150112
val systemError: SystemErr = SystemErr("")
151113
)
152114

153-
public data class Property(
154-
@field:JacksonXmlProperty(isAttribute = true)
155-
val name: String,
156-
@field:JacksonXmlProperty(isAttribute = true)
157-
val value: String)
158-
159-
@JacksonXmlRootElement(localName = "testsuites")
160-
public data class Testsuites(@field:JacksonXmlElementWrapper(useWrapping = false, localName = "testsuite")
161-
@field:JacksonXmlProperty(localName = "testsuite")
162-
val testsuites: List<Testsuite>
163-
)
115+
public data class Property(val name: String, val value: String)
164116

117+
public data class Testsuites(val testsuites: List<Testsuite>)
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package de.itemis.mps.gradle.junit
2+
3+
import org.w3c.dom.Document
4+
import org.w3c.dom.Element
5+
import java.io.ByteArrayOutputStream
6+
import javax.xml.parsers.DocumentBuilderFactory
7+
import javax.xml.transform.OutputKeys
8+
import javax.xml.transform.Transformer
9+
import javax.xml.transform.TransformerFactory
10+
import javax.xml.transform.dom.DOMSource
11+
import javax.xml.transform.stream.StreamResult
12+
13+
/**
14+
* Serializes JUnit test classes to XML format
15+
*/
16+
object JUnitXmlSerializer {
17+
18+
fun toDocument(value: Any): Document =
19+
when (value) {
20+
is Testsuite -> toDocument(value)
21+
is Testsuites -> toDocument(value)
22+
else -> throw IllegalArgumentException("Unsupported argument type: " + value.javaClass)
23+
}
24+
25+
fun toDocument(testsuites: Testsuites): Document {
26+
val doc = createDocument()
27+
val rootElement = doc.createElement("testsuites")
28+
doc.appendChild(rootElement)
29+
30+
testsuites.testsuites.forEach { testsuite ->
31+
addTestsuite(doc, rootElement, testsuite)
32+
}
33+
34+
return doc
35+
}
36+
37+
fun toDocument(testsuite: Testsuite): Document {
38+
val doc = createDocument()
39+
val rootElement = doc.createElement("testsuite")
40+
doc.appendChild(rootElement)
41+
42+
addTestsuiteAttributes(rootElement, testsuite)
43+
addTestsuiteContent(doc, rootElement, testsuite)
44+
45+
return doc
46+
}
47+
48+
private fun createDocument(): Document {
49+
val factory = DocumentBuilderFactory.newInstance()
50+
val builder = factory.newDocumentBuilder()
51+
return builder.newDocument()
52+
}
53+
54+
private fun addTestsuite(doc: Document, parent: Element, testsuite: Testsuite) {
55+
val testsuiteElement = doc.createElement("testsuite")
56+
parent.appendChild(testsuiteElement)
57+
58+
addTestsuiteAttributes(testsuiteElement, testsuite)
59+
addTestsuiteContent(doc, testsuiteElement, testsuite)
60+
}
61+
62+
private fun addTestsuiteAttributes(element: Element, testsuite: Testsuite) {
63+
element.setAttribute("name", testsuite.name)
64+
element.setAttribute("tests", testsuite.tests.toString())
65+
element.setAttribute("errors", testsuite.errors.toString())
66+
element.setAttribute("failures", testsuite.failures.toString())
67+
element.setAttribute("time", testsuite.time.toString())
68+
element.setAttribute("timestamp", testsuite.timestamp)
69+
element.setAttribute("hostname", testsuite.hostname)
70+
71+
testsuite.id?.let { element.setAttribute("id", it.toString()) }
72+
testsuite.pkg?.let { element.setAttribute("package", it) }
73+
testsuite.skipped?.let { element.setAttribute("skipped", it.toString()) }
74+
}
75+
76+
private fun addTestsuiteContent(doc: Document, element: Element, testsuite: Testsuite) {
77+
// Add properties
78+
val propertiesElement = doc.createElement("properties")
79+
element.appendChild(propertiesElement)
80+
81+
testsuite.properties.forEach { property ->
82+
val propertyElement = doc.createElement("property")
83+
propertyElement.setAttribute("name", property.name)
84+
propertyElement.setAttribute("value", property.value)
85+
propertiesElement.appendChild(propertyElement)
86+
}
87+
88+
// Add testcases
89+
testsuite.testcases.forEach { testcase ->
90+
addTestcase(doc, element, testcase)
91+
}
92+
93+
// Add system-out
94+
val systemOutElement = doc.createElement("system-out")
95+
systemOutElement.textContent = testsuite.systemOut.content
96+
element.appendChild(systemOutElement)
97+
98+
// Add system-err
99+
val systemErrElement = doc.createElement("system-err")
100+
systemErrElement.textContent = testsuite.systemError.content
101+
element.appendChild(systemErrElement)
102+
}
103+
104+
private fun addTestcase(doc: Document, parent: Element, testcase: Testcase) {
105+
val testcaseElement = doc.createElement("testcase")
106+
parent.appendChild(testcaseElement)
107+
108+
// Add attributes
109+
testcaseElement.setAttribute("name", testcase.name)
110+
testcaseElement.setAttribute("classname", testcase.classname)
111+
testcaseElement.setAttribute("time", testcase.time.toString())
112+
113+
// Add child elements if present
114+
testcase.skipped?.let {
115+
val skippedElement = doc.createElement("skipped")
116+
skippedElement.textContent = it.content
117+
testcaseElement.appendChild(skippedElement)
118+
}
119+
120+
testcase.error?.let {
121+
val errorElement = doc.createElement("error")
122+
errorElement.setAttribute("message", it.message)
123+
errorElement.setAttribute("type", it.type)
124+
it.content?.let { content -> errorElement.textContent = content }
125+
testcaseElement.appendChild(errorElement)
126+
}
127+
128+
testcase.failure?.let {
129+
val failureElement = doc.createElement("failure")
130+
failureElement.setAttribute("message", it.message)
131+
failureElement.setAttribute("type", it.type)
132+
it.content?.let { content -> failureElement.textContent = content }
133+
testcaseElement.appendChild(failureElement)
134+
}
135+
}
136+
137+
fun documentToByteArray(doc: Document): ByteArray {
138+
val outputStream = ByteArrayOutputStream()
139+
newTransformer().transform(DOMSource(doc), StreamResult(outputStream))
140+
return outputStream.toByteArray()
141+
}
142+
143+
private fun newTransformer(): Transformer {
144+
val transformerFactory = TransformerFactory.newInstance()
145+
val transformer = transformerFactory.newTransformer()
146+
transformer.setOutputProperty(OutputKeys.INDENT, "yes")
147+
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8")
148+
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2")
149+
return transformer
150+
}
151+
}

0 commit comments

Comments
 (0)