quarkusio / quarkus

Quarkus: Supersonic Subatomic Java.
https://quarkus.io
Apache License 2.0
13.79k stars 2.68k forks source link

Add Gradle explanation on how to incorporate Jacoco reports #9138

Closed GregJohnStewart closed 3 years ago

GregJohnStewart commented 4 years ago

I use Gradle for my builds, and am having issues getting my Jacoco coverage reports to be accurate.

I know it is an issue that is addressed in the following guide, but the explanation and resolution is for Maven builds: https://quarkus.io/guides/tests-with-coverage#the-coverage-does-not-seem-to-correspond-to-the-reality and https://quarkus.io/guides/tests-with-coverage#instrumenting-the-classes-instead

I have gotten part of the way there, where I have gotten Jacoco running with offline instrumentation thanks to this example, but still not seeing accurate reports. It would be great to see an explanation on how to accomplish this with Gradle.

quarkusbot commented 4 years ago

/cc @quarkusio/devtools

pschyma commented 4 years ago

Hi,

this is how I use it in a project right now:

plugins {
  kotlin("jvm")
  kotlin("plugin.allopen")

  id("io.quarkus")

  jacoco
}

val quarkusPlatformGroupId: String by ext
val quarkusPlatformArtifactId: String by ext
val quarkusPlatformVersion: String by ext

dependencies {
  implementation(platform(project(":platform")))
  implementation(project(":model"))

  implementation(kotlin("stdlib-jdk8"))

  implementation(platform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
  implementation("io.quarkus:quarkus-kotlin")
  implementation("io.quarkus:quarkus-resteasy")
  implementation("io.quarkus:quarkus-resteasy-jackson")
  implementation("io.quarkus:quarkus-smallrye-openapi")
  implementation("io.quarkus:quarkus-smallrye-fault-tolerance")
  implementation("io.quarkus:quarkus-smallrye-health")
  implementation("io.quarkus:quarkus-smallrye-metrics")
  implementation("io.quarkus:quarkus-smallrye-context-propagation")
  implementation("io.quarkus:quarkus-jdbc-postgresql")
  implementation("io.quarkus:quarkus-flyway")
  implementation("io.quarkus:quarkus-hibernate-validator")

  implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
  implementation("org.zalando:problem")
  implementation("org.zalando:jackson-datatype-problem")

  implementation(platform("org.jdbi:jdbi3-bom:3.13.0"))
  implementation("org.jdbi:jdbi3-core")
  implementation("org.jdbi:jdbi3-kotlin")
  implementation("org.jdbi:jdbi3-kotlin-sqlobject")
  implementation("org.jdbi:jdbi3-postgres")

  testImplementation("io.quarkus:quarkus-junit5")
  testImplementation("org.assertj:assertj-core")
  testImplementation("io.rest-assured:rest-assured:4.3.0")
  testImplementation("io.rest-assured:kotlin-extensions:4.3.0")
  testImplementation("org.mockito:mockito-core")
  testImplementation("org.mockito:mockito-junit-jupiter")
  testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin")

  testImplementation("org.testcontainers:testcontainers:1.14.1")
  testImplementation("org.testcontainers:postgresql:1.14.1")
  testImplementation("org.testcontainers:junit-jupiter:1.14.1")
}

allOpen {
  annotation("javax.enterprise.context.ApplicationScoped")
  annotation("javax.ws.rs.Path")
  annotation("io.quarkus.test.junit.QuarkusTest")
}

val outputDir = file("$projectDir/build/classes/kotlin/main")

quarkus {
  setOutputDirectory(outputDir.absolutePath)
}

tasks {
  test {
    useJUnitPlatform()

    jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
    jvmArgs("--add-opens", "java.base/java.lang.invoke=ALL-UNNAMED")

    systemProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager")
  }

  quarkusDev {
    setSourceDir("$projectDir/src/main/kotlin")
  }

  create("instrument") {
    dependsOn(classes)
    finalizedBy(test)

    val jacocoAnt by configurations
    val originalClasses = file("$outputDir-orig")

    doLast {
      originalClasses.deleteRecursively()
      outputDir.renameTo(originalClasses)

      ant.withGroovyBuilder {
        "taskdef"("name" to "instrument", "classname" to "org.jacoco.ant.InstrumentTask", "classpath" to jacocoAnt.asPath)

        "instrument"("destDir" to outputDir) {
          "fileset"("dir" to originalClasses)
        }
      }
    }
  }

  create("restore") {
    dependsOn("instrument")

    val originalClasses = file("$outputDir-orig")

    doLast {
      outputDir.deleteRecursively()
      originalClasses.renameTo(outputDir)
    }
  }

  jacocoTestReport {
    dependsOn("instrument", test, "restore")
  }

  jacocoTestCoverageVerification {
    dependsOn("instrument", test, "restore")

    violationRules {
      rule {
        element = "CLASS"
        limit {
          counter = "METHOD"
          value = "MISSEDCOUNT"
          maximum = "0".toBigDecimal()
        }
      }
      rule {
        element = "METHOD"
        limit {
          counter = "LINE"
          value = "COVEREDRATIO"
          minimum = "1".toBigDecimal()
        }
        limit {
          counter = "BRANCH"
          value = "COVEREDRATIO"
          minimum = "1".toBigDecimal()
        }
        limit {
          counter = "INSTRUCTION"
          value = "COVEREDRATIO"
          minimum = "1".toBigDecimal()
        }
        limit {
          counter = "COMPLEXITY"
          value = "TOTALCOUNT"
          maximum = "12".toBigDecimal()
        }
      }
    }
  }
}

gradle.taskGraph.whenReady {
  val instrument = tasks.named("instrument").get()

  if (hasTask(instrument)) {
    val originalClasses = file("$outputDir-orig")

    tasks.test.configure {
      classpath = layout.files(originalClasses, classpath.minus(outputDir))
    }
  }
}

It's rather complicated due to:

  1. Quarkus uses hard-coded paths for sources
  2. There is no out-of-the box offline instrument task

The jacoco plugin tasks have exceptions from the jacoco agent complaining about already processed classes, but the coverage works.

For java one needs to change the path in outputDir.

GregJohnStewart commented 4 years ago

I almost feel like this warrants a quarkus jacoco plugin? This is a ton of setup, potentially copy/paste for the most part

pschyma commented 4 years ago

There is already gradle/gradle#2429 but without any progress. Ideally the Gradle issue should be implemented and Quarkus plugin should allow to set the classes path to use for testing.

So we would have an ideal split of responsibilities. But I doubt that this will happen on the Gradle side.

For now I struggled implementing a plugin because of the hard-coded paths in Quarkus.

ferencbeutel4711 commented 3 years ago

sadly, the provided example does not work (at least not with autogenerated methods from panache) :(

glefloch commented 3 years ago

Sorry for the late answer. There is a new integration of jacoco in quarkus.

Now, you just need to add a dependency to testImplementation "io.quarkus:quarkus-jacoco". The report will automatically be generated.

If you build quarkus masterbranch locally, you can already test it. It will be part of quarkus 1.13. Here is the pull request that added this integration https://github.com/quarkusio/quarkus/pull/14384.