diff --git a/README.md b/README.md index 2f438c8..cabf4a0 100644 --- a/README.md +++ b/README.md @@ -58,11 +58,27 @@ Available tasks CheckScoverage --------- -By default, when you launch `gradle checkScoverage` build fail if only 75% of project is covered by tests. +By default, when you launch `gradle checkScoverage` build fail if only 75% of statements in project is covered by tests. To configure it as you want, add this configuration : ``` checkScoverage { - minimumLineRate = 0.5 + minimumRate = 0.5 +} +``` + +You can also modify type of value to check from `Statement`s to `Line`s or `Branch`es: + +``` +checkScoverage { + coverageType = 'Line' + minimumRate = 0.5 +} +``` + +``` +checkScoverage { + coverageType = 'Branch' + minimumRate = 0.5 } ``` diff --git a/src/main/groovy/org/scoverage/OverallCheckTask.groovy b/src/main/groovy/org/scoverage/OverallCheckTask.groovy index bb044a8..bdba5d9 100644 --- a/src/main/groovy/org/scoverage/OverallCheckTask.groovy +++ b/src/main/groovy/org/scoverage/OverallCheckTask.groovy @@ -4,14 +4,49 @@ import org.gradle.api.DefaultTask import org.gradle.api.GradleException import org.gradle.api.tasks.TaskAction +import java.text.DecimalFormat + /** - * Throws a GradleException if overall line coverage dips below the configured percentage. + * Handles different types of coverage Scoverage can measure. + */ +enum CoverageType { + Line('cobertura.xml', 'line-rate', 1.0), + Statement('scoverage.xml', 'statement-rate', 100.0), + Branch('scoverage.xml', 'branch-rate', 100.0) + + /** Name of file with coverage data */ + String fileName + /** Name of param in XML file with coverage value */ + String paramName + /** Used to normalize coverage value */ + private double factor + + private CoverageType(String fileName, String paramName, double factor) { + this.fileName = fileName + this.paramName = paramName + this.factor = factor + } + + /** Normalize coverage value to [0, 1] */ + Double normalize(Double value) { + return value / factor + } +} + +/** + * Throws a GradleException if overall coverage dips below the configured percentage. */ class OverallCheckTask extends DefaultTask { - File cobertura - double minimumLineRate = 0.75 - protected XmlParser parser; + /** Type of coverage to check. Available options: Line, Statement and Branch */ + CoverageType coverageType = CoverageType.Statement + double minimumRate = 0.75 + + /** Set if want to change default from 'reportDir' in scoverage extension. */ + File reportDir + + protected XmlParser parser + protected DecimalFormat df = new DecimalFormat("#.##") OverallCheckTask() { parser = new XmlParser() @@ -19,17 +54,36 @@ class OverallCheckTask extends DefaultTask { parser.setFeature('http://apache.org/xml/features/nonvalidating/load-external-dtd', false) } + /** Extracted to method for testing purposes */ + static String errorMsg(String actual, String expected, CoverageType type) { + return "Only $actual% of project is covered by tests instead of $expected% (coverageType: $type)" + } + + /** Extracted to method for testing purposes */ + static String fileNotFoundErrorMsg(CoverageType coverageType) { + return "Coverage file (type: $coverageType) not found, check your configuration." + } + @TaskAction void requireLineCoverage() { def extension = ScoveragePlugin.extensionIn(project) - if (cobertura == null) cobertura = new File(extension.reportDir, 'cobertura.xml') + File reportFile = new File(reportDir ? reportDir : extension.reportDir, coverageType.fileName) + + try { + def xml = parser.parse(reportFile) + Double overallRate = coverageType.normalize(xml.attribute(coverageType.paramName).toDouble()) + println("$minimumRate - $overallRate") + def difference = (minimumRate - overallRate) - def xml = parser.parse(cobertura) - def overallLineRate = xml.attribute('line-rate').toDouble() - def difference = (minimumLineRate - overallLineRate) + if (difference > 1e-7) { + String is = df.format(overallRate * 100) + String needed = df.format(minimumRate * 100) + throw new GradleException(errorMsg(is, needed, coverageType)) + } + } catch (FileNotFoundException fnfe) { + throw new GradleException(fileNotFoundErrorMsg(coverageType), fnfe) + } - if (difference > 1e-7) - throw new GradleException("Only ${overallLineRate * 100}% of project is covered by tests instead of ${(minimumLineRate * 100).toInteger()}%!") } } diff --git a/src/test/groovy/org/scoverage/OverallCheckTaskTest.groovy b/src/test/groovy/org/scoverage/OverallCheckTaskTest.groovy index ad9baed..7daeeb2 100644 --- a/src/test/groovy/org/scoverage/OverallCheckTaskTest.groovy +++ b/src/test/groovy/org/scoverage/OverallCheckTaskTest.groovy @@ -1,7 +1,7 @@ package org.scoverage +import org.gradle.api.GradleException import org.gradle.api.Project -import org.gradle.api.tasks.TaskExecutionException import org.gradle.testfixtures.ProjectBuilder import org.hamcrest.Description import org.hamcrest.TypeSafeMatcher @@ -9,37 +9,136 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.ExpectedException +/** + * Copied from the Internet, just to check if we have correct exception thrown. + */ +class CauseMatcher extends TypeSafeMatcher { + + private final Class type; + private final String expectedMessage; + + public CauseMatcher(Class type, String expectedMessage) { + this.type = type; + this.expectedMessage = expectedMessage; + } + + @Override + protected boolean matchesSafely(Throwable item) { + return item.getClass().isAssignableFrom(type) && item.getMessage().contains(expectedMessage); + } + + @Override + public void describeTo(Description description) { + description.appendText("expects type ") + .appendValue(type) + .appendText(" and a message ") + .appendValue(expectedMessage); + } +} + class OverallCheckTaskTest { @Rule public ExpectedException expectedException = ExpectedException.none() - private Project projectForLineRate(Number lineRate) { + private Project projectForRate(Number coverageRate, CoverageType type) { Project project = ProjectBuilder.builder().build() project.plugins.apply(ScoveragePlugin) project.tasks.create('bob', OverallCheckTask) { - minimumLineRate = lineRate - cobertura = new File('src/test/resources/cobertura.xml') + minimumRate = coverageRate + reportDir = new File('src/test/resources') + coverageType = type } project } + // error when report file is not there + @Test - void failsWhenLineRateIsBelowTarget(){ - Project project = projectForLineRate(1) - expectedException.expect(TaskExecutionException) + void failsWhenReportFileIsNotFound() { + Project project = ProjectBuilder.builder().build() + project.plugins.apply(ScoveragePlugin) + project.tasks.create('bob', OverallCheckTask) { + minimumRate = 1.0 + reportDir = new File('src/test/nothingthere') + coverageType = CoverageType.Line + } + expectedException.expectCause(new CauseMatcher( + GradleException.class, + OverallCheckTask.fileNotFoundErrorMsg(CoverageType.Line) + )) + project.tasks.bob.execute() + } + + // line coverage + + @Test + void failsWhenLineRateIsBelowTarget() { + Project project = projectForRate(1, CoverageType.Line) + expectedException.expectCause(new CauseMatcher( + GradleException.class, + OverallCheckTask.errorMsg("66", "100", CoverageType.Line) + )) project.tasks.bob.execute() } @Test void doesNotFailWhenLineRateIsAtTarget() throws Exception { - Project project = projectForLineRate(0.66) + Project project = projectForRate(0.66, CoverageType.Line) project.tasks.bob.execute() } @Test void doesNotFailWhenLineRateIsAboveTarget() throws Exception { - Project project = projectForLineRate(0.6) + Project project = projectForRate(0.6, CoverageType.Line) + project.tasks.bob.execute() + } + + // Statement coverage + + @Test + void failsWhenStatementRateIsBelowTarget() { + Project project = projectForRate(1, CoverageType.Statement) + expectedException.expectCause(new CauseMatcher( + GradleException.class, + OverallCheckTask.errorMsg("33.33", "100", CoverageType.Statement) + )) + project.tasks.bob.execute() + } + + @Test + void doesNotFailWhenStatementRateIsAtTarget() throws Exception { + Project project = projectForRate(0.33, CoverageType.Statement) + project.tasks.bob.execute() + } + + @Test + void doesNotFailWhenStatementRateIsAboveTarget() throws Exception { + Project project = projectForRate(0.3, CoverageType.Statement) + project.tasks.bob.execute() + } + + // Branch coverage + + @Test + void failsWhenBranchRateIsBelowTarget() { + Project project = projectForRate(1, CoverageType.Branch) + expectedException.expectCause(new CauseMatcher( + GradleException.class, + OverallCheckTask.errorMsg("50", "100", CoverageType.Branch) + )) + project.tasks.bob.execute() + } + + @Test + void doesNotFailWhenBranchRateIsAtTarget() throws Exception { + Project project = projectForRate(0.50, CoverageType.Branch) + project.tasks.bob.execute() + } + + @Test + void doesNotFailWhenBranchRateIsAboveTarget() throws Exception { + Project project = projectForRate(0.45, CoverageType.Branch) project.tasks.bob.execute() } diff --git a/src/test/happy day/build.gradle b/src/test/happy day/build.gradle index b00ef3b..7f7d433 100644 --- a/src/test/happy day/build.gradle +++ b/src/test/happy day/build.gradle @@ -24,7 +24,8 @@ dependencies { } checkScoverage { - minimumLineRate = 1.0 + minimumRate = 1.0 + coverageType = 'Line' } tasks.withType(ScalaCompile) { diff --git a/src/test/resources/scoverage.xml b/src/test/resources/scoverage.xml new file mode 100644 index 0000000..a3f636e --- /dev/null +++ b/src/test/resources/scoverage.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +