Skip to content

Commit a752950

Browse files
committed
Add initial coveralls plugin
1 parent 753d965 commit a752950

File tree

11 files changed

+235
-18
lines changed

11 files changed

+235
-18
lines changed

build.sbt

+14-6
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
name := "sbt-jacoco"
22
organization := "com.github.sbt"
33

4-
version in ThisBuild := "3.0.3"
4+
version in ThisBuild := "3.1.0-M1"
55

66
sbtPlugin := true
77
crossSbtVersions := Seq("0.13.16", "1.0.2")
88

99
val jacocoVersion = "0.7.9"
10+
val circeVersion = "0.8.0"
1011

1112
libraryDependencies ++= Seq(
12-
"org.jacoco" % "org.jacoco.core" % jacocoVersion,
13-
"org.jacoco" % "org.jacoco.report" % jacocoVersion,
14-
"com.jsuereth" %% "scala-arm" % "2.0",
15-
"org.scalatest" %% "scalatest" % "3.0.4" % Test,
16-
"org.mockito" % "mockito-all" % "1.10.19" % Test
13+
"org.jacoco" % "org.jacoco.core" % jacocoVersion,
14+
"org.jacoco" % "org.jacoco.report" % jacocoVersion,
15+
"com.jsuereth" %% "scala-arm" % "2.0",
16+
"com.fasterxml.jackson.core" % "jackson-core" % "2.9.2",
17+
"org.scalaj" %% "scalaj-http" % "2.3.0",
18+
"commons-codec" % "commons-codec" % "1.11",
19+
"org.scalatest" %% "scalatest" % "3.0.4" % Test,
20+
"org.mockito" % "mockito-all" % "1.10.19" % Test
1721
)
1822

1923
scalacOptions ++= Seq(
@@ -51,3 +55,7 @@ headerLicense := Some(HeaderLicense.Custom(
5155
enablePlugins(ParadoxSitePlugin, GhpagesPlugin)
5256
paradoxNavigationDepth in Paradox := 3
5357
git.remoteRepo := "[email protected]:sbt/sbt-jacoco.git"
58+
59+
addCompilerPlugin(
60+
"org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full
61+
)

src/main/scala/com/github/sbt/jacoco/BaseJacocoPlugin.scala

-4
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,6 @@ private[jacoco] abstract class BaseJacocoPlugin extends AutoPlugin with JacocoKe
125125
private def toClassName(entry: String): String =
126126
entry.stripSuffix(".class").replace('/', '.')
127127

128-
private def projectData(project: ResolvedProject): ProjectData = {
129-
ProjectData(project.id)
130-
}
131-
132128
protected lazy val submoduleSettingsTask: Def.Initialize[Task[(Seq[File], Option[File], Option[File])]] = Def.task {
133129
(classesToCover.value, (sourceDirectory in Compile).?.value, (jacocoDataFile in srcConfig).?.value)
134130
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.github.sbt.jacoco.coveralls
2+
3+
import java.io.{File, FileInputStream}
4+
5+
import sbt.Keys.TaskStreams
6+
7+
import scalaj.http.{Http, MultiPart}
8+
9+
object CoverallsClient {
10+
private val jobsUrl = "https://coveralls.io/api/v1/jobs"
11+
12+
def sendReport(reportFile: File, streams: TaskStreams): Unit = {
13+
val response = Http(jobsUrl)
14+
.postMulti(
15+
MultiPart(
16+
"json_file",
17+
"json_file.json",
18+
"application/json",
19+
new FileInputStream(reportFile),
20+
reportFile.length(),
21+
_ => ())
22+
).asString
23+
24+
if (response.isSuccess) {
25+
streams.log.info("Upload complete")
26+
} else {
27+
streams.log.error(s"Unexpected response from coveralls: ${response.code}")
28+
}
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.github.sbt.jacoco.coveralls
2+
3+
import java.io.File
4+
5+
import com.github.sbt.jacoco.report.formats.JacocoReportFormat
6+
import org.jacoco.report.IReportVisitor
7+
import sbt._
8+
9+
class CoverallsReportFormat(sourceDirs: Seq[File], projectRootDir: File, jobId: String, repoToken: Option[String])
10+
extends JacocoReportFormat {
11+
12+
override def createVisitor(directory: File, encoding: String): IReportVisitor = {
13+
IO.createDirectory(directory)
14+
15+
new CoverallsReportVisitor(directory / "coveralls.json", sourceDirs, projectRootDir, jobId, repoToken)
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package com.github.sbt.jacoco.coveralls
2+
3+
import java.io.File
4+
import java.{util => ju}
5+
6+
import com.fasterxml.jackson.core.{JsonEncoding, JsonFactory}
7+
import org.apache.commons.codec.digest.DigestUtils
8+
import org.jacoco.core.analysis.{IBundleCoverage, ILine, IPackageCoverage, ISourceFileCoverage}
9+
import org.jacoco.core.data.{ExecutionData, SessionInfo}
10+
import org.jacoco.report.{IReportGroupVisitor, IReportVisitor, ISourceFileLocator}
11+
import sbt._
12+
13+
import scala.collection.JavaConverters._
14+
15+
class CoverallsReportVisitor(
16+
output: File,
17+
sourceDirs: Seq[File],
18+
projectRootDir: File,
19+
jobId: String,
20+
repoToken: Option[String])
21+
extends IReportVisitor
22+
with IReportGroupVisitor {
23+
24+
private val digest = new DigestUtils("MD5")
25+
26+
private val jsonFactory = new JsonFactory()
27+
private val json = jsonFactory.createGenerator(output, JsonEncoding.UTF8)
28+
29+
json.writeStartObject()
30+
31+
repoToken foreach { token =>
32+
json.writeStringField("repo_token", token)
33+
}
34+
35+
json.writeStringField("service_job_id", jobId)
36+
json.writeStringField("service_name", "travis-ci")
37+
38+
json.writeArrayFieldStart("source_files")
39+
40+
override def visitInfo(sessionInfos: ju.List[SessionInfo], executionData: ju.Collection[ExecutionData]): Unit = {}
41+
42+
override def visitGroup(name: String): IReportGroupVisitor = this
43+
44+
override def visitBundle(bundle: IBundleCoverage, locator: ISourceFileLocator): Unit = {
45+
bundle.getPackages.asScala foreach { pkg: IPackageCoverage =>
46+
pkg.getSourceFiles.asScala foreach { source: ISourceFileCoverage =>
47+
json.writeStartObject()
48+
49+
//noinspection ScalaStyle
50+
val (filename, md5) = findFile(pkg.getName, source.getName) match {
51+
case Some(file) =>
52+
(IO.relativize(projectRootDir, file).getOrElse(file.getName), digest.digestAsHex(file))
53+
54+
case None =>
55+
(source.getName, "")
56+
}
57+
58+
json.writeStringField("name", filename)
59+
json.writeStringField("source_digest", md5)
60+
61+
json.writeArrayFieldStart("coverage")
62+
63+
(0 to source.getLastLine) foreach { l =>
64+
val line: ILine = source.getLine(l)
65+
66+
if (line.getInstructionCounter.getTotalCount == 0) {
67+
// non-code line
68+
json.writeNull()
69+
} else {
70+
json.writeNumber(line.getInstructionCounter.getCoveredCount)
71+
}
72+
}
73+
74+
json.writeEndArray()
75+
76+
json.writeEndObject()
77+
}
78+
}
79+
}
80+
81+
override def visitEnd(): Unit = {
82+
json.writeEndArray()
83+
json.writeEndObject()
84+
json.close()
85+
}
86+
87+
private def findFile(packageName: String, fileName: String): Option[File] = {
88+
// TODO make common with source file locator
89+
sourceDirs.map(d => d / packageName / fileName).find(_.exists())
90+
}
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.github.sbt.jacoco.coveralls
2+
3+
import java.io.File
4+
5+
import com.github.sbt.jacoco.{JacocoPlugin, _}
6+
import com.github.sbt.jacoco.report.ReportUtils
7+
import sbt.Keys._
8+
import sbt._
9+
10+
object JacocoCoverallsPlugin extends BaseJacocoPlugin {
11+
override def requires: Plugins = JacocoPlugin
12+
override def trigger: PluginTrigger = noTrigger
13+
14+
override protected def srcConfig = Test
15+
16+
object autoImport {
17+
val jacocoCoveralls: TaskKey[Unit] = taskKey("Upload JaCoCo reports to Coveralls")
18+
19+
val jacocoCoverallsJobId: SettingKey[String] = settingKey("todo")
20+
val jacocoCoverallsGenerateReport: TaskKey[Unit] = taskKey("TODO")
21+
val jacocoCoverallsOutput: SettingKey[File] = settingKey("File to store Coveralls coverage")
22+
23+
val jacocoCoverallsRepoToken: SettingKey[Option[String]] = settingKey("todo")
24+
}
25+
26+
import autoImport._ // scalastyle:ignore import.grouping
27+
28+
override def projectSettings: Seq[Setting[_]] = Seq(
29+
jacocoCoverallsOutput := jacocoReportDirectory.value,
30+
jacocoCoveralls := Def.task {
31+
CoverallsClient.sendReport(jacocoCoverallsOutput.value / "coveralls.json", streams.value)
32+
}.value,
33+
jacocoCoverallsGenerateReport := Def.task {
34+
val coverallsFormat =
35+
new CoverallsReportFormat(
36+
coveredSources.value,
37+
baseDirectory.value,
38+
jacocoCoverallsJobId.value,
39+
jacocoCoverallsRepoToken.value)
40+
41+
ReportUtils.generateReport(
42+
jacocoCoverallsOutput.value,
43+
jacocoDataFile.value,
44+
jacocoReportSettings.value.withFormats(coverallsFormat),
45+
coveredSources.value,
46+
classesToCover.value,
47+
jacocoSourceSettings.value,
48+
streams.value,
49+
checkCoverage = false
50+
)
51+
}.value,
52+
jacocoCoveralls := (jacocoCoveralls dependsOn jacocoCoverallsGenerateReport).value,
53+
// TODO fail if no job id
54+
// TODO manual job id
55+
jacocoCoverallsJobId := sys.env.getOrElse("TRAVIS_JOB_ID", "unknown"),
56+
jacocoCoverallsRepoToken := None
57+
)
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.github.sbt
2+
3+
import com.github.sbt.jacoco.data.ProjectData
4+
import sbt.ResolvedProject
5+
6+
package object jacoco {
7+
private[jacoco] def projectData(project: ResolvedProject): ProjectData = {
8+
ProjectData(project.id)
9+
}
10+
}

src/main/scala/com/github/sbt/jacoco/report/Report.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ class Report(
2929
sourceSettings: JacocoSourceSettings,
3030
reportSettings: JacocoReportSettings,
3131
reportDirectory: File,
32-
streams: TaskStreams) {
32+
streams: TaskStreams,
33+
checkCoverage: Boolean) {
3334

3435
private val percentageFormat = new DecimalFormat("#.##")
3536

@@ -39,7 +40,7 @@ class Report(
3940

4041
reportSettings.formats.foreach(createReport(_, bundleCoverage, executionDataStore, sessionInfoStore))
4142

42-
if (!checkCoverage(bundleCoverage)) {
43+
if (checkCoverage && !checkCoverage(bundleCoverage)) {
4344
streams.log error "Required coverage is not met"
4445
// is there a better way to fail build?
4546
sys.exit(1)

src/main/scala/com/github/sbt/jacoco/report/ReportUtils.scala

+8-4
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ object ReportUtils {
2323
sourceDirectories: Seq[File],
2424
classDirectories: Seq[File],
2525
sourceSettings: JacocoSourceSettings,
26-
streams: TaskStreams): Unit = {
26+
streams: TaskStreams,
27+
checkCoverage: Boolean = true): Unit = {
2728

2829
val report = new Report(
2930
reportDirectory = destinationDirectory,
@@ -32,7 +33,8 @@ object ReportUtils {
3233
sourceDirectories = sourceDirectories,
3334
sourceSettings = sourceSettings,
3435
reportSettings = reportSettings,
35-
streams = streams
36+
streams = streams,
37+
checkCoverage = checkCoverage
3638
)
3739

3840
report.generate()
@@ -45,7 +47,8 @@ object ReportUtils {
4547
sourceDirectories: Seq[File],
4648
classDirectories: Seq[File],
4749
sourceSettings: JacocoSourceSettings,
48-
streams: TaskStreams): Unit = {
50+
streams: TaskStreams,
51+
checkCoverage: Boolean = true): Unit = {
4952

5053
val report = new Report(
5154
reportDirectory = destinationDirectory,
@@ -54,7 +57,8 @@ object ReportUtils {
5457
sourceDirectories = sourceDirectories,
5558
sourceSettings = sourceSettings,
5659
reportSettings = reportSettings,
57-
streams = streams
60+
streams = streams,
61+
checkCoverage = checkCoverage
5862
)
5963

6064
report.generate()

src/test/scala-sbt-0.13/com/github/sbt/jacoco/TestCounters.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ class TestCounters {
3636
reportDirectory = new File("."),
3737
executionDataFiles = Nil,
3838
classDirectories = Nil,
39-
sourceDirectories = Nil
39+
sourceDirectories = Nil,
40+
checkCoverage = true
4041
)
4142

4243
when[Logger](mockStreams.log).thenReturn(mockLog)

src/test/scala-sbt-1.0/com/github/sbt/jacoco/TestCounters.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ class TestCounters {
3636
reportDirectory = new File("."),
3737
executionDataFiles = Nil,
3838
classDirectories = Nil,
39-
sourceDirectories = Nil
39+
sourceDirectories = Nil,
40+
checkCoverage = true
4041
)
4142

4243
when[ManagedLogger](mockStreams.log).thenReturn(mockLog)

0 commit comments

Comments
 (0)