Skip to content

Commit 4140954

Browse files
committed
Change internal format from XML to plain text
Change format of `scoverage.coverage.xml` file (containing information about instrumented statements) from XML to plain test (and file name to `scoverage.coverage`). In Scala version 2.13.0-M4 the dependency `scala-compiler` -> `scala-xml` was removed and since Scalac plugin cannot add its own dependencies, one of the solutions is to resign from XML format. The code generating reports will still use `scala-xml` dependency. It's not a problem because it's executes in the context of the build tool (SBT, Maven or Gradle), not Scalac. The side effect of this change is increased performance because writing and reading plain test files is much faster than XML files, especially when these files are large.
1 parent 716b18e commit 4140954

File tree

4 files changed

+202
-153
lines changed

4 files changed

+202
-153
lines changed

scalac-scoverage-plugin/src/main/scala/scoverage/Constants.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package scoverage
22

33
object Constants {
44
// the file that contains the statement mappings
5-
val CoverageFileName = "scoverage.coverage.xml"
5+
val CoverageFileName = "scoverage.coverage"
66
// the final scoverage report
77
val XMLReportFilename = "scoverage.xml"
88
val XMLReportFilenameWithDebug = "scoverage-debug.xml"
Lines changed: 91 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package scoverage
22

3-
import java.io._
3+
import java.io.{BufferedWriter, File, FileOutputStream, OutputStreamWriter, Writer}
44

5-
import scala.io.Source
6-
import scala.xml.{Utility, XML}
5+
import scala.io.{Codec, Source}
76

87
object Serializer {
98

@@ -12,147 +11,109 @@ object Serializer {
1211

1312
// Write out coverage data to given file.
1413
def serialize(coverage: Coverage, file: File): Unit = {
15-
val writer = new BufferedWriter(new FileWriter(file))
16-
serialize(coverage, writer)
17-
writer.close()
14+
val writer: Writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), Codec.UTF8.name))
15+
try {
16+
serialize(coverage, writer)
17+
}
18+
finally {
19+
writer.flush()
20+
writer.close()
21+
}
1822
}
1923

2024
def serialize(coverage: Coverage, writer: Writer): Unit = {
25+
def writeHeader(writer: Writer): Unit = {
26+
writer.write(s"""# Coverage data, format version: 2.0
27+
|# Statement data:
28+
|# - id
29+
|# - source path
30+
|# - package name
31+
|# - class name
32+
|# - class type (Class, Object or Trait)
33+
|# - full class name
34+
|# - method name
35+
|# - start offset
36+
|# - end offset
37+
|# - line number
38+
|# - symbol name
39+
|# - tree name
40+
|# - is branch
41+
|# - invocations count
42+
|# - is ignored
43+
|# - description (can be multi-line)
44+
|# '\f' sign
45+
|# ------------------------------------------
46+
|""".stripMargin)
47+
}
48+
2149
def writeStatement(stmt: Statement, writer: Writer): Unit = {
22-
writer.write {
23-
val xml = <statement>
24-
<source>
25-
{stmt.source}
26-
</source>
27-
<package>
28-
{stmt.location.packageName}
29-
</package>
30-
<class>
31-
{stmt.location.className}
32-
</class>
33-
<classType>
34-
{stmt.location.classType.toString}
35-
</classType>
36-
<fullClassName>
37-
{stmt.location.fullClassName}
38-
</fullClassName>
39-
<method>
40-
{stmt.location.method}
41-
</method>
42-
<path>
43-
{stmt.location.sourcePath}
44-
</path>
45-
<id>
46-
{stmt.id.toString}
47-
</id>
48-
<start>
49-
{stmt.start.toString}
50-
</start>
51-
<end>
52-
{stmt.end.toString}
53-
</end>
54-
<line>
55-
{stmt.line.toString}
56-
</line>
57-
<description>
58-
{escape(stmt.desc)}
59-
</description>
60-
<symbolName>
61-
{escape(stmt.symbolName)}
62-
</symbolName>
63-
<treeName>
64-
{escape(stmt.treeName)}
65-
</treeName>
66-
<branch>
67-
{stmt.branch.toString}
68-
</branch>
69-
<count>
70-
{stmt.count.toString}
71-
</count>
72-
<ignored>
73-
{stmt.ignored.toString}
74-
</ignored>
75-
</statement>
76-
Utility.trim(xml) + "\n"
77-
}
50+
writer.write(s"""${stmt.id}
51+
|${stmt.location.sourcePath}
52+
|${stmt.location.packageName}
53+
|${stmt.location.className}
54+
|${stmt.location.classType}
55+
|${stmt.location.fullClassName}
56+
|${stmt.location.method}
57+
|${stmt.start}
58+
|${stmt.end}
59+
|${stmt.line}
60+
|${stmt.symbolName}
61+
|${stmt.treeName}
62+
|${stmt.branch}
63+
|${stmt.count}
64+
|${stmt.ignored}
65+
|${stmt.desc}
66+
|\f
67+
|""".stripMargin)
7868
}
79-
writer.write("<statements>\n")
69+
70+
writeHeader(writer)
8071
coverage.statements.foreach(stmt => writeStatement(stmt, writer))
81-
writer.write("</statements>")
8272
}
8373

8474
def coverageFile(dataDir: File): File = coverageFile(dataDir.getAbsolutePath)
8575
def coverageFile(dataDir: String): File = new File(dataDir, Constants.CoverageFileName)
8676

87-
def deserialize(str: String): Coverage = {
88-
val xml = XML.loadString(str)
89-
val statements = xml \ "statement" map (node => {
90-
val source = (node \ "source").text
91-
val count = (node \ "count").text.toInt
92-
val ignored = (node \ "ignored").text.toBoolean
93-
val branch = (node \ "branch").text.toBoolean
94-
val _package = (node \ "package").text
95-
val _class = (node \ "class").text
96-
val fullClassName = (node \ "fullClassName").text
97-
val method = (node \ "method").text
98-
val path = (node \ "path").text
99-
val treeName = (node \ "treeName").text
100-
val symbolName = (node \ "symbolName").text
101-
val id = (node \ "id").text.toInt
102-
val line = (node \ "line").text.toInt
103-
val desc = (node \ "description").text
104-
val start = (node \ "start").text.toInt
105-
val end = (node \ "end").text.toInt
106-
val classType = (node \ "classType").text match {
107-
case "Trait" => ClassType.Trait
108-
case "Object" => ClassType.Object
109-
case _ => ClassType.Class
110-
}
111-
Statement(
112-
Location(_package, _class, fullClassName, classType, method, path),
113-
id,
114-
start,
115-
end,
116-
line,
117-
desc,
118-
symbolName,
119-
treeName, branch, count, ignored)
120-
})
121-
122-
val coverage = Coverage()
123-
for ( statement <- statements )
124-
if (statement.ignored) coverage.addIgnoredStatement(statement)
125-
else coverage.add(statement)
126-
coverage
127-
}
128-
12977
def deserialize(file: File): Coverage = {
130-
val str = Source.fromFile(file).mkString
131-
deserialize(str)
78+
deserialize(Source.fromFile(file)(Codec.UTF8).getLines)
13279
}
13380

134-
/**
135-
* This method ensures that the output String has only
136-
* valid XML unicode characters as specified by the
137-
* XML 1.0 standard. For reference, please see
138-
* <a href="http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char">the
139-
* standard</a>. This method will return an empty
140-
* String if the input is null or empty.
141-
*
142-
* @param in The String whose non-valid characters we want to remove.
143-
* @return The in String, stripped of non-valid characters.
144-
* @see http://blog.mark-mclaren.info/2007/02/invalid-xml-characters-when-valid-utf8_5873.html
145-
*
146-
*/
147-
def escape(in: String): String = {
148-
val out = new StringBuilder()
149-
for ( current <- Option(in).getOrElse("").toCharArray ) {
150-
if ((current == 0x9) || (current == 0xA) || (current == 0xD) ||
151-
((current >= 0x20) && (current <= 0xD7FF)) ||
152-
((current >= 0xE000) && (current <= 0xFFFD)) ||
153-
((current >= 0x10000) && (current <= 0x10FFFF)))
154-
out.append(current)
81+
def deserialize(lines: Iterator[String]): Coverage = {
82+
def toStatement(lines: Iterator[String]): Statement = {
83+
val id: Int = lines.next.toInt
84+
val sourcePath = lines.next
85+
val packageName = lines.next
86+
val className = lines.next
87+
val classType = lines.next
88+
val fullClassName = lines.next
89+
val method = lines.next
90+
val loc = Location(packageName, className, fullClassName, ClassType.fromString(classType), method, sourcePath)
91+
val start: Int = lines.next.toInt
92+
val end: Int = lines.next.toInt
93+
val lineNo: Int = lines.next.toInt
94+
val symbolName: String = lines.next
95+
val treeName: String = lines.next
96+
val branch: Boolean = lines.next.toBoolean
97+
val count: Int = lines.next.toInt
98+
val ignored: Boolean = lines.next.toBoolean
99+
val desc = lines.toList.mkString("\n")
100+
Statement(loc, id, start, end, lineNo, desc, symbolName, treeName, branch, count, ignored)
101+
}
102+
103+
val headerFirstLine = lines.next
104+
require(headerFirstLine == "# Coverage data, format version: 2.0", "Wrong file format")
105+
106+
val linesWithoutHeader = lines.dropWhile(_.startsWith("#"))
107+
val coverage = Coverage()
108+
while (!linesWithoutHeader.isEmpty) {
109+
val oneStatementLines = linesWithoutHeader.takeWhile(_ != "\f")
110+
val statement = toStatement(oneStatementLines)
111+
if (statement.ignored)
112+
coverage.addIgnoredStatement(statement)
113+
else
114+
coverage.add(statement)
155115
}
156-
out.mkString
116+
coverage
157117
}
118+
158119
}

scalac-scoverage-plugin/src/main/scala/scoverage/report/ScoverageXmlWriter.scala

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@ class ScoverageXmlWriter(sourceDirectories: Seq[File], outputDir: File, debug: B
4343
start={stmt.start.toString}
4444
end={stmt.end.toString}
4545
line={stmt.line.toString}
46-
symbol={Serializer.escape(stmt.symbolName)}
47-
tree={Serializer.escape(stmt.treeName)}
46+
symbol={escape(stmt.symbolName)}
47+
tree={escape(stmt.treeName)}
4848
branch={stmt.branch.toString}
4949
invocation-count={stmt.count.toString}
5050
ignored={stmt.ignored.toString}>
51-
{Serializer.escape(stmt.desc)}
51+
{escape(stmt.desc)}
5252
</statement>
5353
case false =>
5454
<statement package={stmt.location.packageName}
@@ -101,5 +101,29 @@ class ScoverageXmlWriter(sourceDirectories: Seq[File], outputDir: File, debug: B
101101
</classes>
102102
</package>
103103
}
104+
/**
105+
* This method ensures that the output String has only
106+
* valid XML unicode characters as specified by the
107+
* XML 1.0 standard. For reference, please see
108+
* <a href="http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char">the
109+
* standard</a>. This method will return an empty
110+
* String if the input is null or empty.
111+
*
112+
* @param in The String whose non-valid characters we want to remove.
113+
* @return The in String, stripped of non-valid characters.
114+
* @see http://blog.mark-mclaren.info/2007/02/invalid-xml-characters-when-valid-utf8_5873.html
115+
*
116+
*/
117+
def escape(in: String): String = {
118+
val out = new StringBuilder()
119+
for ( current <- Option(in).getOrElse("").toCharArray ) {
120+
if ((current == 0x9) || (current == 0xA) || (current == 0xD) ||
121+
((current >= 0x20) && (current <= 0xD7FF)) ||
122+
((current >= 0xE000) && (current <= 0xFFFD)) ||
123+
((current >= 0x10000) && (current <= 0x10FFFF)))
124+
out.append(current)
125+
}
126+
out.mkString
127+
}
104128

105129
}

0 commit comments

Comments
 (0)