diff --git a/build.sbt b/build.sbt index 01ea8298..d7507588 100755 --- a/build.sbt +++ b/build.sbt @@ -1,19 +1,23 @@ name := "sbt-jacoco" organization := "com.github.sbt" -version in ThisBuild := "3.0.3" +version in ThisBuild := "3.1.0-M1" sbtPlugin := true crossSbtVersions := Seq("0.13.16", "1.0.2") val jacocoVersion = "0.7.9" +val circeVersion = "0.8.0" libraryDependencies ++= Seq( - "org.jacoco" % "org.jacoco.core" % jacocoVersion, - "org.jacoco" % "org.jacoco.report" % jacocoVersion, - "com.jsuereth" %% "scala-arm" % "2.0", - "org.scalatest" %% "scalatest" % "3.0.4" % Test, - "org.mockito" % "mockito-all" % "1.10.19" % Test + "org.jacoco" % "org.jacoco.core" % jacocoVersion, + "org.jacoco" % "org.jacoco.report" % jacocoVersion, + "com.jsuereth" %% "scala-arm" % "2.0", + "com.fasterxml.jackson.core" % "jackson-core" % "2.9.2", + "org.scalaj" %% "scalaj-http" % "2.3.0", + "commons-codec" % "commons-codec" % "1.11", + "org.scalatest" %% "scalatest" % "3.0.4" % Test, + "org.mockito" % "mockito-all" % "1.10.19" % Test ) scalacOptions ++= Seq( @@ -51,3 +55,7 @@ headerLicense := Some(HeaderLicense.Custom( enablePlugins(ParadoxSitePlugin, GhpagesPlugin) paradoxNavigationDepth in Paradox := 3 git.remoteRepo := "git@github.com:sbt/sbt-jacoco.git" + +addCompilerPlugin( + "org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full +) diff --git a/src/main/scala/com/github/sbt/jacoco/BaseJacocoPlugin.scala b/src/main/scala/com/github/sbt/jacoco/BaseJacocoPlugin.scala index cab3526b..10e01dc9 100644 --- a/src/main/scala/com/github/sbt/jacoco/BaseJacocoPlugin.scala +++ b/src/main/scala/com/github/sbt/jacoco/BaseJacocoPlugin.scala @@ -125,10 +125,6 @@ private[jacoco] abstract class BaseJacocoPlugin extends AutoPlugin with JacocoKe private def toClassName(entry: String): String = entry.stripSuffix(".class").replace('/', '.') - private def projectData(project: ResolvedProject): ProjectData = { - ProjectData(project.id) - } - protected lazy val submoduleSettingsTask: Def.Initialize[Task[(Seq[File], Option[File], Option[File])]] = Def.task { (classesToCover.value, (sourceDirectory in Compile).?.value, (jacocoDataFile in srcConfig).?.value) } diff --git a/src/main/scala/com/github/sbt/jacoco/coveralls/CoverallsClient.scala b/src/main/scala/com/github/sbt/jacoco/coveralls/CoverallsClient.scala new file mode 100644 index 00000000..d9db07f7 --- /dev/null +++ b/src/main/scala/com/github/sbt/jacoco/coveralls/CoverallsClient.scala @@ -0,0 +1,30 @@ +package com.github.sbt.jacoco.coveralls + +import java.io.{File, FileInputStream} + +import sbt.Keys.TaskStreams + +import scalaj.http.{Http, MultiPart} + +object CoverallsClient { + private val jobsUrl = "https://coveralls.io/api/v1/jobs" + + def sendReport(reportFile: File, streams: TaskStreams): Unit = { + val response = Http(jobsUrl) + .postMulti( + MultiPart( + "json_file", + "json_file.json", + "application/json", + new FileInputStream(reportFile), + reportFile.length(), + _ => ()) + ).asString + + if (response.isSuccess) { + streams.log.info("Upload complete") + } else { + streams.log.error(s"Unexpected response from coveralls: ${response.code}") + } + } +} diff --git a/src/main/scala/com/github/sbt/jacoco/coveralls/CoverallsReportFormat.scala b/src/main/scala/com/github/sbt/jacoco/coveralls/CoverallsReportFormat.scala new file mode 100644 index 00000000..dd87b4a5 --- /dev/null +++ b/src/main/scala/com/github/sbt/jacoco/coveralls/CoverallsReportFormat.scala @@ -0,0 +1,17 @@ +package com.github.sbt.jacoco.coveralls + +import java.io.File + +import com.github.sbt.jacoco.report.formats.JacocoReportFormat +import org.jacoco.report.IReportVisitor +import sbt._ + +class CoverallsReportFormat(sourceDirs: Seq[File], projectRootDir: File, jobId: String, repoToken: Option[String]) + extends JacocoReportFormat { + + override def createVisitor(directory: File, encoding: String): IReportVisitor = { + IO.createDirectory(directory) + + new CoverallsReportVisitor(directory / "coveralls.json", sourceDirs, projectRootDir, jobId, repoToken) + } +} diff --git a/src/main/scala/com/github/sbt/jacoco/coveralls/CoverallsReportVisitor.scala b/src/main/scala/com/github/sbt/jacoco/coveralls/CoverallsReportVisitor.scala new file mode 100644 index 00000000..b497dab9 --- /dev/null +++ b/src/main/scala/com/github/sbt/jacoco/coveralls/CoverallsReportVisitor.scala @@ -0,0 +1,91 @@ +package com.github.sbt.jacoco.coveralls + +import java.io.File +import java.{util => ju} + +import com.fasterxml.jackson.core.{JsonEncoding, JsonFactory} +import org.apache.commons.codec.digest.DigestUtils +import org.jacoco.core.analysis.{IBundleCoverage, ILine, IPackageCoverage, ISourceFileCoverage} +import org.jacoco.core.data.{ExecutionData, SessionInfo} +import org.jacoco.report.{IReportGroupVisitor, IReportVisitor, ISourceFileLocator} +import sbt._ + +import scala.collection.JavaConverters._ + +class CoverallsReportVisitor( + output: File, + sourceDirs: Seq[File], + projectRootDir: File, + jobId: String, + repoToken: Option[String]) + extends IReportVisitor + with IReportGroupVisitor { + + private val digest = new DigestUtils("MD5") + + private val jsonFactory = new JsonFactory() + private val json = jsonFactory.createGenerator(output, JsonEncoding.UTF8) + + json.writeStartObject() + + repoToken foreach { token => + json.writeStringField("repo_token", token) + } + + json.writeStringField("service_job_id", jobId) + json.writeStringField("service_name", "travis-ci") + + json.writeArrayFieldStart("source_files") + + override def visitInfo(sessionInfos: ju.List[SessionInfo], executionData: ju.Collection[ExecutionData]): Unit = {} + + override def visitGroup(name: String): IReportGroupVisitor = this + + override def visitBundle(bundle: IBundleCoverage, locator: ISourceFileLocator): Unit = { + bundle.getPackages.asScala foreach { pkg: IPackageCoverage => + pkg.getSourceFiles.asScala foreach { source: ISourceFileCoverage => + json.writeStartObject() + + //noinspection ScalaStyle + val (filename, md5) = findFile(pkg.getName, source.getName) match { + case Some(file) => + (IO.relativize(projectRootDir, file).getOrElse(file.getName), digest.digestAsHex(file)) + + case None => + (source.getName, "") + } + + json.writeStringField("name", filename) + json.writeStringField("source_digest", md5) + + json.writeArrayFieldStart("coverage") + + (0 to source.getLastLine) foreach { l => + val line: ILine = source.getLine(l) + + if (line.getInstructionCounter.getTotalCount == 0) { + // non-code line + json.writeNull() + } else { + json.writeNumber(line.getInstructionCounter.getCoveredCount) + } + } + + json.writeEndArray() + + json.writeEndObject() + } + } + } + + override def visitEnd(): Unit = { + json.writeEndArray() + json.writeEndObject() + json.close() + } + + private def findFile(packageName: String, fileName: String): Option[File] = { + // TODO make common with source file locator + sourceDirs.map(d => d / packageName / fileName).find(_.exists()) + } +} diff --git a/src/main/scala/com/github/sbt/jacoco/coveralls/JacocoCoverallsPlugin.scala b/src/main/scala/com/github/sbt/jacoco/coveralls/JacocoCoverallsPlugin.scala new file mode 100644 index 00000000..552eccb0 --- /dev/null +++ b/src/main/scala/com/github/sbt/jacoco/coveralls/JacocoCoverallsPlugin.scala @@ -0,0 +1,58 @@ +package com.github.sbt.jacoco.coveralls + +import java.io.File + +import com.github.sbt.jacoco.{JacocoPlugin, _} +import com.github.sbt.jacoco.report.ReportUtils +import sbt.Keys._ +import sbt._ + +object JacocoCoverallsPlugin extends BaseJacocoPlugin { + override def requires: Plugins = JacocoPlugin + override def trigger: PluginTrigger = noTrigger + + override protected def srcConfig = Test + + object autoImport { + val jacocoCoveralls: TaskKey[Unit] = taskKey("Upload JaCoCo reports to Coveralls") + + val jacocoCoverallsJobId: SettingKey[String] = settingKey("todo") + val jacocoCoverallsGenerateReport: TaskKey[Unit] = taskKey("TODO") + val jacocoCoverallsOutput: SettingKey[File] = settingKey("File to store Coveralls coverage") + + val jacocoCoverallsRepoToken: SettingKey[Option[String]] = settingKey("todo") + } + + import autoImport._ // scalastyle:ignore import.grouping + + override def projectSettings: Seq[Setting[_]] = Seq( + jacocoCoverallsOutput := jacocoReportDirectory.value, + jacocoCoveralls := Def.task { + CoverallsClient.sendReport(jacocoCoverallsOutput.value / "coveralls.json", streams.value) + }.value, + jacocoCoverallsGenerateReport := Def.task { + val coverallsFormat = + new CoverallsReportFormat( + coveredSources.value, + baseDirectory.value, + jacocoCoverallsJobId.value, + jacocoCoverallsRepoToken.value) + + ReportUtils.generateReport( + jacocoCoverallsOutput.value, + jacocoDataFile.value, + jacocoReportSettings.value.withFormats(coverallsFormat), + coveredSources.value, + classesToCover.value, + jacocoSourceSettings.value, + streams.value, + checkCoverage = false + ) + }.value, + jacocoCoveralls := (jacocoCoveralls dependsOn jacocoCoverallsGenerateReport).value, + // TODO fail if no job id + // TODO manual job id + jacocoCoverallsJobId := sys.env.getOrElse("TRAVIS_JOB_ID", "unknown"), + jacocoCoverallsRepoToken := None + ) +} diff --git a/src/main/scala/com/github/sbt/jacoco/package.scala b/src/main/scala/com/github/sbt/jacoco/package.scala new file mode 100644 index 00000000..9bc96ece --- /dev/null +++ b/src/main/scala/com/github/sbt/jacoco/package.scala @@ -0,0 +1,10 @@ +package com.github.sbt + +import com.github.sbt.jacoco.data.ProjectData +import sbt.ResolvedProject + +package object jacoco { + private[jacoco] def projectData(project: ResolvedProject): ProjectData = { + ProjectData(project.id) + } +} diff --git a/src/main/scala/com/github/sbt/jacoco/report/Report.scala b/src/main/scala/com/github/sbt/jacoco/report/Report.scala index c39ba8f3..81743e3b 100644 --- a/src/main/scala/com/github/sbt/jacoco/report/Report.scala +++ b/src/main/scala/com/github/sbt/jacoco/report/Report.scala @@ -29,7 +29,8 @@ class Report( sourceSettings: JacocoSourceSettings, reportSettings: JacocoReportSettings, reportDirectory: File, - streams: TaskStreams) { + streams: TaskStreams, + checkCoverage: Boolean) { private val percentageFormat = new DecimalFormat("#.##") @@ -39,7 +40,7 @@ class Report( reportSettings.formats.foreach(createReport(_, bundleCoverage, executionDataStore, sessionInfoStore)) - if (!checkCoverage(bundleCoverage)) { + if (checkCoverage && !checkCoverage(bundleCoverage)) { streams.log error "Required coverage is not met" // is there a better way to fail build? sys.exit(1) diff --git a/src/main/scala/com/github/sbt/jacoco/report/ReportUtils.scala b/src/main/scala/com/github/sbt/jacoco/report/ReportUtils.scala index 49cb6a7d..f4792f2a 100644 --- a/src/main/scala/com/github/sbt/jacoco/report/ReportUtils.scala +++ b/src/main/scala/com/github/sbt/jacoco/report/ReportUtils.scala @@ -23,7 +23,8 @@ object ReportUtils { sourceDirectories: Seq[File], classDirectories: Seq[File], sourceSettings: JacocoSourceSettings, - streams: TaskStreams): Unit = { + streams: TaskStreams, + checkCoverage: Boolean = true): Unit = { val report = new Report( reportDirectory = destinationDirectory, @@ -32,7 +33,8 @@ object ReportUtils { sourceDirectories = sourceDirectories, sourceSettings = sourceSettings, reportSettings = reportSettings, - streams = streams + streams = streams, + checkCoverage = checkCoverage ) report.generate() @@ -45,7 +47,8 @@ object ReportUtils { sourceDirectories: Seq[File], classDirectories: Seq[File], sourceSettings: JacocoSourceSettings, - streams: TaskStreams): Unit = { + streams: TaskStreams, + checkCoverage: Boolean = true): Unit = { val report = new Report( reportDirectory = destinationDirectory, @@ -54,7 +57,8 @@ object ReportUtils { sourceDirectories = sourceDirectories, sourceSettings = sourceSettings, reportSettings = reportSettings, - streams = streams + streams = streams, + checkCoverage = checkCoverage ) report.generate() diff --git a/src/test/scala-sbt-0.13/com/github/sbt/jacoco/TestCounters.scala b/src/test/scala-sbt-0.13/com/github/sbt/jacoco/TestCounters.scala index ca1c4ef3..194dcc44 100644 --- a/src/test/scala-sbt-0.13/com/github/sbt/jacoco/TestCounters.scala +++ b/src/test/scala-sbt-0.13/com/github/sbt/jacoco/TestCounters.scala @@ -36,7 +36,8 @@ class TestCounters { reportDirectory = new File("."), executionDataFiles = Nil, classDirectories = Nil, - sourceDirectories = Nil + sourceDirectories = Nil, + checkCoverage = true ) when[Logger](mockStreams.log).thenReturn(mockLog) diff --git a/src/test/scala-sbt-1.0/com/github/sbt/jacoco/TestCounters.scala b/src/test/scala-sbt-1.0/com/github/sbt/jacoco/TestCounters.scala index 5ae28c09..d75528d6 100644 --- a/src/test/scala-sbt-1.0/com/github/sbt/jacoco/TestCounters.scala +++ b/src/test/scala-sbt-1.0/com/github/sbt/jacoco/TestCounters.scala @@ -36,7 +36,8 @@ class TestCounters { reportDirectory = new File("."), executionDataFiles = Nil, classDirectories = Nil, - sourceDirectories = Nil + sourceDirectories = Nil, + checkCoverage = true ) when[ManagedLogger](mockStreams.log).thenReturn(mockLog)