diff --git a/README.md b/README.md index 25394433..65daaa6d 100644 --- a/README.md +++ b/README.md @@ -335,7 +335,12 @@ Read [SBT SCoverage Plugin documentation](https://github.com/scoverage/sbt-scove scoverage-maven-plugin ${scoverage.plugin.version} - 80 + 95 + 90 + 90 + 85 + 85 + 80 true diff --git a/src/it/test_coverage_minima/invoker.properties b/src/it/test_coverage_minima/invoker.properties new file mode 100644 index 00000000..f4f2d3bc --- /dev/null +++ b/src/it/test_coverage_minima/invoker.properties @@ -0,0 +1 @@ +invoker.goals=clean scoverage:check site -e -ntp diff --git a/src/it/test_coverage_minima/module01/pom.xml b/src/it/test_coverage_minima/module01/pom.xml new file mode 100644 index 00000000..eb09e4a1 --- /dev/null +++ b/src/it/test_coverage_minima/module01/pom.xml @@ -0,0 +1,16 @@ + + + + + + it.scoverage-maven-plugin + test_coverage_minima + 1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + module01 + Test SCoverage Coverage Minima : Module 1 + + diff --git a/src/it/test_coverage_minima/module01/src/main/scala/pkg01/HelloService1.scala b/src/it/test_coverage_minima/module01/src/main/scala/pkg01/HelloService1.scala new file mode 100644 index 00000000..9d7c7380 --- /dev/null +++ b/src/it/test_coverage_minima/module01/src/main/scala/pkg01/HelloService1.scala @@ -0,0 +1,12 @@ +package pkg01 + +class HelloService1 +{ + def hello = + { + "Hello from module 1" + } + +} + +object HelloService1 extends HelloService1 diff --git a/src/it/test_coverage_minima/module01/src/test/scala/pkg01/HelloServiceTest.scala b/src/it/test_coverage_minima/module01/src/test/scala/pkg01/HelloServiceTest.scala new file mode 100644 index 00000000..9cac7025 --- /dev/null +++ b/src/it/test_coverage_minima/module01/src/test/scala/pkg01/HelloServiceTest.scala @@ -0,0 +1,14 @@ +package pkg01 + +import org.junit.Test; +import org.junit.Assert.assertEquals + +class HelloServiceTest +{ + @Test + def test1() + { + assertEquals("Hello from module 1", HelloService1.hello) + } + +} diff --git a/src/it/test_coverage_minima/module02/pom.xml b/src/it/test_coverage_minima/module02/pom.xml new file mode 100644 index 00000000..9875282b --- /dev/null +++ b/src/it/test_coverage_minima/module02/pom.xml @@ -0,0 +1,16 @@ + + + + + + it.scoverage-maven-plugin + test_coverage_minima + 1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + module02 + Test SCoverage Coverage Minima : Module 2 + + diff --git a/src/it/test_coverage_minima/module02/src/main/scala/pkg01/HelloService1.scala b/src/it/test_coverage_minima/module02/src/main/scala/pkg01/HelloService1.scala new file mode 100644 index 00000000..1d3c8ca0 --- /dev/null +++ b/src/it/test_coverage_minima/module02/src/main/scala/pkg01/HelloService1.scala @@ -0,0 +1,12 @@ +package pkg01 + +class HelloService1 +{ + def hello = + { + "Hello from module 2" + } + +} + +object HelloService1 extends HelloService1 diff --git a/src/it/test_coverage_minima/module02/src/main/scala/pkg02/HelloService1.scala b/src/it/test_coverage_minima/module02/src/main/scala/pkg02/HelloService1.scala new file mode 100644 index 00000000..b5edc85b --- /dev/null +++ b/src/it/test_coverage_minima/module02/src/main/scala/pkg02/HelloService1.scala @@ -0,0 +1,12 @@ +package pkg02 + +class HelloService1 +{ + def hello = + { + "Hello from module 2" + } + +} + +object HelloService1 extends HelloService1 diff --git a/src/it/test_coverage_minima/module02/src/test/scala/pkg01/HelloServiceTest.scala b/src/it/test_coverage_minima/module02/src/test/scala/pkg01/HelloServiceTest.scala new file mode 100644 index 00000000..8dacc428 --- /dev/null +++ b/src/it/test_coverage_minima/module02/src/test/scala/pkg01/HelloServiceTest.scala @@ -0,0 +1,14 @@ +package pkg01 + +import org.junit.Test; +import org.junit.Assert.assertEquals + +class HelloServiceTest +{ + @Test + def test1() + { + assertEquals("Hello from module 2", HelloService1.hello) + } + +} diff --git a/src/it/test_coverage_minima/module03/pom.xml b/src/it/test_coverage_minima/module03/pom.xml new file mode 100644 index 00000000..a6d61023 --- /dev/null +++ b/src/it/test_coverage_minima/module03/pom.xml @@ -0,0 +1,41 @@ + + + + + + it.scoverage-maven-plugin + test_coverage_minima + 1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + module03 + Test SCoverage Coverage Minima : Module 3 + + + + + @project.groupId@ + @project.artifactId@ + + true + 100 + 100 + 100 + 100 + 100 + 100 + + + + + check + + + + + + + + diff --git a/src/it/test_coverage_minima/module03/src/main/scala/pkg01/HelloService1.scala b/src/it/test_coverage_minima/module03/src/main/scala/pkg01/HelloService1.scala new file mode 100644 index 00000000..478fbada --- /dev/null +++ b/src/it/test_coverage_minima/module03/src/main/scala/pkg01/HelloService1.scala @@ -0,0 +1,12 @@ +package pkg01 + +class HelloService1 +{ + def hello = + { + "Hello from module 3" + } + +} + +object HelloService1 extends HelloService1 diff --git a/src/it/test_coverage_minima/module03/src/test/scala/pkg01/HelloServiceTest.scala b/src/it/test_coverage_minima/module03/src/test/scala/pkg01/HelloServiceTest.scala new file mode 100644 index 00000000..ffe06705 --- /dev/null +++ b/src/it/test_coverage_minima/module03/src/test/scala/pkg01/HelloServiceTest.scala @@ -0,0 +1,14 @@ +package pkg01 + +import org.junit.Test; +import org.junit.Assert.assertEquals + +class HelloServiceTest +{ + @Test + def test1() + { + assertEquals("Hello from module 3", HelloService1.hello) + } + +} diff --git a/src/it/test_coverage_minima/pom.xml b/src/it/test_coverage_minima/pom.xml new file mode 100644 index 00000000..0d6ffc03 --- /dev/null +++ b/src/it/test_coverage_minima/pom.xml @@ -0,0 +1,68 @@ + + + + 4.0.0 + + + it.scoverage-maven-plugin + integration_tests_parent + 1.0-SNAPSHOT + ../integration_tests_parent/pom.xml + + + test_coverage_minima + 1.0-SNAPSHOT + pom + Test Scoverage coverage minima + Test Scoverage coverage minima + + + 2.13 + 12 + + + + module01 + module02 + module03 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + net.alchim31.maven + scala-maven-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + @project.groupId@ + @project.artifactId@ + + true + 95 + 90 + 90 + 85 + 85 + 80 + false + + + + + check + + + + + + + diff --git a/src/it/test_coverage_minima/validate.groovy b/src/it/test_coverage_minima/validate.groovy new file mode 100644 index 00000000..4de80432 --- /dev/null +++ b/src/it/test_coverage_minima/validate.groovy @@ -0,0 +1,44 @@ +def checkModule(logText, module, coverageLog) { + assert new File(basedir, module + "/target/scoverage.xml").exists() + assert new File(basedir, module + "/target/site/scoverage/index.html").exists() + def entry = logText.find { + it.startsWith("scoverage") && + it.contains(":check (default-cli) @ " + module + " ---\n") + } + assert entry != null + assert entry.endsWith(" @ " + module + " ---" + coverageLog.replaceAll("\r\n", "\n") + "[INFO] ") +} + +try { + // check coverage minima + def logText = (new File(basedir, "build.log")).text.replaceAll("\r\n", "\n").split(/\n\[INFO\] \-\-\- /) + + checkModule(logText, "module01", + """ + |[INFO] Coverage is 100%: Statement:Total! + |[INFO] Coverage is 100%: Branch:Total! + |""".stripMargin() + ) + checkModule(logText, "module02", + """ + |[ERROR] Coverage is below minimum [50.00% < 95.00%]: Statement:Total + |[INFO] Coverage is 100%: Branch:Total! + |[ERROR] Coverage is below minimum [0.00% < 90.00%]: Statement:Package:pkg02 + |[ERROR] Coverage is below minimum [0.00% < 85.00%]: Branch:Package:pkg02 + |[ERROR] Coverage is below minimum [0.00% < 85.00%]: Statement:File:HelloService1.scala + |[ERROR] Coverage is below minimum [0.00% < 80.00%]: Branch:File:HelloService1.scala + |""".stripMargin() + ) + checkModule(logText, "module03", + """ + |[INFO] Coverage is 100%: Statement:Total! + |[INFO] Coverage is 100%: Branch:Total! + |""".stripMargin() + ) + + return true + +} catch (Throwable e) { + e.printStackTrace() + return false +} diff --git a/src/main/java/org/scoverage/plugin/SCoverageCheckMojo.java b/src/main/java/org/scoverage/plugin/SCoverageCheckMojo.java index 42d19af2..18940379 100644 --- a/src/main/java/org/scoverage/plugin/SCoverageCheckMojo.java +++ b/src/main/java/org/scoverage/plugin/SCoverageCheckMojo.java @@ -20,9 +20,12 @@ import java.io.File; import java.util.Arrays; import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugin.logging.Log; import org.apache.maven.plugins.annotations.Execute; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; @@ -33,6 +36,10 @@ import scala.jdk.javaapi.CollectionConverters; import scoverage.domain.Coverage; +import scoverage.domain.CoverageMetrics; +import scoverage.domain.DoubleFormat; +import scoverage.domain.MeasuredFile; +import scoverage.domain.MeasuredPackage; import scoverage.reporter.IOUtils; import scoverage.serialize.Serializer; @@ -70,7 +77,7 @@ public class SCoverageCheckMojo private File dataDirectory; /** - * Required minimum coverage. + * Required minimum total statement coverage. *
*
* See https://github.com/scoverage/sbt-scoverage#minimum-coverage for additional documentation. @@ -81,6 +88,66 @@ public class SCoverageCheckMojo @Parameter( property = "scoverage.minimumCoverage", defaultValue = "0" ) private Double minimumCoverage; + /** + * Required minimum total branch coverage. + *
+ *
+ * See https://github.com/scoverage/sbt-scoverage#minimum-coverage for additional documentation. + *
+ * + * @since 2.0.1 + */ + @Parameter( property = "scoverage.minimumCoverageBranchTotal", defaultValue = "0" ) + private Double minimumCoverageBranchTotal; + + /** + * Required minimum per-package statement coverage. + *
+ *
+ * See https://github.com/scoverage/sbt-scoverage#minimum-coverage for additional documentation. + *
+ * + * @since 2.0.1 + */ + @Parameter( property = "scoverage.minimumCoverageStmtPerPackage", defaultValue = "0" ) + private Double minimumCoverageStmtPerPackage; + + /** + * Required minimum per-package branch coverage. + *
+ *
+ * See https://github.com/scoverage/sbt-scoverage#minimum-coverage for additional documentation. + *
+ * + * @since 2.0.1 + */ + @Parameter( property = "scoverage.minimumCoverageBranchPerPackage", defaultValue = "0" ) + private Double minimumCoverageBranchPerPackage; + + /** + * Required minimum per-file statement coverage. + *
+ *
+ * See https://github.com/scoverage/sbt-scoverage#minimum-coverage for additional documentation. + *
+ * + * @since 2.0.1 + */ + @Parameter( property = "scoverage.minimumCoverageStmtPerFile", defaultValue = "0" ) + private Double minimumCoverageStmtPerFile; + + /** + * Required minimum per-file branch coverage. + *
+ *
+ * See https://github.com/scoverage/sbt-scoverage#minimum-coverage for additional documentation. + *
+ * + * @since 2.0.1 + */ + @Parameter( property = "scoverage.minimumCoverageBranchPerFile", defaultValue = "0" ) + private Double minimumCoverageBranchPerFile; + /** * Fail the build if minimum coverage was not reached. *
@@ -168,31 +235,19 @@ public void execute() throws MojoFailureException int invokedBranchesCount = coverage.invokedBranchesCount(); int invokedStatementCount = coverage.invokedStatementCount(); - getLog().info( String.format( "Statement coverage.: %s%%", coverage.statementCoverageFormatted() ) ); - getLog().info( String.format( "Branch coverage....: %s%%", coverage.branchCoverageFormatted() ) ); getLog().debug( String.format( "invokedBranchesCount:%d / branchCount:%d, invokedStatementCount:%d / statementCount:%d", invokedBranchesCount, branchCount, invokedStatementCount, statementCount ) ); - if ( minimumCoverage > 0.0 ) + + boolean ok = checkCoverage( getLog(), "Total", coverage, + minimumCoverage, minimumCoverageBranchTotal, true ); + ok = checkCoverage( getLog(), "Package:", coverage.packages(), MeasuredPackage::name, + minimumCoverageStmtPerPackage, minimumCoverageBranchPerPackage ) && ok; + ok = checkCoverage( getLog(), "File:", coverage.files(), MeasuredFile::filename, + minimumCoverageStmtPerFile, minimumCoverageBranchPerFile ) && ok; + + if ( !ok && failOnMinimumCoverage ) { - String minimumCoverageFormatted = scoverage.domain.DoubleFormat.twoFractionDigits( minimumCoverage ); - if ( is100( minimumCoverage ) && is100( coverage.statementCoveragePercent() ) ) - { - getLog().info( "100% Coverage !" ); - } - else if ( coverage.statementCoveragePercent() < minimumCoverage ) - { - getLog().error( String.format( "Coverage is below minimum [%s%% < %s%%]", - coverage.statementCoverageFormatted(), minimumCoverageFormatted ) ); - if ( failOnMinimumCoverage ) - { - throw new MojoFailureException( "Coverage minimum was not reached" ); - } - } - else - { - getLog().info( String.format( "Coverage is above minimum [%s%% >= %s%%]", - coverage.statementCoverageFormatted(), minimumCoverageFormatted ) ); - } + throw new MojoFailureException( "Coverage minimum was not reached" ); } long te = System.currentTimeMillis(); @@ -201,9 +256,86 @@ else if ( coverage.statementCoveragePercent() < minimumCoverage ) // Private utility methods - private boolean is100( Double d ) + private static boolean is100( Double d ) { return Math.abs( 100 - d ) <= 0.00001d; } -} \ No newline at end of file + private static + boolean checkCoverage( Log logger, String metricPrefix, + scala.collection.Iterable< T > metrics, + Function< T, String > toName, + double minStmt, double minBranch ) + { + return minStmt <= 0 && minBranch <= 0 || checkAll(metrics, cov -> + checkCoverage(logger, metricPrefix + toName.apply(cov), cov, minStmt, minBranch, false) + ); + } + + private static boolean checkCoverage( Log logger, String metric, CoverageMetrics metrics, + double minStmt, double minBranch, boolean logSuccessInfo ) + { + boolean stmt = checkCoverage( logger, "Statement:" + metric, + minStmt, metrics.statementCoveragePercent(), logSuccessInfo ); + boolean branch = checkCoverage( logger, "Branch:" + metric, + minBranch, metrics.branchCoveragePercent(), logSuccessInfo ); + return stmt && branch; + } + + private static boolean checkCoverage( Log logger, String metric, + double minimum, double actual, boolean logSuccessInfo ) + { + if ( minimum <= 0 ) + { + return true; + } + + if ( is100( actual ) ) + { + logSuccess( logger, String.format( "Coverage is 100%%: %s!", metric ), logSuccessInfo ); + return true; + } + + String minimumFormatted = DoubleFormat.twoFractionDigits( minimum ); + String actualFormatted = DoubleFormat.twoFractionDigits( actual ); + boolean ok = minimum <= actual; + + if ( ok ) + { + String message = String.format( "Coverage is above minimum [%s%% >= %s%%]: %s", + actualFormatted, minimumFormatted, metric ); + logSuccess( logger, message, logSuccessInfo ); + } + else + { + String message = String.format( "Coverage is below minimum [%s%% < %s%%]: %s", + actualFormatted, minimumFormatted, metric ); + logger.error( message ); + } + + return ok; + } + + private static void logSuccess( Log logger, String message, boolean logSuccessInfo ) + { + if ( logSuccessInfo ) + { + logger.info( message ); + } + else + { + logger.debug( message ); + } + } + + private static boolean checkAll( scala.collection.Iterable iterable, Predicate predicate ) + { + boolean ok = true; + for ( T elem : CollectionConverters.asJava( iterable ) ) + { + ok = predicate.test( elem ) && ok; + } + return ok; + } + +}