|
| 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