Skip to content

Commit 80ca1a3

Browse files
authored
Merge pull request #2543 from Gedochao/maintenance/test-reports
Generate test reports on the CI
2 parents 79e75dc + 77bf110 commit 80ca1a3

File tree

3 files changed

+321
-0
lines changed

3 files changed

+321
-0
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env -S scala-cli shebang
2+
//> using scala 3
3+
//> using toolkit 0.2.1
4+
//> using dep org.scala-lang.modules::scala-xml:2.2.0
5+
// adapted from https://github.com/vic/mill-test-junit-report
6+
import java.io.File
7+
import scala.collection.mutable.ArrayBuffer
8+
import scala.annotation.tailrec
9+
import java.nio.file.Paths
10+
11+
case class Trace(declaringClass: String, methodName: String, fileName: String, lineNumber: Int) {
12+
override def toString: String = s"$declaringClass.$methodName($fileName:$lineNumber)"
13+
}
14+
15+
case class Failure(name: String, message: String, trace: Seq[Trace])
16+
17+
case class Test(
18+
fullyQualifiedName: String,
19+
selector: String,
20+
duration: Double,
21+
failure: Option[Failure]
22+
)
23+
24+
@tailrec
25+
def findFiles(paths: Seq[os.Path], result: Seq[os.Path] = Nil): Seq[os.Path] =
26+
paths match
27+
case Nil => result
28+
case head :: tail =>
29+
val newFiles =
30+
if head.last == "test.dest" && os.isDir(head) then
31+
os.list(head).filter(f => f.last == "out.json").toList
32+
else Seq.empty
33+
val newDirs = os.list(head).filter(p => os.isDir(p)).toList
34+
findFiles(tail ++ newDirs, result ++ newFiles)
35+
36+
extension (s: String)
37+
def toNormalisedPath: os.Path = if Paths.get(s).isAbsolute then os.Path(s) else os.Path(s, os.pwd)
38+
39+
def printUsageMessage(): Unit = println("Usage: generate-junit-reports <id> <name> <into> <path>")
40+
if args.length != 4 then {
41+
println(s"Error: provided too few arguments: ${args.length}")
42+
printUsageMessage()
43+
System.exit(1)
44+
}
45+
46+
val id: String = args(0)
47+
val name: String = args(1)
48+
49+
if new File(args(2)).exists() then {
50+
println(s"Error: specified output path already exists: ${args(2)}")
51+
System.exit(1)
52+
}
53+
val into = args(2).toNormalisedPath
54+
55+
val pathArg = args(3)
56+
val rootPath: os.Path =
57+
if Paths.get(pathArg).isAbsolute then os.Path(pathArg) else os.Path(pathArg, os.pwd)
58+
if !os.isDir(rootPath) then {
59+
println(s"The path provided is not a directory: $pathArg")
60+
System.exit(1)
61+
}
62+
val reports: Seq[os.Path] = findFiles(Seq(rootPath))
63+
println(s"Found ${reports.length} mill json reports:")
64+
println(reports.mkString("\n"))
65+
println("Reading reports...")
66+
val tests: Seq[Test] = reports.map(x => ujson.read(x.toNIO)).flatMap { json =>
67+
json(1).value.asInstanceOf[ArrayBuffer[ujson.Obj]].map { test =>
68+
Test(
69+
fullyQualifiedName = test("fullyQualifiedName").str,
70+
selector = test("selector").str,
71+
duration = test("duration").num / 1000.0,
72+
failure = test("status").str match {
73+
case "Failure" => Some(Failure(
74+
name = test("exceptionName")(0).str,
75+
message = test("exceptionMsg")(0).str,
76+
trace = test("exceptionTrace")(0).arr.map { st =>
77+
val declaringClass = st("declaringClass").str
78+
val methodName = st("methodName").str
79+
val fileName = st("fileName")(0).str
80+
val lineNumber = st("lineNumber").num.toInt
81+
Trace(declaringClass, methodName, fileName, lineNumber)
82+
}.toList
83+
))
84+
case _ => None
85+
}
86+
)
87+
}
88+
}
89+
println(s"Found ${tests.length} tests.")
90+
println("Generating JUnit XML report...")
91+
val suites = tests.groupBy(_.fullyQualifiedName).map { case (suit, tests) =>
92+
val testcases = tests.map { test =>
93+
<testcase id={test.selector} classname={test.fullyQualifiedName} name={
94+
test.selector.substring(test.fullyQualifiedName.length)
95+
} time={test.duration.toString}>
96+
{
97+
test.failure.map { failure =>
98+
<failure message={failure.message} type="ERROR">
99+
ERROR: {failure.message}
100+
Category: {failure.name}
101+
File: {failure.trace(1).fileName}
102+
Line: {failure.trace(1).lineNumber}
103+
</failure>
104+
}.orNull
105+
}
106+
{
107+
test.failure.map { failure =>
108+
<system-err>{
109+
failure.trace.mkString(s"${failure.name}: ${failure.message}", "\n at ", "")
110+
}</system-err>
111+
}.orNull
112+
}
113+
</testcase>
114+
}
115+
116+
<testsuite id={suit} name={suit} tests={tests.length.toString} failures={
117+
tests.count(_.failure.isDefined).toString
118+
} time={tests.map(_.duration).sum.toString}>
119+
{testcases}
120+
</testsuite>
121+
}
122+
123+
val n = <testsuites id={id} name={name} tests={tests.length.toString} failures={
124+
tests.count(_.failure.isDefined).toString
125+
} time={tests.map(_.duration).sum.toString}>
126+
{suites}
127+
</testsuites>
128+
val prettyXmlPrinter = new scala.xml.PrettyPrinter(80, 2)
129+
val xmlToSave = scala.xml.XML.loadString(prettyXmlPrinter.format(n))
130+
scala.xml.XML.save(filename = into.toString(), node = xmlToSave, xmlDecl = true)
131+
println(s"Generated report at: $into")

0 commit comments

Comments
 (0)