From 5a497878a72de0ce9d17ed42ece798a8c535e248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=20Buransk=C3=BD?= Date: Mon, 3 Feb 2014 13:17:35 -0800 Subject: [PATCH 001/101] Initial commit --- .gitignore | 13 +++++++++++++ README.md | 4 ++++ 2 files changed, 17 insertions(+) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4abe769 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*.class +*.log + +# sbt specific +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ + +# Scala-IDE specific +.scala_dependencies diff --git a/README.md b/README.md new file mode 100644 index 0000000..88b7283 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +sonar-scoverage-plugin +====================== + +Sonar plugin for Scala coverage tool From 4006bfa9a097ed37ef506376f09d81ca8f5615a9 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Mon, 3 Feb 2014 15:53:44 -0800 Subject: [PATCH 002/101] Copy sonar-scala --- .gitignore | 14 +- README.md | 24 +- dev | 8 + pom.xml | 252 + .../plugins/scala/ScalaDefaultProfile.java | 42 + .../buransky/plugins/scala/ScalaPlugin.java | 73 + .../scala/cobertura/CoberturaSensor.java | 60 + .../scala/cobertura/ScalaCoberturaParser.java | 47 + .../colorization/ScalaColorizerFormat.java | 62 + .../scala/colorization/ScalaKeywords.java | 50 + .../plugins/scala/cpd/ScalaCpdMapping.java | 49 + .../plugins/scala/cpd/ScalaTokenizer.java | 52 + .../plugins/scala/language/Comment.java | 120 + .../plugins/scala/language/CommentType.java | 34 + .../plugins/scala/language/Scala.java | 41 + .../plugins/scala/language/ScalaFile.java | 141 + .../plugins/scala/language/ScalaPackage.java | 90 + .../scala/metrics/CommentsAnalyzer.java | 76 + .../plugins/scala/metrics/LinesAnalyzer.java | 61 + .../scala/sensor/AbstractScalaSensor.java | 47 + .../scala/sensor/BaseMetricsSensor.java | 162 + .../sensor/ScalaSourceImporterSensor.java | 89 + .../scala/surefire/SurefireSensor.java | 75 + .../plugins/scala/util/StringUtils.java | 57 + .../plugins/scala/compiler/Compiler.scala | 39 + .../plugins/scala/compiler/Lexer.scala | 124 + .../plugins/scala/compiler/Parser.scala | 62 + .../plugins/scala/compiler/Token.scala | 27 + .../plugins/scala/language/CodeDetector.scala | 68 + .../scala/language/PackageResolver.scala | 76 + .../scala/metrics/ComplexityCalculator.scala | 115 + .../scala/metrics/FunctionCounter.scala | 108 + .../scala/metrics/PublicApiCounter.scala | 98 + .../scala/metrics/StatementCounter.scala | 114 + .../plugins/scala/metrics/TypeCounter.scala | 96 + .../plugins/scala/metrics/package.scala | 68 + .../scala/util/MetricDistribution.scala | 48 + .../plugins/scala/ScalaPluginTest.java | 50 + .../scala/cobertura/CoberturaSensorTest.java | 71 + .../cobertura/ScalaCoberturaParserTest.java | 92 + .../plugins/scala/cpd/ScalaTokenizerTest.java | 128 + .../plugins/scala/language/CommentTest.java | 109 + .../plugins/scala/language/ScalaFileTest.java | 93 + .../plugins/scala/language/ScalaTest.java | 48 + .../scala/metrics/CommentsAnalyzerTest.java | 96 + .../scala/metrics/LinesAnalyzerTest.java | 95 + .../scala/sensor/AbstractScalaSensorTest.java | 65 + .../scala/sensor/BaseMetricsSensorTest.java | 214 + .../sensor/ScalaSourceImporterSensorTest.java | 219 + .../scala/surefire/SurefireSensorTest.java | 80 + .../plugins/scala/util/DummyScalaFile.java | 38 + .../plugins/scala/util/FileTestUtils.java | 86 + .../baseMetricsSensor/ScalaFile1.scala | 5 + .../baseMetricsSensor/ScalaFile2.scala | 5 + .../baseMetricsSensor/ScalaFile3.scala | 5 + .../resources/cpd/Duplications5Tokens.scala | 6 + src/test/resources/cpd/NewlineToken.scala | 5 + src/test/resources/cpd/NewlinesToken.scala | 6 + src/test/resources/cpd/NoDuplications.scala | 3 + .../resources/cpd/TwoDuplicatedBlocks.scala | 11 + src/test/resources/lexer/DocComment1.txt | 1 + .../lexer/HeaderCommentWithCodeBefore.txt | 8 + .../lexer/HeaderCommentWithWrongStart.txt | 4 + .../lexer/NormalCommentWithHeaderComment.txt | 6 + .../resources/lexer/SimpleHeaderComment.txt | 4 + .../plugins/scala/cobertura/coverage.xml | 6768 +++++++++++++++++ .../DeepNestedPackageDeclaration.txt | 18 + ...tedPackageDeclarationWithObjectBetween.txt | 22 + .../NestedPackageDeclaration.txt | 9 + ...tedPackageDeclarationWithObjectBetween.txt | 11 + .../SimplePackageDeclaration.txt | 6 + src/test/resources/scalaFile/ScalaFile1.scala | 5 + .../resources/scalaFile/ScalaTestFile1.scala | 5 + .../scalaSourceImporter/JavaMainFile1.java | 5 + .../scalaSourceImporter/JavaTestFile1.java | 5 + .../scalaSourceImporter/MainFile1.scala | 5 + .../scalaSourceImporter/MainFile2.scala | 5 + .../scalaSourceImporter/MainFile3.scala | 5 + .../scalaSourceImporter/TestFile1.scala | 5 + .../scalaSourceImporter/TestFile2.scala | 5 + .../scalaSourceImporter/TestFile3.scala | 5 + .../plugins/scala/compiler/LexerSpec.scala | 87 + .../scala/language/CodeDetectorSpec.scala | 61 + .../scala/language/PackageResolverSpec.scala | 61 + .../metrics/ComplexityCalculatorSpec.scala | 209 + .../scala/metrics/FunctionCounterSpec.scala | 122 + .../scala/metrics/PublicApiCounterSpec.scala | 150 + .../scala/metrics/StatementCounterSpec.scala | 174 + .../scala/metrics/TypeCounterSpec.scala | 165 + .../scala/util/MetricDistributionSpec.scala | 97 + test | 7 + 91 files changed, 12324 insertions(+), 15 deletions(-) create mode 100755 dev create mode 100644 pom.xml create mode 100644 src/main/java/com/buransky/plugins/scala/ScalaDefaultProfile.java create mode 100644 src/main/java/com/buransky/plugins/scala/ScalaPlugin.java create mode 100644 src/main/java/com/buransky/plugins/scala/cobertura/CoberturaSensor.java create mode 100644 src/main/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParser.java create mode 100644 src/main/java/com/buransky/plugins/scala/colorization/ScalaColorizerFormat.java create mode 100644 src/main/java/com/buransky/plugins/scala/colorization/ScalaKeywords.java create mode 100644 src/main/java/com/buransky/plugins/scala/cpd/ScalaCpdMapping.java create mode 100644 src/main/java/com/buransky/plugins/scala/cpd/ScalaTokenizer.java create mode 100644 src/main/java/com/buransky/plugins/scala/language/Comment.java create mode 100644 src/main/java/com/buransky/plugins/scala/language/CommentType.java create mode 100644 src/main/java/com/buransky/plugins/scala/language/Scala.java create mode 100644 src/main/java/com/buransky/plugins/scala/language/ScalaFile.java create mode 100644 src/main/java/com/buransky/plugins/scala/language/ScalaPackage.java create mode 100644 src/main/java/com/buransky/plugins/scala/metrics/CommentsAnalyzer.java create mode 100644 src/main/java/com/buransky/plugins/scala/metrics/LinesAnalyzer.java create mode 100644 src/main/java/com/buransky/plugins/scala/sensor/AbstractScalaSensor.java create mode 100644 src/main/java/com/buransky/plugins/scala/sensor/BaseMetricsSensor.java create mode 100644 src/main/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensor.java create mode 100644 src/main/java/com/buransky/plugins/scala/surefire/SurefireSensor.java create mode 100644 src/main/java/com/buransky/plugins/scala/util/StringUtils.java create mode 100644 src/main/scala/com/buransky/plugins/scala/compiler/Compiler.scala create mode 100644 src/main/scala/com/buransky/plugins/scala/compiler/Lexer.scala create mode 100644 src/main/scala/com/buransky/plugins/scala/compiler/Parser.scala create mode 100644 src/main/scala/com/buransky/plugins/scala/compiler/Token.scala create mode 100644 src/main/scala/com/buransky/plugins/scala/language/CodeDetector.scala create mode 100644 src/main/scala/com/buransky/plugins/scala/language/PackageResolver.scala create mode 100644 src/main/scala/com/buransky/plugins/scala/metrics/ComplexityCalculator.scala create mode 100644 src/main/scala/com/buransky/plugins/scala/metrics/FunctionCounter.scala create mode 100644 src/main/scala/com/buransky/plugins/scala/metrics/PublicApiCounter.scala create mode 100644 src/main/scala/com/buransky/plugins/scala/metrics/StatementCounter.scala create mode 100644 src/main/scala/com/buransky/plugins/scala/metrics/TypeCounter.scala create mode 100644 src/main/scala/com/buransky/plugins/scala/metrics/package.scala create mode 100644 src/main/scala/com/buransky/plugins/scala/util/MetricDistribution.scala create mode 100644 src/test/java/com/buransky/plugins/scala/ScalaPluginTest.java create mode 100644 src/test/java/com/buransky/plugins/scala/cobertura/CoberturaSensorTest.java create mode 100644 src/test/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParserTest.java create mode 100644 src/test/java/com/buransky/plugins/scala/cpd/ScalaTokenizerTest.java create mode 100644 src/test/java/com/buransky/plugins/scala/language/CommentTest.java create mode 100644 src/test/java/com/buransky/plugins/scala/language/ScalaFileTest.java create mode 100644 src/test/java/com/buransky/plugins/scala/language/ScalaTest.java create mode 100644 src/test/java/com/buransky/plugins/scala/metrics/CommentsAnalyzerTest.java create mode 100644 src/test/java/com/buransky/plugins/scala/metrics/LinesAnalyzerTest.java create mode 100644 src/test/java/com/buransky/plugins/scala/sensor/AbstractScalaSensorTest.java create mode 100644 src/test/java/com/buransky/plugins/scala/sensor/BaseMetricsSensorTest.java create mode 100644 src/test/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensorTest.java create mode 100644 src/test/java/com/buransky/plugins/scala/surefire/SurefireSensorTest.java create mode 100644 src/test/java/com/buransky/plugins/scala/util/DummyScalaFile.java create mode 100644 src/test/java/com/buransky/plugins/scala/util/FileTestUtils.java create mode 100644 src/test/resources/baseMetricsSensor/ScalaFile1.scala create mode 100644 src/test/resources/baseMetricsSensor/ScalaFile2.scala create mode 100644 src/test/resources/baseMetricsSensor/ScalaFile3.scala create mode 100644 src/test/resources/cpd/Duplications5Tokens.scala create mode 100644 src/test/resources/cpd/NewlineToken.scala create mode 100644 src/test/resources/cpd/NewlinesToken.scala create mode 100644 src/test/resources/cpd/NoDuplications.scala create mode 100644 src/test/resources/cpd/TwoDuplicatedBlocks.scala create mode 100644 src/test/resources/lexer/DocComment1.txt create mode 100644 src/test/resources/lexer/HeaderCommentWithCodeBefore.txt create mode 100644 src/test/resources/lexer/HeaderCommentWithWrongStart.txt create mode 100644 src/test/resources/lexer/NormalCommentWithHeaderComment.txt create mode 100644 src/test/resources/lexer/SimpleHeaderComment.txt create mode 100644 src/test/resources/org/sonar/plugins/scala/cobertura/coverage.xml create mode 100644 src/test/resources/packageResolver/DeepNestedPackageDeclaration.txt create mode 100644 src/test/resources/packageResolver/DeepNestedPackageDeclarationWithObjectBetween.txt create mode 100644 src/test/resources/packageResolver/NestedPackageDeclaration.txt create mode 100644 src/test/resources/packageResolver/NestedPackageDeclarationWithObjectBetween.txt create mode 100644 src/test/resources/packageResolver/SimplePackageDeclaration.txt create mode 100644 src/test/resources/scalaFile/ScalaFile1.scala create mode 100644 src/test/resources/scalaFile/ScalaTestFile1.scala create mode 100644 src/test/resources/scalaSourceImporter/JavaMainFile1.java create mode 100644 src/test/resources/scalaSourceImporter/JavaTestFile1.java create mode 100644 src/test/resources/scalaSourceImporter/MainFile1.scala create mode 100644 src/test/resources/scalaSourceImporter/MainFile2.scala create mode 100644 src/test/resources/scalaSourceImporter/MainFile3.scala create mode 100644 src/test/resources/scalaSourceImporter/TestFile1.scala create mode 100644 src/test/resources/scalaSourceImporter/TestFile2.scala create mode 100644 src/test/resources/scalaSourceImporter/TestFile3.scala create mode 100644 src/test/scala/com/buransky/plugins/scala/compiler/LexerSpec.scala create mode 100644 src/test/scala/com/buransky/plugins/scala/language/CodeDetectorSpec.scala create mode 100644 src/test/scala/com/buransky/plugins/scala/language/PackageResolverSpec.scala create mode 100644 src/test/scala/com/buransky/plugins/scala/metrics/ComplexityCalculatorSpec.scala create mode 100644 src/test/scala/com/buransky/plugins/scala/metrics/FunctionCounterSpec.scala create mode 100644 src/test/scala/com/buransky/plugins/scala/metrics/PublicApiCounterSpec.scala create mode 100644 src/test/scala/com/buransky/plugins/scala/metrics/StatementCounterSpec.scala create mode 100644 src/test/scala/com/buransky/plugins/scala/metrics/TypeCounterSpec.scala create mode 100644 src/test/scala/com/buransky/plugins/scala/util/MetricDistributionSpec.scala create mode 100755 test diff --git a/.gitignore b/.gitignore index 4abe769..b4a5277 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,3 @@ -*.class -*.log - -# sbt specific -dist/* +.idea/ target/ -lib_managed/ -src_managed/ -project/boot/ -project/plugins/project/ - -# Scala-IDE specific -.scala_dependencies +*.iml diff --git a/README.md b/README.md index 88b7283..3a4f0eb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,22 @@ -sonar-scoverage-plugin -====================== +Sonar Scala Plugin +=========== +Supports Sonar 3.0+ and requires Cobertura and Surefire plugins. -Sonar plugin for Scala coverage tool +To include test and coverage reports: + +Install these plugins in your scala project: + +https://github.com/mmarich/sbt-simple-junit-xml-reporter-plugin +- Creates junit xml reports for output from scalatest. + +https://github.com/sqality/scct +- Creates a Scala-friendly code-coverage report, and includes a coberura xml report. + + +Add the following properties to your project's sonar-project.properties file: + +sonar.dynamicAnalysis=reuseReports +sonar.surefire.reportsPath=test-reports +sonar.core.codeCoveragePlugin=cobertura +sonar.java.coveragePlugin=cobertura +sonar.cobertura.reportPath=target/scala-[scala-version]/coverage-report/cobertura.xml diff --git a/dev b/dev new file mode 100755 index 0000000..d65e55a --- /dev/null +++ b/dev @@ -0,0 +1,8 @@ +#!/bin/bash + +/home/rado/bin/sonar/bin/linux-x86-64/sonar.sh stop + +mvn install -DskipTests +cp ./target/sonar-scoverage-plugin-0.1-SNAPSHOT.jar /home/rado/bin/sonar/extensions/plugins/ + +/home/rado/bin/sonar/bin/linux-x86-64/sonar.sh start diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..4f86df7 --- /dev/null +++ b/pom.xml @@ -0,0 +1,252 @@ + + + 4.0.0 + + + + + + com.buransky + sonar-scoverage-plugin + + sonar-plugin + 0.1-SNAPSHOT + + Sonar Scoverage Plugin + Sonar Scoverage Plugin + + + + + + GNU LGPL 3 + http://www.gnu.org/licenses/lgpl.txt + repo + + + + + + + + scala-tools + Scala Tools + http://scala-tools.org/repo-releases/ + + true + + + false + + + + + + 3.0 + + + scoverage + Scoverage + + com.buransky.plugins.scala.ScalaPlugin + + 2.9.1 + + + + + org.codehaus.sonar + sonar-plugin-api + ${sonar.version} + + + org.codehaus.sonar.plugins + sonar-surefire-plugin + ${sonar.version} + provided + + + org.codehaus.sonar.plugins + sonar-cobertura-plugin + ${sonar.version} + provided + + + org.scala-lang + scala-library + ${scala.version} + + + org.scala-lang + scala-compiler + ${scala.version} + + + org.scalariform + scalariform_${scala.version} + 0.1.1 + + + + + org.codehaus.sonar + sonar-testing-harness + ${sonar.version} + + + org.scalatest + scalatest_${scala.version} + 1.6.1 + test + + + org.apache.maven + maven-project + 2.2.1 + test + + + + + + + + org.codehaus.mojo + cobertura-maven-plugin + 2.5 + + + org.codehaus.sonar-plugins.pdf-report + maven-pdfreport-plugin + 1.2 + + + org.codehaus.mojo + sonar-maven-plugin + 1.0-beta-2 + + + org.codehaus.sonar + sonar-maven-plugin + ${sonar.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + + org.codehaus.sonar + sonar-packaging-maven-plugin + 1.7 + true + + ${sonar.pluginClass} + + + + org.scala-tools + maven-scala-plugin + 2.15.2 + + + scala-compile + process-resources + + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + + + + + maven-surefire-plugin + 2.6 + + + **/*Spec.class + **/*Test.class + + + + + + diff --git a/src/main/java/com/buransky/plugins/scala/ScalaDefaultProfile.java b/src/main/java/com/buransky/plugins/scala/ScalaDefaultProfile.java new file mode 100644 index 0000000..c65f821 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/ScalaDefaultProfile.java @@ -0,0 +1,42 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala; + +import com.buransky.plugins.scala.language.Scala; +import org.sonar.api.profiles.AnnotationProfileParser; +import org.sonar.api.profiles.ProfileDefinition; +import org.sonar.api.profiles.RulesProfile; +import org.sonar.api.utils.ValidationMessages; + +import java.util.Collections; + +public class ScalaDefaultProfile extends ProfileDefinition { + + private final AnnotationProfileParser annotationProfileParser; + + public ScalaDefaultProfile(AnnotationProfileParser annotationProfileParser) { + this.annotationProfileParser = annotationProfileParser; + } + + @Override + public RulesProfile createProfile(ValidationMessages messages) { + return annotationProfileParser.parse(Scala.INSTANCE.getKey(), "sonar", Scala.INSTANCE.getKey(), Collections.EMPTY_LIST, messages); + } +} diff --git a/src/main/java/com/buransky/plugins/scala/ScalaPlugin.java b/src/main/java/com/buransky/plugins/scala/ScalaPlugin.java new file mode 100644 index 0000000..d80000c --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/ScalaPlugin.java @@ -0,0 +1,73 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala; + +import java.util.ArrayList; +import java.util.List; + +import com.buransky.plugins.scala.cobertura.CoberturaSensor; +import com.buransky.plugins.scala.language.Scala; +import org.sonar.api.Extension; +import org.sonar.api.SonarPlugin; +import com.buransky.plugins.scala.colorization.ScalaColorizerFormat; +import com.buransky.plugins.scala.sensor.BaseMetricsSensor; +import com.buransky.plugins.scala.sensor.ScalaSourceImporterSensor; +import com.buransky.plugins.scala.surefire.SurefireSensor; + +/** + * This class is the entry point for all extensions made by the + * Sonar Scala Plugin. + * + * @author Felix Müller + * @since 0.1 + */ +public class ScalaPlugin extends SonarPlugin { + + public List> getExtensions() { + final List> extensions = new ArrayList>(); + extensions.add(Scala.class); + extensions.add(ScalaSourceImporterSensor.class); + extensions.add(ScalaColorizerFormat.class); + extensions.add(BaseMetricsSensor.class); + extensions.add(ScalaDefaultProfile.class); + extensions.add(CoberturaSensor.class); + extensions.add(SurefireSensor.class); + + return extensions; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + + public static String getPathToScalaLibrary() { + return getPathByResource("scala/package.class"); + } + + /** + * Godin: during execution of Sonar Batch all dependencies of a plugin are downloaded and + * available locally as JAR-files, so we can use this kind of hack to locate JARs. + */ + private static String getPathByResource(String name) { + String path = ScalaPlugin.class.getClassLoader().getResource(name).getPath(); + return path.substring("file:".length(), path.lastIndexOf('!')); + } +} diff --git a/src/main/java/com/buransky/plugins/scala/cobertura/CoberturaSensor.java b/src/main/java/com/buransky/plugins/scala/cobertura/CoberturaSensor.java new file mode 100644 index 0000000..d684634 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/cobertura/CoberturaSensor.java @@ -0,0 +1,60 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.cobertura; + +import com.buransky.plugins.scala.language.Scala; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.batch.CoverageExtension; +import org.sonar.api.batch.Sensor; +import org.sonar.api.batch.SensorContext; +import org.sonar.api.resources.Project; +import org.sonar.plugins.cobertura.api.AbstractCoberturaParser; +import org.sonar.plugins.cobertura.api.CoberturaUtils; + +import java.io.File; + +public class CoberturaSensor implements Sensor, CoverageExtension { + + private static final Logger LOG = LoggerFactory.getLogger(CoberturaSensor.class); + private static final AbstractCoberturaParser COBERTURA_PARSER = new ScalaCoberturaParser(); + + public boolean shouldExecuteOnProject(Project project) { + return project.getAnalysisType().isDynamic(true) && Scala.INSTANCE.getKey().equals(project.getLanguageKey()); + } + + public void analyse(Project project, SensorContext context) { + File report = CoberturaUtils.getReport(project); + if (report != null) { + parseReport(report, context); + } + } + + protected void parseReport(File xmlFile, final SensorContext context) { + LOG.info("parsing {}", xmlFile); + COBERTURA_PARSER.parseReport(xmlFile, context); + } + + @Override + public String toString() { + return "Scala CoberturaSensor"; + } + +} diff --git a/src/main/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParser.java b/src/main/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParser.java new file mode 100644 index 0000000..28cf490 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParser.java @@ -0,0 +1,47 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.cobertura; + +import org.sonar.plugins.cobertura.api.AbstractCoberturaParser; +import org.sonar.api.resources.Resource; +import com.buransky.plugins.scala.language.ScalaFile; + +public class ScalaCoberturaParser extends AbstractCoberturaParser { + @Override + protected Resource getResource(String fileName) { + // TODO update the sbt scct plugin to provide the correct fully qualified class name. + if (fileName.startsWith("src.main.scala.")) + fileName = fileName.replace("src.main.scala.", ""); + else if (fileName.startsWith("app.")) + fileName = fileName.replace("app.", ""); + + int packageTerminator = fileName.lastIndexOf('.'); + + if (packageTerminator < 0 ) { + return new ScalaFile(null, fileName, false); + } + else { + String packageName = fileName.substring(0, packageTerminator); + String className = fileName.substring(packageTerminator + 1, fileName.length()); + + return new ScalaFile(packageName, className, false); + } + } +} diff --git a/src/main/java/com/buransky/plugins/scala/colorization/ScalaColorizerFormat.java b/src/main/java/com/buransky/plugins/scala/colorization/ScalaColorizerFormat.java new file mode 100644 index 0000000..8c35cea --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/colorization/ScalaColorizerFormat.java @@ -0,0 +1,62 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.colorization; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.buransky.plugins.scala.language.Scala; +import org.sonar.api.web.CodeColorizerFormat; +import org.sonar.colorizer.CDocTokenizer; +import org.sonar.colorizer.CppDocTokenizer; +import org.sonar.colorizer.JavaAnnotationTokenizer; +import org.sonar.colorizer.JavadocTokenizer; +import org.sonar.colorizer.KeywordsTokenizer; +import org.sonar.colorizer.LiteralTokenizer; +import org.sonar.colorizer.Tokenizer; + +/** + * This class extends Sonar for code colorization of Scala source. + * + * @author Felix Müller + * @since 0.1 + */ +public class ScalaColorizerFormat extends CodeColorizerFormat { + + private static final String END_SPAN_TAG = ""; + + private static final List TOKENIZERS = Arrays.asList( + new LiteralTokenizer("", END_SPAN_TAG), + new KeywordsTokenizer("", END_SPAN_TAG, ScalaKeywords.getAllKeywords()), + new CDocTokenizer("", END_SPAN_TAG), + new CppDocTokenizer("", END_SPAN_TAG), + new JavadocTokenizer("", END_SPAN_TAG), + new JavaAnnotationTokenizer("", END_SPAN_TAG)); + + public ScalaColorizerFormat() { + super(Scala.INSTANCE.getKey()); + } + + @Override + public List getTokenizers() { + return Collections.unmodifiableList(TOKENIZERS); + } +} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/colorization/ScalaKeywords.java b/src/main/java/com/buransky/plugins/scala/colorization/ScalaKeywords.java new file mode 100644 index 0000000..c1debd8 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/colorization/ScalaKeywords.java @@ -0,0 +1,50 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.colorization; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * This is a helper class for collecting every Scala keyword. + * + * @author Felix Müller + * @since 0.1 + */ +public final class ScalaKeywords { + + private static final Set KEYWORDS = new HashSet(Arrays.asList( + "abstract", "assert", "case", "catch", "class", "def", "do", "else", "extends", "false", + "final", "finally", "for", "forSome", "if", "implicit", "import", "lazy", "match", "new", + "null", "object", "override", "package", "private", "protected", "requires", "return", + "sealed", "super", "this", "throw", "trait", "true", "try", "type", "val", "var", "while", + "with", "yield", "_", ":", "=", "=>", "<-", "<:", "<%", ">:", "#", "@" + )); + + private ScalaKeywords() { + // to prevent instantiation + } + + public static Set getAllKeywords() { + return Collections.unmodifiableSet(KEYWORDS); + } +} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/cpd/ScalaCpdMapping.java b/src/main/java/com/buransky/plugins/scala/cpd/ScalaCpdMapping.java new file mode 100644 index 0000000..bc2ebd7 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/cpd/ScalaCpdMapping.java @@ -0,0 +1,49 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.cpd; + +import com.buransky.plugins.scala.language.Scala; +import net.sourceforge.pmd.cpd.Tokenizer; + +import org.sonar.api.batch.AbstractCpdMapping; +import org.sonar.api.resources.Language; + +/** + * Glue Sonar and PMD CPD together. + * + * @since 0.1 + */ +public class ScalaCpdMapping extends AbstractCpdMapping { + + private final Scala scala; + + public ScalaCpdMapping(Scala scala) { + this.scala = scala; + } + + public Tokenizer getTokenizer() { + return new ScalaTokenizer(); + } + + public Language getLanguage() { + return scala; + } + +} diff --git a/src/main/java/com/buransky/plugins/scala/cpd/ScalaTokenizer.java b/src/main/java/com/buransky/plugins/scala/cpd/ScalaTokenizer.java new file mode 100644 index 0000000..3a345bc --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/cpd/ScalaTokenizer.java @@ -0,0 +1,52 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.cpd; + +import java.util.List; + +import net.sourceforge.pmd.cpd.SourceCode; +import net.sourceforge.pmd.cpd.TokenEntry; +import net.sourceforge.pmd.cpd.Tokenizer; +import net.sourceforge.pmd.cpd.Tokens; + +import org.sonar.plugins.scala.compiler.Lexer; +import org.sonar.plugins.scala.compiler.Token; + +/** + * Scala tokenizer for PMD CPD. + * + * @since 0.1 + */ +public final class ScalaTokenizer implements Tokenizer { + + public void tokenize(SourceCode source, Tokens cpdTokens) { + String filename = source.getFileName(); + + Lexer lexer = new Lexer(); + List tokens = lexer.getTokensOfFile(filename); + for (Token token : tokens) { + TokenEntry cpdToken = new TokenEntry(Integer.toString(token.tokenType()), filename, token.line()); + cpdTokens.add(cpdToken); + } + + cpdTokens.add(TokenEntry.getEOF()); + } + +} diff --git a/src/main/java/com/buransky/plugins/scala/language/Comment.java b/src/main/java/com/buransky/plugins/scala/language/Comment.java new file mode 100644 index 0000000..6f41e97 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/language/Comment.java @@ -0,0 +1,120 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.language; + +import java.io.IOException; +import java.util.List; + +import com.buransky.plugins.scala.util.StringUtils; +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; +import org.sonar.plugins.scala.language.CodeDetector; + +/** + * This class implements a Scala comment and the computation + * of several base metrics for a comment. + * + * @author Felix Müller + * @since 0.1 + */ +public class Comment { + + private final CommentType type; + private final List lines; + + public Comment(String content, CommentType type) throws IOException { + lines = StringUtils.convertStringToListOfLines(content); + this.type = type; + } + + public int getNumberOfLines() { + return lines.size() - getNumberOfBlankLines() - getNumberOfCommentedOutLinesOfCode(); + } + + public int getNumberOfBlankLines() { + int numberOfBlankLines = 0; + for (String comment : lines) { + boolean isBlank = true; + + for (int i = 0; isBlank && i < comment.length(); i++) { + char character = comment.charAt(i); + if (!Character.isWhitespace(character) && character != '*' && character != '/') { + isBlank = false; + } + } + + if (isBlank) { + numberOfBlankLines++; + } + } + return numberOfBlankLines; + } + + public int getNumberOfCommentedOutLinesOfCode() { + if (isDocComment()) { + return 0; + } + + int numberOfCommentedOutLinesOfCode = 0; + for (String line : lines) { + String strippedLine = org.apache.commons.lang.StringUtils.strip(line, " /*"); + if (CodeDetector.hasDetectedCode(strippedLine)) { + numberOfCommentedOutLinesOfCode++; + } + } + return numberOfCommentedOutLinesOfCode; + } + + public boolean isDocComment() { + return type == CommentType.DOC; + } + + public boolean isHeaderComment() { + return type == CommentType.HEADER; + } + + @Override + public int hashCode() { + return new HashCodeBuilder().append(type).append(lines).toHashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Comment)) { + return false; + } + + Comment other = (Comment) obj; + return new EqualsBuilder().append(type, other.type).append(lines, other.lines).isEquals(); + } + + @Override + public String toString() { + final String firstLine = lines.isEmpty() ? "" : lines.get(0); + final String lastLine = lines.isEmpty() ? "" : lines.get(lines.size() - 1); + return new ToStringBuilder(this).append("type", type) + .append("firstLine", firstLine) + .append("lastLine", lastLine) + .append("numberOfLines", getNumberOfLines()) + .append("numberOfCommentedOutLinesOfCode", getNumberOfCommentedOutLinesOfCode()) + .toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/language/CommentType.java b/src/main/java/com/buransky/plugins/scala/language/CommentType.java new file mode 100644 index 0000000..4784575 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/language/CommentType.java @@ -0,0 +1,34 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.language; + +/** + * This enum is a helper to distinguish between the + * different types of comments in Sonar. + * + * @author Felix Müller + * @since 0.1 + */ +public enum CommentType { + + NORMAL, + DOC, + HEADER; +} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/language/Scala.java b/src/main/java/com/buransky/plugins/scala/language/Scala.java new file mode 100644 index 0000000..27b3259 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/language/Scala.java @@ -0,0 +1,41 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.language; + +import org.sonar.api.resources.AbstractLanguage; + +/** + * This class implements Scala as a language for Sonar. + * + * @author Felix Müller + * @since 0.1 + */ +public class Scala extends AbstractLanguage { + + public static final Scala INSTANCE = new Scala(); + + public Scala() { + super("scala", "Scala"); + } + + public String[] getFileSuffixes() { + return new String[] { "scala" }; + } +} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/language/ScalaFile.java b/src/main/java/com/buransky/plugins/scala/language/ScalaFile.java new file mode 100644 index 0000000..668e67e --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/language/ScalaFile.java @@ -0,0 +1,141 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.language; + +import org.apache.commons.lang.StringUtils; +import org.sonar.api.resources.InputFile; +import org.sonar.api.resources.Language; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.resources.Resource; +import org.sonar.api.resources.Scopes; +import org.sonar.api.utils.WildcardPattern; +import org.sonar.plugins.scala.language.PackageResolver; + +/** + * This class implements a Scala source file for Sonar. + * + * @author Felix Müller + * @since 0.1 + */ +public class ScalaFile extends Resource { + + private final boolean isUnitTest; + private final String filename; + private final String longName; + private final ScalaPackage parent; + + public ScalaFile(String packageKey, String className, boolean isUnitTest) { + super(); + this.isUnitTest = isUnitTest; + filename = className.trim(); + + String key; + if (StringUtils.isBlank(packageKey)) { + packageKey = ScalaPackage.DEFAULT_PACKAGE_NAME; + key = new StringBuilder().append(packageKey).append(".").append(this.filename).toString(); + longName = filename; + } else { + packageKey = packageKey.trim(); + key = new StringBuilder().append(packageKey).append(".").append(this.filename).toString(); + longName = key; + } + parent = new ScalaPackage(packageKey); + setKey(key); + } + + @Override + public String getName() { + return filename; + } + + @Override + public String getLongName() { + return longName; + } + + @Override + public String getDescription() { + return null; + } + + @Override + public Language getLanguage() { + return Scala.INSTANCE; + } + + @Override + public String getScope() { + return Scopes.FILE; + } + + @Override + public String getQualifier() { + return isUnitTest ? Qualifiers.UNIT_TEST_FILE : Qualifiers.FILE; + } + + @Override + public ScalaPackage getParent() { + return parent; + } + + @Override + public boolean matchFilePattern(String antPattern) { + final String patternWithoutFileSuffix = StringUtils.substringBeforeLast(antPattern, "."); + final WildcardPattern matcher = WildcardPattern.create(patternWithoutFileSuffix, "."); + return matcher.match(getKey()); + } + + public boolean isUnitTest() { + return isUnitTest; + } + + /** + * Shortcut for {@link #fromInputFile(InputFile, boolean)} for source files. + */ + public static ScalaFile fromInputFile(InputFile inputFile) { + return ScalaFile.fromInputFile(inputFile, false); + } + + /** + * Creates a {@link ScalaFile} from a file in the source directories. + * + * @param inputFile the file object with relative path + * @param isUnitTest whether it is a unit test file or a source file + * @return the {@link ScalaFile} created if exists, null otherwise + */ + public static ScalaFile fromInputFile(InputFile inputFile, boolean isUnitTest) { + if (inputFile == null || inputFile.getFile() == null || inputFile.getRelativePath() == null) { + return null; + } + + final String packageName = PackageResolver.resolvePackageNameOfFile( + inputFile.getFile().getAbsolutePath()); + final String className = resolveClassName(inputFile); + return new ScalaFile(packageName, className, isUnitTest); + } + + private static String resolveClassName(InputFile inputFile) { + String classname = inputFile.getRelativePath(); + if (inputFile.getRelativePath().indexOf('/') >= 0) { + classname = StringUtils.substringAfterLast(inputFile.getRelativePath(), "/"); + } + return StringUtils.substringBeforeLast(classname, "."); + } +} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/language/ScalaPackage.java b/src/main/java/com/buransky/plugins/scala/language/ScalaPackage.java new file mode 100644 index 0000000..67fa99d --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/language/ScalaPackage.java @@ -0,0 +1,90 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.language; + +import org.apache.commons.lang.StringUtils; +import org.sonar.api.resources.Language; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.resources.Resource; +import org.sonar.api.resources.Scopes; +import org.sonar.api.utils.WildcardPattern; + +/** + * This class implements a logical Scala package. + * + * @author Felix Müller + * @since 0.1 + */ +@SuppressWarnings("rawtypes") +public class ScalaPackage extends Resource { + + public static final String DEFAULT_PACKAGE_NAME = "[default]"; + + public ScalaPackage() { + this(null); + } + + public ScalaPackage(String key) { + super(); + setKey(StringUtils.defaultIfEmpty(StringUtils.trim(key), DEFAULT_PACKAGE_NAME)); + } + + @Override + public String getName() { + return getKey(); + } + + @Override + public String getLongName() { + return null; + } + + @Override + public String getDescription() { + return null; + } + + @Override + public Language getLanguage() { + return Scala.INSTANCE; + } + + @Override + public String getScope() { + return Scopes.DIRECTORY; + } + + @Override + public String getQualifier() { + return Qualifiers.PACKAGE; + } + + @Override + public Resource getParent() { + return null; + } + + @Override + public boolean matchFilePattern(String antPattern) { + String patternWithoutFileSuffix = StringUtils.substringBeforeLast(antPattern, "."); + WildcardPattern matcher = WildcardPattern.create(patternWithoutFileSuffix, "."); + return matcher.match(getKey()); + } +} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/metrics/CommentsAnalyzer.java b/src/main/java/com/buransky/plugins/scala/metrics/CommentsAnalyzer.java new file mode 100644 index 0000000..f609140 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/metrics/CommentsAnalyzer.java @@ -0,0 +1,76 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.metrics; + +import java.util.List; + +import com.buransky.plugins.scala.language.Comment; + +/** + * This class implements the computation of basic + * line metrics for a {@link Comment}. + * + * @author Felix Müller + * @since 0.1 + */ +public class CommentsAnalyzer { + + private final List comments; + + public CommentsAnalyzer(List comments) { + this.comments = comments; + } + + public int countCommentLines() { + int commentLines = 0; + for (Comment comment : comments) { + if (!comment.isHeaderComment()) { + commentLines += comment.getNumberOfLines(); + } + } + return commentLines; + } + + public int countHeaderCommentLines() { + int headerCommentLines = 0; + for (Comment comment : comments) { + if (comment.isHeaderComment()) { + headerCommentLines += comment.getNumberOfLines(); + } + } + return headerCommentLines; + } + + public int countCommentedOutLinesOfCode() { + int commentedOutLinesOfCode = 0; + for (Comment comment : comments) { + commentedOutLinesOfCode += comment.getNumberOfCommentedOutLinesOfCode(); + } + return commentedOutLinesOfCode; + } + + public int countBlankCommentLines() { + int blankCommentLines = 0; + for (Comment comment : comments) { + blankCommentLines += comment.getNumberOfBlankLines(); + } + return blankCommentLines; + } +} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/metrics/LinesAnalyzer.java b/src/main/java/com/buransky/plugins/scala/metrics/LinesAnalyzer.java new file mode 100644 index 0000000..a538290 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/metrics/LinesAnalyzer.java @@ -0,0 +1,61 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.metrics; + +import java.util.List; + +import org.apache.commons.lang.StringUtils; + +/** + * This class implements the computation of basic + * line metrics for a {@link ScalaFile}. + * + * @author Felix Müller + * @since 0.1 + */ +public class LinesAnalyzer { + + private final List lines; + private final CommentsAnalyzer commentsAnalyzer; + + public LinesAnalyzer(List lines, CommentsAnalyzer commentsAnalyzer) { + this.lines = lines; + this.commentsAnalyzer = commentsAnalyzer; + } + + public int countLines() { + return lines.size(); + } + + public int countLinesOfCode() { + return countLines() - countBlankLines() - commentsAnalyzer.countCommentLines() + - commentsAnalyzer.countHeaderCommentLines() - commentsAnalyzer.countBlankCommentLines(); + } + + private int countBlankLines() { + int numberOfBlankLines = 0; + for (String line : lines) { + if (StringUtils.isBlank(line)) { + numberOfBlankLines++; + } + } + return numberOfBlankLines; + } +} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/sensor/AbstractScalaSensor.java b/src/main/java/com/buransky/plugins/scala/sensor/AbstractScalaSensor.java new file mode 100644 index 0000000..c98d876 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/sensor/AbstractScalaSensor.java @@ -0,0 +1,47 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.sensor; + +import com.buransky.plugins.scala.language.Scala; +import org.sonar.api.batch.Sensor; +import org.sonar.api.resources.Project; + +/** + * This is a helper base class for sensors that should only be executed on Scala projects. + * + * @author Felix Müller + * @since 0.1 + */ +public abstract class AbstractScalaSensor implements Sensor { + + private final Scala scala; + + protected AbstractScalaSensor(Scala scala) { + this.scala = scala; + } + + public final boolean shouldExecuteOnProject(Project project) { + return project.getLanguage().equals(scala); + } + + public final Scala getScala() { + return scala; + } +} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/sensor/BaseMetricsSensor.java b/src/main/java/com/buransky/plugins/scala/sensor/BaseMetricsSensor.java new file mode 100644 index 0000000..f2ab2b4 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/sensor/BaseMetricsSensor.java @@ -0,0 +1,162 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.sensor; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.buransky.plugins.scala.language.Comment; +import com.buransky.plugins.scala.language.Scala; +import com.buransky.plugins.scala.language.ScalaFile; +import com.buransky.plugins.scala.metrics.CommentsAnalyzer; +import com.buransky.plugins.scala.metrics.LinesAnalyzer; +import com.buransky.plugins.scala.util.StringUtils; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.batch.SensorContext; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.resources.InputFile; +import org.sonar.api.resources.Project; +import org.sonar.api.resources.ProjectFileSystem; +import org.sonar.plugins.scala.compiler.Lexer; +import com.buransky.plugins.scala.language.ScalaPackage; +import org.sonar.plugins.scala.metrics.ComplexityCalculator; +import org.sonar.plugins.scala.metrics.FunctionCounter; +import org.sonar.plugins.scala.metrics.PublicApiCounter; +import org.sonar.plugins.scala.metrics.StatementCounter; +import org.sonar.plugins.scala.metrics.TypeCounter; +import org.sonar.plugins.scala.util.MetricDistribution; + +/** + * This is the main sensor of the Scala plugin. It gathers all results + * of the computation of base metrics for all Scala resources. + * + * @author Felix Müller + * @since 0.1 + */ +public class BaseMetricsSensor extends AbstractScalaSensor { + + private static final Logger LOGGER = LoggerFactory.getLogger(BaseMetricsSensor.class); + + public BaseMetricsSensor(Scala scala) { + super(scala); + } + + public void analyse(Project project, SensorContext sensorContext) { + final ProjectFileSystem fileSystem = project.getFileSystem(); + final String charset = fileSystem.getSourceCharset().toString(); + final Set packages = new HashSet(); + + MetricDistribution complexityOfClasses = null; + MetricDistribution complexityOfFunctions = null; + + for (InputFile inputFile : fileSystem.mainFiles(getScala().getKey())) { + final ScalaFile scalaFile = ScalaFile.fromInputFile(inputFile); + packages.add(scalaFile.getParent()); + sensorContext.saveMeasure(scalaFile, CoreMetrics.FILES, 1.0); + + try { + final String source = FileUtils.readFileToString(inputFile.getFile(), charset); + final List lines = StringUtils.convertStringToListOfLines(source); + final List comments = new Lexer().getComments(source); + + final CommentsAnalyzer commentsAnalyzer = new CommentsAnalyzer(comments); + final LinesAnalyzer linesAnalyzer = new LinesAnalyzer(lines, commentsAnalyzer); + + addLineMetrics(sensorContext, scalaFile, linesAnalyzer); + addCommentMetrics(sensorContext, scalaFile, commentsAnalyzer); + addCodeMetrics(sensorContext, scalaFile, source); + addPublicApiMetrics(sensorContext, scalaFile, source); + + complexityOfClasses = sumUpMetricDistributions(complexityOfClasses, + ComplexityCalculator.measureComplexityOfClasses(source)); + + complexityOfFunctions = sumUpMetricDistributions(complexityOfFunctions, + ComplexityCalculator.measureComplexityOfFunctions(source)); + + } catch (IOException ioe) { + LOGGER.error("Could not read the file: " + inputFile.getFile().getAbsolutePath(), ioe); + } + } + + if (complexityOfClasses != null) + sensorContext.saveMeasure(complexityOfClasses.getMeasure()); + + if (complexityOfFunctions != null) + sensorContext.saveMeasure(complexityOfFunctions.getMeasure()); + + computePackagesMetric(sensorContext, packages); + } + + private void addLineMetrics(SensorContext sensorContext, ScalaFile scalaFile, LinesAnalyzer linesAnalyzer) { + sensorContext.saveMeasure(scalaFile, CoreMetrics.LINES, (double) linesAnalyzer.countLines()); + sensorContext.saveMeasure(scalaFile, CoreMetrics.NCLOC, (double) linesAnalyzer.countLinesOfCode()); + } + + private void addCommentMetrics(SensorContext sensorContext, ScalaFile scalaFile, + CommentsAnalyzer commentsAnalyzer) { + sensorContext.saveMeasure(scalaFile, CoreMetrics.COMMENT_LINES, + (double) commentsAnalyzer.countCommentLines()); + sensorContext.saveMeasure(scalaFile, CoreMetrics.COMMENTED_OUT_CODE_LINES, + (double) commentsAnalyzer.countCommentedOutLinesOfCode()); + } + + private void addCodeMetrics(SensorContext sensorContext, ScalaFile scalaFile, String source) { + sensorContext.saveMeasure(scalaFile, CoreMetrics.CLASSES, + (double) TypeCounter.countTypes(source)); + sensorContext.saveMeasure(scalaFile, CoreMetrics.STATEMENTS, + (double) StatementCounter.countStatements(source)); + sensorContext.saveMeasure(scalaFile, CoreMetrics.FUNCTIONS, + (double) FunctionCounter.countFunctions(source)); + sensorContext.saveMeasure(scalaFile, CoreMetrics.COMPLEXITY, + (double) ComplexityCalculator.measureComplexity(source)); + } + + private void addPublicApiMetrics(SensorContext sensorContext, ScalaFile scalaFile, String source) { + sensorContext.saveMeasure(scalaFile, CoreMetrics.PUBLIC_API, + (double) PublicApiCounter.countPublicApi(source)); + sensorContext.saveMeasure(scalaFile, CoreMetrics.PUBLIC_UNDOCUMENTED_API, + (double) PublicApiCounter.countUndocumentedPublicApi(source)); + } + + private MetricDistribution sumUpMetricDistributions(MetricDistribution oldDistribution, + MetricDistribution newDistribution) { + if (oldDistribution == null) { + return newDistribution; + } + + oldDistribution.add(newDistribution); + return oldDistribution; + } + + private void computePackagesMetric(SensorContext sensorContext, Set packages) { + for (ScalaPackage currentPackage : packages) { + sensorContext.saveMeasure(currentPackage, CoreMetrics.PACKAGES, 1.0); + } + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensor.java b/src/main/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensor.java new file mode 100644 index 0000000..6fd15a4 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensor.java @@ -0,0 +1,89 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.sensor; + +import java.io.IOException; + +import com.buransky.plugins.scala.language.Scala; +import com.buransky.plugins.scala.language.ScalaFile; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.batch.Phase; +import org.sonar.api.batch.Phase.Name; +import org.sonar.api.batch.SensorContext; +import org.sonar.api.resources.InputFile; +import org.sonar.api.resources.Project; +import org.sonar.api.resources.ProjectFileSystem; + +/** + * This Sensor imports all Scala files into Sonar. + * + * @author Felix Müller + * @since 0.1 + */ +@Phase(name = Name.PRE) +public class ScalaSourceImporterSensor extends AbstractScalaSensor { + + private static final Logger LOGGER = LoggerFactory.getLogger(ScalaSourceImporterSensor.class); + + public ScalaSourceImporterSensor(Scala scala) { + super(scala); + } + + public void analyse(Project project, SensorContext sensorContext) { + ProjectFileSystem fileSystem = project.getFileSystem(); + String charset = fileSystem.getSourceCharset().toString(); + + for (InputFile sourceFile : fileSystem.mainFiles(getScala().getKey())) { + addFileToSonar(sensorContext, sourceFile, false, charset); + } + + for (InputFile testFile : fileSystem.testFiles(getScala().getKey())) { + addFileToSonar(sensorContext, testFile, true, charset); + } + } + + private void addFileToSonar(SensorContext sensorContext, InputFile inputFile, + boolean isUnitTest, String charset) { + try { + String source = FileUtils.readFileToString(inputFile.getFile(), charset); + ScalaFile resource = ScalaFile.fromInputFile(inputFile, isUnitTest); + + sensorContext.index(resource); + sensorContext.saveSource(resource, source); + + if (LOGGER.isDebugEnabled()) { + if (isUnitTest) { + LOGGER.debug("Added Scala test file to Sonar: " + inputFile.getFile().getAbsolutePath()); + } else { + LOGGER.debug("Added Scala source file to Sonar: " + inputFile.getFile().getAbsolutePath()); + } + } + } catch (IOException ioe) { + LOGGER.error("Could not read the file: " + inputFile.getFile().getAbsolutePath(), ioe); + } + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/surefire/SurefireSensor.java b/src/main/java/com/buransky/plugins/scala/surefire/SurefireSensor.java new file mode 100644 index 0000000..4a07474 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/surefire/SurefireSensor.java @@ -0,0 +1,75 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.surefire; + +import com.buransky.plugins.scala.language.Scala; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.batch.CoverageExtension; +import org.sonar.api.batch.DependsUpon; +import org.sonar.api.batch.Sensor; +import org.sonar.api.batch.SensorContext; +import org.sonar.api.resources.Project; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.resources.Resource; +import org.sonar.plugins.surefire.api.AbstractSurefireParser; +import org.sonar.plugins.surefire.api.SurefireUtils; + +import java.io.File; + +public class SurefireSensor implements Sensor { + + private static final Logger LOG = LoggerFactory.getLogger(SurefireSensor.class); + + @DependsUpon + public Class dependsUponCoverageSensors() { + return CoverageExtension.class; + } + + public boolean shouldExecuteOnProject(Project project) { + return project.getAnalysisType().isDynamic(true) && Scala.INSTANCE.getKey().equals(project.getLanguageKey()); + } + + public void analyse(Project project, SensorContext context) { + File dir = SurefireUtils.getReportsDirectory(project); + collect(project, context, dir); + } + + protected void collect(Project project, SensorContext context, File reportsDir) { + LOG.info("parsing {}", reportsDir); + SUREFIRE_PARSER.collect(project, context, reportsDir); + } + + private static final AbstractSurefireParser SUREFIRE_PARSER = new AbstractSurefireParser() { + @Override + protected Resource getUnitTestResource(String classKey) { + String filename = classKey.replace('.', '/') + ".scala"; + org.sonar.api.resources.File sonarFile = new org.sonar.api.resources.File(filename); + sonarFile.setQualifier(Qualifiers.UNIT_TEST_FILE); + return sonarFile; + } + }; + + @Override + public String toString() { + return "Scala SurefireSensor"; + } + +} diff --git a/src/main/java/com/buransky/plugins/scala/util/StringUtils.java b/src/main/java/com/buransky/plugins/scala/util/StringUtils.java new file mode 100644 index 0000000..7e32d81 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/util/StringUtils.java @@ -0,0 +1,57 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class StringUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(StringUtils.class); + + private StringUtils() { + // to prevent instantiation + } + + public static List convertStringToListOfLines(String string) throws IOException { + final List lines = new ArrayList(); + BufferedReader reader = null; + try { + reader = new BufferedReader(new StringReader(string)); + String line = null; + while ((line = reader.readLine()) != null) { + lines.add(line); + } + } catch (IOException ioe) { + LOGGER.error("Error while reading the lines of a given string", ioe); + throw ioe; + } finally { + IOUtils.closeQuietly(reader); + } + return lines; + } +} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/compiler/Compiler.scala b/src/main/scala/com/buransky/plugins/scala/compiler/Compiler.scala new file mode 100644 index 0000000..81cc60c --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scala/compiler/Compiler.scala @@ -0,0 +1,39 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.compiler + +import tools.nsc._ +import tools.util.PathResolver._ +import com.buransky.plugins.scala.ScalaPlugin + +/** + * This is a wrapper for the Scala compiler. It is used to access + * the compiler in a more convenient way. + * + * @author Felix Müller + * @since 0.1 + */ +object Compiler extends Global(new Settings()) { + + settings.classpath.append(ScalaPlugin.getPathToScalaLibrary()) + new Run + + override def forScaladoc = true +} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/compiler/Lexer.scala b/src/main/scala/com/buransky/plugins/scala/compiler/Lexer.scala new file mode 100644 index 0000000..b50b1f0 --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scala/compiler/Lexer.scala @@ -0,0 +1,124 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.compiler + +import collection.JavaConversions._ +import collection.mutable.ListBuffer +import tools.nsc._ +import io.AbstractFile + +import com.buransky.plugins.scala.language.{CommentType, Comment} + +/** + * This class is a wrapper for accessing the lexer of the Scala compiler + * from Java in a more convenient way. + * + * @author Felix Müller + * @since 0.1 + */ +class Lexer { + + import Compiler._ + + def getTokens(code: String) : java.util.List[Token] = { + val unit = new CompilationUnit(new util.BatchSourceFile("", code.toCharArray)) + tokenize(unit) + } + + def getTokensOfFile(path: String) : java.util.List[Token] = { + val unit = new CompilationUnit(new util.BatchSourceFile(AbstractFile.getFile(path))) + tokenize(unit) + } + + private def tokenize(unit: CompilationUnit) : java.util.List[Token] = { + val scanner = new syntaxAnalyzer.UnitScanner(unit) + val tokens = ListBuffer[Token]() + + scanner.init() + while (scanner.token != scala.tools.nsc.ast.parser.Tokens.EOF) { + tokens += Token(scanner.token, scanner.parensAnalyzer.line(scanner.offset) + 1) + scanner.nextToken() + } + tokens + } + + def getComments(code: String) : java.util.List[Comment] = { + val unit = new CompilationUnit(new util.BatchSourceFile("", code.toCharArray)) + tokenizeComments(unit) + } + + def getCommentsOfFile(path: String) : java.util.List[Comment] = { + val unit = new CompilationUnit(new util.BatchSourceFile(AbstractFile.getFile(path))) + tokenizeComments(unit) + } + + private def tokenizeComments(unit: CompilationUnit) : java.util.List[Comment] = { + val comments = ListBuffer[Comment]() + val scanner = new syntaxAnalyzer.UnitScanner(unit) { + + private var lastDocCommentRange: Option[Range] = None + + private var foundToken = false + + override def nextToken() { + super.nextToken() + foundToken = token != 0 + } + + override def foundComment(value: String, start: Int, end: Int) = { + super.foundComment(value, start, end) + + def isHeaderComment(value: String) = { + !foundToken && comments.isEmpty && value.trim().startsWith("/*") + } + + lastDocCommentRange match { + + case Some(r: Range) => { + if (r.start != start || r.end != end) { + comments += new Comment(value, CommentType.NORMAL) + } + } + + case None => { + if (isHeaderComment(value)) { + comments += new Comment(value, CommentType.HEADER) + } else { + comments += new Comment(value, CommentType.NORMAL) + } + } + } + } + + override def foundDocComment(value: String, start: Int, end: Int) = { + super.foundDocComment(value, start, end) + comments += new Comment(value, CommentType.DOC) + lastDocCommentRange = Some(Range(start, end)) + } + } + + scanner.init() + while (scanner.token != scala.tools.nsc.ast.parser.Tokens.EOF) { + scanner.nextToken() + } + + comments + } +} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/compiler/Parser.scala b/src/main/scala/com/buransky/plugins/scala/compiler/Parser.scala new file mode 100644 index 0000000..6b8723b --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scala/compiler/Parser.scala @@ -0,0 +1,62 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.compiler + +import tools.nsc._ +import io.AbstractFile + +/** + * This class is a wrapper for accessing the parser of the Scala compiler + * from Java in a more convenient way. + * + * @author Felix Müller + * @since 0.1 + */ +class Parser { + + import Compiler._ + + def parse(code: String) : Tree = { + val batchSourceFile = new util.BatchSourceFile("", code.toCharArray) + parse(batchSourceFile, code.toCharArray) + } + + def parseFile(path: String) = { + val batchSourceFile = new util.BatchSourceFile(AbstractFile.getFile(path)) + parse(batchSourceFile, batchSourceFile.content.array) + } + + private def parse(batchSourceFile: util.BatchSourceFile, code: Array[Char]) = { + val scriptSourceFile = new util.ScriptSourceFile(batchSourceFile, code, 0) + try { + val parser = new syntaxAnalyzer.SourceFileParser(scriptSourceFile) + val tree = parser.templateStatSeq(false)._2 + parser.makePackaging(0, parser.atPos(0, 0, 0)(Ident(nme.EMPTY_PACKAGE_NAME)), tree) + } catch { + case _ => { + val unit = new CompilationUnit(batchSourceFile) + val unitParser = new syntaxAnalyzer.UnitParser(unit) { + override def showSyntaxErrors() { } + } + unitParser.smartParse() + } + } + } +} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/compiler/Token.scala b/src/main/scala/com/buransky/plugins/scala/compiler/Token.scala new file mode 100644 index 0000000..e8fc0bb --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scala/compiler/Token.scala @@ -0,0 +1,27 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.compiler + +/** + * Represent a token. Lines must start at 1. + * + * @since 0.1 + */ +case class Token(tokenType: Int, line: Int) diff --git a/src/main/scala/com/buransky/plugins/scala/language/CodeDetector.scala b/src/main/scala/com/buransky/plugins/scala/language/CodeDetector.scala new file mode 100644 index 0000000..c30969b --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scala/language/CodeDetector.scala @@ -0,0 +1,68 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.language + +import scala.tools.nsc.symtab.StdNames + +import org.sonar.plugins.scala.compiler.{ Compiler, Parser } + +/** + * This object is a helper object for detecting valid Scala code + * in a given piece of source code. + * + * @author Felix Müller + * @since 0.1 + */ +object CodeDetector { + + import Compiler._ + + private lazy val parser = new Parser() + + def hasDetectedCode(code: String) = { + + def lookingForSyntaxTreesWithCode(tree: Tree) : Boolean = tree match { + + case PackageDef(identifier: RefTree, content) => + if (!identifier.name.equals(nme.EMPTY_PACKAGE_NAME)) { + true + } else { + content.exists(lookingForSyntaxTreesWithCode) + } + + case Apply(function, args) => + args.exists(lookingForSyntaxTreesWithCode) + + case ClassDef(_, _, _, _) + | ModuleDef(_, _, _) + | ValDef(_, _, _, _) + | DefDef(_, _, _, _, _, _) + | Function(_ , _) + | Assign(_, _) + | LabelDef(_, _, _) => + true + + case _ => + false + } + + lookingForSyntaxTreesWithCode(parser.parse(code)) + } +} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/language/PackageResolver.scala b/src/main/scala/com/buransky/plugins/scala/language/PackageResolver.scala new file mode 100644 index 0000000..76e7be1 --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scala/language/PackageResolver.scala @@ -0,0 +1,76 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.language + +import org.sonar.plugins.scala.compiler.{ Compiler, Parser } + +/** + * This object is a helper object for resolving the package name of + * a given Scala file. + * + * @author Felix Müller + * @since 0.1 + */ +object PackageResolver { + + import Compiler._ + + private lazy val parser = new Parser() + + /** + * This function resolves the upper package name of a given file. + * + * @param path the path of the given file + * @return the upper package name + */ + def resolvePackageNameOfFile(path: String) : String = { + + def traversePackageDefs(tree: Tree) : Seq[String] = tree match { + + case PackageDef(Ident(name), List(p: PackageDef)) => + List(name.toString()) ++ traversePackageDefs(p) + + case PackageDef(s: Select, List(p: PackageDef)) => + traversePackageDefs(s) ++ traversePackageDefs(p) + + case PackageDef(Ident(name), _) => + List(name.toString()) + + case PackageDef(s: Select, _) => + traversePackageDefs(s) + + case Select(Ident(identName), name) => + List(identName.toString(), name.toString()) + + case Select(qualifiers, name) => + traversePackageDefs(qualifiers) ++ List(name.toString()) + + case _ => + Nil + } + + val packageName = traversePackageDefs(parser.parseFile(path)).foldLeft("")(_ + "." + _) + if (packageName.length() > 0) { + packageName.substring(1) + } else { + packageName + } + } +} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/metrics/ComplexityCalculator.scala b/src/main/scala/com/buransky/plugins/scala/metrics/ComplexityCalculator.scala new file mode 100644 index 0000000..31ae409 --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scala/metrics/ComplexityCalculator.scala @@ -0,0 +1,115 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.metrics + +import collection.mutable.{ ListBuffer, HashMap } + +import org.sonar.api.measures.{ CoreMetrics, Measure, Metric } +import org.sonar.plugins.scala.util.MetricDistribution + +import scalariform.lexer.Tokens._ +import scalariform.parser._ + +/** + * This object is a helper object for measuring complexity + * in a given Scala source. + * + * @author Felix Müller + * @since 0.1 + */ +object ComplexityCalculator { + + private lazy val classComplexityRanges = Array[Number](0, 5, 10, 20, 30, 60, 90) + private lazy val functionComplexityRanges = Array[Number](1, 2, 4, 6, 8, 10, 12) + + def measureComplexity(source: String) : Int = ScalaParser.parse(source) match { + case Some(ast) => measureComplexity(ast) + case _ => 0 + } + + def measureComplexityOfClasses(source: String) : MetricDistribution = { + measureComplexityDistribution(source, CoreMetrics.CLASS_COMPLEXITY_DISTRIBUTION, + classComplexityRanges, classOf[TmplDef]) + } + + def measureComplexityOfFunctions(source: String) : MetricDistribution = { + measureComplexityDistribution(source, CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION, + functionComplexityRanges, classOf[FunDefOrDcl]) + } + + private def measureComplexityDistribution(source: String, metric: Metric, ranges: Array[Number], + typeOfTree: Class[_ <: AstNode]) = { + + def allTreesIn(source: String) : Seq[AstNode] = ScalaParser.parse(source) match { + case Some(ast) => collectTrees(ast, typeOfTree) + case _ => Nil + } + + val distribution = new MetricDistribution(metric, ranges) + allTreesIn(source).foreach(ast => distribution.add(measureComplexity(ast))) + distribution + } + + private def measureComplexity(ast: AstNode) : Int = { + var complexity = 0 + + // TODO measure complexity of return statements + // TODO howto handle nested classes and functions? should + // surrounding function complexity consist of inner function and its own or only it own one? + def measureComplexityOfTree(tree: AstNode) { + tree match { + + case CaseClause(_, _) + | DoExpr(_, _, _, _, _) + | ForExpr(_, _, _, _, _, _, _) + | FunDefOrDcl(_, _, _, _, _, _, _) + | IfExpr(_, _, _, _, _) + | WhileExpr(_, _, _, _) => + complexity += 1 + + case expr: Expr => + if (expr.tokens.head.tokenType == THROW) { + complexity += 1 + } + + case _ => + } + + tree.immediateChildren.foreach(measureComplexityOfTree) + } + + measureComplexityOfTree(ast) + complexity + } + + private def collectTrees(ast: AstNode, typeOfTree: Class[_ <: AstNode]) : Seq[AstNode] = { + val nodes = ListBuffer[AstNode]() + + def collectTreesOfSpecificType(tree: AstNode) { + if (tree.getClass == typeOfTree) { + nodes += tree + } + tree.immediateChildren.foreach(collectTreesOfSpecificType) + } + + collectTreesOfSpecificType(ast) + nodes + } +} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/metrics/FunctionCounter.scala b/src/main/scala/com/buransky/plugins/scala/metrics/FunctionCounter.scala new file mode 100644 index 0000000..92bfac4 --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scala/metrics/FunctionCounter.scala @@ -0,0 +1,108 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.metrics + +import org.sonar.plugins.scala.compiler.{ Compiler, Parser } + +/** + * This object is a helper object for counting all functions + * in a given Scala source. + * + * @author Felix Müller + * @since 0.1 + */ +object FunctionCounter { + + import Compiler._ + + private lazy val parser = new Parser() + + // TODO improve counting functions + def countFunctions(source: String) = { + + def countFunctionTrees(tree: Tree, foundFunctions: Int = 0) : Int = tree match { + + // recursive descent until found a syntax tree with countable functions + case PackageDef(_, content) => + foundFunctions + onList(content, countFunctionTrees(_, 0)) + + case Template(_, _, content) => + foundFunctions + onList(content, countFunctionTrees(_, 0)) + + case ClassDef(_, _, _, content) => + countFunctionTrees(content, foundFunctions) + + case ModuleDef(_, _, content) => + countFunctionTrees(content, foundFunctions) + + case DocDef(_, content) => + countFunctionTrees(content, foundFunctions) + + case ValDef(_, _, _, content) => + countFunctionTrees(content, foundFunctions) + + case Block(stats, expr) => + foundFunctions + onList(stats, countFunctionTrees(_, 0)) + countFunctionTrees(expr) + + case Apply(_, args) => + foundFunctions + onList(args, countFunctionTrees(_, 0)) + + case Assign(_, rhs) => + countFunctionTrees(rhs, foundFunctions) + + case LabelDef(_, _, rhs) => + countFunctionTrees(rhs, foundFunctions) + + case If(cond, thenBlock, elseBlock) => + foundFunctions + countFunctionTrees(cond) + countFunctionTrees(thenBlock) + countFunctionTrees(elseBlock) + + case Match(selector, cases) => + foundFunctions + countFunctionTrees(selector) + onList(cases, countFunctionTrees(_, 0)) + + case CaseDef(pat, guard, body) => + foundFunctions + countFunctionTrees(pat) + countFunctionTrees(guard) + countFunctionTrees(body) + + case Try(block, catches, finalizer) => + foundFunctions + countFunctionTrees(block) + onList(catches, countFunctionTrees(_, 0)) + + case Throw(expr) => + countFunctionTrees(expr, foundFunctions) + + case Function(_, body) => + countFunctionTrees(body, foundFunctions) + + /* + * Countable function declarations are functions and methods. + */ + + case defDef: DefDef => + if (isEmptyConstructor(defDef)) { + countFunctionTrees(defDef.rhs, foundFunctions) + } else { + countFunctionTrees(defDef.rhs, foundFunctions + 1) + } + + case _ => + foundFunctions + } + + countFunctionTrees(parser.parse(source)) + } +} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/metrics/PublicApiCounter.scala b/src/main/scala/com/buransky/plugins/scala/metrics/PublicApiCounter.scala new file mode 100644 index 0000000..8369d29 --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scala/metrics/PublicApiCounter.scala @@ -0,0 +1,98 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.metrics + +import reflect.generic.ModifierFlags +import org.sonar.plugins.scala.compiler.{ Compiler, Parser } + +/** + * This object is a helper object for counting public api members. + * + * @author Felix Müller + * @since 0.1 + */ +object PublicApiCounter { + + import Compiler._ + + private lazy val parser = new Parser() + + private case class PublicApi(isDocumented: Boolean) + + def countPublicApi(source: String) = { + countPublicApiTrees(parser.parse(source)).size + } + + def countUndocumentedPublicApi(source: String) = { + countPublicApiTrees(parser.parse(source)).count(!_.isDocumented) + } + + private def countPublicApiTrees(tree: Tree, wasDocDefBefore: Boolean = false, + foundPublicApiMembers: List[PublicApi] = Nil) : List[PublicApi] = tree match { + + // recursive descent until found a syntax tree with countable public api declarations + case PackageDef(_, content) => + foundPublicApiMembers ++ content.flatMap(countPublicApiTrees(_, false, Nil)) + + case Template(_, _, content) => + foundPublicApiMembers ++ content.flatMap(countPublicApiTrees(_, false, Nil)) + + case DocDef(_, content) => + countPublicApiTrees(content, true, foundPublicApiMembers) + + case Block(stats, expr) => + foundPublicApiMembers ++ stats.flatMap(countPublicApiTrees(_, false, Nil)) ++ countPublicApiTrees(expr) + + case Apply(_, args) => + foundPublicApiMembers ++ args.flatMap(countPublicApiTrees(_, false, Nil)) + + case classDef: ClassDef if (classDef.mods.hasFlag(ModifierFlags.PRIVATE)) => + countPublicApiTrees(classDef.impl, false, foundPublicApiMembers) + + case moduleDef: ModuleDef if (moduleDef.mods.hasFlag(ModifierFlags.PRIVATE)) => + countPublicApiTrees(moduleDef.impl, false, foundPublicApiMembers) + + case defDef: DefDef if (isEmptyConstructor(defDef) || defDef.mods.hasFlag(ModifierFlags.PRIVATE)) => + countPublicApiTrees(defDef.rhs, false, foundPublicApiMembers) + + case valDef: ValDef if (valDef.mods.hasFlag(ModifierFlags.PRIVATE)) => + countPublicApiTrees(valDef.rhs, false, foundPublicApiMembers) + + /* + * Countable public api declarations are classes, objects, traits, functions, + * methods and attributes with public access. + */ + + case ClassDef(_, _, _, impl) => + countPublicApiTrees(impl, false, foundPublicApiMembers ++ List(PublicApi(wasDocDefBefore))) + + case ModuleDef(_, _, impl) => + countPublicApiTrees(impl, false, foundPublicApiMembers ++ List(PublicApi(wasDocDefBefore))) + + case defDef: DefDef => + foundPublicApiMembers ++ List(PublicApi(wasDocDefBefore)) + + case valDef: ValDef => + foundPublicApiMembers ++ List(PublicApi(wasDocDefBefore)) + + case _ => + foundPublicApiMembers + } +} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/metrics/StatementCounter.scala b/src/main/scala/com/buransky/plugins/scala/metrics/StatementCounter.scala new file mode 100644 index 0000000..7502547 --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scala/metrics/StatementCounter.scala @@ -0,0 +1,114 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.metrics + +import scalariform.parser._ + +/** + * This object is a helper object for counting all statements + * in a given Scala source. + * + * @author Felix Müller + * @since 0.1 + */ +object StatementCounter { + + def countStatements(source: String) = { + + def countStatementTreesOnList(trees: List[AstNode]) : Int = { + trees.map(countStatementTrees(_)).foldLeft(0)(_ + _) + } + + def countStatementsOfDefOrDcl(body: AstNode) : Int = { + val bodyStatementCount = countStatementTrees(body) + if (bodyStatementCount == 0) { + 1 + } else { + bodyStatementCount + } + } + + def countStatementTrees(tree: AstNode, foundStatements: Int = 0) : Int = tree match { + + case AnonymousFunction(_, _, body) => + foundStatements + countStatementTrees(body) + + case FunDefOrDcl(_, _, _, _, _, funBodyOption, _) => + funBodyOption match { + case Some(funBody) => + foundStatements + countStatementsOfDefOrDcl(funBody) + case _ => + foundStatements + } + + case PatDefOrDcl(_, _, _, _, equalsClauseOption) => + equalsClauseOption match { + case Some(equalsClause) => + foundStatements + countStatementsOfDefOrDcl(equalsClause._2) + case _ => + foundStatements + } + + case ForExpr(_, _, _, _, _, yieldOption, body) => + val bodyStatementCount = countStatementTrees(body) + yieldOption match { + case Some(_) => + if (bodyStatementCount == 0) { + foundStatements + 2 + } else { + foundStatements + bodyStatementCount + 1 + } + + case _ => + foundStatements + bodyStatementCount + 1 + } + + case IfExpr(_, _, _, body, elseClauseOption) => + elseClauseOption match { + case Some(elseClause) => + foundStatements + 1 + countStatementTrees(body) + countStatementTrees(elseClause) + case _ => + foundStatements + 1 + countStatementTrees(body) + } + + case ElseClause(_, _, elseBody) => + countStatementTrees(elseBody, foundStatements + 1) + + case CallExpr(exprDotOpt, _, _, newLineOptsAndArgumentExpr, _) => + val bodyStatementCount = countStatementTreesOnList(newLineOptsAndArgumentExpr.map(_._2)) + if (bodyStatementCount > 1) { + foundStatements + 1 + bodyStatementCount + } else { + foundStatements + 1 + } + + case InfixExpr(_, _, _, _) | PostfixExpr(_, _) => + foundStatements + 1 + + case _ => + foundStatements + countStatementTreesOnList(tree.immediateChildren) + } + + ScalaParser.parse(source) match { + case Some(ast) => countStatementTrees(ast) + case _ => 0 + } + } +} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/metrics/TypeCounter.scala b/src/main/scala/com/buransky/plugins/scala/metrics/TypeCounter.scala new file mode 100644 index 0000000..aef7266 --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scala/metrics/TypeCounter.scala @@ -0,0 +1,96 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.metrics + +import org.sonar.plugins.scala.compiler.{ Compiler, Parser } + +/** + * This object is a helper object for counting all types + * in a given Scala source. + * + * @author Felix Müller + * @since 0.1 + */ +object TypeCounter { + + import Compiler._ + + private lazy val parser = new Parser() + + def countTypes(source: String) = { + + def countTypeTrees(tree: Tree, foundTypes: Int = 0) : Int = tree match { + + // recursive descent until found a syntax tree with countable type declaration + case PackageDef(_, content) => + foundTypes + onList(content, countTypeTrees(_, 0)) + + case Template(_, _, content) => + foundTypes + onList(content, countTypeTrees(_, 0)) + + case DocDef(_, content) => + countTypeTrees(content, foundTypes) + + case CaseDef(pat, guard, body) => + foundTypes + countTypeTrees(pat) + countTypeTrees(guard) + countTypeTrees(body) + + case DefDef(_, _, _, _, _, content) => + countTypeTrees(content, foundTypes) + + case ValDef(_, _, _, content) => + countTypeTrees(content, foundTypes) + + case Assign(_, rhs) => + countTypeTrees(rhs, foundTypes) + + case LabelDef(_, _, rhs) => + countTypeTrees(rhs, foundTypes) + + case If(cond, thenBlock, elseBlock) => + foundTypes + countTypeTrees(cond) + countTypeTrees(thenBlock) + countTypeTrees(elseBlock) + + case Block(stats, expr) => + foundTypes + onList(stats, countTypeTrees(_, 0)) + countTypeTrees(expr) + + case Match(selector, cases) => + foundTypes + countTypeTrees(selector) + onList(cases, countTypeTrees(_, 0)) + + case Try(block, catches, finalizer) => + foundTypes + countTypeTrees(block) + onList(catches, countTypeTrees(_, 0)) + countTypeTrees(finalizer) + + /* + * Countable type declarations are classes, traits and objects. + * ClassDef represents classes and traits. + * ModuleDef is the syntax tree for object declarations. + */ + + case ClassDef(_, _, _, content) => + countTypeTrees(content, foundTypes + 1) + + case ModuleDef(_, _, content) => + countTypeTrees(content, foundTypes + 1) + + case _ => + foundTypes + } + + countTypeTrees(parser.parse(source)) + } +} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/metrics/package.scala b/src/main/scala/com/buransky/plugins/scala/metrics/package.scala new file mode 100644 index 0000000..08722dd --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scala/metrics/package.scala @@ -0,0 +1,68 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala + +import org.sonar.plugins.scala.compiler.Compiler._ + +package object metrics { + + def isEmptyBlock(block: Tree) = block match { + case literal: Literal => + val isEmptyConstant = literal.value match { + case Constant(value) => value.toString().equals("()") + case _ => false + } + literal.isEmpty || isEmptyConstant + + case _ => block.isEmpty + } + + def isEmptyConstructor(constructor: DefDef) = { + if (constructor.name.startsWith(nme.CONSTRUCTOR) || + constructor.name.startsWith(nme.MIXIN_CONSTRUCTOR)) { + + constructor.rhs match { + + case Block(stats, expr) => + if (stats.size == 0) { + true + } else { + stats.size == 1 && + (stats(0).toString().startsWith("super." + nme.CONSTRUCTOR) || + stats(0).toString().startsWith("super." + nme.MIXIN_CONSTRUCTOR)) && + isEmptyBlock(expr) + } + + case _ => + constructor.isEmpty + } + } else { + false + } + } + + /** + * Helper function which applies a function on every AST in a given list and + * sums up the results. + */ + def onList(trees: List[Tree], treeFunction: Tree => Int) = { + trees.map(treeFunction).foldLeft(0)(_ + _) + } +} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/util/MetricDistribution.scala b/src/main/scala/com/buransky/plugins/scala/util/MetricDistribution.scala new file mode 100644 index 0000000..b3c9566 --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scala/util/MetricDistribution.scala @@ -0,0 +1,48 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.util + +import collection.immutable.TreeMap + +import org.sonar.api.measures.{ Metric, RangeDistributionBuilder } + +class MetricDistribution(metric: Metric, ranges: Array[Number]) { + + var distribution = TreeMap[Double, Int]() + + def add(value: Double) { + add(value, 1) + } + + def add(value: Double, count: Int) { + val oldValue = distribution.getOrElse(value, 0) + distribution = distribution.updated(value, oldValue + count) + } + + def add(metricDistribution: MetricDistribution) { + metricDistribution.distribution.foreach(entry => add(entry._1, entry._2)) + } + + def getMeasure() = { + val rangeDistribution = new RangeDistributionBuilder(metric, ranges) + distribution.foreach(entry => rangeDistribution.add(entry._1, entry._2)) + rangeDistribution.build + } +} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/ScalaPluginTest.java b/src/test/java/com/buransky/plugins/scala/ScalaPluginTest.java new file mode 100644 index 0000000..dc0f698 --- /dev/null +++ b/src/test/java/com/buransky/plugins/scala/ScalaPluginTest.java @@ -0,0 +1,50 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import com.buransky.plugins.scala.cobertura.CoberturaSensor; +import com.buransky.plugins.scala.surefire.SurefireSensor; + +public class ScalaPluginTest { + + @Test + public void shouldHaveExtensions() { + assertThat(new ScalaPlugin().getExtensions().size(), greaterThan(0)); + } + + @Test + public void shouldHaveCoberturaPlugin() { + assertTrue(new ScalaPlugin().getExtensions().contains(CoberturaSensor.class)); + } + + @Test + public void shouldHaveSurefirePlugin() { + assertTrue(new ScalaPlugin().getExtensions().contains(SurefireSensor.class)); + } + + @Test + public void shouldGetPathToDependencies() { + assertThat(ScalaPlugin.getPathToScalaLibrary(), containsString("scala-library")); + } +} diff --git a/src/test/java/com/buransky/plugins/scala/cobertura/CoberturaSensorTest.java b/src/test/java/com/buransky/plugins/scala/cobertura/CoberturaSensorTest.java new file mode 100644 index 0000000..61a1d0d --- /dev/null +++ b/src/test/java/com/buransky/plugins/scala/cobertura/CoberturaSensorTest.java @@ -0,0 +1,71 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.cobertura; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.batch.SensorContext; +import org.sonar.api.resources.Project; +import com.buransky.plugins.scala.language.Scala; +import org.sonar.test.TestUtils; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CoberturaSensorTest { + + private CoberturaSensor sensor; + + @Before + public void setUp() throws Exception { + sensor = new CoberturaSensor(); + } + + /** + * See SONARPLUGINS-696 + */ + @Test + public void shouldParseReport() { + SensorContext context = mock(SensorContext.class); + sensor.parseReport(TestUtils.getResource("/org/sonar/plugins/scala/cobertura/coverage.xml"), context); + } + + @Test + public void shouldNotExecuteIfStaticAnalysis() { + Project project = mock(Project.class); + when(project.getLanguageKey()).thenReturn(Scala.INSTANCE.getKey()); + when(project.getAnalysisType()).thenReturn(Project.AnalysisType.STATIC); + assertFalse(sensor.shouldExecuteOnProject(project)); + } + + @Test + public void shouldNotExecuteOnJavaProject() { + Project project = mock(Project.class); + when(project.getLanguageKey()).thenReturn("java"); + when(project.getAnalysisType()).thenReturn(Project.AnalysisType.DYNAMIC); + assertFalse(sensor.shouldExecuteOnProject(project)); + } + + @Test + public void testToString() { + assertEquals(sensor.toString(), "Scala CoberturaSensor"); + } +} diff --git a/src/test/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParserTest.java b/src/test/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParserTest.java new file mode 100644 index 0000000..83cc7f8 --- /dev/null +++ b/src/test/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParserTest.java @@ -0,0 +1,92 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.cobertura; + +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; + +import org.sonar.api.resources.Resource; +import com.buransky.plugins.scala.language.ScalaFile; +import com.buransky.plugins.scala.language.ScalaPackage; + +public class ScalaCoberturaParserTest { + + private ScalaCoberturaParser underTest; + + @Before + public void setUp() { + underTest = new ScalaCoberturaParser(); + } + + @Test + public void shouldCreateScalaFileResourceWhenDeepPackage() { + Resource resource = underTest.getResource("com.mock.scalapackage.MockScalaClass"); + assertTrue(resource instanceof ScalaFile); + + ScalaFile file = (ScalaFile)resource; + assertEquals("MockScalaClass", file.getName()); + + ScalaPackage scalaPackage = file.getParent(); + assertNotNull(scalaPackage); + assertEquals("com.mock.scalapackage", scalaPackage.getName()); + } + + @Test + public void shouldCreateScalaFileResourceWhenRootPackage() { + Resource resource = underTest.getResource("MockScalaClass"); + assertTrue(resource instanceof ScalaFile); + + ScalaFile file = (ScalaFile)resource; + assertEquals("MockScalaClass", file.getName()); + + ScalaPackage scalaPackage = file.getParent(); + assertNotNull(scalaPackage); + assertEquals("[default]", scalaPackage.getName()); + } + + // TODO remove this test once the sbt scct plugin is patched to produce the correct class name. + @Test + public void shouldCreateScalaFileResourceWhenScctBug() { + Resource resource = underTest.getResource("src.main.scala.com.mock.scalapackage.MockScalaClass"); + assertTrue(resource instanceof ScalaFile); + + ScalaFile file = (ScalaFile)resource; + assertEquals("MockScalaClass", file.getName()); + + ScalaPackage scalaPackage = file.getParent(); + assertNotNull(scalaPackage); + assertEquals("com.mock.scalapackage", scalaPackage.getName()); + } + + @Test + public void shouldCreateScalaFileResourceWhenScctBugForPlayApp() { + Resource resource = underTest.getResource("app.com.mock.scalapackage.MockScalaClass"); + assertTrue(resource instanceof ScalaFile); + + ScalaFile file = (ScalaFile)resource; + assertEquals("MockScalaClass", file.getName()); + + ScalaPackage scalaPackage = file.getParent(); + assertNotNull(scalaPackage); + assertEquals("com.mock.scalapackage", scalaPackage.getName()); + } + +} diff --git a/src/test/java/com/buransky/plugins/scala/cpd/ScalaTokenizerTest.java b/src/test/java/com/buransky/plugins/scala/cpd/ScalaTokenizerTest.java new file mode 100644 index 0000000..e881621 --- /dev/null +++ b/src/test/java/com/buransky/plugins/scala/cpd/ScalaTokenizerTest.java @@ -0,0 +1,128 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.cpd; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import net.sourceforge.pmd.cpd.AbstractLanguage; +import net.sourceforge.pmd.cpd.TokenEntry; + +import org.apache.commons.io.FileUtils; +import org.junit.Before; +import org.junit.Test; +import org.sonar.duplications.cpd.CPD; +import org.sonar.duplications.cpd.Match; + +public class ScalaTokenizerTest { + + @Before + public void init() { + TokenEntry.clearImages(); + } + + @Test + public void noDuplications() throws IOException { + CPD cpd = getCPD(10); + cpd.add(resourceToFile("/cpd/NoDuplications.scala")); + cpd.go(); + assertThat(getMatches(cpd).size(), is(0)); + } + + @Test + public void noDuplicationsWith6Tokens() throws IOException { + CPD cpd = getCPD(6); + cpd.add(resourceToFile("/cpd/Duplications5Tokens.scala")); + cpd.go(); + assertThat(getMatches(cpd).size(), is(0)); + } + + @Test + public void duplicationWith5Tokens() throws IOException { + CPD cpd = getCPD(5); + cpd.add(resourceToFile("/cpd/Duplications5Tokens.scala")); + cpd.go(); + List matches = getMatches(cpd); + assertThat(matches.size(), is(1)); + assertThat(matches.get(0).getFirstMark().getBeginLine(), is(2)); + assertThat(matches.get(0).getSecondMark().getBeginLine(), is(5)); + } + + @Test + public void newLineTokenEnables5TokenDuplication() throws IOException { + CPD cpd = getCPD(5); + cpd.add(resourceToFile("/cpd/NewlineToken.scala")); + cpd.go(); + List matches = getMatches(cpd); + assertThat(matches.get(0).getFirstMark().getBeginLine(), is(2)); + assertThat(matches.get(0).getSecondMark().getBeginLine(), is(3)); + } + + @Test + public void newLineAndNewLinesTokensNo5TokensDuplication() throws IOException { + CPD cpd = getCPD(5); + cpd.add(resourceToFile("/cpd/NewlinesToken.scala")); + cpd.go(); + assertThat(getMatches(cpd).size(), is(0)); + } + + @Test + public void twoDuplicatedBlocks() throws IOException { + CPD cpd = getCPD(5); + cpd.add(resourceToFile("/cpd/TwoDuplicatedBlocks.scala")); + cpd.go(); + List matches = getMatches(cpd); + assertThat(matches.get(0).getFirstMark().getBeginLine(), is(2)); + assertThat(matches.get(0).getSecondMark().getBeginLine(), is(7)); + assertThat(matches.get(0).getLineCount(), is(4)); + } + + private File resourceToFile(String path) { + return FileUtils.toFile(getClass().getResource(path)); + } + + private CPD getCPD(int minimumTokens) { + AbstractLanguage language = new AbstractLanguage(new ScalaTokenizer(), "scala") { + }; + CPD cpd = new CPD(minimumTokens, language); + cpd.setEncoding(Charset.defaultCharset().name()); + cpd.setLoadSourceCodeSlices(false); + return cpd; + } + + private List getMatches(CPD cpd) { + List matches = new ArrayList(); + + Iterator iterator = cpd.getMatches(); + while (iterator.hasNext()) { + matches.add(iterator.next()); + } + + return matches; + } + +} diff --git a/src/test/java/com/buransky/plugins/scala/language/CommentTest.java b/src/test/java/com/buransky/plugins/scala/language/CommentTest.java new file mode 100644 index 0000000..7f81fed --- /dev/null +++ b/src/test/java/com/buransky/plugins/scala/language/CommentTest.java @@ -0,0 +1,109 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.language; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import java.io.IOException; + +import org.junit.Test; + +public class CommentTest { + + @Test + public void shouldCountOneNumberOfCommentLine() throws IOException { + Comment comment = new Comment("// This is a comment", CommentType.NORMAL); + assertThat(comment.getNumberOfLines(), is(1)); + } + + @Test + public void shouldCountAllNumberOfCommentLines() throws IOException { + Comment comment = new Comment("/* This is the first comment line\r\n" + + "* second line\r\n" + + "* and this the third and last line */", + CommentType.NORMAL); + assertThat(comment.getNumberOfLines(), is(3)); + } + + @Test + public void shouldCountZeorCommentLinesIfCommentIsEmpty() throws IOException { + Comment comment = new Comment("", CommentType.NORMAL); + assertThat(comment.getNumberOfLines(), is(0)); + } + + @Test + public void shouldCountOneCommentedOutLineOfCode() throws IOException { + Comment comment = new Comment("// val a = 1", CommentType.NORMAL); + assertThat(comment.getNumberOfCommentedOutLinesOfCode(), is(1)); + } + + @Test + public void shouldCountAllCommentedOutLinesOfCode() throws IOException { + Comment comment = new Comment("/* object Hello {\r\n" + + "* val b = 1 } */", + CommentType.NORMAL); + assertThat(comment.getNumberOfCommentedOutLinesOfCode(), is(2)); + } + + @Test + public void shouldCountZeorCommentedOutLinesOfCodeIfCommentIsEmpty() throws IOException { + Comment comment = new Comment("", CommentType.NORMAL); + assertThat(comment.getNumberOfCommentedOutLinesOfCode(), is(0)); + } + + @Test + public void shouldNotCountAnyCommentedOutLinesOfCodeForDocComments() throws IOException { + Comment comment = new Comment("/** This is a doc comment with some code\r\n" + + "* package hello.world\r\n" + + "* class Test { val a = 1 } */", CommentType.DOC); + assertThat(comment.getNumberOfCommentedOutLinesOfCode(), is(0)); + } + + @Test + public void shouldCountAllBlankCommentLines() throws IOException { + Comment comment = new Comment("/*\r\n" + + "* this is a multi line comment with some blank lines\r\n" + + "* \t \t \r\n" + + "*/", CommentType.NORMAL); + assertThat(comment.getNumberOfBlankLines(), is(3)); + } + + @Test + public void shouldBeNormalComment() throws IOException { + Comment comment = new Comment("", CommentType.NORMAL); + assertThat(comment.isDocComment(), is(false)); + assertThat(comment.isHeaderComment(), is(false)); + } + + @Test + public void shouldBeDocComment() throws IOException { + Comment comment = new Comment("", CommentType.DOC); + assertThat(comment.isDocComment(), is(true)); + assertThat(comment.isHeaderComment(), is(false)); + } + + @Test + public void shouldBeHeaderComment() throws IOException { + Comment comment = new Comment("", CommentType.HEADER); + assertThat(comment.isDocComment(), is(false)); + assertThat(comment.isHeaderComment(), is(true)); + } +} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/language/ScalaFileTest.java b/src/test/java/com/buransky/plugins/scala/language/ScalaFileTest.java new file mode 100644 index 0000000..c4c4c2f --- /dev/null +++ b/src/test/java/com/buransky/plugins/scala/language/ScalaFileTest.java @@ -0,0 +1,93 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.language; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; + +import com.buransky.plugins.scala.util.FileTestUtils; +import org.junit.Test; +import org.sonar.api.resources.InputFile; +import org.sonar.api.resources.Qualifiers; + +public class ScalaFileTest { + + @Test + public void shouldHaveFileQualifierForSourceFile() { + assertThat(new ScalaFile("package", "Class", false).getQualifier(), + equalTo(Qualifiers.FILE)); + } + + @Test + public void shouldHaveTestFileQualifierForTestFile() { + assertThat(new ScalaFile("package", "Class", true).getQualifier(), + equalTo(Qualifiers.UNIT_TEST_FILE)); + } + + @Test + public void shouldCreateScalaFileWithCorrectAttributes() { + InputFile inputFile = FileTestUtils.getInputFiles("/scalaFile/", "ScalaFile", 1).get(0); + ScalaFile scalaFile = ScalaFile.fromInputFile(inputFile); + + assertThat(scalaFile.getLanguage().getKey(), is(Scala.INSTANCE.getKey())); + assertThat(scalaFile.getName(), is("ScalaFile1")); + assertThat(scalaFile.getLongName(), is("scalaFile.ScalaFile1")); + assertThat(scalaFile.getParent().getName(), is("scalaFile")); + assertThat(scalaFile.isUnitTest(), is(false)); + } + + @Test + public void shouldCreateScalaTestFileWithCorrectAttributes() { + InputFile inputFile = FileTestUtils.getInputFiles("/scalaFile/", "ScalaTestFile", 1).get(0); + ScalaFile scalaFile = ScalaFile.fromInputFile(inputFile, true); + + assertThat(scalaFile.getLanguage().getKey(), is(Scala.INSTANCE.getKey())); + assertThat(scalaFile.getName(), is("ScalaTestFile1")); + assertThat(scalaFile.getLongName(), is("scalaFile.ScalaTestFile1")); + assertThat(scalaFile.getParent().getName(), is("scalaFile")); + assertThat(scalaFile.isUnitTest(), is(true)); + } + + @Test + public void shouldNotCreateScalaFileIfInputFileIsNull() { + assertNull(ScalaFile.fromInputFile(null)); + } + + @Test + public void shouldNotCreateScalaFileIfFileIsNull() { + InputFile inputFile = mock(InputFile.class); + when(inputFile.getFile()).thenReturn(null); + assertNull(ScalaFile.fromInputFile(inputFile)); + } + + @Test + public void shouldNotCreateScalaFileIfRelativePathIsNull() { + InputFile inputFile = mock(InputFile.class); + when(inputFile.getFile()).thenReturn(new File("")); + when(inputFile.getRelativePath()).thenReturn(null); + assertNull(ScalaFile.fromInputFile(inputFile)); + } +} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/language/ScalaTest.java b/src/test/java/com/buransky/plugins/scala/language/ScalaTest.java new file mode 100644 index 0000000..5b7b52f --- /dev/null +++ b/src/test/java/com/buransky/plugins/scala/language/ScalaTest.java @@ -0,0 +1,48 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.language; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertThat; + +import org.junit.Test; + +public class ScalaTest { + + @Test + public void shouldHaveScalaLanguageKey() { + assertThat(new Scala().getKey(), equalTo("scala")); + assertThat(Scala.INSTANCE.getKey(), equalTo("scala")); + } + + @Test + public void shouldHaveScalaLanguageName() { + assertThat(new Scala().getName(), equalTo("Scala")); + assertThat(Scala.INSTANCE.getName(), equalTo("Scala")); + } + + @Test + public void shouldHaveScalaFileSuffixes() { + String[] suffixes = new String[] { "scala" }; + assertArrayEquals(new Scala().getFileSuffixes(), suffixes); + assertArrayEquals(Scala.INSTANCE.getFileSuffixes(), suffixes); + } +} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/metrics/CommentsAnalyzerTest.java b/src/test/java/com/buransky/plugins/scala/metrics/CommentsAnalyzerTest.java new file mode 100644 index 0000000..9dc7366 --- /dev/null +++ b/src/test/java/com/buransky/plugins/scala/metrics/CommentsAnalyzerTest.java @@ -0,0 +1,96 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.metrics; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; +import com.buransky.plugins.scala.language.Comment; +import com.buransky.plugins.scala.language.CommentType; + +import scala.actors.threadpool.Arrays; + +public class CommentsAnalyzerTest { + + @Test + public void shouldCountAllCommentLines() throws IOException { + List comments = Arrays.asList(new String[] { + "// this a normal comment", + "/* this is a normal multiline coment\r\n* last line of this comment */", + "// also a normal comment" + }); + CommentsAnalyzer commentAnalyzer = new CommentsAnalyzer(asCommentList(comments, CommentType.NORMAL)); + assertThat(commentAnalyzer.countCommentLines(), is(4)); + } + + @Test + public void shouldCountAllHeaderCommentLines() throws IOException { + List comments = Arrays.asList(new String[] { + "/* this is an one line header comment */", + "/* this is a normal multiline header coment\r\n* last line of this comment */", + "/* also a normal header comment */" + }); + CommentsAnalyzer commentAnalyzer = new CommentsAnalyzer(asCommentList(comments, CommentType.HEADER)); + assertThat(commentAnalyzer.countHeaderCommentLines(), is(4)); + } + + @Test + public void shouldCountAllCommentedOutLinesOfCode() throws IOException { + List comments = Arrays.asList(new String[] { + "// val a = 12", + "/* list.foreach(println(_))\r\n* def inc(x: Int) = x + 1 */", + "// this a normal comment" + }); + CommentsAnalyzer commentAnalyzer = new CommentsAnalyzer(asCommentList(comments, CommentType.NORMAL)); + assertThat(commentAnalyzer.countCommentedOutLinesOfCode(), is(3)); + } + + @Test + public void shouldCountZeroCommentLinesForEmptyCommentsList() { + CommentsAnalyzer commentAnalyzer = new CommentsAnalyzer(Collections.emptyList()); + assertThat(commentAnalyzer.countCommentLines(), is(0)); + } + + @Test + public void shouldCountZeroHeaderCommentLinesForEmptyCommentsList() { + CommentsAnalyzer commentAnalyzer = new CommentsAnalyzer(Collections.emptyList()); + assertThat(commentAnalyzer.countHeaderCommentLines(), is(0)); + } + + @Test + public void shouldCountZeroCommentedOutLinesOfCodeForEmptyCommentsList() { + CommentsAnalyzer commentAnalyzer = new CommentsAnalyzer(Collections.emptyList()); + assertThat(commentAnalyzer.countCommentedOutLinesOfCode(), is(0)); + } + + private List asCommentList(List commentsContent, CommentType type) throws IOException { + List comments = new ArrayList(); + for (String comment : commentsContent) { + comments.add(new Comment(comment, type)); + } + return comments; + } +} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/metrics/LinesAnalyzerTest.java b/src/test/java/com/buransky/plugins/scala/metrics/LinesAnalyzerTest.java new file mode 100644 index 0000000..7cd2f9c --- /dev/null +++ b/src/test/java/com/buransky/plugins/scala/metrics/LinesAnalyzerTest.java @@ -0,0 +1,95 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.metrics; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +import java.io.IOException; +import java.util.List; + +import org.junit.Test; +import org.sonar.plugins.scala.compiler.Lexer; +import com.buransky.plugins.scala.language.Comment; +import com.buransky.plugins.scala.util.StringUtils; + +public class LinesAnalyzerTest { + + @Test + public void shouldCountOneLine() throws IOException { + LinesAnalyzer linesAnalyzer = getLinesAnalyzer("val i = 0"); + assertThat(linesAnalyzer.countLines(), is(1)); + } + + @Test + public void shouldCountAllLines() throws IOException { + LinesAnalyzer linesAnalyzer = getLinesAnalyzer("val i = 0\r\n" + + "println(\"Hallo\")\r\n" + + "\r\n" + + "i = 2"); + assertThat(linesAnalyzer.countLines(), is(4)); + } + + @Test + public void shouldGiveZeroLinesForEmptySource() throws IOException { + LinesAnalyzer linesAnalyzer = getLinesAnalyzer(""); + assertThat(linesAnalyzer.countLines(), is(0)); + } + + @Test + public void shouldCountOneLineOfCode() throws IOException { + LinesAnalyzer linesAnalyzer = getLinesAnalyzer("val i = 0"); + assertThat(linesAnalyzer.countLinesOfCode(), is(1)); + } + + @Test + public void shouldNotCountBlankLinesAsLinesOfCode() throws IOException { + LinesAnalyzer linesAnalyzer = getLinesAnalyzer("val i = 0\r\n" + + "\r\n" + + " \t \r\n" + + "val b = 2"); + assertThat(linesAnalyzer.countLinesOfCode(), is(2)); + } + + @Test + public void shouldNotCountCommentLinesAsLinesOfCode() throws IOException { + LinesAnalyzer linesAnalyzer = getLinesAnalyzer("val i = 0\r\n" + + "// this is comment...\r\n" + + "// test\r\n" + + "val b = 2"); + assertThat(linesAnalyzer.countLinesOfCode(), is(2)); + } + + @Test + public void shouldNotCountHeaderCommentLinesAsLinesOfCode() throws IOException { + LinesAnalyzer linesAnalyzer = getLinesAnalyzer("/**\r\n" + + "* this is a header comment...\r\n" + + "*/\r\n" + + "val b = 2"); + assertThat(linesAnalyzer.countLinesOfCode(), is(1)); + } + + private LinesAnalyzer getLinesAnalyzer(String source) throws IOException { + List lines = StringUtils.convertStringToListOfLines(source); + List comments = new Lexer().getComments(source); + CommentsAnalyzer commentsAnalyzer = new CommentsAnalyzer(comments); + return new LinesAnalyzer(lines, commentsAnalyzer); + } +} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/sensor/AbstractScalaSensorTest.java b/src/test/java/com/buransky/plugins/scala/sensor/AbstractScalaSensorTest.java new file mode 100644 index 0000000..46756be --- /dev/null +++ b/src/test/java/com/buransky/plugins/scala/sensor/AbstractScalaSensorTest.java @@ -0,0 +1,65 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.sensor; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.batch.SensorContext; +import org.sonar.api.resources.Java; +import org.sonar.api.resources.Project; +import com.buransky.plugins.scala.language.Scala; + +public class AbstractScalaSensorTest { + + private AbstractScalaSensor abstractScalaSensor; + + @Before + public void setUp() { + abstractScalaSensor = new AbstractScalaSensor(Scala.INSTANCE) { + + public void analyse(Project project, SensorContext context) { + // dummy implementation, never called in this test + } + }; + } + + @Test + public void shouldOnlyExecuteOnScalaProjects() { + Project scalaProject = mock(Project.class); + when(scalaProject.getLanguage()).thenReturn(Scala.INSTANCE); + Project javaProject = mock(Project.class); + when(javaProject.getLanguage()).thenReturn(Java.INSTANCE); + + assertTrue(abstractScalaSensor.shouldExecuteOnProject(scalaProject)); + assertFalse(abstractScalaSensor.shouldExecuteOnProject(javaProject)); + } + + @Test + public void shouldHaveScalaAsLanguage() { + assertThat(abstractScalaSensor.getScala(), equalTo(new Scala())); + } +} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/sensor/BaseMetricsSensorTest.java b/src/test/java/com/buransky/plugins/scala/sensor/BaseMetricsSensorTest.java new file mode 100644 index 0000000..3e9edd1 --- /dev/null +++ b/src/test/java/com/buransky/plugins/scala/sensor/BaseMetricsSensorTest.java @@ -0,0 +1,214 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.sensor; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.charset.Charset; + +import com.buransky.plugins.scala.util.FileTestUtils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Matchers; +import org.sonar.api.batch.SensorContext; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Measure; +import org.sonar.api.measures.Metric; +import org.sonar.api.resources.Project; +import org.sonar.api.resources.ProjectFileSystem; +import com.buransky.plugins.scala.language.Scala; +import com.buransky.plugins.scala.language.ScalaPackage; + +public class BaseMetricsSensorTest { + + private static final int NUMBER_OF_FILES = 3; + + private BaseMetricsSensor baseMetricsSensor; + + private ProjectFileSystem fileSystem; + private Project project; + private SensorContext sensorContext; + + @Before + public void setUp() { + baseMetricsSensor = new BaseMetricsSensor(Scala.INSTANCE); + + fileSystem = mock(ProjectFileSystem.class); + when(fileSystem.getSourceCharset()).thenReturn(Charset.defaultCharset()); + + project = mock(Project.class); + when(project.getFileSystem()).thenReturn(fileSystem); + + sensorContext = mock(SensorContext.class); + } + + @Test + public void shouldIncrementFileMetricForOneScalaFile() { + analyseOneScalaFile(); + verifyMeasuring(CoreMetrics.FILES, 1.0); + } + + @Test + public void shouldIncreaseFileMetricForAllScalaFiles() throws IOException { + analyseAllScalaFiles(); + verifyMeasuring(CoreMetrics.FILES, NUMBER_OF_FILES, 1.0); + } + + @Test + public void shouldMeasureNothingWhenNoFiles() { + analyseScalaFiles(0); + verifyNoMoreInteractions(sensorContext); + } + + @Test + public void shouldIncrementPackageMetricForOneScalaFile() { + analyseOneScalaFile(); + verify(sensorContext).saveMeasure(any(ScalaPackage.class), eq(CoreMetrics.PACKAGES), eq(1.0)); + } + + @Test + public void shouldIncreasePackageMetricForAllScalaFiles() { + analyseAllScalaFiles(); + verify(sensorContext, times(2)).saveMeasure(any(ScalaPackage.class), eq(CoreMetrics.PACKAGES), eq(1.0)); + } + + @Test + public void shouldMeasureClassComplexityDistributionForOneScalaFileOnlyOnce() { + analyseOneScalaFile(); + verify(sensorContext).saveMeasure(eq(new Measure(CoreMetrics.CLASS_COMPLEXITY_DISTRIBUTION))); + } + + @Test + public void shouldMeasureClassComplexityDistributionForAllScalaFilesOnlyOnce() { + analyseAllScalaFiles(); + verify(sensorContext).saveMeasure(eq(new Measure(CoreMetrics.CLASS_COMPLEXITY_DISTRIBUTION))); + } + + @Test + public void shouldMeasureFunctionComplexityDistributionForOneScalaFileOnlyOnce() { + analyseOneScalaFile(); + verify(sensorContext).saveMeasure(eq(new Measure(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION))); + } + + @Test + public void shouldMeasureFunctionComplexityDistributionForAllScalaFilesOnlyOnce() { + analyseAllScalaFiles(); + verify(sensorContext).saveMeasure(eq(new Measure(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION))); + } + + @Test + public void shouldMeasureLineMetricsForOneScalaFile() { + analyseOneScalaFile(); + verifyMeasuring(CoreMetrics.LINES); + verifyMeasuring(CoreMetrics.NCLOC); + } + + @Test + public void shouldMeasureLineMetricsForAllScalaFiles() { + analyseAllScalaFiles(); + verifyMeasuring(CoreMetrics.LINES, NUMBER_OF_FILES); + verifyMeasuring(CoreMetrics.NCLOC, NUMBER_OF_FILES); + } + + @Test + public void shouldMeasureCommentMetricsForOneScalaFile() { + analyseOneScalaFile(); + verifyMeasuring(CoreMetrics.COMMENT_LINES); + verifyMeasuring(CoreMetrics.COMMENTED_OUT_CODE_LINES); + } + + @Test + public void shouldMeasureCommentMetricsForAllScalaFiles() { + analyseAllScalaFiles(); + verifyMeasuring(CoreMetrics.COMMENT_LINES, NUMBER_OF_FILES); + verifyMeasuring(CoreMetrics.COMMENTED_OUT_CODE_LINES, NUMBER_OF_FILES); + } + + @Test + public void shouldMeasureCodeMetricsForOneScalaFile() { + analyseOneScalaFile(); + verifyMeasuring(CoreMetrics.CLASSES); + verifyMeasuring(CoreMetrics.STATEMENTS); + verifyMeasuring(CoreMetrics.FUNCTIONS); + verifyMeasuring(CoreMetrics.COMPLEXITY); + } + + @Test + public void shouldMeasureCodeMetricsForAllScalaFiles() { + analyseAllScalaFiles(); + verifyMeasuring(CoreMetrics.CLASSES, NUMBER_OF_FILES); + verifyMeasuring(CoreMetrics.STATEMENTS, NUMBER_OF_FILES); + verifyMeasuring(CoreMetrics.FUNCTIONS, NUMBER_OF_FILES); + verifyMeasuring(CoreMetrics.COMPLEXITY, NUMBER_OF_FILES); + } + + @Test + public void shouldMeasurePublicApiMetricsForOneScalaFile() { + analyseOneScalaFile(); + verifyMeasuring(CoreMetrics.PUBLIC_API); + verifyMeasuring(CoreMetrics.PUBLIC_UNDOCUMENTED_API); + } + + @Test + public void shouldMeasurePublicApiMetricsForAllScalaFiles() { + analyseAllScalaFiles(); + verifyMeasuring(CoreMetrics.PUBLIC_API, NUMBER_OF_FILES); + verifyMeasuring(CoreMetrics.PUBLIC_UNDOCUMENTED_API, NUMBER_OF_FILES); + } + + private void verifyMeasuring(Metric metric) { + verifyMeasuring(metric, 1); + } + + private void verifyMeasuring(Metric metric, int numberOfCalls) { + verify(sensorContext, times(numberOfCalls)).saveMeasure(Matchers.eq(FileTestUtils.SCALA_SOURCE_FILE), + eq(metric), any(Double.class)); + } + + private void verifyMeasuring(Metric metric, double value) { + verifyMeasuring(metric, 1, value); + } + + private void verifyMeasuring(Metric metric, int numberOfCalls, double value) { + verify(sensorContext, times(numberOfCalls)).saveMeasure(eq(FileTestUtils.SCALA_SOURCE_FILE), + eq(metric), eq(value)); + } + + private void analyseOneScalaFile() { + analyseScalaFiles(1); + } + + private void analyseAllScalaFiles() { + analyseScalaFiles(NUMBER_OF_FILES); + } + + private void analyseScalaFiles(int numberOfFiles) { + when(fileSystem.mainFiles(baseMetricsSensor.getScala().getKey())) + .thenReturn(FileTestUtils.getInputFiles("/baseMetricsSensor/", "ScalaFile", numberOfFiles)); + baseMetricsSensor.analyse(project, sensorContext); + } +} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensorTest.java b/src/test/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensorTest.java new file mode 100644 index 0000000..b07a6a7 --- /dev/null +++ b/src/test/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensorTest.java @@ -0,0 +1,219 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.sensor; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; +import org.sonar.api.batch.SensorContext; +import org.sonar.api.resources.InputFile; +import org.sonar.api.resources.Java; +import org.sonar.api.resources.Project; +import org.sonar.api.resources.ProjectFileSystem; +import com.buransky.plugins.scala.language.Scala; +import com.buransky.plugins.scala.util.FileTestUtils; + +public class ScalaSourceImporterSensorTest { + + private ScalaSourceImporterSensor scalaSourceImporter; + + private ProjectFileSystem fileSystem; + private Project project; + private SensorContext sensorContext; + + @Before + public void setUp() { + scalaSourceImporter = new ScalaSourceImporterSensor(Scala.INSTANCE); + + fileSystem = mock(ProjectFileSystem.class); + when(fileSystem.getSourceCharset()).thenReturn(Charset.defaultCharset()); + + project = mock(Project.class); + when(project.getFileSystem()).thenReturn(fileSystem); + + sensorContext = mock(SensorContext.class); + } + + @Test + public void shouldImportOnlyOneScalaFile() { + when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getInputFiles(1)); + when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); + + scalaSourceImporter.analyse(project, sensorContext); + + InOrder inOrder = inOrder(sensorContext); + inOrder.verify(sensorContext, times(1)).index(eq(FileTestUtils.SCALA_SOURCE_FILE)); + inOrder.verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_SOURCE_FILE), any(String.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void shouldImportOnlyOneScalaFileWithTheCorrectFileContent() throws IOException { + when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getInputFiles(1)); + when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); + + scalaSourceImporter.analyse(project, sensorContext); + + InOrder inOrder = inOrder(sensorContext); + inOrder.verify(sensorContext, times(1)).index(eq(FileTestUtils.SCALA_SOURCE_FILE)); + inOrder.verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_SOURCE_FILE), + eq(getContentOfFiles(1).get(0))); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void shouldImportAllScalaFiles() { + when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getInputFiles(3)); + when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); + + scalaSourceImporter.analyse(project, sensorContext); + + verify(sensorContext, times(3)).index(eq(FileTestUtils.SCALA_SOURCE_FILE)); + verify(sensorContext, times(3)).saveSource(eq(FileTestUtils.SCALA_SOURCE_FILE), any(String.class)); + verifyNoMoreInteractions(sensorContext); + } + + @Test + public void shouldImportAllScalaFilesAndNotFilesOfOtherLanguages() { + when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getInputFiles(3)); + when(fileSystem.mainFiles(Java.INSTANCE.getKey())) + .thenReturn(FileTestUtils.getInputFiles("/scalaSourceImporter/", "JavaMainFile", "java", 1)); + when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); + + scalaSourceImporter.analyse(project, sensorContext); + + verify(sensorContext, times(3)).index(eq(FileTestUtils.SCALA_SOURCE_FILE)); + verify(sensorContext, times(3)).saveSource(eq(FileTestUtils.SCALA_SOURCE_FILE), any(String.class)); + verifyNoMoreInteractions(sensorContext); + } + + @Test + public void shouldImportAllScalaFilesWithTheCorrectFileContent() throws IOException { + when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getInputFiles(3)); + when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); + + scalaSourceImporter.analyse(project, sensorContext); + + List contentOfFiles = getContentOfFiles(3); + verify(sensorContext, times(3)).index(eq(FileTestUtils.SCALA_SOURCE_FILE)); + verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_SOURCE_FILE), eq(contentOfFiles.get(0))); + verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_SOURCE_FILE), eq(contentOfFiles.get(1))); + verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_SOURCE_FILE), eq(contentOfFiles.get(2))); + verifyNoMoreInteractions(sensorContext); + } + + @Test + public void shouldImportOnlyOneScalaTestFile() { + when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); + when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getTestInputFiles(1)); + + scalaSourceImporter.analyse(project, sensorContext); + + InOrder inOrder = inOrder(sensorContext); + inOrder.verify(sensorContext, times(1)).index(eq(FileTestUtils.SCALA_TEST_FILE)); + inOrder.verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_TEST_FILE), any(String.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void shouldImportOnlyOneScalaTestFileWithTheCorrectFileContent() throws IOException { + when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); + when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getTestInputFiles(1)); + + scalaSourceImporter.analyse(project, sensorContext); + + InOrder inOrder = inOrder(sensorContext); + inOrder.verify(sensorContext, times(1)).index(eq(FileTestUtils.SCALA_TEST_FILE)); + inOrder.verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_TEST_FILE), + eq(getContentOfTestFiles(1).get(0))); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void shouldImportAllScalaTestFiles() { + when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); + when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getTestInputFiles(3)); + + scalaSourceImporter.analyse(project, sensorContext); + + verify(sensorContext, times(3)).index(eq(FileTestUtils.SCALA_TEST_FILE)); + verify(sensorContext, times(3)).saveSource(eq(FileTestUtils.SCALA_TEST_FILE), any(String.class)); + verifyNoMoreInteractions(sensorContext); + } + + @Test + public void shouldImportAllScalaTestFilesAndNotTestFilesOfOtherLanguages() { + when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); + when(fileSystem.testFiles(Java.INSTANCE.getKey())) + .thenReturn(FileTestUtils.getInputFiles("/scalaSourceImporter/", "JavaTestFile", "java", 1)); + when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getTestInputFiles(3)); + + scalaSourceImporter.analyse(project, sensorContext); + + verify(sensorContext, times(3)).index(eq(FileTestUtils.SCALA_TEST_FILE)); + verify(sensorContext, times(3)).saveSource(eq(FileTestUtils.SCALA_TEST_FILE), any(String.class)); + verifyNoMoreInteractions(sensorContext); + } + + @Test + public void shouldImportAllScalaTestFilesWithTheCorrectFileContent() throws IOException { + when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); + when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getTestInputFiles(3)); + + scalaSourceImporter.analyse(project, sensorContext); + + List contentOfFiles = getContentOfTestFiles(3); + verify(sensorContext, times(3)).index(eq(FileTestUtils.SCALA_TEST_FILE)); + verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_TEST_FILE), eq(contentOfFiles.get(0))); + verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_TEST_FILE), eq(contentOfFiles.get(1))); + verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_TEST_FILE), eq(contentOfFiles.get(2))); + verifyNoMoreInteractions(sensorContext); + } + + public List getInputFiles(int numberOfFiles) { + return FileTestUtils.getInputFiles("/scalaSourceImporter/", "MainFile", numberOfFiles); + } + + public List getTestInputFiles(int numberOfFiles) { + return FileTestUtils.getInputFiles("/scalaSourceImporter/", "TestFile", numberOfFiles); + } + + public List getContentOfFiles(int numberOfFiles) throws IOException { + return FileTestUtils.getContentOfFiles("/scalaSourceImporter/", "MainFile", numberOfFiles); + } + + public List getContentOfTestFiles(int numberOfFiles) throws IOException { + return FileTestUtils.getContentOfFiles("/scalaSourceImporter/", "TestFile", numberOfFiles); + } +} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/surefire/SurefireSensorTest.java b/src/test/java/com/buransky/plugins/scala/surefire/SurefireSensorTest.java new file mode 100644 index 0000000..5a3a567 --- /dev/null +++ b/src/test/java/com/buransky/plugins/scala/surefire/SurefireSensorTest.java @@ -0,0 +1,80 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.surefire; + +import org.junit.Test; +import org.junit.Before; +import org.sonar.api.batch.CoverageExtension; +import org.sonar.api.resources.Project; +import com.buransky.plugins.scala.language.Scala; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SurefireSensorTest { + + private SurefireSensor sensor; + private Project project; + + @Before + public void setUp() { + sensor = new SurefireSensor(); + project = mock(Project.class); + } + + @Test + public void shouldExecuteOnReuseReports() { + when(project.getLanguageKey()).thenReturn(Scala.INSTANCE.getKey()); + when(project.getAnalysisType()).thenReturn(Project.AnalysisType.REUSE_REPORTS); + assertTrue(sensor.shouldExecuteOnProject(project)); + } + + @Test + public void shouldExecuteOnDynamicAnalysis() { + when(project.getLanguageKey()).thenReturn(Scala.INSTANCE.getKey()); + when(project.getAnalysisType()).thenReturn(Project.AnalysisType.DYNAMIC); + assertTrue(sensor.shouldExecuteOnProject(project)); + } + + @Test + public void shouldNotExecuteIfStaticAnalysis() { + when(project.getLanguageKey()).thenReturn(Scala.INSTANCE.getKey()); + when(project.getAnalysisType()).thenReturn(Project.AnalysisType.STATIC); + assertFalse(sensor.shouldExecuteOnProject(project)); + } + + @Test + public void shouldNotExecuteOnJavaProject() { + when(project.getLanguageKey()).thenReturn("java"); + when(project.getAnalysisType()).thenReturn(Project.AnalysisType.DYNAMIC); + assertFalse(sensor.shouldExecuteOnProject(project)); + } + + @Test + public void shouldDependOnCoverageSensors() { + assertEquals(CoverageExtension.class, sensor.dependsUponCoverageSensors()); + } + + @Test + public void testToString() { + assertEquals("Scala SurefireSensor", sensor.toString()); + } +} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/util/DummyScalaFile.java b/src/test/java/com/buransky/plugins/scala/util/DummyScalaFile.java new file mode 100644 index 0000000..e655755 --- /dev/null +++ b/src/test/java/com/buransky/plugins/scala/util/DummyScalaFile.java @@ -0,0 +1,38 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.util; + +import com.buransky.plugins.scala.language.ScalaFile; + +public class DummyScalaFile extends ScalaFile { + + public DummyScalaFile(boolean isUnitTest) { + super("", "", isUnitTest); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ScalaFile)) { + return false; + } + ScalaFile other = (ScalaFile) obj; + return isUnitTest() == other.isUnitTest(); + } +} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/util/FileTestUtils.java b/src/test/java/com/buransky/plugins/scala/util/FileTestUtils.java new file mode 100644 index 0000000..75a290a --- /dev/null +++ b/src/test/java/com/buransky/plugins/scala/util/FileTestUtils.java @@ -0,0 +1,86 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.util; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.resources.InputFile; +import org.sonar.api.resources.InputFileUtils; +import com.buransky.plugins.scala.language.ScalaFile; + +public final class FileTestUtils { + + public static final ScalaFile SCALA_SOURCE_FILE = new DummyScalaFile(false); + public static final ScalaFile SCALA_TEST_FILE = new DummyScalaFile(true); + + private static final Logger LOGGER = LoggerFactory.getLogger(FileTestUtils.class); + + private FileTestUtils() { + // to prevent instantiation + } + + public static String getRelativePath(String path) { + return FileTestUtils.class.getResource(path).getFile(); + } + + public static List getInputFiles(String path, String fileNameBase, int numberOfFiles) { + return getInputFiles(path, fileNameBase, "scala", numberOfFiles); + } + + public static List getInputFiles(String path, String fileNameBase, + String fileSuffix, int numberOfFiles) { + List mainFiles = new ArrayList(); + + URL resourceURL = FileTestUtils.class.getResource(path + fileNameBase + "1." + fileSuffix); + for (int i = 1; resourceURL != null && i <= numberOfFiles;) { + mainFiles.add(new File(resourceURL.getFile())); + resourceURL = FileTestUtils.class.getResource(path + fileNameBase + (++i) + "." + fileSuffix); + } + + return InputFileUtils.create(new File(FileTestUtils.class.getResource(path).getFile()), mainFiles); + } + + public static List getContentOfFiles(String path, String fileNameBase, + int numberOfFiles) throws IOException { + List contentOfFiles = new ArrayList(); + + URL resourceURL = FileTestUtils.class.getResource(path + fileNameBase + "1.scala"); + for (int i = 1; resourceURL != null && i <= numberOfFiles;) { + try { + contentOfFiles.add(FileUtils.readFileToString(new File(resourceURL.getFile()), + Charset.defaultCharset().toString())); + } catch (IOException ioe) { + LOGGER.error("Unexpected I/O exception occurred", ioe); + throw ioe; + } + resourceURL = FileTestUtils.class.getResource(path + fileNameBase + (++i) + ".scala"); + } + + return contentOfFiles; + } +} \ No newline at end of file diff --git a/src/test/resources/baseMetricsSensor/ScalaFile1.scala b/src/test/resources/baseMetricsSensor/ScalaFile1.scala new file mode 100644 index 0000000..c739d27 --- /dev/null +++ b/src/test/resources/baseMetricsSensor/ScalaFile1.scala @@ -0,0 +1,5 @@ +package baseMetricsSensor + +class ScalaFile1 { + +} \ No newline at end of file diff --git a/src/test/resources/baseMetricsSensor/ScalaFile2.scala b/src/test/resources/baseMetricsSensor/ScalaFile2.scala new file mode 100644 index 0000000..31a2287 --- /dev/null +++ b/src/test/resources/baseMetricsSensor/ScalaFile2.scala @@ -0,0 +1,5 @@ +package baseMetricsSensor.otherPackage + +class ScalaFile2 { + +} \ No newline at end of file diff --git a/src/test/resources/baseMetricsSensor/ScalaFile3.scala b/src/test/resources/baseMetricsSensor/ScalaFile3.scala new file mode 100644 index 0000000..29c4213 --- /dev/null +++ b/src/test/resources/baseMetricsSensor/ScalaFile3.scala @@ -0,0 +1,5 @@ +package baseMetricsSensor + +class ScalaFile3 { + +} \ No newline at end of file diff --git a/src/test/resources/cpd/Duplications5Tokens.scala b/src/test/resources/cpd/Duplications5Tokens.scala new file mode 100644 index 0000000..6a1150c --- /dev/null +++ b/src/test/resources/cpd/Duplications5Tokens.scala @@ -0,0 +1,6 @@ +class Duplications5Tokens { + val i = 0; + + + val j = 0; +} diff --git a/src/test/resources/cpd/NewlineToken.scala b/src/test/resources/cpd/NewlineToken.scala new file mode 100644 index 0000000..dec3a84 --- /dev/null +++ b/src/test/resources/cpd/NewlineToken.scala @@ -0,0 +1,5 @@ +class NewlineToken { + val i = 42 + val j = 1000 + println("hehe") +} diff --git a/src/test/resources/cpd/NewlinesToken.scala b/src/test/resources/cpd/NewlinesToken.scala new file mode 100644 index 0000000..fd8c9c2 --- /dev/null +++ b/src/test/resources/cpd/NewlinesToken.scala @@ -0,0 +1,6 @@ +class NewlineToken { + val i = 42 + val j = 1000 + + println("hehe") +} diff --git a/src/test/resources/cpd/NoDuplications.scala b/src/test/resources/cpd/NoDuplications.scala new file mode 100644 index 0000000..638d321 --- /dev/null +++ b/src/test/resources/cpd/NoDuplications.scala @@ -0,0 +1,3 @@ +class NoDuplications { + val i = 0 +} diff --git a/src/test/resources/cpd/TwoDuplicatedBlocks.scala b/src/test/resources/cpd/TwoDuplicatedBlocks.scala new file mode 100644 index 0000000..856335f --- /dev/null +++ b/src/test/resources/cpd/TwoDuplicatedBlocks.scala @@ -0,0 +1,11 @@ +class TwoDuplicatedBlocks { + val i = 42 + println("Foo") + println("Bar"); + val j = 0; + + val k = 42 + println("John") + println("Smith"); + val l = 0; +} diff --git a/src/test/resources/lexer/DocComment1.txt b/src/test/resources/lexer/DocComment1.txt new file mode 100644 index 0000000..8f5b166 --- /dev/null +++ b/src/test/resources/lexer/DocComment1.txt @@ -0,0 +1 @@ +/** Hello World */ \ No newline at end of file diff --git a/src/test/resources/lexer/HeaderCommentWithCodeBefore.txt b/src/test/resources/lexer/HeaderCommentWithCodeBefore.txt new file mode 100644 index 0000000..e1ac767 --- /dev/null +++ b/src/test/resources/lexer/HeaderCommentWithCodeBefore.txt @@ -0,0 +1,8 @@ +public class HelloWorld { + val a = 1 +} + +/* + * This comment describes the + * content of the file. + */ \ No newline at end of file diff --git a/src/test/resources/lexer/HeaderCommentWithWrongStart.txt b/src/test/resources/lexer/HeaderCommentWithWrongStart.txt new file mode 100644 index 0000000..c706531 --- /dev/null +++ b/src/test/resources/lexer/HeaderCommentWithWrongStart.txt @@ -0,0 +1,4 @@ +/** + * This comment describes the + * content of the file. + */ \ No newline at end of file diff --git a/src/test/resources/lexer/NormalCommentWithHeaderComment.txt b/src/test/resources/lexer/NormalCommentWithHeaderComment.txt new file mode 100644 index 0000000..2f52a33 --- /dev/null +++ b/src/test/resources/lexer/NormalCommentWithHeaderComment.txt @@ -0,0 +1,6 @@ +// Just a test + +/* + * This comment describes the + * content of the file. + */ \ No newline at end of file diff --git a/src/test/resources/lexer/SimpleHeaderComment.txt b/src/test/resources/lexer/SimpleHeaderComment.txt new file mode 100644 index 0000000..366ad3a --- /dev/null +++ b/src/test/resources/lexer/SimpleHeaderComment.txt @@ -0,0 +1,4 @@ +/* + * This comment describes the + * content of the file. + */ \ No newline at end of file diff --git a/src/test/resources/org/sonar/plugins/scala/cobertura/coverage.xml b/src/test/resources/org/sonar/plugins/scala/cobertura/coverage.xml new file mode 100644 index 0000000..fbd7b37 --- /dev/null +++ b/src/test/resources/org/sonar/plugins/scala/cobertura/coverage.xml @@ -0,0 +1,6768 @@ + + + + /Users/cpicat/myproject/grails-app/domain + + + /Users/cpicat/myproject/grails-app/controllers + + + /Users/cpicat/myproject/grails-app/taglib + + + /Users/cpicat/myproject/src/java + + + /Users/cpicat/myproject/grails-app/services + + + /Users/cpicat/myproject/src/scala + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/packageResolver/DeepNestedPackageDeclaration.txt b/src/test/resources/packageResolver/DeepNestedPackageDeclaration.txt new file mode 100644 index 0000000..4b877bb --- /dev/null +++ b/src/test/resources/packageResolver/DeepNestedPackageDeclaration.txt @@ -0,0 +1,18 @@ +package one.two.three.four { + + package five.six.seven { + + package eight.nine.ten.eleven { + + package twelve.thirteen.fourteen { + + package fifteen.sixteen { + + object A { + val b = 1 + } + } + } + } + } +} \ No newline at end of file diff --git a/src/test/resources/packageResolver/DeepNestedPackageDeclarationWithObjectBetween.txt b/src/test/resources/packageResolver/DeepNestedPackageDeclarationWithObjectBetween.txt new file mode 100644 index 0000000..af000f4 --- /dev/null +++ b/src/test/resources/packageResolver/DeepNestedPackageDeclarationWithObjectBetween.txt @@ -0,0 +1,22 @@ +package one { + + package two.three.four.five.six.seven { + + package eight { + + object B + + package nine.ten.eleven.twelve.thirteen.fourteen { + + object C + + package fifteen.sixteen { + + object A { + val b = 1 + } + } + } + } + } +} \ No newline at end of file diff --git a/src/test/resources/packageResolver/NestedPackageDeclaration.txt b/src/test/resources/packageResolver/NestedPackageDeclaration.txt new file mode 100644 index 0000000..40a9b04 --- /dev/null +++ b/src/test/resources/packageResolver/NestedPackageDeclaration.txt @@ -0,0 +1,9 @@ +package one.two { + + package three { + + object A { + val b = 1 + } + } +} \ No newline at end of file diff --git a/src/test/resources/packageResolver/NestedPackageDeclarationWithObjectBetween.txt b/src/test/resources/packageResolver/NestedPackageDeclarationWithObjectBetween.txt new file mode 100644 index 0000000..1c1ae97 --- /dev/null +++ b/src/test/resources/packageResolver/NestedPackageDeclarationWithObjectBetween.txt @@ -0,0 +1,11 @@ +package one.two { + + object B + + package three { + + object A { + val b = 1 + } + } +} \ No newline at end of file diff --git a/src/test/resources/packageResolver/SimplePackageDeclaration.txt b/src/test/resources/packageResolver/SimplePackageDeclaration.txt new file mode 100644 index 0000000..9f19b5f --- /dev/null +++ b/src/test/resources/packageResolver/SimplePackageDeclaration.txt @@ -0,0 +1,6 @@ +package one { + + object A { + val b = 1 + } +} \ No newline at end of file diff --git a/src/test/resources/scalaFile/ScalaFile1.scala b/src/test/resources/scalaFile/ScalaFile1.scala new file mode 100644 index 0000000..0d30e45 --- /dev/null +++ b/src/test/resources/scalaFile/ScalaFile1.scala @@ -0,0 +1,5 @@ +package scalaFile + +class ScalaFile1 { + +} \ No newline at end of file diff --git a/src/test/resources/scalaFile/ScalaTestFile1.scala b/src/test/resources/scalaFile/ScalaTestFile1.scala new file mode 100644 index 0000000..5f58e94 --- /dev/null +++ b/src/test/resources/scalaFile/ScalaTestFile1.scala @@ -0,0 +1,5 @@ +package scalaFile + +class ScalaTestFile1 { + +} \ No newline at end of file diff --git a/src/test/resources/scalaSourceImporter/JavaMainFile1.java b/src/test/resources/scalaSourceImporter/JavaMainFile1.java new file mode 100644 index 0000000..ad3a4b5 --- /dev/null +++ b/src/test/resources/scalaSourceImporter/JavaMainFile1.java @@ -0,0 +1,5 @@ +package scalaSourceImporter; + +public class JavaMainFile1 { + +} \ No newline at end of file diff --git a/src/test/resources/scalaSourceImporter/JavaTestFile1.java b/src/test/resources/scalaSourceImporter/JavaTestFile1.java new file mode 100644 index 0000000..742f8b0 --- /dev/null +++ b/src/test/resources/scalaSourceImporter/JavaTestFile1.java @@ -0,0 +1,5 @@ +package scalaSourceImporter; + +public class JavaTestFile1 { + +} \ No newline at end of file diff --git a/src/test/resources/scalaSourceImporter/MainFile1.scala b/src/test/resources/scalaSourceImporter/MainFile1.scala new file mode 100644 index 0000000..3cbae7a --- /dev/null +++ b/src/test/resources/scalaSourceImporter/MainFile1.scala @@ -0,0 +1,5 @@ +package scalaSourceImporter + +class MainFile1 { + +} \ No newline at end of file diff --git a/src/test/resources/scalaSourceImporter/MainFile2.scala b/src/test/resources/scalaSourceImporter/MainFile2.scala new file mode 100644 index 0000000..d2474a8 --- /dev/null +++ b/src/test/resources/scalaSourceImporter/MainFile2.scala @@ -0,0 +1,5 @@ +package scalaSourceImporter + +class MainFile2 { + +} \ No newline at end of file diff --git a/src/test/resources/scalaSourceImporter/MainFile3.scala b/src/test/resources/scalaSourceImporter/MainFile3.scala new file mode 100644 index 0000000..a0d7a39 --- /dev/null +++ b/src/test/resources/scalaSourceImporter/MainFile3.scala @@ -0,0 +1,5 @@ +package scalaSourceImporter + +class MainFile3 { + +} \ No newline at end of file diff --git a/src/test/resources/scalaSourceImporter/TestFile1.scala b/src/test/resources/scalaSourceImporter/TestFile1.scala new file mode 100644 index 0000000..efbd263 --- /dev/null +++ b/src/test/resources/scalaSourceImporter/TestFile1.scala @@ -0,0 +1,5 @@ +package scalaSourceImporter + +class TestFile1 { + +} \ No newline at end of file diff --git a/src/test/resources/scalaSourceImporter/TestFile2.scala b/src/test/resources/scalaSourceImporter/TestFile2.scala new file mode 100644 index 0000000..dfc8fc3 --- /dev/null +++ b/src/test/resources/scalaSourceImporter/TestFile2.scala @@ -0,0 +1,5 @@ +package scalaSourceImporter + +class TestFile2 { + +} \ No newline at end of file diff --git a/src/test/resources/scalaSourceImporter/TestFile3.scala b/src/test/resources/scalaSourceImporter/TestFile3.scala new file mode 100644 index 0000000..b7fe94d --- /dev/null +++ b/src/test/resources/scalaSourceImporter/TestFile3.scala @@ -0,0 +1,5 @@ +package scalaSourceImporter + +class TestFile3 { + +} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/compiler/LexerSpec.scala b/src/test/scala/com/buransky/plugins/scala/compiler/LexerSpec.scala new file mode 100644 index 0000000..79d0469 --- /dev/null +++ b/src/test/scala/com/buransky/plugins/scala/compiler/LexerSpec.scala @@ -0,0 +1,87 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.compiler + +import tools.nsc.ast.parser.Tokens._ + +import java.util.Arrays +import org.junit.runner.RunWith +import org.scalatest.FlatSpec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner + + + +import collection.JavaConversions._ +import com.buransky.plugins.scala.language.{CommentType, Comment} +import com.buransky.plugins.scala.util.FileTestUtils + +@RunWith(classOf[JUnitRunner]) +class LexerSpec extends FlatSpec with ShouldMatchers { + + private val lexer = new Lexer() + + private val headerComment = "/*\r\n * This comment describes the\r\n" + + " * content of the file.\r\n */" + + "A lexer" should "tokenize a simple declaration of a value" in { + val tokens = lexer.getTokens("val a = " + "\r\n" + "42") + tokens should equal (Arrays.asList(Token(VAL, 1), Token(IDENTIFIER, 1), Token(EQUALS, 1), Token(INTLIT, 2))) + } + + it should "tokenize a doc comment" in { + val comments = getCommentsOf("DocComment1") + comments should have size(1) + comments should contain (new Comment("/** Hello World */", CommentType.DOC)) + } + + it should "tokenize a header comment" in { + val comments = getCommentsOf("SimpleHeaderComment") + comments should have size(1) + comments should contain (new Comment(headerComment, CommentType.HEADER)) + } + + it should "not tokenize a header comment when it is not the first comment" in { + val comments = getCommentsOf("NormalCommentWithHeaderComment") + comments should have size(2) + comments should contain (new Comment("// Just a test", CommentType.NORMAL)) + comments should contain (new Comment(headerComment, CommentType.NORMAL)) + } + + it should "not tokenize a header comment when it is not starting with /*" in { + val comments = getCommentsOf("HeaderCommentWithWrongStart") + comments should have size(1) + comments should contain (new Comment("/**\r\n * This comment describes the\r\n" + + " * content of the file.\r\n */", CommentType.DOC)) + } + + it should "not tokenize a header comment when there was code before" in { + val comments = getCommentsOf("HeaderCommentWithCodeBefore") + comments should have size(1) + comments should contain (new Comment(headerComment, CommentType.NORMAL)) + } + + // TODO add more specs for lexer + + private def getCommentsOf(fileName: String) = { + val path = FileTestUtils.getRelativePath("/lexer/" + fileName + ".txt") + lexer.getCommentsOfFile(path) + } +} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/language/CodeDetectorSpec.scala b/src/test/scala/com/buransky/plugins/scala/language/CodeDetectorSpec.scala new file mode 100644 index 0000000..f01b2b8 --- /dev/null +++ b/src/test/scala/com/buransky/plugins/scala/language/CodeDetectorSpec.scala @@ -0,0 +1,61 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.language + +import org.junit.runner.RunWith +import org.scalatest.FlatSpec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class CodeDetectorSpec extends FlatSpec with ShouldMatchers { + + "A code detector" should "detect a simple variable declaration" in { + CodeDetector.hasDetectedCode("val a = 1") should be (true) + } + + it should "detect a simple function call" in { + CodeDetector.hasDetectedCode("list.map(_ + \"Hello World\")") should be (true) + } + + it should "detect a simple value assignment" in { + CodeDetector.hasDetectedCode("a = 1 + 2") should be (true) + } + + it should "detect a simple package declaration" in { + CodeDetector.hasDetectedCode("package hello.world") should be (true) + } + + it should "not detect any code in a normal text" in { + CodeDetector.hasDetectedCode("this is just a normal text") should be (false) + } + + it should "not detect any code in a normal comment text" in { + CodeDetector.hasDetectedCode("// this is a normal comment") should be (false) + } + + it should "detect a while loop" in { + CodeDetector.hasDetectedCode("while (i == 2) { println(i); }") should be (true) + } + + it should "detect a for loop" in { + CodeDetector.hasDetectedCode("for (i <- 1 to 10) { println(i); }") should be (true) + } +} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/language/PackageResolverSpec.scala b/src/test/scala/com/buransky/plugins/scala/language/PackageResolverSpec.scala new file mode 100644 index 0000000..47fb3c3 --- /dev/null +++ b/src/test/scala/com/buransky/plugins/scala/language/PackageResolverSpec.scala @@ -0,0 +1,61 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.language + +import org.junit.runner.RunWith +import org.scalatest.FlatSpec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner + +import com.buransky.plugins.scala.util.FileTestUtils +; + +@RunWith(classOf[JUnitRunner]) +class PackageResolverSpec extends FlatSpec with ShouldMatchers { + + "A package resolver" should "resolve the package name of a simple package declaration" in { + getPackageNameOf("SimplePackageDeclaration") should equal ("one") + } + + it should "resolve the package name of a nested package declaration" in { + getPackageNameOf("NestedPackageDeclaration") should equal ("one.two.three") + } + + it should "resolve the package name of a deep nested package declaration" in { + getPackageNameOf("DeepNestedPackageDeclaration") should equal ("one.two.three.four.five.six." + + "seven.eight.nine.ten.eleven.twelve.thirteen.fourteen.fifteen.sixteen") + } + + it should "resolve the upper package name of a nested package declaration with " + + "an object declaration between" in { + getPackageNameOf("NestedPackageDeclarationWithObjectBetween") should equal ("one.two") + } + + it should "resolve the package name of a deep nested package declaration with" + + "an object declaration between" in { + getPackageNameOf("DeepNestedPackageDeclarationWithObjectBetween") should equal ("one.two.three." + + "four.five.six.seven.eight") + } + + private def getPackageNameOf(fileName: String) = { + val path = FileTestUtils.getRelativePath("/packageResolver/" + fileName + ".txt") + PackageResolver.resolvePackageNameOfFile(path) + } +} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/metrics/ComplexityCalculatorSpec.scala b/src/test/scala/com/buransky/plugins/scala/metrics/ComplexityCalculatorSpec.scala new file mode 100644 index 0000000..6730cd9 --- /dev/null +++ b/src/test/scala/com/buransky/plugins/scala/metrics/ComplexityCalculatorSpec.scala @@ -0,0 +1,209 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.metrics + +import org.junit.runner.RunWith +import org.scalatest.FlatSpec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class ComplexityCalculatorSpec extends FlatSpec with ShouldMatchers { + + "A complexity calculator" should "calculate complexity of if expression" in { + ComplexityCalculator.measureComplexity("if (2 == 3) println(123)") should be (1) + } + + it should "calculate complexity of for loop" in { + ComplexityCalculator.measureComplexity("for (i <- 1 to 10) println(i)") should be (1) + } + + it should "calculate complexity of while loop" in { + val source = """var i = 0 + while (i < 10) { + println(i) + i += 1 + }""" + ComplexityCalculator.measureComplexity(source) should be (1) + } + + it should "calculate complexity of do loop" in { + val source = """var i = 0 + do { + println(i) + i += 1 + } while (i < 10)""" + ComplexityCalculator.measureComplexity(source) should be (1) + } + + it should "calculate complexity of throw expression" in { + ComplexityCalculator.measureComplexity("throw new RuntimeException()") should be (1) + } + + it should "calculate complexity of while loop with an if condition and throw expression" in { + val source = """var i = 0 + while (i < 10) { + println(i) + i += 1 + if (i == 9) + throw new RuntimeException() + }""" + ComplexityCalculator.measureComplexity(source) should be (3) + } + + it should "calculate complexity of function definition" in { + ComplexityCalculator.measureComplexity("def inc(i: Int) = i + 1") should be (1) + } + + it should "calculate complexity of function definition with an if condition" in { + val source = """def inc(i: Int) = { + if (i == 0) { + i + 2 + } else { + i + 1 + } + }""" + ComplexityCalculator.measureComplexity(source) should be (2) + } + + it should "calculate complexity of function definition and its whole body" in { + val source = """def inc(i: Int) = { + if (i == 0) { + i + 2 + } else { + while (i < 10) { + if (i == 9) + throw new RuntimeException() + i + 1 + } + } + }""" + ComplexityCalculator.measureComplexity(source) should be (5) + } + + it should "calculate complexity distribution of one function" in { + val source = """def inc(i: Int) = { + if (i == 0) { + i + 2 + } else { + i + 1 + } + }""" + + ComplexityCalculator.measureComplexityOfFunctions(source).getMeasure.getData should include ("2=1") + } + + it should "calculate complexity distribution of two functions" in { + val source = """def inc(i: Int) = { + if (i == 0) { + i + 2 + } else { + i + 1 + } + } + + def dec(i: Int) = i - 1""" + + ComplexityCalculator.measureComplexityOfFunctions(source).getMeasure.getData should include ("1=1") + ComplexityCalculator.measureComplexityOfFunctions(source).getMeasure.getData should include ("2=1") + } + + it should "calculate complexity distribution of all functions" in { + val source = """def inc(i: Int) = { + if (i == 0) { + i + 2 + } else { + i + 1 + } + } + + def dec(i: Int) = i - 1 + def dec2(i: Int) = i - 2 + def dec3(i: Int) = i - 3""" + + ComplexityCalculator.measureComplexityOfFunctions(source).getMeasure.getData should include ("1=3") + ComplexityCalculator.measureComplexityOfFunctions(source).getMeasure.getData should include ("2=1") + } + + it should "calculate complexity distribution of all functions nested in a class" in { + val source = """class A { + def inc(i: Int) = { + if (i == 0) { + i + 2 + } else { + i + 1 + } + } + + def dec(i: Int) = i - 1 + def dec2(i: Int) = i - 2 + def dec3(i: Int) = i - 3 + }""" + + ComplexityCalculator.measureComplexityOfFunctions(source).getMeasure.getData should include ("1=3") + ComplexityCalculator.measureComplexityOfFunctions(source).getMeasure.getData should include ("2=1") + } + + it should "calculate complexity distribution of one class" in { + val source = """class A { + def inc(i: Int) = { + if (i == 0) { + i + 2 + } else { + i + 1 + } + } + }""" + + ComplexityCalculator.measureComplexityOfClasses(source).getMeasure.getData should include ("0=1") + } + + it should "calculate complexity distribution of two classes" in { + val source = """package abc + class A { + def inc(i: Int) = { + if (i == 0) { + i + 2 + } else { + i + 1 + } + } + + def dec(i: Int) = i - 1 + } + + class B { + def inc(i: Int) = { + if (i == 0) { + i + 2 + } else { + i + 1 + } + } + + def dec(i: Int) = i - 1 + def dec2(i: Int) = i - 2 + def dec3(i: Int) = i - 3 + }""" + + ComplexityCalculator.measureComplexityOfClasses(source).getMeasure.getData should include ("0=1") + ComplexityCalculator.measureComplexityOfClasses(source).getMeasure.getData should include ("5=1") + } +} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/metrics/FunctionCounterSpec.scala b/src/test/scala/com/buransky/plugins/scala/metrics/FunctionCounterSpec.scala new file mode 100644 index 0000000..7463cb9 --- /dev/null +++ b/src/test/scala/com/buransky/plugins/scala/metrics/FunctionCounterSpec.scala @@ -0,0 +1,122 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.metrics + +import org.junit.runner.RunWith +import org.scalatest.FlatSpec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class FunctionCounterSpec extends FlatSpec with ShouldMatchers { + + "A function counter" should "count a simple function declaration" in { + FunctionCounter.countFunctions("def test = 42") should be (1) + } + + it should "count a simple method declaration" in { + FunctionCounter.countFunctions("def test { println(42) }") should be (1) + } + + it should "not count a simple function declared as a function literal" in { + FunctionCounter.countFunctions("(i: Int) => i + 1") should be (0) + } + + it should "count a simple function declaration nested in another function" in { + val source = """ + def test = { + def inc(i: Int) = i + 1 + }""" + FunctionCounter.countFunctions(source) should be (2) + } + + it should "count a simple function declaration nested in another method" in { + val source = """ + def test { + def inc(i: Int) = i + 1 + }""" + FunctionCounter.countFunctions(source) should be (2) + } + + it should "not count an empty constructor as a function declaration" in { + val source = "class Person(val name: String) { }" + FunctionCounter.countFunctions(source) should be (0) + } + + it should "count a constructor as a function declaration" in { + val source = """ + class Person(val name: String) { + def this(name: String) { + super(name) + println(name) + } + }""" + FunctionCounter.countFunctions(source) should be (1) + } + + it should "count a simple function declaration nested in an object" in { + val source = """ + object Test { + def inc(i: Int) = { i + 1 } + }""" + FunctionCounter.countFunctions(source) should be (1) + } + + it should "count a simple function declaration nested in a trait" in { + val source = """ + trait Test { + def inc(i: Int) = { i + 1 } + }""" + FunctionCounter.countFunctions(source) should be (1) + } + + it should "count a function declaration with two parameter lists" in { + val source = "def sum(x: Int)(y: Int) = { x + y }" + FunctionCounter.countFunctions(source) should be (1) + } + + it should "count a simple function declaration nested in a trait with self-type annotation" in { + val source = """ + trait Test { + self: HelloWorld => + def inc(i: Int) = { i + 1 } + }""" + FunctionCounter.countFunctions(source) should be (1) + } + + it should "count a function declaration with two parameter lists nested in a trait with self-type annotation" in { + val source = """ + trait Test { + self: HelloWorld => + def sum(x: Int)(y: Int) = { x + y } + }""" + FunctionCounter.countFunctions(source) should be (1) + } + + it should "count a function declaration with if else block in its body" in { + val source = """ + def test(number: Int) : Int = + if (number < 42) + 23 + else + 42""" + FunctionCounter.countFunctions(source) should be (1) + } +} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/metrics/PublicApiCounterSpec.scala b/src/test/scala/com/buransky/plugins/scala/metrics/PublicApiCounterSpec.scala new file mode 100644 index 0000000..f34a7cc --- /dev/null +++ b/src/test/scala/com/buransky/plugins/scala/metrics/PublicApiCounterSpec.scala @@ -0,0 +1,150 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.metrics + +import org.junit.runner.RunWith +import org.scalatest.FlatSpec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class PublicApiCounterSpec extends FlatSpec with ShouldMatchers { + + "A public api counter" should "count a simple function declaration" in { + PublicApiCounter.countPublicApi("def test = 42") should be (1) + } + + it should "count a simple method declaration" in { + PublicApiCounter.countPublicApi("def test { println(42) }") should be (1) + } + + it should "count a simple value declaration" in { + PublicApiCounter.countPublicApi("val maybeImportantNumber = 42") should be (1) + } + + it should "not count a private value declaration" in { + PublicApiCounter.countPublicApi("private val maybeImportantNumber = 42") should be (0) + } + + it should "not count a private function declaration" in { + PublicApiCounter.countPublicApi("private def test = 42") should be (0) + } + + it should "not count a private method declaration" in { + PublicApiCounter.countPublicApi("private def test { println(42) }") should be (0) + } + + it should "count a class declaration" in { + PublicApiCounter.countPublicApi("class A {}") should be (1) + } + + it should "count an object declaration" in { + PublicApiCounter.countPublicApi("object A {}") should be (1) + } + + it should "count a trait declaration" in { + PublicApiCounter.countPublicApi("trait A {}") should be (1) + } + + it should "not count a private class declaration" in { + PublicApiCounter.countPublicApi("private class A {}") should be (0) + } + + it should "not count a private object declaration" in { + PublicApiCounter.countPublicApi("private object A {}") should be (0) + } + + it should "not count a private trait declaration" in { + PublicApiCounter.countPublicApi("private trait A {}") should be (0) + } + + it should "count an undocumented class declaration" in { + PublicApiCounter.countUndocumentedPublicApi("class A {}") should be (1) + } + + it should "not count a documented class declaration as undocumented one" in { + val source = """/** + * This is a comment of a public api member. + */ + class A {}""" + PublicApiCounter.countUndocumentedPublicApi(source) should be (0) + } + + it should "count an undocumented class declaration with package declaration before" in { + val source = """package a.b.c + + class A {}""" + PublicApiCounter.countUndocumentedPublicApi(source) should be (1) + } + + it should "not count a documented class declaration with package declaration before as undocumented one" in { + val source = """package a.b.c + + /** + * This is a comment of a public api member. + */ + class A {}""" + PublicApiCounter.countUndocumentedPublicApi(source) should be (0) + } + + it should "count all public api members of class and its undocumented ones" in { + val source = """package a.b.c + + /** + * This is a comment of a public api member. + */ + class A { + + /** + * Well, don't panic. ;-) + */ + val meaningOfLife = 42 + + val b = "test" + + def helloWorld { printString("Hello World!") } + + private def printString(str: String) { println(str) } + }""" + + PublicApiCounter.countPublicApi(source) should be (4) + PublicApiCounter.countUndocumentedPublicApi(source) should be (2) + } + + it should "not count nested function and method declarations" in { + val source ="""def test = { + def a = 12 + 1 + def b = 13 + 1 + + a + b + 42 + }""" + PublicApiCounter.countPublicApi(source) should be (1) + } + + it should "not count nested value declarations" in { + val source ="""val test = { + def a = 12 + 1 + def b = 13 + 1 + + a + b + 42 + }""" + PublicApiCounter.countPublicApi(source) should be (1) + } +} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/metrics/StatementCounterSpec.scala b/src/test/scala/com/buransky/plugins/scala/metrics/StatementCounterSpec.scala new file mode 100644 index 0000000..c7d4ead --- /dev/null +++ b/src/test/scala/com/buransky/plugins/scala/metrics/StatementCounterSpec.scala @@ -0,0 +1,174 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.metrics + +import org.junit.runner.RunWith +import org.scalatest.FlatSpec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class StatementCounterSpec extends FlatSpec with ShouldMatchers { + + "A statement counter" should "count a simple assignment as a statement" in { + StatementCounter.countStatements("a = 1") should be (1) + } + + it should "count a simple method call as a statement" in { + StatementCounter.countStatements("println(123)") should be (1) + } + + it should "not count a simple variable declaration as a statement" in { + StatementCounter.countStatements("var a") should be (0) + } + + it should "count a simple variable declaration with assignment as a statement" in { + StatementCounter.countStatements("var a = 2") should be (1) + } + + it should "count a while loop as a statement" in { + StatementCounter.countStatements("while (1 == 1) {}") should be (1) + } + + it should "count a for loop as a statement" in { + StatementCounter.countStatements("for (i <- 1 to 10) {}") should be (1) + } + + it should "count a while loop as a statement and all statements in loop body" in { + val source = """ + while (1 == 1) { + val a = inc(2) + }""" + StatementCounter.countStatements(source) should be (2) + } + + it should "count a for loop as a statement and all statements in loop body" in { + val source = """ + for (i <- 1 to 10) { + val a = inc(2) + }""" + StatementCounter.countStatements(source) should be (2) + } + + it should "count if as a statement" in { + val source = """ + if (1 == 1) + println()""" + StatementCounter.countStatements(source) should be (2) + } + + it should "count an if block as a statement and all statements in its body" in { + val source = """ + if (1 + 2 < 4) { + val a = inc(2) + println(3) + def test = { 1 + 2 } + }""" + StatementCounter.countStatements(source) should be (4) + } + + it should "count a simple if else block as a statement" in { + val source = """ + if (1+2 < 4) + println("Hello World") + else + println("123")""" + StatementCounter.countStatements(source) should be (4) + } + + it should "count an if else block as a statement and all statements in its body" in { + val source = """ + if (1 + 2 < 4) { + val a = inc(2) + println("Hello World") + def test = 1 + 2 + } else { + def test2 = 1 + val b = test2 + }""" + StatementCounter.countStatements(source) should be (7) + } + + it should "count all statements in body of a function definition" in { + val source = """ + def test(i: Int) = { + val a = i + 42 + println(a) + println(i + 42) + a + }""" + StatementCounter.countStatements(source) should be (4) + } + + it should "count all statements in body of a value definition" in { + val source = """ + val test = { + val a = i + 42 + println(a) + println(i + 42) + a + }""" + StatementCounter.countStatements(source) should be (4) + } + + it should "count for comprehension with yield statement" in { + val source = "for (x <- List(1, 2, 3, 4, 5) if (x % 2 != 0)) yield x" + StatementCounter.countStatements(source) should be (2) + } + + it should "count for comprehension with more complex yield statement" in { + val source = "for (x <- List(1, 2, 3, 4, 5) if (x % 2 != 0)) yield x + inc(x)" + StatementCounter.countStatements(source) should be (2) + } + + it should "count for comprehension with yield statement where return value is only a literal" in { + val source = "for (x <- List(1, 2, 3, 4, 5) if (x % 2 != 0)) yield 2" + StatementCounter.countStatements(source) should be (2) + } + + it should "count foreach function call on a list as a statement" in { + val source = """ + myList.foreach {i => + println(i) + val a = i + 1 + println("inc: " + i) + }""" + StatementCounter.countStatements(source) should be (4) + } + + it should "count foreach function call and all statements in its body" in { + val source = """ + def foo() = { + List("Hello", "World", "!").foreach(word => + if (find(By(name, word)).isEmpty) + create.name(word).save + ) + }""" + StatementCounter.countStatements(source) should be (3) + } + + it should "count function call in a function definition nested in an object" in { + val source = """ + object name extends MappedPoliteString(this, 100) { + override def validations = valMinLen(1, S.?("attributeName")) _ :: Nil + }""" + StatementCounter.countStatements(source) should be (2) + } +} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/metrics/TypeCounterSpec.scala b/src/test/scala/com/buransky/plugins/scala/metrics/TypeCounterSpec.scala new file mode 100644 index 0000000..6415436 --- /dev/null +++ b/src/test/scala/com/buransky/plugins/scala/metrics/TypeCounterSpec.scala @@ -0,0 +1,165 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.metrics + +import org.junit.runner.RunWith +import org.scalatest.FlatSpec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class TypeCounterSpec extends FlatSpec with ShouldMatchers { + + "A type counter" should "count type of a simple class declaration" in { + TypeCounter.countTypes("class A {}") should be (1) + } + + it should "count type of a simple object declaration" in { + TypeCounter.countTypes("object A {}") should be (1) + } + + it should "count type of a simple trait declaration" in { + TypeCounter.countTypes("trait A {}") should be (1) + } + + it should "count type of a simple case class declaration" in { + TypeCounter.countTypes("case class A {}") should be (1) + } + + it should "count type of a simple class declaration nested in a package" in { + val source = """ + package a.b + class A {}""" + TypeCounter.countTypes(source) should be (1) + } + + it should "count type of a simple class declaration nested in a package with imports" in { + val source = """ + package a.b + import java.util.List + class A {}""" + TypeCounter.countTypes(source) should be (1) + } + + it should "count type of a simple class declaration nested in a package with import and doc comment" in { + val source = """ + package a.b + import java.util.List + /** Doc comment... */ + class A {}""" + TypeCounter.countTypes(source) should be (1) + } + + it should "count type of a simple object declaration nested in a package" in { + val source = """ + package a.b + object A {}""" + TypeCounter.countTypes(source) should be (1) + } + + it should "count types of a simple class declarations" in { + val source = """ + class A {} + class B {}""" + TypeCounter.countTypes(source) should be (2) + } + + it should "count type of a simple class declaration nested in a class" in { + TypeCounter.countTypes("class A { class B {} }") should be (2) + } + + it should "count type of a simple class declaration nested in an object" in { + TypeCounter.countTypes("object A { class B {} }") should be (2) + } + + it should "count type of a simple object declaration nested in a class" in { + TypeCounter.countTypes("class A { object B {} }") should be (2) + } + + it should "count type of a simple object declaration nested in an object" in { + TypeCounter.countTypes("object A { object B {} }") should be (2) + } + + it should "count type of a simple class declaration nested in a function" in { + val source = """ + def fooBar(i: Int) = { + class B { val a = 1 } + i + new B().a + }""" + TypeCounter.countTypes(source) should be (1) + } + + it should "count type of a simple class declaration nested in a value definition" in { + val source = """ + val fooBar = { + class B { val a = 1 } + 1 + new B().a + }""" + TypeCounter.countTypes(source) should be (1) + } + + it should "count type of a simple class declaration nested in an assignment" in { + val source = """ + fooBar = { + class B { val a = 1 } + 1 + new B().a + }""" + TypeCounter.countTypes(source) should be (1) + } + + it should "count type of a simple class declaration nested in a code block" in { + val source = """ + { + 1 + new B().a + class B { val a = 1 } + }""" + TypeCounter.countTypes(source) should be (1) + } + + it should "count type of a simple class declaration nested in a loop" in { + val source = """ + var i = 0 + while (i == 2) { + i = i + new B().a + class B { val a = 1 } + }""" + TypeCounter.countTypes(source) should be (1) + } + + it should "count type of a simple class declaration nested in a match statement" in { + val source = """ + var i = 0 + i match { + case 0 => class B { val a = 1 } + case _ => + }""" + TypeCounter.countTypes(source) should be (1) + } + + it should "count type of a simple class declaration nested in a try statement" in { + val source = """ + try { + class B { val a = 1 } + } catch { + case _ => + }""" + TypeCounter.countTypes(source) should be (1) + } +} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/util/MetricDistributionSpec.scala b/src/test/scala/com/buransky/plugins/scala/util/MetricDistributionSpec.scala new file mode 100644 index 0000000..1402e4b --- /dev/null +++ b/src/test/scala/com/buransky/plugins/scala/util/MetricDistributionSpec.scala @@ -0,0 +1,97 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.util + +import org.junit.runner.RunWith +import org.scalatest.FlatSpec +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.junit.JUnitRunner +import org.sonar.api.measures.CoreMetrics + +@RunWith(classOf[JUnitRunner]) +class MetricDistributionSpec extends FlatSpec with ShouldMatchers { + + val metric = CoreMetrics.CLASS_COMPLEXITY_DISTRIBUTION + val ranges = Array[Number](1, 5, 10) + + "A metric distribution" should "increment occurence of value" in { + val distribution = new MetricDistribution(metric, ranges) + distribution.add(1.0) + distribution.getMeasure.getData should be ("1=1;5=0;10=0") + } + + it should "increment occurence of all values" in { + val distribution = new MetricDistribution(metric, ranges) + distribution.add(1.0) + distribution.add(10.0) + distribution.add(5.0) + distribution.getMeasure.getData should be ("1=1;5=1;10=1") + } + + it should "increase occurence of value by submitted number" in { + val distribution = new MetricDistribution(metric, ranges) + distribution.add(1.0, 3) + distribution.getMeasure.getData should be ("1=3;5=0;10=0") + } + + it should "increase occurence of all values by submitted number" in { + val distribution = new MetricDistribution(metric, ranges) + distribution.add(1.0, 3) + distribution.add(10.0, 8) + distribution.add(5.0, 2) + distribution.getMeasure.getData should be ("1=3;5=2;10=8") + } + + it should "increase occurence of value by submitted distribution" in { + val distribution = new MetricDistribution(metric, ranges) + distribution.add(1.0, 3) + + val otherDistribution = new MetricDistribution(metric, ranges) + otherDistribution.add(distribution) + + otherDistribution.getMeasure.getData should be ("1=3;5=0;10=0") + } + + it should "increase occurence of all values by submitted distribution" in { + val distribution = new MetricDistribution(metric, ranges) + distribution.add(1.0, 3) + distribution.add(10.0, 8) + distribution.add(5.0, 2) + + val otherDistribution = new MetricDistribution(metric, ranges) + otherDistribution.add(distribution) + + otherDistribution.getMeasure.getData should be ("1=3;5=2;10=8") + } + + it should "output an empty distribution properly" in { + val distribution = new MetricDistribution(metric, ranges) + distribution.getMeasure.getData should be ("1=0;5=0;10=0") + } + + it should "copy an empty distribution properly" in { + val distribution = new MetricDistribution(metric, ranges) + + val otherDistribution = new MetricDistribution(metric, ranges) + otherDistribution.add(distribution) + + otherDistribution.getMeasure.getData should be ("1=0;5=0;10=0") + } +} \ No newline at end of file diff --git a/test b/test new file mode 100755 index 0000000..27b0981 --- /dev/null +++ b/test @@ -0,0 +1,7 @@ +#!/bin/bash + +cd /home/rado/workspace/aaa +#sbt clean scoverage:test +/home/rado/bin/sonar-runner/bin/sonar-runner + +cd /home/rado//workspace/sonar-scoverage-plugin From 00f1eba102797d7521246214dc101373110f7b8c Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Tue, 4 Feb 2014 11:26:15 -0800 Subject: [PATCH 003/101] parseFakeReport --- .../scala/cobertura/CoberturaSensor.java | 86 +++++++-- .../scala/cobertura/ScalaCoberturaParser.java | 42 +++-- .../plugins/scala/language/ScalaRealFile.java | 119 ++++++++++++ .../scala/sensor/BaseMetricsSensor.java | 178 +++++++++--------- .../sensor/ScalaSourceImporterSensor.java | 74 ++++---- 5 files changed, 341 insertions(+), 158 deletions(-) create mode 100644 src/main/java/com/buransky/plugins/scala/language/ScalaRealFile.java diff --git a/src/main/java/com/buransky/plugins/scala/cobertura/CoberturaSensor.java b/src/main/java/com/buransky/plugins/scala/cobertura/CoberturaSensor.java index d684634..7bf2e18 100644 --- a/src/main/java/com/buransky/plugins/scala/cobertura/CoberturaSensor.java +++ b/src/main/java/com/buransky/plugins/scala/cobertura/CoberturaSensor.java @@ -20,41 +20,91 @@ package com.buransky.plugins.scala.cobertura; import com.buransky.plugins.scala.language.Scala; +import com.buransky.plugins.scala.language.ScalaRealFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.batch.CoverageExtension; import org.sonar.api.batch.Sensor; import org.sonar.api.batch.SensorContext; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.CoverageMeasuresBuilder; +import org.sonar.api.measures.Measure; +import org.sonar.api.resources.Directory; +import org.sonar.api.resources.InputFile; import org.sonar.api.resources.Project; +import org.sonar.api.resources.ProjectFileSystem; import org.sonar.plugins.cobertura.api.AbstractCoberturaParser; import org.sonar.plugins.cobertura.api.CoberturaUtils; import java.io.File; +import java.util.HashMap; +import java.util.Map; public class CoberturaSensor implements Sensor, CoverageExtension { - private static final Logger LOG = LoggerFactory.getLogger(CoberturaSensor.class); - private static final AbstractCoberturaParser COBERTURA_PARSER = new ScalaCoberturaParser(); + private static final Logger LOG = LoggerFactory.getLogger(CoberturaSensor.class); + private static final AbstractCoberturaParser COBERTURA_PARSER = new ScalaCoberturaParser(); - public boolean shouldExecuteOnProject(Project project) { - return project.getAnalysisType().isDynamic(true) && Scala.INSTANCE.getKey().equals(project.getLanguageKey()); - } + public boolean shouldExecuteOnProject(Project project) { + return project.getAnalysisType().isDynamic(true) && Scala.INSTANCE.getKey().equals(project.getLanguageKey()); + } + + public void analyse(Project project, SensorContext context) { + File report = CoberturaUtils.getReport(project); + if (report != null) { + //parseReport(report, context); + parseFakeReport(project, context); + } + } + + protected void parseReport(File xmlFile, final SensorContext context) { + LOG.info("parsing {}", xmlFile); + + COBERTURA_PARSER.parseReport(xmlFile, context); + } - public void analyse(Project project, SensorContext context) { - File report = CoberturaUtils.getReport(project); - if (report != null) { - parseReport(report, context); + @Override + public String toString() { + return "Scala CoberturaSensor"; } - } - protected void parseReport(File xmlFile, final SensorContext context) { - LOG.info("parsing {}", xmlFile); - COBERTURA_PARSER.parseReport(xmlFile, context); - } + private void parseFakeReport(Project project, final SensorContext context) { + ProjectFileSystem fileSystem = project.getFileSystem(); - @Override - public String toString() { - return "Scala CoberturaSensor"; - } + HashMap dirs = new HashMap(); + for (InputFile sourceFile : fileSystem.mainFiles("scala")) { + LOG.info("[CoberturaSensor] Set coverage for [" + sourceFile.getRelativePath() + "]"); + ScalaRealFile scalaSourcefile = ScalaRealFile.fromInputFile(sourceFile); + + CoverageMeasuresBuilder coverage = CoverageMeasuresBuilder.create(); + coverage.setHits(1, 1); + coverage.setHits(2, 2); + coverage.setHits(3, 3); + coverage.setHits(4, 0); + coverage.setHits(5, 0); + coverage.setHits(6, 0); + coverage.setHits(7, 0); + coverage.setHits(8, 1); + coverage.setHits(9, 0); + coverage.setHits(10, 2); + coverage.setHits(11, 0); + coverage.setHits(12, 3); + coverage.setHits(13, 0); + + for (Measure measure : coverage.createMeasures()) { + context.saveMeasure(scalaSourcefile, measure); + } + + context.saveMeasure(scalaSourcefile, new Measure(CoreMetrics.COVERAGE, 51.4)); + dirs.put(scalaSourcefile.getParent().getKey(), scalaSourcefile.getParent()); + } + + for (Map.Entry e: dirs.entrySet()) { + LOG.info("[CoberturaSensor] Set dir coverage for [" + e.getKey() + "]"); + context.saveMeasure(e.getValue(), new Measure(CoreMetrics.COVERAGE, 23.4)); + } + + context.saveMeasure(project, new Measure(CoreMetrics.COVERAGE, 12.3)); + } } diff --git a/src/main/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParser.java b/src/main/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParser.java index 28cf490..143a358 100644 --- a/src/main/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParser.java +++ b/src/main/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParser.java @@ -19,29 +19,43 @@ */ package com.buransky.plugins.scala.cobertura; +import com.buransky.plugins.scala.language.ScalaRealFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.sonar.plugins.cobertura.api.AbstractCoberturaParser; import org.sonar.api.resources.Resource; import com.buransky.plugins.scala.language.ScalaFile; +import java.io.File; + public class ScalaCoberturaParser extends AbstractCoberturaParser { + private static final Logger LOG = LoggerFactory.getLogger(ScalaCoberturaParser.class); + @Override protected Resource getResource(String fileName) { - // TODO update the sbt scct plugin to provide the correct fully qualified class name. - if (fileName.startsWith("src.main.scala.")) - fileName = fileName.replace("src.main.scala.", ""); - else if (fileName.startsWith("app.")) - fileName = fileName.replace("app.", ""); +// // TODO update the sbt scct plugin to provide the correct fully qualified class name. +// if (fileName.startsWith("src.main.scala.")) +// fileName = fileName.replace("src.main.scala.", ""); +// else if (fileName.startsWith("app.")) +// fileName = fileName.replace("app.", ""); +// +// int packageTerminator = fileName.lastIndexOf('.'); +// +// if (packageTerminator < 0 ) { +// return new ScalaFile(null, fileName, false); +// } +// else { +// String packageName = fileName.substring(0, packageTerminator); +// String className = fileName.substring(packageTerminator + 1, fileName.length()); +// +// return new ScalaFile(packageName, className, false); +// } - int packageTerminator = fileName.lastIndexOf('.'); + File f = new File(fileName); + Resource result = new ScalaRealFile(f.getParent(), f.getName(), false); - if (packageTerminator < 0 ) { - return new ScalaFile(null, fileName, false); - } - else { - String packageName = fileName.substring(0, packageTerminator); - String className = fileName.substring(packageTerminator + 1, fileName.length()); + LOG.info("[Scoverage] getResource [" + result.getKey() + "]"); - return new ScalaFile(packageName, className, false); - } + return result; } } diff --git a/src/main/java/com/buransky/plugins/scala/language/ScalaRealFile.java b/src/main/java/com/buransky/plugins/scala/language/ScalaRealFile.java new file mode 100644 index 0000000..5efcbf1 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scala/language/ScalaRealFile.java @@ -0,0 +1,119 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2013 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scala.language; + +import org.apache.commons.lang.StringUtils; +import org.sonar.api.resources.*; +import org.sonar.api.utils.WildcardPattern; + +import java.io.File; + +/** + * This class implements a Scala source file for Sonar. + * + * @author Felix Müller + * @since 0.1 + */ +public class ScalaRealFile extends Resource { + + private final boolean isUnitTest; + private final String directory; + private final String fileName; + private final Directory parent; + + public ScalaRealFile(String directory, String fileName, boolean isUnitTest) { + super(); + this.isUnitTest = isUnitTest; + + this.directory = (directory == null) ? "" : directory.trim(); + this.fileName = fileName.trim(); + + parent = new Directory(directory); + setKey(getLongName()); + } + + @Override + public String getName() { + return fileName; + } + + @Override + public String getLongName() { + return directory + File.pathSeparatorChar + fileName; + } + + @Override + public String getDescription() { + return null; + } + + @Override + public Language getLanguage() { + return Scala.INSTANCE; + } + + @Override + public String getScope() { + return Scopes.FILE; + } + + @Override + public String getQualifier() { + return isUnitTest ? Qualifiers.UNIT_TEST_FILE : Qualifiers.FILE; + } + + @Override + public Directory getParent() { + return parent; + } + + @Override + public boolean matchFilePattern(String antPattern) { + final String patternWithoutFileSuffix = StringUtils.substringBeforeLast(antPattern, "."); + final WildcardPattern matcher = WildcardPattern.create(patternWithoutFileSuffix, "."); + return matcher.match(getKey()); + } + + public boolean isUnitTest() { + return isUnitTest; + } + + /** + * Shortcut for {@link #fromInputFile(org.sonar.api.resources.InputFile, boolean)} for source files. + */ + public static ScalaRealFile fromInputFile(InputFile inputFile) { + return ScalaRealFile.fromInputFile(inputFile, false); + } + + /** + * Creates a {@link com.buransky.plugins.scala.language.ScalaRealFile} from a file in the source directories. + * + * @param inputFile the file object with relative path + * @param isUnitTest whether it is a unit test file or a source file + * @return the {@link com.buransky.plugins.scala.language.ScalaRealFile} created if exists, null otherwise + */ + public static ScalaRealFile fromInputFile(InputFile inputFile, boolean isUnitTest) { + if (inputFile == null || inputFile.getFile() == null || inputFile.getRelativePath() == null) { + return null; + } + + return new ScalaRealFile(inputFile.getFileBaseDir().getPath(), inputFile.getFile().getName(), isUnitTest); + } +} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/sensor/BaseMetricsSensor.java b/src/main/java/com/buransky/plugins/scala/sensor/BaseMetricsSensor.java index f2ab2b4..5006b4d 100644 --- a/src/main/java/com/buransky/plugins/scala/sensor/BaseMetricsSensor.java +++ b/src/main/java/com/buransky/plugins/scala/sensor/BaseMetricsSensor.java @@ -24,9 +24,7 @@ import java.util.List; import java.util.Set; -import com.buransky.plugins.scala.language.Comment; -import com.buransky.plugins.scala.language.Scala; -import com.buransky.plugins.scala.language.ScalaFile; +import com.buransky.plugins.scala.language.*; import com.buransky.plugins.scala.metrics.CommentsAnalyzer; import com.buransky.plugins.scala.metrics.LinesAnalyzer; import com.buransky.plugins.scala.util.StringUtils; @@ -35,11 +33,11 @@ import org.slf4j.LoggerFactory; import org.sonar.api.batch.SensorContext; import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.resources.Directory; import org.sonar.api.resources.InputFile; import org.sonar.api.resources.Project; import org.sonar.api.resources.ProjectFileSystem; import org.sonar.plugins.scala.compiler.Lexer; -import com.buransky.plugins.scala.language.ScalaPackage; import org.sonar.plugins.scala.metrics.ComplexityCalculator; import org.sonar.plugins.scala.metrics.FunctionCounter; import org.sonar.plugins.scala.metrics.PublicApiCounter; @@ -56,107 +54,107 @@ */ public class BaseMetricsSensor extends AbstractScalaSensor { - private static final Logger LOGGER = LoggerFactory.getLogger(BaseMetricsSensor.class); + private static final Logger LOGGER = LoggerFactory.getLogger(BaseMetricsSensor.class); - public BaseMetricsSensor(Scala scala) { - super(scala); - } + public BaseMetricsSensor(Scala scala) { + super(scala); + } + + public void analyse(Project project, SensorContext sensorContext) { + final ProjectFileSystem fileSystem = project.getFileSystem(); + final String charset = fileSystem.getSourceCharset().toString(); + final Set directories = new HashSet(); + + MetricDistribution complexityOfClasses = null; + MetricDistribution complexityOfFunctions = null; + + for (InputFile inputFile : fileSystem.mainFiles(getScala().getKey())) { + final ScalaRealFile scalaFile = ScalaRealFile.fromInputFile(inputFile); + directories.add(scalaFile.getParent()); + sensorContext.saveMeasure(scalaFile, CoreMetrics.FILES, 1.0); - public void analyse(Project project, SensorContext sensorContext) { - final ProjectFileSystem fileSystem = project.getFileSystem(); - final String charset = fileSystem.getSourceCharset().toString(); - final Set packages = new HashSet(); + try { + final String source = FileUtils.readFileToString(inputFile.getFile(), charset); + final List lines = StringUtils.convertStringToListOfLines(source); + final List comments = new Lexer().getComments(source); - MetricDistribution complexityOfClasses = null; - MetricDistribution complexityOfFunctions = null; + final CommentsAnalyzer commentsAnalyzer = new CommentsAnalyzer(comments); + final LinesAnalyzer linesAnalyzer = new LinesAnalyzer(lines, commentsAnalyzer); - for (InputFile inputFile : fileSystem.mainFiles(getScala().getKey())) { - final ScalaFile scalaFile = ScalaFile.fromInputFile(inputFile); - packages.add(scalaFile.getParent()); - sensorContext.saveMeasure(scalaFile, CoreMetrics.FILES, 1.0); + addLineMetrics(sensorContext, scalaFile, linesAnalyzer); + addCommentMetrics(sensorContext, scalaFile, commentsAnalyzer); + addCodeMetrics(sensorContext, scalaFile, source); + addPublicApiMetrics(sensorContext, scalaFile, source); - try { - final String source = FileUtils.readFileToString(inputFile.getFile(), charset); - final List lines = StringUtils.convertStringToListOfLines(source); - final List comments = new Lexer().getComments(source); + complexityOfClasses = sumUpMetricDistributions(complexityOfClasses, + ComplexityCalculator.measureComplexityOfClasses(source)); - final CommentsAnalyzer commentsAnalyzer = new CommentsAnalyzer(comments); - final LinesAnalyzer linesAnalyzer = new LinesAnalyzer(lines, commentsAnalyzer); + complexityOfFunctions = sumUpMetricDistributions(complexityOfFunctions, + ComplexityCalculator.measureComplexityOfFunctions(source)); - addLineMetrics(sensorContext, scalaFile, linesAnalyzer); - addCommentMetrics(sensorContext, scalaFile, commentsAnalyzer); - addCodeMetrics(sensorContext, scalaFile, source); - addPublicApiMetrics(sensorContext, scalaFile, source); + } catch (IOException ioe) { + LOGGER.error("Could not read the file: " + inputFile.getFile().getAbsolutePath(), ioe); + } + } - complexityOfClasses = sumUpMetricDistributions(complexityOfClasses, - ComplexityCalculator.measureComplexityOfClasses(source)); + if (complexityOfClasses != null) + sensorContext.saveMeasure(complexityOfClasses.getMeasure()); - complexityOfFunctions = sumUpMetricDistributions(complexityOfFunctions, - ComplexityCalculator.measureComplexityOfFunctions(source)); + if (complexityOfFunctions != null) + sensorContext.saveMeasure(complexityOfFunctions.getMeasure()); - } catch (IOException ioe) { - LOGGER.error("Could not read the file: " + inputFile.getFile().getAbsolutePath(), ioe); - } + computePackagesMetric(sensorContext, directories); } - if (complexityOfClasses != null) - sensorContext.saveMeasure(complexityOfClasses.getMeasure()); - - if (complexityOfFunctions != null) - sensorContext.saveMeasure(complexityOfFunctions.getMeasure()); - - computePackagesMetric(sensorContext, packages); - } - - private void addLineMetrics(SensorContext sensorContext, ScalaFile scalaFile, LinesAnalyzer linesAnalyzer) { - sensorContext.saveMeasure(scalaFile, CoreMetrics.LINES, (double) linesAnalyzer.countLines()); - sensorContext.saveMeasure(scalaFile, CoreMetrics.NCLOC, (double) linesAnalyzer.countLinesOfCode()); - } - - private void addCommentMetrics(SensorContext sensorContext, ScalaFile scalaFile, - CommentsAnalyzer commentsAnalyzer) { - sensorContext.saveMeasure(scalaFile, CoreMetrics.COMMENT_LINES, - (double) commentsAnalyzer.countCommentLines()); - sensorContext.saveMeasure(scalaFile, CoreMetrics.COMMENTED_OUT_CODE_LINES, - (double) commentsAnalyzer.countCommentedOutLinesOfCode()); - } - - private void addCodeMetrics(SensorContext sensorContext, ScalaFile scalaFile, String source) { - sensorContext.saveMeasure(scalaFile, CoreMetrics.CLASSES, - (double) TypeCounter.countTypes(source)); - sensorContext.saveMeasure(scalaFile, CoreMetrics.STATEMENTS, - (double) StatementCounter.countStatements(source)); - sensorContext.saveMeasure(scalaFile, CoreMetrics.FUNCTIONS, - (double) FunctionCounter.countFunctions(source)); - sensorContext.saveMeasure(scalaFile, CoreMetrics.COMPLEXITY, - (double) ComplexityCalculator.measureComplexity(source)); - } - - private void addPublicApiMetrics(SensorContext sensorContext, ScalaFile scalaFile, String source) { - sensorContext.saveMeasure(scalaFile, CoreMetrics.PUBLIC_API, - (double) PublicApiCounter.countPublicApi(source)); - sensorContext.saveMeasure(scalaFile, CoreMetrics.PUBLIC_UNDOCUMENTED_API, - (double) PublicApiCounter.countUndocumentedPublicApi(source)); - } - - private MetricDistribution sumUpMetricDistributions(MetricDistribution oldDistribution, - MetricDistribution newDistribution) { - if (oldDistribution == null) { - return newDistribution; + private void addLineMetrics(SensorContext sensorContext, ScalaRealFile scalaFile, LinesAnalyzer linesAnalyzer) { + sensorContext.saveMeasure(scalaFile, CoreMetrics.LINES, (double) linesAnalyzer.countLines()); + sensorContext.saveMeasure(scalaFile, CoreMetrics.NCLOC, (double) linesAnalyzer.countLinesOfCode()); } - oldDistribution.add(newDistribution); - return oldDistribution; - } + private void addCommentMetrics(SensorContext sensorContext, ScalaRealFile scalaFile, + CommentsAnalyzer commentsAnalyzer) { + sensorContext.saveMeasure(scalaFile, CoreMetrics.COMMENT_LINES, + (double) commentsAnalyzer.countCommentLines()); + sensorContext.saveMeasure(scalaFile, CoreMetrics.COMMENTED_OUT_CODE_LINES, + (double) commentsAnalyzer.countCommentedOutLinesOfCode()); + } + + private void addCodeMetrics(SensorContext sensorContext, ScalaRealFile scalaFile, String source) { + sensorContext.saveMeasure(scalaFile, CoreMetrics.CLASSES, + (double) TypeCounter.countTypes(source)); + sensorContext.saveMeasure(scalaFile, CoreMetrics.STATEMENTS, + (double) StatementCounter.countStatements(source)); + sensorContext.saveMeasure(scalaFile, CoreMetrics.FUNCTIONS, + (double) FunctionCounter.countFunctions(source)); + sensorContext.saveMeasure(scalaFile, CoreMetrics.COMPLEXITY, + (double) ComplexityCalculator.measureComplexity(source)); + } - private void computePackagesMetric(SensorContext sensorContext, Set packages) { - for (ScalaPackage currentPackage : packages) { - sensorContext.saveMeasure(currentPackage, CoreMetrics.PACKAGES, 1.0); + private void addPublicApiMetrics(SensorContext sensorContext, ScalaRealFile scalaFile, String source) { + sensorContext.saveMeasure(scalaFile, CoreMetrics.PUBLIC_API, + (double) PublicApiCounter.countPublicApi(source)); + sensorContext.saveMeasure(scalaFile, CoreMetrics.PUBLIC_UNDOCUMENTED_API, + (double) PublicApiCounter.countUndocumentedPublicApi(source)); } - } - @Override - public String toString() { - return getClass().getSimpleName(); - } + private MetricDistribution sumUpMetricDistributions(MetricDistribution oldDistribution, + MetricDistribution newDistribution) { + if (oldDistribution == null) { + return newDistribution; + } + + oldDistribution.add(newDistribution); + return oldDistribution; + } + + private void computePackagesMetric(SensorContext sensorContext, Set directories) { + for (Directory directory : directories) { + sensorContext.saveMeasure(directory, CoreMetrics.PACKAGES, 1.0); + } + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } } \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensor.java b/src/main/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensor.java index 6fd15a4..52d8e00 100644 --- a/src/main/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensor.java +++ b/src/main/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensor.java @@ -19,10 +19,8 @@ */ package com.buransky.plugins.scala.sensor; -import java.io.IOException; - import com.buransky.plugins.scala.language.Scala; -import com.buransky.plugins.scala.language.ScalaFile; +import com.buransky.plugins.scala.language.ScalaRealFile; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +31,8 @@ import org.sonar.api.resources.Project; import org.sonar.api.resources.ProjectFileSystem; +import java.io.IOException; + /** * This Sensor imports all Scala files into Sonar. * @@ -42,48 +42,50 @@ @Phase(name = Name.PRE) public class ScalaSourceImporterSensor extends AbstractScalaSensor { - private static final Logger LOGGER = LoggerFactory.getLogger(ScalaSourceImporterSensor.class); + private static final Logger LOGGER = LoggerFactory.getLogger(ScalaSourceImporterSensor.class); + + public ScalaSourceImporterSensor(Scala scala) { + super(scala); + } - public ScalaSourceImporterSensor(Scala scala) { - super(scala); - } + public void analyse(Project project, SensorContext sensorContext) { + ProjectFileSystem fileSystem = project.getFileSystem(); + String charset = fileSystem.getSourceCharset().toString(); - public void analyse(Project project, SensorContext sensorContext) { - ProjectFileSystem fileSystem = project.getFileSystem(); - String charset = fileSystem.getSourceCharset().toString(); + for (InputFile sourceFile : fileSystem.mainFiles(getScala().getKey())) { + addFileToSonar(sensorContext, sourceFile, false, charset); + } - for (InputFile sourceFile : fileSystem.mainFiles(getScala().getKey())) { - addFileToSonar(sensorContext, sourceFile, false, charset); + for (InputFile testFile : fileSystem.testFiles(getScala().getKey())) { + addFileToSonar(sensorContext, testFile, true, charset); + } } - for (InputFile testFile : fileSystem.testFiles(getScala().getKey())) { - addFileToSonar(sensorContext, testFile, true, charset); - } - } + private void addFileToSonar(SensorContext sensorContext, InputFile inputFile, + boolean isUnitTest, String charset) { + try { + String source = FileUtils.readFileToString(inputFile.getFile(), charset); - private void addFileToSonar(SensorContext sensorContext, InputFile inputFile, - boolean isUnitTest, String charset) { - try { - String source = FileUtils.readFileToString(inputFile.getFile(), charset); - ScalaFile resource = ScalaFile.fromInputFile(inputFile, isUnitTest); + //ScalaFile resource = ScalaFile.fromInputFile(inputFile, isUnitTest); + ScalaRealFile resource = ScalaRealFile.fromInputFile(inputFile, isUnitTest); - sensorContext.index(resource); - sensorContext.saveSource(resource, source); + sensorContext.index(resource); + sensorContext.saveSource(resource, source); - if (LOGGER.isDebugEnabled()) { - if (isUnitTest) { - LOGGER.debug("Added Scala test file to Sonar: " + inputFile.getFile().getAbsolutePath()); - } else { - LOGGER.debug("Added Scala source file to Sonar: " + inputFile.getFile().getAbsolutePath()); + if (LOGGER.isDebugEnabled()) { + if (isUnitTest) { + LOGGER.debug("Added Scala test file to Sonar: " + inputFile.getFile().getAbsolutePath()); + } else { + LOGGER.debug("Added Scala source file to Sonar: " + inputFile.getFile().getAbsolutePath()); + } + } + } catch (IOException ioe) { + LOGGER.error("Could not read the file: " + inputFile.getFile().getAbsolutePath(), ioe); } - } - } catch (IOException ioe) { - LOGGER.error("Could not read the file: " + inputFile.getFile().getAbsolutePath(), ioe); } - } - @Override - public String toString() { - return getClass().getSimpleName(); - } + @Override + public String toString() { + return getClass().getSimpleName(); + } } \ No newline at end of file From fa99ed572dfad4eab9190287d8d8b5715c2728c1 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Tue, 4 Feb 2014 11:59:25 -0800 Subject: [PATCH 004/101] . --- .../plugins/scala/ScalaDefaultProfile.java | 42 - .../buransky/plugins/scala/ScalaPlugin.java | 59 +- .../scala/cobertura/CoberturaSensor.java | 16 +- .../scala/cobertura/ScalaCoberturaParser.java | 61 - .../colorization/ScalaColorizerFormat.java | 62 - .../scala/colorization/ScalaKeywords.java | 50 - .../plugins/scala/cpd/ScalaCpdMapping.java | 49 - .../plugins/scala/metrics/LinesAnalyzer.java | 61 - .../scala/sensor/BaseMetricsSensor.java | 160 - .../scala/surefire/SurefireSensor.java | 75 - .../plugins/scala/ScalaPluginTest.java | 50 - .../scala/cobertura/CoberturaSensorTest.java | 71 - .../cobertura/ScalaCoberturaParserTest.java | 92 - .../plugins/scala/cpd/ScalaTokenizerTest.java | 128 - .../plugins/scala/language/CommentTest.java | 109 - .../plugins/scala/language/ScalaFileTest.java | 93 - .../plugins/scala/language/ScalaTest.java | 48 - .../scala/metrics/CommentsAnalyzerTest.java | 96 - .../scala/metrics/LinesAnalyzerTest.java | 95 - .../scala/sensor/AbstractScalaSensorTest.java | 65 - .../scala/sensor/BaseMetricsSensorTest.java | 214 - .../sensor/ScalaSourceImporterSensorTest.java | 219 - .../scala/surefire/SurefireSensorTest.java | 80 - .../plugins/scala/util/DummyScalaFile.java | 38 - .../plugins/scala/util/FileTestUtils.java | 86 - .../baseMetricsSensor/ScalaFile1.scala | 5 - .../baseMetricsSensor/ScalaFile2.scala | 5 - .../baseMetricsSensor/ScalaFile3.scala | 5 - .../resources/cpd/Duplications5Tokens.scala | 6 - src/test/resources/cpd/NewlineToken.scala | 5 - src/test/resources/cpd/NewlinesToken.scala | 6 - src/test/resources/cpd/NoDuplications.scala | 3 - .../resources/cpd/TwoDuplicatedBlocks.scala | 11 - src/test/resources/lexer/DocComment1.txt | 1 - .../lexer/HeaderCommentWithCodeBefore.txt | 8 - .../lexer/HeaderCommentWithWrongStart.txt | 4 - .../lexer/NormalCommentWithHeaderComment.txt | 6 - .../resources/lexer/SimpleHeaderComment.txt | 4 - .../plugins/scala/cobertura/coverage.xml | 6768 ----------------- .../DeepNestedPackageDeclaration.txt | 18 - ...tedPackageDeclarationWithObjectBetween.txt | 22 - .../NestedPackageDeclaration.txt | 9 - ...tedPackageDeclarationWithObjectBetween.txt | 11 - .../SimplePackageDeclaration.txt | 6 - src/test/resources/scalaFile/ScalaFile1.scala | 5 - .../resources/scalaFile/ScalaTestFile1.scala | 5 - .../scalaSourceImporter/JavaMainFile1.java | 5 - .../scalaSourceImporter/JavaTestFile1.java | 5 - .../scalaSourceImporter/MainFile1.scala | 5 - .../scalaSourceImporter/MainFile2.scala | 5 - .../scalaSourceImporter/MainFile3.scala | 5 - .../scalaSourceImporter/TestFile1.scala | 5 - .../scalaSourceImporter/TestFile2.scala | 5 - .../scalaSourceImporter/TestFile3.scala | 5 - .../plugins/scala/compiler/LexerSpec.scala | 87 - .../scala/language/CodeDetectorSpec.scala | 61 - .../scala/language/PackageResolverSpec.scala | 61 - .../metrics/ComplexityCalculatorSpec.scala | 209 - .../scala/metrics/FunctionCounterSpec.scala | 122 - .../scala/metrics/PublicApiCounterSpec.scala | 150 - .../scala/metrics/StatementCounterSpec.scala | 174 - .../scala/metrics/TypeCounterSpec.scala | 165 - .../scala/util/MetricDistributionSpec.scala | 97 - 63 files changed, 27 insertions(+), 10171 deletions(-) delete mode 100644 src/main/java/com/buransky/plugins/scala/ScalaDefaultProfile.java delete mode 100644 src/main/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParser.java delete mode 100644 src/main/java/com/buransky/plugins/scala/colorization/ScalaColorizerFormat.java delete mode 100644 src/main/java/com/buransky/plugins/scala/colorization/ScalaKeywords.java delete mode 100644 src/main/java/com/buransky/plugins/scala/cpd/ScalaCpdMapping.java delete mode 100644 src/main/java/com/buransky/plugins/scala/metrics/LinesAnalyzer.java delete mode 100644 src/main/java/com/buransky/plugins/scala/sensor/BaseMetricsSensor.java delete mode 100644 src/main/java/com/buransky/plugins/scala/surefire/SurefireSensor.java delete mode 100644 src/test/java/com/buransky/plugins/scala/ScalaPluginTest.java delete mode 100644 src/test/java/com/buransky/plugins/scala/cobertura/CoberturaSensorTest.java delete mode 100644 src/test/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParserTest.java delete mode 100644 src/test/java/com/buransky/plugins/scala/cpd/ScalaTokenizerTest.java delete mode 100644 src/test/java/com/buransky/plugins/scala/language/CommentTest.java delete mode 100644 src/test/java/com/buransky/plugins/scala/language/ScalaFileTest.java delete mode 100644 src/test/java/com/buransky/plugins/scala/language/ScalaTest.java delete mode 100644 src/test/java/com/buransky/plugins/scala/metrics/CommentsAnalyzerTest.java delete mode 100644 src/test/java/com/buransky/plugins/scala/metrics/LinesAnalyzerTest.java delete mode 100644 src/test/java/com/buransky/plugins/scala/sensor/AbstractScalaSensorTest.java delete mode 100644 src/test/java/com/buransky/plugins/scala/sensor/BaseMetricsSensorTest.java delete mode 100644 src/test/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensorTest.java delete mode 100644 src/test/java/com/buransky/plugins/scala/surefire/SurefireSensorTest.java delete mode 100644 src/test/java/com/buransky/plugins/scala/util/DummyScalaFile.java delete mode 100644 src/test/java/com/buransky/plugins/scala/util/FileTestUtils.java delete mode 100644 src/test/resources/baseMetricsSensor/ScalaFile1.scala delete mode 100644 src/test/resources/baseMetricsSensor/ScalaFile2.scala delete mode 100644 src/test/resources/baseMetricsSensor/ScalaFile3.scala delete mode 100644 src/test/resources/cpd/Duplications5Tokens.scala delete mode 100644 src/test/resources/cpd/NewlineToken.scala delete mode 100644 src/test/resources/cpd/NewlinesToken.scala delete mode 100644 src/test/resources/cpd/NoDuplications.scala delete mode 100644 src/test/resources/cpd/TwoDuplicatedBlocks.scala delete mode 100644 src/test/resources/lexer/DocComment1.txt delete mode 100644 src/test/resources/lexer/HeaderCommentWithCodeBefore.txt delete mode 100644 src/test/resources/lexer/HeaderCommentWithWrongStart.txt delete mode 100644 src/test/resources/lexer/NormalCommentWithHeaderComment.txt delete mode 100644 src/test/resources/lexer/SimpleHeaderComment.txt delete mode 100644 src/test/resources/org/sonar/plugins/scala/cobertura/coverage.xml delete mode 100644 src/test/resources/packageResolver/DeepNestedPackageDeclaration.txt delete mode 100644 src/test/resources/packageResolver/DeepNestedPackageDeclarationWithObjectBetween.txt delete mode 100644 src/test/resources/packageResolver/NestedPackageDeclaration.txt delete mode 100644 src/test/resources/packageResolver/NestedPackageDeclarationWithObjectBetween.txt delete mode 100644 src/test/resources/packageResolver/SimplePackageDeclaration.txt delete mode 100644 src/test/resources/scalaFile/ScalaFile1.scala delete mode 100644 src/test/resources/scalaFile/ScalaTestFile1.scala delete mode 100644 src/test/resources/scalaSourceImporter/JavaMainFile1.java delete mode 100644 src/test/resources/scalaSourceImporter/JavaTestFile1.java delete mode 100644 src/test/resources/scalaSourceImporter/MainFile1.scala delete mode 100644 src/test/resources/scalaSourceImporter/MainFile2.scala delete mode 100644 src/test/resources/scalaSourceImporter/MainFile3.scala delete mode 100644 src/test/resources/scalaSourceImporter/TestFile1.scala delete mode 100644 src/test/resources/scalaSourceImporter/TestFile2.scala delete mode 100644 src/test/resources/scalaSourceImporter/TestFile3.scala delete mode 100644 src/test/scala/com/buransky/plugins/scala/compiler/LexerSpec.scala delete mode 100644 src/test/scala/com/buransky/plugins/scala/language/CodeDetectorSpec.scala delete mode 100644 src/test/scala/com/buransky/plugins/scala/language/PackageResolverSpec.scala delete mode 100644 src/test/scala/com/buransky/plugins/scala/metrics/ComplexityCalculatorSpec.scala delete mode 100644 src/test/scala/com/buransky/plugins/scala/metrics/FunctionCounterSpec.scala delete mode 100644 src/test/scala/com/buransky/plugins/scala/metrics/PublicApiCounterSpec.scala delete mode 100644 src/test/scala/com/buransky/plugins/scala/metrics/StatementCounterSpec.scala delete mode 100644 src/test/scala/com/buransky/plugins/scala/metrics/TypeCounterSpec.scala delete mode 100644 src/test/scala/com/buransky/plugins/scala/util/MetricDistributionSpec.scala diff --git a/src/main/java/com/buransky/plugins/scala/ScalaDefaultProfile.java b/src/main/java/com/buransky/plugins/scala/ScalaDefaultProfile.java deleted file mode 100644 index c65f821..0000000 --- a/src/main/java/com/buransky/plugins/scala/ScalaDefaultProfile.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala; - -import com.buransky.plugins.scala.language.Scala; -import org.sonar.api.profiles.AnnotationProfileParser; -import org.sonar.api.profiles.ProfileDefinition; -import org.sonar.api.profiles.RulesProfile; -import org.sonar.api.utils.ValidationMessages; - -import java.util.Collections; - -public class ScalaDefaultProfile extends ProfileDefinition { - - private final AnnotationProfileParser annotationProfileParser; - - public ScalaDefaultProfile(AnnotationProfileParser annotationProfileParser) { - this.annotationProfileParser = annotationProfileParser; - } - - @Override - public RulesProfile createProfile(ValidationMessages messages) { - return annotationProfileParser.parse(Scala.INSTANCE.getKey(), "sonar", Scala.INSTANCE.getKey(), Collections.EMPTY_LIST, messages); - } -} diff --git a/src/main/java/com/buransky/plugins/scala/ScalaPlugin.java b/src/main/java/com/buransky/plugins/scala/ScalaPlugin.java index d80000c..13d558c 100644 --- a/src/main/java/com/buransky/plugins/scala/ScalaPlugin.java +++ b/src/main/java/com/buransky/plugins/scala/ScalaPlugin.java @@ -19,17 +19,14 @@ */ package com.buransky.plugins.scala; -import java.util.ArrayList; -import java.util.List; - import com.buransky.plugins.scala.cobertura.CoberturaSensor; import com.buransky.plugins.scala.language.Scala; +import com.buransky.plugins.scala.sensor.ScalaSourceImporterSensor; import org.sonar.api.Extension; import org.sonar.api.SonarPlugin; -import com.buransky.plugins.scala.colorization.ScalaColorizerFormat; -import com.buransky.plugins.scala.sensor.BaseMetricsSensor; -import com.buransky.plugins.scala.sensor.ScalaSourceImporterSensor; -import com.buransky.plugins.scala.surefire.SurefireSensor; + +import java.util.ArrayList; +import java.util.List; /** * This class is the entry point for all extensions made by the @@ -40,34 +37,30 @@ */ public class ScalaPlugin extends SonarPlugin { - public List> getExtensions() { - final List> extensions = new ArrayList>(); - extensions.add(Scala.class); - extensions.add(ScalaSourceImporterSensor.class); - extensions.add(ScalaColorizerFormat.class); - extensions.add(BaseMetricsSensor.class); - extensions.add(ScalaDefaultProfile.class); - extensions.add(CoberturaSensor.class); - extensions.add(SurefireSensor.class); + public List> getExtensions() { + final List> extensions = new ArrayList>(); + extensions.add(Scala.class); + extensions.add(ScalaSourceImporterSensor.class); + extensions.add(CoberturaSensor.class); - return extensions; - } + return extensions; + } - @Override - public String toString() { - return getClass().getSimpleName(); - } + @Override + public String toString() { + return getClass().getSimpleName(); + } - public static String getPathToScalaLibrary() { - return getPathByResource("scala/package.class"); - } + public static String getPathToScalaLibrary() { + return getPathByResource("scala/package.class"); + } - /** - * Godin: during execution of Sonar Batch all dependencies of a plugin are downloaded and - * available locally as JAR-files, so we can use this kind of hack to locate JARs. - */ - private static String getPathByResource(String name) { - String path = ScalaPlugin.class.getClassLoader().getResource(name).getPath(); - return path.substring("file:".length(), path.lastIndexOf('!')); - } + /** + * Godin: during execution of Sonar Batch all dependencies of a plugin are downloaded and + * available locally as JAR-files, so we can use this kind of hack to locate JARs. + */ + private static String getPathByResource(String name) { + String path = ScalaPlugin.class.getClassLoader().getResource(name).getPath(); + return path.substring("file:".length(), path.lastIndexOf('!')); + } } diff --git a/src/main/java/com/buransky/plugins/scala/cobertura/CoberturaSensor.java b/src/main/java/com/buransky/plugins/scala/cobertura/CoberturaSensor.java index 7bf2e18..1b72d48 100644 --- a/src/main/java/com/buransky/plugins/scala/cobertura/CoberturaSensor.java +++ b/src/main/java/com/buransky/plugins/scala/cobertura/CoberturaSensor.java @@ -33,34 +33,20 @@ import org.sonar.api.resources.InputFile; import org.sonar.api.resources.Project; import org.sonar.api.resources.ProjectFileSystem; -import org.sonar.plugins.cobertura.api.AbstractCoberturaParser; -import org.sonar.plugins.cobertura.api.CoberturaUtils; -import java.io.File; import java.util.HashMap; import java.util.Map; public class CoberturaSensor implements Sensor, CoverageExtension { private static final Logger LOG = LoggerFactory.getLogger(CoberturaSensor.class); - private static final AbstractCoberturaParser COBERTURA_PARSER = new ScalaCoberturaParser(); public boolean shouldExecuteOnProject(Project project) { return project.getAnalysisType().isDynamic(true) && Scala.INSTANCE.getKey().equals(project.getLanguageKey()); } public void analyse(Project project, SensorContext context) { - File report = CoberturaUtils.getReport(project); - if (report != null) { - //parseReport(report, context); - parseFakeReport(project, context); - } - } - - protected void parseReport(File xmlFile, final SensorContext context) { - LOG.info("parsing {}", xmlFile); - - COBERTURA_PARSER.parseReport(xmlFile, context); + parseFakeReport(project, context); } @Override diff --git a/src/main/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParser.java b/src/main/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParser.java deleted file mode 100644 index 143a358..0000000 --- a/src/main/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParser.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.cobertura; - -import com.buransky.plugins.scala.language.ScalaRealFile; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonar.plugins.cobertura.api.AbstractCoberturaParser; -import org.sonar.api.resources.Resource; -import com.buransky.plugins.scala.language.ScalaFile; - -import java.io.File; - -public class ScalaCoberturaParser extends AbstractCoberturaParser { - private static final Logger LOG = LoggerFactory.getLogger(ScalaCoberturaParser.class); - - @Override - protected Resource getResource(String fileName) { -// // TODO update the sbt scct plugin to provide the correct fully qualified class name. -// if (fileName.startsWith("src.main.scala.")) -// fileName = fileName.replace("src.main.scala.", ""); -// else if (fileName.startsWith("app.")) -// fileName = fileName.replace("app.", ""); -// -// int packageTerminator = fileName.lastIndexOf('.'); -// -// if (packageTerminator < 0 ) { -// return new ScalaFile(null, fileName, false); -// } -// else { -// String packageName = fileName.substring(0, packageTerminator); -// String className = fileName.substring(packageTerminator + 1, fileName.length()); -// -// return new ScalaFile(packageName, className, false); -// } - - File f = new File(fileName); - Resource result = new ScalaRealFile(f.getParent(), f.getName(), false); - - LOG.info("[Scoverage] getResource [" + result.getKey() + "]"); - - return result; - } -} diff --git a/src/main/java/com/buransky/plugins/scala/colorization/ScalaColorizerFormat.java b/src/main/java/com/buransky/plugins/scala/colorization/ScalaColorizerFormat.java deleted file mode 100644 index 8c35cea..0000000 --- a/src/main/java/com/buransky/plugins/scala/colorization/ScalaColorizerFormat.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.colorization; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import com.buransky.plugins.scala.language.Scala; -import org.sonar.api.web.CodeColorizerFormat; -import org.sonar.colorizer.CDocTokenizer; -import org.sonar.colorizer.CppDocTokenizer; -import org.sonar.colorizer.JavaAnnotationTokenizer; -import org.sonar.colorizer.JavadocTokenizer; -import org.sonar.colorizer.KeywordsTokenizer; -import org.sonar.colorizer.LiteralTokenizer; -import org.sonar.colorizer.Tokenizer; - -/** - * This class extends Sonar for code colorization of Scala source. - * - * @author Felix Müller - * @since 0.1 - */ -public class ScalaColorizerFormat extends CodeColorizerFormat { - - private static final String END_SPAN_TAG = ""; - - private static final List TOKENIZERS = Arrays.asList( - new LiteralTokenizer("", END_SPAN_TAG), - new KeywordsTokenizer("", END_SPAN_TAG, ScalaKeywords.getAllKeywords()), - new CDocTokenizer("", END_SPAN_TAG), - new CppDocTokenizer("", END_SPAN_TAG), - new JavadocTokenizer("", END_SPAN_TAG), - new JavaAnnotationTokenizer("", END_SPAN_TAG)); - - public ScalaColorizerFormat() { - super(Scala.INSTANCE.getKey()); - } - - @Override - public List getTokenizers() { - return Collections.unmodifiableList(TOKENIZERS); - } -} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/colorization/ScalaKeywords.java b/src/main/java/com/buransky/plugins/scala/colorization/ScalaKeywords.java deleted file mode 100644 index c1debd8..0000000 --- a/src/main/java/com/buransky/plugins/scala/colorization/ScalaKeywords.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.colorization; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - * This is a helper class for collecting every Scala keyword. - * - * @author Felix Müller - * @since 0.1 - */ -public final class ScalaKeywords { - - private static final Set KEYWORDS = new HashSet(Arrays.asList( - "abstract", "assert", "case", "catch", "class", "def", "do", "else", "extends", "false", - "final", "finally", "for", "forSome", "if", "implicit", "import", "lazy", "match", "new", - "null", "object", "override", "package", "private", "protected", "requires", "return", - "sealed", "super", "this", "throw", "trait", "true", "try", "type", "val", "var", "while", - "with", "yield", "_", ":", "=", "=>", "<-", "<:", "<%", ">:", "#", "@" - )); - - private ScalaKeywords() { - // to prevent instantiation - } - - public static Set getAllKeywords() { - return Collections.unmodifiableSet(KEYWORDS); - } -} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/cpd/ScalaCpdMapping.java b/src/main/java/com/buransky/plugins/scala/cpd/ScalaCpdMapping.java deleted file mode 100644 index bc2ebd7..0000000 --- a/src/main/java/com/buransky/plugins/scala/cpd/ScalaCpdMapping.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.cpd; - -import com.buransky.plugins.scala.language.Scala; -import net.sourceforge.pmd.cpd.Tokenizer; - -import org.sonar.api.batch.AbstractCpdMapping; -import org.sonar.api.resources.Language; - -/** - * Glue Sonar and PMD CPD together. - * - * @since 0.1 - */ -public class ScalaCpdMapping extends AbstractCpdMapping { - - private final Scala scala; - - public ScalaCpdMapping(Scala scala) { - this.scala = scala; - } - - public Tokenizer getTokenizer() { - return new ScalaTokenizer(); - } - - public Language getLanguage() { - return scala; - } - -} diff --git a/src/main/java/com/buransky/plugins/scala/metrics/LinesAnalyzer.java b/src/main/java/com/buransky/plugins/scala/metrics/LinesAnalyzer.java deleted file mode 100644 index a538290..0000000 --- a/src/main/java/com/buransky/plugins/scala/metrics/LinesAnalyzer.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.metrics; - -import java.util.List; - -import org.apache.commons.lang.StringUtils; - -/** - * This class implements the computation of basic - * line metrics for a {@link ScalaFile}. - * - * @author Felix Müller - * @since 0.1 - */ -public class LinesAnalyzer { - - private final List lines; - private final CommentsAnalyzer commentsAnalyzer; - - public LinesAnalyzer(List lines, CommentsAnalyzer commentsAnalyzer) { - this.lines = lines; - this.commentsAnalyzer = commentsAnalyzer; - } - - public int countLines() { - return lines.size(); - } - - public int countLinesOfCode() { - return countLines() - countBlankLines() - commentsAnalyzer.countCommentLines() - - commentsAnalyzer.countHeaderCommentLines() - commentsAnalyzer.countBlankCommentLines(); - } - - private int countBlankLines() { - int numberOfBlankLines = 0; - for (String line : lines) { - if (StringUtils.isBlank(line)) { - numberOfBlankLines++; - } - } - return numberOfBlankLines; - } -} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/sensor/BaseMetricsSensor.java b/src/main/java/com/buransky/plugins/scala/sensor/BaseMetricsSensor.java deleted file mode 100644 index 5006b4d..0000000 --- a/src/main/java/com/buransky/plugins/scala/sensor/BaseMetricsSensor.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.sensor; - -import java.io.IOException; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import com.buransky.plugins.scala.language.*; -import com.buransky.plugins.scala.metrics.CommentsAnalyzer; -import com.buransky.plugins.scala.metrics.LinesAnalyzer; -import com.buransky.plugins.scala.util.StringUtils; -import org.apache.commons.io.FileUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonar.api.batch.SensorContext; -import org.sonar.api.measures.CoreMetrics; -import org.sonar.api.resources.Directory; -import org.sonar.api.resources.InputFile; -import org.sonar.api.resources.Project; -import org.sonar.api.resources.ProjectFileSystem; -import org.sonar.plugins.scala.compiler.Lexer; -import org.sonar.plugins.scala.metrics.ComplexityCalculator; -import org.sonar.plugins.scala.metrics.FunctionCounter; -import org.sonar.plugins.scala.metrics.PublicApiCounter; -import org.sonar.plugins.scala.metrics.StatementCounter; -import org.sonar.plugins.scala.metrics.TypeCounter; -import org.sonar.plugins.scala.util.MetricDistribution; - -/** - * This is the main sensor of the Scala plugin. It gathers all results - * of the computation of base metrics for all Scala resources. - * - * @author Felix Müller - * @since 0.1 - */ -public class BaseMetricsSensor extends AbstractScalaSensor { - - private static final Logger LOGGER = LoggerFactory.getLogger(BaseMetricsSensor.class); - - public BaseMetricsSensor(Scala scala) { - super(scala); - } - - public void analyse(Project project, SensorContext sensorContext) { - final ProjectFileSystem fileSystem = project.getFileSystem(); - final String charset = fileSystem.getSourceCharset().toString(); - final Set directories = new HashSet(); - - MetricDistribution complexityOfClasses = null; - MetricDistribution complexityOfFunctions = null; - - for (InputFile inputFile : fileSystem.mainFiles(getScala().getKey())) { - final ScalaRealFile scalaFile = ScalaRealFile.fromInputFile(inputFile); - directories.add(scalaFile.getParent()); - sensorContext.saveMeasure(scalaFile, CoreMetrics.FILES, 1.0); - - try { - final String source = FileUtils.readFileToString(inputFile.getFile(), charset); - final List lines = StringUtils.convertStringToListOfLines(source); - final List comments = new Lexer().getComments(source); - - final CommentsAnalyzer commentsAnalyzer = new CommentsAnalyzer(comments); - final LinesAnalyzer linesAnalyzer = new LinesAnalyzer(lines, commentsAnalyzer); - - addLineMetrics(sensorContext, scalaFile, linesAnalyzer); - addCommentMetrics(sensorContext, scalaFile, commentsAnalyzer); - addCodeMetrics(sensorContext, scalaFile, source); - addPublicApiMetrics(sensorContext, scalaFile, source); - - complexityOfClasses = sumUpMetricDistributions(complexityOfClasses, - ComplexityCalculator.measureComplexityOfClasses(source)); - - complexityOfFunctions = sumUpMetricDistributions(complexityOfFunctions, - ComplexityCalculator.measureComplexityOfFunctions(source)); - - } catch (IOException ioe) { - LOGGER.error("Could not read the file: " + inputFile.getFile().getAbsolutePath(), ioe); - } - } - - if (complexityOfClasses != null) - sensorContext.saveMeasure(complexityOfClasses.getMeasure()); - - if (complexityOfFunctions != null) - sensorContext.saveMeasure(complexityOfFunctions.getMeasure()); - - computePackagesMetric(sensorContext, directories); - } - - private void addLineMetrics(SensorContext sensorContext, ScalaRealFile scalaFile, LinesAnalyzer linesAnalyzer) { - sensorContext.saveMeasure(scalaFile, CoreMetrics.LINES, (double) linesAnalyzer.countLines()); - sensorContext.saveMeasure(scalaFile, CoreMetrics.NCLOC, (double) linesAnalyzer.countLinesOfCode()); - } - - private void addCommentMetrics(SensorContext sensorContext, ScalaRealFile scalaFile, - CommentsAnalyzer commentsAnalyzer) { - sensorContext.saveMeasure(scalaFile, CoreMetrics.COMMENT_LINES, - (double) commentsAnalyzer.countCommentLines()); - sensorContext.saveMeasure(scalaFile, CoreMetrics.COMMENTED_OUT_CODE_LINES, - (double) commentsAnalyzer.countCommentedOutLinesOfCode()); - } - - private void addCodeMetrics(SensorContext sensorContext, ScalaRealFile scalaFile, String source) { - sensorContext.saveMeasure(scalaFile, CoreMetrics.CLASSES, - (double) TypeCounter.countTypes(source)); - sensorContext.saveMeasure(scalaFile, CoreMetrics.STATEMENTS, - (double) StatementCounter.countStatements(source)); - sensorContext.saveMeasure(scalaFile, CoreMetrics.FUNCTIONS, - (double) FunctionCounter.countFunctions(source)); - sensorContext.saveMeasure(scalaFile, CoreMetrics.COMPLEXITY, - (double) ComplexityCalculator.measureComplexity(source)); - } - - private void addPublicApiMetrics(SensorContext sensorContext, ScalaRealFile scalaFile, String source) { - sensorContext.saveMeasure(scalaFile, CoreMetrics.PUBLIC_API, - (double) PublicApiCounter.countPublicApi(source)); - sensorContext.saveMeasure(scalaFile, CoreMetrics.PUBLIC_UNDOCUMENTED_API, - (double) PublicApiCounter.countUndocumentedPublicApi(source)); - } - - private MetricDistribution sumUpMetricDistributions(MetricDistribution oldDistribution, - MetricDistribution newDistribution) { - if (oldDistribution == null) { - return newDistribution; - } - - oldDistribution.add(newDistribution); - return oldDistribution; - } - - private void computePackagesMetric(SensorContext sensorContext, Set directories) { - for (Directory directory : directories) { - sensorContext.saveMeasure(directory, CoreMetrics.PACKAGES, 1.0); - } - } - - @Override - public String toString() { - return getClass().getSimpleName(); - } -} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/surefire/SurefireSensor.java b/src/main/java/com/buransky/plugins/scala/surefire/SurefireSensor.java deleted file mode 100644 index 4a07474..0000000 --- a/src/main/java/com/buransky/plugins/scala/surefire/SurefireSensor.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.surefire; - -import com.buransky.plugins.scala.language.Scala; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonar.api.batch.CoverageExtension; -import org.sonar.api.batch.DependsUpon; -import org.sonar.api.batch.Sensor; -import org.sonar.api.batch.SensorContext; -import org.sonar.api.resources.Project; -import org.sonar.api.resources.Qualifiers; -import org.sonar.api.resources.Resource; -import org.sonar.plugins.surefire.api.AbstractSurefireParser; -import org.sonar.plugins.surefire.api.SurefireUtils; - -import java.io.File; - -public class SurefireSensor implements Sensor { - - private static final Logger LOG = LoggerFactory.getLogger(SurefireSensor.class); - - @DependsUpon - public Class dependsUponCoverageSensors() { - return CoverageExtension.class; - } - - public boolean shouldExecuteOnProject(Project project) { - return project.getAnalysisType().isDynamic(true) && Scala.INSTANCE.getKey().equals(project.getLanguageKey()); - } - - public void analyse(Project project, SensorContext context) { - File dir = SurefireUtils.getReportsDirectory(project); - collect(project, context, dir); - } - - protected void collect(Project project, SensorContext context, File reportsDir) { - LOG.info("parsing {}", reportsDir); - SUREFIRE_PARSER.collect(project, context, reportsDir); - } - - private static final AbstractSurefireParser SUREFIRE_PARSER = new AbstractSurefireParser() { - @Override - protected Resource getUnitTestResource(String classKey) { - String filename = classKey.replace('.', '/') + ".scala"; - org.sonar.api.resources.File sonarFile = new org.sonar.api.resources.File(filename); - sonarFile.setQualifier(Qualifiers.UNIT_TEST_FILE); - return sonarFile; - } - }; - - @Override - public String toString() { - return "Scala SurefireSensor"; - } - -} diff --git a/src/test/java/com/buransky/plugins/scala/ScalaPluginTest.java b/src/test/java/com/buransky/plugins/scala/ScalaPluginTest.java deleted file mode 100644 index dc0f698..0000000 --- a/src/test/java/com/buransky/plugins/scala/ScalaPluginTest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala; - -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; - -import org.junit.Test; -import com.buransky.plugins.scala.cobertura.CoberturaSensor; -import com.buransky.plugins.scala.surefire.SurefireSensor; - -public class ScalaPluginTest { - - @Test - public void shouldHaveExtensions() { - assertThat(new ScalaPlugin().getExtensions().size(), greaterThan(0)); - } - - @Test - public void shouldHaveCoberturaPlugin() { - assertTrue(new ScalaPlugin().getExtensions().contains(CoberturaSensor.class)); - } - - @Test - public void shouldHaveSurefirePlugin() { - assertTrue(new ScalaPlugin().getExtensions().contains(SurefireSensor.class)); - } - - @Test - public void shouldGetPathToDependencies() { - assertThat(ScalaPlugin.getPathToScalaLibrary(), containsString("scala-library")); - } -} diff --git a/src/test/java/com/buransky/plugins/scala/cobertura/CoberturaSensorTest.java b/src/test/java/com/buransky/plugins/scala/cobertura/CoberturaSensorTest.java deleted file mode 100644 index 61a1d0d..0000000 --- a/src/test/java/com/buransky/plugins/scala/cobertura/CoberturaSensorTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.cobertura; - -import org.junit.Before; -import org.junit.Test; -import org.sonar.api.batch.SensorContext; -import org.sonar.api.resources.Project; -import com.buransky.plugins.scala.language.Scala; -import org.sonar.test.TestUtils; - -import static org.junit.Assert.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class CoberturaSensorTest { - - private CoberturaSensor sensor; - - @Before - public void setUp() throws Exception { - sensor = new CoberturaSensor(); - } - - /** - * See SONARPLUGINS-696 - */ - @Test - public void shouldParseReport() { - SensorContext context = mock(SensorContext.class); - sensor.parseReport(TestUtils.getResource("/org/sonar/plugins/scala/cobertura/coverage.xml"), context); - } - - @Test - public void shouldNotExecuteIfStaticAnalysis() { - Project project = mock(Project.class); - when(project.getLanguageKey()).thenReturn(Scala.INSTANCE.getKey()); - when(project.getAnalysisType()).thenReturn(Project.AnalysisType.STATIC); - assertFalse(sensor.shouldExecuteOnProject(project)); - } - - @Test - public void shouldNotExecuteOnJavaProject() { - Project project = mock(Project.class); - when(project.getLanguageKey()).thenReturn("java"); - when(project.getAnalysisType()).thenReturn(Project.AnalysisType.DYNAMIC); - assertFalse(sensor.shouldExecuteOnProject(project)); - } - - @Test - public void testToString() { - assertEquals(sensor.toString(), "Scala CoberturaSensor"); - } -} diff --git a/src/test/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParserTest.java b/src/test/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParserTest.java deleted file mode 100644 index 83cc7f8..0000000 --- a/src/test/java/com/buransky/plugins/scala/cobertura/ScalaCoberturaParserTest.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.cobertura; - -import org.junit.Before; -import org.junit.Test; -import static org.junit.Assert.*; - -import org.sonar.api.resources.Resource; -import com.buransky.plugins.scala.language.ScalaFile; -import com.buransky.plugins.scala.language.ScalaPackage; - -public class ScalaCoberturaParserTest { - - private ScalaCoberturaParser underTest; - - @Before - public void setUp() { - underTest = new ScalaCoberturaParser(); - } - - @Test - public void shouldCreateScalaFileResourceWhenDeepPackage() { - Resource resource = underTest.getResource("com.mock.scalapackage.MockScalaClass"); - assertTrue(resource instanceof ScalaFile); - - ScalaFile file = (ScalaFile)resource; - assertEquals("MockScalaClass", file.getName()); - - ScalaPackage scalaPackage = file.getParent(); - assertNotNull(scalaPackage); - assertEquals("com.mock.scalapackage", scalaPackage.getName()); - } - - @Test - public void shouldCreateScalaFileResourceWhenRootPackage() { - Resource resource = underTest.getResource("MockScalaClass"); - assertTrue(resource instanceof ScalaFile); - - ScalaFile file = (ScalaFile)resource; - assertEquals("MockScalaClass", file.getName()); - - ScalaPackage scalaPackage = file.getParent(); - assertNotNull(scalaPackage); - assertEquals("[default]", scalaPackage.getName()); - } - - // TODO remove this test once the sbt scct plugin is patched to produce the correct class name. - @Test - public void shouldCreateScalaFileResourceWhenScctBug() { - Resource resource = underTest.getResource("src.main.scala.com.mock.scalapackage.MockScalaClass"); - assertTrue(resource instanceof ScalaFile); - - ScalaFile file = (ScalaFile)resource; - assertEquals("MockScalaClass", file.getName()); - - ScalaPackage scalaPackage = file.getParent(); - assertNotNull(scalaPackage); - assertEquals("com.mock.scalapackage", scalaPackage.getName()); - } - - @Test - public void shouldCreateScalaFileResourceWhenScctBugForPlayApp() { - Resource resource = underTest.getResource("app.com.mock.scalapackage.MockScalaClass"); - assertTrue(resource instanceof ScalaFile); - - ScalaFile file = (ScalaFile)resource; - assertEquals("MockScalaClass", file.getName()); - - ScalaPackage scalaPackage = file.getParent(); - assertNotNull(scalaPackage); - assertEquals("com.mock.scalapackage", scalaPackage.getName()); - } - -} diff --git a/src/test/java/com/buransky/plugins/scala/cpd/ScalaTokenizerTest.java b/src/test/java/com/buransky/plugins/scala/cpd/ScalaTokenizerTest.java deleted file mode 100644 index e881621..0000000 --- a/src/test/java/com/buransky/plugins/scala/cpd/ScalaTokenizerTest.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.cpd; - -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import net.sourceforge.pmd.cpd.AbstractLanguage; -import net.sourceforge.pmd.cpd.TokenEntry; - -import org.apache.commons.io.FileUtils; -import org.junit.Before; -import org.junit.Test; -import org.sonar.duplications.cpd.CPD; -import org.sonar.duplications.cpd.Match; - -public class ScalaTokenizerTest { - - @Before - public void init() { - TokenEntry.clearImages(); - } - - @Test - public void noDuplications() throws IOException { - CPD cpd = getCPD(10); - cpd.add(resourceToFile("/cpd/NoDuplications.scala")); - cpd.go(); - assertThat(getMatches(cpd).size(), is(0)); - } - - @Test - public void noDuplicationsWith6Tokens() throws IOException { - CPD cpd = getCPD(6); - cpd.add(resourceToFile("/cpd/Duplications5Tokens.scala")); - cpd.go(); - assertThat(getMatches(cpd).size(), is(0)); - } - - @Test - public void duplicationWith5Tokens() throws IOException { - CPD cpd = getCPD(5); - cpd.add(resourceToFile("/cpd/Duplications5Tokens.scala")); - cpd.go(); - List matches = getMatches(cpd); - assertThat(matches.size(), is(1)); - assertThat(matches.get(0).getFirstMark().getBeginLine(), is(2)); - assertThat(matches.get(0).getSecondMark().getBeginLine(), is(5)); - } - - @Test - public void newLineTokenEnables5TokenDuplication() throws IOException { - CPD cpd = getCPD(5); - cpd.add(resourceToFile("/cpd/NewlineToken.scala")); - cpd.go(); - List matches = getMatches(cpd); - assertThat(matches.get(0).getFirstMark().getBeginLine(), is(2)); - assertThat(matches.get(0).getSecondMark().getBeginLine(), is(3)); - } - - @Test - public void newLineAndNewLinesTokensNo5TokensDuplication() throws IOException { - CPD cpd = getCPD(5); - cpd.add(resourceToFile("/cpd/NewlinesToken.scala")); - cpd.go(); - assertThat(getMatches(cpd).size(), is(0)); - } - - @Test - public void twoDuplicatedBlocks() throws IOException { - CPD cpd = getCPD(5); - cpd.add(resourceToFile("/cpd/TwoDuplicatedBlocks.scala")); - cpd.go(); - List matches = getMatches(cpd); - assertThat(matches.get(0).getFirstMark().getBeginLine(), is(2)); - assertThat(matches.get(0).getSecondMark().getBeginLine(), is(7)); - assertThat(matches.get(0).getLineCount(), is(4)); - } - - private File resourceToFile(String path) { - return FileUtils.toFile(getClass().getResource(path)); - } - - private CPD getCPD(int minimumTokens) { - AbstractLanguage language = new AbstractLanguage(new ScalaTokenizer(), "scala") { - }; - CPD cpd = new CPD(minimumTokens, language); - cpd.setEncoding(Charset.defaultCharset().name()); - cpd.setLoadSourceCodeSlices(false); - return cpd; - } - - private List getMatches(CPD cpd) { - List matches = new ArrayList(); - - Iterator iterator = cpd.getMatches(); - while (iterator.hasNext()) { - matches.add(iterator.next()); - } - - return matches; - } - -} diff --git a/src/test/java/com/buransky/plugins/scala/language/CommentTest.java b/src/test/java/com/buransky/plugins/scala/language/CommentTest.java deleted file mode 100644 index 7f81fed..0000000 --- a/src/test/java/com/buransky/plugins/scala/language/CommentTest.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.language; - -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; - -import java.io.IOException; - -import org.junit.Test; - -public class CommentTest { - - @Test - public void shouldCountOneNumberOfCommentLine() throws IOException { - Comment comment = new Comment("// This is a comment", CommentType.NORMAL); - assertThat(comment.getNumberOfLines(), is(1)); - } - - @Test - public void shouldCountAllNumberOfCommentLines() throws IOException { - Comment comment = new Comment("/* This is the first comment line\r\n" - + "* second line\r\n" - + "* and this the third and last line */", - CommentType.NORMAL); - assertThat(comment.getNumberOfLines(), is(3)); - } - - @Test - public void shouldCountZeorCommentLinesIfCommentIsEmpty() throws IOException { - Comment comment = new Comment("", CommentType.NORMAL); - assertThat(comment.getNumberOfLines(), is(0)); - } - - @Test - public void shouldCountOneCommentedOutLineOfCode() throws IOException { - Comment comment = new Comment("// val a = 1", CommentType.NORMAL); - assertThat(comment.getNumberOfCommentedOutLinesOfCode(), is(1)); - } - - @Test - public void shouldCountAllCommentedOutLinesOfCode() throws IOException { - Comment comment = new Comment("/* object Hello {\r\n" - + "* val b = 1 } */", - CommentType.NORMAL); - assertThat(comment.getNumberOfCommentedOutLinesOfCode(), is(2)); - } - - @Test - public void shouldCountZeorCommentedOutLinesOfCodeIfCommentIsEmpty() throws IOException { - Comment comment = new Comment("", CommentType.NORMAL); - assertThat(comment.getNumberOfCommentedOutLinesOfCode(), is(0)); - } - - @Test - public void shouldNotCountAnyCommentedOutLinesOfCodeForDocComments() throws IOException { - Comment comment = new Comment("/** This is a doc comment with some code\r\n" - + "* package hello.world\r\n" - + "* class Test { val a = 1 } */", CommentType.DOC); - assertThat(comment.getNumberOfCommentedOutLinesOfCode(), is(0)); - } - - @Test - public void shouldCountAllBlankCommentLines() throws IOException { - Comment comment = new Comment("/*\r\n" - + "* this is a multi line comment with some blank lines\r\n" - + "* \t \t \r\n" - + "*/", CommentType.NORMAL); - assertThat(comment.getNumberOfBlankLines(), is(3)); - } - - @Test - public void shouldBeNormalComment() throws IOException { - Comment comment = new Comment("", CommentType.NORMAL); - assertThat(comment.isDocComment(), is(false)); - assertThat(comment.isHeaderComment(), is(false)); - } - - @Test - public void shouldBeDocComment() throws IOException { - Comment comment = new Comment("", CommentType.DOC); - assertThat(comment.isDocComment(), is(true)); - assertThat(comment.isHeaderComment(), is(false)); - } - - @Test - public void shouldBeHeaderComment() throws IOException { - Comment comment = new Comment("", CommentType.HEADER); - assertThat(comment.isDocComment(), is(false)); - assertThat(comment.isHeaderComment(), is(true)); - } -} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/language/ScalaFileTest.java b/src/test/java/com/buransky/plugins/scala/language/ScalaFileTest.java deleted file mode 100644 index c4c4c2f..0000000 --- a/src/test/java/com/buransky/plugins/scala/language/ScalaFileTest.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.language; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.io.File; - -import com.buransky.plugins.scala.util.FileTestUtils; -import org.junit.Test; -import org.sonar.api.resources.InputFile; -import org.sonar.api.resources.Qualifiers; - -public class ScalaFileTest { - - @Test - public void shouldHaveFileQualifierForSourceFile() { - assertThat(new ScalaFile("package", "Class", false).getQualifier(), - equalTo(Qualifiers.FILE)); - } - - @Test - public void shouldHaveTestFileQualifierForTestFile() { - assertThat(new ScalaFile("package", "Class", true).getQualifier(), - equalTo(Qualifiers.UNIT_TEST_FILE)); - } - - @Test - public void shouldCreateScalaFileWithCorrectAttributes() { - InputFile inputFile = FileTestUtils.getInputFiles("/scalaFile/", "ScalaFile", 1).get(0); - ScalaFile scalaFile = ScalaFile.fromInputFile(inputFile); - - assertThat(scalaFile.getLanguage().getKey(), is(Scala.INSTANCE.getKey())); - assertThat(scalaFile.getName(), is("ScalaFile1")); - assertThat(scalaFile.getLongName(), is("scalaFile.ScalaFile1")); - assertThat(scalaFile.getParent().getName(), is("scalaFile")); - assertThat(scalaFile.isUnitTest(), is(false)); - } - - @Test - public void shouldCreateScalaTestFileWithCorrectAttributes() { - InputFile inputFile = FileTestUtils.getInputFiles("/scalaFile/", "ScalaTestFile", 1).get(0); - ScalaFile scalaFile = ScalaFile.fromInputFile(inputFile, true); - - assertThat(scalaFile.getLanguage().getKey(), is(Scala.INSTANCE.getKey())); - assertThat(scalaFile.getName(), is("ScalaTestFile1")); - assertThat(scalaFile.getLongName(), is("scalaFile.ScalaTestFile1")); - assertThat(scalaFile.getParent().getName(), is("scalaFile")); - assertThat(scalaFile.isUnitTest(), is(true)); - } - - @Test - public void shouldNotCreateScalaFileIfInputFileIsNull() { - assertNull(ScalaFile.fromInputFile(null)); - } - - @Test - public void shouldNotCreateScalaFileIfFileIsNull() { - InputFile inputFile = mock(InputFile.class); - when(inputFile.getFile()).thenReturn(null); - assertNull(ScalaFile.fromInputFile(inputFile)); - } - - @Test - public void shouldNotCreateScalaFileIfRelativePathIsNull() { - InputFile inputFile = mock(InputFile.class); - when(inputFile.getFile()).thenReturn(new File("")); - when(inputFile.getRelativePath()).thenReturn(null); - assertNull(ScalaFile.fromInputFile(inputFile)); - } -} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/language/ScalaTest.java b/src/test/java/com/buransky/plugins/scala/language/ScalaTest.java deleted file mode 100644 index 5b7b52f..0000000 --- a/src/test/java/com/buransky/plugins/scala/language/ScalaTest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.language; - -import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertThat; - -import org.junit.Test; - -public class ScalaTest { - - @Test - public void shouldHaveScalaLanguageKey() { - assertThat(new Scala().getKey(), equalTo("scala")); - assertThat(Scala.INSTANCE.getKey(), equalTo("scala")); - } - - @Test - public void shouldHaveScalaLanguageName() { - assertThat(new Scala().getName(), equalTo("Scala")); - assertThat(Scala.INSTANCE.getName(), equalTo("Scala")); - } - - @Test - public void shouldHaveScalaFileSuffixes() { - String[] suffixes = new String[] { "scala" }; - assertArrayEquals(new Scala().getFileSuffixes(), suffixes); - assertArrayEquals(Scala.INSTANCE.getFileSuffixes(), suffixes); - } -} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/metrics/CommentsAnalyzerTest.java b/src/test/java/com/buransky/plugins/scala/metrics/CommentsAnalyzerTest.java deleted file mode 100644 index 9dc7366..0000000 --- a/src/test/java/com/buransky/plugins/scala/metrics/CommentsAnalyzerTest.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.metrics; - -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.junit.Test; -import com.buransky.plugins.scala.language.Comment; -import com.buransky.plugins.scala.language.CommentType; - -import scala.actors.threadpool.Arrays; - -public class CommentsAnalyzerTest { - - @Test - public void shouldCountAllCommentLines() throws IOException { - List comments = Arrays.asList(new String[] { - "// this a normal comment", - "/* this is a normal multiline coment\r\n* last line of this comment */", - "// also a normal comment" - }); - CommentsAnalyzer commentAnalyzer = new CommentsAnalyzer(asCommentList(comments, CommentType.NORMAL)); - assertThat(commentAnalyzer.countCommentLines(), is(4)); - } - - @Test - public void shouldCountAllHeaderCommentLines() throws IOException { - List comments = Arrays.asList(new String[] { - "/* this is an one line header comment */", - "/* this is a normal multiline header coment\r\n* last line of this comment */", - "/* also a normal header comment */" - }); - CommentsAnalyzer commentAnalyzer = new CommentsAnalyzer(asCommentList(comments, CommentType.HEADER)); - assertThat(commentAnalyzer.countHeaderCommentLines(), is(4)); - } - - @Test - public void shouldCountAllCommentedOutLinesOfCode() throws IOException { - List comments = Arrays.asList(new String[] { - "// val a = 12", - "/* list.foreach(println(_))\r\n* def inc(x: Int) = x + 1 */", - "// this a normal comment" - }); - CommentsAnalyzer commentAnalyzer = new CommentsAnalyzer(asCommentList(comments, CommentType.NORMAL)); - assertThat(commentAnalyzer.countCommentedOutLinesOfCode(), is(3)); - } - - @Test - public void shouldCountZeroCommentLinesForEmptyCommentsList() { - CommentsAnalyzer commentAnalyzer = new CommentsAnalyzer(Collections.emptyList()); - assertThat(commentAnalyzer.countCommentLines(), is(0)); - } - - @Test - public void shouldCountZeroHeaderCommentLinesForEmptyCommentsList() { - CommentsAnalyzer commentAnalyzer = new CommentsAnalyzer(Collections.emptyList()); - assertThat(commentAnalyzer.countHeaderCommentLines(), is(0)); - } - - @Test - public void shouldCountZeroCommentedOutLinesOfCodeForEmptyCommentsList() { - CommentsAnalyzer commentAnalyzer = new CommentsAnalyzer(Collections.emptyList()); - assertThat(commentAnalyzer.countCommentedOutLinesOfCode(), is(0)); - } - - private List asCommentList(List commentsContent, CommentType type) throws IOException { - List comments = new ArrayList(); - for (String comment : commentsContent) { - comments.add(new Comment(comment, type)); - } - return comments; - } -} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/metrics/LinesAnalyzerTest.java b/src/test/java/com/buransky/plugins/scala/metrics/LinesAnalyzerTest.java deleted file mode 100644 index 7cd2f9c..0000000 --- a/src/test/java/com/buransky/plugins/scala/metrics/LinesAnalyzerTest.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.metrics; - -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; - -import java.io.IOException; -import java.util.List; - -import org.junit.Test; -import org.sonar.plugins.scala.compiler.Lexer; -import com.buransky.plugins.scala.language.Comment; -import com.buransky.plugins.scala.util.StringUtils; - -public class LinesAnalyzerTest { - - @Test - public void shouldCountOneLine() throws IOException { - LinesAnalyzer linesAnalyzer = getLinesAnalyzer("val i = 0"); - assertThat(linesAnalyzer.countLines(), is(1)); - } - - @Test - public void shouldCountAllLines() throws IOException { - LinesAnalyzer linesAnalyzer = getLinesAnalyzer("val i = 0\r\n" - + "println(\"Hallo\")\r\n" - + "\r\n" - + "i = 2"); - assertThat(linesAnalyzer.countLines(), is(4)); - } - - @Test - public void shouldGiveZeroLinesForEmptySource() throws IOException { - LinesAnalyzer linesAnalyzer = getLinesAnalyzer(""); - assertThat(linesAnalyzer.countLines(), is(0)); - } - - @Test - public void shouldCountOneLineOfCode() throws IOException { - LinesAnalyzer linesAnalyzer = getLinesAnalyzer("val i = 0"); - assertThat(linesAnalyzer.countLinesOfCode(), is(1)); - } - - @Test - public void shouldNotCountBlankLinesAsLinesOfCode() throws IOException { - LinesAnalyzer linesAnalyzer = getLinesAnalyzer("val i = 0\r\n" + - "\r\n" + - " \t \r\n" + - "val b = 2"); - assertThat(linesAnalyzer.countLinesOfCode(), is(2)); - } - - @Test - public void shouldNotCountCommentLinesAsLinesOfCode() throws IOException { - LinesAnalyzer linesAnalyzer = getLinesAnalyzer("val i = 0\r\n" + - "// this is comment...\r\n" + - "// test\r\n" + - "val b = 2"); - assertThat(linesAnalyzer.countLinesOfCode(), is(2)); - } - - @Test - public void shouldNotCountHeaderCommentLinesAsLinesOfCode() throws IOException { - LinesAnalyzer linesAnalyzer = getLinesAnalyzer("/**\r\n" + - "* this is a header comment...\r\n" + - "*/\r\n" + - "val b = 2"); - assertThat(linesAnalyzer.countLinesOfCode(), is(1)); - } - - private LinesAnalyzer getLinesAnalyzer(String source) throws IOException { - List lines = StringUtils.convertStringToListOfLines(source); - List comments = new Lexer().getComments(source); - CommentsAnalyzer commentsAnalyzer = new CommentsAnalyzer(comments); - return new LinesAnalyzer(lines, commentsAnalyzer); - } -} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/sensor/AbstractScalaSensorTest.java b/src/test/java/com/buransky/plugins/scala/sensor/AbstractScalaSensorTest.java deleted file mode 100644 index 46756be..0000000 --- a/src/test/java/com/buransky/plugins/scala/sensor/AbstractScalaSensorTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.sensor; - -import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import org.junit.Before; -import org.junit.Test; -import org.sonar.api.batch.SensorContext; -import org.sonar.api.resources.Java; -import org.sonar.api.resources.Project; -import com.buransky.plugins.scala.language.Scala; - -public class AbstractScalaSensorTest { - - private AbstractScalaSensor abstractScalaSensor; - - @Before - public void setUp() { - abstractScalaSensor = new AbstractScalaSensor(Scala.INSTANCE) { - - public void analyse(Project project, SensorContext context) { - // dummy implementation, never called in this test - } - }; - } - - @Test - public void shouldOnlyExecuteOnScalaProjects() { - Project scalaProject = mock(Project.class); - when(scalaProject.getLanguage()).thenReturn(Scala.INSTANCE); - Project javaProject = mock(Project.class); - when(javaProject.getLanguage()).thenReturn(Java.INSTANCE); - - assertTrue(abstractScalaSensor.shouldExecuteOnProject(scalaProject)); - assertFalse(abstractScalaSensor.shouldExecuteOnProject(javaProject)); - } - - @Test - public void shouldHaveScalaAsLanguage() { - assertThat(abstractScalaSensor.getScala(), equalTo(new Scala())); - } -} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/sensor/BaseMetricsSensorTest.java b/src/test/java/com/buransky/plugins/scala/sensor/BaseMetricsSensorTest.java deleted file mode 100644 index 3e9edd1..0000000 --- a/src/test/java/com/buransky/plugins/scala/sensor/BaseMetricsSensorTest.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.sensor; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.nio.charset.Charset; - -import com.buransky.plugins.scala.util.FileTestUtils; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Matchers; -import org.sonar.api.batch.SensorContext; -import org.sonar.api.measures.CoreMetrics; -import org.sonar.api.measures.Measure; -import org.sonar.api.measures.Metric; -import org.sonar.api.resources.Project; -import org.sonar.api.resources.ProjectFileSystem; -import com.buransky.plugins.scala.language.Scala; -import com.buransky.plugins.scala.language.ScalaPackage; - -public class BaseMetricsSensorTest { - - private static final int NUMBER_OF_FILES = 3; - - private BaseMetricsSensor baseMetricsSensor; - - private ProjectFileSystem fileSystem; - private Project project; - private SensorContext sensorContext; - - @Before - public void setUp() { - baseMetricsSensor = new BaseMetricsSensor(Scala.INSTANCE); - - fileSystem = mock(ProjectFileSystem.class); - when(fileSystem.getSourceCharset()).thenReturn(Charset.defaultCharset()); - - project = mock(Project.class); - when(project.getFileSystem()).thenReturn(fileSystem); - - sensorContext = mock(SensorContext.class); - } - - @Test - public void shouldIncrementFileMetricForOneScalaFile() { - analyseOneScalaFile(); - verifyMeasuring(CoreMetrics.FILES, 1.0); - } - - @Test - public void shouldIncreaseFileMetricForAllScalaFiles() throws IOException { - analyseAllScalaFiles(); - verifyMeasuring(CoreMetrics.FILES, NUMBER_OF_FILES, 1.0); - } - - @Test - public void shouldMeasureNothingWhenNoFiles() { - analyseScalaFiles(0); - verifyNoMoreInteractions(sensorContext); - } - - @Test - public void shouldIncrementPackageMetricForOneScalaFile() { - analyseOneScalaFile(); - verify(sensorContext).saveMeasure(any(ScalaPackage.class), eq(CoreMetrics.PACKAGES), eq(1.0)); - } - - @Test - public void shouldIncreasePackageMetricForAllScalaFiles() { - analyseAllScalaFiles(); - verify(sensorContext, times(2)).saveMeasure(any(ScalaPackage.class), eq(CoreMetrics.PACKAGES), eq(1.0)); - } - - @Test - public void shouldMeasureClassComplexityDistributionForOneScalaFileOnlyOnce() { - analyseOneScalaFile(); - verify(sensorContext).saveMeasure(eq(new Measure(CoreMetrics.CLASS_COMPLEXITY_DISTRIBUTION))); - } - - @Test - public void shouldMeasureClassComplexityDistributionForAllScalaFilesOnlyOnce() { - analyseAllScalaFiles(); - verify(sensorContext).saveMeasure(eq(new Measure(CoreMetrics.CLASS_COMPLEXITY_DISTRIBUTION))); - } - - @Test - public void shouldMeasureFunctionComplexityDistributionForOneScalaFileOnlyOnce() { - analyseOneScalaFile(); - verify(sensorContext).saveMeasure(eq(new Measure(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION))); - } - - @Test - public void shouldMeasureFunctionComplexityDistributionForAllScalaFilesOnlyOnce() { - analyseAllScalaFiles(); - verify(sensorContext).saveMeasure(eq(new Measure(CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION))); - } - - @Test - public void shouldMeasureLineMetricsForOneScalaFile() { - analyseOneScalaFile(); - verifyMeasuring(CoreMetrics.LINES); - verifyMeasuring(CoreMetrics.NCLOC); - } - - @Test - public void shouldMeasureLineMetricsForAllScalaFiles() { - analyseAllScalaFiles(); - verifyMeasuring(CoreMetrics.LINES, NUMBER_OF_FILES); - verifyMeasuring(CoreMetrics.NCLOC, NUMBER_OF_FILES); - } - - @Test - public void shouldMeasureCommentMetricsForOneScalaFile() { - analyseOneScalaFile(); - verifyMeasuring(CoreMetrics.COMMENT_LINES); - verifyMeasuring(CoreMetrics.COMMENTED_OUT_CODE_LINES); - } - - @Test - public void shouldMeasureCommentMetricsForAllScalaFiles() { - analyseAllScalaFiles(); - verifyMeasuring(CoreMetrics.COMMENT_LINES, NUMBER_OF_FILES); - verifyMeasuring(CoreMetrics.COMMENTED_OUT_CODE_LINES, NUMBER_OF_FILES); - } - - @Test - public void shouldMeasureCodeMetricsForOneScalaFile() { - analyseOneScalaFile(); - verifyMeasuring(CoreMetrics.CLASSES); - verifyMeasuring(CoreMetrics.STATEMENTS); - verifyMeasuring(CoreMetrics.FUNCTIONS); - verifyMeasuring(CoreMetrics.COMPLEXITY); - } - - @Test - public void shouldMeasureCodeMetricsForAllScalaFiles() { - analyseAllScalaFiles(); - verifyMeasuring(CoreMetrics.CLASSES, NUMBER_OF_FILES); - verifyMeasuring(CoreMetrics.STATEMENTS, NUMBER_OF_FILES); - verifyMeasuring(CoreMetrics.FUNCTIONS, NUMBER_OF_FILES); - verifyMeasuring(CoreMetrics.COMPLEXITY, NUMBER_OF_FILES); - } - - @Test - public void shouldMeasurePublicApiMetricsForOneScalaFile() { - analyseOneScalaFile(); - verifyMeasuring(CoreMetrics.PUBLIC_API); - verifyMeasuring(CoreMetrics.PUBLIC_UNDOCUMENTED_API); - } - - @Test - public void shouldMeasurePublicApiMetricsForAllScalaFiles() { - analyseAllScalaFiles(); - verifyMeasuring(CoreMetrics.PUBLIC_API, NUMBER_OF_FILES); - verifyMeasuring(CoreMetrics.PUBLIC_UNDOCUMENTED_API, NUMBER_OF_FILES); - } - - private void verifyMeasuring(Metric metric) { - verifyMeasuring(metric, 1); - } - - private void verifyMeasuring(Metric metric, int numberOfCalls) { - verify(sensorContext, times(numberOfCalls)).saveMeasure(Matchers.eq(FileTestUtils.SCALA_SOURCE_FILE), - eq(metric), any(Double.class)); - } - - private void verifyMeasuring(Metric metric, double value) { - verifyMeasuring(metric, 1, value); - } - - private void verifyMeasuring(Metric metric, int numberOfCalls, double value) { - verify(sensorContext, times(numberOfCalls)).saveMeasure(eq(FileTestUtils.SCALA_SOURCE_FILE), - eq(metric), eq(value)); - } - - private void analyseOneScalaFile() { - analyseScalaFiles(1); - } - - private void analyseAllScalaFiles() { - analyseScalaFiles(NUMBER_OF_FILES); - } - - private void analyseScalaFiles(int numberOfFiles) { - when(fileSystem.mainFiles(baseMetricsSensor.getScala().getKey())) - .thenReturn(FileTestUtils.getInputFiles("/baseMetricsSensor/", "ScalaFile", numberOfFiles)); - baseMetricsSensor.analyse(project, sensorContext); - } -} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensorTest.java b/src/test/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensorTest.java deleted file mode 100644 index b07a6a7..0000000 --- a/src/test/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensorTest.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.sensor; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.List; - -import org.junit.Before; -import org.junit.Test; -import org.mockito.InOrder; -import org.sonar.api.batch.SensorContext; -import org.sonar.api.resources.InputFile; -import org.sonar.api.resources.Java; -import org.sonar.api.resources.Project; -import org.sonar.api.resources.ProjectFileSystem; -import com.buransky.plugins.scala.language.Scala; -import com.buransky.plugins.scala.util.FileTestUtils; - -public class ScalaSourceImporterSensorTest { - - private ScalaSourceImporterSensor scalaSourceImporter; - - private ProjectFileSystem fileSystem; - private Project project; - private SensorContext sensorContext; - - @Before - public void setUp() { - scalaSourceImporter = new ScalaSourceImporterSensor(Scala.INSTANCE); - - fileSystem = mock(ProjectFileSystem.class); - when(fileSystem.getSourceCharset()).thenReturn(Charset.defaultCharset()); - - project = mock(Project.class); - when(project.getFileSystem()).thenReturn(fileSystem); - - sensorContext = mock(SensorContext.class); - } - - @Test - public void shouldImportOnlyOneScalaFile() { - when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getInputFiles(1)); - when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); - - scalaSourceImporter.analyse(project, sensorContext); - - InOrder inOrder = inOrder(sensorContext); - inOrder.verify(sensorContext, times(1)).index(eq(FileTestUtils.SCALA_SOURCE_FILE)); - inOrder.verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_SOURCE_FILE), any(String.class)); - inOrder.verifyNoMoreInteractions(); - } - - @Test - public void shouldImportOnlyOneScalaFileWithTheCorrectFileContent() throws IOException { - when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getInputFiles(1)); - when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); - - scalaSourceImporter.analyse(project, sensorContext); - - InOrder inOrder = inOrder(sensorContext); - inOrder.verify(sensorContext, times(1)).index(eq(FileTestUtils.SCALA_SOURCE_FILE)); - inOrder.verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_SOURCE_FILE), - eq(getContentOfFiles(1).get(0))); - inOrder.verifyNoMoreInteractions(); - } - - @Test - public void shouldImportAllScalaFiles() { - when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getInputFiles(3)); - when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); - - scalaSourceImporter.analyse(project, sensorContext); - - verify(sensorContext, times(3)).index(eq(FileTestUtils.SCALA_SOURCE_FILE)); - verify(sensorContext, times(3)).saveSource(eq(FileTestUtils.SCALA_SOURCE_FILE), any(String.class)); - verifyNoMoreInteractions(sensorContext); - } - - @Test - public void shouldImportAllScalaFilesAndNotFilesOfOtherLanguages() { - when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getInputFiles(3)); - when(fileSystem.mainFiles(Java.INSTANCE.getKey())) - .thenReturn(FileTestUtils.getInputFiles("/scalaSourceImporter/", "JavaMainFile", "java", 1)); - when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); - - scalaSourceImporter.analyse(project, sensorContext); - - verify(sensorContext, times(3)).index(eq(FileTestUtils.SCALA_SOURCE_FILE)); - verify(sensorContext, times(3)).saveSource(eq(FileTestUtils.SCALA_SOURCE_FILE), any(String.class)); - verifyNoMoreInteractions(sensorContext); - } - - @Test - public void shouldImportAllScalaFilesWithTheCorrectFileContent() throws IOException { - when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getInputFiles(3)); - when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); - - scalaSourceImporter.analyse(project, sensorContext); - - List contentOfFiles = getContentOfFiles(3); - verify(sensorContext, times(3)).index(eq(FileTestUtils.SCALA_SOURCE_FILE)); - verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_SOURCE_FILE), eq(contentOfFiles.get(0))); - verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_SOURCE_FILE), eq(contentOfFiles.get(1))); - verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_SOURCE_FILE), eq(contentOfFiles.get(2))); - verifyNoMoreInteractions(sensorContext); - } - - @Test - public void shouldImportOnlyOneScalaTestFile() { - when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); - when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getTestInputFiles(1)); - - scalaSourceImporter.analyse(project, sensorContext); - - InOrder inOrder = inOrder(sensorContext); - inOrder.verify(sensorContext, times(1)).index(eq(FileTestUtils.SCALA_TEST_FILE)); - inOrder.verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_TEST_FILE), any(String.class)); - inOrder.verifyNoMoreInteractions(); - } - - @Test - public void shouldImportOnlyOneScalaTestFileWithTheCorrectFileContent() throws IOException { - when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); - when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getTestInputFiles(1)); - - scalaSourceImporter.analyse(project, sensorContext); - - InOrder inOrder = inOrder(sensorContext); - inOrder.verify(sensorContext, times(1)).index(eq(FileTestUtils.SCALA_TEST_FILE)); - inOrder.verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_TEST_FILE), - eq(getContentOfTestFiles(1).get(0))); - inOrder.verifyNoMoreInteractions(); - } - - @Test - public void shouldImportAllScalaTestFiles() { - when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); - when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getTestInputFiles(3)); - - scalaSourceImporter.analyse(project, sensorContext); - - verify(sensorContext, times(3)).index(eq(FileTestUtils.SCALA_TEST_FILE)); - verify(sensorContext, times(3)).saveSource(eq(FileTestUtils.SCALA_TEST_FILE), any(String.class)); - verifyNoMoreInteractions(sensorContext); - } - - @Test - public void shouldImportAllScalaTestFilesAndNotTestFilesOfOtherLanguages() { - when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); - when(fileSystem.testFiles(Java.INSTANCE.getKey())) - .thenReturn(FileTestUtils.getInputFiles("/scalaSourceImporter/", "JavaTestFile", "java", 1)); - when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getTestInputFiles(3)); - - scalaSourceImporter.analyse(project, sensorContext); - - verify(sensorContext, times(3)).index(eq(FileTestUtils.SCALA_TEST_FILE)); - verify(sensorContext, times(3)).saveSource(eq(FileTestUtils.SCALA_TEST_FILE), any(String.class)); - verifyNoMoreInteractions(sensorContext); - } - - @Test - public void shouldImportAllScalaTestFilesWithTheCorrectFileContent() throws IOException { - when(fileSystem.mainFiles(scalaSourceImporter.getScala().getKey())).thenReturn(new ArrayList()); - when(fileSystem.testFiles(scalaSourceImporter.getScala().getKey())).thenReturn(getTestInputFiles(3)); - - scalaSourceImporter.analyse(project, sensorContext); - - List contentOfFiles = getContentOfTestFiles(3); - verify(sensorContext, times(3)).index(eq(FileTestUtils.SCALA_TEST_FILE)); - verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_TEST_FILE), eq(contentOfFiles.get(0))); - verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_TEST_FILE), eq(contentOfFiles.get(1))); - verify(sensorContext, times(1)).saveSource(eq(FileTestUtils.SCALA_TEST_FILE), eq(contentOfFiles.get(2))); - verifyNoMoreInteractions(sensorContext); - } - - public List getInputFiles(int numberOfFiles) { - return FileTestUtils.getInputFiles("/scalaSourceImporter/", "MainFile", numberOfFiles); - } - - public List getTestInputFiles(int numberOfFiles) { - return FileTestUtils.getInputFiles("/scalaSourceImporter/", "TestFile", numberOfFiles); - } - - public List getContentOfFiles(int numberOfFiles) throws IOException { - return FileTestUtils.getContentOfFiles("/scalaSourceImporter/", "MainFile", numberOfFiles); - } - - public List getContentOfTestFiles(int numberOfFiles) throws IOException { - return FileTestUtils.getContentOfFiles("/scalaSourceImporter/", "TestFile", numberOfFiles); - } -} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/surefire/SurefireSensorTest.java b/src/test/java/com/buransky/plugins/scala/surefire/SurefireSensorTest.java deleted file mode 100644 index 5a3a567..0000000 --- a/src/test/java/com/buransky/plugins/scala/surefire/SurefireSensorTest.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.surefire; - -import org.junit.Test; -import org.junit.Before; -import org.sonar.api.batch.CoverageExtension; -import org.sonar.api.resources.Project; -import com.buransky.plugins.scala.language.Scala; - -import static org.junit.Assert.*; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class SurefireSensorTest { - - private SurefireSensor sensor; - private Project project; - - @Before - public void setUp() { - sensor = new SurefireSensor(); - project = mock(Project.class); - } - - @Test - public void shouldExecuteOnReuseReports() { - when(project.getLanguageKey()).thenReturn(Scala.INSTANCE.getKey()); - when(project.getAnalysisType()).thenReturn(Project.AnalysisType.REUSE_REPORTS); - assertTrue(sensor.shouldExecuteOnProject(project)); - } - - @Test - public void shouldExecuteOnDynamicAnalysis() { - when(project.getLanguageKey()).thenReturn(Scala.INSTANCE.getKey()); - when(project.getAnalysisType()).thenReturn(Project.AnalysisType.DYNAMIC); - assertTrue(sensor.shouldExecuteOnProject(project)); - } - - @Test - public void shouldNotExecuteIfStaticAnalysis() { - when(project.getLanguageKey()).thenReturn(Scala.INSTANCE.getKey()); - when(project.getAnalysisType()).thenReturn(Project.AnalysisType.STATIC); - assertFalse(sensor.shouldExecuteOnProject(project)); - } - - @Test - public void shouldNotExecuteOnJavaProject() { - when(project.getLanguageKey()).thenReturn("java"); - when(project.getAnalysisType()).thenReturn(Project.AnalysisType.DYNAMIC); - assertFalse(sensor.shouldExecuteOnProject(project)); - } - - @Test - public void shouldDependOnCoverageSensors() { - assertEquals(CoverageExtension.class, sensor.dependsUponCoverageSensors()); - } - - @Test - public void testToString() { - assertEquals("Scala SurefireSensor", sensor.toString()); - } -} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/util/DummyScalaFile.java b/src/test/java/com/buransky/plugins/scala/util/DummyScalaFile.java deleted file mode 100644 index e655755..0000000 --- a/src/test/java/com/buransky/plugins/scala/util/DummyScalaFile.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.util; - -import com.buransky.plugins.scala.language.ScalaFile; - -public class DummyScalaFile extends ScalaFile { - - public DummyScalaFile(boolean isUnitTest) { - super("", "", isUnitTest); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof ScalaFile)) { - return false; - } - ScalaFile other = (ScalaFile) obj; - return isUnitTest() == other.isUnitTest(); - } -} \ No newline at end of file diff --git a/src/test/java/com/buransky/plugins/scala/util/FileTestUtils.java b/src/test/java/com/buransky/plugins/scala/util/FileTestUtils.java deleted file mode 100644 index 75a290a..0000000 --- a/src/test/java/com/buransky/plugins/scala/util/FileTestUtils.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.util; - -import java.io.File; -import java.io.IOException; -import java.net.URL; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.io.FileUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonar.api.resources.InputFile; -import org.sonar.api.resources.InputFileUtils; -import com.buransky.plugins.scala.language.ScalaFile; - -public final class FileTestUtils { - - public static final ScalaFile SCALA_SOURCE_FILE = new DummyScalaFile(false); - public static final ScalaFile SCALA_TEST_FILE = new DummyScalaFile(true); - - private static final Logger LOGGER = LoggerFactory.getLogger(FileTestUtils.class); - - private FileTestUtils() { - // to prevent instantiation - } - - public static String getRelativePath(String path) { - return FileTestUtils.class.getResource(path).getFile(); - } - - public static List getInputFiles(String path, String fileNameBase, int numberOfFiles) { - return getInputFiles(path, fileNameBase, "scala", numberOfFiles); - } - - public static List getInputFiles(String path, String fileNameBase, - String fileSuffix, int numberOfFiles) { - List mainFiles = new ArrayList(); - - URL resourceURL = FileTestUtils.class.getResource(path + fileNameBase + "1." + fileSuffix); - for (int i = 1; resourceURL != null && i <= numberOfFiles;) { - mainFiles.add(new File(resourceURL.getFile())); - resourceURL = FileTestUtils.class.getResource(path + fileNameBase + (++i) + "." + fileSuffix); - } - - return InputFileUtils.create(new File(FileTestUtils.class.getResource(path).getFile()), mainFiles); - } - - public static List getContentOfFiles(String path, String fileNameBase, - int numberOfFiles) throws IOException { - List contentOfFiles = new ArrayList(); - - URL resourceURL = FileTestUtils.class.getResource(path + fileNameBase + "1.scala"); - for (int i = 1; resourceURL != null && i <= numberOfFiles;) { - try { - contentOfFiles.add(FileUtils.readFileToString(new File(resourceURL.getFile()), - Charset.defaultCharset().toString())); - } catch (IOException ioe) { - LOGGER.error("Unexpected I/O exception occurred", ioe); - throw ioe; - } - resourceURL = FileTestUtils.class.getResource(path + fileNameBase + (++i) + ".scala"); - } - - return contentOfFiles; - } -} \ No newline at end of file diff --git a/src/test/resources/baseMetricsSensor/ScalaFile1.scala b/src/test/resources/baseMetricsSensor/ScalaFile1.scala deleted file mode 100644 index c739d27..0000000 --- a/src/test/resources/baseMetricsSensor/ScalaFile1.scala +++ /dev/null @@ -1,5 +0,0 @@ -package baseMetricsSensor - -class ScalaFile1 { - -} \ No newline at end of file diff --git a/src/test/resources/baseMetricsSensor/ScalaFile2.scala b/src/test/resources/baseMetricsSensor/ScalaFile2.scala deleted file mode 100644 index 31a2287..0000000 --- a/src/test/resources/baseMetricsSensor/ScalaFile2.scala +++ /dev/null @@ -1,5 +0,0 @@ -package baseMetricsSensor.otherPackage - -class ScalaFile2 { - -} \ No newline at end of file diff --git a/src/test/resources/baseMetricsSensor/ScalaFile3.scala b/src/test/resources/baseMetricsSensor/ScalaFile3.scala deleted file mode 100644 index 29c4213..0000000 --- a/src/test/resources/baseMetricsSensor/ScalaFile3.scala +++ /dev/null @@ -1,5 +0,0 @@ -package baseMetricsSensor - -class ScalaFile3 { - -} \ No newline at end of file diff --git a/src/test/resources/cpd/Duplications5Tokens.scala b/src/test/resources/cpd/Duplications5Tokens.scala deleted file mode 100644 index 6a1150c..0000000 --- a/src/test/resources/cpd/Duplications5Tokens.scala +++ /dev/null @@ -1,6 +0,0 @@ -class Duplications5Tokens { - val i = 0; - - - val j = 0; -} diff --git a/src/test/resources/cpd/NewlineToken.scala b/src/test/resources/cpd/NewlineToken.scala deleted file mode 100644 index dec3a84..0000000 --- a/src/test/resources/cpd/NewlineToken.scala +++ /dev/null @@ -1,5 +0,0 @@ -class NewlineToken { - val i = 42 - val j = 1000 - println("hehe") -} diff --git a/src/test/resources/cpd/NewlinesToken.scala b/src/test/resources/cpd/NewlinesToken.scala deleted file mode 100644 index fd8c9c2..0000000 --- a/src/test/resources/cpd/NewlinesToken.scala +++ /dev/null @@ -1,6 +0,0 @@ -class NewlineToken { - val i = 42 - val j = 1000 - - println("hehe") -} diff --git a/src/test/resources/cpd/NoDuplications.scala b/src/test/resources/cpd/NoDuplications.scala deleted file mode 100644 index 638d321..0000000 --- a/src/test/resources/cpd/NoDuplications.scala +++ /dev/null @@ -1,3 +0,0 @@ -class NoDuplications { - val i = 0 -} diff --git a/src/test/resources/cpd/TwoDuplicatedBlocks.scala b/src/test/resources/cpd/TwoDuplicatedBlocks.scala deleted file mode 100644 index 856335f..0000000 --- a/src/test/resources/cpd/TwoDuplicatedBlocks.scala +++ /dev/null @@ -1,11 +0,0 @@ -class TwoDuplicatedBlocks { - val i = 42 - println("Foo") - println("Bar"); - val j = 0; - - val k = 42 - println("John") - println("Smith"); - val l = 0; -} diff --git a/src/test/resources/lexer/DocComment1.txt b/src/test/resources/lexer/DocComment1.txt deleted file mode 100644 index 8f5b166..0000000 --- a/src/test/resources/lexer/DocComment1.txt +++ /dev/null @@ -1 +0,0 @@ -/** Hello World */ \ No newline at end of file diff --git a/src/test/resources/lexer/HeaderCommentWithCodeBefore.txt b/src/test/resources/lexer/HeaderCommentWithCodeBefore.txt deleted file mode 100644 index e1ac767..0000000 --- a/src/test/resources/lexer/HeaderCommentWithCodeBefore.txt +++ /dev/null @@ -1,8 +0,0 @@ -public class HelloWorld { - val a = 1 -} - -/* - * This comment describes the - * content of the file. - */ \ No newline at end of file diff --git a/src/test/resources/lexer/HeaderCommentWithWrongStart.txt b/src/test/resources/lexer/HeaderCommentWithWrongStart.txt deleted file mode 100644 index c706531..0000000 --- a/src/test/resources/lexer/HeaderCommentWithWrongStart.txt +++ /dev/null @@ -1,4 +0,0 @@ -/** - * This comment describes the - * content of the file. - */ \ No newline at end of file diff --git a/src/test/resources/lexer/NormalCommentWithHeaderComment.txt b/src/test/resources/lexer/NormalCommentWithHeaderComment.txt deleted file mode 100644 index 2f52a33..0000000 --- a/src/test/resources/lexer/NormalCommentWithHeaderComment.txt +++ /dev/null @@ -1,6 +0,0 @@ -// Just a test - -/* - * This comment describes the - * content of the file. - */ \ No newline at end of file diff --git a/src/test/resources/lexer/SimpleHeaderComment.txt b/src/test/resources/lexer/SimpleHeaderComment.txt deleted file mode 100644 index 366ad3a..0000000 --- a/src/test/resources/lexer/SimpleHeaderComment.txt +++ /dev/null @@ -1,4 +0,0 @@ -/* - * This comment describes the - * content of the file. - */ \ No newline at end of file diff --git a/src/test/resources/org/sonar/plugins/scala/cobertura/coverage.xml b/src/test/resources/org/sonar/plugins/scala/cobertura/coverage.xml deleted file mode 100644 index fbd7b37..0000000 --- a/src/test/resources/org/sonar/plugins/scala/cobertura/coverage.xml +++ /dev/null @@ -1,6768 +0,0 @@ - - - - /Users/cpicat/myproject/grails-app/domain - - - /Users/cpicat/myproject/grails-app/controllers - - - /Users/cpicat/myproject/grails-app/taglib - - - /Users/cpicat/myproject/src/java - - - /Users/cpicat/myproject/grails-app/services - - - /Users/cpicat/myproject/src/scala - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/test/resources/packageResolver/DeepNestedPackageDeclaration.txt b/src/test/resources/packageResolver/DeepNestedPackageDeclaration.txt deleted file mode 100644 index 4b877bb..0000000 --- a/src/test/resources/packageResolver/DeepNestedPackageDeclaration.txt +++ /dev/null @@ -1,18 +0,0 @@ -package one.two.three.four { - - package five.six.seven { - - package eight.nine.ten.eleven { - - package twelve.thirteen.fourteen { - - package fifteen.sixteen { - - object A { - val b = 1 - } - } - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/packageResolver/DeepNestedPackageDeclarationWithObjectBetween.txt b/src/test/resources/packageResolver/DeepNestedPackageDeclarationWithObjectBetween.txt deleted file mode 100644 index af000f4..0000000 --- a/src/test/resources/packageResolver/DeepNestedPackageDeclarationWithObjectBetween.txt +++ /dev/null @@ -1,22 +0,0 @@ -package one { - - package two.three.four.five.six.seven { - - package eight { - - object B - - package nine.ten.eleven.twelve.thirteen.fourteen { - - object C - - package fifteen.sixteen { - - object A { - val b = 1 - } - } - } - } - } -} \ No newline at end of file diff --git a/src/test/resources/packageResolver/NestedPackageDeclaration.txt b/src/test/resources/packageResolver/NestedPackageDeclaration.txt deleted file mode 100644 index 40a9b04..0000000 --- a/src/test/resources/packageResolver/NestedPackageDeclaration.txt +++ /dev/null @@ -1,9 +0,0 @@ -package one.two { - - package three { - - object A { - val b = 1 - } - } -} \ No newline at end of file diff --git a/src/test/resources/packageResolver/NestedPackageDeclarationWithObjectBetween.txt b/src/test/resources/packageResolver/NestedPackageDeclarationWithObjectBetween.txt deleted file mode 100644 index 1c1ae97..0000000 --- a/src/test/resources/packageResolver/NestedPackageDeclarationWithObjectBetween.txt +++ /dev/null @@ -1,11 +0,0 @@ -package one.two { - - object B - - package three { - - object A { - val b = 1 - } - } -} \ No newline at end of file diff --git a/src/test/resources/packageResolver/SimplePackageDeclaration.txt b/src/test/resources/packageResolver/SimplePackageDeclaration.txt deleted file mode 100644 index 9f19b5f..0000000 --- a/src/test/resources/packageResolver/SimplePackageDeclaration.txt +++ /dev/null @@ -1,6 +0,0 @@ -package one { - - object A { - val b = 1 - } -} \ No newline at end of file diff --git a/src/test/resources/scalaFile/ScalaFile1.scala b/src/test/resources/scalaFile/ScalaFile1.scala deleted file mode 100644 index 0d30e45..0000000 --- a/src/test/resources/scalaFile/ScalaFile1.scala +++ /dev/null @@ -1,5 +0,0 @@ -package scalaFile - -class ScalaFile1 { - -} \ No newline at end of file diff --git a/src/test/resources/scalaFile/ScalaTestFile1.scala b/src/test/resources/scalaFile/ScalaTestFile1.scala deleted file mode 100644 index 5f58e94..0000000 --- a/src/test/resources/scalaFile/ScalaTestFile1.scala +++ /dev/null @@ -1,5 +0,0 @@ -package scalaFile - -class ScalaTestFile1 { - -} \ No newline at end of file diff --git a/src/test/resources/scalaSourceImporter/JavaMainFile1.java b/src/test/resources/scalaSourceImporter/JavaMainFile1.java deleted file mode 100644 index ad3a4b5..0000000 --- a/src/test/resources/scalaSourceImporter/JavaMainFile1.java +++ /dev/null @@ -1,5 +0,0 @@ -package scalaSourceImporter; - -public class JavaMainFile1 { - -} \ No newline at end of file diff --git a/src/test/resources/scalaSourceImporter/JavaTestFile1.java b/src/test/resources/scalaSourceImporter/JavaTestFile1.java deleted file mode 100644 index 742f8b0..0000000 --- a/src/test/resources/scalaSourceImporter/JavaTestFile1.java +++ /dev/null @@ -1,5 +0,0 @@ -package scalaSourceImporter; - -public class JavaTestFile1 { - -} \ No newline at end of file diff --git a/src/test/resources/scalaSourceImporter/MainFile1.scala b/src/test/resources/scalaSourceImporter/MainFile1.scala deleted file mode 100644 index 3cbae7a..0000000 --- a/src/test/resources/scalaSourceImporter/MainFile1.scala +++ /dev/null @@ -1,5 +0,0 @@ -package scalaSourceImporter - -class MainFile1 { - -} \ No newline at end of file diff --git a/src/test/resources/scalaSourceImporter/MainFile2.scala b/src/test/resources/scalaSourceImporter/MainFile2.scala deleted file mode 100644 index d2474a8..0000000 --- a/src/test/resources/scalaSourceImporter/MainFile2.scala +++ /dev/null @@ -1,5 +0,0 @@ -package scalaSourceImporter - -class MainFile2 { - -} \ No newline at end of file diff --git a/src/test/resources/scalaSourceImporter/MainFile3.scala b/src/test/resources/scalaSourceImporter/MainFile3.scala deleted file mode 100644 index a0d7a39..0000000 --- a/src/test/resources/scalaSourceImporter/MainFile3.scala +++ /dev/null @@ -1,5 +0,0 @@ -package scalaSourceImporter - -class MainFile3 { - -} \ No newline at end of file diff --git a/src/test/resources/scalaSourceImporter/TestFile1.scala b/src/test/resources/scalaSourceImporter/TestFile1.scala deleted file mode 100644 index efbd263..0000000 --- a/src/test/resources/scalaSourceImporter/TestFile1.scala +++ /dev/null @@ -1,5 +0,0 @@ -package scalaSourceImporter - -class TestFile1 { - -} \ No newline at end of file diff --git a/src/test/resources/scalaSourceImporter/TestFile2.scala b/src/test/resources/scalaSourceImporter/TestFile2.scala deleted file mode 100644 index dfc8fc3..0000000 --- a/src/test/resources/scalaSourceImporter/TestFile2.scala +++ /dev/null @@ -1,5 +0,0 @@ -package scalaSourceImporter - -class TestFile2 { - -} \ No newline at end of file diff --git a/src/test/resources/scalaSourceImporter/TestFile3.scala b/src/test/resources/scalaSourceImporter/TestFile3.scala deleted file mode 100644 index b7fe94d..0000000 --- a/src/test/resources/scalaSourceImporter/TestFile3.scala +++ /dev/null @@ -1,5 +0,0 @@ -package scalaSourceImporter - -class TestFile3 { - -} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/compiler/LexerSpec.scala b/src/test/scala/com/buransky/plugins/scala/compiler/LexerSpec.scala deleted file mode 100644 index 79d0469..0000000 --- a/src/test/scala/com/buransky/plugins/scala/compiler/LexerSpec.scala +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.compiler - -import tools.nsc.ast.parser.Tokens._ - -import java.util.Arrays -import org.junit.runner.RunWith -import org.scalatest.FlatSpec -import org.scalatest.matchers.ShouldMatchers -import org.scalatest.junit.JUnitRunner - - - -import collection.JavaConversions._ -import com.buransky.plugins.scala.language.{CommentType, Comment} -import com.buransky.plugins.scala.util.FileTestUtils - -@RunWith(classOf[JUnitRunner]) -class LexerSpec extends FlatSpec with ShouldMatchers { - - private val lexer = new Lexer() - - private val headerComment = "/*\r\n * This comment describes the\r\n" + - " * content of the file.\r\n */" - - "A lexer" should "tokenize a simple declaration of a value" in { - val tokens = lexer.getTokens("val a = " + "\r\n" + "42") - tokens should equal (Arrays.asList(Token(VAL, 1), Token(IDENTIFIER, 1), Token(EQUALS, 1), Token(INTLIT, 2))) - } - - it should "tokenize a doc comment" in { - val comments = getCommentsOf("DocComment1") - comments should have size(1) - comments should contain (new Comment("/** Hello World */", CommentType.DOC)) - } - - it should "tokenize a header comment" in { - val comments = getCommentsOf("SimpleHeaderComment") - comments should have size(1) - comments should contain (new Comment(headerComment, CommentType.HEADER)) - } - - it should "not tokenize a header comment when it is not the first comment" in { - val comments = getCommentsOf("NormalCommentWithHeaderComment") - comments should have size(2) - comments should contain (new Comment("// Just a test", CommentType.NORMAL)) - comments should contain (new Comment(headerComment, CommentType.NORMAL)) - } - - it should "not tokenize a header comment when it is not starting with /*" in { - val comments = getCommentsOf("HeaderCommentWithWrongStart") - comments should have size(1) - comments should contain (new Comment("/**\r\n * This comment describes the\r\n" + - " * content of the file.\r\n */", CommentType.DOC)) - } - - it should "not tokenize a header comment when there was code before" in { - val comments = getCommentsOf("HeaderCommentWithCodeBefore") - comments should have size(1) - comments should contain (new Comment(headerComment, CommentType.NORMAL)) - } - - // TODO add more specs for lexer - - private def getCommentsOf(fileName: String) = { - val path = FileTestUtils.getRelativePath("/lexer/" + fileName + ".txt") - lexer.getCommentsOfFile(path) - } -} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/language/CodeDetectorSpec.scala b/src/test/scala/com/buransky/plugins/scala/language/CodeDetectorSpec.scala deleted file mode 100644 index f01b2b8..0000000 --- a/src/test/scala/com/buransky/plugins/scala/language/CodeDetectorSpec.scala +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.language - -import org.junit.runner.RunWith -import org.scalatest.FlatSpec -import org.scalatest.matchers.ShouldMatchers -import org.scalatest.junit.JUnitRunner - -@RunWith(classOf[JUnitRunner]) -class CodeDetectorSpec extends FlatSpec with ShouldMatchers { - - "A code detector" should "detect a simple variable declaration" in { - CodeDetector.hasDetectedCode("val a = 1") should be (true) - } - - it should "detect a simple function call" in { - CodeDetector.hasDetectedCode("list.map(_ + \"Hello World\")") should be (true) - } - - it should "detect a simple value assignment" in { - CodeDetector.hasDetectedCode("a = 1 + 2") should be (true) - } - - it should "detect a simple package declaration" in { - CodeDetector.hasDetectedCode("package hello.world") should be (true) - } - - it should "not detect any code in a normal text" in { - CodeDetector.hasDetectedCode("this is just a normal text") should be (false) - } - - it should "not detect any code in a normal comment text" in { - CodeDetector.hasDetectedCode("// this is a normal comment") should be (false) - } - - it should "detect a while loop" in { - CodeDetector.hasDetectedCode("while (i == 2) { println(i); }") should be (true) - } - - it should "detect a for loop" in { - CodeDetector.hasDetectedCode("for (i <- 1 to 10) { println(i); }") should be (true) - } -} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/language/PackageResolverSpec.scala b/src/test/scala/com/buransky/plugins/scala/language/PackageResolverSpec.scala deleted file mode 100644 index 47fb3c3..0000000 --- a/src/test/scala/com/buransky/plugins/scala/language/PackageResolverSpec.scala +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.language - -import org.junit.runner.RunWith -import org.scalatest.FlatSpec -import org.scalatest.matchers.ShouldMatchers -import org.scalatest.junit.JUnitRunner - -import com.buransky.plugins.scala.util.FileTestUtils -; - -@RunWith(classOf[JUnitRunner]) -class PackageResolverSpec extends FlatSpec with ShouldMatchers { - - "A package resolver" should "resolve the package name of a simple package declaration" in { - getPackageNameOf("SimplePackageDeclaration") should equal ("one") - } - - it should "resolve the package name of a nested package declaration" in { - getPackageNameOf("NestedPackageDeclaration") should equal ("one.two.three") - } - - it should "resolve the package name of a deep nested package declaration" in { - getPackageNameOf("DeepNestedPackageDeclaration") should equal ("one.two.three.four.five.six." + - "seven.eight.nine.ten.eleven.twelve.thirteen.fourteen.fifteen.sixteen") - } - - it should "resolve the upper package name of a nested package declaration with " + - "an object declaration between" in { - getPackageNameOf("NestedPackageDeclarationWithObjectBetween") should equal ("one.two") - } - - it should "resolve the package name of a deep nested package declaration with" + - "an object declaration between" in { - getPackageNameOf("DeepNestedPackageDeclarationWithObjectBetween") should equal ("one.two.three." + - "four.five.six.seven.eight") - } - - private def getPackageNameOf(fileName: String) = { - val path = FileTestUtils.getRelativePath("/packageResolver/" + fileName + ".txt") - PackageResolver.resolvePackageNameOfFile(path) - } -} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/metrics/ComplexityCalculatorSpec.scala b/src/test/scala/com/buransky/plugins/scala/metrics/ComplexityCalculatorSpec.scala deleted file mode 100644 index 6730cd9..0000000 --- a/src/test/scala/com/buransky/plugins/scala/metrics/ComplexityCalculatorSpec.scala +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.metrics - -import org.junit.runner.RunWith -import org.scalatest.FlatSpec -import org.scalatest.matchers.ShouldMatchers -import org.scalatest.junit.JUnitRunner - -@RunWith(classOf[JUnitRunner]) -class ComplexityCalculatorSpec extends FlatSpec with ShouldMatchers { - - "A complexity calculator" should "calculate complexity of if expression" in { - ComplexityCalculator.measureComplexity("if (2 == 3) println(123)") should be (1) - } - - it should "calculate complexity of for loop" in { - ComplexityCalculator.measureComplexity("for (i <- 1 to 10) println(i)") should be (1) - } - - it should "calculate complexity of while loop" in { - val source = """var i = 0 - while (i < 10) { - println(i) - i += 1 - }""" - ComplexityCalculator.measureComplexity(source) should be (1) - } - - it should "calculate complexity of do loop" in { - val source = """var i = 0 - do { - println(i) - i += 1 - } while (i < 10)""" - ComplexityCalculator.measureComplexity(source) should be (1) - } - - it should "calculate complexity of throw expression" in { - ComplexityCalculator.measureComplexity("throw new RuntimeException()") should be (1) - } - - it should "calculate complexity of while loop with an if condition and throw expression" in { - val source = """var i = 0 - while (i < 10) { - println(i) - i += 1 - if (i == 9) - throw new RuntimeException() - }""" - ComplexityCalculator.measureComplexity(source) should be (3) - } - - it should "calculate complexity of function definition" in { - ComplexityCalculator.measureComplexity("def inc(i: Int) = i + 1") should be (1) - } - - it should "calculate complexity of function definition with an if condition" in { - val source = """def inc(i: Int) = { - if (i == 0) { - i + 2 - } else { - i + 1 - } - }""" - ComplexityCalculator.measureComplexity(source) should be (2) - } - - it should "calculate complexity of function definition and its whole body" in { - val source = """def inc(i: Int) = { - if (i == 0) { - i + 2 - } else { - while (i < 10) { - if (i == 9) - throw new RuntimeException() - i + 1 - } - } - }""" - ComplexityCalculator.measureComplexity(source) should be (5) - } - - it should "calculate complexity distribution of one function" in { - val source = """def inc(i: Int) = { - if (i == 0) { - i + 2 - } else { - i + 1 - } - }""" - - ComplexityCalculator.measureComplexityOfFunctions(source).getMeasure.getData should include ("2=1") - } - - it should "calculate complexity distribution of two functions" in { - val source = """def inc(i: Int) = { - if (i == 0) { - i + 2 - } else { - i + 1 - } - } - - def dec(i: Int) = i - 1""" - - ComplexityCalculator.measureComplexityOfFunctions(source).getMeasure.getData should include ("1=1") - ComplexityCalculator.measureComplexityOfFunctions(source).getMeasure.getData should include ("2=1") - } - - it should "calculate complexity distribution of all functions" in { - val source = """def inc(i: Int) = { - if (i == 0) { - i + 2 - } else { - i + 1 - } - } - - def dec(i: Int) = i - 1 - def dec2(i: Int) = i - 2 - def dec3(i: Int) = i - 3""" - - ComplexityCalculator.measureComplexityOfFunctions(source).getMeasure.getData should include ("1=3") - ComplexityCalculator.measureComplexityOfFunctions(source).getMeasure.getData should include ("2=1") - } - - it should "calculate complexity distribution of all functions nested in a class" in { - val source = """class A { - def inc(i: Int) = { - if (i == 0) { - i + 2 - } else { - i + 1 - } - } - - def dec(i: Int) = i - 1 - def dec2(i: Int) = i - 2 - def dec3(i: Int) = i - 3 - }""" - - ComplexityCalculator.measureComplexityOfFunctions(source).getMeasure.getData should include ("1=3") - ComplexityCalculator.measureComplexityOfFunctions(source).getMeasure.getData should include ("2=1") - } - - it should "calculate complexity distribution of one class" in { - val source = """class A { - def inc(i: Int) = { - if (i == 0) { - i + 2 - } else { - i + 1 - } - } - }""" - - ComplexityCalculator.measureComplexityOfClasses(source).getMeasure.getData should include ("0=1") - } - - it should "calculate complexity distribution of two classes" in { - val source = """package abc - class A { - def inc(i: Int) = { - if (i == 0) { - i + 2 - } else { - i + 1 - } - } - - def dec(i: Int) = i - 1 - } - - class B { - def inc(i: Int) = { - if (i == 0) { - i + 2 - } else { - i + 1 - } - } - - def dec(i: Int) = i - 1 - def dec2(i: Int) = i - 2 - def dec3(i: Int) = i - 3 - }""" - - ComplexityCalculator.measureComplexityOfClasses(source).getMeasure.getData should include ("0=1") - ComplexityCalculator.measureComplexityOfClasses(source).getMeasure.getData should include ("5=1") - } -} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/metrics/FunctionCounterSpec.scala b/src/test/scala/com/buransky/plugins/scala/metrics/FunctionCounterSpec.scala deleted file mode 100644 index 7463cb9..0000000 --- a/src/test/scala/com/buransky/plugins/scala/metrics/FunctionCounterSpec.scala +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.metrics - -import org.junit.runner.RunWith -import org.scalatest.FlatSpec -import org.scalatest.matchers.ShouldMatchers -import org.scalatest.junit.JUnitRunner - -@RunWith(classOf[JUnitRunner]) -class FunctionCounterSpec extends FlatSpec with ShouldMatchers { - - "A function counter" should "count a simple function declaration" in { - FunctionCounter.countFunctions("def test = 42") should be (1) - } - - it should "count a simple method declaration" in { - FunctionCounter.countFunctions("def test { println(42) }") should be (1) - } - - it should "not count a simple function declared as a function literal" in { - FunctionCounter.countFunctions("(i: Int) => i + 1") should be (0) - } - - it should "count a simple function declaration nested in another function" in { - val source = """ - def test = { - def inc(i: Int) = i + 1 - }""" - FunctionCounter.countFunctions(source) should be (2) - } - - it should "count a simple function declaration nested in another method" in { - val source = """ - def test { - def inc(i: Int) = i + 1 - }""" - FunctionCounter.countFunctions(source) should be (2) - } - - it should "not count an empty constructor as a function declaration" in { - val source = "class Person(val name: String) { }" - FunctionCounter.countFunctions(source) should be (0) - } - - it should "count a constructor as a function declaration" in { - val source = """ - class Person(val name: String) { - def this(name: String) { - super(name) - println(name) - } - }""" - FunctionCounter.countFunctions(source) should be (1) - } - - it should "count a simple function declaration nested in an object" in { - val source = """ - object Test { - def inc(i: Int) = { i + 1 } - }""" - FunctionCounter.countFunctions(source) should be (1) - } - - it should "count a simple function declaration nested in a trait" in { - val source = """ - trait Test { - def inc(i: Int) = { i + 1 } - }""" - FunctionCounter.countFunctions(source) should be (1) - } - - it should "count a function declaration with two parameter lists" in { - val source = "def sum(x: Int)(y: Int) = { x + y }" - FunctionCounter.countFunctions(source) should be (1) - } - - it should "count a simple function declaration nested in a trait with self-type annotation" in { - val source = """ - trait Test { - self: HelloWorld => - def inc(i: Int) = { i + 1 } - }""" - FunctionCounter.countFunctions(source) should be (1) - } - - it should "count a function declaration with two parameter lists nested in a trait with self-type annotation" in { - val source = """ - trait Test { - self: HelloWorld => - def sum(x: Int)(y: Int) = { x + y } - }""" - FunctionCounter.countFunctions(source) should be (1) - } - - it should "count a function declaration with if else block in its body" in { - val source = """ - def test(number: Int) : Int = - if (number < 42) - 23 - else - 42""" - FunctionCounter.countFunctions(source) should be (1) - } -} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/metrics/PublicApiCounterSpec.scala b/src/test/scala/com/buransky/plugins/scala/metrics/PublicApiCounterSpec.scala deleted file mode 100644 index f34a7cc..0000000 --- a/src/test/scala/com/buransky/plugins/scala/metrics/PublicApiCounterSpec.scala +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.metrics - -import org.junit.runner.RunWith -import org.scalatest.FlatSpec -import org.scalatest.matchers.ShouldMatchers -import org.scalatest.junit.JUnitRunner - -@RunWith(classOf[JUnitRunner]) -class PublicApiCounterSpec extends FlatSpec with ShouldMatchers { - - "A public api counter" should "count a simple function declaration" in { - PublicApiCounter.countPublicApi("def test = 42") should be (1) - } - - it should "count a simple method declaration" in { - PublicApiCounter.countPublicApi("def test { println(42) }") should be (1) - } - - it should "count a simple value declaration" in { - PublicApiCounter.countPublicApi("val maybeImportantNumber = 42") should be (1) - } - - it should "not count a private value declaration" in { - PublicApiCounter.countPublicApi("private val maybeImportantNumber = 42") should be (0) - } - - it should "not count a private function declaration" in { - PublicApiCounter.countPublicApi("private def test = 42") should be (0) - } - - it should "not count a private method declaration" in { - PublicApiCounter.countPublicApi("private def test { println(42) }") should be (0) - } - - it should "count a class declaration" in { - PublicApiCounter.countPublicApi("class A {}") should be (1) - } - - it should "count an object declaration" in { - PublicApiCounter.countPublicApi("object A {}") should be (1) - } - - it should "count a trait declaration" in { - PublicApiCounter.countPublicApi("trait A {}") should be (1) - } - - it should "not count a private class declaration" in { - PublicApiCounter.countPublicApi("private class A {}") should be (0) - } - - it should "not count a private object declaration" in { - PublicApiCounter.countPublicApi("private object A {}") should be (0) - } - - it should "not count a private trait declaration" in { - PublicApiCounter.countPublicApi("private trait A {}") should be (0) - } - - it should "count an undocumented class declaration" in { - PublicApiCounter.countUndocumentedPublicApi("class A {}") should be (1) - } - - it should "not count a documented class declaration as undocumented one" in { - val source = """/** - * This is a comment of a public api member. - */ - class A {}""" - PublicApiCounter.countUndocumentedPublicApi(source) should be (0) - } - - it should "count an undocumented class declaration with package declaration before" in { - val source = """package a.b.c - - class A {}""" - PublicApiCounter.countUndocumentedPublicApi(source) should be (1) - } - - it should "not count a documented class declaration with package declaration before as undocumented one" in { - val source = """package a.b.c - - /** - * This is a comment of a public api member. - */ - class A {}""" - PublicApiCounter.countUndocumentedPublicApi(source) should be (0) - } - - it should "count all public api members of class and its undocumented ones" in { - val source = """package a.b.c - - /** - * This is a comment of a public api member. - */ - class A { - - /** - * Well, don't panic. ;-) - */ - val meaningOfLife = 42 - - val b = "test" - - def helloWorld { printString("Hello World!") } - - private def printString(str: String) { println(str) } - }""" - - PublicApiCounter.countPublicApi(source) should be (4) - PublicApiCounter.countUndocumentedPublicApi(source) should be (2) - } - - it should "not count nested function and method declarations" in { - val source ="""def test = { - def a = 12 + 1 - def b = 13 + 1 - - a + b + 42 - }""" - PublicApiCounter.countPublicApi(source) should be (1) - } - - it should "not count nested value declarations" in { - val source ="""val test = { - def a = 12 + 1 - def b = 13 + 1 - - a + b + 42 - }""" - PublicApiCounter.countPublicApi(source) should be (1) - } -} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/metrics/StatementCounterSpec.scala b/src/test/scala/com/buransky/plugins/scala/metrics/StatementCounterSpec.scala deleted file mode 100644 index c7d4ead..0000000 --- a/src/test/scala/com/buransky/plugins/scala/metrics/StatementCounterSpec.scala +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.metrics - -import org.junit.runner.RunWith -import org.scalatest.FlatSpec -import org.scalatest.matchers.ShouldMatchers -import org.scalatest.junit.JUnitRunner - -@RunWith(classOf[JUnitRunner]) -class StatementCounterSpec extends FlatSpec with ShouldMatchers { - - "A statement counter" should "count a simple assignment as a statement" in { - StatementCounter.countStatements("a = 1") should be (1) - } - - it should "count a simple method call as a statement" in { - StatementCounter.countStatements("println(123)") should be (1) - } - - it should "not count a simple variable declaration as a statement" in { - StatementCounter.countStatements("var a") should be (0) - } - - it should "count a simple variable declaration with assignment as a statement" in { - StatementCounter.countStatements("var a = 2") should be (1) - } - - it should "count a while loop as a statement" in { - StatementCounter.countStatements("while (1 == 1) {}") should be (1) - } - - it should "count a for loop as a statement" in { - StatementCounter.countStatements("for (i <- 1 to 10) {}") should be (1) - } - - it should "count a while loop as a statement and all statements in loop body" in { - val source = """ - while (1 == 1) { - val a = inc(2) - }""" - StatementCounter.countStatements(source) should be (2) - } - - it should "count a for loop as a statement and all statements in loop body" in { - val source = """ - for (i <- 1 to 10) { - val a = inc(2) - }""" - StatementCounter.countStatements(source) should be (2) - } - - it should "count if as a statement" in { - val source = """ - if (1 == 1) - println()""" - StatementCounter.countStatements(source) should be (2) - } - - it should "count an if block as a statement and all statements in its body" in { - val source = """ - if (1 + 2 < 4) { - val a = inc(2) - println(3) - def test = { 1 + 2 } - }""" - StatementCounter.countStatements(source) should be (4) - } - - it should "count a simple if else block as a statement" in { - val source = """ - if (1+2 < 4) - println("Hello World") - else - println("123")""" - StatementCounter.countStatements(source) should be (4) - } - - it should "count an if else block as a statement and all statements in its body" in { - val source = """ - if (1 + 2 < 4) { - val a = inc(2) - println("Hello World") - def test = 1 + 2 - } else { - def test2 = 1 - val b = test2 - }""" - StatementCounter.countStatements(source) should be (7) - } - - it should "count all statements in body of a function definition" in { - val source = """ - def test(i: Int) = { - val a = i + 42 - println(a) - println(i + 42) - a - }""" - StatementCounter.countStatements(source) should be (4) - } - - it should "count all statements in body of a value definition" in { - val source = """ - val test = { - val a = i + 42 - println(a) - println(i + 42) - a - }""" - StatementCounter.countStatements(source) should be (4) - } - - it should "count for comprehension with yield statement" in { - val source = "for (x <- List(1, 2, 3, 4, 5) if (x % 2 != 0)) yield x" - StatementCounter.countStatements(source) should be (2) - } - - it should "count for comprehension with more complex yield statement" in { - val source = "for (x <- List(1, 2, 3, 4, 5) if (x % 2 != 0)) yield x + inc(x)" - StatementCounter.countStatements(source) should be (2) - } - - it should "count for comprehension with yield statement where return value is only a literal" in { - val source = "for (x <- List(1, 2, 3, 4, 5) if (x % 2 != 0)) yield 2" - StatementCounter.countStatements(source) should be (2) - } - - it should "count foreach function call on a list as a statement" in { - val source = """ - myList.foreach {i => - println(i) - val a = i + 1 - println("inc: " + i) - }""" - StatementCounter.countStatements(source) should be (4) - } - - it should "count foreach function call and all statements in its body" in { - val source = """ - def foo() = { - List("Hello", "World", "!").foreach(word => - if (find(By(name, word)).isEmpty) - create.name(word).save - ) - }""" - StatementCounter.countStatements(source) should be (3) - } - - it should "count function call in a function definition nested in an object" in { - val source = """ - object name extends MappedPoliteString(this, 100) { - override def validations = valMinLen(1, S.?("attributeName")) _ :: Nil - }""" - StatementCounter.countStatements(source) should be (2) - } -} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/metrics/TypeCounterSpec.scala b/src/test/scala/com/buransky/plugins/scala/metrics/TypeCounterSpec.scala deleted file mode 100644 index 6415436..0000000 --- a/src/test/scala/com/buransky/plugins/scala/metrics/TypeCounterSpec.scala +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.metrics - -import org.junit.runner.RunWith -import org.scalatest.FlatSpec -import org.scalatest.matchers.ShouldMatchers -import org.scalatest.junit.JUnitRunner - -@RunWith(classOf[JUnitRunner]) -class TypeCounterSpec extends FlatSpec with ShouldMatchers { - - "A type counter" should "count type of a simple class declaration" in { - TypeCounter.countTypes("class A {}") should be (1) - } - - it should "count type of a simple object declaration" in { - TypeCounter.countTypes("object A {}") should be (1) - } - - it should "count type of a simple trait declaration" in { - TypeCounter.countTypes("trait A {}") should be (1) - } - - it should "count type of a simple case class declaration" in { - TypeCounter.countTypes("case class A {}") should be (1) - } - - it should "count type of a simple class declaration nested in a package" in { - val source = """ - package a.b - class A {}""" - TypeCounter.countTypes(source) should be (1) - } - - it should "count type of a simple class declaration nested in a package with imports" in { - val source = """ - package a.b - import java.util.List - class A {}""" - TypeCounter.countTypes(source) should be (1) - } - - it should "count type of a simple class declaration nested in a package with import and doc comment" in { - val source = """ - package a.b - import java.util.List - /** Doc comment... */ - class A {}""" - TypeCounter.countTypes(source) should be (1) - } - - it should "count type of a simple object declaration nested in a package" in { - val source = """ - package a.b - object A {}""" - TypeCounter.countTypes(source) should be (1) - } - - it should "count types of a simple class declarations" in { - val source = """ - class A {} - class B {}""" - TypeCounter.countTypes(source) should be (2) - } - - it should "count type of a simple class declaration nested in a class" in { - TypeCounter.countTypes("class A { class B {} }") should be (2) - } - - it should "count type of a simple class declaration nested in an object" in { - TypeCounter.countTypes("object A { class B {} }") should be (2) - } - - it should "count type of a simple object declaration nested in a class" in { - TypeCounter.countTypes("class A { object B {} }") should be (2) - } - - it should "count type of a simple object declaration nested in an object" in { - TypeCounter.countTypes("object A { object B {} }") should be (2) - } - - it should "count type of a simple class declaration nested in a function" in { - val source = """ - def fooBar(i: Int) = { - class B { val a = 1 } - i + new B().a - }""" - TypeCounter.countTypes(source) should be (1) - } - - it should "count type of a simple class declaration nested in a value definition" in { - val source = """ - val fooBar = { - class B { val a = 1 } - 1 + new B().a - }""" - TypeCounter.countTypes(source) should be (1) - } - - it should "count type of a simple class declaration nested in an assignment" in { - val source = """ - fooBar = { - class B { val a = 1 } - 1 + new B().a - }""" - TypeCounter.countTypes(source) should be (1) - } - - it should "count type of a simple class declaration nested in a code block" in { - val source = """ - { - 1 + new B().a - class B { val a = 1 } - }""" - TypeCounter.countTypes(source) should be (1) - } - - it should "count type of a simple class declaration nested in a loop" in { - val source = """ - var i = 0 - while (i == 2) { - i = i + new B().a - class B { val a = 1 } - }""" - TypeCounter.countTypes(source) should be (1) - } - - it should "count type of a simple class declaration nested in a match statement" in { - val source = """ - var i = 0 - i match { - case 0 => class B { val a = 1 } - case _ => - }""" - TypeCounter.countTypes(source) should be (1) - } - - it should "count type of a simple class declaration nested in a try statement" in { - val source = """ - try { - class B { val a = 1 } - } catch { - case _ => - }""" - TypeCounter.countTypes(source) should be (1) - } -} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scala/util/MetricDistributionSpec.scala b/src/test/scala/com/buransky/plugins/scala/util/MetricDistributionSpec.scala deleted file mode 100644 index 1402e4b..0000000 --- a/src/test/scala/com/buransky/plugins/scala/util/MetricDistributionSpec.scala +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.util - -import org.junit.runner.RunWith -import org.scalatest.FlatSpec -import org.scalatest.matchers.ShouldMatchers -import org.scalatest.junit.JUnitRunner -import org.sonar.api.measures.CoreMetrics - -@RunWith(classOf[JUnitRunner]) -class MetricDistributionSpec extends FlatSpec with ShouldMatchers { - - val metric = CoreMetrics.CLASS_COMPLEXITY_DISTRIBUTION - val ranges = Array[Number](1, 5, 10) - - "A metric distribution" should "increment occurence of value" in { - val distribution = new MetricDistribution(metric, ranges) - distribution.add(1.0) - distribution.getMeasure.getData should be ("1=1;5=0;10=0") - } - - it should "increment occurence of all values" in { - val distribution = new MetricDistribution(metric, ranges) - distribution.add(1.0) - distribution.add(10.0) - distribution.add(5.0) - distribution.getMeasure.getData should be ("1=1;5=1;10=1") - } - - it should "increase occurence of value by submitted number" in { - val distribution = new MetricDistribution(metric, ranges) - distribution.add(1.0, 3) - distribution.getMeasure.getData should be ("1=3;5=0;10=0") - } - - it should "increase occurence of all values by submitted number" in { - val distribution = new MetricDistribution(metric, ranges) - distribution.add(1.0, 3) - distribution.add(10.0, 8) - distribution.add(5.0, 2) - distribution.getMeasure.getData should be ("1=3;5=2;10=8") - } - - it should "increase occurence of value by submitted distribution" in { - val distribution = new MetricDistribution(metric, ranges) - distribution.add(1.0, 3) - - val otherDistribution = new MetricDistribution(metric, ranges) - otherDistribution.add(distribution) - - otherDistribution.getMeasure.getData should be ("1=3;5=0;10=0") - } - - it should "increase occurence of all values by submitted distribution" in { - val distribution = new MetricDistribution(metric, ranges) - distribution.add(1.0, 3) - distribution.add(10.0, 8) - distribution.add(5.0, 2) - - val otherDistribution = new MetricDistribution(metric, ranges) - otherDistribution.add(distribution) - - otherDistribution.getMeasure.getData should be ("1=3;5=2;10=8") - } - - it should "output an empty distribution properly" in { - val distribution = new MetricDistribution(metric, ranges) - distribution.getMeasure.getData should be ("1=0;5=0;10=0") - } - - it should "copy an empty distribution properly" in { - val distribution = new MetricDistribution(metric, ranges) - - val otherDistribution = new MetricDistribution(metric, ranges) - otherDistribution.add(distribution) - - otherDistribution.getMeasure.getData should be ("1=0;5=0;10=0") - } -} \ No newline at end of file From f2e9305369396280618c692b2acb7b1fefcb9ce7 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Tue, 4 Feb 2014 12:03:50 -0800 Subject: [PATCH 005/101] Min --- .../plugins/scala/cpd/ScalaTokenizer.java | 52 ------- .../plugins/scala/language/Comment.java | 120 --------------- .../plugins/scala/language/CommentType.java | 34 ----- .../plugins/scala/language/ScalaFile.java | 141 ------------------ .../plugins/scala/language/ScalaPackage.java | 90 ----------- .../scala/metrics/CommentsAnalyzer.java | 76 ---------- .../plugins/scala/util/StringUtils.java | 57 ------- .../plugins/scala/compiler/Compiler.scala | 39 ----- .../plugins/scala/compiler/Lexer.scala | 124 --------------- .../plugins/scala/compiler/Parser.scala | 62 -------- .../plugins/scala/compiler/Token.scala | 27 ---- .../plugins/scala/language/CodeDetector.scala | 68 --------- .../scala/language/PackageResolver.scala | 76 ---------- .../scala/metrics/ComplexityCalculator.scala | 115 -------------- .../scala/metrics/FunctionCounter.scala | 108 -------------- .../scala/metrics/PublicApiCounter.scala | 98 ------------ .../scala/metrics/StatementCounter.scala | 114 -------------- .../plugins/scala/metrics/TypeCounter.scala | 96 ------------ .../plugins/scala/metrics/package.scala | 68 --------- .../scala/util/MetricDistribution.scala | 48 ------ 20 files changed, 1613 deletions(-) delete mode 100644 src/main/java/com/buransky/plugins/scala/cpd/ScalaTokenizer.java delete mode 100644 src/main/java/com/buransky/plugins/scala/language/Comment.java delete mode 100644 src/main/java/com/buransky/plugins/scala/language/CommentType.java delete mode 100644 src/main/java/com/buransky/plugins/scala/language/ScalaFile.java delete mode 100644 src/main/java/com/buransky/plugins/scala/language/ScalaPackage.java delete mode 100644 src/main/java/com/buransky/plugins/scala/metrics/CommentsAnalyzer.java delete mode 100644 src/main/java/com/buransky/plugins/scala/util/StringUtils.java delete mode 100644 src/main/scala/com/buransky/plugins/scala/compiler/Compiler.scala delete mode 100644 src/main/scala/com/buransky/plugins/scala/compiler/Lexer.scala delete mode 100644 src/main/scala/com/buransky/plugins/scala/compiler/Parser.scala delete mode 100644 src/main/scala/com/buransky/plugins/scala/compiler/Token.scala delete mode 100644 src/main/scala/com/buransky/plugins/scala/language/CodeDetector.scala delete mode 100644 src/main/scala/com/buransky/plugins/scala/language/PackageResolver.scala delete mode 100644 src/main/scala/com/buransky/plugins/scala/metrics/ComplexityCalculator.scala delete mode 100644 src/main/scala/com/buransky/plugins/scala/metrics/FunctionCounter.scala delete mode 100644 src/main/scala/com/buransky/plugins/scala/metrics/PublicApiCounter.scala delete mode 100644 src/main/scala/com/buransky/plugins/scala/metrics/StatementCounter.scala delete mode 100644 src/main/scala/com/buransky/plugins/scala/metrics/TypeCounter.scala delete mode 100644 src/main/scala/com/buransky/plugins/scala/metrics/package.scala delete mode 100644 src/main/scala/com/buransky/plugins/scala/util/MetricDistribution.scala diff --git a/src/main/java/com/buransky/plugins/scala/cpd/ScalaTokenizer.java b/src/main/java/com/buransky/plugins/scala/cpd/ScalaTokenizer.java deleted file mode 100644 index 3a345bc..0000000 --- a/src/main/java/com/buransky/plugins/scala/cpd/ScalaTokenizer.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.cpd; - -import java.util.List; - -import net.sourceforge.pmd.cpd.SourceCode; -import net.sourceforge.pmd.cpd.TokenEntry; -import net.sourceforge.pmd.cpd.Tokenizer; -import net.sourceforge.pmd.cpd.Tokens; - -import org.sonar.plugins.scala.compiler.Lexer; -import org.sonar.plugins.scala.compiler.Token; - -/** - * Scala tokenizer for PMD CPD. - * - * @since 0.1 - */ -public final class ScalaTokenizer implements Tokenizer { - - public void tokenize(SourceCode source, Tokens cpdTokens) { - String filename = source.getFileName(); - - Lexer lexer = new Lexer(); - List tokens = lexer.getTokensOfFile(filename); - for (Token token : tokens) { - TokenEntry cpdToken = new TokenEntry(Integer.toString(token.tokenType()), filename, token.line()); - cpdTokens.add(cpdToken); - } - - cpdTokens.add(TokenEntry.getEOF()); - } - -} diff --git a/src/main/java/com/buransky/plugins/scala/language/Comment.java b/src/main/java/com/buransky/plugins/scala/language/Comment.java deleted file mode 100644 index 6f41e97..0000000 --- a/src/main/java/com/buransky/plugins/scala/language/Comment.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.language; - -import java.io.IOException; -import java.util.List; - -import com.buransky.plugins.scala.util.StringUtils; -import org.apache.commons.lang.builder.EqualsBuilder; -import org.apache.commons.lang.builder.HashCodeBuilder; -import org.apache.commons.lang.builder.ToStringBuilder; -import org.sonar.plugins.scala.language.CodeDetector; - -/** - * This class implements a Scala comment and the computation - * of several base metrics for a comment. - * - * @author Felix Müller - * @since 0.1 - */ -public class Comment { - - private final CommentType type; - private final List lines; - - public Comment(String content, CommentType type) throws IOException { - lines = StringUtils.convertStringToListOfLines(content); - this.type = type; - } - - public int getNumberOfLines() { - return lines.size() - getNumberOfBlankLines() - getNumberOfCommentedOutLinesOfCode(); - } - - public int getNumberOfBlankLines() { - int numberOfBlankLines = 0; - for (String comment : lines) { - boolean isBlank = true; - - for (int i = 0; isBlank && i < comment.length(); i++) { - char character = comment.charAt(i); - if (!Character.isWhitespace(character) && character != '*' && character != '/') { - isBlank = false; - } - } - - if (isBlank) { - numberOfBlankLines++; - } - } - return numberOfBlankLines; - } - - public int getNumberOfCommentedOutLinesOfCode() { - if (isDocComment()) { - return 0; - } - - int numberOfCommentedOutLinesOfCode = 0; - for (String line : lines) { - String strippedLine = org.apache.commons.lang.StringUtils.strip(line, " /*"); - if (CodeDetector.hasDetectedCode(strippedLine)) { - numberOfCommentedOutLinesOfCode++; - } - } - return numberOfCommentedOutLinesOfCode; - } - - public boolean isDocComment() { - return type == CommentType.DOC; - } - - public boolean isHeaderComment() { - return type == CommentType.HEADER; - } - - @Override - public int hashCode() { - return new HashCodeBuilder().append(type).append(lines).toHashCode(); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof Comment)) { - return false; - } - - Comment other = (Comment) obj; - return new EqualsBuilder().append(type, other.type).append(lines, other.lines).isEquals(); - } - - @Override - public String toString() { - final String firstLine = lines.isEmpty() ? "" : lines.get(0); - final String lastLine = lines.isEmpty() ? "" : lines.get(lines.size() - 1); - return new ToStringBuilder(this).append("type", type) - .append("firstLine", firstLine) - .append("lastLine", lastLine) - .append("numberOfLines", getNumberOfLines()) - .append("numberOfCommentedOutLinesOfCode", getNumberOfCommentedOutLinesOfCode()) - .toString(); - } -} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/language/CommentType.java b/src/main/java/com/buransky/plugins/scala/language/CommentType.java deleted file mode 100644 index 4784575..0000000 --- a/src/main/java/com/buransky/plugins/scala/language/CommentType.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.language; - -/** - * This enum is a helper to distinguish between the - * different types of comments in Sonar. - * - * @author Felix Müller - * @since 0.1 - */ -public enum CommentType { - - NORMAL, - DOC, - HEADER; -} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/language/ScalaFile.java b/src/main/java/com/buransky/plugins/scala/language/ScalaFile.java deleted file mode 100644 index 668e67e..0000000 --- a/src/main/java/com/buransky/plugins/scala/language/ScalaFile.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.language; - -import org.apache.commons.lang.StringUtils; -import org.sonar.api.resources.InputFile; -import org.sonar.api.resources.Language; -import org.sonar.api.resources.Qualifiers; -import org.sonar.api.resources.Resource; -import org.sonar.api.resources.Scopes; -import org.sonar.api.utils.WildcardPattern; -import org.sonar.plugins.scala.language.PackageResolver; - -/** - * This class implements a Scala source file for Sonar. - * - * @author Felix Müller - * @since 0.1 - */ -public class ScalaFile extends Resource { - - private final boolean isUnitTest; - private final String filename; - private final String longName; - private final ScalaPackage parent; - - public ScalaFile(String packageKey, String className, boolean isUnitTest) { - super(); - this.isUnitTest = isUnitTest; - filename = className.trim(); - - String key; - if (StringUtils.isBlank(packageKey)) { - packageKey = ScalaPackage.DEFAULT_PACKAGE_NAME; - key = new StringBuilder().append(packageKey).append(".").append(this.filename).toString(); - longName = filename; - } else { - packageKey = packageKey.trim(); - key = new StringBuilder().append(packageKey).append(".").append(this.filename).toString(); - longName = key; - } - parent = new ScalaPackage(packageKey); - setKey(key); - } - - @Override - public String getName() { - return filename; - } - - @Override - public String getLongName() { - return longName; - } - - @Override - public String getDescription() { - return null; - } - - @Override - public Language getLanguage() { - return Scala.INSTANCE; - } - - @Override - public String getScope() { - return Scopes.FILE; - } - - @Override - public String getQualifier() { - return isUnitTest ? Qualifiers.UNIT_TEST_FILE : Qualifiers.FILE; - } - - @Override - public ScalaPackage getParent() { - return parent; - } - - @Override - public boolean matchFilePattern(String antPattern) { - final String patternWithoutFileSuffix = StringUtils.substringBeforeLast(antPattern, "."); - final WildcardPattern matcher = WildcardPattern.create(patternWithoutFileSuffix, "."); - return matcher.match(getKey()); - } - - public boolean isUnitTest() { - return isUnitTest; - } - - /** - * Shortcut for {@link #fromInputFile(InputFile, boolean)} for source files. - */ - public static ScalaFile fromInputFile(InputFile inputFile) { - return ScalaFile.fromInputFile(inputFile, false); - } - - /** - * Creates a {@link ScalaFile} from a file in the source directories. - * - * @param inputFile the file object with relative path - * @param isUnitTest whether it is a unit test file or a source file - * @return the {@link ScalaFile} created if exists, null otherwise - */ - public static ScalaFile fromInputFile(InputFile inputFile, boolean isUnitTest) { - if (inputFile == null || inputFile.getFile() == null || inputFile.getRelativePath() == null) { - return null; - } - - final String packageName = PackageResolver.resolvePackageNameOfFile( - inputFile.getFile().getAbsolutePath()); - final String className = resolveClassName(inputFile); - return new ScalaFile(packageName, className, isUnitTest); - } - - private static String resolveClassName(InputFile inputFile) { - String classname = inputFile.getRelativePath(); - if (inputFile.getRelativePath().indexOf('/') >= 0) { - classname = StringUtils.substringAfterLast(inputFile.getRelativePath(), "/"); - } - return StringUtils.substringBeforeLast(classname, "."); - } -} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/language/ScalaPackage.java b/src/main/java/com/buransky/plugins/scala/language/ScalaPackage.java deleted file mode 100644 index 67fa99d..0000000 --- a/src/main/java/com/buransky/plugins/scala/language/ScalaPackage.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.language; - -import org.apache.commons.lang.StringUtils; -import org.sonar.api.resources.Language; -import org.sonar.api.resources.Qualifiers; -import org.sonar.api.resources.Resource; -import org.sonar.api.resources.Scopes; -import org.sonar.api.utils.WildcardPattern; - -/** - * This class implements a logical Scala package. - * - * @author Felix Müller - * @since 0.1 - */ -@SuppressWarnings("rawtypes") -public class ScalaPackage extends Resource { - - public static final String DEFAULT_PACKAGE_NAME = "[default]"; - - public ScalaPackage() { - this(null); - } - - public ScalaPackage(String key) { - super(); - setKey(StringUtils.defaultIfEmpty(StringUtils.trim(key), DEFAULT_PACKAGE_NAME)); - } - - @Override - public String getName() { - return getKey(); - } - - @Override - public String getLongName() { - return null; - } - - @Override - public String getDescription() { - return null; - } - - @Override - public Language getLanguage() { - return Scala.INSTANCE; - } - - @Override - public String getScope() { - return Scopes.DIRECTORY; - } - - @Override - public String getQualifier() { - return Qualifiers.PACKAGE; - } - - @Override - public Resource getParent() { - return null; - } - - @Override - public boolean matchFilePattern(String antPattern) { - String patternWithoutFileSuffix = StringUtils.substringBeforeLast(antPattern, "."); - WildcardPattern matcher = WildcardPattern.create(patternWithoutFileSuffix, "."); - return matcher.match(getKey()); - } -} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/metrics/CommentsAnalyzer.java b/src/main/java/com/buransky/plugins/scala/metrics/CommentsAnalyzer.java deleted file mode 100644 index f609140..0000000 --- a/src/main/java/com/buransky/plugins/scala/metrics/CommentsAnalyzer.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.metrics; - -import java.util.List; - -import com.buransky.plugins.scala.language.Comment; - -/** - * This class implements the computation of basic - * line metrics for a {@link Comment}. - * - * @author Felix Müller - * @since 0.1 - */ -public class CommentsAnalyzer { - - private final List comments; - - public CommentsAnalyzer(List comments) { - this.comments = comments; - } - - public int countCommentLines() { - int commentLines = 0; - for (Comment comment : comments) { - if (!comment.isHeaderComment()) { - commentLines += comment.getNumberOfLines(); - } - } - return commentLines; - } - - public int countHeaderCommentLines() { - int headerCommentLines = 0; - for (Comment comment : comments) { - if (comment.isHeaderComment()) { - headerCommentLines += comment.getNumberOfLines(); - } - } - return headerCommentLines; - } - - public int countCommentedOutLinesOfCode() { - int commentedOutLinesOfCode = 0; - for (Comment comment : comments) { - commentedOutLinesOfCode += comment.getNumberOfCommentedOutLinesOfCode(); - } - return commentedOutLinesOfCode; - } - - public int countBlankCommentLines() { - int blankCommentLines = 0; - for (Comment comment : comments) { - blankCommentLines += comment.getNumberOfBlankLines(); - } - return blankCommentLines; - } -} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/util/StringUtils.java b/src/main/java/com/buransky/plugins/scala/util/StringUtils.java deleted file mode 100644 index 7e32d81..0000000 --- a/src/main/java/com/buransky/plugins/scala/util/StringUtils.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.util; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.StringReader; -import java.util.ArrayList; -import java.util.List; - -import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public final class StringUtils { - - private static final Logger LOGGER = LoggerFactory.getLogger(StringUtils.class); - - private StringUtils() { - // to prevent instantiation - } - - public static List convertStringToListOfLines(String string) throws IOException { - final List lines = new ArrayList(); - BufferedReader reader = null; - try { - reader = new BufferedReader(new StringReader(string)); - String line = null; - while ((line = reader.readLine()) != null) { - lines.add(line); - } - } catch (IOException ioe) { - LOGGER.error("Error while reading the lines of a given string", ioe); - throw ioe; - } finally { - IOUtils.closeQuietly(reader); - } - return lines; - } -} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/compiler/Compiler.scala b/src/main/scala/com/buransky/plugins/scala/compiler/Compiler.scala deleted file mode 100644 index 81cc60c..0000000 --- a/src/main/scala/com/buransky/plugins/scala/compiler/Compiler.scala +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.compiler - -import tools.nsc._ -import tools.util.PathResolver._ -import com.buransky.plugins.scala.ScalaPlugin - -/** - * This is a wrapper for the Scala compiler. It is used to access - * the compiler in a more convenient way. - * - * @author Felix Müller - * @since 0.1 - */ -object Compiler extends Global(new Settings()) { - - settings.classpath.append(ScalaPlugin.getPathToScalaLibrary()) - new Run - - override def forScaladoc = true -} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/compiler/Lexer.scala b/src/main/scala/com/buransky/plugins/scala/compiler/Lexer.scala deleted file mode 100644 index b50b1f0..0000000 --- a/src/main/scala/com/buransky/plugins/scala/compiler/Lexer.scala +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.compiler - -import collection.JavaConversions._ -import collection.mutable.ListBuffer -import tools.nsc._ -import io.AbstractFile - -import com.buransky.plugins.scala.language.{CommentType, Comment} - -/** - * This class is a wrapper for accessing the lexer of the Scala compiler - * from Java in a more convenient way. - * - * @author Felix Müller - * @since 0.1 - */ -class Lexer { - - import Compiler._ - - def getTokens(code: String) : java.util.List[Token] = { - val unit = new CompilationUnit(new util.BatchSourceFile("", code.toCharArray)) - tokenize(unit) - } - - def getTokensOfFile(path: String) : java.util.List[Token] = { - val unit = new CompilationUnit(new util.BatchSourceFile(AbstractFile.getFile(path))) - tokenize(unit) - } - - private def tokenize(unit: CompilationUnit) : java.util.List[Token] = { - val scanner = new syntaxAnalyzer.UnitScanner(unit) - val tokens = ListBuffer[Token]() - - scanner.init() - while (scanner.token != scala.tools.nsc.ast.parser.Tokens.EOF) { - tokens += Token(scanner.token, scanner.parensAnalyzer.line(scanner.offset) + 1) - scanner.nextToken() - } - tokens - } - - def getComments(code: String) : java.util.List[Comment] = { - val unit = new CompilationUnit(new util.BatchSourceFile("", code.toCharArray)) - tokenizeComments(unit) - } - - def getCommentsOfFile(path: String) : java.util.List[Comment] = { - val unit = new CompilationUnit(new util.BatchSourceFile(AbstractFile.getFile(path))) - tokenizeComments(unit) - } - - private def tokenizeComments(unit: CompilationUnit) : java.util.List[Comment] = { - val comments = ListBuffer[Comment]() - val scanner = new syntaxAnalyzer.UnitScanner(unit) { - - private var lastDocCommentRange: Option[Range] = None - - private var foundToken = false - - override def nextToken() { - super.nextToken() - foundToken = token != 0 - } - - override def foundComment(value: String, start: Int, end: Int) = { - super.foundComment(value, start, end) - - def isHeaderComment(value: String) = { - !foundToken && comments.isEmpty && value.trim().startsWith("/*") - } - - lastDocCommentRange match { - - case Some(r: Range) => { - if (r.start != start || r.end != end) { - comments += new Comment(value, CommentType.NORMAL) - } - } - - case None => { - if (isHeaderComment(value)) { - comments += new Comment(value, CommentType.HEADER) - } else { - comments += new Comment(value, CommentType.NORMAL) - } - } - } - } - - override def foundDocComment(value: String, start: Int, end: Int) = { - super.foundDocComment(value, start, end) - comments += new Comment(value, CommentType.DOC) - lastDocCommentRange = Some(Range(start, end)) - } - } - - scanner.init() - while (scanner.token != scala.tools.nsc.ast.parser.Tokens.EOF) { - scanner.nextToken() - } - - comments - } -} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/compiler/Parser.scala b/src/main/scala/com/buransky/plugins/scala/compiler/Parser.scala deleted file mode 100644 index 6b8723b..0000000 --- a/src/main/scala/com/buransky/plugins/scala/compiler/Parser.scala +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.compiler - -import tools.nsc._ -import io.AbstractFile - -/** - * This class is a wrapper for accessing the parser of the Scala compiler - * from Java in a more convenient way. - * - * @author Felix Müller - * @since 0.1 - */ -class Parser { - - import Compiler._ - - def parse(code: String) : Tree = { - val batchSourceFile = new util.BatchSourceFile("", code.toCharArray) - parse(batchSourceFile, code.toCharArray) - } - - def parseFile(path: String) = { - val batchSourceFile = new util.BatchSourceFile(AbstractFile.getFile(path)) - parse(batchSourceFile, batchSourceFile.content.array) - } - - private def parse(batchSourceFile: util.BatchSourceFile, code: Array[Char]) = { - val scriptSourceFile = new util.ScriptSourceFile(batchSourceFile, code, 0) - try { - val parser = new syntaxAnalyzer.SourceFileParser(scriptSourceFile) - val tree = parser.templateStatSeq(false)._2 - parser.makePackaging(0, parser.atPos(0, 0, 0)(Ident(nme.EMPTY_PACKAGE_NAME)), tree) - } catch { - case _ => { - val unit = new CompilationUnit(batchSourceFile) - val unitParser = new syntaxAnalyzer.UnitParser(unit) { - override def showSyntaxErrors() { } - } - unitParser.smartParse() - } - } - } -} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/compiler/Token.scala b/src/main/scala/com/buransky/plugins/scala/compiler/Token.scala deleted file mode 100644 index e8fc0bb..0000000 --- a/src/main/scala/com/buransky/plugins/scala/compiler/Token.scala +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.compiler - -/** - * Represent a token. Lines must start at 1. - * - * @since 0.1 - */ -case class Token(tokenType: Int, line: Int) diff --git a/src/main/scala/com/buransky/plugins/scala/language/CodeDetector.scala b/src/main/scala/com/buransky/plugins/scala/language/CodeDetector.scala deleted file mode 100644 index c30969b..0000000 --- a/src/main/scala/com/buransky/plugins/scala/language/CodeDetector.scala +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.language - -import scala.tools.nsc.symtab.StdNames - -import org.sonar.plugins.scala.compiler.{ Compiler, Parser } - -/** - * This object is a helper object for detecting valid Scala code - * in a given piece of source code. - * - * @author Felix Müller - * @since 0.1 - */ -object CodeDetector { - - import Compiler._ - - private lazy val parser = new Parser() - - def hasDetectedCode(code: String) = { - - def lookingForSyntaxTreesWithCode(tree: Tree) : Boolean = tree match { - - case PackageDef(identifier: RefTree, content) => - if (!identifier.name.equals(nme.EMPTY_PACKAGE_NAME)) { - true - } else { - content.exists(lookingForSyntaxTreesWithCode) - } - - case Apply(function, args) => - args.exists(lookingForSyntaxTreesWithCode) - - case ClassDef(_, _, _, _) - | ModuleDef(_, _, _) - | ValDef(_, _, _, _) - | DefDef(_, _, _, _, _, _) - | Function(_ , _) - | Assign(_, _) - | LabelDef(_, _, _) => - true - - case _ => - false - } - - lookingForSyntaxTreesWithCode(parser.parse(code)) - } -} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/language/PackageResolver.scala b/src/main/scala/com/buransky/plugins/scala/language/PackageResolver.scala deleted file mode 100644 index 76e7be1..0000000 --- a/src/main/scala/com/buransky/plugins/scala/language/PackageResolver.scala +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.language - -import org.sonar.plugins.scala.compiler.{ Compiler, Parser } - -/** - * This object is a helper object for resolving the package name of - * a given Scala file. - * - * @author Felix Müller - * @since 0.1 - */ -object PackageResolver { - - import Compiler._ - - private lazy val parser = new Parser() - - /** - * This function resolves the upper package name of a given file. - * - * @param path the path of the given file - * @return the upper package name - */ - def resolvePackageNameOfFile(path: String) : String = { - - def traversePackageDefs(tree: Tree) : Seq[String] = tree match { - - case PackageDef(Ident(name), List(p: PackageDef)) => - List(name.toString()) ++ traversePackageDefs(p) - - case PackageDef(s: Select, List(p: PackageDef)) => - traversePackageDefs(s) ++ traversePackageDefs(p) - - case PackageDef(Ident(name), _) => - List(name.toString()) - - case PackageDef(s: Select, _) => - traversePackageDefs(s) - - case Select(Ident(identName), name) => - List(identName.toString(), name.toString()) - - case Select(qualifiers, name) => - traversePackageDefs(qualifiers) ++ List(name.toString()) - - case _ => - Nil - } - - val packageName = traversePackageDefs(parser.parseFile(path)).foldLeft("")(_ + "." + _) - if (packageName.length() > 0) { - packageName.substring(1) - } else { - packageName - } - } -} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/metrics/ComplexityCalculator.scala b/src/main/scala/com/buransky/plugins/scala/metrics/ComplexityCalculator.scala deleted file mode 100644 index 31ae409..0000000 --- a/src/main/scala/com/buransky/plugins/scala/metrics/ComplexityCalculator.scala +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.metrics - -import collection.mutable.{ ListBuffer, HashMap } - -import org.sonar.api.measures.{ CoreMetrics, Measure, Metric } -import org.sonar.plugins.scala.util.MetricDistribution - -import scalariform.lexer.Tokens._ -import scalariform.parser._ - -/** - * This object is a helper object for measuring complexity - * in a given Scala source. - * - * @author Felix Müller - * @since 0.1 - */ -object ComplexityCalculator { - - private lazy val classComplexityRanges = Array[Number](0, 5, 10, 20, 30, 60, 90) - private lazy val functionComplexityRanges = Array[Number](1, 2, 4, 6, 8, 10, 12) - - def measureComplexity(source: String) : Int = ScalaParser.parse(source) match { - case Some(ast) => measureComplexity(ast) - case _ => 0 - } - - def measureComplexityOfClasses(source: String) : MetricDistribution = { - measureComplexityDistribution(source, CoreMetrics.CLASS_COMPLEXITY_DISTRIBUTION, - classComplexityRanges, classOf[TmplDef]) - } - - def measureComplexityOfFunctions(source: String) : MetricDistribution = { - measureComplexityDistribution(source, CoreMetrics.FUNCTION_COMPLEXITY_DISTRIBUTION, - functionComplexityRanges, classOf[FunDefOrDcl]) - } - - private def measureComplexityDistribution(source: String, metric: Metric, ranges: Array[Number], - typeOfTree: Class[_ <: AstNode]) = { - - def allTreesIn(source: String) : Seq[AstNode] = ScalaParser.parse(source) match { - case Some(ast) => collectTrees(ast, typeOfTree) - case _ => Nil - } - - val distribution = new MetricDistribution(metric, ranges) - allTreesIn(source).foreach(ast => distribution.add(measureComplexity(ast))) - distribution - } - - private def measureComplexity(ast: AstNode) : Int = { - var complexity = 0 - - // TODO measure complexity of return statements - // TODO howto handle nested classes and functions? should - // surrounding function complexity consist of inner function and its own or only it own one? - def measureComplexityOfTree(tree: AstNode) { - tree match { - - case CaseClause(_, _) - | DoExpr(_, _, _, _, _) - | ForExpr(_, _, _, _, _, _, _) - | FunDefOrDcl(_, _, _, _, _, _, _) - | IfExpr(_, _, _, _, _) - | WhileExpr(_, _, _, _) => - complexity += 1 - - case expr: Expr => - if (expr.tokens.head.tokenType == THROW) { - complexity += 1 - } - - case _ => - } - - tree.immediateChildren.foreach(measureComplexityOfTree) - } - - measureComplexityOfTree(ast) - complexity - } - - private def collectTrees(ast: AstNode, typeOfTree: Class[_ <: AstNode]) : Seq[AstNode] = { - val nodes = ListBuffer[AstNode]() - - def collectTreesOfSpecificType(tree: AstNode) { - if (tree.getClass == typeOfTree) { - nodes += tree - } - tree.immediateChildren.foreach(collectTreesOfSpecificType) - } - - collectTreesOfSpecificType(ast) - nodes - } -} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/metrics/FunctionCounter.scala b/src/main/scala/com/buransky/plugins/scala/metrics/FunctionCounter.scala deleted file mode 100644 index 92bfac4..0000000 --- a/src/main/scala/com/buransky/plugins/scala/metrics/FunctionCounter.scala +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.metrics - -import org.sonar.plugins.scala.compiler.{ Compiler, Parser } - -/** - * This object is a helper object for counting all functions - * in a given Scala source. - * - * @author Felix Müller - * @since 0.1 - */ -object FunctionCounter { - - import Compiler._ - - private lazy val parser = new Parser() - - // TODO improve counting functions - def countFunctions(source: String) = { - - def countFunctionTrees(tree: Tree, foundFunctions: Int = 0) : Int = tree match { - - // recursive descent until found a syntax tree with countable functions - case PackageDef(_, content) => - foundFunctions + onList(content, countFunctionTrees(_, 0)) - - case Template(_, _, content) => - foundFunctions + onList(content, countFunctionTrees(_, 0)) - - case ClassDef(_, _, _, content) => - countFunctionTrees(content, foundFunctions) - - case ModuleDef(_, _, content) => - countFunctionTrees(content, foundFunctions) - - case DocDef(_, content) => - countFunctionTrees(content, foundFunctions) - - case ValDef(_, _, _, content) => - countFunctionTrees(content, foundFunctions) - - case Block(stats, expr) => - foundFunctions + onList(stats, countFunctionTrees(_, 0)) + countFunctionTrees(expr) - - case Apply(_, args) => - foundFunctions + onList(args, countFunctionTrees(_, 0)) - - case Assign(_, rhs) => - countFunctionTrees(rhs, foundFunctions) - - case LabelDef(_, _, rhs) => - countFunctionTrees(rhs, foundFunctions) - - case If(cond, thenBlock, elseBlock) => - foundFunctions + countFunctionTrees(cond) + countFunctionTrees(thenBlock) + countFunctionTrees(elseBlock) - - case Match(selector, cases) => - foundFunctions + countFunctionTrees(selector) + onList(cases, countFunctionTrees(_, 0)) - - case CaseDef(pat, guard, body) => - foundFunctions + countFunctionTrees(pat) + countFunctionTrees(guard) + countFunctionTrees(body) - - case Try(block, catches, finalizer) => - foundFunctions + countFunctionTrees(block) + onList(catches, countFunctionTrees(_, 0)) - - case Throw(expr) => - countFunctionTrees(expr, foundFunctions) - - case Function(_, body) => - countFunctionTrees(body, foundFunctions) - - /* - * Countable function declarations are functions and methods. - */ - - case defDef: DefDef => - if (isEmptyConstructor(defDef)) { - countFunctionTrees(defDef.rhs, foundFunctions) - } else { - countFunctionTrees(defDef.rhs, foundFunctions + 1) - } - - case _ => - foundFunctions - } - - countFunctionTrees(parser.parse(source)) - } -} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/metrics/PublicApiCounter.scala b/src/main/scala/com/buransky/plugins/scala/metrics/PublicApiCounter.scala deleted file mode 100644 index 8369d29..0000000 --- a/src/main/scala/com/buransky/plugins/scala/metrics/PublicApiCounter.scala +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.metrics - -import reflect.generic.ModifierFlags -import org.sonar.plugins.scala.compiler.{ Compiler, Parser } - -/** - * This object is a helper object for counting public api members. - * - * @author Felix Müller - * @since 0.1 - */ -object PublicApiCounter { - - import Compiler._ - - private lazy val parser = new Parser() - - private case class PublicApi(isDocumented: Boolean) - - def countPublicApi(source: String) = { - countPublicApiTrees(parser.parse(source)).size - } - - def countUndocumentedPublicApi(source: String) = { - countPublicApiTrees(parser.parse(source)).count(!_.isDocumented) - } - - private def countPublicApiTrees(tree: Tree, wasDocDefBefore: Boolean = false, - foundPublicApiMembers: List[PublicApi] = Nil) : List[PublicApi] = tree match { - - // recursive descent until found a syntax tree with countable public api declarations - case PackageDef(_, content) => - foundPublicApiMembers ++ content.flatMap(countPublicApiTrees(_, false, Nil)) - - case Template(_, _, content) => - foundPublicApiMembers ++ content.flatMap(countPublicApiTrees(_, false, Nil)) - - case DocDef(_, content) => - countPublicApiTrees(content, true, foundPublicApiMembers) - - case Block(stats, expr) => - foundPublicApiMembers ++ stats.flatMap(countPublicApiTrees(_, false, Nil)) ++ countPublicApiTrees(expr) - - case Apply(_, args) => - foundPublicApiMembers ++ args.flatMap(countPublicApiTrees(_, false, Nil)) - - case classDef: ClassDef if (classDef.mods.hasFlag(ModifierFlags.PRIVATE)) => - countPublicApiTrees(classDef.impl, false, foundPublicApiMembers) - - case moduleDef: ModuleDef if (moduleDef.mods.hasFlag(ModifierFlags.PRIVATE)) => - countPublicApiTrees(moduleDef.impl, false, foundPublicApiMembers) - - case defDef: DefDef if (isEmptyConstructor(defDef) || defDef.mods.hasFlag(ModifierFlags.PRIVATE)) => - countPublicApiTrees(defDef.rhs, false, foundPublicApiMembers) - - case valDef: ValDef if (valDef.mods.hasFlag(ModifierFlags.PRIVATE)) => - countPublicApiTrees(valDef.rhs, false, foundPublicApiMembers) - - /* - * Countable public api declarations are classes, objects, traits, functions, - * methods and attributes with public access. - */ - - case ClassDef(_, _, _, impl) => - countPublicApiTrees(impl, false, foundPublicApiMembers ++ List(PublicApi(wasDocDefBefore))) - - case ModuleDef(_, _, impl) => - countPublicApiTrees(impl, false, foundPublicApiMembers ++ List(PublicApi(wasDocDefBefore))) - - case defDef: DefDef => - foundPublicApiMembers ++ List(PublicApi(wasDocDefBefore)) - - case valDef: ValDef => - foundPublicApiMembers ++ List(PublicApi(wasDocDefBefore)) - - case _ => - foundPublicApiMembers - } -} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/metrics/StatementCounter.scala b/src/main/scala/com/buransky/plugins/scala/metrics/StatementCounter.scala deleted file mode 100644 index 7502547..0000000 --- a/src/main/scala/com/buransky/plugins/scala/metrics/StatementCounter.scala +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.metrics - -import scalariform.parser._ - -/** - * This object is a helper object for counting all statements - * in a given Scala source. - * - * @author Felix Müller - * @since 0.1 - */ -object StatementCounter { - - def countStatements(source: String) = { - - def countStatementTreesOnList(trees: List[AstNode]) : Int = { - trees.map(countStatementTrees(_)).foldLeft(0)(_ + _) - } - - def countStatementsOfDefOrDcl(body: AstNode) : Int = { - val bodyStatementCount = countStatementTrees(body) - if (bodyStatementCount == 0) { - 1 - } else { - bodyStatementCount - } - } - - def countStatementTrees(tree: AstNode, foundStatements: Int = 0) : Int = tree match { - - case AnonymousFunction(_, _, body) => - foundStatements + countStatementTrees(body) - - case FunDefOrDcl(_, _, _, _, _, funBodyOption, _) => - funBodyOption match { - case Some(funBody) => - foundStatements + countStatementsOfDefOrDcl(funBody) - case _ => - foundStatements - } - - case PatDefOrDcl(_, _, _, _, equalsClauseOption) => - equalsClauseOption match { - case Some(equalsClause) => - foundStatements + countStatementsOfDefOrDcl(equalsClause._2) - case _ => - foundStatements - } - - case ForExpr(_, _, _, _, _, yieldOption, body) => - val bodyStatementCount = countStatementTrees(body) - yieldOption match { - case Some(_) => - if (bodyStatementCount == 0) { - foundStatements + 2 - } else { - foundStatements + bodyStatementCount + 1 - } - - case _ => - foundStatements + bodyStatementCount + 1 - } - - case IfExpr(_, _, _, body, elseClauseOption) => - elseClauseOption match { - case Some(elseClause) => - foundStatements + 1 + countStatementTrees(body) + countStatementTrees(elseClause) - case _ => - foundStatements + 1 + countStatementTrees(body) - } - - case ElseClause(_, _, elseBody) => - countStatementTrees(elseBody, foundStatements + 1) - - case CallExpr(exprDotOpt, _, _, newLineOptsAndArgumentExpr, _) => - val bodyStatementCount = countStatementTreesOnList(newLineOptsAndArgumentExpr.map(_._2)) - if (bodyStatementCount > 1) { - foundStatements + 1 + bodyStatementCount - } else { - foundStatements + 1 - } - - case InfixExpr(_, _, _, _) | PostfixExpr(_, _) => - foundStatements + 1 - - case _ => - foundStatements + countStatementTreesOnList(tree.immediateChildren) - } - - ScalaParser.parse(source) match { - case Some(ast) => countStatementTrees(ast) - case _ => 0 - } - } -} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/metrics/TypeCounter.scala b/src/main/scala/com/buransky/plugins/scala/metrics/TypeCounter.scala deleted file mode 100644 index aef7266..0000000 --- a/src/main/scala/com/buransky/plugins/scala/metrics/TypeCounter.scala +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.metrics - -import org.sonar.plugins.scala.compiler.{ Compiler, Parser } - -/** - * This object is a helper object for counting all types - * in a given Scala source. - * - * @author Felix Müller - * @since 0.1 - */ -object TypeCounter { - - import Compiler._ - - private lazy val parser = new Parser() - - def countTypes(source: String) = { - - def countTypeTrees(tree: Tree, foundTypes: Int = 0) : Int = tree match { - - // recursive descent until found a syntax tree with countable type declaration - case PackageDef(_, content) => - foundTypes + onList(content, countTypeTrees(_, 0)) - - case Template(_, _, content) => - foundTypes + onList(content, countTypeTrees(_, 0)) - - case DocDef(_, content) => - countTypeTrees(content, foundTypes) - - case CaseDef(pat, guard, body) => - foundTypes + countTypeTrees(pat) + countTypeTrees(guard) + countTypeTrees(body) - - case DefDef(_, _, _, _, _, content) => - countTypeTrees(content, foundTypes) - - case ValDef(_, _, _, content) => - countTypeTrees(content, foundTypes) - - case Assign(_, rhs) => - countTypeTrees(rhs, foundTypes) - - case LabelDef(_, _, rhs) => - countTypeTrees(rhs, foundTypes) - - case If(cond, thenBlock, elseBlock) => - foundTypes + countTypeTrees(cond) + countTypeTrees(thenBlock) + countTypeTrees(elseBlock) - - case Block(stats, expr) => - foundTypes + onList(stats, countTypeTrees(_, 0)) + countTypeTrees(expr) - - case Match(selector, cases) => - foundTypes + countTypeTrees(selector) + onList(cases, countTypeTrees(_, 0)) - - case Try(block, catches, finalizer) => - foundTypes + countTypeTrees(block) + onList(catches, countTypeTrees(_, 0)) + countTypeTrees(finalizer) - - /* - * Countable type declarations are classes, traits and objects. - * ClassDef represents classes and traits. - * ModuleDef is the syntax tree for object declarations. - */ - - case ClassDef(_, _, _, content) => - countTypeTrees(content, foundTypes + 1) - - case ModuleDef(_, _, content) => - countTypeTrees(content, foundTypes + 1) - - case _ => - foundTypes - } - - countTypeTrees(parser.parse(source)) - } -} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/metrics/package.scala b/src/main/scala/com/buransky/plugins/scala/metrics/package.scala deleted file mode 100644 index 08722dd..0000000 --- a/src/main/scala/com/buransky/plugins/scala/metrics/package.scala +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala - -import org.sonar.plugins.scala.compiler.Compiler._ - -package object metrics { - - def isEmptyBlock(block: Tree) = block match { - case literal: Literal => - val isEmptyConstant = literal.value match { - case Constant(value) => value.toString().equals("()") - case _ => false - } - literal.isEmpty || isEmptyConstant - - case _ => block.isEmpty - } - - def isEmptyConstructor(constructor: DefDef) = { - if (constructor.name.startsWith(nme.CONSTRUCTOR) || - constructor.name.startsWith(nme.MIXIN_CONSTRUCTOR)) { - - constructor.rhs match { - - case Block(stats, expr) => - if (stats.size == 0) { - true - } else { - stats.size == 1 && - (stats(0).toString().startsWith("super." + nme.CONSTRUCTOR) || - stats(0).toString().startsWith("super." + nme.MIXIN_CONSTRUCTOR)) && - isEmptyBlock(expr) - } - - case _ => - constructor.isEmpty - } - } else { - false - } - } - - /** - * Helper function which applies a function on every AST in a given list and - * sums up the results. - */ - def onList(trees: List[Tree], treeFunction: Tree => Int) = { - trees.map(treeFunction).foldLeft(0)(_ + _) - } -} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scala/util/MetricDistribution.scala b/src/main/scala/com/buransky/plugins/scala/util/MetricDistribution.scala deleted file mode 100644 index b3c9566..0000000 --- a/src/main/scala/com/buransky/plugins/scala/util/MetricDistribution.scala +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package org.sonar.plugins.scala.util - -import collection.immutable.TreeMap - -import org.sonar.api.measures.{ Metric, RangeDistributionBuilder } - -class MetricDistribution(metric: Metric, ranges: Array[Number]) { - - var distribution = TreeMap[Double, Int]() - - def add(value: Double) { - add(value, 1) - } - - def add(value: Double, count: Int) { - val oldValue = distribution.getOrElse(value, 0) - distribution = distribution.updated(value, oldValue + count) - } - - def add(metricDistribution: MetricDistribution) { - metricDistribution.distribution.foreach(entry => add(entry._1, entry._2)) - } - - def getMeasure() = { - val rangeDistribution = new RangeDistributionBuilder(metric, ranges) - distribution.foreach(entry => rangeDistribution.add(entry._1, entry._2)) - rangeDistribution.build - } -} \ No newline at end of file From 339e4a797bd3800efe06f96eeb7669e3244a9b04 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Tue, 4 Feb 2014 12:18:30 -0800 Subject: [PATCH 006/101] . --- pom.xml | 2 +- .../buransky/plugins/scala/ScalaPlugin.java | 66 ------------------- .../plugins/scala/language/Scala.java | 41 ------------ .../plugins/scoverage/ScoveragePlugin.java | 27 ++++++++ .../plugins/scoverage/language/Scala.java | 16 +++++ .../language/ScalaFile.java} | 47 +++---------- .../sensor/AbstractScalaSensor.java | 4 +- .../sensor/ScalaSourceImporterSensor.java | 35 ++-------- .../sensor/ScoverageSensor.java} | 18 ++--- 9 files changed, 68 insertions(+), 188 deletions(-) delete mode 100644 src/main/java/com/buransky/plugins/scala/ScalaPlugin.java delete mode 100644 src/main/java/com/buransky/plugins/scala/language/Scala.java create mode 100644 src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java create mode 100644 src/main/java/com/buransky/plugins/scoverage/language/Scala.java rename src/main/java/com/buransky/plugins/{scala/language/ScalaRealFile.java => scoverage/language/ScalaFile.java} (50%) rename src/main/java/com/buransky/plugins/{scala => scoverage}/sensor/AbstractScalaSensor.java (90%) rename src/main/java/com/buransky/plugins/{scala => scoverage}/sensor/ScalaSourceImporterSensor.java (61%) rename src/main/java/com/buransky/plugins/{scala/cobertura/CoberturaSensor.java => scoverage/sensor/ScoverageSensor.java} (85%) diff --git a/pom.xml b/pom.xml index 4f86df7..699c885 100644 --- a/pom.xml +++ b/pom.xml @@ -76,7 +76,7 @@ scoverage Scoverage - com.buransky.plugins.scala.ScalaPlugin + com.buransky.plugins.scoverage.ScoveragePlugin 2.9.1 diff --git a/src/main/java/com/buransky/plugins/scala/ScalaPlugin.java b/src/main/java/com/buransky/plugins/scala/ScalaPlugin.java deleted file mode 100644 index 13d558c..0000000 --- a/src/main/java/com/buransky/plugins/scala/ScalaPlugin.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala; - -import com.buransky.plugins.scala.cobertura.CoberturaSensor; -import com.buransky.plugins.scala.language.Scala; -import com.buransky.plugins.scala.sensor.ScalaSourceImporterSensor; -import org.sonar.api.Extension; -import org.sonar.api.SonarPlugin; - -import java.util.ArrayList; -import java.util.List; - -/** - * This class is the entry point for all extensions made by the - * Sonar Scala Plugin. - * - * @author Felix Müller - * @since 0.1 - */ -public class ScalaPlugin extends SonarPlugin { - - public List> getExtensions() { - final List> extensions = new ArrayList>(); - extensions.add(Scala.class); - extensions.add(ScalaSourceImporterSensor.class); - extensions.add(CoberturaSensor.class); - - return extensions; - } - - @Override - public String toString() { - return getClass().getSimpleName(); - } - - public static String getPathToScalaLibrary() { - return getPathByResource("scala/package.class"); - } - - /** - * Godin: during execution of Sonar Batch all dependencies of a plugin are downloaded and - * available locally as JAR-files, so we can use this kind of hack to locate JARs. - */ - private static String getPathByResource(String name) { - String path = ScalaPlugin.class.getClassLoader().getResource(name).getPath(); - return path.substring("file:".length(), path.lastIndexOf('!')); - } -} diff --git a/src/main/java/com/buransky/plugins/scala/language/Scala.java b/src/main/java/com/buransky/plugins/scala/language/Scala.java deleted file mode 100644 index 27b3259..0000000 --- a/src/main/java/com/buransky/plugins/scala/language/Scala.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.language; - -import org.sonar.api.resources.AbstractLanguage; - -/** - * This class implements Scala as a language for Sonar. - * - * @author Felix Müller - * @since 0.1 - */ -public class Scala extends AbstractLanguage { - - public static final Scala INSTANCE = new Scala(); - - public Scala() { - super("scala", "Scala"); - } - - public String[] getFileSuffixes() { - return new String[] { "scala" }; - } -} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java b/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java new file mode 100644 index 0000000..f288f8e --- /dev/null +++ b/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java @@ -0,0 +1,27 @@ +package com.buransky.plugins.scoverage; + +import com.buransky.plugins.scoverage.sensor.ScoverageSensor; +import com.buransky.plugins.scoverage.language.Scala; +import com.buransky.plugins.scoverage.sensor.ScalaSourceImporterSensor; +import org.sonar.api.Extension; +import org.sonar.api.SonarPlugin; + +import java.util.ArrayList; +import java.util.List; + +public class ScoveragePlugin extends SonarPlugin { + + public List> getExtensions() { + final List> extensions = new ArrayList>(); + extensions.add(Scala.class); + extensions.add(ScalaSourceImporterSensor.class); + extensions.add(ScoverageSensor.class); + + return extensions; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/src/main/java/com/buransky/plugins/scoverage/language/Scala.java b/src/main/java/com/buransky/plugins/scoverage/language/Scala.java new file mode 100644 index 0000000..08cf742 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scoverage/language/Scala.java @@ -0,0 +1,16 @@ +package com.buransky.plugins.scoverage.language; + +import org.sonar.api.resources.AbstractLanguage; + +public class Scala extends AbstractLanguage { + + public static final Scala INSTANCE = new Scala(); + + public Scala() { + super("scala", "Scala"); + } + + public String[] getFileSuffixes() { + return new String[] { "scala" }; + } +} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/language/ScalaRealFile.java b/src/main/java/com/buransky/plugins/scoverage/language/ScalaFile.java similarity index 50% rename from src/main/java/com/buransky/plugins/scala/language/ScalaRealFile.java rename to src/main/java/com/buransky/plugins/scoverage/language/ScalaFile.java index 5efcbf1..6b68e56 100644 --- a/src/main/java/com/buransky/plugins/scala/language/ScalaRealFile.java +++ b/src/main/java/com/buransky/plugins/scoverage/language/ScalaFile.java @@ -1,23 +1,4 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.language; +package com.buransky.plugins.scoverage.language; import org.apache.commons.lang.StringUtils; import org.sonar.api.resources.*; @@ -25,20 +6,14 @@ import java.io.File; -/** - * This class implements a Scala source file for Sonar. - * - * @author Felix Müller - * @since 0.1 - */ -public class ScalaRealFile extends Resource { +public class ScalaFile extends Resource { private final boolean isUnitTest; private final String directory; private final String fileName; private final Directory parent; - public ScalaRealFile(String directory, String fileName, boolean isUnitTest) { + public ScalaFile(String directory, String fileName, boolean isUnitTest) { super(); this.isUnitTest = isUnitTest; @@ -91,29 +66,25 @@ public boolean matchFilePattern(String antPattern) { return matcher.match(getKey()); } - public boolean isUnitTest() { - return isUnitTest; - } - /** * Shortcut for {@link #fromInputFile(org.sonar.api.resources.InputFile, boolean)} for source files. */ - public static ScalaRealFile fromInputFile(InputFile inputFile) { - return ScalaRealFile.fromInputFile(inputFile, false); + public static ScalaFile fromInputFile(InputFile inputFile) { + return ScalaFile.fromInputFile(inputFile, false); } /** - * Creates a {@link com.buransky.plugins.scala.language.ScalaRealFile} from a file in the source directories. + * Creates a {@link ScalaFile} from a file in the source directories. * * @param inputFile the file object with relative path * @param isUnitTest whether it is a unit test file or a source file - * @return the {@link com.buransky.plugins.scala.language.ScalaRealFile} created if exists, null otherwise + * @return the {@link ScalaFile} created if exists, null otherwise */ - public static ScalaRealFile fromInputFile(InputFile inputFile, boolean isUnitTest) { + public static ScalaFile fromInputFile(InputFile inputFile, boolean isUnitTest) { if (inputFile == null || inputFile.getFile() == null || inputFile.getRelativePath() == null) { return null; } - return new ScalaRealFile(inputFile.getFileBaseDir().getPath(), inputFile.getFile().getName(), isUnitTest); + return new ScalaFile(inputFile.getFileBaseDir().getPath(), inputFile.getFile().getName(), isUnitTest); } } \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scala/sensor/AbstractScalaSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/AbstractScalaSensor.java similarity index 90% rename from src/main/java/com/buransky/plugins/scala/sensor/AbstractScalaSensor.java rename to src/main/java/com/buransky/plugins/scoverage/sensor/AbstractScalaSensor.java index c98d876..ad335c8 100644 --- a/src/main/java/com/buransky/plugins/scala/sensor/AbstractScalaSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/AbstractScalaSensor.java @@ -17,9 +17,9 @@ * License along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 */ -package com.buransky.plugins.scala.sensor; +package com.buransky.plugins.scoverage.sensor; -import com.buransky.plugins.scala.language.Scala; +import com.buransky.plugins.scoverage.language.Scala; import org.sonar.api.batch.Sensor; import org.sonar.api.resources.Project; diff --git a/src/main/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScalaSourceImporterSensor.java similarity index 61% rename from src/main/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensor.java rename to src/main/java/com/buransky/plugins/scoverage/sensor/ScalaSourceImporterSensor.java index 52d8e00..079f3b6 100644 --- a/src/main/java/com/buransky/plugins/scala/sensor/ScalaSourceImporterSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScalaSourceImporterSensor.java @@ -1,26 +1,7 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scala.sensor; +package com.buransky.plugins.scoverage.sensor; -import com.buransky.plugins.scala.language.Scala; -import com.buransky.plugins.scala.language.ScalaRealFile; +import com.buransky.plugins.scoverage.language.Scala; +import com.buransky.plugins.scoverage.language.ScalaFile; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,12 +14,6 @@ import java.io.IOException; -/** - * This Sensor imports all Scala files into Sonar. - * - * @author Felix Müller - * @since 0.1 - */ @Phase(name = Name.PRE) public class ScalaSourceImporterSensor extends AbstractScalaSensor { @@ -65,9 +40,7 @@ private void addFileToSonar(SensorContext sensorContext, InputFile inputFile, boolean isUnitTest, String charset) { try { String source = FileUtils.readFileToString(inputFile.getFile(), charset); - - //ScalaFile resource = ScalaFile.fromInputFile(inputFile, isUnitTest); - ScalaRealFile resource = ScalaRealFile.fromInputFile(inputFile, isUnitTest); + ScalaFile resource = ScalaFile.fromInputFile(inputFile, isUnitTest); sensorContext.index(resource); sensorContext.saveSource(resource, source); diff --git a/src/main/java/com/buransky/plugins/scala/cobertura/CoberturaSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java similarity index 85% rename from src/main/java/com/buransky/plugins/scala/cobertura/CoberturaSensor.java rename to src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java index 1b72d48..8b8e323 100644 --- a/src/main/java/com/buransky/plugins/scala/cobertura/CoberturaSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java @@ -17,10 +17,10 @@ * License along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 */ -package com.buransky.plugins.scala.cobertura; +package com.buransky.plugins.scoverage.sensor; -import com.buransky.plugins.scala.language.Scala; -import com.buransky.plugins.scala.language.ScalaRealFile; +import com.buransky.plugins.scoverage.language.Scala; +import com.buransky.plugins.scoverage.language.ScalaFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.batch.CoverageExtension; @@ -37,9 +37,9 @@ import java.util.HashMap; import java.util.Map; -public class CoberturaSensor implements Sensor, CoverageExtension { +public class ScoverageSensor implements Sensor, CoverageExtension { - private static final Logger LOG = LoggerFactory.getLogger(CoberturaSensor.class); + private static final Logger LOG = LoggerFactory.getLogger(ScoverageSensor.class); public boolean shouldExecuteOnProject(Project project) { return project.getAnalysisType().isDynamic(true) && Scala.INSTANCE.getKey().equals(project.getLanguageKey()); @@ -51,7 +51,7 @@ public void analyse(Project project, SensorContext context) { @Override public String toString() { - return "Scala CoberturaSensor"; + return "Scala ScoverageSensor"; } private void parseFakeReport(Project project, final SensorContext context) { @@ -59,8 +59,8 @@ private void parseFakeReport(Project project, final SensorContext context) { HashMap dirs = new HashMap(); for (InputFile sourceFile : fileSystem.mainFiles("scala")) { - LOG.info("[CoberturaSensor] Set coverage for [" + sourceFile.getRelativePath() + "]"); - ScalaRealFile scalaSourcefile = ScalaRealFile.fromInputFile(sourceFile); + LOG.info("[ScoverageSensor] Set coverage for [" + sourceFile.getRelativePath() + "]"); + ScalaFile scalaSourcefile = ScalaFile.fromInputFile(sourceFile); CoverageMeasuresBuilder coverage = CoverageMeasuresBuilder.create(); coverage.setHits(1, 1); @@ -86,7 +86,7 @@ private void parseFakeReport(Project project, final SensorContext context) { } for (Map.Entry e: dirs.entrySet()) { - LOG.info("[CoberturaSensor] Set dir coverage for [" + e.getKey() + "]"); + LOG.info("[ScoverageSensor] Set dir coverage for [" + e.getKey() + "]"); context.saveMeasure(e.getValue(), new Measure(CoreMetrics.COVERAGE, 23.4)); } From fa853f9f06225422f3b11e68649f8149af05b3ff Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Tue, 4 Feb 2014 12:40:56 -0800 Subject: [PATCH 007/101] Pure Java --- .../plugins/scoverage/ScoveragePlugin.java | 4 +- .../plugins/scoverage/language/ScalaFile.java | 89 +------------------ .../scoverage/sensor/AbstractScalaSensor.java | 47 ---------- .../scoverage/sensor/ScoverageSensor.java | 26 +----- ...ava => ScoverageSourceImporterSensor.java} | 41 +++++---- 5 files changed, 27 insertions(+), 180 deletions(-) delete mode 100644 src/main/java/com/buransky/plugins/scoverage/sensor/AbstractScalaSensor.java rename src/main/java/com/buransky/plugins/scoverage/sensor/{ScalaSourceImporterSensor.java => ScoverageSourceImporterSensor.java} (53%) diff --git a/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java b/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java index f288f8e..688b3e5 100644 --- a/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java +++ b/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java @@ -2,7 +2,7 @@ import com.buransky.plugins.scoverage.sensor.ScoverageSensor; import com.buransky.plugins.scoverage.language.Scala; -import com.buransky.plugins.scoverage.sensor.ScalaSourceImporterSensor; +import com.buransky.plugins.scoverage.sensor.ScoverageSourceImporterSensor; import org.sonar.api.Extension; import org.sonar.api.SonarPlugin; @@ -14,7 +14,7 @@ public class ScoveragePlugin extends SonarPlugin { public List> getExtensions() { final List> extensions = new ArrayList>(); extensions.add(Scala.class); - extensions.add(ScalaSourceImporterSensor.class); + extensions.add(ScoverageSourceImporterSensor.class); extensions.add(ScoverageSensor.class); return extensions; diff --git a/src/main/java/com/buransky/plugins/scoverage/language/ScalaFile.java b/src/main/java/com/buransky/plugins/scoverage/language/ScalaFile.java index 6b68e56..661c21b 100644 --- a/src/main/java/com/buransky/plugins/scoverage/language/ScalaFile.java +++ b/src/main/java/com/buransky/plugins/scoverage/language/ScalaFile.java @@ -1,90 +1,7 @@ package com.buransky.plugins.scoverage.language; -import org.apache.commons.lang.StringUtils; -import org.sonar.api.resources.*; -import org.sonar.api.utils.WildcardPattern; - -import java.io.File; - -public class ScalaFile extends Resource { - - private final boolean isUnitTest; - private final String directory; - private final String fileName; - private final Directory parent; - - public ScalaFile(String directory, String fileName, boolean isUnitTest) { - super(); - this.isUnitTest = isUnitTest; - - this.directory = (directory == null) ? "" : directory.trim(); - this.fileName = fileName.trim(); - - parent = new Directory(directory); - setKey(getLongName()); - } - - @Override - public String getName() { - return fileName; - } - - @Override - public String getLongName() { - return directory + File.pathSeparatorChar + fileName; - } - - @Override - public String getDescription() { - return null; - } - - @Override - public Language getLanguage() { - return Scala.INSTANCE; - } - - @Override - public String getScope() { - return Scopes.FILE; - } - - @Override - public String getQualifier() { - return isUnitTest ? Qualifiers.UNIT_TEST_FILE : Qualifiers.FILE; - } - - @Override - public Directory getParent() { - return parent; - } - - @Override - public boolean matchFilePattern(String antPattern) { - final String patternWithoutFileSuffix = StringUtils.substringBeforeLast(antPattern, "."); - final WildcardPattern matcher = WildcardPattern.create(patternWithoutFileSuffix, "."); - return matcher.match(getKey()); - } - - /** - * Shortcut for {@link #fromInputFile(org.sonar.api.resources.InputFile, boolean)} for source files. - */ - public static ScalaFile fromInputFile(InputFile inputFile) { - return ScalaFile.fromInputFile(inputFile, false); - } - - /** - * Creates a {@link ScalaFile} from a file in the source directories. - * - * @param inputFile the file object with relative path - * @param isUnitTest whether it is a unit test file or a source file - * @return the {@link ScalaFile} created if exists, null otherwise - */ - public static ScalaFile fromInputFile(InputFile inputFile, boolean isUnitTest) { - if (inputFile == null || inputFile.getFile() == null || inputFile.getRelativePath() == null) { - return null; - } - - return new ScalaFile(inputFile.getFileBaseDir().getPath(), inputFile.getFile().getName(), isUnitTest); +public class ScalaFile extends org.sonar.api.resources.File { + public ScalaFile(String directory, String fileName) { + super(Scala.INSTANCE, directory, fileName); } } \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/AbstractScalaSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/AbstractScalaSensor.java deleted file mode 100644 index ad335c8..0000000 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/AbstractScalaSensor.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scoverage.sensor; - -import com.buransky.plugins.scoverage.language.Scala; -import org.sonar.api.batch.Sensor; -import org.sonar.api.resources.Project; - -/** - * This is a helper base class for sensors that should only be executed on Scala projects. - * - * @author Felix Müller - * @since 0.1 - */ -public abstract class AbstractScalaSensor implements Sensor { - - private final Scala scala; - - protected AbstractScalaSensor(Scala scala) { - this.scala = scala; - } - - public final boolean shouldExecuteOnProject(Project project) { - return project.getLanguage().equals(scala); - } - - public final Scala getScala() { - return scala; - } -} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java index 8b8e323..5ba2e67 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java @@ -1,22 +1,3 @@ -/* - * Sonar Scala Plugin - * Copyright (C) 2011 - 2013 All contributors - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ package com.buransky.plugins.scoverage.sensor; import com.buransky.plugins.scoverage.language.Scala; @@ -29,10 +10,7 @@ import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.CoverageMeasuresBuilder; import org.sonar.api.measures.Measure; -import org.sonar.api.resources.Directory; -import org.sonar.api.resources.InputFile; -import org.sonar.api.resources.Project; -import org.sonar.api.resources.ProjectFileSystem; +import org.sonar.api.resources.*; import java.util.HashMap; import java.util.Map; @@ -60,7 +38,7 @@ private void parseFakeReport(Project project, final SensorContext context) { HashMap dirs = new HashMap(); for (InputFile sourceFile : fileSystem.mainFiles("scala")) { LOG.info("[ScoverageSensor] Set coverage for [" + sourceFile.getRelativePath() + "]"); - ScalaFile scalaSourcefile = ScalaFile.fromInputFile(sourceFile); + File scalaSourcefile = ScalaFile.fromIOFile(sourceFile.getFile(), project); CoverageMeasuresBuilder coverage = CoverageMeasuresBuilder.create(); coverage.setHits(1, 1); diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScalaSourceImporterSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java similarity index 53% rename from src/main/java/com/buransky/plugins/scoverage/sensor/ScalaSourceImporterSensor.java rename to src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java index 079f3b6..def3a25 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScalaSourceImporterSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java @@ -7,7 +7,9 @@ import org.slf4j.LoggerFactory; import org.sonar.api.batch.Phase; import org.sonar.api.batch.Phase.Name; +import org.sonar.api.batch.Sensor; import org.sonar.api.batch.SensorContext; +import org.sonar.api.resources.File; import org.sonar.api.resources.InputFile; import org.sonar.api.resources.Project; import org.sonar.api.resources.ProjectFileSystem; @@ -15,43 +17,40 @@ import java.io.IOException; @Phase(name = Name.PRE) -public class ScalaSourceImporterSensor extends AbstractScalaSensor { +public class ScoverageSourceImporterSensor implements Sensor { - private static final Logger LOGGER = LoggerFactory.getLogger(ScalaSourceImporterSensor.class); + private static final Logger LOGGER = LoggerFactory.getLogger(ScoverageSourceImporterSensor.class); + private final Scala scala; - public ScalaSourceImporterSensor(Scala scala) { - super(scala); + public ScoverageSourceImporterSensor(Scala scala) { + this.scala = scala; + } + + public boolean shouldExecuteOnProject(Project project) { + return project.getLanguage().equals(scala); } public void analyse(Project project, SensorContext sensorContext) { ProjectFileSystem fileSystem = project.getFileSystem(); String charset = fileSystem.getSourceCharset().toString(); - for (InputFile sourceFile : fileSystem.mainFiles(getScala().getKey())) { - addFileToSonar(sensorContext, sourceFile, false, charset); - } - - for (InputFile testFile : fileSystem.testFiles(getScala().getKey())) { - addFileToSonar(sensorContext, testFile, true, charset); + for (InputFile sourceFile : fileSystem.mainFiles(scala.getKey())) { + addFileToSonar(project, sensorContext, sourceFile, charset); } } - private void addFileToSonar(SensorContext sensorContext, InputFile inputFile, - boolean isUnitTest, String charset) { + private void addFileToSonar(Project project, SensorContext sensorContext, InputFile inputFile, + String charset) { try { String source = FileUtils.readFileToString(inputFile.getFile(), charset); - ScalaFile resource = ScalaFile.fromInputFile(inputFile, isUnitTest); + File resource = ScalaFile.fromIOFile(inputFile.getFile(), project); + if (resource == null) { + LOGGER.warn("[ScoverageSourceImporterSensor] Resource null! " + inputFile.getRelativePath()); + return; + } sensorContext.index(resource); sensorContext.saveSource(resource, source); - - if (LOGGER.isDebugEnabled()) { - if (isUnitTest) { - LOGGER.debug("Added Scala test file to Sonar: " + inputFile.getFile().getAbsolutePath()); - } else { - LOGGER.debug("Added Scala source file to Sonar: " + inputFile.getFile().getAbsolutePath()); - } - } } catch (IOException ioe) { LOGGER.error("Could not read the file: " + inputFile.getFile().getAbsolutePath(), ioe); } From 158bf608135643f49f979c7bd3d371af12489167 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Tue, 4 Feb 2014 15:54:17 -0800 Subject: [PATCH 008/101] . --- .../plugins/scoverage/language/ScalaFile.java | 4 + .../scoverage/sensor/ScoverageSensor.java | 133 ++++++++++++++---- .../sensor/ScoverageSourceImporterSensor.java | 7 +- .../plugins/scoverage/ScoverageParser.scala | 43 ++++++ 4 files changed, 155 insertions(+), 32 deletions(-) create mode 100644 src/main/scala/com/buransky/plugins/scoverage/ScoverageParser.scala diff --git a/src/main/java/com/buransky/plugins/scoverage/language/ScalaFile.java b/src/main/java/com/buransky/plugins/scoverage/language/ScalaFile.java index 661c21b..b94391e 100644 --- a/src/main/java/com/buransky/plugins/scoverage/language/ScalaFile.java +++ b/src/main/java/com/buransky/plugins/scoverage/language/ScalaFile.java @@ -1,6 +1,10 @@ package com.buransky.plugins.scoverage.language; public class ScalaFile extends org.sonar.api.resources.File { + public ScalaFile(String key) { + super(Scala.INSTANCE, key); + } + public ScalaFile(String directory, String fileName) { super(Scala.INSTANCE, directory, fileName); } diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java index 5ba2e67..8d3ba05 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java @@ -1,5 +1,9 @@ package com.buransky.plugins.scoverage.sensor; +import com.buransky.plugins.scoverage.FileStatementCoverage; +import com.buransky.plugins.scoverage.ParentStatementCoverage; +import com.buransky.plugins.scoverage.ScoverageParser; +import com.buransky.plugins.scoverage.StatementCoverage; import com.buransky.plugins.scoverage.language.Scala; import com.buransky.plugins.scoverage.language.ScalaFile; import org.slf4j.Logger; @@ -11,25 +15,101 @@ import org.sonar.api.measures.CoverageMeasuresBuilder; import org.sonar.api.measures.Measure; import org.sonar.api.resources.*; +import org.sonar.api.resources.File; +import scala.collection.JavaConversions; +import java.io.*; import java.util.HashMap; import java.util.Map; public class ScoverageSensor implements Sensor, CoverageExtension { - - private static final Logger LOG = LoggerFactory.getLogger(ScoverageSensor.class); + private static final Logger log = LoggerFactory.getLogger(ScoverageSensor.class); public boolean shouldExecuteOnProject(Project project) { return project.getAnalysisType().isDynamic(true) && Scala.INSTANCE.getKey().equals(project.getLanguageKey()); } public void analyse(Project project, SensorContext context) { - parseFakeReport(project, context); + processProject(ScoverageParser.parse(""), project, context); + //parseFakeReport(project, context); } @Override public String toString() { - return "Scala ScoverageSensor"; + return "Scoverage sensor"; + } + + private void processProject(ParentStatementCoverage projectCoverage, + Project project, SensorContext context) { + // Save project measure + context.saveMeasure(project, new Measure(CoreMetrics.COVERAGE, projectCoverage.rate())); + log("Project coverage = " + projectCoverage.rate()); + + // Process children + processChildren(projectCoverage.children(), project, context, ""); + } + + private void processDirectory(ParentStatementCoverage directoryCoverage, + Project project, SensorContext context, + String directory) { + log("Process directory [" + directoryCoverage.name() + "]"); + + // Save directory measure + //context.saveMeasure(project, new Measure(CoreMetrics.COVERAGE, directoryCoverage.rate())); + + // Process children + processChildren(directoryCoverage.children(), project, context, + appendFilePath(directory, directoryCoverage.name())); + } + + private String appendFilePath(String src, String name) { + String result; + if (!src.isEmpty()) + result = src + java.io.File.separator; + else + result = ""; + + return result + name; + } + + private void processFile(FileStatementCoverage fileCoverage, SensorContext context, + String directory) { + File scalaSourcefile = new ScalaFile(appendFilePath(directory, fileCoverage.name())); + context.saveMeasure(scalaSourcefile, new Measure(CoreMetrics.COVERAGE, fileCoverage.rate())); + + log("Process file [" + scalaSourcefile.getKey() + ", " + fileCoverage.rate() + "]"); + } + + private void processChildren(scala.collection.Iterable children, + Project project, SensorContext context, + String directory) { + log("Process children [" + directory + "]"); + + // Process children + for (StatementCoverage child: JavaConversions.asJavaIterable(children)) { + processChild(child, project, context, directory); + } + } + + private void processChild(StatementCoverage dirOrFile, + Project project, SensorContext context, + String directory) { + if (dirOrFile instanceof ParentStatementCoverage) { + processDirectory((ParentStatementCoverage) dirOrFile, project, context, directory); + } + else { + if (dirOrFile instanceof FileStatementCoverage) { + processFile((FileStatementCoverage) dirOrFile, context, directory); + } + else { + throw new IllegalStateException("Not a file or directory coverage! [" + + dirOrFile.getClass().getName() + "]"); + } + } + } + + private static void log(String message) { + log.info("[Scoverage] " + message); } private void parseFakeReport(Project project, final SensorContext context) { @@ -37,34 +117,35 @@ private void parseFakeReport(Project project, final SensorContext context) { HashMap dirs = new HashMap(); for (InputFile sourceFile : fileSystem.mainFiles("scala")) { - LOG.info("[ScoverageSensor] Set coverage for [" + sourceFile.getRelativePath() + "]"); - File scalaSourcefile = ScalaFile.fromIOFile(sourceFile.getFile(), project); - - CoverageMeasuresBuilder coverage = CoverageMeasuresBuilder.create(); - coverage.setHits(1, 1); - coverage.setHits(2, 2); - coverage.setHits(3, 3); - coverage.setHits(4, 0); - coverage.setHits(5, 0); - coverage.setHits(6, 0); - coverage.setHits(7, 0); - coverage.setHits(8, 1); - coverage.setHits(9, 0); - coverage.setHits(10, 2); - coverage.setHits(11, 0); - coverage.setHits(12, 3); - coverage.setHits(13, 0); - - for (Measure measure : coverage.createMeasures()) { - context.saveMeasure(scalaSourcefile, measure); - } + File scalaSourcefile = new ScalaFile(File.fromIOFile(sourceFile.getFile(), project).getKey()); context.saveMeasure(scalaSourcefile, new Measure(CoreMetrics.COVERAGE, 51.4)); + log("Process fake file [" + scalaSourcefile.getKey() + "]"); + +// CoverageMeasuresBuilder coverage = CoverageMeasuresBuilder.create(); +// coverage.setHits(1, 1); +// coverage.setHits(2, 2); +// coverage.setHits(3, 3); +// coverage.setHits(4, 0); +// coverage.setHits(5, 0); +// coverage.setHits(6, 0); +// coverage.setHits(7, 0); +// coverage.setHits(8, 1); +// coverage.setHits(9, 0); +// coverage.setHits(10, 2); +// coverage.setHits(11, 0); +// coverage.setHits(12, 3); +// coverage.setHits(13, 0); +// +// for (Measure measure : coverage.createMeasures()) { +// context.saveMeasure(scalaSourcefile, measure); +// } + dirs.put(scalaSourcefile.getParent().getKey(), scalaSourcefile.getParent()); } for (Map.Entry e: dirs.entrySet()) { - LOG.info("[ScoverageSensor] Set dir coverage for [" + e.getKey() + "]"); + log.info("[ScoverageSensor] Set dir coverage for [" + e.getKey() + "]"); context.saveMeasure(e.getValue(), new Measure(CoreMetrics.COVERAGE, 23.4)); } diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java index def3a25..84d13f6 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java @@ -43,7 +43,7 @@ private void addFileToSonar(Project project, SensorContext sensorContext, InputF String charset) { try { String source = FileUtils.readFileToString(inputFile.getFile(), charset); - File resource = ScalaFile.fromIOFile(inputFile.getFile(), project); + ScalaFile resource = new ScalaFile(File.fromIOFile(inputFile.getFile(), project).getKey()); if (resource == null) { LOGGER.warn("[ScoverageSourceImporterSensor] Resource null! " + inputFile.getRelativePath()); return; @@ -55,9 +55,4 @@ private void addFileToSonar(Project project, SensorContext sensorContext, InputF LOGGER.error("Could not read the file: " + inputFile.getFile().getAbsolutePath(), ioe); } } - - @Override - public String toString() { - return getClass().getSimpleName(); - } } \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scoverage/ScoverageParser.scala b/src/main/scala/com/buransky/plugins/scoverage/ScoverageParser.scala new file mode 100644 index 0000000..df4dcaa --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scoverage/ScoverageParser.scala @@ -0,0 +1,43 @@ +package com.buransky.plugins.scoverage + + +object ScoverageParser { + def parse(scoverageXmlPath: String): ParentStatementCoverage = { + val errorCodeFile = FileStatementCoverage("ErrorCode.scala", 17, 13) + val graphFile = FileStatementCoverage("Graph.scala", 42, 0) + + val file2 = FileStatementCoverage("file2.scala", 2, 1) + val bbbDir = ParentStatementCoverage("bbb", Seq(file2)) + + val file1 = FileStatementCoverage("file1.scala", 100, 33) + val aaaDir = ParentStatementCoverage("aaa", Seq(file1, errorCodeFile, graphFile, bbbDir)) + + val project = ParentStatementCoverage("project", Seq(aaaDir)) + + project + } +} + +trait StatementCoverage { + lazy val rate: Double = (coveredStatementsCount.toDouble / statementsCount.toDouble) * 100.0 + + val name: String + val statementsCount: Int + val coveredStatementsCount: Int + + require(statementsCount >= 0, "Statements count cannot be negative! [" + statementsCount + "]") + require(coveredStatementsCount >= 0, "Statements count cannot be negative! [" + + coveredStatementsCount + "]") + require(coveredStatementsCount <= statementsCount, + "Number of covered statements cannot be more than total number of statements! [" + + statementsCount + ", " + coveredStatementsCount + "]") +} + +case class ParentStatementCoverage(name: String, children: Iterable[StatementCoverage]) + extends StatementCoverage { + val statementsCount = children.map(_.statementsCount).sum + val coveredStatementsCount = children.map(_.coveredStatementsCount).sum +} + +case class FileStatementCoverage(name: String, statementsCount: Int, + coveredStatementsCount: Int) extends StatementCoverage \ No newline at end of file From 58d0d1f8d1d33c7e982a5466496ba3d1fecbe98c Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Tue, 4 Feb 2014 17:31:36 -0800 Subject: [PATCH 009/101] . --- .../plugins/scoverage/ScoveragePlugin.java | 4 ++ .../plugins/scoverage/language/ScalaFile.java | 11 --- .../scoverage/measure/ScalaMetrics.java | 26 +++++++ .../scoverage/resource/ScalaDirectory.java | 40 +++++++++++ .../plugins/scoverage/resource/ScalaFile.java | 67 ++++++++++++++++++ .../scoverage/sensor/ScoverageSensor.java | 70 +++++++++---------- .../sensor/ScoverageSourceImporterSensor.java | 15 ++-- .../scoverage/widget/ScoverageWidget.java | 20 ++++++ .../plugins/scoverage/widget.html.erb | 10 +++ 9 files changed, 211 insertions(+), 52 deletions(-) delete mode 100644 src/main/java/com/buransky/plugins/scoverage/language/ScalaFile.java create mode 100644 src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java create mode 100644 src/main/java/com/buransky/plugins/scoverage/resource/ScalaDirectory.java create mode 100644 src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java create mode 100644 src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java create mode 100644 src/main/resources/com/buransky/plugins/scoverage/widget.html.erb diff --git a/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java b/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java index 688b3e5..2c05ee3 100644 --- a/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java +++ b/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java @@ -1,8 +1,10 @@ package com.buransky.plugins.scoverage; +import com.buransky.plugins.scoverage.measure.ScalaMetrics; import com.buransky.plugins.scoverage.sensor.ScoverageSensor; import com.buransky.plugins.scoverage.language.Scala; import com.buransky.plugins.scoverage.sensor.ScoverageSourceImporterSensor; +import com.buransky.plugins.scoverage.widget.ScoverageWidget; import org.sonar.api.Extension; import org.sonar.api.SonarPlugin; @@ -13,9 +15,11 @@ public class ScoveragePlugin extends SonarPlugin { public List> getExtensions() { final List> extensions = new ArrayList>(); + extensions.add(ScalaMetrics.class); extensions.add(Scala.class); extensions.add(ScoverageSourceImporterSensor.class); extensions.add(ScoverageSensor.class); + extensions.add(ScoverageWidget.class); return extensions; } diff --git a/src/main/java/com/buransky/plugins/scoverage/language/ScalaFile.java b/src/main/java/com/buransky/plugins/scoverage/language/ScalaFile.java deleted file mode 100644 index b94391e..0000000 --- a/src/main/java/com/buransky/plugins/scoverage/language/ScalaFile.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.buransky.plugins.scoverage.language; - -public class ScalaFile extends org.sonar.api.resources.File { - public ScalaFile(String key) { - super(Scala.INSTANCE, key); - } - - public ScalaFile(String directory, String fileName) { - super(Scala.INSTANCE, directory, fileName); - } -} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java b/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java new file mode 100644 index 0000000..bb6095a --- /dev/null +++ b/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java @@ -0,0 +1,26 @@ +package com.buransky.plugins.scoverage.measure; + +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Metric; +import org.sonar.api.measures.Metrics; + +import java.util.Arrays; +import java.util.List; + +public final class ScalaMetrics implements Metrics { + public static final String STATEMENT_COVERAGE_KEY = "scoverage"; + public static final Metric STATEMENT_COVERAGE = new Metric.Builder(STATEMENT_COVERAGE_KEY, + "Statement coverage", Metric.ValueType.PERCENT) + .setDescription("Statement coverage by unit tests") + .setDirection(Metric.DIRECTION_BETTER) + .setQualitative(true) + .setDomain(CoreMetrics.DOMAIN_TESTS) + .setWorstValue(0.0) + .setBestValue(100.0) + .create(); + + @Override + public List getMetrics() { + return Arrays.asList(STATEMENT_COVERAGE); + } +} diff --git a/src/main/java/com/buransky/plugins/scoverage/resource/ScalaDirectory.java b/src/main/java/com/buransky/plugins/scoverage/resource/ScalaDirectory.java new file mode 100644 index 0000000..1787f13 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scoverage/resource/ScalaDirectory.java @@ -0,0 +1,40 @@ +package com.buransky.plugins.scoverage.resource; + +import com.buransky.plugins.scoverage.language.Scala; +import org.sonar.api.resources.Directory; +import org.sonar.api.resources.Language; +import org.sonar.api.resources.Resource; + +public class ScalaDirectory extends Directory { + private final String name; + private final ScalaDirectory parent; + + public ScalaDirectory(String key) { + super(key); + + int i = getKey().lastIndexOf(SEPARATOR); + if (i > 0) { + parent = new ScalaDirectory(key.substring(0, i)); + name = key.substring(i + 1); + } + else { + name = key; + parent = null; + } + } + + @Override + public String getName() { + return name; + } + + @Override + public Language getLanguage() { + return Scala.INSTANCE; + } + + @Override + public Resource getParent() { + return parent; + } +} diff --git a/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java b/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java new file mode 100644 index 0000000..e9b25b3 --- /dev/null +++ b/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java @@ -0,0 +1,67 @@ +package com.buransky.plugins.scoverage.resource; + +import com.buransky.plugins.scoverage.language.Scala; +import org.sonar.api.resources.File; +import org.sonar.api.resources.Language; +import org.sonar.api.resources.Resource; + +public class ScalaFile extends Resource { + private final File file; + private ScalaDirectory parent; + + public ScalaFile(String key) { + if (key == null) + throw new IllegalArgumentException("Key cannot be null!"); + + file = new File(key); + setKey(key); + } + + @Override + public String getName() { + return file.getName(); + } + + @Override + public String getLongName() { + return file.getLongName(); + } + + @Override + public String getDescription() { + return file.getDescription(); + } + + @Override + public Language getLanguage() { + return Scala.INSTANCE; + } + + @Override + public String getScope() { + return file.getScope(); + } + + @Override + public String getQualifier() { + return file.getQualifier(); + } + + @Override + public ScalaDirectory getParent() { + if (parent == null) { + parent = new ScalaDirectory(file.getParent().getKey()); + } + return parent; + } + + @Override + public boolean matchFilePattern(String antPattern) { + return file.matchFilePattern(antPattern); + } + + @Override + public String toString() { + return file.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java index 8d3ba05..06c905e 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java @@ -5,20 +5,19 @@ import com.buransky.plugins.scoverage.ScoverageParser; import com.buransky.plugins.scoverage.StatementCoverage; import com.buransky.plugins.scoverage.language.Scala; -import com.buransky.plugins.scoverage.language.ScalaFile; +import com.buransky.plugins.scoverage.measure.ScalaMetrics; +import com.buransky.plugins.scoverage.resource.ScalaDirectory; +import com.buransky.plugins.scoverage.resource.ScalaFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.batch.CoverageExtension; import org.sonar.api.batch.Sensor; import org.sonar.api.batch.SensorContext; import org.sonar.api.measures.CoreMetrics; -import org.sonar.api.measures.CoverageMeasuresBuilder; import org.sonar.api.measures.Measure; import org.sonar.api.resources.*; -import org.sonar.api.resources.File; import scala.collection.JavaConversions; -import java.io.*; import java.util.HashMap; import java.util.Map; @@ -42,60 +41,46 @@ public String toString() { private void processProject(ParentStatementCoverage projectCoverage, Project project, SensorContext context) { // Save project measure - context.saveMeasure(project, new Measure(CoreMetrics.COVERAGE, projectCoverage.rate())); + context.saveMeasure(project, createStatementCoverage(projectCoverage.rate())); log("Project coverage = " + projectCoverage.rate()); // Process children - processChildren(projectCoverage.children(), project, context, ""); + processChildren(projectCoverage.children(), context, ""); } - private void processDirectory(ParentStatementCoverage directoryCoverage, - Project project, SensorContext context, - String directory) { - log("Process directory [" + directoryCoverage.name() + "]"); + private void processDirectory(ParentStatementCoverage directoryCoverage, SensorContext context, + String parentDirectory) { + String currentDirectory = appendFilePath(parentDirectory, directoryCoverage.name()); - // Save directory measure - //context.saveMeasure(project, new Measure(CoreMetrics.COVERAGE, directoryCoverage.rate())); + ScalaDirectory directory = new ScalaDirectory(currentDirectory); + context.saveMeasure(directory, createStatementCoverage(directoryCoverage.rate())); - // Process children - processChildren(directoryCoverage.children(), project, context, - appendFilePath(directory, directoryCoverage.name())); - } + log("Process directory [" + directory.getKey() + ", " + directoryCoverage.rate() + "]"); - private String appendFilePath(String src, String name) { - String result; - if (!src.isEmpty()) - result = src + java.io.File.separator; - else - result = ""; - - return result + name; + // Process children + processChildren(directoryCoverage.children(), context, currentDirectory); } private void processFile(FileStatementCoverage fileCoverage, SensorContext context, String directory) { - File scalaSourcefile = new ScalaFile(appendFilePath(directory, fileCoverage.name())); - context.saveMeasure(scalaSourcefile, new Measure(CoreMetrics.COVERAGE, fileCoverage.rate())); + ScalaFile scalaSourcefile = new ScalaFile(appendFilePath(directory, fileCoverage.name())); + context.saveMeasure(scalaSourcefile, createStatementCoverage(fileCoverage.rate())); log("Process file [" + scalaSourcefile.getKey() + ", " + fileCoverage.rate() + "]"); } - private void processChildren(scala.collection.Iterable children, - Project project, SensorContext context, + private void processChildren(scala.collection.Iterable children, SensorContext context, String directory) { - log("Process children [" + directory + "]"); - // Process children for (StatementCoverage child: JavaConversions.asJavaIterable(children)) { - processChild(child, project, context, directory); + processChild(child, context, directory); } } - private void processChild(StatementCoverage dirOrFile, - Project project, SensorContext context, + private void processChild(StatementCoverage dirOrFile, SensorContext context, String directory) { if (dirOrFile instanceof ParentStatementCoverage) { - processDirectory((ParentStatementCoverage) dirOrFile, project, context, directory); + processDirectory((ParentStatementCoverage) dirOrFile, context, directory); } else { if (dirOrFile instanceof FileStatementCoverage) { @@ -108,6 +93,21 @@ private void processChild(StatementCoverage dirOrFile, } } + private Measure createStatementCoverage(Double rate) { + return new Measure(ScalaMetrics.STATEMENT_COVERAGE, rate); + } + + private String appendFilePath(String src, String name) { + String result; + if (!src.isEmpty()) + result = src + java.io.File.separator; + else + result = ""; + + return result + name; + } + + private static void log(String message) { log.info("[Scoverage] " + message); } @@ -117,7 +117,7 @@ private void parseFakeReport(Project project, final SensorContext context) { HashMap dirs = new HashMap(); for (InputFile sourceFile : fileSystem.mainFiles("scala")) { - File scalaSourcefile = new ScalaFile(File.fromIOFile(sourceFile.getFile(), project).getKey()); + ScalaFile scalaSourcefile = new ScalaFile(File.fromIOFile(sourceFile.getFile(), project).getKey()); context.saveMeasure(scalaSourcefile, new Measure(CoreMetrics.COVERAGE, 51.4)); log("Process fake file [" + scalaSourcefile.getKey() + "]"); diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java index 84d13f6..27e0705 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java @@ -1,7 +1,7 @@ package com.buransky.plugins.scoverage.sensor; import com.buransky.plugins.scoverage.language.Scala; -import com.buransky.plugins.scoverage.language.ScalaFile; +import com.buransky.plugins.scoverage.resource.ScalaFile; import org.apache.commons.io.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,15 +39,18 @@ public void analyse(Project project, SensorContext sensorContext) { } } + @Override + public String toString() { + return "Scoverage source importer"; + } + private void addFileToSonar(Project project, SensorContext sensorContext, InputFile inputFile, String charset) { try { String source = FileUtils.readFileToString(inputFile.getFile(), charset); - ScalaFile resource = new ScalaFile(File.fromIOFile(inputFile.getFile(), project).getKey()); - if (resource == null) { - LOGGER.warn("[ScoverageSourceImporterSensor] Resource null! " + inputFile.getRelativePath()); - return; - } + + String key = File.fromIOFile(inputFile.getFile(), project).getKey(); + ScalaFile resource = new ScalaFile(key); sensorContext.index(resource); sensorContext.saveSource(resource, source); diff --git a/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java b/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java new file mode 100644 index 0000000..096716d --- /dev/null +++ b/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java @@ -0,0 +1,20 @@ +package com.buransky.plugins.scoverage.widget; + +import org.sonar.api.web.AbstractRubyTemplate; +import org.sonar.api.web.RubyRailsWidget; + +public class ScoverageWidget extends AbstractRubyTemplate implements RubyRailsWidget { + + public String getId() { + return "scoverage"; + } + + public String getTitle() { + return "Statement coverage"; + } + + @Override + protected String getTemplatePath() { + return "/com/buransky/plugins/scoverage/widget.html.erb"; + } +} diff --git a/src/main/resources/com/buransky/plugins/scoverage/widget.html.erb b/src/main/resources/com/buransky/plugins/scoverage/widget.html.erb new file mode 100644 index 0000000..065d5c8 --- /dev/null +++ b/src/main/resources/com/buransky/plugins/scoverage/widget.html.erb @@ -0,0 +1,10 @@ +<% measure=measure('scoverage') + if measure +%> +
+

+ Statement coverage : <%= format_measure(measure, :suffix => ' %') %> + <%= dashboard_configuration.selected_period? ? format_variation(measure) : trend_icon(measure) -%> +

+
+<% end %> \ No newline at end of file From 3f92496f3490b6a8354ac30cfe478a517a57cd1f Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Tue, 4 Feb 2014 18:08:54 -0800 Subject: [PATCH 010/101] . --- .../scoverage/sensor/ScoverageSensor.java | 21 +++++++++++++++---- .../plugins/scoverage/ScoverageParser.scala | 16 +++++++++----- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java index 06c905e..24ba564 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java @@ -1,9 +1,6 @@ package com.buransky.plugins.scoverage.sensor; -import com.buransky.plugins.scoverage.FileStatementCoverage; -import com.buransky.plugins.scoverage.ParentStatementCoverage; -import com.buransky.plugins.scoverage.ScoverageParser; -import com.buransky.plugins.scoverage.StatementCoverage; +import com.buransky.plugins.scoverage.*; import com.buransky.plugins.scoverage.language.Scala; import com.buransky.plugins.scoverage.measure.ScalaMetrics; import com.buransky.plugins.scoverage.resource.ScalaDirectory; @@ -14,6 +11,7 @@ import org.sonar.api.batch.Sensor; import org.sonar.api.batch.SensorContext; import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.CoverageMeasuresBuilder; import org.sonar.api.measures.Measure; import org.sonar.api.resources.*; import scala.collection.JavaConversions; @@ -67,6 +65,21 @@ private void processFile(FileStatementCoverage fileCoverage, SensorContext conte context.saveMeasure(scalaSourcefile, createStatementCoverage(fileCoverage.rate())); log("Process file [" + scalaSourcefile.getKey() + ", " + fileCoverage.rate() + "]"); + + // Save line coverage. This is needed just for source code highlighting. + saveLineCoverage(fileCoverage.lines(), scalaSourcefile, context); + } + + private void saveLineCoverage(scala.collection.Iterable coveredLines, + ScalaFile scalaSourcefile, SensorContext context) { + CoverageMeasuresBuilder coverage = CoverageMeasuresBuilder.create(); + for (CoveredLine coveredLine: JavaConversions.asJavaIterable(coveredLines)) { + coverage.setHits(coveredLine.line(), coveredLine.hitCount()); + } + + for (Measure measure : coverage.createMeasures()) { + context.saveMeasure(scalaSourcefile, measure); + } } private void processChildren(scala.collection.Iterable children, SensorContext context, diff --git a/src/main/scala/com/buransky/plugins/scoverage/ScoverageParser.scala b/src/main/scala/com/buransky/plugins/scoverage/ScoverageParser.scala index df4dcaa..9e176f3 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/ScoverageParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/ScoverageParser.scala @@ -3,13 +3,17 @@ package com.buransky.plugins.scoverage object ScoverageParser { def parse(scoverageXmlPath: String): ParentStatementCoverage = { - val errorCodeFile = FileStatementCoverage("ErrorCode.scala", 17, 13) - val graphFile = FileStatementCoverage("Graph.scala", 42, 0) + val errorCodeFile = FileStatementCoverage("ErrorCode.scala", 17, 13, + List(CoveredLine(10, 2), CoveredLine(11, 0), + CoveredLine(25, 1))) - val file2 = FileStatementCoverage("file2.scala", 2, 1) + val graphFile = FileStatementCoverage("Graph.scala", 42, 0, + List(CoveredLine(33, 0), CoveredLine(3, 1), CoveredLine(1, 0), CoveredLine(2, 2))) + + val file2 = FileStatementCoverage("file2.scala", 2, 1, Nil) val bbbDir = ParentStatementCoverage("bbb", Seq(file2)) - val file1 = FileStatementCoverage("file1.scala", 100, 33) + val file1 = FileStatementCoverage("file1.scala", 100, 33, Nil) val aaaDir = ParentStatementCoverage("aaa", Seq(file1, errorCodeFile, graphFile, bbbDir)) val project = ParentStatementCoverage("project", Seq(aaaDir)) @@ -40,4 +44,6 @@ case class ParentStatementCoverage(name: String, children: Iterable[StatementCov } case class FileStatementCoverage(name: String, statementsCount: Int, - coveredStatementsCount: Int) extends StatementCoverage \ No newline at end of file + coveredStatementsCount: Int, lines: Iterable[CoveredLine]) extends StatementCoverage + +case class CoveredLine(line: Int, hitCount: Int) \ No newline at end of file From a85e82b0168402fe77b2acd7db7330be694b821e Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Wed, 5 Feb 2014 16:34:40 -0800 Subject: [PATCH 011/101] . --- .../scoverage/sensor/ScoverageSensor.java | 30 +++++-- .../plugins/scoverage/ScoverageParser.scala | 49 ---------- .../scoverage/ScoverageReportParser.scala | 5 ++ .../plugins/scoverage/StatementCoverage.scala | 89 +++++++++++++++++++ .../scoverage/xml/ScoverageParser.scala | 27 ++++++ 5 files changed, 144 insertions(+), 56 deletions(-) delete mode 100644 src/main/scala/com/buransky/plugins/scoverage/ScoverageParser.scala create mode 100644 src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala create mode 100644 src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala create mode 100644 src/main/scala/com/buransky/plugins/scoverage/xml/ScoverageParser.scala diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java index 24ba564..1c08fe4 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java @@ -5,6 +5,7 @@ import com.buransky.plugins.scoverage.measure.ScalaMetrics; import com.buransky.plugins.scoverage.resource.ScalaDirectory; import com.buransky.plugins.scoverage.resource.ScalaFile; +import com.buransky.plugins.scoverage.xml.XmlScoverageReportParser$; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.batch.CoverageExtension; @@ -21,13 +22,22 @@ public class ScoverageSensor implements Sensor, CoverageExtension { private static final Logger log = LoggerFactory.getLogger(ScoverageSensor.class); + private final ScoverageReportParser scoverageReportParser; + + public ScoverageSensor() { + this(XmlScoverageReportParser$.MODULE$); + } + + public ScoverageSensor(ScoverageReportParser scoverageReportParser) { + this.scoverageReportParser = scoverageReportParser; + } public boolean shouldExecuteOnProject(Project project) { return project.getAnalysisType().isDynamic(true) && Scala.INSTANCE.getKey().equals(project.getLanguageKey()); } public void analyse(Project project, SensorContext context) { - processProject(ScoverageParser.parse(""), project, context); + processProject(scoverageReportParser.parse(""), project, context); //parseFakeReport(project, context); } @@ -36,7 +46,7 @@ public String toString() { return "Scoverage sensor"; } - private void processProject(ParentStatementCoverage projectCoverage, + private void processProject(ProjectStatementCoverage projectCoverage, Project project, SensorContext context) { // Save project measure context.saveMeasure(project, createStatementCoverage(projectCoverage.rate())); @@ -46,7 +56,7 @@ private void processProject(ParentStatementCoverage projectCoverage, processChildren(projectCoverage.children(), context, ""); } - private void processDirectory(ParentStatementCoverage directoryCoverage, SensorContext context, + private void processDirectory(DirectoryStatementCoverage directoryCoverage, SensorContext context, String parentDirectory) { String currentDirectory = appendFilePath(parentDirectory, directoryCoverage.name()); @@ -67,16 +77,22 @@ private void processFile(FileStatementCoverage fileCoverage, SensorContext conte log("Process file [" + scalaSourcefile.getKey() + ", " + fileCoverage.rate() + "]"); // Save line coverage. This is needed just for source code highlighting. - saveLineCoverage(fileCoverage.lines(), scalaSourcefile, context); + saveLineCoverage(fileCoverage.statements(), scalaSourcefile, context); } - private void saveLineCoverage(scala.collection.Iterable coveredLines, + private void saveLineCoverage(scala.collection.Iterable coveredStatements, ScalaFile scalaSourcefile, SensorContext context) { + // Convert statements to lines + scala.collection.Iterable coveredLines = + StatementCoverage$.MODULE$.statementCoverageToLineCoverage(coveredStatements); + + // Set line hits CoverageMeasuresBuilder coverage = CoverageMeasuresBuilder.create(); for (CoveredLine coveredLine: JavaConversions.asJavaIterable(coveredLines)) { coverage.setHits(coveredLine.line(), coveredLine.hitCount()); } + // Save measures for (Measure measure : coverage.createMeasures()) { context.saveMeasure(scalaSourcefile, measure); } @@ -92,8 +108,8 @@ private void processChildren(scala.collection.Iterable childr private void processChild(StatementCoverage dirOrFile, SensorContext context, String directory) { - if (dirOrFile instanceof ParentStatementCoverage) { - processDirectory((ParentStatementCoverage) dirOrFile, context, directory); + if (dirOrFile instanceof DirectoryStatementCoverage) { + processDirectory((DirectoryStatementCoverage) dirOrFile, context, directory); } else { if (dirOrFile instanceof FileStatementCoverage) { diff --git a/src/main/scala/com/buransky/plugins/scoverage/ScoverageParser.scala b/src/main/scala/com/buransky/plugins/scoverage/ScoverageParser.scala deleted file mode 100644 index 9e176f3..0000000 --- a/src/main/scala/com/buransky/plugins/scoverage/ScoverageParser.scala +++ /dev/null @@ -1,49 +0,0 @@ -package com.buransky.plugins.scoverage - - -object ScoverageParser { - def parse(scoverageXmlPath: String): ParentStatementCoverage = { - val errorCodeFile = FileStatementCoverage("ErrorCode.scala", 17, 13, - List(CoveredLine(10, 2), CoveredLine(11, 0), - CoveredLine(25, 1))) - - val graphFile = FileStatementCoverage("Graph.scala", 42, 0, - List(CoveredLine(33, 0), CoveredLine(3, 1), CoveredLine(1, 0), CoveredLine(2, 2))) - - val file2 = FileStatementCoverage("file2.scala", 2, 1, Nil) - val bbbDir = ParentStatementCoverage("bbb", Seq(file2)) - - val file1 = FileStatementCoverage("file1.scala", 100, 33, Nil) - val aaaDir = ParentStatementCoverage("aaa", Seq(file1, errorCodeFile, graphFile, bbbDir)) - - val project = ParentStatementCoverage("project", Seq(aaaDir)) - - project - } -} - -trait StatementCoverage { - lazy val rate: Double = (coveredStatementsCount.toDouble / statementsCount.toDouble) * 100.0 - - val name: String - val statementsCount: Int - val coveredStatementsCount: Int - - require(statementsCount >= 0, "Statements count cannot be negative! [" + statementsCount + "]") - require(coveredStatementsCount >= 0, "Statements count cannot be negative! [" + - coveredStatementsCount + "]") - require(coveredStatementsCount <= statementsCount, - "Number of covered statements cannot be more than total number of statements! [" + - statementsCount + ", " + coveredStatementsCount + "]") -} - -case class ParentStatementCoverage(name: String, children: Iterable[StatementCoverage]) - extends StatementCoverage { - val statementsCount = children.map(_.statementsCount).sum - val coveredStatementsCount = children.map(_.coveredStatementsCount).sum -} - -case class FileStatementCoverage(name: String, statementsCount: Int, - coveredStatementsCount: Int, lines: Iterable[CoveredLine]) extends StatementCoverage - -case class CoveredLine(line: Int, hitCount: Int) \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala new file mode 100644 index 0000000..5af2eb9 --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala @@ -0,0 +1,5 @@ +package com.buransky.plugins.scoverage + +trait ScoverageReportParser { + def parse(scoverageReportPath: String): ProjectStatementCoverage +} diff --git a/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala b/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala new file mode 100644 index 0000000..e28108c --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala @@ -0,0 +1,89 @@ +package com.buransky.plugins.scoverage + +/** + * Statement coverage represents rate at which are statements of a certain source code unit + * being covered by tests. + */ +sealed trait StatementCoverage { + /** + * Percentage rate ranging from 0 up to 100%. + */ + lazy val rate: Double = (coveredStatementsCount.toDouble / statementCount.toDouble) * 100.0 + + /** + * Total number of all statements within the source code unit, + */ + val statementCount: Int + + /** + * Number of statements covered by unit tests. + */ + val coveredStatementsCount: Int + + require(statementCount >= 0, "Statements count cannot be negative! [" + statementCount + "]") + require(coveredStatementsCount >= 0, "Statements count cannot be negative! [" + + coveredStatementsCount + "]") + require(coveredStatementsCount <= statementCount, + "Number of covered statements cannot be more than total number of statements! [" + + statementCount + ", " + coveredStatementsCount + "]") +} + +/** + * Allows to build tree structure from state coverage values. + */ +trait NodeStatementCoverage extends StatementCoverage { + val children: Iterable[StatementCoverage] + val statementCount = children.map(_.statementCount).sum + val coveredStatementsCount = children.map(_.coveredStatementsCount).sum +} + +/** + * Root node. In multi-module projects it can contain other ProjectStatementCoverage + * elements as children. + * + * @param name Name of the project or module. + * @param children + */ +case class ProjectStatementCoverage(name: String, children: Iterable[StatementCoverage]) + extends NodeStatementCoverage + +case class DirectoryStatementCoverage(name: String, children: Iterable[StatementCoverage]) + extends NodeStatementCoverage + +case class FileStatementCoverage(name: String, statementCount: Int, coveredStatementsCount: Int, + statements: Iterable[CoveredStatement]) extends StatementCoverage + +case class StatementPosition(line: Int, pos: Int) + +case class CoveredStatement(start: StatementPosition, end: StatementPosition, hitCount: Int) + +case class CoveredLine(line: Int, hitCount: Int) + +object StatementCoverage { + /** + * Utility method to transform statement coverage to line coverage. Pessimistic logic is used + * meaning that line hit count is minimum of hit counts of all statements on the given line. + * + * Example: If a line contains two statements, one is covered by 3 hits, the other one is + * without any hits, then the whole line is treated as uncovered. + * + * @param statements Statement coverage. + * @return Line coverage. + */ + def statementCoverageToLineCoverage(statements: Iterable[CoveredStatement]): Iterable[CoveredLine] = { + // Handle statements that end on a different line than start + val multilineStatements = statements.filter { s => s.start.line != s.end.line } + val extraStatements = multilineStatements.flatMap { s => + for (i <- (s.start.line + 1) to s.end.line) + yield CoveredStatement(StatementPosition(i, 0), StatementPosition(i, 0), s.hitCount) + } + + // Group statements by starting line + val lineStatements = (statements ++ extraStatements).groupBy(_.start.line) + + // Pessimistic approach: line hit count is a minimum of hit counts of all statements on the line + lineStatements.map { lineStatement => + CoveredLine(lineStatement._1, lineStatement._2.map(_.hitCount).min) + } + } +} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/ScoverageParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/ScoverageParser.scala new file mode 100644 index 0000000..f7c269e --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scoverage/xml/ScoverageParser.scala @@ -0,0 +1,27 @@ +package com.buransky.plugins.scoverage.xml + +import com.buransky.plugins.scoverage._ + +object XmlScoverageReportParser extends ScoverageReportParser { + def parse(scoverageReportPath: String): ProjectStatementCoverage = { + val errorCodeFile = FileStatementCoverage("ErrorCode.scala", 17, 13, + List(simpleStatement(10, 2), simpleStatement(11, 0), + simpleStatement(25, 1))) + + val graphFile = FileStatementCoverage("Graph.scala", 42, 0, + List(simpleStatement(33, 0), simpleStatement(3, 1), simpleStatement(1, 0), simpleStatement(2, 2))) + + val file2 = FileStatementCoverage("file2.scala", 2, 1, Nil) + val bbbDir = DirectoryStatementCoverage("bbb", Seq(file2)) + + val file1 = FileStatementCoverage("file1.scala", 100, 33, Nil) + val aaaDir = DirectoryStatementCoverage("aaa", Seq(file1, errorCodeFile, graphFile, bbbDir)) + + val project = ProjectStatementCoverage("project", Seq(aaaDir)) + + project + } + + private def simpleStatement(line: Int, hitCount: Int): CoveredStatement = + CoveredStatement(StatementPosition(line, 0), StatementPosition(line, 0), hitCount) +} From 957d5f96f075d4028502c34cfa5708fd1e8adaa5 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Wed, 5 Feb 2014 21:27:06 -0800 Subject: [PATCH 012/101] XML parser --- pom.xml | 11 +- .../scoverage/sensor/ScoverageSensor.java | 4 +- .../scoverage/ScoverageReportParser.scala | 5 +- .../plugins/scoverage/StatementCoverage.scala | 2 +- ....scala => StubScoverageReportParser.scala} | 17 +- .../xml/XmlScoverageReportParser.scala | 189 +++++++++ .../xml/XmlScoverageReportParserSpec.scala | 35 ++ .../scoverage/xml/data/XmlReportFile1.scala | 394 ++++++++++++++++++ 8 files changed, 642 insertions(+), 15 deletions(-) rename src/main/scala/com/buransky/plugins/scoverage/xml/{ScoverageParser.scala => StubScoverageReportParser.scala} (64%) create mode 100644 src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala create mode 100644 src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala create mode 100644 src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala diff --git a/pom.xml b/pom.xml index 699c885..8265e88 100644 --- a/pom.xml +++ b/pom.xml @@ -78,7 +78,7 @@ com.buransky.plugins.scoverage.ScoveragePlugin - 2.9.1 + 2.10.3 @@ -109,11 +109,6 @@ scala-compiler ${scala.version} - - org.scalariform - scalariform_${scala.version} - 0.1.1 - @@ -123,8 +118,8 @@ org.scalatest - scalatest_${scala.version} - 1.6.1 + scalatest_2.10 + 2.0 test diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java index 1c08fe4..161a01e 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java @@ -25,7 +25,7 @@ public class ScoverageSensor implements Sensor, CoverageExtension { private final ScoverageReportParser scoverageReportParser; public ScoverageSensor() { - this(XmlScoverageReportParser$.MODULE$); + this(XmlScoverageReportParser$.MODULE$.apply("")); } public ScoverageSensor(ScoverageReportParser scoverageReportParser) { @@ -37,7 +37,7 @@ public boolean shouldExecuteOnProject(Project project) { } public void analyse(Project project, SensorContext context) { - processProject(scoverageReportParser.parse(""), project, context); + processProject(scoverageReportParser.parse(), project, context); //parseFakeReport(project, context); } diff --git a/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala index 5af2eb9..432ce55 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala @@ -1,5 +1,8 @@ package com.buransky.plugins.scoverage trait ScoverageReportParser { - def parse(scoverageReportPath: String): ProjectStatementCoverage + def parse(): ProjectStatementCoverage } + +case class ScoverageException(message: String, source: Throwable = null) + extends Exception(message, source) diff --git a/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala b/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala index e28108c..baa5431 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala @@ -75,7 +75,7 @@ object StatementCoverage { val multilineStatements = statements.filter { s => s.start.line != s.end.line } val extraStatements = multilineStatements.flatMap { s => for (i <- (s.start.line + 1) to s.end.line) - yield CoveredStatement(StatementPosition(i, 0), StatementPosition(i, 0), s.hitCount) + yield CoveredStatement(StatementPosition(i, 0), StatementPosition(i, 0), s.hitCount) } // Group statements by starting line diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/ScoverageParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala similarity index 64% rename from src/main/scala/com/buransky/plugins/scoverage/xml/ScoverageParser.scala rename to src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala index f7c269e..8446ea0 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/xml/ScoverageParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala @@ -1,9 +1,15 @@ package com.buransky.plugins.scoverage.xml import com.buransky.plugins.scoverage._ - -object XmlScoverageReportParser extends ScoverageReportParser { - def parse(scoverageReportPath: String): ProjectStatementCoverage = { +import com.buransky.plugins.scoverage.ProjectStatementCoverage +import com.buransky.plugins.scoverage.CoveredStatement +import com.buransky.plugins.scoverage.StatementPosition +import com.buransky.plugins.scoverage.FileStatementCoverage +import com.buransky.plugins.scoverage.DirectoryStatementCoverage +import scala.io.Source + +class StubScoverageReportParser extends ScoverageReportParser { + def parse(): ProjectStatementCoverage = { val errorCodeFile = FileStatementCoverage("ErrorCode.scala", 17, 13, List(simpleStatement(10, 2), simpleStatement(11, 0), simpleStatement(25, 1))) @@ -22,6 +28,11 @@ object XmlScoverageReportParser extends ScoverageReportParser { project } + def parse(source: Source): ProjectStatementCoverage = { + ProjectStatementCoverage("x", Nil) + } + private def simpleStatement(line: Int, hitCount: Int): CoveredStatement = CoveredStatement(StatementPosition(line, 0), StatementPosition(line, 0), hitCount) + } diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala new file mode 100644 index 0000000..ad1b4d4 --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala @@ -0,0 +1,189 @@ +package com.buransky.plugins.scoverage.xml + +import com.buransky.plugins.scoverage._ +import scala.io.Source +import scala.xml.parsing.ConstructingParser +import scala.xml.{Text, NamespaceBinding, MetaData} +import org.apache.log4j.Logger +import scala.collection.mutable +import java.nio.file.Paths +import scala.annotation.tailrec +import java.io.File + +class XmlScoverageReportParser(source: Source) extends ConstructingParser(source, false) + with ScoverageReportParser { + private val log = Logger.getLogger(classOf[XmlScoverageReportParser]) + + private val CLASS_ELEMENT = "class" + private val FILENAME_ATTRIBUTE = "filename" + private val STATEMENT_ELEMENT = "statement" + private val START_ATTRIBUTE = "start" + private val LINE_ATTRIBUTE = "line" + private val INVOCATION_COUNT_ATTRIBUTE = "invocation-count" + + val statementsInFile: mutable.HashMap[String, List[CoveredStatement]] = mutable.HashMap.empty + var currentFilePath: Option[String] = None + + def parse(): ProjectStatementCoverage = { + // Initialze + nextch() + + // Parse + document() + + // Transform map to project + projectFromMap(statementsInFile.toMap) + } + + override def elemStart(pos: Int, pre: String, label: String, attrs: MetaData, scope: NamespaceBinding) { + label match { + case CLASS_ELEMENT => { + currentFilePath = Some(fixLeadingSlash(getText(attrs, FILENAME_ATTRIBUTE))) + log.debug("Current file path: " + currentFilePath.get) + } + case STATEMENT_ELEMENT => { + currentFilePath match { + case Some(cfp) => { + val start = getInt(attrs, START_ATTRIBUTE) + val line = getInt(attrs, LINE_ATTRIBUTE) + val hits = getInt(attrs, INVOCATION_COUNT_ATTRIBUTE) + + // Add covered statement to the mutable map + val pos = StatementPosition(line, start) + addCoveredStatement(cfp, CoveredStatement(pos, pos, hits)) + + log.debug("Statement added: " + line + ", " + hits + ", " + start) + } + case None => throw new ScoverageException("Current file path not set!") + } + } + case _ => // Nothing to do + } + + super.elemStart(pos, pre, label, attrs, scope) + } + + private def addCoveredStatement(filePath: String, coveredStatement: CoveredStatement) { + statementsInFile.get(filePath) match { + case None => statementsInFile.put(filePath, List(coveredStatement)) + case Some(s) => statementsInFile.update(filePath, coveredStatement :: s) + } + } + + /** + * Remove this when scoverage is fixed! + */ + private def fixLeadingSlash(filePath: String) = { + if (filePath.startsWith(File.separator)) + filePath.drop(File.separator.length) + else + filePath + } + + private def getInt(attrs: MetaData, name: String) = getText(attrs, name).toInt + + private def getText(attrs: MetaData, name: String): String = { + attrs.get(name) match { + case Some(attr) => { + attr match { + case text: Text => text.toString + case _ => throw new ScoverageException("Not a text attribute!") + } + } + case None => throw new ScoverageException("Attribute doesn't exit! [" + name + "]") + } + } + + private case class DirOrFile(name: String, var children: List[DirOrFile], + coverage: Option[FileStatementCoverage]) { + def get(name: String) = children.find(_.name == name) + + @tailrec + final def add(chain: DirOrFile) { + get(chain.name) match { + case None => children = chain :: children + case Some(child) => { + chain.children match { + case h :: t => { + if (t != Nil) + throw new IllegalStateException("This is not a linear chain!") + + child.add(h) + } + case _ => // Duplicate file? Should not happen. + } + } + } + } + + def toStatementCoverage: StatementCoverage = { + val childNodes = children.map(_.toStatementCoverage) + + childNodes match { + case Nil => coverage.get + case _ => DirectoryStatementCoverage(name, childNodes) + } + } + } + + private def projectFromMap(statementsInFile: Map[String, List[CoveredStatement]]): + ProjectStatementCoverage = { + + // Transform to file statement coverage + val files = fileStatementCoverage(statementsInFile) + + // Transform file paths to chain of case classes + val chained = files.map(fsc => pathToChain(fsc._1, fsc._2)) + + // Merge chains into one tree + val root = DirOrFile("", Nil, None) + chained.foreach(root.add(_)) + + // Transform file system tree into coverage structure tree + ProjectStatementCoverage("", List(root.toStatementCoverage)) + } + + private def pathToChain(filePath: String, coverage: FileStatementCoverage): DirOrFile = { + val path = Paths.get(filePath) + + val names = for (i <- 0 to path.getNameCount - 1) + yield path.getFileName.toString + + names.foldLeft(DirOrFile("", Nil, Some(coverage))) { (dirOrFile, name) => + val child = DirOrFile(name, Nil, dirOrFile.coverage) + dirOrFile.children = List(child) + child + } + } + + private def fileStatementCoverage(statementsInFile: Map[String, List[CoveredStatement]]): + Map[String, FileStatementCoverage] = { + statementsInFile.map { sif => + val fileStatementCoverage = FileStatementCoverage(Paths.get(sif._1).getFileName.toString, + sif._2.length, coveredStatements(sif._2), sif._2) + + (sif._1, fileStatementCoverage) + } + } + + private def coveredStatements(statements: Iterable[CoveredStatement]) = + statements.count(_.hitCount > 0) +} + +object XmlScoverageReportParser { + def apply(scoverageReportPath: String): XmlScoverageReportParser = { + require(scoverageReportPath != null) + require(!scoverageReportPath.trim.isEmpty) + + new XmlScoverageReportParser(sourceFromFile(scoverageReportPath)) + } + + private def sourceFromFile(scoverageReportPath: String) = { + try { + Source.fromFile(scoverageReportPath) + } + catch { + case ex: Exception => throw ScoverageException("Cannot parse file! [" + scoverageReportPath + "]", ex) + } + } +} \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala new file mode 100644 index 0000000..437a4a5 --- /dev/null +++ b/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala @@ -0,0 +1,35 @@ +package com.buransky.plugins.scoverage.xml + +import org.scalatest.{FlatSpec, Matchers} +import org.scalatest.junit.JUnitRunner +import org.junit.runner.RunWith +import com.buransky.plugins.scoverage.xml.data.XmlReportFile1 +import com.buransky.plugins.scoverage.ScoverageException +import scala.io.Source + +@RunWith(classOf[JUnitRunner]) +class XmlScoverageReportParserSpec extends FlatSpec with Matchers { + behavior of "parse file path" + + it must "fail for null path" in { + the[IllegalArgumentException] thrownBy XmlScoverageReportParser(null.asInstanceOf[String]) + } + + it must "fail for empty path" in { + the[IllegalArgumentException] thrownBy XmlScoverageReportParser("") + } + + it must "fail for not existing path" in { + the[ScoverageException] thrownBy XmlScoverageReportParser("/x/a/b/c/1/2/3/4.xml") + } + + behavior of "parse source" + + it must "work" in { + val parser = new XmlScoverageReportParser(Source.fromString(XmlReportFile1.data)) + val projectCoverage = parser.parse() + + val expected = BigDecimal(24.53) + BigDecimal(projectCoverage.rate).setScale(2, BigDecimal.RoundingMode.HALF_UP) should equal(expected) + } +} diff --git a/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala b/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala new file mode 100644 index 0000000..b450837 --- /dev/null +++ b/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala @@ -0,0 +1,394 @@ +package com.buransky.plugins.scoverage.xml.data + +object XmlReportFile1 { + val data = + """ + | + | + | + | + | + | + | + | + | + | MyServiceClientError.this.error("zipcodeinvalid") + | + | + | + | + | + | + | + | + | + | + | 2 + | + | + | 3 + | + | + | scala.Some.apply[String]("One") + | + | + | new $anon() + | + | + | + | + | + | + | + | + | + | + | MyServiceLogicError.this.error("logicfailed") + | + | + | + | + | + | + | + | + | + | + | StructuredErrorCode.this.parent.toString() + | + | + | p.==("") + | + | + | "" + | + | + | { + | scoverage.Invoker.invoked(8, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | "" + |} + | + | + | p.+("-") + | + | + | { + | scoverage.Invoker.invoked(10, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | p.+("-") + |} + | + | + | StructuredErrorCode.this.name + | + | + | if ({ + | scoverage.Invoker.invoked(7, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | p.==("") + |}) + | { + | scoverage.Invoker.invoked(9, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | { + | scoverage.Invoker.invoked(8, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | "" + | } + | } + |else + | { + | scoverage.Invoker.invoked(11, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | { + | scoverage.Invoker.invoked(10, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | p.+("-") + | } + | }.+({ + | scoverage.Invoker.invoked(12, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | StructuredErrorCode.this.name + |}) + | + | + | + | + | + | + | errorCode.==(this) + | + | + | true + | + | + | { + | scoverage.Invoker.invoked(2, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | true + |} + | + | + | StructuredErrorCode.this.parent.is(errorCode) + | + | + | { + | scoverage.Invoker.invoked(4, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | StructuredErrorCode.this.parent.is(errorCode) + |} + | + | + | + | + | + | + | StructuredErrorCode.apply(name, this) + | + | + | + | + | + | + | + | + | + | + | ClientError.required + | + | + | scala.this.Predef.println({ + | scoverage.Invoker.invoked(25, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | ClientError.required + |}) + | + | + | ClientError.invalid + | + | + | scala.this.Predef.println({ + | scoverage.Invoker.invoked(27, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | ClientError.invalid + |}) + | + | + | scala.this.Predef.println(MySqlError) + | + | + | MySqlError.syntax + | + | + | scala.this.Predef.println({ + | scoverage.Invoker.invoked(30, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | MySqlError.syntax + |}) + | + | + | MyServiceLogicError.logicFailed + | + | + | scala.this.Predef.println({ + | scoverage.Invoker.invoked(32, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | MyServiceLogicError.logicFailed + |}) + | + | + | ClientError.required + | + | + | e + | + | + | scala.this.Predef.println("required") + | + | + | { + | scoverage.Invoker.invoked(36, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scala.this.Predef.println("required") + |} + | + | + | scala.this.Predef.println("invalid") + | + | + | { + | scoverage.Invoker.invoked(38, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scala.this.Predef.println("invalid") + |} + | + | + | () + | + | + | { + | scoverage.Invoker.invoked(40, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | () + |} + | + | + | MyServiceServerError.mongoDbError.is(ServerError) + | + | + | scala.this.Predef.println("This is a server error") + | + | + | { + | scoverage.Invoker.invoked(43, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scala.this.Predef.println("This is a server error") + |} + | + | + | () + | + | + | { + | scoverage.Invoker.invoked(45, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | () + |} + | + | + | + | + | + | + | + | + | + | + | MySqlError.this.error("syntax") + | + | + | MySqlError.this.error("connection") + | + | + | + | + | + | + | + | + | + | + | MyServiceServerError.this.error("mongodberror") + | + | + | + | + | + | + | + | + | + | + | "" + | + | + | + | + | + | + | false + | + | + | + | + | + | + | + | + | + | + | ServerError.this.error("solar") + | + | + | + | + | + | + | + | + | + | + | ClientError.this.error("required") + | + | + | ClientError.this.error("invalid") + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | aaa.MakeRectangleModelFromFile.apply(null) + | + | + | x.isInstanceOf[Serializable] + | + | + | scala.this.Predef.println({ + | scoverage.Invoker.invoked(52, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | x.isInstanceOf[Serializable] + |}) + | + | + | + | + | + | + | + | + | + """.stripMargin +} From c48f6879d4f71773e2ce957317666d5204b17955 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Wed, 5 Feb 2014 21:42:23 -0800 Subject: [PATCH 013/101] . --- .../scoverage/sensor/ScoverageSensor.java | 8 +- .../scoverage/ScoverageReportParser.scala | 2 +- .../xml/StubScoverageReportParser.scala | 2 +- ...XmlScoverageReportConstructingParser.scala | 170 ++++++++++++++++ .../xml/XmlScoverageReportParser.scala | 184 ++---------------- ...coverageReportConstructingParserSpec.scala | 20 ++ .../xml/XmlScoverageReportParserSpec.scala | 16 +- 7 files changed, 212 insertions(+), 190 deletions(-) create mode 100644 src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala create mode 100644 src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java index 161a01e..3428418 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java @@ -25,7 +25,7 @@ public class ScoverageSensor implements Sensor, CoverageExtension { private final ScoverageReportParser scoverageReportParser; public ScoverageSensor() { - this(XmlScoverageReportParser$.MODULE$.apply("")); + this(XmlScoverageReportParser$.MODULE$.apply()); } public ScoverageSensor(ScoverageReportParser scoverageReportParser) { @@ -37,7 +37,7 @@ public boolean shouldExecuteOnProject(Project project) { } public void analyse(Project project, SensorContext context) { - processProject(scoverageReportParser.parse(), project, context); + processProject(scoverageReportParser.parse(getScoverageReportPath(project)), project, context); //parseFakeReport(project, context); } @@ -46,6 +46,10 @@ public String toString() { return "Scoverage sensor"; } + private String getScoverageReportPath(Project project) { + return ""; + } + private void processProject(ProjectStatementCoverage projectCoverage, Project project, SensorContext context) { // Save project measure diff --git a/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala index 432ce55..dd01ab5 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala @@ -1,7 +1,7 @@ package com.buransky.plugins.scoverage trait ScoverageReportParser { - def parse(): ProjectStatementCoverage + def parse(reportFilePath: String): ProjectStatementCoverage } case class ScoverageException(message: String, source: Throwable = null) diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala index 8446ea0..a6bb58b 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala @@ -9,7 +9,7 @@ import com.buransky.plugins.scoverage.DirectoryStatementCoverage import scala.io.Source class StubScoverageReportParser extends ScoverageReportParser { - def parse(): ProjectStatementCoverage = { + def parse(reportFilePath: String): ProjectStatementCoverage = { val errorCodeFile = FileStatementCoverage("ErrorCode.scala", 17, 13, List(simpleStatement(10, 2), simpleStatement(11, 0), simpleStatement(25, 1))) diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala new file mode 100644 index 0000000..fb5a1b6 --- /dev/null +++ b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala @@ -0,0 +1,170 @@ +package com.buransky.plugins.scoverage.xml + +import com.buransky.plugins.scoverage._ +import scala.io.Source +import scala.xml.parsing.ConstructingParser +import scala.xml.{Text, NamespaceBinding, MetaData} +import org.apache.log4j.Logger +import scala.collection.mutable +import java.nio.file.Paths +import scala.annotation.tailrec +import java.io.File + +class XmlScoverageReportConstructingParser(source: Source) extends ConstructingParser(source, false) { + private val log = Logger.getLogger(classOf[XmlScoverageReportConstructingParser]) + + private val CLASS_ELEMENT = "class" + private val FILENAME_ATTRIBUTE = "filename" + private val STATEMENT_ELEMENT = "statement" + private val START_ATTRIBUTE = "start" + private val LINE_ATTRIBUTE = "line" + private val INVOCATION_COUNT_ATTRIBUTE = "invocation-count" + + val statementsInFile: mutable.HashMap[String, List[CoveredStatement]] = mutable.HashMap.empty + var currentFilePath: Option[String] = None + + def parse(): ProjectStatementCoverage = { + // Initialze + nextch() + + // Parse + document() + + // Transform map to project + projectFromMap(statementsInFile.toMap) + } + + override def elemStart(pos: Int, pre: String, label: String, attrs: MetaData, scope: NamespaceBinding) { + label match { + case CLASS_ELEMENT => { + currentFilePath = Some(fixLeadingSlash(getText(attrs, FILENAME_ATTRIBUTE))) + log.debug("Current file path: " + currentFilePath.get) + } + case STATEMENT_ELEMENT => { + currentFilePath match { + case Some(cfp) => { + val start = getInt(attrs, START_ATTRIBUTE) + val line = getInt(attrs, LINE_ATTRIBUTE) + val hits = getInt(attrs, INVOCATION_COUNT_ATTRIBUTE) + + // Add covered statement to the mutable map + val pos = StatementPosition(line, start) + addCoveredStatement(cfp, CoveredStatement(pos, pos, hits)) + + log.debug("Statement added: " + line + ", " + hits + ", " + start) + } + case None => throw new ScoverageException("Current file path not set!") + } + } + case _ => // Nothing to do + } + + super.elemStart(pos, pre, label, attrs, scope) + } + + private def addCoveredStatement(filePath: String, coveredStatement: CoveredStatement) { + statementsInFile.get(filePath) match { + case None => statementsInFile.put(filePath, List(coveredStatement)) + case Some(s) => statementsInFile.update(filePath, coveredStatement :: s) + } + } + + /** + * Remove this when scoverage is fixed! + */ + private def fixLeadingSlash(filePath: String) = { + if (filePath.startsWith(File.separator)) + filePath.drop(File.separator.length) + else + filePath + } + + private def getInt(attrs: MetaData, name: String) = getText(attrs, name).toInt + + private def getText(attrs: MetaData, name: String): String = { + attrs.get(name) match { + case Some(attr) => { + attr match { + case text: Text => text.toString + case _ => throw new ScoverageException("Not a text attribute!") + } + } + case None => throw new ScoverageException("Attribute doesn't exit! [" + name + "]") + } + } + + private case class DirOrFile(name: String, var children: List[DirOrFile], + coverage: Option[FileStatementCoverage]) { + def get(name: String) = children.find(_.name == name) + + @tailrec + final def add(chain: DirOrFile) { + get(chain.name) match { + case None => children = chain :: children + case Some(child) => { + chain.children match { + case h :: t => { + if (t != Nil) + throw new IllegalStateException("This is not a linear chain!") + + child.add(h) + } + case _ => // Duplicate file? Should not happen. + } + } + } + } + + def toStatementCoverage: StatementCoverage = { + val childNodes = children.map(_.toStatementCoverage) + + childNodes match { + case Nil => coverage.get + case _ => DirectoryStatementCoverage(name, childNodes) + } + } + } + + private def projectFromMap(statementsInFile: Map[String, List[CoveredStatement]]): + ProjectStatementCoverage = { + + // Transform to file statement coverage + val files = fileStatementCoverage(statementsInFile) + + // Transform file paths to chain of case classes + val chained = files.map(fsc => pathToChain(fsc._1, fsc._2)) + + // Merge chains into one tree + val root = DirOrFile("", Nil, None) + chained.foreach(root.add(_)) + + // Transform file system tree into coverage structure tree + ProjectStatementCoverage("", List(root.toStatementCoverage)) + } + + private def pathToChain(filePath: String, coverage: FileStatementCoverage): DirOrFile = { + val path = Paths.get(filePath) + + val names = for (i <- 0 to path.getNameCount - 1) + yield path.getFileName.toString + + names.foldLeft(DirOrFile("", Nil, Some(coverage))) { (dirOrFile, name) => + val child = DirOrFile(name, Nil, dirOrFile.coverage) + dirOrFile.children = List(child) + child + } + } + + private def fileStatementCoverage(statementsInFile: Map[String, List[CoveredStatement]]): + Map[String, FileStatementCoverage] = { + statementsInFile.map { sif => + val fileStatementCoverage = FileStatementCoverage(Paths.get(sif._1).getFileName.toString, + sif._2.length, coveredStatements(sif._2), sif._2) + + (sif._1, fileStatementCoverage) + } + } + + private def coveredStatements(statements: Iterable[CoveredStatement]) = + statements.count(_.hitCount > 0) +} \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala index ad1b4d4..4f50569 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala @@ -1,181 +1,15 @@ package com.buransky.plugins.scoverage.xml -import com.buransky.plugins.scoverage._ import scala.io.Source -import scala.xml.parsing.ConstructingParser -import scala.xml.{Text, NamespaceBinding, MetaData} -import org.apache.log4j.Logger -import scala.collection.mutable -import java.nio.file.Paths -import scala.annotation.tailrec -import java.io.File +import com.buransky.plugins.scoverage.{ProjectStatementCoverage, ScoverageReportParser, ScoverageException} -class XmlScoverageReportParser(source: Source) extends ConstructingParser(source, false) - with ScoverageReportParser { - private val log = Logger.getLogger(classOf[XmlScoverageReportParser]) +class XmlScoverageReportParser extends ScoverageReportParser { + def parse(reportFilePath: String): ProjectStatementCoverage = { + require(reportFilePath != null) + require(!reportFilePath.trim.isEmpty) - private val CLASS_ELEMENT = "class" - private val FILENAME_ATTRIBUTE = "filename" - private val STATEMENT_ELEMENT = "statement" - private val START_ATTRIBUTE = "start" - private val LINE_ATTRIBUTE = "line" - private val INVOCATION_COUNT_ATTRIBUTE = "invocation-count" - - val statementsInFile: mutable.HashMap[String, List[CoveredStatement]] = mutable.HashMap.empty - var currentFilePath: Option[String] = None - - def parse(): ProjectStatementCoverage = { - // Initialze - nextch() - - // Parse - document() - - // Transform map to project - projectFromMap(statementsInFile.toMap) - } - - override def elemStart(pos: Int, pre: String, label: String, attrs: MetaData, scope: NamespaceBinding) { - label match { - case CLASS_ELEMENT => { - currentFilePath = Some(fixLeadingSlash(getText(attrs, FILENAME_ATTRIBUTE))) - log.debug("Current file path: " + currentFilePath.get) - } - case STATEMENT_ELEMENT => { - currentFilePath match { - case Some(cfp) => { - val start = getInt(attrs, START_ATTRIBUTE) - val line = getInt(attrs, LINE_ATTRIBUTE) - val hits = getInt(attrs, INVOCATION_COUNT_ATTRIBUTE) - - // Add covered statement to the mutable map - val pos = StatementPosition(line, start) - addCoveredStatement(cfp, CoveredStatement(pos, pos, hits)) - - log.debug("Statement added: " + line + ", " + hits + ", " + start) - } - case None => throw new ScoverageException("Current file path not set!") - } - } - case _ => // Nothing to do - } - - super.elemStart(pos, pre, label, attrs, scope) - } - - private def addCoveredStatement(filePath: String, coveredStatement: CoveredStatement) { - statementsInFile.get(filePath) match { - case None => statementsInFile.put(filePath, List(coveredStatement)) - case Some(s) => statementsInFile.update(filePath, coveredStatement :: s) - } - } - - /** - * Remove this when scoverage is fixed! - */ - private def fixLeadingSlash(filePath: String) = { - if (filePath.startsWith(File.separator)) - filePath.drop(File.separator.length) - else - filePath - } - - private def getInt(attrs: MetaData, name: String) = getText(attrs, name).toInt - - private def getText(attrs: MetaData, name: String): String = { - attrs.get(name) match { - case Some(attr) => { - attr match { - case text: Text => text.toString - case _ => throw new ScoverageException("Not a text attribute!") - } - } - case None => throw new ScoverageException("Attribute doesn't exit! [" + name + "]") - } - } - - private case class DirOrFile(name: String, var children: List[DirOrFile], - coverage: Option[FileStatementCoverage]) { - def get(name: String) = children.find(_.name == name) - - @tailrec - final def add(chain: DirOrFile) { - get(chain.name) match { - case None => children = chain :: children - case Some(child) => { - chain.children match { - case h :: t => { - if (t != Nil) - throw new IllegalStateException("This is not a linear chain!") - - child.add(h) - } - case _ => // Duplicate file? Should not happen. - } - } - } - } - - def toStatementCoverage: StatementCoverage = { - val childNodes = children.map(_.toStatementCoverage) - - childNodes match { - case Nil => coverage.get - case _ => DirectoryStatementCoverage(name, childNodes) - } - } - } - - private def projectFromMap(statementsInFile: Map[String, List[CoveredStatement]]): - ProjectStatementCoverage = { - - // Transform to file statement coverage - val files = fileStatementCoverage(statementsInFile) - - // Transform file paths to chain of case classes - val chained = files.map(fsc => pathToChain(fsc._1, fsc._2)) - - // Merge chains into one tree - val root = DirOrFile("", Nil, None) - chained.foreach(root.add(_)) - - // Transform file system tree into coverage structure tree - ProjectStatementCoverage("", List(root.toStatementCoverage)) - } - - private def pathToChain(filePath: String, coverage: FileStatementCoverage): DirOrFile = { - val path = Paths.get(filePath) - - val names = for (i <- 0 to path.getNameCount - 1) - yield path.getFileName.toString - - names.foldLeft(DirOrFile("", Nil, Some(coverage))) { (dirOrFile, name) => - val child = DirOrFile(name, Nil, dirOrFile.coverage) - dirOrFile.children = List(child) - child - } - } - - private def fileStatementCoverage(statementsInFile: Map[String, List[CoveredStatement]]): - Map[String, FileStatementCoverage] = { - statementsInFile.map { sif => - val fileStatementCoverage = FileStatementCoverage(Paths.get(sif._1).getFileName.toString, - sif._2.length, coveredStatements(sif._2), sif._2) - - (sif._1, fileStatementCoverage) - } - } - - private def coveredStatements(statements: Iterable[CoveredStatement]) = - statements.count(_.hitCount > 0) -} - -object XmlScoverageReportParser { - def apply(scoverageReportPath: String): XmlScoverageReportParser = { - require(scoverageReportPath != null) - require(!scoverageReportPath.trim.isEmpty) - - new XmlScoverageReportParser(sourceFromFile(scoverageReportPath)) + val parser = new XmlScoverageReportConstructingParser(sourceFromFile(reportFilePath)) + parser.parse() } private def sourceFromFile(scoverageReportPath: String) = { @@ -186,4 +20,8 @@ object XmlScoverageReportParser { case ex: Exception => throw ScoverageException("Cannot parse file! [" + scoverageReportPath + "]", ex) } } +} + +object XmlScoverageReportParser { + def apply() = new XmlScoverageReportParser } \ No newline at end of file diff --git a/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala new file mode 100644 index 0000000..452c24e --- /dev/null +++ b/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala @@ -0,0 +1,20 @@ +package com.buransky.plugins.scoverage.xml + +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner +import org.scalatest.{Matchers, FlatSpec} +import scala.io.Source +import com.buransky.plugins.scoverage.xml.data.XmlReportFile1 + +@RunWith(classOf[JUnitRunner]) +class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { + behavior of "parse source" + + it must "work" in { + val parser = new XmlScoverageReportConstructingParser(Source.fromString(XmlReportFile1.data)) + val projectCoverage = parser.parse() + + val expected = BigDecimal(24.53) + BigDecimal(projectCoverage.rate).setScale(2, BigDecimal.RoundingMode.HALF_UP) should equal(expected) + } +} diff --git a/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala index 437a4a5..5c5ba96 100644 --- a/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala +++ b/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala @@ -12,24 +12,14 @@ class XmlScoverageReportParserSpec extends FlatSpec with Matchers { behavior of "parse file path" it must "fail for null path" in { - the[IllegalArgumentException] thrownBy XmlScoverageReportParser(null.asInstanceOf[String]) + the[IllegalArgumentException] thrownBy XmlScoverageReportParser().parse(null.asInstanceOf[String]) } it must "fail for empty path" in { - the[IllegalArgumentException] thrownBy XmlScoverageReportParser("") + the[IllegalArgumentException] thrownBy XmlScoverageReportParser().parse("") } it must "fail for not existing path" in { - the[ScoverageException] thrownBy XmlScoverageReportParser("/x/a/b/c/1/2/3/4.xml") - } - - behavior of "parse source" - - it must "work" in { - val parser = new XmlScoverageReportParser(Source.fromString(XmlReportFile1.data)) - val projectCoverage = parser.parse() - - val expected = BigDecimal(24.53) - BigDecimal(projectCoverage.rate).setScale(2, BigDecimal.RoundingMode.HALF_UP) should equal(expected) + the[ScoverageException] thrownBy XmlScoverageReportParser().parse("/x/a/b/c/1/2/3/4.xml") } } From 95c7fb0c070fa4db39674e640e8e980f7d1319db Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Wed, 5 Feb 2014 22:01:48 -0800 Subject: [PATCH 014/101] . --- pom.xml | 19 +++++----- .../scoverage/sensor/ScoverageSensor.java | 38 +++++++++++++++---- .../xml/XmlScoverageReportParser.scala | 5 +++ 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index 8265e88..42507a4 100644 --- a/pom.xml +++ b/pom.xml @@ -69,7 +69,8 @@ - 3.0 + 3.6 + 1.4 @@ -78,7 +79,7 @@ com.buransky.plugins.scoverage.ScoveragePlugin - 2.10.3 + 2.10.0 @@ -86,19 +87,16 @@ org.codehaus.sonar sonar-plugin-api ${sonar.version} - - - org.codehaus.sonar.plugins - sonar-surefire-plugin - ${sonar.version} provided - org.codehaus.sonar.plugins - sonar-cobertura-plugin - ${sonar.version} + org.codehaus.sonar-plugins.java + sonar-java-plugin + ${sonar-java.version} + sonar-plugin provided + org.scala-lang scala-library @@ -115,6 +113,7 @@ org.codehaus.sonar sonar-testing-harness ${sonar.version} + test org.scalatest diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java index 3428418..5951d68 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java @@ -11,11 +11,14 @@ import org.sonar.api.batch.CoverageExtension; import org.sonar.api.batch.Sensor; import org.sonar.api.batch.SensorContext; +import org.sonar.api.config.Settings; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.CoverageMeasuresBuilder; import org.sonar.api.measures.Measure; import org.sonar.api.resources.*; +import org.sonar.api.scan.filesystem.ModuleFileSystem; import scala.collection.JavaConversions; +import org.sonar.api.scan.filesystem.PathResolver; import java.util.HashMap; import java.util.Map; @@ -23,13 +26,22 @@ public class ScoverageSensor implements Sensor, CoverageExtension { private static final Logger log = LoggerFactory.getLogger(ScoverageSensor.class); private final ScoverageReportParser scoverageReportParser; + private final Settings settings; + private final PathResolver pathResolver; + private final ModuleFileSystem moduleFileSystem; - public ScoverageSensor() { - this(XmlScoverageReportParser$.MODULE$.apply()); + private static final String SCOVERAGE_REPORT_PATH_PROPERTY = "sonar.scoverage.reportPath"; + + public ScoverageSensor(Settings settings, PathResolver pathResolver, ModuleFileSystem fileSystem) { + this(XmlScoverageReportParser$.MODULE$.apply(), settings, pathResolver, fileSystem); } - public ScoverageSensor(ScoverageReportParser scoverageReportParser) { + public ScoverageSensor(ScoverageReportParser scoverageReportParser, Settings settings, + PathResolver pathResolver, ModuleFileSystem moduleFileSystem) { this.scoverageReportParser = scoverageReportParser; + this.settings = settings; + this.pathResolver = pathResolver; + this.moduleFileSystem = moduleFileSystem; } public boolean shouldExecuteOnProject(Project project) { @@ -37,8 +49,9 @@ public boolean shouldExecuteOnProject(Project project) { } public void analyse(Project project, SensorContext context) { - processProject(scoverageReportParser.parse(getScoverageReportPath(project)), project, context); - //parseFakeReport(project, context); + String reportPath = getScoverageReportPath(); + if (reportPath != null) + processProject(scoverageReportParser.parse(reportPath), project, context); } @Override @@ -46,8 +59,19 @@ public String toString() { return "Scoverage sensor"; } - private String getScoverageReportPath(Project project) { - return ""; + private String getScoverageReportPath() { + String path = settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY); + if (path == null) { + log.error("Scoverage report path not set! [" + SCOVERAGE_REPORT_PATH_PROPERTY + "]"); + return null; + } + java.io.File report = pathResolver.relativeFile(moduleFileSystem.baseDir(), path); + if (!report.exists() || !report.isFile()) { + log.error("Scoverage report not found at {}", report); + return null; + } + + return report.getAbsolutePath(); } private void processProject(ProjectStatementCoverage projectCoverage, diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala index 4f50569..d3da1f5 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala @@ -2,12 +2,17 @@ package com.buransky.plugins.scoverage.xml import scala.io.Source import com.buransky.plugins.scoverage.{ProjectStatementCoverage, ScoverageReportParser, ScoverageException} +import org.apache.log4j.Logger class XmlScoverageReportParser extends ScoverageReportParser { + private val log = Logger.getLogger(classOf[XmlScoverageReportParser]) + def parse(reportFilePath: String): ProjectStatementCoverage = { require(reportFilePath != null) require(!reportFilePath.trim.isEmpty) + log.info("Parsing Scoverage report. [" + reportFilePath + "]") + val parser = new XmlScoverageReportConstructingParser(sourceFromFile(reportFilePath)) parser.parse() } From 140d3ba9c6f2add6cc68f55ed630324024fea038 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Thu, 6 Feb 2014 10:43:54 -0800 Subject: [PATCH 015/101] Fix --- ...XmlScoverageReportConstructingParser.scala | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala index fb5a1b6..943f9c2 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala @@ -123,6 +123,14 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP case _ => DirectoryStatementCoverage(name, childNodes) } } + + def toProjectStatementCoverage: ProjectStatementCoverage = { + toStatementCoverage match { + case node: NodeStatementCoverage => ProjectStatementCoverage("", node.children) + case file: FileStatementCoverage => ProjectStatementCoverage("", List(file)) + case _ => throw new ScoverageException("Illegal statement coverage!") + } + } } private def projectFromMap(statementsInFile: Map[String, List[CoveredStatement]]): @@ -139,20 +147,30 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP chained.foreach(root.add(_)) // Transform file system tree into coverage structure tree - ProjectStatementCoverage("", List(root.toStatementCoverage)) + root.toProjectStatementCoverage } private def pathToChain(filePath: String, coverage: FileStatementCoverage): DirOrFile = { val path = Paths.get(filePath) - val names = for (i <- 0 to path.getNameCount - 1) - yield path.getFileName.toString + if (path.getNameCount < 1) + throw new ScoverageException("Path cannot be empty!") - names.foldLeft(DirOrFile("", Nil, Some(coverage))) { (dirOrFile, name) => - val child = DirOrFile(name, Nil, dirOrFile.coverage) - dirOrFile.children = List(child) - child - } + // Get directories + val dirs = for (i <- 0 to path.getNameCount - 2) + yield DirOrFile(path.getName(i).toString, Nil, None) + + // Chain directories + for (i <- 0 to dirs.length - 2) + dirs(i).children = List(dirs(i + 1)) + + // Get file + val file = DirOrFile(path.getName(path.getNameCount - 1).toString, Nil, Some(coverage)) + + // Append file + dirs.last.children = List(file) + + dirs(0) } private def fileStatementCoverage(statementsInFile: Map[String, List[CoveredStatement]]): From 5ac543dc9f30feb9d62ed579c94a059fed0b1dfa Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Thu, 6 Feb 2014 11:05:15 -0800 Subject: [PATCH 016/101] Testing --- ...coverageReportConstructingParserSpec.scala | 49 ++- .../scoverage/xml/data/XmlReportFile1.scala | 390 ++++++++++++++++++ 2 files changed, 435 insertions(+), 4 deletions(-) diff --git a/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala index 452c24e..2e0667a 100644 --- a/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala +++ b/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala @@ -5,16 +5,57 @@ import org.scalatest.junit.JUnitRunner import org.scalatest.{Matchers, FlatSpec} import scala.io.Source import com.buransky.plugins.scoverage.xml.data.XmlReportFile1 +import scala._ +import com.buransky.plugins.scoverage.FileStatementCoverage +import com.buransky.plugins.scoverage.DirectoryStatementCoverage @RunWith(classOf[JUnitRunner]) class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { behavior of "parse source" - it must "work" in { - val parser = new XmlScoverageReportConstructingParser(Source.fromString(XmlReportFile1.data)) + it must "parse file1 correctly" in { + parseFile1(XmlReportFile1.data) + } + + it must "parse file1 correctly even without XML declaration" in { + parseFile1(XmlReportFile1.dataWithoutDeclaration) + } + + private def parseFile1(data: String) { + val parser = new XmlScoverageReportConstructingParser(Source.fromString(data)) val projectCoverage = parser.parse() - val expected = BigDecimal(24.53) - BigDecimal(projectCoverage.rate).setScale(2, BigDecimal.RoundingMode.HALF_UP) should equal(expected) + // Assert coverage + checkRate(24.53, projectCoverage.rate) + + // Assert structure + projectCoverage.name should equal("") + + val projectChildren = projectCoverage.children.toList + projectChildren.length should equal(1) + projectChildren(0) shouldBe a [DirectoryStatementCoverage] + + val aaa = projectChildren(0).asInstanceOf[DirectoryStatementCoverage] + aaa.name should equal("aaa") + checkRate(24.53, aaa.rate) + + val aaaChildren = aaa.children.toList.sortBy(_.statementCount) + aaaChildren.length should equal(2) + + aaaChildren(1) shouldBe a [FileStatementCoverage] + val errorCode = aaaChildren(1).asInstanceOf[FileStatementCoverage] + errorCode.name should equal("ErrorCode.scala") + errorCode.statementCount should equal (46) + errorCode.coveredStatementsCount should equal (13) + + aaaChildren(0) shouldBe a [FileStatementCoverage] + val graph = aaaChildren(0).asInstanceOf[FileStatementCoverage] + graph.name should equal("Graph.scala") + graph.statementCount should equal (7) + graph.coveredStatementsCount should equal (0) + } + + private def checkRate(expected: Double, real: Double) { + BigDecimal(real).setScale(2, BigDecimal.RoundingMode.HALF_UP).should(equal(BigDecimal(expected))) } } diff --git a/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala b/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala index b450837..d2587f5 100644 --- a/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala +++ b/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala @@ -391,4 +391,394 @@ object XmlReportFile1 { | | """.stripMargin + + val dataWithoutDeclaration = + """ + | + | + | + | + | + | + | + | + | MyServiceClientError.this.error("zipcodeinvalid") + | + | + | + | + | + | + | + | + | + | + | 2 + | + | + | 3 + | + | + | scala.Some.apply[String]("One") + | + | + | new $anon() + | + | + | + | + | + | + | + | + | + | + | MyServiceLogicError.this.error("logicfailed") + | + | + | + | + | + | + | + | + | + | + | StructuredErrorCode.this.parent.toString() + | + | + | p.==("") + | + | + | "" + | + | + | { + | scoverage.Invoker.invoked(8, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | "" + |} + | + | + | p.+("-") + | + | + | { + | scoverage.Invoker.invoked(10, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | p.+("-") + |} + | + | + | StructuredErrorCode.this.name + | + | + | if ({ + | scoverage.Invoker.invoked(7, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | p.==("") + |}) + | { + | scoverage.Invoker.invoked(9, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | { + | scoverage.Invoker.invoked(8, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | "" + | } + | } + |else + | { + | scoverage.Invoker.invoked(11, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | { + | scoverage.Invoker.invoked(10, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | p.+("-") + | } + | }.+({ + | scoverage.Invoker.invoked(12, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | StructuredErrorCode.this.name + |}) + | + | + | + | + | + | + | errorCode.==(this) + | + | + | true + | + | + | { + | scoverage.Invoker.invoked(2, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | true + |} + | + | + | StructuredErrorCode.this.parent.is(errorCode) + | + | + | { + | scoverage.Invoker.invoked(4, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | StructuredErrorCode.this.parent.is(errorCode) + |} + | + | + | + | + | + | + | StructuredErrorCode.apply(name, this) + | + | + | + | + | + | + | + | + | + | + | ClientError.required + | + | + | scala.this.Predef.println({ + | scoverage.Invoker.invoked(25, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | ClientError.required + |}) + | + | + | ClientError.invalid + | + | + | scala.this.Predef.println({ + | scoverage.Invoker.invoked(27, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | ClientError.invalid + |}) + | + | + | scala.this.Predef.println(MySqlError) + | + | + | MySqlError.syntax + | + | + | scala.this.Predef.println({ + | scoverage.Invoker.invoked(30, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | MySqlError.syntax + |}) + | + | + | MyServiceLogicError.logicFailed + | + | + | scala.this.Predef.println({ + | scoverage.Invoker.invoked(32, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | MyServiceLogicError.logicFailed + |}) + | + | + | ClientError.required + | + | + | e + | + | + | scala.this.Predef.println("required") + | + | + | { + | scoverage.Invoker.invoked(36, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scala.this.Predef.println("required") + |} + | + | + | scala.this.Predef.println("invalid") + | + | + | { + | scoverage.Invoker.invoked(38, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scala.this.Predef.println("invalid") + |} + | + | + | () + | + | + | { + | scoverage.Invoker.invoked(40, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | () + |} + | + | + | MyServiceServerError.mongoDbError.is(ServerError) + | + | + | scala.this.Predef.println("This is a server error") + | + | + | { + | scoverage.Invoker.invoked(43, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scala.this.Predef.println("This is a server error") + |} + | + | + | () + | + | + | { + | scoverage.Invoker.invoked(45, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | () + |} + | + | + | + | + | + | + | + | + | + | + | MySqlError.this.error("syntax") + | + | + | MySqlError.this.error("connection") + | + | + | + | + | + | + | + | + | + | + | MyServiceServerError.this.error("mongodberror") + | + | + | + | + | + | + | + | + | + | + | "" + | + | + | + | + | + | + | false + | + | + | + | + | + | + | + | + | + | + | ServerError.this.error("solar") + | + | + | + | + | + | + | + | + | + | + | ClientError.this.error("required") + | + | + | ClientError.this.error("invalid") + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | aaa.MakeRectangleModelFromFile.apply(null) + | + | + | x.isInstanceOf[Serializable] + | + | + | scala.this.Predef.println({ + | scoverage.Invoker.invoked(52, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | x.isInstanceOf[Serializable] + |}) + | + | + | + | + | + | + | + | + | + """.stripMargin } From 754df7d91658cad649de3f570ed53661db0c14e0 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Thu, 6 Feb 2014 14:31:34 -0800 Subject: [PATCH 017/101] . --- LICENSE.txt | 165 +++++++++++++++++++++ README.md | 23 +-- pom.xml | 403 +++++++++++++++++++++------------------------------- 3 files changed, 331 insertions(+), 260 deletions(-) create mode 100644 LICENSE.txt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..02bbb60 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. \ No newline at end of file diff --git a/README.md b/README.md index 3a4f0eb..97d4cb2 100644 --- a/README.md +++ b/README.md @@ -1,22 +1 @@ -Sonar Scala Plugin -=========== -Supports Sonar 3.0+ and requires Cobertura and Surefire plugins. - -To include test and coverage reports: - -Install these plugins in your scala project: - -https://github.com/mmarich/sbt-simple-junit-xml-reporter-plugin -- Creates junit xml reports for output from scalatest. - -https://github.com/sqality/scct -- Creates a Scala-friendly code-coverage report, and includes a coberura xml report. - - -Add the following properties to your project's sonar-project.properties file: - -sonar.dynamicAnalysis=reuseReports -sonar.surefire.reportsPath=test-reports -sonar.core.codeCoveragePlugin=cobertura -sonar.java.coveragePlugin=cobertura -sonar.cobertura.reportPath=target/scala-[scala-version]/coverage-report/cobertura.xml +#Sonar Scoverage Plugin# \ No newline at end of file diff --git a/pom.xml b/pom.xml index 42507a4..90f41de 100644 --- a/pom.xml +++ b/pom.xml @@ -1,246 +1,173 @@ - - 4.0.0 + + 4.0.0 - + com.buransky + sonar-scoverage-plugin - + sonar-plugin + 1.0-SNAPSHOT - com.buransky - sonar-scoverage-plugin - - sonar-plugin - 0.1-SNAPSHOT - - Sonar Scoverage Plugin + Sonar Scoverage Plugin Sonar Scoverage Plugin - - - - - GNU LGPL 3 - http://www.gnu.org/licenses/lgpl.txt - repo - - - - - - - - scala-tools - Scala Tools - http://scala-tools.org/repo-releases/ - - true - - - false - - - - - - 3.6 - 1.4 - - - scoverage - Scoverage - - com.buransky.plugins.scoverage.ScoveragePlugin - - 2.10.0 - - - - - org.codehaus.sonar - sonar-plugin-api - ${sonar.version} - provided - - - org.codehaus.sonar-plugins.java - sonar-java-plugin - ${sonar-java.version} - sonar-plugin - provided - - - - org.scala-lang - scala-library - ${scala.version} - - - org.scala-lang - scala-compiler - ${scala.version} - - - - - org.codehaus.sonar - sonar-testing-harness - ${sonar.version} - test - - - org.scalatest - scalatest_2.10 - 2.0 - test - - - org.apache.maven - maven-project - 2.2.1 - test - - - - - - - - org.codehaus.mojo - cobertura-maven-plugin - 2.5 - - - org.codehaus.sonar-plugins.pdf-report - maven-pdfreport-plugin - 1.2 - - - org.codehaus.mojo - sonar-maven-plugin - 1.0-beta-2 - - - org.codehaus.sonar - sonar-maven-plugin - ${sonar.version} - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.1 - - + + + GNU LGPL 3 + http://www.gnu.org/licenses/lgpl.txt + repo + + + + + + scala-tools + Scala Tools + http://scala-tools.org/repo-releases/ + + true + + + false + + + + + + 3.6 + 1.4 + + + scoverage + Scoverage + + com.buransky.plugins.scoverage.ScoveragePlugin + + 2.10.0 + + + + org.codehaus.sonar - sonar-packaging-maven-plugin - 1.7 - true - - ${sonar.pluginClass} - - - - org.scala-tools - maven-scala-plugin - 2.15.2 - - - scala-compile - process-resources - - compile - - - - scala-test-compile - process-test-resources - - testCompile - - - - - - - - maven-surefire-plugin - 2.6 - - - **/*Spec.class - **/*Test.class - - - - - + sonar-plugin-api + ${sonar.version} + provided + + + org.codehaus.sonar-plugins.java + sonar-java-plugin + ${sonar-java.version} + sonar-plugin + provided + + + + org.scala-lang + scala-library + ${scala.version} + + + org.scala-lang + scala-compiler + ${scala.version} + + + + + org.codehaus.sonar + sonar-testing-harness + ${sonar.version} + test + + + org.scalatest + scalatest_2.10 + 2.0 + test + + + org.apache.maven + maven-project + 2.2.1 + test + + + + + + + + org.codehaus.mojo + cobertura-maven-plugin + 2.5 + + + org.codehaus.sonar-plugins.pdf-report + maven-pdfreport-plugin + 1.2 + + + org.codehaus.mojo + sonar-maven-plugin + 1.0-beta-2 + + + org.codehaus.sonar + sonar-maven-plugin + ${sonar.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + + org.codehaus.sonar + sonar-packaging-maven-plugin + 1.7 + true + + ${sonar.pluginClass} + + + + org.scala-tools + maven-scala-plugin + 2.15.2 + + + scala-compile + process-resources + + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + + + + maven-surefire-plugin + 2.6 + + + **/*Spec.class + **/*Test.class + + + + + From 831bbdbf53c70862591f3783257f49de6949df6d Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Thu, 6 Feb 2014 15:23:02 -0800 Subject: [PATCH 018/101] Parent POM, license --- pom.xml | 67 ++++++++++++++----- .../plugins/scoverage/ScoveragePlugin.java | 19 ++++++ .../plugins/scoverage/language/Scala.java | 19 ++++++ .../scoverage/measure/ScalaMetrics.java | 19 ++++++ .../scoverage/resource/ScalaDirectory.java | 19 ++++++ .../plugins/scoverage/resource/ScalaFile.java | 19 ++++++ .../scoverage/sensor/ScoverageSensor.java | 19 ++++++ .../sensor/ScoverageSourceImporterSensor.java | 19 ++++++ .../scoverage/widget/ScoverageWidget.java | 19 ++++++ 9 files changed, 202 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index 90f41de..78269a0 100644 --- a/pom.xml +++ b/pom.xml @@ -3,14 +3,39 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 - com.buransky - sonar-scoverage-plugin + + org.codehaus.sonar-plugins + parent + 18 + ../parent/pom.xml + - sonar-plugin + sonar-scoverage-plugin 1.0-SNAPSHOT - + sonar-plugin Sonar Scoverage Plugin - Sonar Scoverage Plugin + + Sonar plugin for importing statement coverage reports generated by Scoverage. + https://github.com/RadoBuransky/sonar-scoverage-plugin + 2013 + + + Rado Buransky + http://www.buransky.com + + + @@ -36,7 +61,6 @@ 3.6 - 1.4 @@ -58,7 +82,7 @@ org.codehaus.sonar-plugins.java sonar-java-plugin - ${sonar-java.version} + 1.5 sonar-plugin provided @@ -75,27 +99,34 @@ - - org.codehaus.sonar - sonar-testing-harness - ${sonar.version} - test - org.scalatest scalatest_2.10 2.0 test + + junit + junit + 4.11 + + + + org.scala-tools maven-scala-plugin @@ -158,6 +190,7 @@ + diff --git a/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java b/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java index 2c05ee3..2f799e7 100644 --- a/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java +++ b/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java @@ -1,3 +1,22 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ package com.buransky.plugins.scoverage; import com.buransky.plugins.scoverage.measure.ScalaMetrics; diff --git a/src/main/java/com/buransky/plugins/scoverage/language/Scala.java b/src/main/java/com/buransky/plugins/scoverage/language/Scala.java index 08cf742..297bcee 100644 --- a/src/main/java/com/buransky/plugins/scoverage/language/Scala.java +++ b/src/main/java/com/buransky/plugins/scoverage/language/Scala.java @@ -1,3 +1,22 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ package com.buransky.plugins.scoverage.language; import org.sonar.api.resources.AbstractLanguage; diff --git a/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java b/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java index bb6095a..bb66fda 100644 --- a/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java +++ b/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java @@ -1,3 +1,22 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ package com.buransky.plugins.scoverage.measure; import org.sonar.api.measures.CoreMetrics; diff --git a/src/main/java/com/buransky/plugins/scoverage/resource/ScalaDirectory.java b/src/main/java/com/buransky/plugins/scoverage/resource/ScalaDirectory.java index 1787f13..186d4e8 100644 --- a/src/main/java/com/buransky/plugins/scoverage/resource/ScalaDirectory.java +++ b/src/main/java/com/buransky/plugins/scoverage/resource/ScalaDirectory.java @@ -1,3 +1,22 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ package com.buransky.plugins.scoverage.resource; import com.buransky.plugins.scoverage.language.Scala; diff --git a/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java b/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java index e9b25b3..9178949 100644 --- a/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java +++ b/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java @@ -1,3 +1,22 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ package com.buransky.plugins.scoverage.resource; import com.buransky.plugins.scoverage.language.Scala; diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java index 5951d68..5d77789 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java @@ -1,3 +1,22 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ package com.buransky.plugins.scoverage.sensor; import com.buransky.plugins.scoverage.*; diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java index 27e0705..8d8bc71 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java @@ -1,3 +1,22 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ package com.buransky.plugins.scoverage.sensor; import com.buransky.plugins.scoverage.language.Scala; diff --git a/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java b/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java index 096716d..109d941 100644 --- a/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java +++ b/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java @@ -1,3 +1,22 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ package com.buransky.plugins.scoverage.widget; import org.sonar.api.web.AbstractRubyTemplate; From 32abf66cfb1bf25dfc0687f30929f6f68dea2b1c Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Thu, 6 Feb 2014 15:28:41 -0800 Subject: [PATCH 019/101] Scala 2.10.0, licenses --- pom.xml | 69 ------------------- .../scoverage/ScoverageReportParser.scala | 19 +++++ .../plugins/scoverage/StatementCoverage.scala | 19 +++++ .../xml/StubScoverageReportParser.scala | 19 +++++ ...XmlScoverageReportConstructingParser.scala | 19 +++++ .../xml/XmlScoverageReportParser.scala | 19 +++++ ...coverageReportConstructingParserSpec.scala | 19 +++++ .../xml/XmlScoverageReportParserSpec.scala | 19 +++++ .../scoverage/xml/data/XmlReportFile1.scala | 19 +++++ 9 files changed, 152 insertions(+), 69 deletions(-) diff --git a/pom.xml b/pom.xml index 78269a0..994d04b 100644 --- a/pom.xml +++ b/pom.xml @@ -62,11 +62,8 @@ 3.6 - scoverage Scoverage - com.buransky.plugins.scoverage.ScoveragePlugin 2.10.0 @@ -110,64 +107,10 @@ junit 4.11 - - - - org.scala-tools maven-scala-plugin @@ -189,18 +132,6 @@ - - diff --git a/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala index dd01ab5..90cd988 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala @@ -1,3 +1,22 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ package com.buransky.plugins.scoverage trait ScoverageReportParser { diff --git a/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala b/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala index baa5431..6a5fcba 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala @@ -1,3 +1,22 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ package com.buransky.plugins.scoverage /** diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala index a6bb58b..268c954 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala @@ -1,3 +1,22 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ package com.buransky.plugins.scoverage.xml import com.buransky.plugins.scoverage._ diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala index 943f9c2..6eb1841 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala @@ -1,3 +1,22 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ package com.buransky.plugins.scoverage.xml import com.buransky.plugins.scoverage._ diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala index d3da1f5..6224af5 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala @@ -1,3 +1,22 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ package com.buransky.plugins.scoverage.xml import scala.io.Source diff --git a/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala index 2e0667a..790dc58 100644 --- a/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala +++ b/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala @@ -1,3 +1,22 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ package com.buransky.plugins.scoverage.xml import org.junit.runner.RunWith diff --git a/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala index 5c5ba96..fc83bbb 100644 --- a/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala +++ b/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala @@ -1,3 +1,22 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ package com.buransky.plugins.scoverage.xml import org.scalatest.{FlatSpec, Matchers} diff --git a/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala b/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala index d2587f5..8be51ed 100644 --- a/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala +++ b/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala @@ -1,3 +1,22 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ package com.buransky.plugins.scoverage.xml.data object XmlReportFile1 { From 35596926fc263f31d4a9e34b18cbd56367e11142 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Thu, 6 Feb 2014 16:09:08 -0800 Subject: [PATCH 020/101] Sonar API v4.0 --- dev | 2 +- pom.xml | 2 +- .../scoverage/measure/ScalaMetrics.java | 2 +- .../plugins/scoverage/resource/ScalaFile.java | 2 +- .../scoverage/sensor/ScoverageSensor.java | 41 ------------------- .../sensor/ScoverageSourceImporterSensor.java | 24 ++++++----- ...XmlScoverageReportConstructingParser.scala | 16 ++++---- 7 files changed, 26 insertions(+), 63 deletions(-) diff --git a/dev b/dev index d65e55a..130ee7c 100755 --- a/dev +++ b/dev @@ -3,6 +3,6 @@ /home/rado/bin/sonar/bin/linux-x86-64/sonar.sh stop mvn install -DskipTests -cp ./target/sonar-scoverage-plugin-0.1-SNAPSHOT.jar /home/rado/bin/sonar/extensions/plugins/ +cp ./target/sonar-scoverage-plugin-1.0-SNAPSHOT.jar /home/rado/bin/sonar/extensions/plugins/ /home/rado/bin/sonar/bin/linux-x86-64/sonar.sh start diff --git a/pom.xml b/pom.xml index 994d04b..50a2ecf 100644 --- a/pom.xml +++ b/pom.xml @@ -60,7 +60,7 @@ - 3.6 + 4.0 scoverage Scoverage diff --git a/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java b/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java index bb66fda..1e5342e 100644 --- a/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java +++ b/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java @@ -27,7 +27,7 @@ import java.util.List; public final class ScalaMetrics implements Metrics { - public static final String STATEMENT_COVERAGE_KEY = "scoverage"; + private static final String STATEMENT_COVERAGE_KEY = "scoverage"; public static final Metric STATEMENT_COVERAGE = new Metric.Builder(STATEMENT_COVERAGE_KEY, "Statement coverage", Metric.ValueType.PERCENT) .setDescription("Statement coverage by unit tests") diff --git a/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java b/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java index 9178949..6c5f9ba 100644 --- a/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java +++ b/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java @@ -24,7 +24,7 @@ import org.sonar.api.resources.Language; import org.sonar.api.resources.Resource; -public class ScalaFile extends Resource { +public class ScalaFile extends Resource { private final File file; private ScalaDirectory parent; diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java index 5d77789..8e152d7 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java @@ -183,49 +183,8 @@ private String appendFilePath(String src, String name) { return result + name; } - private static void log(String message) { log.info("[Scoverage] " + message); } - private void parseFakeReport(Project project, final SensorContext context) { - ProjectFileSystem fileSystem = project.getFileSystem(); - - HashMap dirs = new HashMap(); - for (InputFile sourceFile : fileSystem.mainFiles("scala")) { - ScalaFile scalaSourcefile = new ScalaFile(File.fromIOFile(sourceFile.getFile(), project).getKey()); - - context.saveMeasure(scalaSourcefile, new Measure(CoreMetrics.COVERAGE, 51.4)); - log("Process fake file [" + scalaSourcefile.getKey() + "]"); - -// CoverageMeasuresBuilder coverage = CoverageMeasuresBuilder.create(); -// coverage.setHits(1, 1); -// coverage.setHits(2, 2); -// coverage.setHits(3, 3); -// coverage.setHits(4, 0); -// coverage.setHits(5, 0); -// coverage.setHits(6, 0); -// coverage.setHits(7, 0); -// coverage.setHits(8, 1); -// coverage.setHits(9, 0); -// coverage.setHits(10, 2); -// coverage.setHits(11, 0); -// coverage.setHits(12, 3); -// coverage.setHits(13, 0); -// -// for (Measure measure : coverage.createMeasures()) { -// context.saveMeasure(scalaSourcefile, measure); -// } - - dirs.put(scalaSourcefile.getParent().getKey(), scalaSourcefile.getParent()); - } - - for (Map.Entry e: dirs.entrySet()) { - log.info("[ScoverageSensor] Set dir coverage for [" + e.getKey() + "]"); - context.saveMeasure(e.getValue(), new Measure(CoreMetrics.COVERAGE, 23.4)); - } - - context.saveMeasure(project, new Measure(CoreMetrics.COVERAGE, 12.3)); - } - } diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java index 8d8bc71..4709028 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java @@ -29,9 +29,10 @@ import org.sonar.api.batch.Sensor; import org.sonar.api.batch.SensorContext; import org.sonar.api.resources.File; -import org.sonar.api.resources.InputFile; import org.sonar.api.resources.Project; -import org.sonar.api.resources.ProjectFileSystem; +import org.sonar.api.scan.filesystem.FileQuery; +import org.sonar.api.scan.filesystem.FileType; +import org.sonar.api.scan.filesystem.ModuleFileSystem; import java.io.IOException; @@ -40,9 +41,11 @@ public class ScoverageSourceImporterSensor implements Sensor { private static final Logger LOGGER = LoggerFactory.getLogger(ScoverageSourceImporterSensor.class); private final Scala scala; + private final ModuleFileSystem moduleFileSystem; - public ScoverageSourceImporterSensor(Scala scala) { + public ScoverageSourceImporterSensor(Scala scala, ModuleFileSystem moduleFileSystem) { this.scala = scala; + this.moduleFileSystem = moduleFileSystem; } public boolean shouldExecuteOnProject(Project project) { @@ -50,10 +53,10 @@ public boolean shouldExecuteOnProject(Project project) { } public void analyse(Project project, SensorContext sensorContext) { - ProjectFileSystem fileSystem = project.getFileSystem(); - String charset = fileSystem.getSourceCharset().toString(); + String charset = moduleFileSystem.sourceCharset().toString(); - for (InputFile sourceFile : fileSystem.mainFiles(scala.getKey())) { + FileQuery query = FileQuery.on(FileType.SOURCE).onLanguage(scala.getKey()); + for (java.io.File sourceFile : moduleFileSystem.files(query)) { addFileToSonar(project, sensorContext, sourceFile, charset); } } @@ -63,18 +66,17 @@ public String toString() { return "Scoverage source importer"; } - private void addFileToSonar(Project project, SensorContext sensorContext, InputFile inputFile, + private void addFileToSonar(Project project, SensorContext sensorContext, java.io.File sourceFile, String charset) { try { - String source = FileUtils.readFileToString(inputFile.getFile(), charset); - - String key = File.fromIOFile(inputFile.getFile(), project).getKey(); + String source = FileUtils.readFileToString(sourceFile, charset); + String key = File.fromIOFile(sourceFile, project).getKey(); ScalaFile resource = new ScalaFile(key); sensorContext.index(resource); sensorContext.saveSource(resource, source); } catch (IOException ioe) { - LOGGER.error("Could not read the file: " + inputFile.getFile().getAbsolutePath(), ioe); + LOGGER.error("Could not read the file: " + sourceFile.getAbsolutePath(), ioe); } } } \ No newline at end of file diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala index 6eb1841..15d114b 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala @@ -25,7 +25,6 @@ import scala.xml.parsing.ConstructingParser import scala.xml.{Text, NamespaceBinding, MetaData} import org.apache.log4j.Logger import scala.collection.mutable -import java.nio.file.Paths import scala.annotation.tailrec import java.io.File @@ -170,21 +169,22 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP } private def pathToChain(filePath: String, coverage: FileStatementCoverage): DirOrFile = { - val path = Paths.get(filePath) + //val path = Paths.get(filePath) + val path = splitPath(filePath) - if (path.getNameCount < 1) + if (path.length < 1) throw new ScoverageException("Path cannot be empty!") // Get directories - val dirs = for (i <- 0 to path.getNameCount - 2) - yield DirOrFile(path.getName(i).toString, Nil, None) + val dirs = for (i <- 0 to path.length - 2) + yield DirOrFile(path(i), Nil, None) // Chain directories for (i <- 0 to dirs.length - 2) dirs(i).children = List(dirs(i + 1)) // Get file - val file = DirOrFile(path.getName(path.getNameCount - 1).toString, Nil, Some(coverage)) + val file = DirOrFile(path(path.length - 1).toString, Nil, Some(coverage)) // Append file dirs.last.children = List(file) @@ -195,7 +195,7 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP private def fileStatementCoverage(statementsInFile: Map[String, List[CoveredStatement]]): Map[String, FileStatementCoverage] = { statementsInFile.map { sif => - val fileStatementCoverage = FileStatementCoverage(Paths.get(sif._1).getFileName.toString, + val fileStatementCoverage = FileStatementCoverage(splitPath(sif._1).last, sif._2.length, coveredStatements(sif._2), sif._2) (sif._1, fileStatementCoverage) @@ -204,4 +204,6 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP private def coveredStatements(statements: Iterable[CoveredStatement]) = statements.count(_.hitCount > 0) + + private def splitPath(filePath: String) = filePath.split(File.separator) } \ No newline at end of file From 4804dc94a1c67a7e187bcd2bfbc7e33b2d3ae3f4 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Thu, 6 Feb 2014 16:45:53 -0800 Subject: [PATCH 021/101] Javadoc --- .../plugins/scoverage/ScoveragePlugin.java | 5 ++++ .../plugins/scoverage/language/Scala.java | 5 ++++ .../scoverage/measure/ScalaMetrics.java | 5 ++++ .../plugins/scoverage/resource/ScalaFile.java | 11 +++++--- ...alaDirectory.java => SingleDirectory.java} | 14 ++++++++--- .../scoverage/sensor/ScoverageSensor.java | 12 ++++----- .../sensor/ScoverageSourceImporterSensor.java | 5 ++++ .../scoverage/widget/ScoverageWidget.java | 5 ++++ .../scoverage/ScoverageReportParser.scala | 10 ++++++++ .../plugins/scoverage/StatementCoverage.scala | 25 ++++++++++++++++--- .../xml/StubScoverageReportParser.scala | 5 ++++ ...XmlScoverageReportConstructingParser.scala | 5 ++++ .../xml/XmlScoverageReportParser.scala | 5 ++++ 13 files changed, 96 insertions(+), 16 deletions(-) rename src/main/java/com/buransky/plugins/scoverage/resource/{ScalaDirectory.java => SingleDirectory.java} (81%) diff --git a/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java b/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java index 2f799e7..19e0121 100644 --- a/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java +++ b/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java @@ -30,6 +30,11 @@ import java.util.ArrayList; import java.util.List; +/** + * Plugin entry point. + * + * @author Rado Buransky + */ public class ScoveragePlugin extends SonarPlugin { public List> getExtensions() { diff --git a/src/main/java/com/buransky/plugins/scoverage/language/Scala.java b/src/main/java/com/buransky/plugins/scoverage/language/Scala.java index 297bcee..74abe0c 100644 --- a/src/main/java/com/buransky/plugins/scoverage/language/Scala.java +++ b/src/main/java/com/buransky/plugins/scoverage/language/Scala.java @@ -21,6 +21,11 @@ import org.sonar.api.resources.AbstractLanguage; +/** + * Scala language. + * + * @author Rado Buransky + */ public class Scala extends AbstractLanguage { public static final Scala INSTANCE = new Scala(); diff --git a/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java b/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java index 1e5342e..0d4e634 100644 --- a/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java +++ b/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java @@ -26,6 +26,11 @@ import java.util.Arrays; import java.util.List; +/** + * Statement coverage metric definition. + * + * @author Rado Buransky + */ public final class ScalaMetrics implements Metrics { private static final String STATEMENT_COVERAGE_KEY = "scoverage"; public static final Metric STATEMENT_COVERAGE = new Metric.Builder(STATEMENT_COVERAGE_KEY, diff --git a/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java b/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java index 6c5f9ba..be9c86d 100644 --- a/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java +++ b/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java @@ -24,9 +24,14 @@ import org.sonar.api.resources.Language; import org.sonar.api.resources.Resource; +/** + * Scala source code file resource. + * + * @author Rado Buransky + */ public class ScalaFile extends Resource { private final File file; - private ScalaDirectory parent; + private SingleDirectory parent; public ScalaFile(String key) { if (key == null) @@ -67,9 +72,9 @@ public String getQualifier() { } @Override - public ScalaDirectory getParent() { + public SingleDirectory getParent() { if (parent == null) { - parent = new ScalaDirectory(file.getParent().getKey()); + parent = new SingleDirectory(file.getParent().getKey()); } return parent; } diff --git a/src/main/java/com/buransky/plugins/scoverage/resource/ScalaDirectory.java b/src/main/java/com/buransky/plugins/scoverage/resource/SingleDirectory.java similarity index 81% rename from src/main/java/com/buransky/plugins/scoverage/resource/ScalaDirectory.java rename to src/main/java/com/buransky/plugins/scoverage/resource/SingleDirectory.java index 186d4e8..1d7c08c 100644 --- a/src/main/java/com/buransky/plugins/scoverage/resource/ScalaDirectory.java +++ b/src/main/java/com/buransky/plugins/scoverage/resource/SingleDirectory.java @@ -24,16 +24,22 @@ import org.sonar.api.resources.Language; import org.sonar.api.resources.Resource; -public class ScalaDirectory extends Directory { +/** + * Single directory in file system. Unlike org.sonar.api.resources.Directory that can represent + * a chain of directories. + * + * @author Rado Buransky + */ +public class SingleDirectory extends Directory { private final String name; - private final ScalaDirectory parent; + private final SingleDirectory parent; - public ScalaDirectory(String key) { + public SingleDirectory(String key) { super(key); int i = getKey().lastIndexOf(SEPARATOR); if (i > 0) { - parent = new ScalaDirectory(key.substring(0, i)); + parent = new SingleDirectory(key.substring(0, i)); name = key.substring(i + 1); } else { diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java index 8e152d7..2f2ae0f 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java @@ -22,7 +22,6 @@ import com.buransky.plugins.scoverage.*; import com.buransky.plugins.scoverage.language.Scala; import com.buransky.plugins.scoverage.measure.ScalaMetrics; -import com.buransky.plugins.scoverage.resource.ScalaDirectory; import com.buransky.plugins.scoverage.resource.ScalaFile; import com.buransky.plugins.scoverage.xml.XmlScoverageReportParser$; import org.slf4j.Logger; @@ -31,7 +30,6 @@ import org.sonar.api.batch.Sensor; import org.sonar.api.batch.SensorContext; import org.sonar.api.config.Settings; -import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.CoverageMeasuresBuilder; import org.sonar.api.measures.Measure; import org.sonar.api.resources.*; @@ -39,9 +37,11 @@ import scala.collection.JavaConversions; import org.sonar.api.scan.filesystem.PathResolver; -import java.util.HashMap; -import java.util.Map; - +/** + * Main sensor for importing Scoverage report to Sonar. + * + * @author Rado Buransky + */ public class ScoverageSensor implements Sensor, CoverageExtension { private static final Logger log = LoggerFactory.getLogger(ScoverageSensor.class); private final ScoverageReportParser scoverageReportParser; @@ -107,7 +107,7 @@ private void processDirectory(DirectoryStatementCoverage directoryCoverage, Sens String parentDirectory) { String currentDirectory = appendFilePath(parentDirectory, directoryCoverage.name()); - ScalaDirectory directory = new ScalaDirectory(currentDirectory); + com.buransky.plugins.scoverage.resource.SingleDirectory directory = new com.buransky.plugins.scoverage.resource.SingleDirectory(currentDirectory); context.saveMeasure(directory, createStatementCoverage(directoryCoverage.rate())); log("Process directory [" + directory.getKey() + ", " + directoryCoverage.rate() + "]"); diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java index 4709028..30eb848 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java @@ -36,6 +36,11 @@ import java.io.IOException; +/** + * Imports Scala source code files to Sonar. + * + * @author Rado Buransky + */ @Phase(name = Name.PRE) public class ScoverageSourceImporterSensor implements Sensor { diff --git a/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java b/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java index 109d941..ea3fe9f 100644 --- a/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java +++ b/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java @@ -22,6 +22,11 @@ import org.sonar.api.web.AbstractRubyTemplate; import org.sonar.api.web.RubyRailsWidget; +/** + * UI widget that can be added to the main dashboard to display overall statement coverage for the project. + * + * @author Rado Buransky + */ public class ScoverageWidget extends AbstractRubyTemplate implements RubyRailsWidget { public String getId() { diff --git a/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala index 90cd988..b3c84c6 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala @@ -19,9 +19,19 @@ */ package com.buransky.plugins.scoverage +/** + * Interface for Scoverage report parser. + * + * @author Rado Buransky + */ trait ScoverageReportParser { def parse(reportFilePath: String): ProjectStatementCoverage } +/** + * Common Scoverage exception. + * + * @author Rado Buransky + */ case class ScoverageException(message: String, source: Throwable = null) extends Exception(message, source) diff --git a/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala b/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala index 6a5fcba..93e2b1a 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala @@ -22,6 +22,8 @@ package com.buransky.plugins.scoverage /** * Statement coverage represents rate at which are statements of a certain source code unit * being covered by tests. + * + * @author Rado Buransky */ sealed trait StatementCoverage { /** @@ -59,23 +61,40 @@ trait NodeStatementCoverage extends StatementCoverage { /** * Root node. In multi-module projects it can contain other ProjectStatementCoverage * elements as children. - * - * @param name Name of the project or module. - * @param children */ case class ProjectStatementCoverage(name: String, children: Iterable[StatementCoverage]) extends NodeStatementCoverage +/** + * Physical directory in file system. + */ case class DirectoryStatementCoverage(name: String, children: Iterable[StatementCoverage]) extends NodeStatementCoverage +/** + * Scala source code file. + */ case class FileStatementCoverage(name: String, statementCount: Int, coveredStatementsCount: Int, statements: Iterable[CoveredStatement]) extends StatementCoverage +/** + * Position a Scala source code file. + */ case class StatementPosition(line: Int, pos: Int) +/** + * Coverage information about the Scala statement. + * + * @param start Starting position of the statement. + * @param end Ending position of the statement. + * @param hitCount How many times has the statement been hit by unit tests. Zero means + * that the statement is not covered. + */ case class CoveredStatement(start: StatementPosition, end: StatementPosition, hitCount: Int) +/** + * Aggregated statement coverage for a given source code line. + */ case class CoveredLine(line: Int, hitCount: Int) object StatementCoverage { diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala index 268c954..adcb548 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala @@ -27,6 +27,11 @@ import com.buransky.plugins.scoverage.FileStatementCoverage import com.buransky.plugins.scoverage.DirectoryStatementCoverage import scala.io.Source +/** + * Stub with some dummy data so that we don't have to parse XML for testing. + * + * @author Rado Buransky + */ class StubScoverageReportParser extends ScoverageReportParser { def parse(reportFilePath: String): ProjectStatementCoverage = { val errorCodeFile = FileStatementCoverage("ErrorCode.scala", 17, 13, diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala index 15d114b..2b30b04 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala @@ -28,6 +28,11 @@ import scala.collection.mutable import scala.annotation.tailrec import java.io.File +/** + * Scoverage XML parser based on ConstructingParser provided by standard Scala library. + * + * @author Rado Buransky + */ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingParser(source, false) { private val log = Logger.getLogger(classOf[XmlScoverageReportConstructingParser]) diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala index 6224af5..b9f839b 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala @@ -23,6 +23,11 @@ import scala.io.Source import com.buransky.plugins.scoverage.{ProjectStatementCoverage, ScoverageReportParser, ScoverageException} import org.apache.log4j.Logger +/** + * Bridge between parser implementation and coverage provider. + * + * @author Rado Buransky + */ class XmlScoverageReportParser extends ScoverageReportParser { private val log = Logger.getLogger(classOf[XmlScoverageReportParser]) From 943f66f93ed5bdc1f619bddec6c047cfca24335d Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Thu, 6 Feb 2014 17:20:23 -0800 Subject: [PATCH 022/101] Doc --- README.md | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 97d4cb2..8454c9e 100644 --- a/README.md +++ b/README.md @@ -1 +1,45 @@ -#Sonar Scoverage Plugin# \ No newline at end of file +#Sonar Scoverage Plugin# + +Plugin for [SonarQube] 4+ that imports statement coverage generated by [Scoverage] for Scala projects. + +## Installation ## + +Download and copy sonar-scoverage-plugin-1.0-SNAPSHOT.jar to the Sonar plugins directory +(usually /extensions/plugins). Restart Sonar. + +## Configure Sonar runner ## + +Set location of the **scoverage.xml** file in the **sonar-project.properties** located in your project's +root directory: + + ... + sonar.scoverage.reportPath=target/scala-2.10/scoverage-report/scoverage.xml + ... + +## Run Scoverage and Sonar runner ## + +If your project is based on SBT and you're using [Scoverage plugin for SBT] [sbt-scoverage] you can +generate the Scoverage report by executing following from command line: + + sbt clean scoverage:test + +And then run Sonar runner to upload the report to the Sonar server: + + sonar-runner + +## Add statement coverage columns ## + +To see the actual statement coverage percentage you need to log in to Sonar as admin. +Click **Components** section on the left side, then click **Customize ON** in the top-right corner and then +add **Statement coverage** column. + +## Add widget ## + +You can also add **Statement coverage widget** to your project's dashboard. Log in to Sonar as admin. Go to +the project dashboard, click **Configure widgets** in the top-right corner, click **Add widget** button in +the **Custom Measures** section. Click **Edit** in the newly added **Custom Measures** widget and choose +**Statement coverage** for **Metric 1**. Click **Save**, **Back to dashboard**. Enjoy. + +[SonarQube]: http://www.sonarqube.org/ "SonarQube" +[Scoverage]: https://github.com/scoverage/scalac-scoverage-plugin "Scoverage" +[sbt-scoverage]: https://github.com/scoverage/sbt-scoverage \ No newline at end of file From 97a40b8e0d991b5ff8dc48b67ebec66155aabba7 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Thu, 6 Feb 2014 17:30:06 -0800 Subject: [PATCH 023/101] . --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8454c9e..1d52649 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Plugin for [SonarQube] 4+ that imports statement coverage generated by [Scoverag ## Installation ## -Download and copy sonar-scoverage-plugin-1.0-SNAPSHOT.jar to the Sonar plugins directory +Download and copy [sonar-scoverage-plugin-1.0-SNAPSHOT.jar] [PluginJar] to the Sonar plugins directory (usually /extensions/plugins). Restart Sonar. ## Configure Sonar runner ## @@ -40,6 +40,7 @@ the project dashboard, click **Configure widgets** in the top-right corner, clic the **Custom Measures** section. Click **Edit** in the newly added **Custom Measures** widget and choose **Statement coverage** for **Metric 1**. Click **Save**, **Back to dashboard**. Enjoy. +[PluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v1.0-SNAPSHOT/sonar-scoverage-plugin-1.0-SNAPSHOT.jar [SonarQube]: http://www.sonarqube.org/ "SonarQube" [Scoverage]: https://github.com/scoverage/scalac-scoverage-plugin "Scoverage" [sbt-scoverage]: https://github.com/scoverage/sbt-scoverage \ No newline at end of file From 549beffde6df7f1fab2aa13fab5cdf0f6ad7ccd9 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 7 Feb 2014 10:18:42 -0800 Subject: [PATCH 024/101] Doc update --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1d52649..136ceed 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ #Sonar Scoverage Plugin# -Plugin for [SonarQube] 4+ that imports statement coverage generated by [Scoverage] for Scala projects. +Plugin for [SonarQube] that imports statement coverage generated by [Scoverage] for Scala projects. + +## Requirements ## + +- [SonarQube] 4.0 +- [Scoverage] 0.95.7 ## Installation ## From 21be1a5119ed5edf1d167f57c4bb0019cc28727b Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 7 Feb 2014 11:14:09 -0800 Subject: [PATCH 025/101] Fix files located in the root directory. --- .../plugins/scoverage/resource/ScalaFile.java | 5 ++++ .../scoverage/sensor/ScoverageSensor.java | 16 +++++------- .../plugins/scoverage/util/LogUtil.java | 26 +++++++++++++++++++ 3 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/buransky/plugins/scoverage/util/LogUtil.java diff --git a/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java b/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java index be9c86d..5524bb6 100644 --- a/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java +++ b/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java @@ -23,6 +23,7 @@ import org.sonar.api.resources.File; import org.sonar.api.resources.Language; import org.sonar.api.resources.Resource; +import org.sonar.api.resources.Directory; /** * Scala source code file resource. @@ -76,6 +77,10 @@ public SingleDirectory getParent() { if (parent == null) { parent = new SingleDirectory(file.getParent().getKey()); } + + if (Directory.ROOT.equals(parent.getKey())) + return null; + return parent; } diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java index 2f2ae0f..0967c12 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java @@ -36,6 +36,7 @@ import org.sonar.api.scan.filesystem.ModuleFileSystem; import scala.collection.JavaConversions; import org.sonar.api.scan.filesystem.PathResolver; +import com.buransky.plugins.scoverage.util.LogUtil; /** * Main sensor for importing Scoverage report to Sonar. @@ -81,12 +82,12 @@ public String toString() { private String getScoverageReportPath() { String path = settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY); if (path == null) { - log.error("Scoverage report path not set! [" + SCOVERAGE_REPORT_PATH_PROPERTY + "]"); + log.info(LogUtil.f("Report path not set! [" + SCOVERAGE_REPORT_PATH_PROPERTY + "]")); return null; } java.io.File report = pathResolver.relativeFile(moduleFileSystem.baseDir(), path); if (!report.exists() || !report.isFile()) { - log.error("Scoverage report not found at {}", report); + log.error(LogUtil.f("Report not found at {}"), report); return null; } @@ -97,7 +98,7 @@ private void processProject(ProjectStatementCoverage projectCoverage, Project project, SensorContext context) { // Save project measure context.saveMeasure(project, createStatementCoverage(projectCoverage.rate())); - log("Project coverage = " + projectCoverage.rate()); + log.info(LogUtil.f("Project coverage = " + projectCoverage.rate())); // Process children processChildren(projectCoverage.children(), context, ""); @@ -110,7 +111,7 @@ private void processDirectory(DirectoryStatementCoverage directoryCoverage, Sens com.buransky.plugins.scoverage.resource.SingleDirectory directory = new com.buransky.plugins.scoverage.resource.SingleDirectory(currentDirectory); context.saveMeasure(directory, createStatementCoverage(directoryCoverage.rate())); - log("Process directory [" + directory.getKey() + ", " + directoryCoverage.rate() + "]"); + log.info(LogUtil.f("Process directory [" + directory.getKey() + ", " + directoryCoverage.rate() + "]")); // Process children processChildren(directoryCoverage.children(), context, currentDirectory); @@ -121,7 +122,7 @@ private void processFile(FileStatementCoverage fileCoverage, SensorContext conte ScalaFile scalaSourcefile = new ScalaFile(appendFilePath(directory, fileCoverage.name())); context.saveMeasure(scalaSourcefile, createStatementCoverage(fileCoverage.rate())); - log("Process file [" + scalaSourcefile.getKey() + ", " + fileCoverage.rate() + "]"); + log.info(LogUtil.f("Process file [" + scalaSourcefile.getKey() + ", " + fileCoverage.rate() + "]")); // Save line coverage. This is needed just for source code highlighting. saveLineCoverage(fileCoverage.statements(), scalaSourcefile, context); @@ -182,9 +183,4 @@ private String appendFilePath(String src, String name) { return result + name; } - - private static void log(String message) { - log.info("[Scoverage] " + message); - } - } diff --git a/src/main/java/com/buransky/plugins/scoverage/util/LogUtil.java b/src/main/java/com/buransky/plugins/scoverage/util/LogUtil.java new file mode 100644 index 0000000..5c7454f --- /dev/null +++ b/src/main/java/com/buransky/plugins/scoverage/util/LogUtil.java @@ -0,0 +1,26 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scoverage.util; + +public class LogUtil { + public static String f(String msg) { + return "[scoverage] " + msg; + } +} From 8087190b157b86c08162f0681cebae97f1dbdba0 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 7 Feb 2014 13:03:15 -0800 Subject: [PATCH 026/101] Overall coverage for multimodule projects --- .../scoverage/measure/ScalaMetrics.java | 17 +++- .../scoverage/sensor/ScoverageSensor.java | 82 +++++++++++++++---- .../xml/XmlScoverageReportParser.scala | 3 +- 3 files changed, 84 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java b/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java index 0d4e634..15a74ab 100644 --- a/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java +++ b/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java @@ -22,6 +22,7 @@ import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.Metric; import org.sonar.api.measures.Metrics; +import org.sonar.api.measures.Metric.ValueType; import java.util.Arrays; import java.util.List; @@ -34,8 +35,8 @@ public final class ScalaMetrics implements Metrics { private static final String STATEMENT_COVERAGE_KEY = "scoverage"; public static final Metric STATEMENT_COVERAGE = new Metric.Builder(STATEMENT_COVERAGE_KEY, - "Statement coverage", Metric.ValueType.PERCENT) - .setDescription("Statement coverage by unit tests") + "Statement coverage", ValueType.PERCENT) + .setDescription("Statement coverage by tests") .setDirection(Metric.DIRECTION_BETTER) .setQualitative(true) .setDomain(CoreMetrics.DOMAIN_TESTS) @@ -43,8 +44,18 @@ public final class ScalaMetrics implements Metrics { .setBestValue(100.0) .create(); + public static final String COVERED_STATEMENTS_KEY = "covered_statements"; + public static final Metric COVERED_STATEMENTS = new Metric.Builder(COVERED_STATEMENTS_KEY, + "Covered statements", Metric.ValueType.INT) + .setDescription("Number of statements covered by tests") + .setDirection(Metric.DIRECTION_BETTER) + .setQualitative(false) + .setDomain(CoreMetrics.DOMAIN_SIZE) + .setFormula(new org.sonar.api.measures.SumChildValuesFormula(false)) + .create(); + @Override public List getMetrics() { - return Arrays.asList(STATEMENT_COVERAGE); + return Arrays.asList(STATEMENT_COVERAGE, COVERED_STATEMENTS); } } diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java index 0967c12..676d51b 100644 --- a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java +++ b/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java @@ -37,6 +37,8 @@ import scala.collection.JavaConversions; import org.sonar.api.scan.filesystem.PathResolver; import com.buransky.plugins.scoverage.util.LogUtil; +import org.sonar.api.measures.CoreMetrics; +import com.buransky.plugins.scoverage.resource.SingleDirectory; /** * Main sensor for importing Scoverage report to Sonar. @@ -70,21 +72,56 @@ public boolean shouldExecuteOnProject(Project project) { public void analyse(Project project, SensorContext context) { String reportPath = getScoverageReportPath(); - if (reportPath != null) + if (reportPath != null) { processProject(scoverageReportParser.parse(reportPath), project, context); + } + else { + if (project.isModule()) { + log.warn(LogUtil.f("Report path not set for " + project.name() + " module! [" + + project.name() + "." + SCOVERAGE_REPORT_PATH_PROPERTY + "]")); + } + else { + // Compute overall statement coverage from submodules + long totalStatementCount = 0; + long coveredStatementCount = 0; + for (Project module: project.getModules()) { + // Aggregate modules + Measure moduleStatementCount = context.getMeasure(module, CoreMetrics.STATEMENTS); + Measure moduleCoveredStatementCount = context.getMeasure(module, ScalaMetrics.COVERED_STATEMENTS); + + if ((moduleStatementCount == null) || (moduleCoveredStatementCount == null)) + log.debug(LogUtil.f("Module has no statement coverage. [" + module.name() + "]")); + else { + totalStatementCount += moduleStatementCount.getValue(); + coveredStatementCount += moduleCoveredStatementCount.getValue(); + + log.debug(LogUtil.f("Statement count for " + module.name() + " module. [" + + moduleStatementCount.getValue() + ", " + moduleCoveredStatementCount.getValue() + "]")); + } + } + + if (totalStatementCount > 0) { + Double overall = (coveredStatementCount / (double)totalStatementCount) * 100.0; + + // Set overall statement coverage + context.saveMeasure(project, createStatementCoverage(overall)); + + log.info(LogUtil.f("Overall statement coverage is " + String.format("%1$,.2f", overall))); + } + } + } } @Override public String toString() { - return "Scoverage sensor"; + return getClass().getSimpleName(); } private String getScoverageReportPath() { String path = settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY); - if (path == null) { - log.info(LogUtil.f("Report path not set! [" + SCOVERAGE_REPORT_PATH_PROPERTY + "]")); + if (path == null) return null; - } + java.io.File report = pathResolver.relativeFile(moduleFileSystem.baseDir(), path); if (!report.exists() || !report.isFile()) { log.error(LogUtil.f("Report not found at {}"), report); @@ -96,9 +133,11 @@ private String getScoverageReportPath() { private void processProject(ProjectStatementCoverage projectCoverage, Project project, SensorContext context) { - // Save project measure - context.saveMeasure(project, createStatementCoverage(projectCoverage.rate())); - log.info(LogUtil.f("Project coverage = " + projectCoverage.rate())); + // Save measures + saveMeasures(context, project, projectCoverage); + + log.info(LogUtil.f("Statement coverage for " + project.getKey() + " is " + + String.format("%1$,.2f", projectCoverage.rate()))); // Process children processChildren(projectCoverage.children(), context, ""); @@ -108,10 +147,8 @@ private void processDirectory(DirectoryStatementCoverage directoryCoverage, Sens String parentDirectory) { String currentDirectory = appendFilePath(parentDirectory, directoryCoverage.name()); - com.buransky.plugins.scoverage.resource.SingleDirectory directory = new com.buransky.plugins.scoverage.resource.SingleDirectory(currentDirectory); - context.saveMeasure(directory, createStatementCoverage(directoryCoverage.rate())); - - log.info(LogUtil.f("Process directory [" + directory.getKey() + ", " + directoryCoverage.rate() + "]")); + // Save measures + saveMeasures(context, new SingleDirectory(currentDirectory), directoryCoverage); // Process children processChildren(directoryCoverage.children(), context, currentDirectory); @@ -120,14 +157,23 @@ private void processDirectory(DirectoryStatementCoverage directoryCoverage, Sens private void processFile(FileStatementCoverage fileCoverage, SensorContext context, String directory) { ScalaFile scalaSourcefile = new ScalaFile(appendFilePath(directory, fileCoverage.name())); - context.saveMeasure(scalaSourcefile, createStatementCoverage(fileCoverage.rate())); - log.info(LogUtil.f("Process file [" + scalaSourcefile.getKey() + ", " + fileCoverage.rate() + "]")); + // Save measures + saveMeasures(context, scalaSourcefile, fileCoverage); // Save line coverage. This is needed just for source code highlighting. saveLineCoverage(fileCoverage.statements(), scalaSourcefile, context); } + private void saveMeasures(SensorContext context, Resource resource, StatementCoverage statementCoverage) { + context.saveMeasure(resource, createStatementCoverage(statementCoverage.rate())); + context.saveMeasure(resource, createStatementCount(statementCoverage.statementCount())); + context.saveMeasure(resource, createCoveredStatementCount(statementCoverage.coveredStatementsCount())); + + log.debug(LogUtil.f("Save measures [" + statementCoverage.rate() + ", " + statementCoverage.statementCount() + + ", " + statementCoverage.coveredStatementsCount() + ", " + resource.getKey() + "]")); + } + private void saveLineCoverage(scala.collection.Iterable coveredStatements, ScalaFile scalaSourcefile, SensorContext context) { // Convert statements to lines @@ -174,6 +220,14 @@ private Measure createStatementCoverage(Double rate) { return new Measure(ScalaMetrics.STATEMENT_COVERAGE, rate); } + private Measure createStatementCount(int statements) { + return new Measure(CoreMetrics.STATEMENTS, (double)statements); + } + + private Measure createCoveredStatementCount(int coveredStatements) { + return new Measure(ScalaMetrics.COVERED_STATEMENTS, (double)coveredStatements); + } + private String appendFilePath(String src, String name) { String result; if (!src.isEmpty()) diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala index b9f839b..59c037c 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala @@ -22,6 +22,7 @@ package com.buransky.plugins.scoverage.xml import scala.io.Source import com.buransky.plugins.scoverage.{ProjectStatementCoverage, ScoverageReportParser, ScoverageException} import org.apache.log4j.Logger +import com.buransky.plugins.scoverage.util.LogUtil /** * Bridge between parser implementation and coverage provider. @@ -35,7 +36,7 @@ class XmlScoverageReportParser extends ScoverageReportParser { require(reportFilePath != null) require(!reportFilePath.trim.isEmpty) - log.info("Parsing Scoverage report. [" + reportFilePath + "]") + log.debug(LogUtil.f("Reading report. [" + reportFilePath + "]")) val parser = new XmlScoverageReportConstructingParser(sourceFromFile(reportFilePath)) parser.parse() From 5e926ae29bc463baa9764fd5229f4a553a9630ef Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 7 Feb 2014 13:12:35 -0800 Subject: [PATCH 027/101] Restructure --- dev | 8 - parent/pom.xml | 580 ++++++++++++++++++ .gitignore => plugin/.gitignore | 0 pom.xml => plugin/pom.xml | 0 .../plugins/scoverage/ScoveragePlugin.java | 0 .../plugins/scoverage/language/Scala.java | 0 .../scoverage/measure/ScalaMetrics.java | 0 .../plugins/scoverage/resource/ScalaFile.java | 0 .../scoverage/resource/SingleDirectory.java | 0 .../scoverage/sensor/ScoverageSensor.java | 0 .../sensor/ScoverageSourceImporterSensor.java | 0 .../plugins/scoverage/util/LogUtil.java | 0 .../scoverage/widget/ScoverageWidget.java | 0 .../plugins/scoverage/widget.html.erb | 0 .../scoverage/ScoverageReportParser.scala | 0 .../plugins/scoverage/StatementCoverage.scala | 0 .../xml/StubScoverageReportParser.scala | 0 ...XmlScoverageReportConstructingParser.scala | 0 .../xml/XmlScoverageReportParser.scala | 0 ...coverageReportConstructingParserSpec.scala | 0 .../xml/XmlScoverageReportParserSpec.scala | 0 .../scoverage/xml/data/XmlReportFile1.scala | 0 test | 7 - 23 files changed, 580 insertions(+), 15 deletions(-) delete mode 100755 dev create mode 100644 parent/pom.xml rename .gitignore => plugin/.gitignore (100%) rename pom.xml => plugin/pom.xml (100%) rename {src => plugin/src}/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java (100%) rename {src => plugin/src}/main/java/com/buransky/plugins/scoverage/language/Scala.java (100%) rename {src => plugin/src}/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java (100%) rename {src => plugin/src}/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java (100%) rename {src => plugin/src}/main/java/com/buransky/plugins/scoverage/resource/SingleDirectory.java (100%) rename {src => plugin/src}/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java (100%) rename {src => plugin/src}/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java (100%) rename {src => plugin/src}/main/java/com/buransky/plugins/scoverage/util/LogUtil.java (100%) rename {src => plugin/src}/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java (100%) rename {src => plugin/src}/main/resources/com/buransky/plugins/scoverage/widget.html.erb (100%) rename {src => plugin/src}/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala (100%) rename {src => plugin/src}/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala (100%) rename {src => plugin/src}/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala (100%) rename {src => plugin/src}/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala (100%) rename {src => plugin/src}/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala (100%) rename {src => plugin/src}/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala (100%) rename {src => plugin/src}/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala (100%) rename {src => plugin/src}/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala (100%) delete mode 100755 test diff --git a/dev b/dev deleted file mode 100755 index 130ee7c..0000000 --- a/dev +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -/home/rado/bin/sonar/bin/linux-x86-64/sonar.sh stop - -mvn install -DskipTests -cp ./target/sonar-scoverage-plugin-1.0-SNAPSHOT.jar /home/rado/bin/sonar/extensions/plugins/ - -/home/rado/bin/sonar/bin/linux-x86-64/sonar.sh start diff --git a/parent/pom.xml b/parent/pom.xml new file mode 100644 index 0000000..c9fa59f --- /dev/null +++ b/parent/pom.xml @@ -0,0 +1,580 @@ + + + 4.0.0 + + org.codehaus.sonar-plugins + parent + 18 + pom + + Sonar plugins parent + http://sonar-plugins.codehaus.org + 2009 + + + + GNU LGPL 3 + http://www.gnu.org/licenses/lgpl.txt + repo + + + + + + Sonar dev mailing list + http://xircles.codehaus.org/projects/sonar/lists + http://xircles.codehaus.org/projects/sonar/lists + dev@sonar.codehaus.org + + + + + + ${maven.min.version} + + + + scm:svn:http://svn.codehaus.org/sonar-plugins/tags/18 + scm:svn:https://svn.codehaus.org/sonar-plugins/tags/18 + scm:svn:https://svn.codehaus.org/sonar-plugins/tags/18 + + + jira + http://jira.codehaus.org/browse/SONARPLUGINS + + + bamboo + http://bamboo.ci.codehaus.org/browse/SONAR + + + + codehaus.org + Sonar plugins repository + dav:https://dav.codehaus.org/repository/sonar-plugins + + + ${sonar.snapshotRepository.id} + Sonar plugins snapshot repository + false + ${sonar.snapshotRepository.url} + + + + + UTF-8 + 2.2.1 + 1.6 + ${maven.build.timestamp} + yyyy-MM-dd'T'HH:mm:ssZ + codehaus.org + dav:https://dav.codehaus.org/snapshots.repository/sonar-plugins + + + + + 2.4 + 2.5 + 3.0 + 2.8 + 2.7 + 1.2 + 2.12.4 + 2.4 + 2.4 + 1.7 + 2.9 + 1.9.0 + 3.2 + 2.4.2 + 2.6 + 1.7.1 + 2.2.1 + 2.12.4 + 3.2 + 1.4 + + 1.9 + 1.2 + 1.0-beta-1 + + 1.5 + 1.6 + + + GNU LGPL 3 + ${project.name} + ${project.inceptionYear} + ${project.organization.name} + dev@sonar.codehaus.org + + + org.codehaus.mojo.signature + java16 + 1.1 + + + + + + + org.apache.maven.wagon + wagon-webdav + 1.0-beta-2 + + + + + + + + org.codehaus.mojo + animal-sniffer-maven-plugin + ${version.animal-sniffer.plugin} + + + ${animal-sniffer.signature.groupId} + ${animal-sniffer.signature.artifactId} + ${animal-sniffer.signature.version} + + + + + org.apache.maven.plugins + maven-assembly-plugin + ${version.assembly.plugin} + + + + + 420 + + 493 + 493 + + + + + org.codehaus.mojo + buildnumber-maven-plugin + ${version.buildnumber.plugin} + + + org.apache.maven.plugins + maven-clean-plugin + ${version.clean.plugin} + + + org.apache.maven.plugins + maven-compiler-plugin + ${version.compiler.plugin} + + + org.apache.maven.plugins + maven-dependency-plugin + ${version.dependency.plugin} + + + org.apache.maven.plugins + maven-deploy-plugin + ${version.deploy.plugin} + + + org.apache.maven.plugins + maven-enforcer-plugin + ${version.enforcer.plugin} + + + org.apache.maven.plugins + maven-failsafe-plugin + ${version.failsafe.plugin} + + + org.apache.maven.plugins + maven-install-plugin + ${version.install.plugin} + + + org.sonatype.plugins + jarjar-maven-plugin + ${version.jarjar.plugin} + + + com.mycila.maven-license-plugin + maven-license-plugin + ${version.license.plugin} + + + org.apache.maven.plugins + maven-jar-plugin + ${version.jar.plugin} + + + + ${project.version} + + ${buildNumber} + ${timestamp} + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${version.javadoc.plugin} + + true + + + + org.apache.maven.plugins + maven-plugin-plugin + ${version.plugin.plugin} + + + org.apache.maven.plugins + maven-release-plugin + ${version.release.plugin} + + https://svn.codehaus.org/sonar-plugins/tags + true + false + + -Prelease + + + + + org.apache.maven.scm + maven-scm-api + 1.8.1 + + + org.apache.maven.scm + maven-scm-provider-gitexe + 1.8.1 + + + + + org.apache.maven.plugins + maven-resources-plugin + ${version.resources.plugin} + + + org.apache.maven.plugins + maven-shade-plugin + ${version.shade.plugin} + + + org.apache.maven.plugins + maven-source-plugin + ${version.source.plugin} + + + org.apache.maven.plugins + maven-surefire-plugin + ${version.surefire.plugin} + + + org.apache.maven.plugins + maven-site-plugin + ${version.site.plugin} + + + org.apache.maven.plugins + maven-gpg-plugin + ${version.gpg.plugin} + + + org.codehaus.mojo + native2ascii-maven-plugin + ${version.native2ascii.plugin} + + + org.codehaus.sonar + sonar-packaging-maven-plugin + ${version.sonar-packaging.plugin} + + + org.codehaus.sonar + sonar-dev-maven-plugin + ${version.sonar-dev.plugin} + + + + + + + org.codehaus.mojo + buildnumber-maven-plugin + + + validate + + create + + + + + false + false + true + 0 + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${jdk.min.version} + ${jdk.min.version} + + + + + org.apache.maven.plugins + maven-surefire-plugin + + random + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce + + enforce + + + + + To build this project Maven ${maven.min.version} (or upper) is required. Please install it. + ${maven.min.version} + + + To build this project JDK ${jdk.min.version} (or upper) is required. Please install it. + ${jdk.min.version} + + + + Build reproducibility : always define plugin versions! + true + true + clean,deploy + + + + Animal-sniffer throws exception when icu4j version 2.6.1 used. + true + + com.ibm.icu:icu4j:[2.6.1] + + + + + + + + + + + org.codehaus.mojo + animal-sniffer-maven-plugin + + + enforce-java-api-compatibility + verify + + check + + + + ${animal-sniffer.signature.groupId} + ${animal-sniffer.signature.artifactId} + ${animal-sniffer.signature.version} + + + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + verify + + jar-no-fork + + + + + + + com.mycila.maven-license-plugin + maven-license-plugin + + + org.codehaus.sonar-plugins + license-headers + 1.0 + + + +
org/sonar/plugins/licenseheaders/${license.name}.txt
+ + org/sonar/plugins/licenseheaders/SonarSource.txt + + true + + src/main/java/** + src/test/java/** + + + SLASHSTAR_STYLE + + + ${license.title} + ${license.year} + ${license.owner} + ${license.mailto} + + ${project.build.sourceEncoding} +
+ + + enforce-license-headers + validate + + check + + + +
+ + + org.codehaus.sonar + sonar-packaging-maven-plugin + true + + + + + ${buildNumber} + ${timestamp} + + + + +
+
+ + + + skipSanityChecks + + + skipSanityChecks + true + + + + true + true + + + + + release + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + + + coverage-per-test + + + org.codehaus.sonar-plugins.java + sonar-jacoco-listeners + 1.2 + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + listener + org.sonar.java.jacoco.JUnitListener + + + + + + + + + integration-tests + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + integration-test + integration-test + + integration-test + + + + verify + verify + + verify + + + + + + + + +
diff --git a/.gitignore b/plugin/.gitignore similarity index 100% rename from .gitignore rename to plugin/.gitignore diff --git a/pom.xml b/plugin/pom.xml similarity index 100% rename from pom.xml rename to plugin/pom.xml diff --git a/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java b/plugin/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java similarity index 100% rename from src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java rename to plugin/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java diff --git a/src/main/java/com/buransky/plugins/scoverage/language/Scala.java b/plugin/src/main/java/com/buransky/plugins/scoverage/language/Scala.java similarity index 100% rename from src/main/java/com/buransky/plugins/scoverage/language/Scala.java rename to plugin/src/main/java/com/buransky/plugins/scoverage/language/Scala.java diff --git a/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java b/plugin/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java similarity index 100% rename from src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java rename to plugin/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java diff --git a/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java b/plugin/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java similarity index 100% rename from src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java rename to plugin/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java diff --git a/src/main/java/com/buransky/plugins/scoverage/resource/SingleDirectory.java b/plugin/src/main/java/com/buransky/plugins/scoverage/resource/SingleDirectory.java similarity index 100% rename from src/main/java/com/buransky/plugins/scoverage/resource/SingleDirectory.java rename to plugin/src/main/java/com/buransky/plugins/scoverage/resource/SingleDirectory.java diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java b/plugin/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java similarity index 100% rename from src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java rename to plugin/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java diff --git a/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java b/plugin/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java similarity index 100% rename from src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java rename to plugin/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java diff --git a/src/main/java/com/buransky/plugins/scoverage/util/LogUtil.java b/plugin/src/main/java/com/buransky/plugins/scoverage/util/LogUtil.java similarity index 100% rename from src/main/java/com/buransky/plugins/scoverage/util/LogUtil.java rename to plugin/src/main/java/com/buransky/plugins/scoverage/util/LogUtil.java diff --git a/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java b/plugin/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java similarity index 100% rename from src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java rename to plugin/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java diff --git a/src/main/resources/com/buransky/plugins/scoverage/widget.html.erb b/plugin/src/main/resources/com/buransky/plugins/scoverage/widget.html.erb similarity index 100% rename from src/main/resources/com/buransky/plugins/scoverage/widget.html.erb rename to plugin/src/main/resources/com/buransky/plugins/scoverage/widget.html.erb diff --git a/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala similarity index 100% rename from src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala rename to plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala diff --git a/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala similarity index 100% rename from src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala rename to plugin/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala similarity index 100% rename from src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala rename to plugin/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala similarity index 100% rename from src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala rename to plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala diff --git a/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala similarity index 100% rename from src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala rename to plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala diff --git a/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala similarity index 100% rename from src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala rename to plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala diff --git a/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala similarity index 100% rename from src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala rename to plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala diff --git a/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala similarity index 100% rename from src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala rename to plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala diff --git a/test b/test deleted file mode 100755 index 27b0981..0000000 --- a/test +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -cd /home/rado/workspace/aaa -#sbt clean scoverage:test -/home/rado/bin/sonar-runner/bin/sonar-runner - -cd /home/rado//workspace/sonar-scoverage-plugin From eafe0590272aad2e1fa7d376e0f72010f6499265 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 7 Feb 2014 13:20:28 -0800 Subject: [PATCH 028/101] . --- plugin/.gitignore | 1 + plugin/README.md | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 plugin/README.md diff --git a/plugin/.gitignore b/plugin/.gitignore index b4a5277..25b0cae 100644 --- a/plugin/.gitignore +++ b/plugin/.gitignore @@ -1,3 +1,4 @@ .idea/ target/ *.iml +dev \ No newline at end of file diff --git a/plugin/README.md b/plugin/README.md new file mode 100644 index 0000000..0016f5e --- /dev/null +++ b/plugin/README.md @@ -0,0 +1,11 @@ +# Sonar Scoverage Plugin source code # + +Useful bash script for plugin development to stop Sonar server, build plugin, copy it to Sonar plugin +directory and start Sonar server again: + + /bin/linux-x86-64/sonar.sh stop + + mvn install + cp ./target/sonar-scoverage-plugin-1.0-SNAPSHOT.jar /extensions/plugins/ + + /bin/linux-x86-64/sonar.sh start \ No newline at end of file From 9e49a280803021b0add6bf3ad4c869d55da9ccf6 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 7 Feb 2014 13:32:20 -0800 Subject: [PATCH 029/101] Travis CI --- .travis.yml | 3 +++ README.md | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e891641 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: java +jdk: + - openjdk6 diff --git a/README.md b/README.md index 136ceed..4c7b636 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ #Sonar Scoverage Plugin# +[![Build Status](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin.png)](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin) + Plugin for [SonarQube] that imports statement coverage generated by [Scoverage] for Scala projects. ## Requirements ## @@ -48,4 +50,4 @@ the **Custom Measures** section. Click **Edit** in the newly added **Custom Meas [PluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v1.0-SNAPSHOT/sonar-scoverage-plugin-1.0-SNAPSHOT.jar [SonarQube]: http://www.sonarqube.org/ "SonarQube" [Scoverage]: https://github.com/scoverage/scalac-scoverage-plugin "Scoverage" -[sbt-scoverage]: https://github.com/scoverage/sbt-scoverage \ No newline at end of file +[sbt-scoverage]: https://github.com/scoverage/sbt-scoverage From dab15acf578cf427fa37bbdac8fea4e292e032bf Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 7 Feb 2014 13:35:33 -0800 Subject: [PATCH 030/101] Travis CI --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index e891641..d03df88 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ language: java +script: "mvn -f .releng/pom.xml install" jdk: - openjdk6 From b05e3d82abba31d6666c489c572fd64a8fca6fde Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 7 Feb 2014 13:37:48 -0800 Subject: [PATCH 031/101] Travis CI --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d03df88..7bdce0d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ language: java -script: "mvn -f .releng/pom.xml install" +script: "mvn -f plugin/pom.xml install" jdk: - openjdk6 From a463dcc5920ac0e24a15e64a2fd95cf604e73f51 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 7 Feb 2014 14:42:27 -0800 Subject: [PATCH 032/101] multi-module sample project --- samples/sbt/multi-module/.gitignore | 3 ++ samples/sbt/multi-module/build.sbt | 9 ++++++ samples/sbt/multi-module/module1/build.sbt | 13 +++++++++ .../sbt/multiModule/module1/Beer.scala | 29 +++++++++++++++++++ .../samples/sbt/multiModule/module1/Pub.scala | 11 +++++++ .../sbt/multiModule/module1/BeerSpec.scala | 18 ++++++++++++ .../sbt/multiModule/module1/PubSpec.scala | 11 +++++++ samples/sbt/multi-module/project/Common.scala | 5 ++++ samples/sbt/multi-module/project/plugins.sbt | 3 ++ .../sbt/multi-module/sonar-project.properties | 15 ++++++++++ 10 files changed, 117 insertions(+) create mode 100644 samples/sbt/multi-module/.gitignore create mode 100644 samples/sbt/multi-module/build.sbt create mode 100644 samples/sbt/multi-module/module1/build.sbt create mode 100644 samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Beer.scala create mode 100644 samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Pub.scala create mode 100644 samples/sbt/multi-module/module1/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/BeerSpec.scala create mode 100644 samples/sbt/multi-module/module1/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/PubSpec.scala create mode 100644 samples/sbt/multi-module/project/Common.scala create mode 100644 samples/sbt/multi-module/project/plugins.sbt create mode 100644 samples/sbt/multi-module/sonar-project.properties diff --git a/samples/sbt/multi-module/.gitignore b/samples/sbt/multi-module/.gitignore new file mode 100644 index 0000000..0c08aab --- /dev/null +++ b/samples/sbt/multi-module/.gitignore @@ -0,0 +1,3 @@ +.idea +.idea_modules +target \ No newline at end of file diff --git a/samples/sbt/multi-module/build.sbt b/samples/sbt/multi-module/build.sbt new file mode 100644 index 0000000..1f1caef --- /dev/null +++ b/samples/sbt/multi-module/build.sbt @@ -0,0 +1,9 @@ +organization := "com.buransky" + +scalaVersion := "2.10.3" + +lazy val root = project.in(file(".")).aggregate(module1, module2) + +lazy val module1 = project.in(file("module1")) + +lazy val module2 = project.in(file("module2")) diff --git a/samples/sbt/multi-module/module1/build.sbt b/samples/sbt/multi-module/module1/build.sbt new file mode 100644 index 0000000..e3d30a2 --- /dev/null +++ b/samples/sbt/multi-module/module1/build.sbt @@ -0,0 +1,13 @@ +organization := Common.organization + +name := Common.baseName + "-module1" + +version := Common.version + +scalaVersion := "2.10.3" + +libraryDependencies ++= Seq( + "org.scalatest" %% "scalatest" % "2.0" % "test" +) + +ScoverageSbtPlugin.instrumentSettings \ No newline at end of file diff --git a/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Beer.scala b/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Beer.scala new file mode 100644 index 0000000..d3f76a2 --- /dev/null +++ b/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Beer.scala @@ -0,0 +1,29 @@ +package com.buransky.plugins.scoverage.samples.sbt.multiModule.module1 + +import scala.util.Random + +trait Beer { + val volume: Double + def isGood: Boolean = (volume > 0.0) +} + +case object EmptyBeer extends { + val volume = 0.0 +} with Beer + +trait SlovakBeer extends Beer { + override def isGood = Random.nextBoolean +} + +trait BelgianBeer extends Beer { + if (volume > 0.25) + throw new IllegalArgumentException("Too big beer for belgian beer!") + + override def isGood = true +} + +case class HordonBeer(volume: Double) extends SlovakBeer { + override def isGood = false +} + +case class ChimayBeer(volume: Double) extends BelgianBeer diff --git a/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Pub.scala b/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Pub.scala new file mode 100644 index 0000000..e8a21a1 --- /dev/null +++ b/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Pub.scala @@ -0,0 +1,11 @@ +package com.buransky.plugins.scoverage.samples.sbt.multiModule.module1 + +trait Pub { + def offer: Iterable[_ <: Beer] + def giveMeGreat: Beer +} + +object Delirium extends Pub { + def offer = List(HordonBeer(0.5), ChimayBeer(0.2)) + def giveMeGreat = offer.filter(_.isGood).filter(_.volume > 0.3).head +} \ No newline at end of file diff --git a/samples/sbt/multi-module/module1/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/BeerSpec.scala b/samples/sbt/multi-module/module1/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/BeerSpec.scala new file mode 100644 index 0000000..8502ce1 --- /dev/null +++ b/samples/sbt/multi-module/module1/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/BeerSpec.scala @@ -0,0 +1,18 @@ +package com.buransky.plugins.scoverage.samples.sbt.multiModule.module1 + +import org.scalatest.{Matchers, FlatSpec} + +class BeerSpec extends FlatSpec with Matchers { + behavior of "Beer" + + "isGood" must "be true if not empty" in { + val beer = new Beer { val volume = 0.1 } + beer.isGood should equal(true) + } + + behavior of "EmptyBeer" + + it must "be empty" in { + EmptyBeer.volume should equal(0.0) + } +} diff --git a/samples/sbt/multi-module/module1/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/PubSpec.scala b/samples/sbt/multi-module/module1/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/PubSpec.scala new file mode 100644 index 0000000..71c40a4 --- /dev/null +++ b/samples/sbt/multi-module/module1/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/PubSpec.scala @@ -0,0 +1,11 @@ +package com.buransky.plugins.scoverage.samples.sbt.multiModule.module1 + +import org.scalatest.{FlatSpec, Matchers} + +class PubSpec extends FlatSpec with Matchers { + behavior of "Delirium" + + it must "give me what I want" in { + the[NoSuchElementException] thrownBy Delirium.giveMeGreat + } +} \ No newline at end of file diff --git a/samples/sbt/multi-module/project/Common.scala b/samples/sbt/multi-module/project/Common.scala new file mode 100644 index 0000000..b3f598a --- /dev/null +++ b/samples/sbt/multi-module/project/Common.scala @@ -0,0 +1,5 @@ +object Common { + val organization = "com.buransky" + val baseName = "multi-module" + val version = "1.0.0" +} \ No newline at end of file diff --git a/samples/sbt/multi-module/project/plugins.sbt b/samples/sbt/multi-module/project/plugins.sbt new file mode 100644 index 0000000..046c8d7 --- /dev/null +++ b/samples/sbt/multi-module/project/plugins.sbt @@ -0,0 +1,3 @@ +resolvers += Classpaths.sbtPluginReleases + +addSbtPlugin("com.sksamuel.scoverage" %% "sbt-scoverage" % "0.95.7") \ No newline at end of file diff --git a/samples/sbt/multi-module/sonar-project.properties b/samples/sbt/multi-module/sonar-project.properties new file mode 100644 index 0000000..e464abb --- /dev/null +++ b/samples/sbt/multi-module/sonar-project.properties @@ -0,0 +1,15 @@ +sonar.projectKey=com.buranskt:multi-module +sonar.projectName=Sonar Scoverage plugin multi-module sample project +sonar.projectVersion=1.0.0 + +sonar.language=scala + +sonar.modules=module1 + +module1.sonar.sources=src/main/scala +module1.sonar.tests=src/test/scala +module1.sonar.scoverage.reportPath=target/scala-2.10/scoverage-report/scoverage.xml + +module2.sonar.sources=src/main/scala +module2.sonar.tests=src/test/scala +#module2.sonar.scoverage.reportPath=target/scala-2.10/scoverage-report/scoverage.xml \ No newline at end of file From 816b6039175e52429728a6e0eae2f52af6bb250b Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 7 Feb 2014 15:06:38 -0800 Subject: [PATCH 033/101] multi-module sample --- samples/sbt/multi-module/module2/build.sbt | 13 +++++++++++++ .../samples/sbt/multiModule/module2/Animal.scala | 13 +++++++++++++ .../sbt/multiModule/module2/AnimalSpec.scala | 11 +++++++++++ samples/sbt/multi-module/sonar-project.properties | 4 ++-- 4 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 samples/sbt/multi-module/module2/build.sbt create mode 100644 samples/sbt/multi-module/module2/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module2/Animal.scala create mode 100644 samples/sbt/multi-module/module2/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module2/AnimalSpec.scala diff --git a/samples/sbt/multi-module/module2/build.sbt b/samples/sbt/multi-module/module2/build.sbt new file mode 100644 index 0000000..3d44cc1 --- /dev/null +++ b/samples/sbt/multi-module/module2/build.sbt @@ -0,0 +1,13 @@ +organization := Common.organization + +name := Common.baseName + "-module2" + +version := Common.version + +scalaVersion := "2.10.3" + +libraryDependencies ++= Seq( + "org.scalatest" %% "scalatest" % "2.0" % "test" +) + +ScoverageSbtPlugin.instrumentSettings \ No newline at end of file diff --git a/samples/sbt/multi-module/module2/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module2/Animal.scala b/samples/sbt/multi-module/module2/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module2/Animal.scala new file mode 100644 index 0000000..6272933 --- /dev/null +++ b/samples/sbt/multi-module/module2/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module2/Animal.scala @@ -0,0 +1,13 @@ +package com.buransky.plugins.scoverage.samples.sbt.multiModule.module2 + +trait Animal { + val legs: Int + val eyes: Int + val canFly: Boolean + val canSwim: Boolean +} + +object Animal { + def fancy(farm: Iterable[Animal]): Iterable[Animal] = + farm.filter(_.legs > 10).filter(_.canFly).filter(_.canSwim) +} \ No newline at end of file diff --git a/samples/sbt/multi-module/module2/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module2/AnimalSpec.scala b/samples/sbt/multi-module/module2/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module2/AnimalSpec.scala new file mode 100644 index 0000000..2088f82 --- /dev/null +++ b/samples/sbt/multi-module/module2/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module2/AnimalSpec.scala @@ -0,0 +1,11 @@ +package com.buransky.plugins.scoverage.samples.sbt.multiModule.module2 + +import org.scalatest.{Matchers, FlatSpec} + +class AnimalSpec extends FlatSpec with Matchers { + behavior of "fancy" + + it should "do nothing" in { + Animal.fancy(Nil) should equal(Nil) + } +} diff --git a/samples/sbt/multi-module/sonar-project.properties b/samples/sbt/multi-module/sonar-project.properties index e464abb..19829f7 100644 --- a/samples/sbt/multi-module/sonar-project.properties +++ b/samples/sbt/multi-module/sonar-project.properties @@ -4,7 +4,7 @@ sonar.projectVersion=1.0.0 sonar.language=scala -sonar.modules=module1 +sonar.modules=module1,module2 module1.sonar.sources=src/main/scala module1.sonar.tests=src/test/scala @@ -12,4 +12,4 @@ module1.sonar.scoverage.reportPath=target/scala-2.10/scoverage-report/scoverage. module2.sonar.sources=src/main/scala module2.sonar.tests=src/test/scala -#module2.sonar.scoverage.reportPath=target/scala-2.10/scoverage-report/scoverage.xml \ No newline at end of file +module2.sonar.scoverage.reportPath=target/scala-2.10/scoverage-report/scoverage.xml \ No newline at end of file From e08f84d5cbe9fcc7d782ac68837458ba359c4140 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 7 Feb 2014 15:23:32 -0800 Subject: [PATCH 034/101] . --- README.md | 4 ++++ doc/img/01_dashboard.png | Bin 0 -> 51803 bytes doc/img/02_detail.png | Bin 0 -> 59495 bytes doc/img/03_columns.png | Bin 0 -> 54122 bytes doc/img/04_coverage.png | Bin 0 -> 57559 bytes samples/sbt/multi-module/README.md | 15 +++++++++++++++ 6 files changed, 19 insertions(+) create mode 100644 doc/img/01_dashboard.png create mode 100644 doc/img/02_detail.png create mode 100644 doc/img/03_columns.png create mode 100644 doc/img/04_coverage.png create mode 100644 samples/sbt/multi-module/README.md diff --git a/README.md b/README.md index 4c7b636..3951207 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,10 @@ the project dashboard, click **Configure widgets** in the top-right corner, clic the **Custom Measures** section. Click **Edit** in the newly added **Custom Measures** widget and choose **Statement coverage** for **Metric 1**. Click **Save**, **Back to dashboard**. Enjoy. +## Screenshots ## + +![Alt text](/doc/img/01_dashboard.png "Project dashboard with Scoverage plugin") + [PluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v1.0-SNAPSHOT/sonar-scoverage-plugin-1.0-SNAPSHOT.jar [SonarQube]: http://www.sonarqube.org/ "SonarQube" [Scoverage]: https://github.com/scoverage/scalac-scoverage-plugin "Scoverage" diff --git a/doc/img/01_dashboard.png b/doc/img/01_dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..5f1d893b73261044eca7edf22937e1b5aab45d3a GIT binary patch literal 51803 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 L6)6$c|N8#{8}%_= literal 0 HcmV?d00001 diff --git a/doc/img/02_detail.png b/doc/img/02_detail.png new file mode 100644 index 0000000000000000000000000000000000000000..513d4f45a45ef5193c8a18bb0910d16b8b3cc130 GIT binary patch literal 59495 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≪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 TSWPR#K0zqRD1(b$z5n!I0(VwP literal 0 HcmV?d00001 diff --git a/doc/img/03_columns.png b/doc/img/03_columns.png new file mode 100644 index 0000000000000000000000000000000000000000..74003c47a1af183d3b3e63272b1d0d3c5ad4977d GIT binary patch literal 54122 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%0Dx;?4a3?*2;Da3&V48G1S*Wnlbex|{_`$9w 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 X`*~d_@8Ajwje$d*IOW6h4!`ve!rbQ@ literal 0 HcmV?d00001 diff --git a/doc/img/04_coverage.png b/doc/img/04_coverage.png new file mode 100644 index 0000000000000000000000000000000000000000..a60e7574c0052df2b438fab013e75a6e671cfbff GIT binary patch literal 57559 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 z zs_j_{LT>$M;U=UM{NU)&zX>Vw*dPsTRcpM6iB=fE0R|3D`4&G#-?`3KSj}uwowu26 z446FgAJj6hspes1l+7EHCn9oZ0z-$ZmZ71T}TjqW#ee4Y{KiH@`u;KIn4*odK3VBdk)U6LzemFy?;mB zhI_xj_9Y$mRBz-$`(`iR6A8(N*!gNau3xSGVOIHcJ^q8Rp8;>g{|JD0jfc4!-VkcGfM=sW`B4je5GLcAw~43qFy=%42aXQ`u=TzHXvuUhvw! zJ(yGdl^OW{chtADc=eZ}rae!-Cta}=dKFuaR8&=Q%dI}It=FhJJ^A)ix!cTbYnRv? zE~z7jl3k*xYEJ8uR|>q!)D#Dl3(O_b5GjD_gs-zDj+FHXKObn2(imd<KaFkBD?Ahz$Cwkd;I<9;rr+tfPnzhB(FQzNL)M`Mk%IJ zaU@w&$zWtz*o^{MLS2JZ@!ztZo<_{xT6aT2%^g2iekgn^lfzUDM~ivero{Cg;X9DQ zPQxV8%9lu#Y@3`s_FFaU6;x@tTv|NEzO5&;fyxmdMAX;*uFXAJ#fH+!I!?8 z3ubNJloeLvPdq@AYPECYn8L)qQ)P6|c4>)RWr_AiP9@ofgGQ&$G-0p)aR zYUG@w6*#>vE*Fcg_^Vp!km=AwdF|Z(dDMH9Rs%&7Ik#&)iVcUjnYnq}*PoZ+03T__ zJIAD%J!`s{gc7LS@|71V6D`?$Up#(CrB3yoynMYx1bjM@?7SE`ua(pG*r@IlBWhz@ z9}DIwjs6WThiVp_k8%Y9^TsVUfrqt07@{XSKK3X2*Gb{{PW-a91*8M($>%Q7@5s{( zSrcT-wdrG&>RN9qh5Ejm{RD)KyObvv%pH7NE$x5X+v~pu%=ERhM%4}Q<=pgY>O2Gx zfedBx6vb-_IG4b=^;Mtf_RV^_Uf0}V3I;}bB^Qarpr`nN!rO#|r-+DvqF8{)u$kRg zYT^fte)0x7cX+uG!sTxY9>=D?u-jImHx(VR0XnXC5h+fzw&#_7$$~;EB2O|0dP67x zpl$v|gfkYo|9ZW!rp>;SRwD}V$3HmZp!#S{(-G1>fqHE3hJd?>DdnxK%Hn(T}pcT=gsJ%wq8{96XV9-yE2INO&8@6WqMY;`?ZO)Ki zT2p}b2G3`f|1k6JxUo{!0XY~6+2dG_YF%@;e#0{{JgryC!5B}6zTde=?VgB<;F%wU?!qmzF;4kH8vmTfkkgK@vq~;)m;k^%^D_ ziIM)WG3CDb>=$hl$(V41(XsV5F3FPU4=g4|)wM((mDNwgxFFxs7%urNFfTdRFm%(% z5co-jZBbd{Q3e2u`Dey?QSf9az6gey<)uXa$aSxiJL zlU^SuFm$Yu@_5chKGY3!=Ncq*N{NgkfqevgejQ$tSGx{8O$?j2`H;_@xPCJ8Zn&*Q z_V^Dch405}wQN|aoYB@$1iPFBA#r!0^cfZ4!&p$m8(8!$JY(x?)m^q4iExBcVjr$) zdGXQFRCY$8nA)H$%1Bvx9j4C)e0+P%PV8vXSi#3Rt;O4umyD LB4> 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 BOxpke literal 0 HcmV?d00001 diff --git a/samples/sbt/multi-module/README.md b/samples/sbt/multi-module/README.md new file mode 100644 index 0000000..68bc857 --- /dev/null +++ b/samples/sbt/multi-module/README.md @@ -0,0 +1,15 @@ +# Multi-module SBT sample project for Sonar Scoverage plugin # + +Run scoverage to generate coverage reports: + + $ sbt clean scoverage:test + +And then run Sonar runner to upload data from reports to the Sonar server: + + $ sonar-runner + +## Requirements ## + +- Installed Sonar Scoverage plugin +- Installed SBT +- Installed Sonar runner \ No newline at end of file From 14393e1e5823c548d18f6c0885312e99d7bf7203 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 7 Feb 2014 15:29:57 -0800 Subject: [PATCH 035/101] . --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3951207..02e988f 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,17 @@ the **Custom Measures** section. Click **Edit** in the newly added **Custom Meas ## Screenshots ## -![Alt text](/doc/img/01_dashboard.png "Project dashboard with Scoverage plugin") +Project dashboard with Scoverage plugin: +![Project dashboard with Scoverage plugin](/doc/img/01_dashboard.png "Project dashboard with Scoverage plugin") + +Multi-module project overview: +![Multi-module project overview](/doc/img/02_detail.png "Multi-module project overview") + +Columns with statement coverage, total number of statements and number of covered statements: +![Columns](/doc/img/03_columns.png "Columns") + +Source code markup with covered and uncovered lines: +![Source code markup](/doc/img/04_coverage.png "Source code markup") [PluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v1.0-SNAPSHOT/sonar-scoverage-plugin-1.0-SNAPSHOT.jar [SonarQube]: http://www.sonarqube.org/ "SonarQube" From da30aa3bcb23dbfbe5e2555d6f4fe3dbda00f5e3 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 7 Feb 2014 15:44:33 -0800 Subject: [PATCH 036/101] . --- plugin/pom.xml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index 50a2ecf..9d3aeb0 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -11,7 +11,7 @@ sonar-scoverage-plugin - 1.0-SNAPSHOT + 1.0.0 sonar-plugin Sonar Scoverage Plugin @@ -24,6 +24,15 @@ http://www.buransky.com + + + radoburansky + Rado Buransky + radoburansky@gmail.com + http://www.buransky.com/ + + + org.codehaus.sonar sonar-plugin-api ${sonar.version} provided - - org.codehaus.sonar-plugins.java - sonar-java-plugin - 1.5 - sonar-plugin - provided - + org.scala-lang scala-library ${scala.version} - - org.scala-lang - scala-compiler - ${scala.version} - - + org.scalatest scalatest_2.10 @@ -117,6 +107,20 @@ 4.11 test + + + + net.sourceforge.findbugs + jsr305 + 1.3.7 + provided + + + org.apache.maven + maven-project + 2.2.1 + provided + diff --git a/plugin/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java b/plugin/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java deleted file mode 100644 index 19e0121..0000000 --- a/plugin/src/main/java/com/buransky/plugins/scoverage/ScoveragePlugin.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Sonar Scoverage Plugin - * Copyright (C) 2013 Rado Buransky - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scoverage; - -import com.buransky.plugins.scoverage.measure.ScalaMetrics; -import com.buransky.plugins.scoverage.sensor.ScoverageSensor; -import com.buransky.plugins.scoverage.language.Scala; -import com.buransky.plugins.scoverage.sensor.ScoverageSourceImporterSensor; -import com.buransky.plugins.scoverage.widget.ScoverageWidget; -import org.sonar.api.Extension; -import org.sonar.api.SonarPlugin; - -import java.util.ArrayList; -import java.util.List; - -/** - * Plugin entry point. - * - * @author Rado Buransky - */ -public class ScoveragePlugin extends SonarPlugin { - - public List> getExtensions() { - final List> extensions = new ArrayList>(); - extensions.add(ScalaMetrics.class); - extensions.add(Scala.class); - extensions.add(ScoverageSourceImporterSensor.class); - extensions.add(ScoverageSensor.class); - extensions.add(ScoverageWidget.class); - - return extensions; - } - - @Override - public String toString() { - return getClass().getSimpleName(); - } -} diff --git a/plugin/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java b/plugin/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java deleted file mode 100644 index 15a74ab..0000000 --- a/plugin/src/main/java/com/buransky/plugins/scoverage/measure/ScalaMetrics.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Sonar Scoverage Plugin - * Copyright (C) 2013 Rado Buransky - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scoverage.measure; - -import org.sonar.api.measures.CoreMetrics; -import org.sonar.api.measures.Metric; -import org.sonar.api.measures.Metrics; -import org.sonar.api.measures.Metric.ValueType; - -import java.util.Arrays; -import java.util.List; - -/** - * Statement coverage metric definition. - * - * @author Rado Buransky - */ -public final class ScalaMetrics implements Metrics { - private static final String STATEMENT_COVERAGE_KEY = "scoverage"; - public static final Metric STATEMENT_COVERAGE = new Metric.Builder(STATEMENT_COVERAGE_KEY, - "Statement coverage", ValueType.PERCENT) - .setDescription("Statement coverage by tests") - .setDirection(Metric.DIRECTION_BETTER) - .setQualitative(true) - .setDomain(CoreMetrics.DOMAIN_TESTS) - .setWorstValue(0.0) - .setBestValue(100.0) - .create(); - - public static final String COVERED_STATEMENTS_KEY = "covered_statements"; - public static final Metric COVERED_STATEMENTS = new Metric.Builder(COVERED_STATEMENTS_KEY, - "Covered statements", Metric.ValueType.INT) - .setDescription("Number of statements covered by tests") - .setDirection(Metric.DIRECTION_BETTER) - .setQualitative(false) - .setDomain(CoreMetrics.DOMAIN_SIZE) - .setFormula(new org.sonar.api.measures.SumChildValuesFormula(false)) - .create(); - - @Override - public List getMetrics() { - return Arrays.asList(STATEMENT_COVERAGE, COVERED_STATEMENTS); - } -} diff --git a/plugin/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java b/plugin/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java deleted file mode 100644 index 5524bb6..0000000 --- a/plugin/src/main/java/com/buransky/plugins/scoverage/resource/ScalaFile.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Sonar Scoverage Plugin - * Copyright (C) 2013 Rado Buransky - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scoverage.resource; - -import com.buransky.plugins.scoverage.language.Scala; -import org.sonar.api.resources.File; -import org.sonar.api.resources.Language; -import org.sonar.api.resources.Resource; -import org.sonar.api.resources.Directory; - -/** - * Scala source code file resource. - * - * @author Rado Buransky - */ -public class ScalaFile extends Resource { - private final File file; - private SingleDirectory parent; - - public ScalaFile(String key) { - if (key == null) - throw new IllegalArgumentException("Key cannot be null!"); - - file = new File(key); - setKey(key); - } - - @Override - public String getName() { - return file.getName(); - } - - @Override - public String getLongName() { - return file.getLongName(); - } - - @Override - public String getDescription() { - return file.getDescription(); - } - - @Override - public Language getLanguage() { - return Scala.INSTANCE; - } - - @Override - public String getScope() { - return file.getScope(); - } - - @Override - public String getQualifier() { - return file.getQualifier(); - } - - @Override - public SingleDirectory getParent() { - if (parent == null) { - parent = new SingleDirectory(file.getParent().getKey()); - } - - if (Directory.ROOT.equals(parent.getKey())) - return null; - - return parent; - } - - @Override - public boolean matchFilePattern(String antPattern) { - return file.matchFilePattern(antPattern); - } - - @Override - public String toString() { - return file.toString(); - } -} \ No newline at end of file diff --git a/plugin/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java b/plugin/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java deleted file mode 100644 index c1a2f2f..0000000 --- a/plugin/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSensor.java +++ /dev/null @@ -1,264 +0,0 @@ -/* - * Sonar Scoverage Plugin - * Copyright (C) 2013 Rado Buransky - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scoverage.sensor; - -import com.buransky.plugins.scoverage.*; -import com.buransky.plugins.scoverage.language.Scala; -import com.buransky.plugins.scoverage.measure.ScalaMetrics; -import com.buransky.plugins.scoverage.resource.ScalaFile; -import com.buransky.plugins.scoverage.xml.XmlScoverageReportParser$; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonar.api.batch.CoverageExtension; -import org.sonar.api.batch.Sensor; -import org.sonar.api.batch.SensorContext; -import org.sonar.api.config.Settings; -import org.sonar.api.measures.CoverageMeasuresBuilder; -import org.sonar.api.measures.Measure; -import org.sonar.api.resources.*; -import org.sonar.api.scan.filesystem.ModuleFileSystem; -import scala.collection.JavaConversions; -import org.sonar.api.scan.filesystem.PathResolver; -import com.buransky.plugins.scoverage.util.LogUtil; -import org.sonar.api.measures.CoreMetrics; -import com.buransky.plugins.scoverage.resource.SingleDirectory; - -/** - * Main sensor for importing Scoverage report to Sonar. - * - * @author Rado Buransky - */ -public class ScoverageSensor implements Sensor, CoverageExtension { - private static final Logger log = LoggerFactory.getLogger(ScoverageSensor.class); - private final ScoverageReportParser scoverageReportParser; - private final Settings settings; - private final PathResolver pathResolver; - private final ModuleFileSystem moduleFileSystem; - - private static final String SCOVERAGE_REPORT_PATH_PROPERTY = "sonar.scoverage.reportPath"; - - public ScoverageSensor(Settings settings, PathResolver pathResolver, ModuleFileSystem fileSystem) { - this(XmlScoverageReportParser$.MODULE$.apply(), settings, pathResolver, fileSystem); - } - - public ScoverageSensor(ScoverageReportParser scoverageReportParser, Settings settings, - PathResolver pathResolver, ModuleFileSystem moduleFileSystem) { - this.scoverageReportParser = scoverageReportParser; - this.settings = settings; - this.pathResolver = pathResolver; - this.moduleFileSystem = moduleFileSystem; - } - - public boolean shouldExecuteOnProject(Project project) { - return project.getAnalysisType().isDynamic(true) && Scala.INSTANCE.getKey().equals(project.getLanguageKey()); - } - - public void analyse(Project project, SensorContext context) { - String reportPath = getScoverageReportPath(); - if (reportPath != null) - // Single-module project - processProject(scoverageReportParser.parse(reportPath), project, context); - else - // Multi-module project has report path set for each module individually - analyseMultiModuleProject(project, context); - } - - @Override - public String toString() { - return getClass().getSimpleName(); - } - - private String getScoverageReportPath() { - String path = settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY); - if (path == null) - return null; - - java.io.File report = pathResolver.relativeFile(moduleFileSystem.baseDir(), path); - if (!report.exists() || !report.isFile()) { - log.error(LogUtil.f("Report not found at {}"), report); - return null; - } - - return report.getAbsolutePath(); - } - - private void analyseMultiModuleProject(Project project, SensorContext context) { - if (project.isModule()) { - log.warn(LogUtil.f("Report path not set for " + project.name() + " module! [" + - project.name() + "." + SCOVERAGE_REPORT_PATH_PROPERTY + "]")); - return; - } - - // Compute overall statement coverage from submodules - long totalStatementCount = 0; - long coveredStatementCount = 0; - for (Project module: project.getModules()) { - totalStatementCount += analyseStatementCountForModule(module, context); - coveredStatementCount += analyseCoveredStatementCountForModule(module, context); - } - - if (totalStatementCount > 0) { - // Convert to percentage - Double overall = (coveredStatementCount / (double)totalStatementCount) * 100.0; - - // Set overall statement coverage - context.saveMeasure(project, createStatementCoverage(overall)); - - log.info(LogUtil.f("Overall statement coverage is " + String.format("%1$,.2f", overall))); - } - } - - private long analyseCoveredStatementCountForModule(Project module, SensorContext context) { - // Aggregate modules - Measure moduleCoveredStatementCount = context.getMeasure(module, ScalaMetrics.COVERED_STATEMENTS); - - if (moduleCoveredStatementCount == null) { - log.debug(LogUtil.f("Module has no statement coverage. [" + module.name() + "]")); - return 0; - } - - log.debug(LogUtil.f("Covered statement count for " + module.name() + " module. [" + - moduleCoveredStatementCount.getValue() + "]")); - - return moduleCoveredStatementCount.getValue().longValue(); - } - - private long analyseStatementCountForModule(Project module, SensorContext context) { - // Aggregate modules - Measure moduleStatementCount = context.getMeasure(module, CoreMetrics.STATEMENTS); - - if (moduleStatementCount == null) { - log.debug(LogUtil.f("Module has no number of statements. [" + module.name() + "]")); - return 0; - } - - log.debug(LogUtil.f("Statement count for " + module.name() + " module. [" + - moduleStatementCount.getValue() + "]")); - - return moduleStatementCount.getValue().longValue(); - } - - private void processProject(ProjectStatementCoverage projectCoverage, - Project project, SensorContext context) { - // Save measures - saveMeasures(context, project, projectCoverage); - - log.info(LogUtil.f("Statement coverage for " + project.getKey() + " is " + - String.format("%1$,.2f", projectCoverage.rate()))); - - // Process children - processChildren(projectCoverage.children(), context, ""); - } - - private void processDirectory(DirectoryStatementCoverage directoryCoverage, SensorContext context, - String parentDirectory) { - String currentDirectory = appendFilePath(parentDirectory, directoryCoverage.name()); - - // Save measures - saveMeasures(context, new SingleDirectory(currentDirectory), directoryCoverage); - - // Process children - processChildren(directoryCoverage.children(), context, currentDirectory); - } - - private void processFile(FileStatementCoverage fileCoverage, SensorContext context, - String directory) { - ScalaFile scalaSourcefile = new ScalaFile(appendFilePath(directory, fileCoverage.name())); - - // Save measures - saveMeasures(context, scalaSourcefile, fileCoverage); - - // Save line coverage. This is needed just for source code highlighting. - saveLineCoverage(fileCoverage.statements(), scalaSourcefile, context); - } - - private void saveMeasures(SensorContext context, Resource resource, StatementCoverage statementCoverage) { - context.saveMeasure(resource, createStatementCoverage(statementCoverage.rate())); - context.saveMeasure(resource, createStatementCount(statementCoverage.statementCount())); - context.saveMeasure(resource, createCoveredStatementCount(statementCoverage.coveredStatementsCount())); - - log.debug(LogUtil.f("Save measures [" + statementCoverage.rate() + ", " + statementCoverage.statementCount() + - ", " + statementCoverage.coveredStatementsCount() + ", " + resource.getKey() + "]")); - } - - private void saveLineCoverage(scala.collection.Iterable coveredStatements, - ScalaFile scalaSourcefile, SensorContext context) { - // Convert statements to lines - scala.collection.Iterable coveredLines = - StatementCoverage$.MODULE$.statementCoverageToLineCoverage(coveredStatements); - - // Set line hits - CoverageMeasuresBuilder coverage = CoverageMeasuresBuilder.create(); - for (CoveredLine coveredLine: JavaConversions.asJavaIterable(coveredLines)) { - coverage.setHits(coveredLine.line(), coveredLine.hitCount()); - } - - // Save measures - for (Measure measure : coverage.createMeasures()) { - context.saveMeasure(scalaSourcefile, measure); - } - } - - private void processChildren(scala.collection.Iterable children, SensorContext context, - String directory) { - // Process children - for (StatementCoverage child: JavaConversions.asJavaIterable(children)) { - processChild(child, context, directory); - } - } - - private void processChild(StatementCoverage dirOrFile, SensorContext context, - String directory) { - if (dirOrFile instanceof DirectoryStatementCoverage) { - processDirectory((DirectoryStatementCoverage) dirOrFile, context, directory); - } - else { - if (dirOrFile instanceof FileStatementCoverage) { - processFile((FileStatementCoverage) dirOrFile, context, directory); - } - else { - throw new IllegalStateException("Not a file or directory coverage! [" + - dirOrFile.getClass().getName() + "]"); - } - } - } - - private Measure createStatementCoverage(Double rate) { - return new Measure(ScalaMetrics.STATEMENT_COVERAGE, rate); - } - - private Measure createStatementCount(int statements) { - return new Measure(CoreMetrics.STATEMENTS, (double)statements); - } - - private Measure createCoveredStatementCount(int coveredStatements) { - return new Measure(ScalaMetrics.COVERED_STATEMENTS, (double)coveredStatements); - } - - private String appendFilePath(String src, String name) { - String result; - if (!src.isEmpty()) - result = src + java.io.File.separator; - else - result = ""; - - return result + name; - } -} diff --git a/plugin/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java b/plugin/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java deleted file mode 100644 index 30eb848..0000000 --- a/plugin/src/main/java/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Sonar Scoverage Plugin - * Copyright (C) 2013 Rado Buransky - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scoverage.sensor; - -import com.buransky.plugins.scoverage.language.Scala; -import com.buransky.plugins.scoverage.resource.ScalaFile; -import org.apache.commons.io.FileUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonar.api.batch.Phase; -import org.sonar.api.batch.Phase.Name; -import org.sonar.api.batch.Sensor; -import org.sonar.api.batch.SensorContext; -import org.sonar.api.resources.File; -import org.sonar.api.resources.Project; -import org.sonar.api.scan.filesystem.FileQuery; -import org.sonar.api.scan.filesystem.FileType; -import org.sonar.api.scan.filesystem.ModuleFileSystem; - -import java.io.IOException; - -/** - * Imports Scala source code files to Sonar. - * - * @author Rado Buransky - */ -@Phase(name = Name.PRE) -public class ScoverageSourceImporterSensor implements Sensor { - - private static final Logger LOGGER = LoggerFactory.getLogger(ScoverageSourceImporterSensor.class); - private final Scala scala; - private final ModuleFileSystem moduleFileSystem; - - public ScoverageSourceImporterSensor(Scala scala, ModuleFileSystem moduleFileSystem) { - this.scala = scala; - this.moduleFileSystem = moduleFileSystem; - } - - public boolean shouldExecuteOnProject(Project project) { - return project.getLanguage().equals(scala); - } - - public void analyse(Project project, SensorContext sensorContext) { - String charset = moduleFileSystem.sourceCharset().toString(); - - FileQuery query = FileQuery.on(FileType.SOURCE).onLanguage(scala.getKey()); - for (java.io.File sourceFile : moduleFileSystem.files(query)) { - addFileToSonar(project, sensorContext, sourceFile, charset); - } - } - - @Override - public String toString() { - return "Scoverage source importer"; - } - - private void addFileToSonar(Project project, SensorContext sensorContext, java.io.File sourceFile, - String charset) { - try { - String source = FileUtils.readFileToString(sourceFile, charset); - String key = File.fromIOFile(sourceFile, project).getKey(); - ScalaFile resource = new ScalaFile(key); - - sensorContext.index(resource); - sensorContext.saveSource(resource, source); - } catch (IOException ioe) { - LOGGER.error("Could not read the file: " + sourceFile.getAbsolutePath(), ioe); - } - } -} \ No newline at end of file diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala new file mode 100644 index 0000000..c7c3409 --- /dev/null +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala @@ -0,0 +1,45 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scoverage + +import org.sonar.api.{Extension, SonarPlugin} +import com.buransky.plugins.scoverage.measure.ScalaMetrics +import scala.collection.mutable.ListBuffer +import com.buransky.plugins.scoverage.sensor.{ScoverageSensor, ScoverageSourceImporterSensor} +import com.buransky.plugins.scoverage.language.Scala +import com.buransky.plugins.scoverage.widget.ScoverageWidget +import scala.collection.JavaConversions._ + +/** + * Plugin entry point. + * + * @author Rado Buransky + */ +class ScoveragePlugin extends SonarPlugin { + override def getExtensions: java.util.List[Class[_ <: Extension]] = ListBuffer( + classOf[Scala], + classOf[ScalaMetrics], + classOf[ScoverageSourceImporterSensor], + classOf[ScoverageSensor], + classOf[ScoverageWidget] + ) + + override val toString = getClass.getSimpleName +} diff --git a/plugin/src/main/java/com/buransky/plugins/scoverage/language/Scala.java b/plugin/src/main/scala/com/buransky/plugins/scoverage/language/Scala.scala similarity index 70% rename from plugin/src/main/java/com/buransky/plugins/scoverage/language/Scala.java rename to plugin/src/main/scala/com/buransky/plugins/scoverage/language/Scala.scala index 74abe0c..43d881c 100644 --- a/plugin/src/main/java/com/buransky/plugins/scoverage/language/Scala.java +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/language/Scala.scala @@ -1,40 +1,37 @@ -/* - * Sonar Scoverage Plugin - * Copyright (C) 2013 Rado Buransky - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scoverage.language; - -import org.sonar.api.resources.AbstractLanguage; - -/** - * Scala language. - * - * @author Rado Buransky - */ -public class Scala extends AbstractLanguage { - - public static final Scala INSTANCE = new Scala(); - - public Scala() { - super("scala", "Scala"); - } - - public String[] getFileSuffixes() { - return new String[] { "scala" }; - } +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scoverage.language + +import org.sonar.api.resources.AbstractLanguage + +/** + * Scala language. + * + * @author Rado Buransky + */ +class Scala extends AbstractLanguage(Scala.key, Scala.name) { + val getFileSuffixes = Array(Scala.fileExtension) +} + +object Scala { + val key = "scala" + val name = "Scala" + val fileExtension = "scala" } \ No newline at end of file diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala new file mode 100644 index 0000000..9ad2401 --- /dev/null +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala @@ -0,0 +1,58 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scoverage.measure + +import org.sonar.api.measures.{CoreMetrics, Metric, Metrics} +import org.sonar.api.measures.Metric.ValueType +import scala.collection.JavaConversions._ +import scala.collection.mutable.ListBuffer + +/** + * Statement coverage metric definition. + * + * @author Rado Buransky + */ +class ScalaMetrics extends Metrics { + override def getMetrics = ListBuffer(ScalaMetrics.statementCoverage, ScalaMetrics.coveredStatements) +} + +object ScalaMetrics { + private val STATEMENT_COVERAGE_KEY = "scoverage" + private val COVERED_STATEMENTS_KEY = "covered_statements" + + lazy val statementCoverage = new Metric.Builder(STATEMENT_COVERAGE_KEY, + "Statement coverage", ValueType.PERCENT) + .setDescription("Statement coverage by tests") + .setDirection(Metric.DIRECTION_BETTER) + .setQualitative(true) + .setDomain(CoreMetrics.DOMAIN_TESTS) + .setWorstValue(0.0) + .setBestValue(100.0) + .create() + + lazy val coveredStatements = new Metric.Builder(COVERED_STATEMENTS_KEY, + "Covered statements", Metric.ValueType.INT) + .setDescription("Number of statements covered by tests") + .setDirection(Metric.DIRECTION_BETTER) + .setQualitative(false) + .setDomain(CoreMetrics.DOMAIN_SIZE) + .setFormula(new org.sonar.api.measures.SumChildValuesFormula(false)) + .create() +} \ No newline at end of file diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/resource/ScalaFile.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/resource/ScalaFile.scala new file mode 100644 index 0000000..dda8848 --- /dev/null +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/resource/ScalaFile.scala @@ -0,0 +1,62 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scoverage.resource + +import org.sonar.api.resources.{Directory, File, Resource} +import com.buransky.plugins.scoverage.language.Scala + +/** + * Scala source code file resource. + * + * @author Rado Buransky + */ +class ScalaFile(key: String, scala: Scala) extends Resource { + if (key == null) + throw new IllegalArgumentException("Key cannot be null!"); + + setKey(key) + + private val file = new File(key) + + override lazy val getName = file.getName + + override lazy val getLongName = file.getLongName + + override lazy val getDescription = file.getDescription + + override lazy val getLanguage = scala + + override lazy val getScope = file.getScope + + override lazy val getQualifier = file.getQualifier + + override lazy val getParent = { + val dir = new SingleDirectory(file.getParent.getKey, scala) + + if (Directory.ROOT == dir.getKey()) + null + else + dir + } + + override def matchFilePattern(antPattern: String) = file.matchFilePattern(antPattern) + + override lazy val toString = file.toString +} diff --git a/plugin/src/main/java/com/buransky/plugins/scoverage/resource/SingleDirectory.java b/plugin/src/main/scala/com/buransky/plugins/scoverage/resource/SingleDirectory.scala similarity index 51% rename from plugin/src/main/java/com/buransky/plugins/scoverage/resource/SingleDirectory.java rename to plugin/src/main/scala/com/buransky/plugins/scoverage/resource/SingleDirectory.scala index 1d7c08c..5be5c84 100644 --- a/plugin/src/main/java/com/buransky/plugins/scoverage/resource/SingleDirectory.java +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/resource/SingleDirectory.scala @@ -17,12 +17,10 @@ * License along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 */ -package com.buransky.plugins.scoverage.resource; +package com.buransky.plugins.scoverage.resource -import com.buransky.plugins.scoverage.language.Scala; -import org.sonar.api.resources.Directory; -import org.sonar.api.resources.Language; -import org.sonar.api.resources.Resource; +import org.sonar.api.resources.Directory +import com.buransky.plugins.scoverage.language.Scala /** * Single directory in file system. Unlike org.sonar.api.resources.Directory that can represent @@ -30,36 +28,26 @@ * * @author Rado Buransky */ -public class SingleDirectory extends Directory { - private final String name; - private final SingleDirectory parent; +class SingleDirectory(key: String, scala: Scala) extends Directory(key) { + private val name: String = { + val i = key.lastIndexOf(Directory.SEPARATOR) + if (i > 0) + key.substring(i + 1) + else + key + } - public SingleDirectory(String key) { - super(key); + private val parent: Option[SingleDirectory] = { + val i = key.lastIndexOf(Directory.SEPARATOR) + if (i > 0) + Some(new SingleDirectory(key.substring(0, i), scala)) + else + None + } - int i = getKey().lastIndexOf(SEPARATOR); - if (i > 0) { - parent = new SingleDirectory(key.substring(0, i)); - name = key.substring(i + 1); - } - else { - name = key; - parent = null; - } - } + override lazy val getName = name - @Override - public String getName() { - return name; - } + override lazy val getLanguage = scala - @Override - public Language getLanguage() { - return Scala.INSTANCE; - } - - @Override - public Resource getParent() { - return parent; - } -} + override lazy val getParent = parent.getOrElse(null) +} \ No newline at end of file diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala new file mode 100644 index 0000000..3f9be02 --- /dev/null +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala @@ -0,0 +1,210 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scoverage.sensor + +import org.sonar.api.batch.{SensorContext, CoverageExtension, Sensor} +import org.sonar.api.measures.{CoverageMeasuresBuilder, CoreMetrics, Measure} +import com.buransky.plugins.scoverage.measure.ScalaMetrics +import com.buransky.plugins.scoverage._ +import com.buransky.plugins.scoverage.resource.{SingleDirectory, ScalaFile} +import scala.collection.JavaConversions._ +import org.sonar.api.resources.{Project, Resource} +import com.buransky.plugins.scoverage.util.LogUtil +import com.buransky.plugins.scoverage.CoveredStatement +import com.buransky.plugins.scoverage.FileStatementCoverage +import com.buransky.plugins.scoverage.DirectoryStatementCoverage +import com.buransky.plugins.scoverage.language.Scala +import org.sonar.api.scan.filesystem.{PathResolver, ModuleFileSystem} +import org.sonar.api.config.Settings +import org.slf4j.LoggerFactory +import com.buransky.plugins.scoverage.xml.XmlScoverageReportParser + +/** + * Main sensor for importing Scoverage report to Sonar. + * + * @author Rado Buransky + */ +class ScoverageSensor(settings: Settings, pathResolver: PathResolver, moduleFileSystem: ModuleFileSystem, scala: Scala) + extends Sensor with CoverageExtension { + private val log = LoggerFactory.getLogger(classOf[ScoverageSensor]) + private val SCOVERAGE_REPORT_PATH_PROPERTY = "sonar.scoverage.reportPath" + protected lazy val scoverageReportParser = XmlScoverageReportParser() + + override def shouldExecuteOnProject(project: Project): Boolean = + project.getAnalysisType.isDynamic(true) && (scala.getKey == project.getLanguageKey) + + override def analyse(project: Project, context: SensorContext) { + scoverageReportPath match { + case Some(reportPath) => + // Single-module project + processProject(scoverageReportParser.parse(reportPath), project, context) + + case None => + // Multi-module project has report path set for each module individually + analyseMultiModuleProject(project, context) + } + } + + override val toString = getClass.getSimpleName + + private lazy val scoverageReportPath: Option[String] = { + settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY) match { + case null => None + case path: String => + pathResolver.relativeFile(moduleFileSystem.baseDir, path) match { + case report: java.io.File if !report.exists || !report.isFile => + log.error(LogUtil.f("Report not found at {}"), report) + None + + case report: java.io.File => Some(report.getAbsolutePath) + } + } + } + + private def analyseMultiModuleProject(project: Project, context: SensorContext) { + project.isModule match { + case true => log.warn(LogUtil.f("Report path not set for " + project.name + " module! [" + + project.name + "." + SCOVERAGE_REPORT_PATH_PROPERTY + "]")) + case _ => + // Compute overall statement coverage from submodules + val totalStatementCount = project.getModules.map(analyseStatementCountForModule(_, context)).sum + val coveredStatementCount = project.getModules.map(analyseCoveredStatementCountForModule(_, context)).sum + + if (totalStatementCount > 0) { + // Convert to percentage + val overall = (coveredStatementCount.toDouble / totalStatementCount.toDouble) * 100.0 + + // Set overall statement coverage + context.saveMeasure(project, createStatementCoverage(overall)) + + log.info(LogUtil.f("Overall statement coverage is " + ("%1.2f" format overall))) + } + } + } + + private def analyseCoveredStatementCountForModule(module: Project, context: SensorContext): Long = { + // Aggregate modules + context.getMeasure(module, ScalaMetrics.coveredStatements) match { + case null => + log.debug(LogUtil.f("Module has no statement coverage. [" + module.name + "]")) + 0 + case moduleCoveredStatementCount: Measure => + log.debug(LogUtil.f("Covered statement count for " + module.name + " module. [" + + moduleCoveredStatementCount.getValue + "]")) + + moduleCoveredStatementCount.getValue.toLong + } + } + + private def analyseStatementCountForModule(module: Project, context: SensorContext): Long = { + // Aggregate modules + context.getMeasure(module, CoreMetrics.STATEMENTS) match { + case null => + log.debug(LogUtil.f("Module has no number of statements. [" + module.name + "]")) + 0 + + case moduleStatementCount: Measure => + log.debug(LogUtil.f("Statement count for " + module.name + " module. [" + + moduleStatementCount.getValue + "]")) + + moduleStatementCount.getValue.toLong + } + } + + private def processProject(projectCoverage: ProjectStatementCoverage, project: Project, context: SensorContext) { + // Save measures + saveMeasures(context, project, projectCoverage) + + log.info(LogUtil.f("Statement coverage for " + project.getKey + " is " + ("%1.2f" format projectCoverage.rate))) + + // Process children + processChildren(projectCoverage.children, context, "") + } + + private def processDirectory(directoryCoverage: DirectoryStatementCoverage, context: SensorContext, + parentDirectory: String) { + val currentDirectory = appendFilePath(parentDirectory, directoryCoverage.name) + + // Save measures + saveMeasures(context, new SingleDirectory(currentDirectory, scala), directoryCoverage) + + // Process children + processChildren(directoryCoverage.children, context, currentDirectory) + } + + private def processFile(fileCoverage: FileStatementCoverage, context: SensorContext, directory: String) { + val scalaSourceFile = new ScalaFile(appendFilePath(directory, fileCoverage.name), scala) + + // Save measures + saveMeasures(context, scalaSourceFile, fileCoverage) + + // Save line coverage. This is needed just for source code highlighting. + saveLineCoverage(fileCoverage.statements, scalaSourceFile, context) + } + + private def saveMeasures(context: SensorContext, resource: Resource, statementCoverage: StatementCoverage) { + context.saveMeasure(resource, createStatementCoverage(statementCoverage.rate)) + context.saveMeasure(resource, createStatementCount(statementCoverage.statementCount)) + context.saveMeasure(resource, createCoveredStatementCount(statementCoverage.coveredStatementsCount)) + + log.debug(LogUtil.f("Save measures [" + statementCoverage.rate + ", " + statementCoverage.statementCount + + ", " + statementCoverage.coveredStatementsCount + ", " + resource.getKey + "]")) + } + + private def saveLineCoverage(coveredStatements: Iterable[CoveredStatement], scalaSourceFile: ScalaFile, + context: SensorContext) { + // Convert statements to lines + val coveredLines = StatementCoverage.statementCoverageToLineCoverage(coveredStatements) + + // Set line hits + val coverage = CoverageMeasuresBuilder.create() + coveredLines.foreach { coveredLine => + coverage.setHits(coveredLine.line, coveredLine.hitCount) + } + + // Save measures + coverage.createMeasures().toList.foreach(context.saveMeasure(scalaSourceFile, _)) + } + + private def processChildren(children: Iterable[StatementCoverage], context: SensorContext, directory: String) { + children.foreach(processChild(_, context, directory)) + } + + private def processChild(dirOrFile: StatementCoverage, context: SensorContext, directory: String) { + dirOrFile match { + case dir: DirectoryStatementCoverage => processDirectory(dir, context, directory) + case file: FileStatementCoverage => processFile(file, context, directory) + case _ => throw new IllegalStateException("Not a file or directory coverage! [" + + dirOrFile.getClass.getName + "]") + } + } + + private def createStatementCoverage(rate: Double): Measure = new Measure(ScalaMetrics.statementCoverage, rate) + + private def createStatementCount(statements: Int): Measure = new Measure(CoreMetrics.STATEMENTS, statements) + + private def createCoveredStatementCount(coveredStatements: Int): Measure = + new Measure(ScalaMetrics.coveredStatements, coveredStatements); + + private def appendFilePath(src: String, name: String) = { + val result = if (!src.isEmpty) src + java.io.File.separator else "" + result + name + } +} diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.scala new file mode 100644 index 0000000..fdbf152 --- /dev/null +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.scala @@ -0,0 +1,68 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scoverage.sensor + +import org.sonar.api.batch.{SensorContext, Phase, Sensor} +import org.sonar.api.batch.Phase.Name +import org.slf4j.LoggerFactory +import org.sonar.api.scan.filesystem.{FileType, FileQuery, ModuleFileSystem} +import org.sonar.api.resources.{File, Project} +import com.buransky.plugins.scoverage.language.Scala +import scala.collection.JavaConversions._ +import java.io.IOException +import com.buransky.plugins.scoverage.resource.ScalaFile +import org.apache.commons.io.FileUtils + +/** + * Imports Scala source code files to Sonar. + * + * @author Rado Buransky + */ +@Phase(name = Name.PRE) +class ScoverageSourceImporterSensor(moduleFileSystem: ModuleFileSystem, scala: Scala) extends Sensor { + private val log = LoggerFactory.getLogger(classOf[ScoverageSourceImporterSensor]) + + override def shouldExecuteOnProject(project: Project) = + (project.getLanguage != null) && (project.getLanguage.getKey == scala.getKey) + + override def analyse(project: Project, sensorContext: SensorContext) = { + val charset = moduleFileSystem.sourceCharset().toString() + val query = FileQuery.on(FileType.SOURCE).onLanguage(scala.getKey) + moduleFileSystem.files(query).toList.foreach { sourceFile => + addFileToSonar(project, sensorContext, sourceFile, charset) + } + } + + override val toString = "Scoverage source importer" + + private def addFileToSonar(project: Project, sensorContext: SensorContext, sourceFile: java.io.File, + charset: String) = { + try { + val source = FileUtils.readFileToString(sourceFile, charset) + val key = File.fromIOFile(sourceFile, project).getKey() + val resource = new ScalaFile(key, scala) + + sensorContext.index(resource) + sensorContext.saveSource(resource, source) + } catch { + case ioe: IOException => log.error("Could not read the file: " + sourceFile.getAbsolutePath, ioe) + } + } +} diff --git a/plugin/src/main/java/com/buransky/plugins/scoverage/util/LogUtil.java b/plugin/src/main/scala/com/buransky/plugins/scoverage/util/LogUtil.scala similarity index 83% rename from plugin/src/main/java/com/buransky/plugins/scoverage/util/LogUtil.java rename to plugin/src/main/scala/com/buransky/plugins/scoverage/util/LogUtil.scala index 5c7454f..bc28dd1 100644 --- a/plugin/src/main/java/com/buransky/plugins/scoverage/util/LogUtil.java +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/util/LogUtil.scala @@ -17,10 +17,13 @@ * License along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 */ -package com.buransky.plugins.scoverage.util; +package com.buransky.plugins.scoverage.util -public class LogUtil { - public static String f(String msg) { - return "[scoverage] " + msg; - } +/** + * Logging helper. + * + * @author Rado Buransky + */ +object LogUtil { + def f(msg: String) = "[scoverage] " + msg } diff --git a/plugin/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java b/plugin/src/main/scala/com/buransky/plugins/scoverage/widget/ScoverageWidget.scala similarity index 66% rename from plugin/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java rename to plugin/src/main/scala/com/buransky/plugins/scoverage/widget/ScoverageWidget.scala index ea3fe9f..ba0a06e 100644 --- a/plugin/src/main/java/com/buransky/plugins/scoverage/widget/ScoverageWidget.java +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/widget/ScoverageWidget.scala @@ -17,28 +17,17 @@ * License along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 */ -package com.buransky.plugins.scoverage.widget; +package com.buransky.plugins.scoverage.widget -import org.sonar.api.web.AbstractRubyTemplate; -import org.sonar.api.web.RubyRailsWidget; +import org.sonar.api.web.{RubyRailsWidget, AbstractRubyTemplate} /** * UI widget that can be added to the main dashboard to display overall statement coverage for the project. * * @author Rado Buransky */ -public class ScoverageWidget extends AbstractRubyTemplate implements RubyRailsWidget { - - public String getId() { - return "scoverage"; - } - - public String getTitle() { - return "Statement coverage"; - } - - @Override - protected String getTemplatePath() { - return "/com/buransky/plugins/scoverage/widget.html.erb"; - } +class ScoverageWidget extends AbstractRubyTemplate with RubyRailsWidget { + val getId = "scoverage" + val getTitle = "Statement coverage" + override val getTemplatePath = "/com/buransky/plugins/scoverage/widget.html.erb" } diff --git a/samples/sbt/multi-module/sonar-project.properties b/samples/sbt/multi-module/sonar-project.properties index 19829f7..cc12a04 100644 --- a/samples/sbt/multi-module/sonar-project.properties +++ b/samples/sbt/multi-module/sonar-project.properties @@ -1,4 +1,4 @@ -sonar.projectKey=com.buranskt:multi-module +sonar.projectKey=com.buransky:multi-module sonar.projectName=Sonar Scoverage plugin multi-module sample project sonar.projectVersion=1.0.0 @@ -12,4 +12,4 @@ module1.sonar.scoverage.reportPath=target/scala-2.10/scoverage-report/scoverage. module2.sonar.sources=src/main/scala module2.sonar.tests=src/test/scala -module2.sonar.scoverage.reportPath=target/scala-2.10/scoverage-report/scoverage.xml \ No newline at end of file +module2.sonar.scoverage.reportPath=target/scala-2.10/scoverage-report/scoverage.xml From a90d9dedfdff479e71eb5e87169d057dbbeee11b Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Tue, 11 Feb 2014 18:48:21 -0800 Subject: [PATCH 044/101] Testing --- plugin/pom.xml | 7 +- .../plugins/scoverage/StatementCoverage.scala | 6 +- .../scoverage/resource/SingleDirectory.scala | 10 +- .../scoverage/sensor/ScoverageSensor.scala | 4 +- .../ScoverageSourceImporterSensor.scala | 2 +- .../resource/SingleDirectorySpec.scala | 63 ++++++++++++ .../sensor/ScoverageSensorSpec.scala | 98 +++++++++++++++++++ .../ScoverageSourceImporterSensorSpec.scala | 64 ++++++++++++ .../scoverage/sensor/TestSensorContext.scala | 64 ++++++++++++ 9 files changed, 305 insertions(+), 13 deletions(-) create mode 100644 plugin/src/test/scala/com/buransky/plugins/scoverage/resource/SingleDirectorySpec.scala create mode 100644 plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala create mode 100644 plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensorSpec.scala create mode 100644 plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala diff --git a/plugin/pom.xml b/plugin/pom.xml index e783a78..aa8bdd9 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -107,6 +107,12 @@ 4.11 test + + org.mockito + mockito-all + 1.9.5 + test + @@ -152,7 +158,6 @@ **/*Spec.class - **/*Test.class diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala index 93e2b1a..e8643b2 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala @@ -29,7 +29,11 @@ sealed trait StatementCoverage { /** * Percentage rate ranging from 0 up to 100%. */ - lazy val rate: Double = (coveredStatementsCount.toDouble / statementCount.toDouble) * 100.0 + lazy val rate: Double = + if (statementCount == 0) + 0.0 + else + (coveredStatementsCount.toDouble / statementCount.toDouble) * 100.0 /** * Total number of all statements within the source code unit, diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/resource/SingleDirectory.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/resource/SingleDirectory.scala index 5be5c84..1a140e0 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/resource/SingleDirectory.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/resource/SingleDirectory.scala @@ -31,18 +31,12 @@ import com.buransky.plugins.scoverage.language.Scala class SingleDirectory(key: String, scala: Scala) extends Directory(key) { private val name: String = { val i = key.lastIndexOf(Directory.SEPARATOR) - if (i > 0) - key.substring(i + 1) - else - key + if (i >= 0) key.substring(i + 1) else key } private val parent: Option[SingleDirectory] = { val i = key.lastIndexOf(Directory.SEPARATOR) - if (i > 0) - Some(new SingleDirectory(key.substring(0, i), scala)) - else - None + if (i > 0) Some(new SingleDirectory(key.substring(0, i), scala)) else None } override lazy val getName = name diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala index 3f9be02..88b25c2 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala @@ -44,8 +44,8 @@ import com.buransky.plugins.scoverage.xml.XmlScoverageReportParser class ScoverageSensor(settings: Settings, pathResolver: PathResolver, moduleFileSystem: ModuleFileSystem, scala: Scala) extends Sensor with CoverageExtension { private val log = LoggerFactory.getLogger(classOf[ScoverageSensor]) - private val SCOVERAGE_REPORT_PATH_PROPERTY = "sonar.scoverage.reportPath" - protected lazy val scoverageReportParser = XmlScoverageReportParser() + protected val SCOVERAGE_REPORT_PATH_PROPERTY = "sonar.scoverage.reportPath" + protected lazy val scoverageReportParser: ScoverageReportParser = XmlScoverageReportParser() override def shouldExecuteOnProject(project: Project): Boolean = project.getAnalysisType.isDynamic(true) && (scala.getKey == project.getLanguageKey) diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.scala index fdbf152..759db21 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.scala @@ -57,7 +57,7 @@ class ScoverageSourceImporterSensor(moduleFileSystem: ModuleFileSystem, scala: S try { val source = FileUtils.readFileToString(sourceFile, charset) val key = File.fromIOFile(sourceFile, project).getKey() - val resource = new ScalaFile(key, scala) + val resource = new ScalaFile(key, scala) sensorContext.index(resource) sensorContext.saveSource(resource, source) diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/resource/SingleDirectorySpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/resource/SingleDirectorySpec.scala new file mode 100644 index 0000000..d08a6eb --- /dev/null +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/resource/SingleDirectorySpec.scala @@ -0,0 +1,63 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scoverage.resource + +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner +import org.scalatest.{Matchers, FlatSpec} +import com.buransky.plugins.scoverage.language.Scala + +@RunWith(classOf[JUnitRunner]) +class SingleDirectorySpec extends FlatSpec with Matchers { + behavior of "SingleDirectory" + + it should "work for simple relative unix name" in { + val dir = new SingleDirectory("a", new Scala) + dir.getName should equal("a") + dir.getParent should equal(null) + } + + it should "work for simple absolute unix name" in { + val dir = new SingleDirectory("/a", new Scala) + dir.getName should equal("a") + dir.getParent should equal(null) + } + + it should "work for complex relative unix name" in { + complexCase("a/b/c/d/eee", List("eee", "d", "c", "b", "a")) + } + + it should "work for complex absolute unix name" in { + complexCase("/a/b/c/d/eee", List("eee", "d", "c", "b", "a")) + } + + private def complexCase(path: String, dirs: List[String]) { + def check(dir: SingleDirectory, dirs: List[String]) { + dir.getName should equal(dirs.head) + + dir.getParent match { + case p: SingleDirectory => check(p, dirs.tail) + case _ => dir.getParent should equal(null) + } + } + + check(new SingleDirectory(path, new Scala), dirs) + } +} diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala new file mode 100644 index 0000000..1127d23 --- /dev/null +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala @@ -0,0 +1,98 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scoverage.sensor + +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner +import org.scalatest.{Matchers, FlatSpec} +import org.scalatest.mock.MockitoSugar +import org.sonar.api.resources.Project +import org.mockito.Mockito._ +import com.buransky.plugins.scoverage.language.Scala +import org.sonar.api.scan.filesystem.{PathResolver, ModuleFileSystem} +import org.sonar.api.config.Settings +import org.sonar.api.resources.Project.AnalysisType +import org.sonar.api.batch.SensorContext +import com.buransky.plugins.scoverage.{ProjectStatementCoverage, ScoverageReportParser} + +@RunWith(classOf[JUnitRunner]) +class ScoverageSensorSpec extends FlatSpec with Matchers with MockitoSugar { + behavior of "shouldExecuteOnProject" + + it should "succeed for Scala project" in new ShouldExecuteOnProject { + checkShouldExecuteOnProject("scala", true) + } + + it should "fail for Java project" in new ShouldExecuteOnProject { + checkShouldExecuteOnProject("java", false) + } + + class ShouldExecuteOnProject extends ScoverageSensorScope { + protected def checkShouldExecuteOnProject(language: String, expectedResult: Boolean) { + // Setup + val project = mock[Project] + when(project.getAnalysisType).thenReturn(AnalysisType.DYNAMIC) + when(project.getLanguageKey).thenReturn(language) + + // Execute & asser + shouldExecuteOnProject(project) should equal(expectedResult) + + verify(project, times(1)).getAnalysisType + verify(project, times(1)).getLanguageKey + + } + } + + behavior of "analyse for single project" + + it should "set 0% coverage for a project without children" in new AnalyseScoverageSensorScope { + // Setup + val pathToScoverageReport = "#path-to-scoverage-report#" + val reportAbsolutePath = "#report-absolute-path#" + val projectStatementCoverage = ProjectStatementCoverage("project-name", Nil) + val reportFile = mock[java.io.File] + val moduleBaseDir = mock[java.io.File] + when(reportFile.exists).thenReturn(true) + when(reportFile.isFile).thenReturn(true) + when(reportFile.getAbsolutePath).thenReturn(reportAbsolutePath) + when(settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY)).thenReturn(pathToScoverageReport) + when(moduleFileSystem.baseDir).thenReturn(moduleBaseDir) + when(pathResolver.relativeFile(moduleBaseDir, pathToScoverageReport)).thenReturn(reportFile) + when(scoverageReportParser.parse(reportAbsolutePath)).thenReturn(projectStatementCoverage) + + // Execute + analyse(project, context) + } + + class AnalyseScoverageSensorScope extends ScoverageSensorScope { + val project = mock[Project] + val context = new TestSensorContext + + override protected lazy val scoverageReportParser = mock[ScoverageReportParser] + } + + class ScoverageSensorScope extends { + val scala = new Scala + val settings = mock[Settings] + val pathResolver = mock[PathResolver] + val moduleFileSystem = mock[ModuleFileSystem] + } with ScoverageSensor(settings, pathResolver, moduleFileSystem, scala) + +} diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensorSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensorSpec.scala new file mode 100644 index 0000000..fa442a3 --- /dev/null +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensorSpec.scala @@ -0,0 +1,64 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scoverage.sensor + +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner +import org.scalatest.{Matchers, FlatSpec} +import org.sonar.api.scan.filesystem.ModuleFileSystem +import org.scalatest.mock.MockitoSugar +import com.buransky.plugins.scoverage.language.Scala +import org.sonar.api.resources.{AbstractLanguage, Project} +import org.mockito.Mockito._ + +@RunWith(classOf[JUnitRunner]) +class ScoverageSourceImporterSensorSpec extends FlatSpec with Matchers with MockitoSugar { + behavior of "shouldExecuteOnProject" + + it should "succeed for Scala project" in new ScoverageSourceImporterSensorScope { + // Setup + val project = mock[Project] + when(project.getLanguage).thenReturn(scala) + + // Execute & asser + shouldExecuteOnProject(project) should equal(true) + + verify(project, times(2)).getLanguage + } + + it should "fail for Java project" in new ScoverageSourceImporterSensorScope { + // Setup + val java = new AbstractLanguage("java", "Java") { + val getFileSuffixes = Array("java") + } + val project = mock[Project] + when(project.getLanguage).thenReturn(java) + + // Execute & asser + shouldExecuteOnProject(project) should equal(false) + + verify(project, times(2)).getLanguage + } + + class ScoverageSourceImporterSensorScope extends { + val scala = new Scala + val moduleFileSystem = mock[ModuleFileSystem] + } with ScoverageSourceImporterSensor(moduleFileSystem, scala) +} diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala new file mode 100644 index 0000000..14a44a3 --- /dev/null +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala @@ -0,0 +1,64 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scoverage.sensor + +import org.sonar.api.batch.SensorContext +import org.sonar.api.resources.Resource +import org.sonar.api.measures.{Measure, Metric} +import scala.collection.mutable + +class TestSensorContext extends SensorContext { + private val measures = mutable.Map[String, Measure]() + + def createEvent(x$1: org.sonar.api.resources.Resource,x$2: String,x$3: String,x$4: String,x$5: java.util.Date): org.sonar.api.batch.Event = ??? + def deleteEvent(x$1: org.sonar.api.batch.Event): Unit = ??? + def deleteLink(x$1: String): Unit = ??? + def getChildren(x$1: org.sonar.api.resources.Resource): java.util.Collection[org.sonar.api.resources.Resource] = ??? + def getDependencies(): java.util.Set[org.sonar.api.design.Dependency] = ??? + def getEvents(x$1: org.sonar.api.resources.Resource): java.util.List[org.sonar.api.batch.Event] = ??? + def getIncomingDependencies(x$1: org.sonar.api.resources.Resource): java.util.Collection[org.sonar.api.design.Dependency] = ??? + def getMeasure(x$1: org.sonar.api.resources.Resource,x$2: org.sonar.api.measures.Metric): org.sonar.api.measures.Measure = ??? + def getMeasure(x$1: org.sonar.api.measures.Metric): org.sonar.api.measures.Measure = ??? + def getMeasures[M](x$1: org.sonar.api.resources.Resource,x$2: org.sonar.api.measures.MeasuresFilter[M]): M = ??? + def getMeasures[M](x$1: org.sonar.api.measures.MeasuresFilter[M]): M = ??? + def getOutgoingDependencies(x$1: org.sonar.api.resources.Resource): java.util.Collection[org.sonar.api.design.Dependency] = ??? + def getParent(x$1: org.sonar.api.resources.Resource): org.sonar.api.resources.Resource = ??? + def getResource[R <: org.sonar.api.resources.Resource](x$1: R): R = ??? + def index(x$1: org.sonar.api.resources.Resource,x$2: org.sonar.api.resources.Resource): Boolean = ??? + def index(x$1: org.sonar.api.resources.Resource): Boolean = ??? + def isExcluded(x$1: org.sonar.api.resources.Resource): Boolean = ??? + def isIndexed(x$1: org.sonar.api.resources.Resource,x$2: Boolean): Boolean = ??? + def saveDependency(x$1: org.sonar.api.design.Dependency): org.sonar.api.design.Dependency = ??? + def saveLink(x$1: org.sonar.api.resources.ProjectLink): Unit = ??? + + def saveMeasure(resource: Resource, measure: Measure): Measure = { + measures.put(resource.getKey, measure) + measure + } + + def saveMeasure(x$1: Resource,x$2: Metric,x$3: java.lang.Double): Measure = ??? + def saveMeasure(x$1: org.sonar.api.measures.Metric,x$2: java.lang.Double): org.sonar.api.measures.Measure = ??? + def saveMeasure(x$1: org.sonar.api.measures.Measure): org.sonar.api.measures.Measure = ??? + def saveResource(x$1: org.sonar.api.resources.Resource): String = ??? + def saveSource(x$1: org.sonar.api.resources.Resource,x$2: String): Unit = ??? + def saveViolation(x$1: org.sonar.api.rules.Violation): Unit = ??? + def saveViolation(x$1: org.sonar.api.rules.Violation,x$2: Boolean): Unit = ??? + def saveViolations(x$1: java.util.Collection[org.sonar.api.rules.Violation]): Unit = ??? +} From a40f9f0733f6a4d721831e2c192b0d47fa084b2c Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Wed, 19 Mar 2014 10:41:14 -0700 Subject: [PATCH 045/101] Resolve issue #1 --- .../xml/XmlScoverageReportConstructingParser.scala | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala index 2b30b04..4a8f410 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala @@ -191,10 +191,16 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP // Get file val file = DirOrFile(path(path.length - 1).toString, Nil, Some(coverage)) - // Append file - dirs.last.children = List(file) + if (dirs.isEmpty) { + // File in root dir + file + } + else { + // Append file + dirs.last.children = List(file) - dirs(0) + dirs(0) + } } private def fileStatementCoverage(statementsInFile: Map[String, List[CoveredStatement]]): From 10396ee356204b17f2071c285f5c73d505d23e1a Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Wed, 19 Mar 2014 13:10:23 -0700 Subject: [PATCH 046/101] Release 1.0.1 --- plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index aa8bdd9..319e452 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -11,7 +11,7 @@ sonar-scoverage-plugin - 1.0.1-SNAPSHOT + 1.0.1 sonar-plugin Sonar Scoverage Plugin From 61015d1c6e60e6aa82e72cd5942bbd02f1d99570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=20Buransk=C3=BD?= Date: Wed, 19 Mar 2014 13:13:39 -0700 Subject: [PATCH 047/101] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2eacd56..d6e44d8 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ If you have Sonar 3.5.1, take a look into the [dedicated branch] [Plugin351] or ## Installation ## -Download and copy [sonar-scoverage-plugin-1.0.0.jar] [PluginJar] to the Sonar plugins directory +Download and copy [sonar-scoverage-plugin-1.0.1.jar] [PluginJar] to the Sonar plugins directory (usually /extensions/plugins). Restart Sonar. ## Configure Sonar runner ## @@ -86,7 +86,7 @@ Columns with statement coverage, total number of statements and number of covere Source code markup with covered and uncovered lines: ![Source code markup](/doc/img/04_coverage.png "Source code markup") -[PluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v1.0.0/sonar-scoverage-plugin-1.0.0.jar +[PluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/1.0.1/sonar-scoverage-plugin-1.0.1.jar [SonarQube]: http://www.sonarqube.org/ "SonarQube" [Scoverage]: https://github.com/scoverage/scalac-scoverage-plugin "Scoverage" [sbt-scoverage]: https://github.com/scoverage/sbt-scoverage From ec180c8a16dd3517aaac9e61ffe5859da5218979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=20Buransk=C3=BD?= Date: Mon, 31 Mar 2014 15:24:58 -0700 Subject: [PATCH 048/101] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6e44d8..1e079cf 100644 --- a/README.md +++ b/README.md @@ -91,4 +91,4 @@ Source code markup with covered and uncovered lines: [Scoverage]: https://github.com/scoverage/scalac-scoverage-plugin "Scoverage" [sbt-scoverage]: https://github.com/scoverage/sbt-scoverage [Plugin351]: https://github.com/RadoBuransky/sonar-scoverage-plugin/tree/sonar3.5.1 -[Plugin351Jar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v1.0.1-Sonar3.5.1/sonar-scoverage-plugin-sonar3.5.1-1.0.1.jar +[Plugin351Jar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v1.0.2-Sonar3.5.1/sonar-scoverage-plugin-sonar3.5.1-1.0.2.jar From cde97fd4de56b7e4018c370ed944e6abea167b2f Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Sat, 26 Apr 2014 11:02:53 -0700 Subject: [PATCH 049/101] Issue #2 fixed --- plugin/pom.xml | 2 +- .../plugins/scoverage/util/PathUtil.scala | 31 +++++++++++++ ...XmlScoverageReportConstructingParser.scala | 8 ++-- .../plugins/scoverage/util/PathUtilSpec.scala | 46 +++++++++++++++++++ .../sbt/multiModule/module1/Beer.scala | 2 + 5 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala create mode 100644 plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala diff --git a/plugin/pom.xml b/plugin/pom.xml index 319e452..2aa1f68 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -11,7 +11,7 @@ sonar-scoverage-plugin - 1.0.1 + 1.0.2 sonar-plugin Sonar Scoverage Plugin diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala new file mode 100644 index 0000000..e4da7e8 --- /dev/null +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala @@ -0,0 +1,31 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scoverage.util + +import java.io.File +/** + * File path helper. + * + * @author Rado Buransky + */ +object PathUtil { + def splitPath(filePath: String, separator: String = File.separator): List[String] = + filePath.split(separator.replaceAllLiterally("\\", "\\\\")).toList +} diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala index 4a8f410..ba92f0e 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala @@ -27,6 +27,7 @@ import org.apache.log4j.Logger import scala.collection.mutable import scala.annotation.tailrec import java.io.File +import com.buransky.plugins.scoverage.util.PathUtil /** * Scoverage XML parser based on ConstructingParser provided by standard Scala library. @@ -174,8 +175,7 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP } private def pathToChain(filePath: String, coverage: FileStatementCoverage): DirOrFile = { - //val path = Paths.get(filePath) - val path = splitPath(filePath) + val path = PathUtil.splitPath(filePath) if (path.length < 1) throw new ScoverageException("Path cannot be empty!") @@ -206,7 +206,7 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP private def fileStatementCoverage(statementsInFile: Map[String, List[CoveredStatement]]): Map[String, FileStatementCoverage] = { statementsInFile.map { sif => - val fileStatementCoverage = FileStatementCoverage(splitPath(sif._1).last, + val fileStatementCoverage = FileStatementCoverage(PathUtil.splitPath(sif._1).last, sif._2.length, coveredStatements(sif._2), sif._2) (sif._1, fileStatementCoverage) @@ -215,6 +215,4 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP private def coveredStatements(statements: Iterable[CoveredStatement]) = statements.count(_.hitCount > 0) - - private def splitPath(filePath: String) = filePath.split(File.separator) } \ No newline at end of file diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala new file mode 100644 index 0000000..28efa25 --- /dev/null +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala @@ -0,0 +1,46 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scoverage.util + +import org.scalatest.{FlatSpec, Matchers} +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class UnixPathUtilSpec extends ParamPathUtilSpec("Unix", "/") + +@RunWith(classOf[JUnitRunner]) +class WindowsPathUtilSpec extends ParamPathUtilSpec("Windows", "\\") + +abstract class ParamPathUtilSpec(osName: String, separator: String) extends FlatSpec with Matchers { + behavior of s"splitPath for ${osName}" + + it should "work for empty path" in { + PathUtil.splitPath("", separator) should equal(List("")) + } + + it should "work with separator at the beginning" in { + PathUtil.splitPath(s"${separator}a", separator) should equal(List("", "a")) + } + + it should "work with separator in the middle" in { + PathUtil.splitPath(s"a${separator}b", separator) should equal(List("a", "b")) + } +} \ No newline at end of file diff --git a/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Beer.scala b/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Beer.scala index d3f76a2..0608bf1 100644 --- a/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Beer.scala +++ b/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Beer.scala @@ -20,6 +20,8 @@ trait BelgianBeer extends Beer { throw new IllegalArgumentException("Too big beer for belgian beer!") override def isGood = true + + def f(l: List[Int]) = l.filter(_ > 0).filter(_ < 42).takeWhile(_ != 3).foreach(println(_)) } case class HordonBeer(volume: Double) extends SlovakBeer { From 22ad638495ef81e55097f8714e30db09f2c8e291 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Sat, 26 Apr 2014 11:08:30 -0700 Subject: [PATCH 050/101] Remove unwanted commit --- .../scoverage/samples/sbt/multiModule/module1/Beer.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Beer.scala b/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Beer.scala index 0608bf1..d3f76a2 100644 --- a/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Beer.scala +++ b/samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Beer.scala @@ -20,8 +20,6 @@ trait BelgianBeer extends Beer { throw new IllegalArgumentException("Too big beer for belgian beer!") override def isGood = true - - def f(l: List[Int]) = l.filter(_ > 0).filter(_ < 42).takeWhile(_ != 3).foreach(println(_)) } case class HordonBeer(volume: Double) extends SlovakBeer { From 4527070deed65e545d2b61b87052b7cfa1b1036b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=20Buransk=C3=BD?= Date: Sat, 26 Apr 2014 11:18:26 -0700 Subject: [PATCH 051/101] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e079cf..aa63ba9 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Columns with statement coverage, total number of statements and number of covere Source code markup with covered and uncovered lines: ![Source code markup](/doc/img/04_coverage.png "Source code markup") -[PluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/1.0.1/sonar-scoverage-plugin-1.0.1.jar +[PluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/1.0.2/sonar-scoverage-plugin-1.0.2.jar [SonarQube]: http://www.sonarqube.org/ "SonarQube" [Scoverage]: https://github.com/scoverage/scalac-scoverage-plugin "Scoverage" [sbt-scoverage]: https://github.com/scoverage/sbt-scoverage From bd18186713284145c0848ddcd8de8ea39bddcc82 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Tue, 23 Sep 2014 22:03:39 +0200 Subject: [PATCH 052/101] Upgrade to Sonar API 4.2, release 1.1.0 --- plugin/pom.xml | 4 +- .../plugins/scoverage/ScoveragePlugin.scala | 10 +-- .../scoverage/resource/ScalaFile.scala | 62 --------------- .../scoverage/resource/SingleDirectory.scala | 47 ------------ .../scoverage/sensor/ScoverageSensor.scala | 64 +++++++++------- .../ScoverageSourceImporterSensor.scala | 68 ----------------- .../resource/SingleDirectorySpec.scala | 63 ---------------- .../sensor/ScoverageSensorSpec.scala | 75 +++++++++++-------- .../ScoverageSourceImporterSensorSpec.scala | 64 ---------------- .../scoverage/sensor/TestSensorContext.scala | 5 ++ 10 files changed, 89 insertions(+), 373 deletions(-) delete mode 100644 plugin/src/main/scala/com/buransky/plugins/scoverage/resource/ScalaFile.scala delete mode 100644 plugin/src/main/scala/com/buransky/plugins/scoverage/resource/SingleDirectory.scala delete mode 100644 plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.scala delete mode 100644 plugin/src/test/scala/com/buransky/plugins/scoverage/resource/SingleDirectorySpec.scala delete mode 100644 plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensorSpec.scala diff --git a/plugin/pom.xml b/plugin/pom.xml index 2aa1f68..0009f51 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -11,7 +11,7 @@ sonar-scoverage-plugin - 1.0.2 + 1.1.0 sonar-plugin Sonar Scoverage Plugin @@ -69,7 +69,7 @@ - 4.1 + 4.2 scoverage Scoverage diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala index c7c3409..59c1921 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala @@ -19,13 +19,14 @@ */ package com.buransky.plugins.scoverage -import org.sonar.api.{Extension, SonarPlugin} -import com.buransky.plugins.scoverage.measure.ScalaMetrics -import scala.collection.mutable.ListBuffer -import com.buransky.plugins.scoverage.sensor.{ScoverageSensor, ScoverageSourceImporterSensor} import com.buransky.plugins.scoverage.language.Scala +import com.buransky.plugins.scoverage.measure.ScalaMetrics +import com.buransky.plugins.scoverage.sensor.ScoverageSensor import com.buransky.plugins.scoverage.widget.ScoverageWidget +import org.sonar.api.{Extension, SonarPlugin} + import scala.collection.JavaConversions._ +import scala.collection.mutable.ListBuffer /** * Plugin entry point. @@ -36,7 +37,6 @@ class ScoveragePlugin extends SonarPlugin { override def getExtensions: java.util.List[Class[_ <: Extension]] = ListBuffer( classOf[Scala], classOf[ScalaMetrics], - classOf[ScoverageSourceImporterSensor], classOf[ScoverageSensor], classOf[ScoverageWidget] ) diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/resource/ScalaFile.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/resource/ScalaFile.scala deleted file mode 100644 index dda8848..0000000 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/resource/ScalaFile.scala +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Sonar Scoverage Plugin - * Copyright (C) 2013 Rado Buransky - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scoverage.resource - -import org.sonar.api.resources.{Directory, File, Resource} -import com.buransky.plugins.scoverage.language.Scala - -/** - * Scala source code file resource. - * - * @author Rado Buransky - */ -class ScalaFile(key: String, scala: Scala) extends Resource { - if (key == null) - throw new IllegalArgumentException("Key cannot be null!"); - - setKey(key) - - private val file = new File(key) - - override lazy val getName = file.getName - - override lazy val getLongName = file.getLongName - - override lazy val getDescription = file.getDescription - - override lazy val getLanguage = scala - - override lazy val getScope = file.getScope - - override lazy val getQualifier = file.getQualifier - - override lazy val getParent = { - val dir = new SingleDirectory(file.getParent.getKey, scala) - - if (Directory.ROOT == dir.getKey()) - null - else - dir - } - - override def matchFilePattern(antPattern: String) = file.matchFilePattern(antPattern) - - override lazy val toString = file.toString -} diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/resource/SingleDirectory.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/resource/SingleDirectory.scala deleted file mode 100644 index 1a140e0..0000000 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/resource/SingleDirectory.scala +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Sonar Scoverage Plugin - * Copyright (C) 2013 Rado Buransky - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scoverage.resource - -import org.sonar.api.resources.Directory -import com.buransky.plugins.scoverage.language.Scala - -/** - * Single directory in file system. Unlike org.sonar.api.resources.Directory that can represent - * a chain of directories. - * - * @author Rado Buransky - */ -class SingleDirectory(key: String, scala: Scala) extends Directory(key) { - private val name: String = { - val i = key.lastIndexOf(Directory.SEPARATOR) - if (i >= 0) key.substring(i + 1) else key - } - - private val parent: Option[SingleDirectory] = { - val i = key.lastIndexOf(Directory.SEPARATOR) - if (i > 0) Some(new SingleDirectory(key.substring(0, i), scala)) else None - } - - override lazy val getName = name - - override lazy val getLanguage = scala - - override lazy val getParent = parent.getOrElse(null) -} \ No newline at end of file diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala index 88b25c2..26cf8b3 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala @@ -19,36 +19,34 @@ */ package com.buransky.plugins.scoverage.sensor -import org.sonar.api.batch.{SensorContext, CoverageExtension, Sensor} -import org.sonar.api.measures.{CoverageMeasuresBuilder, CoreMetrics, Measure} +import com.buransky.plugins.scoverage.language.Scala import com.buransky.plugins.scoverage.measure.ScalaMetrics -import com.buransky.plugins.scoverage._ -import com.buransky.plugins.scoverage.resource.{SingleDirectory, ScalaFile} -import scala.collection.JavaConversions._ -import org.sonar.api.resources.{Project, Resource} import com.buransky.plugins.scoverage.util.LogUtil -import com.buransky.plugins.scoverage.CoveredStatement -import com.buransky.plugins.scoverage.FileStatementCoverage -import com.buransky.plugins.scoverage.DirectoryStatementCoverage -import com.buransky.plugins.scoverage.language.Scala -import org.sonar.api.scan.filesystem.{PathResolver, ModuleFileSystem} -import org.sonar.api.config.Settings -import org.slf4j.LoggerFactory import com.buransky.plugins.scoverage.xml.XmlScoverageReportParser +import com.buransky.plugins.scoverage.{CoveredStatement, DirectoryStatementCoverage, FileStatementCoverage, _} +import org.slf4j.LoggerFactory +import org.sonar.api.batch.fs.{FileSystem, InputFile} +import org.sonar.api.batch.{CoverageExtension, Sensor, SensorContext} +import org.sonar.api.config.Settings +import org.sonar.api.measures.{CoreMetrics, CoverageMeasuresBuilder, Measure} +import org.sonar.api.resources.{File, Project, Resource} +import org.sonar.api.scan.filesystem.PathResolver + +import scala.collection.JavaConversions._ /** * Main sensor for importing Scoverage report to Sonar. * * @author Rado Buransky */ -class ScoverageSensor(settings: Settings, pathResolver: PathResolver, moduleFileSystem: ModuleFileSystem, scala: Scala) +class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem: FileSystem, scala: Scala) extends Sensor with CoverageExtension { private val log = LoggerFactory.getLogger(classOf[ScoverageSensor]) protected val SCOVERAGE_REPORT_PATH_PROPERTY = "sonar.scoverage.reportPath" protected lazy val scoverageReportParser: ScoverageReportParser = XmlScoverageReportParser() override def shouldExecuteOnProject(project: Project): Boolean = - project.getAnalysisType.isDynamic(true) && (scala.getKey == project.getLanguageKey) + project.getAnalysisType.isDynamic(true) && fileSystem.languages().contains(scala.getKey) override def analyse(project: Project, context: SensorContext) { scoverageReportPath match { @@ -68,7 +66,7 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, moduleFile settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY) match { case null => None case path: String => - pathResolver.relativeFile(moduleFileSystem.baseDir, path) match { + pathResolver.relativeFile(fileSystem.baseDir, path) match { case report: java.io.File if !report.exists || !report.isFile => log.error(LogUtil.f("Report not found at {}"), report) None @@ -140,23 +138,31 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, moduleFile private def processDirectory(directoryCoverage: DirectoryStatementCoverage, context: SensorContext, parentDirectory: String) { - val currentDirectory = appendFilePath(parentDirectory, directoryCoverage.name) - - // Save measures - saveMeasures(context, new SingleDirectory(currentDirectory, scala), directoryCoverage) - // Process children - processChildren(directoryCoverage.children, context, currentDirectory) + processChildren(directoryCoverage.children, context, appendFilePath(parentDirectory, directoryCoverage.name)) } private def processFile(fileCoverage: FileStatementCoverage, context: SensorContext, directory: String) { - val scalaSourceFile = new ScalaFile(appendFilePath(directory, fileCoverage.name), scala) + val relativePath = appendFilePath(directory, fileCoverage.name) - // Save measures - saveMeasures(context, scalaSourceFile, fileCoverage) + val p = fileSystem.predicates() + val files = fileSystem.inputFiles(p.and(p.matchesPathPattern("**/" + relativePath), + p.hasLanguage(scala.getKey), p.hasType(InputFile.Type.MAIN))) - // Save line coverage. This is needed just for source code highlighting. - saveLineCoverage(fileCoverage.statements, scalaSourceFile, context) + files.headOption match { + case Some(file) => { + //val scalaSourceFile = new ScalaFile(file.relativePath(), scala) + val scalaSourceFile = File.create(file.relativePath()) + + // Save measures + saveMeasures(context, scalaSourceFile, fileCoverage) + + // Save line coverage. This is needed just for source code highlighting. + saveLineCoverage(fileCoverage.statements, scalaSourceFile, context) + } + + case None => log.warn("File not found in file system! " + relativePath) + } } private def saveMeasures(context: SensorContext, resource: Resource, statementCoverage: StatementCoverage) { @@ -168,7 +174,7 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, moduleFile ", " + statementCoverage.coveredStatementsCount + ", " + resource.getKey + "]")) } - private def saveLineCoverage(coveredStatements: Iterable[CoveredStatement], scalaSourceFile: ScalaFile, + private def saveLineCoverage(coveredStatements: Iterable[CoveredStatement], resource: Resource, context: SensorContext) { // Convert statements to lines val coveredLines = StatementCoverage.statementCoverageToLineCoverage(coveredStatements) @@ -180,7 +186,7 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, moduleFile } // Save measures - coverage.createMeasures().toList.foreach(context.saveMeasure(scalaSourceFile, _)) + coverage.createMeasures().toList.foreach(context.saveMeasure(resource, _)) } private def processChildren(children: Iterable[StatementCoverage], context: SensorContext, directory: String) { diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.scala deleted file mode 100644 index 759db21..0000000 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensor.scala +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Sonar Scoverage Plugin - * Copyright (C) 2013 Rado Buransky - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scoverage.sensor - -import org.sonar.api.batch.{SensorContext, Phase, Sensor} -import org.sonar.api.batch.Phase.Name -import org.slf4j.LoggerFactory -import org.sonar.api.scan.filesystem.{FileType, FileQuery, ModuleFileSystem} -import org.sonar.api.resources.{File, Project} -import com.buransky.plugins.scoverage.language.Scala -import scala.collection.JavaConversions._ -import java.io.IOException -import com.buransky.plugins.scoverage.resource.ScalaFile -import org.apache.commons.io.FileUtils - -/** - * Imports Scala source code files to Sonar. - * - * @author Rado Buransky - */ -@Phase(name = Name.PRE) -class ScoverageSourceImporterSensor(moduleFileSystem: ModuleFileSystem, scala: Scala) extends Sensor { - private val log = LoggerFactory.getLogger(classOf[ScoverageSourceImporterSensor]) - - override def shouldExecuteOnProject(project: Project) = - (project.getLanguage != null) && (project.getLanguage.getKey == scala.getKey) - - override def analyse(project: Project, sensorContext: SensorContext) = { - val charset = moduleFileSystem.sourceCharset().toString() - val query = FileQuery.on(FileType.SOURCE).onLanguage(scala.getKey) - moduleFileSystem.files(query).toList.foreach { sourceFile => - addFileToSonar(project, sensorContext, sourceFile, charset) - } - } - - override val toString = "Scoverage source importer" - - private def addFileToSonar(project: Project, sensorContext: SensorContext, sourceFile: java.io.File, - charset: String) = { - try { - val source = FileUtils.readFileToString(sourceFile, charset) - val key = File.fromIOFile(sourceFile, project).getKey() - val resource = new ScalaFile(key, scala) - - sensorContext.index(resource) - sensorContext.saveSource(resource, source) - } catch { - case ioe: IOException => log.error("Could not read the file: " + sourceFile.getAbsolutePath, ioe) - } - } -} diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/resource/SingleDirectorySpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/resource/SingleDirectorySpec.scala deleted file mode 100644 index d08a6eb..0000000 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/resource/SingleDirectorySpec.scala +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Sonar Scoverage Plugin - * Copyright (C) 2013 Rado Buransky - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scoverage.resource - -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner -import org.scalatest.{Matchers, FlatSpec} -import com.buransky.plugins.scoverage.language.Scala - -@RunWith(classOf[JUnitRunner]) -class SingleDirectorySpec extends FlatSpec with Matchers { - behavior of "SingleDirectory" - - it should "work for simple relative unix name" in { - val dir = new SingleDirectory("a", new Scala) - dir.getName should equal("a") - dir.getParent should equal(null) - } - - it should "work for simple absolute unix name" in { - val dir = new SingleDirectory("/a", new Scala) - dir.getName should equal("a") - dir.getParent should equal(null) - } - - it should "work for complex relative unix name" in { - complexCase("a/b/c/d/eee", List("eee", "d", "c", "b", "a")) - } - - it should "work for complex absolute unix name" in { - complexCase("/a/b/c/d/eee", List("eee", "d", "c", "b", "a")) - } - - private def complexCase(path: String, dirs: List[String]) { - def check(dir: SingleDirectory, dirs: List[String]) { - dir.getName should equal(dirs.head) - - dir.getParent match { - case p: SingleDirectory => check(p, dirs.tail) - case _ => dir.getParent should equal(null) - } - } - - check(new SingleDirectory(path, new Scala), dirs) - } -} diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala index 1127d23..11ad086 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala @@ -1,61 +1,70 @@ /* - * Sonar Scoverage Plugin - * Copyright (C) 2013 Rado Buransky - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ +* Sonar Scoverage Plugin +* Copyright (C) 2013 Rado Buransky +* dev@sonar.codehaus.org +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 3 of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 +*/ package com.buransky.plugins.scoverage.sensor +import java.util + +import com.buransky.plugins.scoverage.language.Scala +import com.buransky.plugins.scoverage.{ProjectStatementCoverage, ScoverageReportParser} import org.junit.runner.RunWith +import org.mockito.Mockito._ import org.scalatest.junit.JUnitRunner -import org.scalatest.{Matchers, FlatSpec} import org.scalatest.mock.MockitoSugar -import org.sonar.api.resources.Project -import org.mockito.Mockito._ -import com.buransky.plugins.scoverage.language.Scala -import org.sonar.api.scan.filesystem.{PathResolver, ModuleFileSystem} +import org.scalatest.{FlatSpec, Matchers} +import org.sonar.api.batch.fs.FileSystem import org.sonar.api.config.Settings +import org.sonar.api.resources.Project import org.sonar.api.resources.Project.AnalysisType -import org.sonar.api.batch.SensorContext -import com.buransky.plugins.scoverage.{ProjectStatementCoverage, ScoverageReportParser} +import org.sonar.api.scan.filesystem.PathResolver + +import scala.collection.JavaConversions._ + @RunWith(classOf[JUnitRunner]) class ScoverageSensorSpec extends FlatSpec with Matchers with MockitoSugar { behavior of "shouldExecuteOnProject" it should "succeed for Scala project" in new ShouldExecuteOnProject { - checkShouldExecuteOnProject("scala", true) + checkShouldExecuteOnProject(List("scala"), true) + } + + it should "succeed for mixed projects" in new ShouldExecuteOnProject { + checkShouldExecuteOnProject(List("scala", "java"), true) } it should "fail for Java project" in new ShouldExecuteOnProject { - checkShouldExecuteOnProject("java", false) + checkShouldExecuteOnProject(List("java"), false) } class ShouldExecuteOnProject extends ScoverageSensorScope { - protected def checkShouldExecuteOnProject(language: String, expectedResult: Boolean) { + protected def checkShouldExecuteOnProject(languages: Iterable[String], expectedResult: Boolean) { // Setup val project = mock[Project] when(project.getAnalysisType).thenReturn(AnalysisType.DYNAMIC) - when(project.getLanguageKey).thenReturn(language) + when(fileSystem.languages()).thenReturn(new util.TreeSet(languages)) // Execute & asser shouldExecuteOnProject(project) should equal(expectedResult) verify(project, times(1)).getAnalysisType - verify(project, times(1)).getLanguageKey + verify(fileSystem, times(1)).languages } } @@ -73,7 +82,7 @@ class ScoverageSensorSpec extends FlatSpec with Matchers with MockitoSugar { when(reportFile.isFile).thenReturn(true) when(reportFile.getAbsolutePath).thenReturn(reportAbsolutePath) when(settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY)).thenReturn(pathToScoverageReport) - when(moduleFileSystem.baseDir).thenReturn(moduleBaseDir) + when(fileSystem.baseDir).thenReturn(moduleBaseDir) when(pathResolver.relativeFile(moduleBaseDir, pathToScoverageReport)).thenReturn(reportFile) when(scoverageReportParser.parse(reportAbsolutePath)).thenReturn(projectStatementCoverage) @@ -92,7 +101,7 @@ class ScoverageSensorSpec extends FlatSpec with Matchers with MockitoSugar { val scala = new Scala val settings = mock[Settings] val pathResolver = mock[PathResolver] - val moduleFileSystem = mock[ModuleFileSystem] - } with ScoverageSensor(settings, pathResolver, moduleFileSystem, scala) + val fileSystem = mock[FileSystem] + } with ScoverageSensor(settings, pathResolver, fileSystem, scala) } diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensorSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensorSpec.scala deleted file mode 100644 index fa442a3..0000000 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSourceImporterSensorSpec.scala +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Sonar Scoverage Plugin - * Copyright (C) 2013 Rado Buransky - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scoverage.sensor - -import org.junit.runner.RunWith -import org.scalatest.junit.JUnitRunner -import org.scalatest.{Matchers, FlatSpec} -import org.sonar.api.scan.filesystem.ModuleFileSystem -import org.scalatest.mock.MockitoSugar -import com.buransky.plugins.scoverage.language.Scala -import org.sonar.api.resources.{AbstractLanguage, Project} -import org.mockito.Mockito._ - -@RunWith(classOf[JUnitRunner]) -class ScoverageSourceImporterSensorSpec extends FlatSpec with Matchers with MockitoSugar { - behavior of "shouldExecuteOnProject" - - it should "succeed for Scala project" in new ScoverageSourceImporterSensorScope { - // Setup - val project = mock[Project] - when(project.getLanguage).thenReturn(scala) - - // Execute & asser - shouldExecuteOnProject(project) should equal(true) - - verify(project, times(2)).getLanguage - } - - it should "fail for Java project" in new ScoverageSourceImporterSensorScope { - // Setup - val java = new AbstractLanguage("java", "Java") { - val getFileSuffixes = Array("java") - } - val project = mock[Project] - when(project.getLanguage).thenReturn(java) - - // Execute & asser - shouldExecuteOnProject(project) should equal(false) - - verify(project, times(2)).getLanguage - } - - class ScoverageSourceImporterSensorScope extends { - val scala = new Scala - val moduleFileSystem = mock[ModuleFileSystem] - } with ScoverageSourceImporterSensor(moduleFileSystem, scala) -} diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala index 14a44a3..0adf198 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala @@ -19,7 +19,10 @@ */ package com.buransky.plugins.scoverage.sensor +import java.lang + import org.sonar.api.batch.SensorContext +import org.sonar.api.batch.fs.InputFile import org.sonar.api.resources.Resource import org.sonar.api.measures.{Measure, Metric} import scala.collection.mutable @@ -61,4 +64,6 @@ class TestSensorContext extends SensorContext { def saveViolation(x$1: org.sonar.api.rules.Violation): Unit = ??? def saveViolation(x$1: org.sonar.api.rules.Violation,x$2: Boolean): Unit = ??? def saveViolations(x$1: java.util.Collection[org.sonar.api.rules.Violation]): Unit = ??? + override def saveMeasure(p1: InputFile, p2: Metric, p3: lang.Double): Measure = ??? + override def saveMeasure(p1: InputFile, p2: Measure): Measure = ??? } From de2468804f01facdc5a822607d06b1ee9a75f711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=20Buransk=C3=BD?= Date: Tue, 23 Sep 2014 22:12:24 +0200 Subject: [PATCH 053/101] Update README.md --- README.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index aa63ba9..7510fc1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -#Sonar Scoverage Plugin# +#Sonar Scoverage Plugin 1.1.0# [![Build Status](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin.png)](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin) @@ -22,17 +22,17 @@ just plain average of coverage rates for sub-projects. ## Requirements ## -- [SonarQube] 4.0 +- [SonarQube] 4.2 - [Scoverage] 0.95.7 -### Support for older version of Sonar 3.5.1 ### +### Support for older versions of Sonar ### -If you have Sonar 3.5.1, take a look into the [dedicated branch] [Plugin351] or directly -[download binary JAR] [Plugin351Jar]. +- SonarQube 4.0: Install version 1.0.2 sonar-scoverage-plugin-1.0.2.jar] [Plugin102Jar]. +- SonarQube 3.5.1: Take a look into the [dedicated branch] [Plugin351] or directly [download binary JAR] [Plugin351Jar]. ## Installation ## -Download and copy [sonar-scoverage-plugin-1.0.1.jar] [PluginJar] to the Sonar plugins directory +Download and copy [sonar-scoverage-plugin-1.1.0.jar] [LatestPluginJar] to the Sonar plugins directory (usually /extensions/plugins). Restart Sonar. ## Configure Sonar runner ## @@ -86,7 +86,14 @@ Columns with statement coverage, total number of statements and number of covere Source code markup with covered and uncovered lines: ![Source code markup](/doc/img/04_coverage.png "Source code markup") -[PluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/1.0.2/sonar-scoverage-plugin-1.0.2.jar +## Changelog ## + +### 1.1.0 - 23 Sep 2014 ### + +- Upgrade to SonarQube 4.2 API + +[LatestPluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/1.1.0/sonar-scoverage-plugin-1.1.0.jar +[Plugin102Jar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/1.0.2/sonar-scoverage-plugin-1.0.2.jar [SonarQube]: http://www.sonarqube.org/ "SonarQube" [Scoverage]: https://github.com/scoverage/scalac-scoverage-plugin "Scoverage" [sbt-scoverage]: https://github.com/scoverage/sbt-scoverage From 74f1a0ff23ab2d187cf61f97d0c221f42b5364ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=20Buransk=C3=BD?= Date: Tue, 23 Sep 2014 22:13:25 +0200 Subject: [PATCH 054/101] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7510fc1..96a56ac 100644 --- a/README.md +++ b/README.md @@ -25,16 +25,16 @@ just plain average of coverage rates for sub-projects. - [SonarQube] 4.2 - [Scoverage] 0.95.7 -### Support for older versions of Sonar ### - -- SonarQube 4.0: Install version 1.0.2 sonar-scoverage-plugin-1.0.2.jar] [Plugin102Jar]. -- SonarQube 3.5.1: Take a look into the [dedicated branch] [Plugin351] or directly [download binary JAR] [Plugin351Jar]. - ## Installation ## Download and copy [sonar-scoverage-plugin-1.1.0.jar] [LatestPluginJar] to the Sonar plugins directory (usually /extensions/plugins). Restart Sonar. +### Support for older versions of Sonar ### + +- SonarQube 4.0: Install version 1.0.2 [sonar-scoverage-plugin-1.0.2.jar] [Plugin102Jar]. +- SonarQube 3.5.1: Take a look into the [dedicated branch] [Plugin351] or directly [download binary JAR] [Plugin351Jar]. + ## Configure Sonar runner ## Set location of the **scoverage.xml** file in the **sonar-project.properties** located in your project's From 6b45f8edbcdb79e710c4ab684752fc63ed03df8b Mon Sep 17 00:00:00 2001 From: Jacek Laskowski Date: Fri, 17 Oct 2014 21:17:48 +0200 Subject: [PATCH 055/101] Build simplifications --- samples/sbt/multi-module/build.sbt | 10 +++++----- samples/sbt/multi-module/module1/build.sbt | 10 +--------- samples/sbt/multi-module/module2/build.sbt | 10 +--------- samples/sbt/multi-module/project/Common.scala | 5 +++-- samples/sbt/multi-module/project/build.properties | 1 + 5 files changed, 11 insertions(+), 25 deletions(-) create mode 100644 samples/sbt/multi-module/project/build.properties diff --git a/samples/sbt/multi-module/build.sbt b/samples/sbt/multi-module/build.sbt index 1f1caef..4aaffe8 100644 --- a/samples/sbt/multi-module/build.sbt +++ b/samples/sbt/multi-module/build.sbt @@ -1,9 +1,9 @@ -organization := "com.buransky" +organization in ThisBuild := "com.buransky" -scalaVersion := "2.10.3" +scalaVersion in ThisBuild := "2.10.4" -lazy val root = project.in(file(".")).aggregate(module1, module2) +version in ThisBuild := "1.0.0" -lazy val module1 = project.in(file("module1")) +lazy val module1 = project -lazy val module2 = project.in(file("module2")) +lazy val module2 = project diff --git a/samples/sbt/multi-module/module1/build.sbt b/samples/sbt/multi-module/module1/build.sbt index e3d30a2..df33176 100644 --- a/samples/sbt/multi-module/module1/build.sbt +++ b/samples/sbt/multi-module/module1/build.sbt @@ -1,13 +1,5 @@ -organization := Common.organization - name := Common.baseName + "-module1" -version := Common.version - -scalaVersion := "2.10.3" - -libraryDependencies ++= Seq( - "org.scalatest" %% "scalatest" % "2.0" % "test" -) +libraryDependencies += Common.scalatest ScoverageSbtPlugin.instrumentSettings \ No newline at end of file diff --git a/samples/sbt/multi-module/module2/build.sbt b/samples/sbt/multi-module/module2/build.sbt index 3d44cc1..9c66ac8 100644 --- a/samples/sbt/multi-module/module2/build.sbt +++ b/samples/sbt/multi-module/module2/build.sbt @@ -1,13 +1,5 @@ -organization := Common.organization - name := Common.baseName + "-module2" -version := Common.version - -scalaVersion := "2.10.3" - -libraryDependencies ++= Seq( - "org.scalatest" %% "scalatest" % "2.0" % "test" -) +libraryDependencies += Common.scalatest ScoverageSbtPlugin.instrumentSettings \ No newline at end of file diff --git a/samples/sbt/multi-module/project/Common.scala b/samples/sbt/multi-module/project/Common.scala index b3f598a..5daaf4f 100644 --- a/samples/sbt/multi-module/project/Common.scala +++ b/samples/sbt/multi-module/project/Common.scala @@ -1,5 +1,6 @@ +import sbt._ + object Common { - val organization = "com.buransky" val baseName = "multi-module" - val version = "1.0.0" + val scalatest = "org.scalatest" %% "scalatest" % "2.0" % "test" } \ No newline at end of file diff --git a/samples/sbt/multi-module/project/build.properties b/samples/sbt/multi-module/project/build.properties new file mode 100644 index 0000000..df58110 --- /dev/null +++ b/samples/sbt/multi-module/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.6 \ No newline at end of file From d3eeec5bd4a9dfba36a73a4e239913668508b681 Mon Sep 17 00:00:00 2001 From: Stephen Samuel Date: Fri, 28 Nov 2014 11:14:37 +0000 Subject: [PATCH 056/101] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 96a56ac..fe9c9e3 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ root directory: If your project is based on SBT and you're using [Scoverage plugin for SBT] [sbt-scoverage] you can generate the Scoverage report by executing following from command line: - $ sbt clean scoverage:test + $ sbt clean coverage test And then run Sonar runner to upload the report to the Sonar server: From dd1a44f38f67cbb03b8f9b9d78e5c2f874513c24 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Tue, 28 Apr 2015 16:43:28 +0200 Subject: [PATCH 057/101] Upgrade to Scala 2.11.6 --- plugin/pom.xml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index 0009f51..7a2632a 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -75,7 +75,8 @@ Scoverage com.buransky.plugins.scoverage.ScoveragePlugin - 2.10.0 + 2.11 + 2.11.6 @@ -91,14 +92,14 @@ org.scala-lang scala-library - ${scala.version} + ${scala.full.version} org.scalatest - scalatest_2.10 - 2.0 + scalatest_${scala.version} + 2.2.4 test From 1c4686bc79c2c63b0c5433fda6dcce4297d47c2b Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Tue, 28 Apr 2015 17:01:53 +0200 Subject: [PATCH 058/101] sonar-plugins parent 19 --- parent/pom.xml | 81 ++++++++++++++++++++------------------------------ plugin/pom.xml | 4 +-- 2 files changed, 35 insertions(+), 50 deletions(-) diff --git a/parent/pom.xml b/parent/pom.xml index c9fa59f..4d49acd 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -4,7 +4,7 @@ org.codehaus.sonar-plugins parent - 18 + 19 pom Sonar plugins parent @@ -34,9 +34,10 @@ - scm:svn:http://svn.codehaus.org/sonar-plugins/tags/18 - scm:svn:https://svn.codehaus.org/sonar-plugins/tags/18 - scm:svn:https://svn.codehaus.org/sonar-plugins/tags/18 + scm:git:https://github.com/SonarSource/parent-oss.git + scm:git:git@github.com:SonarSource/parent-oss.git + https://github.com/SonarSource/parent-oss + 19 jira @@ -62,8 +63,8 @@ UTF-8 - 2.2.1 - 1.6 + 3.0.5 + 1.7 ${maven.build.timestamp} yyyy-MM-dd'T'HH:mm:ssZ codehaus.org @@ -72,33 +73,33 @@ - 2.4 - 2.5 - 3.0 - 2.8 - 2.7 - 1.2 - 2.12.4 - 2.4 - 2.4 - 1.7 - 2.9 - 1.9.0 - 3.2 - 2.4.2 - 2.6 - 1.7.1 - 2.2.1 - 2.12.4 - 3.2 - 1.4 + 2.5.3 + 2.6.1 + 3.2 + 2.9 + 2.8.2 + 1.3.1 + 2.18 + 2.5.2 + 2.5 + 1.9 + 2.10.1 + 1.10.b1 + 3.3 + 2.5.1 + 2.7 + 2.3 + 2.4 + 2.18 + 3.4 + 1.5 - 1.9 - 1.2 + 1.13 + 1.3 1.0-beta-1 - 1.5 - 1.6 + 1.12.1 + 1.8 GNU LGPL 3 @@ -109,8 +110,8 @@ org.codehaus.mojo.signature - java16 - 1.1 + java17 + 1.0 @@ -245,22 +246,6 @@ --> -Prelease - - - - org.apache.maven.scm - maven-scm-api - 1.8.1 - - - org.apache.maven.scm - maven-scm-provider-gitexe - 1.8.1 - - org.apache.maven.plugins diff --git a/plugin/pom.xml b/plugin/pom.xml index 7a2632a..e413680 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -6,12 +6,12 @@ org.codehaus.sonar-plugins parent - 18 + 19 ../parent/pom.xml sonar-scoverage-plugin - 1.1.0 + 5.1.0 sonar-plugin Sonar Scoverage Plugin From a5e15c2181408e3d12ddb670ed2439c840c8bbd5 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Tue, 28 Apr 2015 19:10:44 +0200 Subject: [PATCH 059/101] v5.1.0-SNAPSHOT --- README.md | 4 ++-- plugin/README.md | 2 +- plugin/pom.xml | 7 ++++++- samples/sbt/multi-module/README.md | 8 +++++--- samples/sbt/multi-module/build.sbt | 4 ++-- samples/sbt/multi-module/module1/build.sbt | 4 +--- samples/sbt/multi-module/module2/build.sbt | 4 +--- samples/sbt/multi-module/project/Common.scala | 2 +- samples/sbt/multi-module/project/plugins.sbt | 2 +- samples/sbt/multi-module/sonar-project.properties | 6 +++--- 10 files changed, 23 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index fe9c9e3..0bdaef7 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ just plain average of coverage rates for sub-projects. ## Requirements ## -- [SonarQube] 4.2 -- [Scoverage] 0.95.7 +- [SonarQube] 5.1 +- [Scoverage] 1.1.0 ## Installation ## diff --git a/plugin/README.md b/plugin/README.md index 0016f5e..9444fa4 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -6,6 +6,6 @@ directory and start Sonar server again: /bin/linux-x86-64/sonar.sh stop mvn install - cp ./target/sonar-scoverage-plugin-1.0-SNAPSHOT.jar /extensions/plugins/ + cp ./target/sonar-scoverage-plugin-5.1.0-SNAPSHOT.jar /extensions/plugins/ /bin/linux-x86-64/sonar.sh start \ No newline at end of file diff --git a/plugin/pom.xml b/plugin/pom.xml index e413680..31420c9 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -11,7 +11,7 @@ sonar-scoverage-plugin - 5.1.0 + 5.1.0-SNAPSHOT sonar-plugin Sonar Scoverage Plugin @@ -94,6 +94,11 @@ scala-library ${scala.full.version} + + org.scala-lang.modules + scala-xml_${scala.version} + 1.0.3 + diff --git a/samples/sbt/multi-module/README.md b/samples/sbt/multi-module/README.md index 68bc857..c03f628 100644 --- a/samples/sbt/multi-module/README.md +++ b/samples/sbt/multi-module/README.md @@ -1,10 +1,12 @@ # Multi-module SBT sample project for Sonar Scoverage plugin # -Run scoverage to generate coverage reports: + 1. Create quality profile for Scala language and set it to be used by default. - $ sbt clean scoverage:test + 2. Run scoverage to generate coverage reports: -And then run Sonar runner to upload data from reports to the Sonar server: + $ sbt clean coverage test + + 3. And then run Sonar runner to upload data from reports to the Sonar server: $ sonar-runner diff --git a/samples/sbt/multi-module/build.sbt b/samples/sbt/multi-module/build.sbt index 4aaffe8..95a652a 100644 --- a/samples/sbt/multi-module/build.sbt +++ b/samples/sbt/multi-module/build.sbt @@ -1,8 +1,8 @@ organization in ThisBuild := "com.buransky" -scalaVersion in ThisBuild := "2.10.4" +scalaVersion in ThisBuild := "2.11.6" -version in ThisBuild := "1.0.0" +version in ThisBuild := "5.1.0" lazy val module1 = project diff --git a/samples/sbt/multi-module/module1/build.sbt b/samples/sbt/multi-module/module1/build.sbt index df33176..c40c574 100644 --- a/samples/sbt/multi-module/module1/build.sbt +++ b/samples/sbt/multi-module/module1/build.sbt @@ -1,5 +1,3 @@ name := Common.baseName + "-module1" -libraryDependencies += Common.scalatest - -ScoverageSbtPlugin.instrumentSettings \ No newline at end of file +libraryDependencies += Common.scalatest \ No newline at end of file diff --git a/samples/sbt/multi-module/module2/build.sbt b/samples/sbt/multi-module/module2/build.sbt index 9c66ac8..983896c 100644 --- a/samples/sbt/multi-module/module2/build.sbt +++ b/samples/sbt/multi-module/module2/build.sbt @@ -1,5 +1,3 @@ name := Common.baseName + "-module2" -libraryDependencies += Common.scalatest - -ScoverageSbtPlugin.instrumentSettings \ No newline at end of file +libraryDependencies += Common.scalatest \ No newline at end of file diff --git a/samples/sbt/multi-module/project/Common.scala b/samples/sbt/multi-module/project/Common.scala index 5daaf4f..9f60f92 100644 --- a/samples/sbt/multi-module/project/Common.scala +++ b/samples/sbt/multi-module/project/Common.scala @@ -2,5 +2,5 @@ import sbt._ object Common { val baseName = "multi-module" - val scalatest = "org.scalatest" %% "scalatest" % "2.0" % "test" + val scalatest = "org.scalatest" % "scalatest_2.11" % "2.2.4" } \ No newline at end of file diff --git a/samples/sbt/multi-module/project/plugins.sbt b/samples/sbt/multi-module/project/plugins.sbt index 046c8d7..6fa98f9 100644 --- a/samples/sbt/multi-module/project/plugins.sbt +++ b/samples/sbt/multi-module/project/plugins.sbt @@ -1,3 +1,3 @@ resolvers += Classpaths.sbtPluginReleases -addSbtPlugin("com.sksamuel.scoverage" %% "sbt-scoverage" % "0.95.7") \ No newline at end of file +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.1.0") \ No newline at end of file diff --git a/samples/sbt/multi-module/sonar-project.properties b/samples/sbt/multi-module/sonar-project.properties index cc12a04..c783f1b 100644 --- a/samples/sbt/multi-module/sonar-project.properties +++ b/samples/sbt/multi-module/sonar-project.properties @@ -1,6 +1,6 @@ sonar.projectKey=com.buransky:multi-module sonar.projectName=Sonar Scoverage plugin multi-module sample project -sonar.projectVersion=1.0.0 +sonar.projectVersion=5.1.0 sonar.language=scala @@ -8,8 +8,8 @@ sonar.modules=module1,module2 module1.sonar.sources=src/main/scala module1.sonar.tests=src/test/scala -module1.sonar.scoverage.reportPath=target/scala-2.10/scoverage-report/scoverage.xml +module1.sonar.scoverage.reportPath=target/scala-2.11/scoverage-report/scoverage.xml module2.sonar.sources=src/main/scala module2.sonar.tests=src/test/scala -module2.sonar.scoverage.reportPath=target/scala-2.10/scoverage-report/scoverage.xml +module2.sonar.scoverage.reportPath=target/scala-2.11/scoverage-report/scoverage.xml From 33beb48f40b65388d96ababf42789ad7926dc70a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=20Buransk=C3=BD?= Date: Tue, 28 Apr 2015 19:18:30 +0200 Subject: [PATCH 060/101] Update README.md --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0bdaef7..6f3e02b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -#Sonar Scoverage Plugin 1.1.0# +#Sonar Scoverage Plugin 5.1.0# [![Build Status](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin.png)](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin) @@ -27,11 +27,12 @@ just plain average of coverage rates for sub-projects. ## Installation ## -Download and copy [sonar-scoverage-plugin-1.1.0.jar] [LatestPluginJar] to the Sonar plugins directory +Download and copy [sonar-scoverage-plugin-5.1.0-SNAPSHOT.jar] [LatestPluginJar] to the Sonar plugins directory (usually /extensions/plugins). Restart Sonar. ### Support for older versions of Sonar ### +- SonarQube 4.2: Install version 1.1.0 [sonar-scoverage-plugin-1.1.0.jar] [Plugin110Jar]. - SonarQube 4.0: Install version 1.0.2 [sonar-scoverage-plugin-1.0.2.jar] [Plugin102Jar]. - SonarQube 3.5.1: Take a look into the [dedicated branch] [Plugin351] or directly [download binary JAR] [Plugin351Jar]. @@ -41,7 +42,7 @@ Set location of the **scoverage.xml** file in the **sonar-project.properties** l root directory: ... - sonar.scoverage.reportPath=target/scala-2.10/scoverage-report/scoverage.xml + sonar.scoverage.reportPath=target/scala-2.11/scoverage-report/scoverage.xml ... ## Run Scoverage and Sonar runner ## @@ -88,11 +89,16 @@ Source code markup with covered and uncovered lines: ## Changelog ## +### 5.1.0 - 28 Apr 2015 ### + +- Upgrade to SonarQube 5.1 API + ### 1.1.0 - 23 Sep 2014 ### - Upgrade to SonarQube 4.2 API -[LatestPluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/1.1.0/sonar-scoverage-plugin-1.1.0.jar +[LatestPluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v5.1.0-SNAPSHOT/sonar-scoverage-plugin-5.1.0-SNAPSHOT.jar +[Plugin110Jar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/1.1.0/sonar-scoverage-plugin-1.1.0.jar [Plugin102Jar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/1.0.2/sonar-scoverage-plugin-1.0.2.jar [SonarQube]: http://www.sonarqube.org/ "SonarQube" [Scoverage]: https://github.com/scoverage/scalac-scoverage-plugin "Scoverage" From 26dbc10a796491d08a6dd01a951d12dc03ee0862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=20Buransk=C3=BD?= Date: Tue, 28 Apr 2015 19:45:02 +0200 Subject: [PATCH 061/101] Build with OpenJDK 7 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7bdce0d..96c5f08 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ language: java script: "mvn -f plugin/pom.xml install" jdk: - - openjdk6 + - openjdk7 From b6437910a9bae509579a100812c78271a4e7f429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=20Buransk=C3=BD?= Date: Tue, 28 Apr 2015 20:35:39 +0200 Subject: [PATCH 062/101] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index fe9c9e3..fd47aaa 100644 --- a/README.md +++ b/README.md @@ -99,3 +99,5 @@ Source code markup with covered and uncovered lines: [sbt-scoverage]: https://github.com/scoverage/sbt-scoverage [Plugin351]: https://github.com/RadoBuransky/sonar-scoverage-plugin/tree/sonar3.5.1 [Plugin351Jar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v1.0.2-Sonar3.5.1/sonar-scoverage-plugin-sonar3.5.1-1.0.2.jar + +[![Analytics](https://ga-beacon.appspot.com/UA-55603212-2/sonar-scoverage-plugin/README.md)](https://github.com/igrigorik/ga-beacon) From a7a1ab481eed3dd2d7ee790bc4d8861050da2116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=20Buransk=C3=BD?= Date: Tue, 28 Apr 2015 20:39:41 +0200 Subject: [PATCH 063/101] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index fd47aaa..736cf9f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ #Sonar Scoverage Plugin 1.1.0# [![Build Status](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin.png)](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin) +[![Analytics](https://ga-beacon.appspot.com/UA-55603212-2/sonar-scoverage-plugin/README.md)](https://github.com/igrigorik/ga-beacon) Plugin for [SonarQube] that imports statement coverage generated by [Scoverage] for Scala projects. @@ -99,5 +100,3 @@ Source code markup with covered and uncovered lines: [sbt-scoverage]: https://github.com/scoverage/sbt-scoverage [Plugin351]: https://github.com/RadoBuransky/sonar-scoverage-plugin/tree/sonar3.5.1 [Plugin351Jar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v1.0.2-Sonar3.5.1/sonar-scoverage-plugin-sonar3.5.1-1.0.2.jar - -[![Analytics](https://ga-beacon.appspot.com/UA-55603212-2/sonar-scoverage-plugin/README.md)](https://github.com/igrigorik/ga-beacon) From 6b32c5b74b00195923ab6c3c2fcf99f258e82d3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=20Buransk=C3=BD?= Date: Tue, 28 Apr 2015 20:48:53 +0200 Subject: [PATCH 064/101] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 736cf9f..53eb1b4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ #Sonar Scoverage Plugin 1.1.0# [![Build Status](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin.png)](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin) -[![Analytics](https://ga-beacon.appspot.com/UA-55603212-2/sonar-scoverage-plugin/README.md)](https://github.com/igrigorik/ga-beacon) +[![Analytics](https://ga-beacon.appspot.com/UA-55603212-2/sonar-scoverage-plugin/README.md?pixel)](https://github.com/igrigorik/ga-beacon) Plugin for [SonarQube] that imports statement coverage generated by [Scoverage] for Scala projects. From 9f92cc9053342f48870b3867100df804f8a3b05d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=20Buransk=C3=BD?= Date: Tue, 28 Apr 2015 20:53:13 +0200 Subject: [PATCH 065/101] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 53eb1b4..fcfda86 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ #Sonar Scoverage Plugin 1.1.0# [![Build Status](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin.png)](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin) -[![Analytics](https://ga-beacon.appspot.com/UA-55603212-2/sonar-scoverage-plugin/README.md?pixel)](https://github.com/igrigorik/ga-beacon) +[![Analytics](https://ga-beacon.appspot.com/UA-55603212-2/sonar-scoverage-plugin)](https://github.com/igrigorik/ga-beacon) Plugin for [SonarQube] that imports statement coverage generated by [Scoverage] for Scala projects. From 809163c5eebf9436ca92a90557fa2c9b57c1c526 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Sun, 3 May 2015 15:41:32 +0200 Subject: [PATCH 066/101] Fixing... --- .../scoverage/sensor/ScoverageSensor.scala | 2 +- ...XmlScoverageReportConstructingParser.scala | 36 +++++++++---------- ...coverageReportConstructingParserSpec.scala | 31 +++++++++++----- .../scoverage/xml/data/XmlReportFile1.scala | 36 ++++++++++++++++++- 4 files changed, 75 insertions(+), 30 deletions(-) diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala index 26cf8b3..3e67c6b 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala @@ -161,7 +161,7 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem saveLineCoverage(fileCoverage.statements, scalaSourceFile, context) } - case None => log.warn("File not found in file system! " + relativePath) + case None => log.warn(s"File not found in file system! [$directory, ${fileCoverage.name}]") } } diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala index ba92f0e..e427b06 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala @@ -48,7 +48,7 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP var currentFilePath: Option[String] = None def parse(): ProjectStatementCoverage = { - // Initialze + // Initialize nextch() // Parse @@ -60,13 +60,13 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP override def elemStart(pos: Int, pre: String, label: String, attrs: MetaData, scope: NamespaceBinding) { label match { - case CLASS_ELEMENT => { + case CLASS_ELEMENT => currentFilePath = Some(fixLeadingSlash(getText(attrs, FILENAME_ATTRIBUTE))) log.debug("Current file path: " + currentFilePath.get) - } - case STATEMENT_ELEMENT => { + + case STATEMENT_ELEMENT => currentFilePath match { - case Some(cfp) => { + case Some(cfp) => val start = getInt(attrs, START_ATTRIBUTE) val line = getInt(attrs, LINE_ATTRIBUTE) val hits = getInt(attrs, INVOCATION_COUNT_ATTRIBUTE) @@ -76,10 +76,9 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP addCoveredStatement(cfp, CoveredStatement(pos, pos, hits)) log.debug("Statement added: " + line + ", " + hits + ", " + start) - } + case None => throw new ScoverageException("Current file path not set!") } - } case _ => // Nothing to do } @@ -94,11 +93,13 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP } /** - * Remove this when scoverage is fixed! + * Remove this when scoverage is fixed! It's just a hack. + * Old Scoverage has incorrectly added leading '/' to relative file paths. */ private def fixLeadingSlash(filePath: String) = { - if (filePath.startsWith(File.separator)) + if (filePath.startsWith(File.separator) && !new File(filePath).exists()) { filePath.drop(File.separator.length) + } else filePath } @@ -107,12 +108,11 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP private def getText(attrs: MetaData, name: String): String = { attrs.get(name) match { - case Some(attr) => { + case Some(attr) => attr match { - case text: Text => text.toString + case text: Text => text.toString() case _ => throw new ScoverageException("Not a text attribute!") } - } case None => throw new ScoverageException("Attribute doesn't exit! [" + name + "]") } } @@ -125,17 +125,14 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP final def add(chain: DirOrFile) { get(chain.name) match { case None => children = chain :: children - case Some(child) => { + case Some(child) => chain.children match { - case h :: t => { + case h :: t => if (t != Nil) throw new IllegalStateException("This is not a linear chain!") - child.add(h) - } case _ => // Duplicate file? Should not happen. } - } } } @@ -168,7 +165,7 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP // Merge chains into one tree val root = DirOrFile("", Nil, None) - chained.foreach(root.add(_)) + chained.foreach(root.add) // Transform file system tree into coverage structure tree root.toProjectStatementCoverage @@ -198,8 +195,7 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP else { // Append file dirs.last.children = List(file) - - dirs(0) + dirs.head } } diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala index 790dc58..2e96b81 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala @@ -25,28 +25,43 @@ import org.scalatest.{Matchers, FlatSpec} import scala.io.Source import com.buransky.plugins.scoverage.xml.data.XmlReportFile1 import scala._ -import com.buransky.plugins.scoverage.FileStatementCoverage -import com.buransky.plugins.scoverage.DirectoryStatementCoverage +import com.buransky.plugins.scoverage.{ProjectStatementCoverage, FileStatementCoverage, DirectoryStatementCoverage} @RunWith(classOf[JUnitRunner]) class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { behavior of "parse source" - it must "parse file1 correctly" in { - parseFile1(XmlReportFile1.data) + ignore must "parse old broken Scoverage 0.95 file correctly" in { + assertReportFile(XmlReportFile1.scoverage095Data, 24.53)(assertScoverage095Data) } - it must "parse file1 correctly even without XML declaration" in { - parseFile1(XmlReportFile1.dataWithoutDeclaration) + it must "parse new fixed Scoverage 1.0.4 file correctly" in { + assertReportFile(XmlReportFile1.scoverage104Data, 50.0) { projectCoverage => + assert(projectCoverage.name === "") + assert(projectCoverage.children.size.toInt === 1) + projectCoverage.children.head match { + case mainClass: FileStatementCoverage => + assert(mainClass.name == "/home/rado/workspace/sonar-test/src/main/scala/com/rr/test/sonar/MainClass.scala") + case other => fail(s"This is not a file statement coverage! [$other]") + } + } } - private def parseFile1(data: String) { + ignore must "parse file1 correctly even without XML declaration" in { + assertReportFile(XmlReportFile1.dataWithoutDeclaration, 24.53)(assertScoverage095Data) + } + + private def assertReportFile(data: String, expectedCoverage: Double)(f: (ProjectStatementCoverage) => Unit) { val parser = new XmlScoverageReportConstructingParser(Source.fromString(data)) val projectCoverage = parser.parse() // Assert coverage - checkRate(24.53, projectCoverage.rate) + checkRate(expectedCoverage, projectCoverage.rate) + + f(projectCoverage) + } + private def assertScoverage095Data(projectCoverage: ProjectStatementCoverage): Unit = { // Assert structure projectCoverage.name should equal("") diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala index 8be51ed..526ca7a 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala @@ -20,7 +20,41 @@ package com.buransky.plugins.scoverage.xml.data object XmlReportFile1 { - val data = + val scoverage104Data = + """ + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + |""".stripMargin + + val scoverage095Data = """ | | From a6d2a8320593085ca027f860710076decc1c2ab2 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Sun, 3 May 2015 18:42:42 +0200 Subject: [PATCH 067/101] Use FileSystem.hasPath for absolute paths --- plugin/pom.xml | 2 +- .../scoverage/sensor/ScoverageSensor.scala | 31 +++++++++++++------ .../plugins/scoverage/util/PathUtil.scala | 5 ++- .../sensor/ScoverageSensorSpec.scala | 23 ++++++++++++-- .../plugins/scoverage/util/PathUtilSpec.scala | 4 +-- ...coverageReportConstructingParserSpec.scala | 23 ++++++++------ 6 files changed, 63 insertions(+), 25 deletions(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index 31420c9..7c74405 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -11,7 +11,7 @@ sonar-scoverage-plugin - 5.1.0-SNAPSHOT + 5.1.1-SNAPSHOT sonar-plugin Sonar Scoverage Plugin diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala index 3e67c6b..ba81f0f 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala @@ -19,6 +19,8 @@ */ package com.buransky.plugins.scoverage.sensor +import java.io + import com.buransky.plugins.scoverage.language.Scala import com.buransky.plugins.scoverage.measure.ScalaMetrics import com.buransky.plugins.scoverage.util.LogUtil @@ -143,15 +145,17 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem } private def processFile(fileCoverage: FileStatementCoverage, context: SensorContext, directory: String) { - val relativePath = appendFilePath(directory, fileCoverage.name) - + val path = appendFilePath(directory, fileCoverage.name) val p = fileSystem.predicates() - val files = fileSystem.inputFiles(p.and(p.matchesPathPattern("**/" + relativePath), - p.hasLanguage(scala.getKey), p.hasType(InputFile.Type.MAIN))) + + val pathPredicate = if (new io.File(path).isAbsolute) p.hasAbsolutePath(path) else p.matchesPathPattern("**/" + path) + val files = fileSystem.inputFiles(p.and( + pathPredicate, + p.hasLanguage(scala.getKey), + p.hasType(InputFile.Type.MAIN))) files.headOption match { - case Some(file) => { - //val scalaSourceFile = new ScalaFile(file.relativePath(), scala) + case Some(file) => val scalaSourceFile = File.create(file.relativePath()) // Save measures @@ -159,9 +163,13 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem // Save line coverage. This is needed just for source code highlighting. saveLineCoverage(fileCoverage.statements, scalaSourceFile, context) - } - case None => log.warn(s"File not found in file system! [$directory, ${fileCoverage.name}]") + case None => { + fileSystem.inputFiles(p.all()).foreach { inputFile => + log.debug(inputFile.absolutePath()) + } + log.warn(s"File not found in file system! [$pathPredicate]") + } } } @@ -210,7 +218,12 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem new Measure(ScalaMetrics.coveredStatements, coveredStatements); private def appendFilePath(src: String, name: String) = { - val result = if (!src.isEmpty) src + java.io.File.separator else "" + val result = src match { + case java.io.File.separator => java.io.File.separator + case empty if empty.isEmpty => "" + case other => other + java.io.File.separator + } + result + name } } diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala index e4da7e8..d4a8938 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala @@ -27,5 +27,8 @@ import java.io.File */ object PathUtil { def splitPath(filePath: String, separator: String = File.separator): List[String] = - filePath.split(separator.replaceAllLiterally("\\", "\\\\")).toList + filePath.split(separator.replaceAllLiterally("\\", "\\\\")).toList match { + case "" :: tail if tail.nonEmpty => separator :: tail + case other => other + } } diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala index 11ad086..c47151d 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala @@ -19,16 +19,17 @@ */ package com.buransky.plugins.scoverage.sensor +import java.io.File import java.util import com.buransky.plugins.scoverage.language.Scala -import com.buransky.plugins.scoverage.{ProjectStatementCoverage, ScoverageReportParser} +import com.buransky.plugins.scoverage.{FileStatementCoverage, DirectoryStatementCoverage, ProjectStatementCoverage, ScoverageReportParser} import org.junit.runner.RunWith import org.mockito.Mockito._ import org.scalatest.junit.JUnitRunner import org.scalatest.mock.MockitoSugar import org.scalatest.{FlatSpec, Matchers} -import org.sonar.api.batch.fs.FileSystem +import org.sonar.api.batch.fs.{FilePredicate, FilePredicates, FileSystem} import org.sonar.api.config.Settings import org.sonar.api.resources.Project import org.sonar.api.resources.Project.AnalysisType @@ -75,19 +76,35 @@ class ScoverageSensorSpec extends FlatSpec with Matchers with MockitoSugar { // Setup val pathToScoverageReport = "#path-to-scoverage-report#" val reportAbsolutePath = "#report-absolute-path#" - val projectStatementCoverage = ProjectStatementCoverage("project-name", Nil) + val projectStatementCoverage = + ProjectStatementCoverage("project-name", List( + DirectoryStatementCoverage(File.separator, List( + DirectoryStatementCoverage("home", List( + FileStatementCoverage("a.scala", 3, 2, Nil) + )) + )), + DirectoryStatementCoverage("x", List( + FileStatementCoverage("b.scala", 1, 0, Nil) + )) + )) val reportFile = mock[java.io.File] val moduleBaseDir = mock[java.io.File] + val filePredicates = mock[FilePredicates] when(reportFile.exists).thenReturn(true) when(reportFile.isFile).thenReturn(true) when(reportFile.getAbsolutePath).thenReturn(reportAbsolutePath) when(settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY)).thenReturn(pathToScoverageReport) when(fileSystem.baseDir).thenReturn(moduleBaseDir) + when(fileSystem.predicates).thenReturn(filePredicates) + when(fileSystem.inputFiles(org.mockito.Matchers.any[FilePredicate]())).thenReturn(Nil) when(pathResolver.relativeFile(moduleBaseDir, pathToScoverageReport)).thenReturn(reportFile) when(scoverageReportParser.parse(reportAbsolutePath)).thenReturn(projectStatementCoverage) // Execute analyse(project, context) + + verify(filePredicates).hasAbsolutePath("/home/a.scala") + verify(filePredicates).matchesPathPattern("**/x/b.scala") } class AnalyseScoverageSensorScope extends ScoverageSensorScope { diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala index 28efa25..bae563d 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala @@ -30,14 +30,14 @@ class UnixPathUtilSpec extends ParamPathUtilSpec("Unix", "/") class WindowsPathUtilSpec extends ParamPathUtilSpec("Windows", "\\") abstract class ParamPathUtilSpec(osName: String, separator: String) extends FlatSpec with Matchers { - behavior of s"splitPath for ${osName}" + behavior of s"splitPath for $osName" it should "work for empty path" in { PathUtil.splitPath("", separator) should equal(List("")) } it should "work with separator at the beginning" in { - PathUtil.splitPath(s"${separator}a", separator) should equal(List("", "a")) + PathUtil.splitPath(s"${separator}a", separator) should equal(List(separator, "a")) } it should "work with separator in the middle" in { diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala index 2e96b81..bf9b33b 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala @@ -31,7 +31,7 @@ import com.buransky.plugins.scoverage.{ProjectStatementCoverage, FileStatementCo class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { behavior of "parse source" - ignore must "parse old broken Scoverage 0.95 file correctly" in { + it must "parse old broken Scoverage 0.95 file correctly" in { assertReportFile(XmlReportFile1.scoverage095Data, 24.53)(assertScoverage095Data) } @@ -40,14 +40,19 @@ class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { assert(projectCoverage.name === "") assert(projectCoverage.children.size.toInt === 1) projectCoverage.children.head match { - case mainClass: FileStatementCoverage => - assert(mainClass.name == "/home/rado/workspace/sonar-test/src/main/scala/com/rr/test/sonar/MainClass.scala") - case other => fail(s"This is not a file statement coverage! [$other]") + case rootDir: DirectoryStatementCoverage => + assert(rootDir.name == "/") + rootDir.children.head match { + case homeDir: DirectoryStatementCoverage => + assert(homeDir.name == "home") + case other => fail(s"This is not a home statement coverage! [$other]") + } + case other => fail(s"This is not a directory statement coverage! [$other]") } } } - ignore must "parse file1 correctly even without XML declaration" in { + it must "parse file1 correctly even without XML declaration" in { assertReportFile(XmlReportFile1.dataWithoutDeclaration, 24.53)(assertScoverage095Data) } @@ -67,9 +72,9 @@ class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { val projectChildren = projectCoverage.children.toList projectChildren.length should equal(1) - projectChildren(0) shouldBe a [DirectoryStatementCoverage] + projectChildren.head shouldBe a [DirectoryStatementCoverage] - val aaa = projectChildren(0).asInstanceOf[DirectoryStatementCoverage] + val aaa = projectChildren.head.asInstanceOf[DirectoryStatementCoverage] aaa.name should equal("aaa") checkRate(24.53, aaa.rate) @@ -82,8 +87,8 @@ class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { errorCode.statementCount should equal (46) errorCode.coveredStatementsCount should equal (13) - aaaChildren(0) shouldBe a [FileStatementCoverage] - val graph = aaaChildren(0).asInstanceOf[FileStatementCoverage] + aaaChildren.head shouldBe a [FileStatementCoverage] + val graph = aaaChildren.head.asInstanceOf[FileStatementCoverage] graph.name should equal("Graph.scala") graph.statementCount should equal (7) graph.coveredStatementsCount should equal (0) From 7539604ceb5209ef4c9aed79b6163c537d37133a Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Sun, 3 May 2015 15:41:32 +0200 Subject: [PATCH 068/101] Fixing... (cherry picked from commit 809163c) --- .../scoverage/sensor/ScoverageSensor.scala | 2 +- ...XmlScoverageReportConstructingParser.scala | 36 +++++++++---------- ...coverageReportConstructingParserSpec.scala | 31 +++++++++++----- .../scoverage/xml/data/XmlReportFile1.scala | 36 ++++++++++++++++++- 4 files changed, 75 insertions(+), 30 deletions(-) diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala index 26cf8b3..3e67c6b 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala @@ -161,7 +161,7 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem saveLineCoverage(fileCoverage.statements, scalaSourceFile, context) } - case None => log.warn("File not found in file system! " + relativePath) + case None => log.warn(s"File not found in file system! [$directory, ${fileCoverage.name}]") } } diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala index ba92f0e..e427b06 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala @@ -48,7 +48,7 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP var currentFilePath: Option[String] = None def parse(): ProjectStatementCoverage = { - // Initialze + // Initialize nextch() // Parse @@ -60,13 +60,13 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP override def elemStart(pos: Int, pre: String, label: String, attrs: MetaData, scope: NamespaceBinding) { label match { - case CLASS_ELEMENT => { + case CLASS_ELEMENT => currentFilePath = Some(fixLeadingSlash(getText(attrs, FILENAME_ATTRIBUTE))) log.debug("Current file path: " + currentFilePath.get) - } - case STATEMENT_ELEMENT => { + + case STATEMENT_ELEMENT => currentFilePath match { - case Some(cfp) => { + case Some(cfp) => val start = getInt(attrs, START_ATTRIBUTE) val line = getInt(attrs, LINE_ATTRIBUTE) val hits = getInt(attrs, INVOCATION_COUNT_ATTRIBUTE) @@ -76,10 +76,9 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP addCoveredStatement(cfp, CoveredStatement(pos, pos, hits)) log.debug("Statement added: " + line + ", " + hits + ", " + start) - } + case None => throw new ScoverageException("Current file path not set!") } - } case _ => // Nothing to do } @@ -94,11 +93,13 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP } /** - * Remove this when scoverage is fixed! + * Remove this when scoverage is fixed! It's just a hack. + * Old Scoverage has incorrectly added leading '/' to relative file paths. */ private def fixLeadingSlash(filePath: String) = { - if (filePath.startsWith(File.separator)) + if (filePath.startsWith(File.separator) && !new File(filePath).exists()) { filePath.drop(File.separator.length) + } else filePath } @@ -107,12 +108,11 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP private def getText(attrs: MetaData, name: String): String = { attrs.get(name) match { - case Some(attr) => { + case Some(attr) => attr match { - case text: Text => text.toString + case text: Text => text.toString() case _ => throw new ScoverageException("Not a text attribute!") } - } case None => throw new ScoverageException("Attribute doesn't exit! [" + name + "]") } } @@ -125,17 +125,14 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP final def add(chain: DirOrFile) { get(chain.name) match { case None => children = chain :: children - case Some(child) => { + case Some(child) => chain.children match { - case h :: t => { + case h :: t => if (t != Nil) throw new IllegalStateException("This is not a linear chain!") - child.add(h) - } case _ => // Duplicate file? Should not happen. } - } } } @@ -168,7 +165,7 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP // Merge chains into one tree val root = DirOrFile("", Nil, None) - chained.foreach(root.add(_)) + chained.foreach(root.add) // Transform file system tree into coverage structure tree root.toProjectStatementCoverage @@ -198,8 +195,7 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP else { // Append file dirs.last.children = List(file) - - dirs(0) + dirs.head } } diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala index 790dc58..2e96b81 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala @@ -25,28 +25,43 @@ import org.scalatest.{Matchers, FlatSpec} import scala.io.Source import com.buransky.plugins.scoverage.xml.data.XmlReportFile1 import scala._ -import com.buransky.plugins.scoverage.FileStatementCoverage -import com.buransky.plugins.scoverage.DirectoryStatementCoverage +import com.buransky.plugins.scoverage.{ProjectStatementCoverage, FileStatementCoverage, DirectoryStatementCoverage} @RunWith(classOf[JUnitRunner]) class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { behavior of "parse source" - it must "parse file1 correctly" in { - parseFile1(XmlReportFile1.data) + ignore must "parse old broken Scoverage 0.95 file correctly" in { + assertReportFile(XmlReportFile1.scoverage095Data, 24.53)(assertScoverage095Data) } - it must "parse file1 correctly even without XML declaration" in { - parseFile1(XmlReportFile1.dataWithoutDeclaration) + it must "parse new fixed Scoverage 1.0.4 file correctly" in { + assertReportFile(XmlReportFile1.scoverage104Data, 50.0) { projectCoverage => + assert(projectCoverage.name === "") + assert(projectCoverage.children.size.toInt === 1) + projectCoverage.children.head match { + case mainClass: FileStatementCoverage => + assert(mainClass.name == "/home/rado/workspace/sonar-test/src/main/scala/com/rr/test/sonar/MainClass.scala") + case other => fail(s"This is not a file statement coverage! [$other]") + } + } } - private def parseFile1(data: String) { + ignore must "parse file1 correctly even without XML declaration" in { + assertReportFile(XmlReportFile1.dataWithoutDeclaration, 24.53)(assertScoverage095Data) + } + + private def assertReportFile(data: String, expectedCoverage: Double)(f: (ProjectStatementCoverage) => Unit) { val parser = new XmlScoverageReportConstructingParser(Source.fromString(data)) val projectCoverage = parser.parse() // Assert coverage - checkRate(24.53, projectCoverage.rate) + checkRate(expectedCoverage, projectCoverage.rate) + + f(projectCoverage) + } + private def assertScoverage095Data(projectCoverage: ProjectStatementCoverage): Unit = { // Assert structure projectCoverage.name should equal("") diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala index 8be51ed..526ca7a 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala @@ -20,7 +20,41 @@ package com.buransky.plugins.scoverage.xml.data object XmlReportFile1 { - val data = + val scoverage104Data = + """ + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + |""".stripMargin + + val scoverage095Data = """ | | From 8becc1971ec3bfe9487ac6781225f593e6bf8655 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Thu, 7 May 2015 19:19:34 +0200 Subject: [PATCH 069/101] Cherry-picked issue 12 fix --- plugin/pom.xml | 14 ++++++--- .../scoverage/sensor/ScoverageSensor.scala | 31 +++++++++++++------ .../plugins/scoverage/util/PathUtil.scala | 5 ++- .../sensor/ScoverageSensorSpec.scala | 23 ++++++++++++-- .../plugins/scoverage/util/PathUtilSpec.scala | 4 +-- ...coverageReportConstructingParserSpec.scala | 23 ++++++++------ 6 files changed, 72 insertions(+), 28 deletions(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index 0009f51..5f4e378 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -75,7 +75,8 @@ Scoverage com.buransky.plugins.scoverage.ScoveragePlugin - 2.10.0 + 2.11 + 2.11.6
@@ -91,14 +92,19 @@ org.scala-lang scala-library - ${scala.version} + ${scala.full.version} + + + org.scala-lang.modules + scala-xml_${scala.version} + 1.0.3 org.scalatest - scalatest_2.10 - 2.0 + scalatest_${scala.version} + 2.2.4 test diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala index 3e67c6b..ba81f0f 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala @@ -19,6 +19,8 @@ */ package com.buransky.plugins.scoverage.sensor +import java.io + import com.buransky.plugins.scoverage.language.Scala import com.buransky.plugins.scoverage.measure.ScalaMetrics import com.buransky.plugins.scoverage.util.LogUtil @@ -143,15 +145,17 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem } private def processFile(fileCoverage: FileStatementCoverage, context: SensorContext, directory: String) { - val relativePath = appendFilePath(directory, fileCoverage.name) - + val path = appendFilePath(directory, fileCoverage.name) val p = fileSystem.predicates() - val files = fileSystem.inputFiles(p.and(p.matchesPathPattern("**/" + relativePath), - p.hasLanguage(scala.getKey), p.hasType(InputFile.Type.MAIN))) + + val pathPredicate = if (new io.File(path).isAbsolute) p.hasAbsolutePath(path) else p.matchesPathPattern("**/" + path) + val files = fileSystem.inputFiles(p.and( + pathPredicate, + p.hasLanguage(scala.getKey), + p.hasType(InputFile.Type.MAIN))) files.headOption match { - case Some(file) => { - //val scalaSourceFile = new ScalaFile(file.relativePath(), scala) + case Some(file) => val scalaSourceFile = File.create(file.relativePath()) // Save measures @@ -159,9 +163,13 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem // Save line coverage. This is needed just for source code highlighting. saveLineCoverage(fileCoverage.statements, scalaSourceFile, context) - } - case None => log.warn(s"File not found in file system! [$directory, ${fileCoverage.name}]") + case None => { + fileSystem.inputFiles(p.all()).foreach { inputFile => + log.debug(inputFile.absolutePath()) + } + log.warn(s"File not found in file system! [$pathPredicate]") + } } } @@ -210,7 +218,12 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem new Measure(ScalaMetrics.coveredStatements, coveredStatements); private def appendFilePath(src: String, name: String) = { - val result = if (!src.isEmpty) src + java.io.File.separator else "" + val result = src match { + case java.io.File.separator => java.io.File.separator + case empty if empty.isEmpty => "" + case other => other + java.io.File.separator + } + result + name } } diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala index e4da7e8..d4a8938 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala @@ -27,5 +27,8 @@ import java.io.File */ object PathUtil { def splitPath(filePath: String, separator: String = File.separator): List[String] = - filePath.split(separator.replaceAllLiterally("\\", "\\\\")).toList + filePath.split(separator.replaceAllLiterally("\\", "\\\\")).toList match { + case "" :: tail if tail.nonEmpty => separator :: tail + case other => other + } } diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala index 11ad086..c47151d 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala @@ -19,16 +19,17 @@ */ package com.buransky.plugins.scoverage.sensor +import java.io.File import java.util import com.buransky.plugins.scoverage.language.Scala -import com.buransky.plugins.scoverage.{ProjectStatementCoverage, ScoverageReportParser} +import com.buransky.plugins.scoverage.{FileStatementCoverage, DirectoryStatementCoverage, ProjectStatementCoverage, ScoverageReportParser} import org.junit.runner.RunWith import org.mockito.Mockito._ import org.scalatest.junit.JUnitRunner import org.scalatest.mock.MockitoSugar import org.scalatest.{FlatSpec, Matchers} -import org.sonar.api.batch.fs.FileSystem +import org.sonar.api.batch.fs.{FilePredicate, FilePredicates, FileSystem} import org.sonar.api.config.Settings import org.sonar.api.resources.Project import org.sonar.api.resources.Project.AnalysisType @@ -75,19 +76,35 @@ class ScoverageSensorSpec extends FlatSpec with Matchers with MockitoSugar { // Setup val pathToScoverageReport = "#path-to-scoverage-report#" val reportAbsolutePath = "#report-absolute-path#" - val projectStatementCoverage = ProjectStatementCoverage("project-name", Nil) + val projectStatementCoverage = + ProjectStatementCoverage("project-name", List( + DirectoryStatementCoverage(File.separator, List( + DirectoryStatementCoverage("home", List( + FileStatementCoverage("a.scala", 3, 2, Nil) + )) + )), + DirectoryStatementCoverage("x", List( + FileStatementCoverage("b.scala", 1, 0, Nil) + )) + )) val reportFile = mock[java.io.File] val moduleBaseDir = mock[java.io.File] + val filePredicates = mock[FilePredicates] when(reportFile.exists).thenReturn(true) when(reportFile.isFile).thenReturn(true) when(reportFile.getAbsolutePath).thenReturn(reportAbsolutePath) when(settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY)).thenReturn(pathToScoverageReport) when(fileSystem.baseDir).thenReturn(moduleBaseDir) + when(fileSystem.predicates).thenReturn(filePredicates) + when(fileSystem.inputFiles(org.mockito.Matchers.any[FilePredicate]())).thenReturn(Nil) when(pathResolver.relativeFile(moduleBaseDir, pathToScoverageReport)).thenReturn(reportFile) when(scoverageReportParser.parse(reportAbsolutePath)).thenReturn(projectStatementCoverage) // Execute analyse(project, context) + + verify(filePredicates).hasAbsolutePath("/home/a.scala") + verify(filePredicates).matchesPathPattern("**/x/b.scala") } class AnalyseScoverageSensorScope extends ScoverageSensorScope { diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala index 28efa25..bae563d 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala @@ -30,14 +30,14 @@ class UnixPathUtilSpec extends ParamPathUtilSpec("Unix", "/") class WindowsPathUtilSpec extends ParamPathUtilSpec("Windows", "\\") abstract class ParamPathUtilSpec(osName: String, separator: String) extends FlatSpec with Matchers { - behavior of s"splitPath for ${osName}" + behavior of s"splitPath for $osName" it should "work for empty path" in { PathUtil.splitPath("", separator) should equal(List("")) } it should "work with separator at the beginning" in { - PathUtil.splitPath(s"${separator}a", separator) should equal(List("", "a")) + PathUtil.splitPath(s"${separator}a", separator) should equal(List(separator, "a")) } it should "work with separator in the middle" in { diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala index 2e96b81..bf9b33b 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala @@ -31,7 +31,7 @@ import com.buransky.plugins.scoverage.{ProjectStatementCoverage, FileStatementCo class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { behavior of "parse source" - ignore must "parse old broken Scoverage 0.95 file correctly" in { + it must "parse old broken Scoverage 0.95 file correctly" in { assertReportFile(XmlReportFile1.scoverage095Data, 24.53)(assertScoverage095Data) } @@ -40,14 +40,19 @@ class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { assert(projectCoverage.name === "") assert(projectCoverage.children.size.toInt === 1) projectCoverage.children.head match { - case mainClass: FileStatementCoverage => - assert(mainClass.name == "/home/rado/workspace/sonar-test/src/main/scala/com/rr/test/sonar/MainClass.scala") - case other => fail(s"This is not a file statement coverage! [$other]") + case rootDir: DirectoryStatementCoverage => + assert(rootDir.name == "/") + rootDir.children.head match { + case homeDir: DirectoryStatementCoverage => + assert(homeDir.name == "home") + case other => fail(s"This is not a home statement coverage! [$other]") + } + case other => fail(s"This is not a directory statement coverage! [$other]") } } } - ignore must "parse file1 correctly even without XML declaration" in { + it must "parse file1 correctly even without XML declaration" in { assertReportFile(XmlReportFile1.dataWithoutDeclaration, 24.53)(assertScoverage095Data) } @@ -67,9 +72,9 @@ class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { val projectChildren = projectCoverage.children.toList projectChildren.length should equal(1) - projectChildren(0) shouldBe a [DirectoryStatementCoverage] + projectChildren.head shouldBe a [DirectoryStatementCoverage] - val aaa = projectChildren(0).asInstanceOf[DirectoryStatementCoverage] + val aaa = projectChildren.head.asInstanceOf[DirectoryStatementCoverage] aaa.name should equal("aaa") checkRate(24.53, aaa.rate) @@ -82,8 +87,8 @@ class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { errorCode.statementCount should equal (46) errorCode.coveredStatementsCount should equal (13) - aaaChildren(0) shouldBe a [FileStatementCoverage] - val graph = aaaChildren(0).asInstanceOf[FileStatementCoverage] + aaaChildren.head shouldBe a [FileStatementCoverage] + val graph = aaaChildren.head.asInstanceOf[FileStatementCoverage] graph.name should equal("Graph.scala") graph.statementCount should equal (7) graph.coveredStatementsCount should equal (0) From 188bbf000b92fb35f9072a8ef7167bbbd0ae149b Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Thu, 7 May 2015 19:28:35 +0200 Subject: [PATCH 070/101] Change version to match Sonar API version 4.2 --- plugin/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index 5f4e378..f624e69 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -11,7 +11,7 @@ sonar-scoverage-plugin - 1.1.0 + 4.2.0 sonar-plugin Sonar Scoverage Plugin From fb5268caf6f78adbcd4662fbe1186fce0c129779 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Thu, 7 May 2015 19:35:46 +0200 Subject: [PATCH 071/101] Fix issue #13, https://groups.google.com/forum/#!topic/scala-language/F2e5pZtGDZo --- .../com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala index 26cf8b3..afd2f87 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala @@ -147,7 +147,7 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem val p = fileSystem.predicates() val files = fileSystem.inputFiles(p.and(p.matchesPathPattern("**/" + relativePath), - p.hasLanguage(scala.getKey), p.hasType(InputFile.Type.MAIN))) + p.hasLanguage(scala.getKey), p.hasType(InputFile.Type.MAIN))).toList files.headOption match { case Some(file) => { From abd88d00f5b4348873b5e7a0df1a8dcb4934f3fd Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Thu, 7 May 2015 19:43:07 +0200 Subject: [PATCH 072/101] Plugin version 5.1.1, sonar.version 5.1 --- plugin/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/pom.xml b/plugin/pom.xml index 7c74405..1d3929b 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -11,7 +11,7 @@ sonar-scoverage-plugin - 5.1.1-SNAPSHOT + 5.1.1 sonar-plugin Sonar Scoverage Plugin @@ -69,7 +69,7 @@ - 4.2 + 5.1 scoverage Scoverage From 9a33e8ecf518fff2036dbdb5e631bbcfd1572fe7 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 8 May 2015 12:07:30 +0200 Subject: [PATCH 073/101] Update Scala, Sonar API, fix issues --- plugin/dev.sh | 19 +++ plugin/pom.xml | 4 +- .../scoverage/measure/ScalaMetrics.scala | 6 +- .../scoverage/sensor/ScoverageSensor.scala | 20 +-- .../sensor/ScoverageSensorSpec.scala | 2 - .../scoverage/sensor/TestSensorContext.scala | 116 ++++++++++++------ ...coverageReportConstructingParserSpec.scala | 7 +- .../scoverage/xml/data/XmlReportFile1.scala | 86 ++++++------- 8 files changed, 155 insertions(+), 105 deletions(-) create mode 100755 plugin/dev.sh diff --git a/plugin/dev.sh b/plugin/dev.sh new file mode 100755 index 0000000..b9c0b1a --- /dev/null +++ b/plugin/dev.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +SONAR_HOME=~/bin/sonarqube-4.5.4 +PLUGIN_VERSION=4.5.0 + +mvn install + +PLUGIN_FILE="./target/sonar-scoverage-plugin-$PLUGIN_VERSION.jar" +if [ ! -f $PLUGIN_FILE ]; then + echo "Plugin jar not found! [$PLUGIN_FILE]" + exit 1 +fi + +$SONAR_HOME/bin/linux-x86-64/sonar.sh stop + +rm $SONAR_HOME/extensions/plugins/sonar-scoverage-plugin-* +cp $PLUGIN_FILE $SONAR_HOME/extensions/plugins/sonar-scoverage-plugin-$PLUGIN_VERSION.jar + +$SONAR_HOME/bin/linux-x86-64/sonar.sh start diff --git a/plugin/pom.xml b/plugin/pom.xml index f624e69..c81762a 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -11,7 +11,7 @@ sonar-scoverage-plugin - 4.2.0 + 4.5.0 sonar-plugin Sonar Scoverage Plugin @@ -69,7 +69,7 @@ - 4.2 + 4.5.4 scoverage Scoverage diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala index 9ad2401..4b7434d 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala @@ -30,7 +30,7 @@ import scala.collection.mutable.ListBuffer * @author Rado Buransky */ class ScalaMetrics extends Metrics { - override def getMetrics = ListBuffer(ScalaMetrics.statementCoverage, ScalaMetrics.coveredStatements) + override def getMetrics = ListBuffer(ScalaMetrics.statementCoverage, ScalaMetrics.coveredStatements).toList } object ScalaMetrics { @@ -45,7 +45,7 @@ object ScalaMetrics { .setDomain(CoreMetrics.DOMAIN_TESTS) .setWorstValue(0.0) .setBestValue(100.0) - .create() + .create[java.lang.Double]() lazy val coveredStatements = new Metric.Builder(COVERED_STATEMENTS_KEY, "Covered statements", Metric.ValueType.INT) @@ -54,5 +54,5 @@ object ScalaMetrics { .setQualitative(false) .setDomain(CoreMetrics.DOMAIN_SIZE) .setFormula(new org.sonar.api.measures.SumChildValuesFormula(false)) - .create() + .create[java.lang.Integer]() } \ No newline at end of file diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala index ca5349b..0281217 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala @@ -47,8 +47,7 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem protected val SCOVERAGE_REPORT_PATH_PROPERTY = "sonar.scoverage.reportPath" protected lazy val scoverageReportParser: ScoverageReportParser = XmlScoverageReportParser() - override def shouldExecuteOnProject(project: Project): Boolean = - project.getAnalysisType.isDynamic(true) && fileSystem.languages().contains(scala.getKey) + override def shouldExecuteOnProject(project: Project): Boolean = fileSystem.languages().contains(scala.getKey) override def analyse(project: Project, context: SensorContext) { scoverageReportPath match { @@ -105,7 +104,7 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem case null => log.debug(LogUtil.f("Module has no statement coverage. [" + module.name + "]")) 0 - case moduleCoveredStatementCount: Measure => + case moduleCoveredStatementCount: Measure[_] => log.debug(LogUtil.f("Covered statement count for " + module.name + " module. [" + moduleCoveredStatementCount.getValue + "]")) @@ -120,7 +119,7 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem log.debug(LogUtil.f("Module has no number of statements. [" + module.name + "]")) 0 - case moduleStatementCount: Measure => + case moduleStatementCount: Measure[_] => log.debug(LogUtil.f("Statement count for " + module.name + " module. [" + moduleStatementCount.getValue + "]")) @@ -175,7 +174,8 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem private def saveMeasures(context: SensorContext, resource: Resource, statementCoverage: StatementCoverage) { context.saveMeasure(resource, createStatementCoverage(statementCoverage.rate)) - context.saveMeasure(resource, createStatementCount(statementCoverage.statementCount)) + if (context.getMeasure(CoreMetrics.STATEMENTS) == null) + context.saveMeasure(resource, createStatementCount(statementCoverage.statementCount)) context.saveMeasure(resource, createCoveredStatementCount(statementCoverage.coveredStatementsCount)) log.debug(LogUtil.f("Save measures [" + statementCoverage.rate + ", " + statementCoverage.statementCount + @@ -210,12 +210,14 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem } } - private def createStatementCoverage(rate: Double): Measure = new Measure(ScalaMetrics.statementCoverage, rate) + private def createStatementCoverage[T <: Serializable](rate: Double): Measure[T] = + new Measure[T](ScalaMetrics.statementCoverage, rate) - private def createStatementCount(statements: Int): Measure = new Measure(CoreMetrics.STATEMENTS, statements) + private def createStatementCount[T <: Serializable](statements: Int): Measure[T] = + new Measure(CoreMetrics.STATEMENTS, statements.toDouble, 0) - private def createCoveredStatementCount(coveredStatements: Int): Measure = - new Measure(ScalaMetrics.coveredStatements, coveredStatements); + private def createCoveredStatementCount[T <: Serializable](coveredStatements: Int): Measure[T] = + new Measure(ScalaMetrics.coveredStatements, coveredStatements.toDouble, 0) private def appendFilePath(src: String, name: String) = { val result = src match { diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala index c47151d..1e0c2ec 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala @@ -58,13 +58,11 @@ class ScoverageSensorSpec extends FlatSpec with Matchers with MockitoSugar { protected def checkShouldExecuteOnProject(languages: Iterable[String], expectedResult: Boolean) { // Setup val project = mock[Project] - when(project.getAnalysisType).thenReturn(AnalysisType.DYNAMIC) when(fileSystem.languages()).thenReturn(new util.TreeSet(languages)) // Execute & asser shouldExecuteOnProject(project) should equal(expectedResult) - verify(project, times(1)).getAnalysisType verify(fileSystem, times(1)).languages } diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala index 0adf198..088448e 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala @@ -19,51 +19,87 @@ */ package com.buransky.plugins.scoverage.sensor -import java.lang +import java.lang.Double +import java.util.Date +import java.{io, util} + +import org.sonar.api.batch.fs.{InputFile, InputPath} +import org.sonar.api.batch.{Event, SensorContext} +import org.sonar.api.design.Dependency +import org.sonar.api.measures.{Measure, MeasuresFilter, Metric} +import org.sonar.api.resources.{ProjectLink, Resource} +import org.sonar.api.rules.Violation -import org.sonar.api.batch.SensorContext -import org.sonar.api.batch.fs.InputFile -import org.sonar.api.resources.Resource -import org.sonar.api.measures.{Measure, Metric} import scala.collection.mutable class TestSensorContext extends SensorContext { - private val measures = mutable.Map[String, Measure]() - - def createEvent(x$1: org.sonar.api.resources.Resource,x$2: String,x$3: String,x$4: String,x$5: java.util.Date): org.sonar.api.batch.Event = ??? - def deleteEvent(x$1: org.sonar.api.batch.Event): Unit = ??? - def deleteLink(x$1: String): Unit = ??? - def getChildren(x$1: org.sonar.api.resources.Resource): java.util.Collection[org.sonar.api.resources.Resource] = ??? - def getDependencies(): java.util.Set[org.sonar.api.design.Dependency] = ??? - def getEvents(x$1: org.sonar.api.resources.Resource): java.util.List[org.sonar.api.batch.Event] = ??? - def getIncomingDependencies(x$1: org.sonar.api.resources.Resource): java.util.Collection[org.sonar.api.design.Dependency] = ??? - def getMeasure(x$1: org.sonar.api.resources.Resource,x$2: org.sonar.api.measures.Metric): org.sonar.api.measures.Measure = ??? - def getMeasure(x$1: org.sonar.api.measures.Metric): org.sonar.api.measures.Measure = ??? - def getMeasures[M](x$1: org.sonar.api.resources.Resource,x$2: org.sonar.api.measures.MeasuresFilter[M]): M = ??? - def getMeasures[M](x$1: org.sonar.api.measures.MeasuresFilter[M]): M = ??? - def getOutgoingDependencies(x$1: org.sonar.api.resources.Resource): java.util.Collection[org.sonar.api.design.Dependency] = ??? - def getParent(x$1: org.sonar.api.resources.Resource): org.sonar.api.resources.Resource = ??? - def getResource[R <: org.sonar.api.resources.Resource](x$1: R): R = ??? - def index(x$1: org.sonar.api.resources.Resource,x$2: org.sonar.api.resources.Resource): Boolean = ??? - def index(x$1: org.sonar.api.resources.Resource): Boolean = ??? - def isExcluded(x$1: org.sonar.api.resources.Resource): Boolean = ??? - def isIndexed(x$1: org.sonar.api.resources.Resource,x$2: Boolean): Boolean = ??? - def saveDependency(x$1: org.sonar.api.design.Dependency): org.sonar.api.design.Dependency = ??? - def saveLink(x$1: org.sonar.api.resources.ProjectLink): Unit = ??? - - def saveMeasure(resource: Resource, measure: Measure): Measure = { + + private val measures = mutable.Map[String, Measure[_ <: io.Serializable]]() + + override def saveDependency(dependency: Dependency): Dependency = ??? + + override def isExcluded(reference: Resource): Boolean = ??? + + override def deleteLink(key: String): Unit = ??? + + override def isIndexed(reference: Resource, acceptExcluded: Boolean): Boolean = ??? + + override def saveViolations(violations: util.Collection[Violation]): Unit = ??? + + override def getParent(reference: Resource): Resource = ??? + + override def getOutgoingDependencies(from: Resource): util.Collection[Dependency] = ??? + + override def saveSource(reference: Resource, source: String): Unit = ??? + + override def getMeasures[M](filter: MeasuresFilter[M]): M = ??? + + override def getMeasures[M](resource: Resource, filter: MeasuresFilter[M]): M = ??? + + override def deleteEvent(event: Event): Unit = ??? + + override def saveViolation(violation: Violation, force: Boolean): Unit = ??? + + override def saveViolation(violation: Violation): Unit = ??? + + override def saveResource(resource: Resource): String = ??? + + override def getEvents(resource: Resource): util.List[Event] = ??? + + override def getDependencies: util.Set[Dependency] = ??? + + override def getIncomingDependencies(to: Resource): util.Collection[Dependency] = ??? + + override def index(resource: Resource): Boolean = ??? + + override def index(resource: Resource, parentReference: Resource): Boolean = ??? + + override def saveLink(link: ProjectLink): Unit = ??? + + override def getMeasure[G <: io.Serializable](metric: Metric[G]): Measure[G] = measures.get(metric.getKey).orNull.asInstanceOf[Measure[G]] + + override def getMeasure[G <: io.Serializable](resource: Resource, metric: Metric[G]): Measure[G] = ??? + + override def getChildren(reference: Resource): util.Collection[Resource] = ??? + + override def createEvent(resource: Resource, name: String, description: String, category: String, date: Date): Event = ??? + + override def getResource[R <: Resource](reference: R): R = ??? + + override def getResource(inputPath: InputPath): Resource = ??? + + override def saveMeasure(measure: Measure[_ <: io.Serializable]): Measure[_ <: io.Serializable] = ??? + + override def saveMeasure(metric: Metric[_ <: io.Serializable], value: Double): Measure[_ <: io.Serializable] = ??? + + override def saveMeasure(resource: Resource, metric: Metric[_ <: io.Serializable], value: Double): Measure[_ <: io.Serializable] = ??? + + override def saveMeasure(resource: Resource, measure: Measure[_ <: io.Serializable]): Measure[_ <: io.Serializable] = { measures.put(resource.getKey, measure) measure } - def saveMeasure(x$1: Resource,x$2: Metric,x$3: java.lang.Double): Measure = ??? - def saveMeasure(x$1: org.sonar.api.measures.Metric,x$2: java.lang.Double): org.sonar.api.measures.Measure = ??? - def saveMeasure(x$1: org.sonar.api.measures.Measure): org.sonar.api.measures.Measure = ??? - def saveResource(x$1: org.sonar.api.resources.Resource): String = ??? - def saveSource(x$1: org.sonar.api.resources.Resource,x$2: String): Unit = ??? - def saveViolation(x$1: org.sonar.api.rules.Violation): Unit = ??? - def saveViolation(x$1: org.sonar.api.rules.Violation,x$2: Boolean): Unit = ??? - def saveViolations(x$1: java.util.Collection[org.sonar.api.rules.Violation]): Unit = ??? - override def saveMeasure(p1: InputFile, p2: Metric, p3: lang.Double): Measure = ??? - override def saveMeasure(p1: InputFile, p2: Measure): Measure = ??? -} + override def saveMeasure(inputFile: InputFile, metric: Metric[_ <: io.Serializable], value: Double): Measure[_ <: io.Serializable] = ??? + + override def saveMeasure(inputFile: InputFile, measure: Measure[_ <: io.Serializable]): Measure[_ <: io.Serializable] = ??? +} \ No newline at end of file diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala index bf9b33b..aa4325e 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala @@ -41,12 +41,7 @@ class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { assert(projectCoverage.children.size.toInt === 1) projectCoverage.children.head match { case rootDir: DirectoryStatementCoverage => - assert(rootDir.name == "/") - rootDir.children.head match { - case homeDir: DirectoryStatementCoverage => - assert(homeDir.name == "home") - case other => fail(s"This is not a home statement coverage! [$other]") - } + assert(rootDir.name == "a1b2c3") case other => fail(s"This is not a directory statement coverage! [$other]") } } diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala index 526ca7a..82ec85c 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala @@ -28,13 +28,13 @@ object XmlReportFile1 { | | | + | name="MainClass" filename="/a1b2c3/workspace/sonar-test/src/main/scala/com/rr/test/sonar/MainClass.scala" statement-count="2" statements-invoked="1" statement-rate="50.00" branch-rate="100.00"> | | | | + | package="com.rr.test.sonar" class="MainClass" class-type="Class" top-level-class="MainClass" source="/a1b2c3/workspace/sonar-test/src/main/scala/com/rr/test/sonar/MainClass.scala" method="times" start="161" end="162" line="14" branch="false" invocation-count="0"> | | | @@ -42,7 +42,7 @@ object XmlReportFile1 { | name="com.rr.test.sonar/MainClass/plus" statement-count="1" statements-invoked="1" statement-rate="100.00" branch-rate="100.00"> | | + | package="com.rr.test.sonar" class="MainClass" class-type="Class" top-level-class="MainClass" source="/a1b2c3/workspace/sonar-test/src/main/scala/com/rr/test/sonar/MainClass.scala" method="plus" start="132" end="133" line="12" branch="false" invocation-count="1"> | | | @@ -127,7 +127,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(8, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(8, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | "" |} | @@ -138,7 +138,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(10, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(10, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | p.+("-") |} | @@ -149,25 +149,25 @@ object XmlReportFile1 { | | if ({ - | scoverage.Invoker.invoked(7, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(7, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | p.==("") |}) | { - | scoverage.Invoker.invoked(9, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(9, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | { - | scoverage.Invoker.invoked(8, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(8, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | "" | } | } |else | { - | scoverage.Invoker.invoked(11, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(11, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | { - | scoverage.Invoker.invoked(10, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(10, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | p.+("-") | } | }.+({ - | scoverage.Invoker.invoked(12, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(12, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | StructuredErrorCode.this.name |}) | @@ -186,7 +186,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(2, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(2, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | true |} | @@ -197,7 +197,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(4, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(4, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | StructuredErrorCode.this.parent.is(errorCode) |} | @@ -224,7 +224,7 @@ object XmlReportFile1 { | | scala.this.Predef.println({ - | scoverage.Invoker.invoked(25, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(25, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | ClientError.required |}) | @@ -235,7 +235,7 @@ object XmlReportFile1 { | | scala.this.Predef.println({ - | scoverage.Invoker.invoked(27, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(27, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | ClientError.invalid |}) | @@ -250,7 +250,7 @@ object XmlReportFile1 { | | scala.this.Predef.println({ - | scoverage.Invoker.invoked(30, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(30, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | MySqlError.syntax |}) | @@ -261,7 +261,7 @@ object XmlReportFile1 { | | scala.this.Predef.println({ - | scoverage.Invoker.invoked(32, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(32, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | MyServiceLogicError.logicFailed |}) | @@ -280,7 +280,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(36, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(36, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | scala.this.Predef.println("required") |} | @@ -291,7 +291,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(38, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(38, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | scala.this.Predef.println("invalid") |} | @@ -302,7 +302,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(40, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(40, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | () |} | @@ -317,7 +317,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(43, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(43, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | scala.this.Predef.println("This is a server error") |} | @@ -328,7 +328,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(45, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(45, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | () |} | @@ -431,7 +431,7 @@ object XmlReportFile1 { | | scala.this.Predef.println({ - | scoverage.Invoker.invoked(52, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(52, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | x.isInstanceOf[Serializable] |}) | @@ -517,7 +517,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(8, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(8, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | "" |} | @@ -528,7 +528,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(10, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(10, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | p.+("-") |} | @@ -539,25 +539,25 @@ object XmlReportFile1 { | | if ({ - | scoverage.Invoker.invoked(7, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(7, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | p.==("") |}) | { - | scoverage.Invoker.invoked(9, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(9, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | { - | scoverage.Invoker.invoked(8, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(8, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | "" | } | } |else | { - | scoverage.Invoker.invoked(11, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(11, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | { - | scoverage.Invoker.invoked(10, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(10, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | p.+("-") | } | }.+({ - | scoverage.Invoker.invoked(12, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(12, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | StructuredErrorCode.this.name |}) | @@ -576,7 +576,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(2, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(2, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | true |} | @@ -587,7 +587,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(4, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(4, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | StructuredErrorCode.this.parent.is(errorCode) |} | @@ -614,7 +614,7 @@ object XmlReportFile1 { | | scala.this.Predef.println({ - | scoverage.Invoker.invoked(25, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(25, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | ClientError.required |}) | @@ -625,7 +625,7 @@ object XmlReportFile1 { | | scala.this.Predef.println({ - | scoverage.Invoker.invoked(27, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(27, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | ClientError.invalid |}) | @@ -640,7 +640,7 @@ object XmlReportFile1 { | | scala.this.Predef.println({ - | scoverage.Invoker.invoked(30, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(30, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | MySqlError.syntax |}) | @@ -651,7 +651,7 @@ object XmlReportFile1 { | | scala.this.Predef.println({ - | scoverage.Invoker.invoked(32, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(32, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | MyServiceLogicError.logicFailed |}) | @@ -670,7 +670,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(36, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(36, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | scala.this.Predef.println("required") |} | @@ -681,7 +681,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(38, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(38, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | scala.this.Predef.println("invalid") |} | @@ -692,7 +692,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(40, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(40, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | () |} | @@ -707,7 +707,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(43, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(43, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | scala.this.Predef.println("This is a server error") |} | @@ -718,7 +718,7 @@ object XmlReportFile1 { | | { - | scoverage.Invoker.invoked(45, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(45, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | () |} | @@ -821,7 +821,7 @@ object XmlReportFile1 { | | scala.this.Predef.println({ - | scoverage.Invoker.invoked(52, "/home/rado/workspace/aaa/target/scala-2.10/scoverage.measurement"); + | scoverage.Invoker.invoked(52, "/a1b2c3/workspace/aaa/target/scala-2.10/scoverage.measurement"); | x.isInstanceOf[Serializable] |}) | From a37dc0fe4de7a87aa64f62a0a6c34ac4c4d22410 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 8 May 2015 09:30:38 +0200 Subject: [PATCH 074/101] Upgrade Scala, ScalaTest, Scoverage to the latest versions (cherry picked from commit 604d43d) --- samples/sbt/multi-module/build.sbt | 2 +- samples/sbt/multi-module/module1/build.sbt | 8 +++----- samples/sbt/multi-module/module2/build.sbt | 8 +++----- samples/sbt/multi-module/project/plugins.sbt | 2 +- samples/sbt/multi-module/sonar-project.properties | 4 ++-- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/samples/sbt/multi-module/build.sbt b/samples/sbt/multi-module/build.sbt index 1f1caef..3548db2 100644 --- a/samples/sbt/multi-module/build.sbt +++ b/samples/sbt/multi-module/build.sbt @@ -1,6 +1,6 @@ organization := "com.buransky" -scalaVersion := "2.10.3" +scalaVersion := "2.11.6" lazy val root = project.in(file(".")).aggregate(module1, module2) diff --git a/samples/sbt/multi-module/module1/build.sbt b/samples/sbt/multi-module/module1/build.sbt index e3d30a2..e775d96 100644 --- a/samples/sbt/multi-module/module1/build.sbt +++ b/samples/sbt/multi-module/module1/build.sbt @@ -4,10 +4,8 @@ name := Common.baseName + "-module1" version := Common.version -scalaVersion := "2.10.3" +scalaVersion := "2.11.6" libraryDependencies ++= Seq( - "org.scalatest" %% "scalatest" % "2.0" % "test" -) - -ScoverageSbtPlugin.instrumentSettings \ No newline at end of file + "org.scalatest" %% "scalatest" % "2.2.4" % "test" +) \ No newline at end of file diff --git a/samples/sbt/multi-module/module2/build.sbt b/samples/sbt/multi-module/module2/build.sbt index 3d44cc1..df80a0f 100644 --- a/samples/sbt/multi-module/module2/build.sbt +++ b/samples/sbt/multi-module/module2/build.sbt @@ -4,10 +4,8 @@ name := Common.baseName + "-module2" version := Common.version -scalaVersion := "2.10.3" +scalaVersion := "2.11.6" libraryDependencies ++= Seq( - "org.scalatest" %% "scalatest" % "2.0" % "test" -) - -ScoverageSbtPlugin.instrumentSettings \ No newline at end of file + "org.scalatest" %% "scalatest" % "2.2.4" % "test" +) \ No newline at end of file diff --git a/samples/sbt/multi-module/project/plugins.sbt b/samples/sbt/multi-module/project/plugins.sbt index 046c8d7..6fa98f9 100644 --- a/samples/sbt/multi-module/project/plugins.sbt +++ b/samples/sbt/multi-module/project/plugins.sbt @@ -1,3 +1,3 @@ resolvers += Classpaths.sbtPluginReleases -addSbtPlugin("com.sksamuel.scoverage" %% "sbt-scoverage" % "0.95.7") \ No newline at end of file +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.1.0") \ No newline at end of file diff --git a/samples/sbt/multi-module/sonar-project.properties b/samples/sbt/multi-module/sonar-project.properties index cc12a04..a1de43a 100644 --- a/samples/sbt/multi-module/sonar-project.properties +++ b/samples/sbt/multi-module/sonar-project.properties @@ -8,8 +8,8 @@ sonar.modules=module1,module2 module1.sonar.sources=src/main/scala module1.sonar.tests=src/test/scala -module1.sonar.scoverage.reportPath=target/scala-2.10/scoverage-report/scoverage.xml +module1.sonar.scoverage.reportPath=target/scala-2.11/scoverage-report/scoverage.xml module2.sonar.sources=src/main/scala module2.sonar.tests=src/test/scala -module2.sonar.scoverage.reportPath=target/scala-2.10/scoverage-report/scoverage.xml +module2.sonar.scoverage.reportPath=target/scala-2.11/scoverage-report/scoverage.xml From 583e5ffde66fed88fb0521b11d64573a027b1a5a Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 8 May 2015 11:25:41 +0200 Subject: [PATCH 075/101] Update README.md (cherry picked from commit a9e6a7e) --- README.md | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index aa63ba9..4838f5f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -#Sonar Scoverage Plugin# +#Scoverage Plugin for Sonar 4.2# [![Build Status](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin.png)](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin) @@ -22,17 +22,12 @@ just plain average of coverage rates for sub-projects. ## Requirements ## -- [SonarQube] 4.0 -- [Scoverage] 0.95.7 - -### Support for older version of Sonar 3.5.1 ### - -If you have Sonar 3.5.1, take a look into the [dedicated branch] [Plugin351] or directly -[download binary JAR] [Plugin351Jar]. +- [SonarQube] 4.2 +- [Scoverage] 1.1.0 ## Installation ## -Download and copy [sonar-scoverage-plugin-1.0.1.jar] [PluginJar] to the Sonar plugins directory +Download and copy [sonar-scoverage-plugin-4.2.0.jar] [PluginJar] to the Sonar plugins directory (usually /extensions/plugins). Restart Sonar. ## Configure Sonar runner ## @@ -41,7 +36,7 @@ Set location of the **scoverage.xml** file in the **sonar-project.properties** l root directory: ... - sonar.scoverage.reportPath=target/scala-2.10/scoverage-report/scoverage.xml + sonar.scoverage.reportPath=target/scala-2.11/scoverage-report/scoverage.xml ... ## Run Scoverage and Sonar runner ## @@ -86,9 +81,7 @@ Columns with statement coverage, total number of statements and number of covere Source code markup with covered and uncovered lines: ![Source code markup](/doc/img/04_coverage.png "Source code markup") -[PluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/1.0.2/sonar-scoverage-plugin-1.0.2.jar +[PluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v4.2.0/sonar-scoverage-plugin-4.2.0.jar [SonarQube]: http://www.sonarqube.org/ "SonarQube" [Scoverage]: https://github.com/scoverage/scalac-scoverage-plugin "Scoverage" [sbt-scoverage]: https://github.com/scoverage/sbt-scoverage -[Plugin351]: https://github.com/RadoBuransky/sonar-scoverage-plugin/tree/sonar3.5.1 -[Plugin351Jar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v1.0.2-Sonar3.5.1/sonar-scoverage-plugin-sonar3.5.1-1.0.2.jar From 199f33053ee48f79fe0640efd37b145e7b6366c5 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 8 May 2015 12:17:01 +0200 Subject: [PATCH 076/101] Update README.md --- README.md | 8 ++++---- plugin/README.md | 9 +-------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4838f5f..4181784 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -#Scoverage Plugin for Sonar 4.2# +#Scoverage Plugin for Sonar 4.5# [![Build Status](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin.png)](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin) @@ -22,12 +22,12 @@ just plain average of coverage rates for sub-projects. ## Requirements ## -- [SonarQube] 4.2 +- [SonarQube] 4.5 - [Scoverage] 1.1.0 ## Installation ## -Download and copy [sonar-scoverage-plugin-4.2.0.jar] [PluginJar] to the Sonar plugins directory +Download and copy [sonar-scoverage-plugin-4.5.0.jar] [PluginJar] to the Sonar plugins directory (usually /extensions/plugins). Restart Sonar. ## Configure Sonar runner ## @@ -81,7 +81,7 @@ Columns with statement coverage, total number of statements and number of covere Source code markup with covered and uncovered lines: ![Source code markup](/doc/img/04_coverage.png "Source code markup") -[PluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v4.2.0/sonar-scoverage-plugin-4.2.0.jar +[PluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v4.5.0/sonar-scoverage-plugin-4.5.0.jar [SonarQube]: http://www.sonarqube.org/ "SonarQube" [Scoverage]: https://github.com/scoverage/scalac-scoverage-plugin "Scoverage" [sbt-scoverage]: https://github.com/scoverage/sbt-scoverage diff --git a/plugin/README.md b/plugin/README.md index 0016f5e..8604b3a 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -1,11 +1,4 @@ # Sonar Scoverage Plugin source code # Useful bash script for plugin development to stop Sonar server, build plugin, copy it to Sonar plugin -directory and start Sonar server again: - - /bin/linux-x86-64/sonar.sh stop - - mvn install - cp ./target/sonar-scoverage-plugin-1.0-SNAPSHOT.jar /extensions/plugins/ - - /bin/linux-x86-64/sonar.sh start \ No newline at end of file +directory and start Sonar server again is in the `dev.sh` file. \ No newline at end of file From af20fed4671452e0fb9d5ca6c9d734c45fde50ae Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 8 May 2015 12:21:51 +0200 Subject: [PATCH 077/101] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4181784..e9fdef9 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ root directory: If your project is based on SBT and you're using [Scoverage plugin for SBT] [sbt-scoverage] you can generate the Scoverage report by executing following from command line: - $ sbt clean scoverage:test + $ sbt clean coverage test And then run Sonar runner to upload the report to the Sonar server: From 90d859ebc48fd1d9ab95df1018909df1e41c2b8c Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 8 May 2015 12:34:41 +0200 Subject: [PATCH 078/101] Replace SLF4J with Sonar logging --- .../scoverage/sensor/ScoverageSensor.scala | 4 +-- ...XmlScoverageReportConstructingParser.scala | 16 +++++----- .../xml/XmlScoverageReportParser.scala | 9 +++--- .../scoverage/sensor/TestSensorContext.scala | 29 +++++++++++++++++-- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala index 0281217..bad6f98 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala @@ -26,13 +26,13 @@ import com.buransky.plugins.scoverage.measure.ScalaMetrics import com.buransky.plugins.scoverage.util.LogUtil import com.buransky.plugins.scoverage.xml.XmlScoverageReportParser import com.buransky.plugins.scoverage.{CoveredStatement, DirectoryStatementCoverage, FileStatementCoverage, _} -import org.slf4j.LoggerFactory import org.sonar.api.batch.fs.{FileSystem, InputFile} import org.sonar.api.batch.{CoverageExtension, Sensor, SensorContext} import org.sonar.api.config.Settings import org.sonar.api.measures.{CoreMetrics, CoverageMeasuresBuilder, Measure} import org.sonar.api.resources.{File, Project, Resource} import org.sonar.api.scan.filesystem.PathResolver +import org.sonar.api.utils.log.Loggers import scala.collection.JavaConversions._ @@ -43,7 +43,7 @@ import scala.collection.JavaConversions._ */ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem: FileSystem, scala: Scala) extends Sensor with CoverageExtension { - private val log = LoggerFactory.getLogger(classOf[ScoverageSensor]) + private val log = Loggers.get(classOf[ScoverageSensor]) protected val SCOVERAGE_REPORT_PATH_PROPERTY = "sonar.scoverage.reportPath" protected lazy val scoverageReportParser: ScoverageReportParser = XmlScoverageReportParser() diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala index e427b06..d597aa7 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala @@ -19,15 +19,17 @@ */ package com.buransky.plugins.scoverage.xml +import java.io.File + import com.buransky.plugins.scoverage._ +import com.buransky.plugins.scoverage.util.PathUtil +import org.sonar.api.utils.log.Loggers + +import scala.annotation.tailrec +import scala.collection.mutable import scala.io.Source import scala.xml.parsing.ConstructingParser -import scala.xml.{Text, NamespaceBinding, MetaData} -import org.apache.log4j.Logger -import scala.collection.mutable -import scala.annotation.tailrec -import java.io.File -import com.buransky.plugins.scoverage.util.PathUtil +import scala.xml.{MetaData, NamespaceBinding, Text} /** * Scoverage XML parser based on ConstructingParser provided by standard Scala library. @@ -35,7 +37,7 @@ import com.buransky.plugins.scoverage.util.PathUtil * @author Rado Buransky */ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingParser(source, false) { - private val log = Logger.getLogger(classOf[XmlScoverageReportConstructingParser]) + private val log = Loggers.get(classOf[XmlScoverageReportConstructingParser]) private val CLASS_ELEMENT = "class" private val FILENAME_ATTRIBUTE = "filename" diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala index 59c037c..b5f1c5a 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala @@ -19,10 +19,11 @@ */ package com.buransky.plugins.scoverage.xml -import scala.io.Source -import com.buransky.plugins.scoverage.{ProjectStatementCoverage, ScoverageReportParser, ScoverageException} -import org.apache.log4j.Logger import com.buransky.plugins.scoverage.util.LogUtil +import com.buransky.plugins.scoverage.{ProjectStatementCoverage, ScoverageException, ScoverageReportParser} +import org.sonar.api.utils.log.Loggers + +import scala.io.Source /** * Bridge between parser implementation and coverage provider. @@ -30,7 +31,7 @@ import com.buransky.plugins.scoverage.util.LogUtil * @author Rado Buransky */ class XmlScoverageReportParser extends ScoverageReportParser { - private val log = Logger.getLogger(classOf[XmlScoverageReportParser]) + private val log = Loggers.get(classOf[XmlScoverageReportParser]) def parse(reportFilePath: String): ProjectStatementCoverage = { require(reportFilePath != null) diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala index 088448e..17f7f47 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala @@ -23,8 +23,15 @@ import java.lang.Double import java.util.Date import java.{io, util} -import org.sonar.api.batch.fs.{InputFile, InputPath} -import org.sonar.api.batch.{Event, SensorContext} +import org.sonar.api.batch.fs.{FileSystem, InputFile, InputPath} +import org.sonar.api.batch.rule.ActiveRules +import org.sonar.api.batch.sensor.dependency.NewDependency +import org.sonar.api.batch.sensor.duplication.NewDuplication +import org.sonar.api.batch.sensor.highlighting.NewHighlighting +import org.sonar.api.batch.sensor.issue.NewIssue +import org.sonar.api.batch.sensor.measure.NewMeasure +import org.sonar.api.batch.{AnalysisMode, Event, SensorContext} +import org.sonar.api.config.Settings import org.sonar.api.design.Dependency import org.sonar.api.measures.{Measure, MeasuresFilter, Metric} import org.sonar.api.resources.{ProjectLink, Resource} @@ -102,4 +109,22 @@ class TestSensorContext extends SensorContext { override def saveMeasure(inputFile: InputFile, metric: Metric[_ <: io.Serializable], value: Double): Measure[_ <: io.Serializable] = ??? override def saveMeasure(inputFile: InputFile, measure: Measure[_ <: io.Serializable]): Measure[_ <: io.Serializable] = ??? + + override def newDuplication(): NewDuplication = ??? + + override def activeRules(): ActiveRules = ??? + + override def newHighlighting(): NewHighlighting = ??? + + override def analysisMode(): AnalysisMode = ??? + + override def fileSystem(): FileSystem = ??? + + override def newDependency(): NewDependency = ??? + + override def settings(): Settings = ??? + + override def newMeasure[G <: io.Serializable](): NewMeasure[G] = ??? + + override def newIssue(): NewIssue = ??? } \ No newline at end of file From a485ebbc23093a2e956ef12d5fd60622f1550b1d Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 8 May 2015 13:04:05 +0200 Subject: [PATCH 079/101] Add javax.persistence to compile-time dependencies --- plugin/dev.sh | 4 ++-- plugin/pom.xml | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/plugin/dev.sh b/plugin/dev.sh index b9c0b1a..385c76e 100755 --- a/plugin/dev.sh +++ b/plugin/dev.sh @@ -1,7 +1,7 @@ #!/bin/bash -SONAR_HOME=~/bin/sonarqube-4.5.4 -PLUGIN_VERSION=4.5.0 +SONAR_HOME=~/bin/sonarqube-5.1 +PLUGIN_VERSION=5.1.1 mvn install diff --git a/plugin/pom.xml b/plugin/pom.xml index 1d3929b..73081c3 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -133,6 +133,12 @@ 2.2.1 provided + + org.eclipse.persistence + javax.persistence + 2.1.0 + compile + From fdf41c322dd835e6bfddd5bdaad08d3545762989 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 8 May 2015 13:14:32 +0200 Subject: [PATCH 080/101] Update README.md --- README.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index cfa6d55..b3a11c9 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,14 @@ just plain average of coverage rates for sub-projects. ## Installation ## -Download and copy [sonar-scoverage-plugin-1.0.1.jar] [PluginJar] to the Sonar plugins directory +Download and copy [sonar-scoverage-plugin-5.1.1.jar] [PluginJar] to the Sonar plugins directory (usually /extensions/plugins). Restart Sonar. ### Support for older versions of Sonar ### -- SonarQube 4.2: Install version 1.1.0 [sonar-scoverage-plugin-1.1.0.jar] [Plugin110Jar]. -- SonarQube 4.0: Install version 1.0.2 [sonar-scoverage-plugin-1.0.2.jar] [Plugin102Jar]. -- SonarQube 3.5.1: Take a look into the [dedicated branch] [Plugin351] or directly [download binary JAR] [Plugin351Jar]. +- [SonarQube 4.5] (https://github.com/RadoBuransky/sonar-scoverage-plugin/tree/sonar45) +- [SonarQube 4.2] (https://github.com/RadoBuransky/sonar-scoverage-plugin/tree/sonar45) +- [SonarQube 3.5] (https://github.com/RadoBuransky/sonar-scoverage-plugin/tree/sonar35) ## Configure Sonar runner ## @@ -90,7 +90,7 @@ Source code markup with covered and uncovered lines: ## Changelog ## -### 5.1.0 - 28 Apr 2015 ### +### 5.1.1 - 7 May 2015 ### - Upgrade to SonarQube 5.1 API @@ -98,9 +98,7 @@ Source code markup with covered and uncovered lines: - Upgrade to SonarQube 4.2 API -[LatestPluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v5.1.0-SNAPSHOT/sonar-scoverage-plugin-5.1.0-SNAPSHOT.jar -[Plugin110Jar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/1.1.0/sonar-scoverage-plugin-1.1.0.jar -[Plugin102Jar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/1.0.2/sonar-scoverage-plugin-1.0.2.jar +[LatestPluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v5.1.1/sonar-scoverage-plugin-5.1.1.jar [SonarQube]: http://www.sonarqube.org/ "SonarQube" [Scoverage]: https://github.com/scoverage/scalac-scoverage-plugin "Scoverage" -[sbt-scoverage]: https://github.com/scoverage/sbt-scoverage +[sbt-scoverage]: https://github.com/scoverage/sbt-scoverage \ No newline at end of file From 8114523333d222aceb16546562bb415ad2a27fe8 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 8 May 2015 13:29:30 +0200 Subject: [PATCH 081/101] Update README.md --- README.md | 18 +++++++++--------- doc/img/01.png | Bin 0 -> 68358 bytes doc/img/01_dashboard.png | Bin 51803 -> 0 bytes doc/img/02.png | Bin 0 -> 65058 bytes doc/img/02_detail.png | Bin 59495 -> 0 bytes doc/img/03.png | Bin 0 -> 73068 bytes doc/img/03_columns.png | Bin 54122 -> 0 bytes doc/img/04.png | Bin 0 -> 109527 bytes doc/img/04_coverage.png | Bin 57559 -> 0 bytes 9 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 doc/img/01.png delete mode 100644 doc/img/01_dashboard.png create mode 100644 doc/img/02.png delete mode 100644 doc/img/02_detail.png create mode 100644 doc/img/03.png delete mode 100644 doc/img/03_columns.png create mode 100644 doc/img/04.png delete mode 100644 doc/img/04_coverage.png diff --git a/README.md b/README.md index b3a11c9..ac4f271 100644 --- a/README.md +++ b/README.md @@ -26,17 +26,17 @@ just plain average of coverage rates for sub-projects. - [SonarQube] 5.1 - [Scoverage] 1.1.0 -## Installation ## - -Download and copy [sonar-scoverage-plugin-5.1.1.jar] [PluginJar] to the Sonar plugins directory -(usually /extensions/plugins). Restart Sonar. - ### Support for older versions of Sonar ### - [SonarQube 4.5] (https://github.com/RadoBuransky/sonar-scoverage-plugin/tree/sonar45) - [SonarQube 4.2] (https://github.com/RadoBuransky/sonar-scoverage-plugin/tree/sonar45) - [SonarQube 3.5] (https://github.com/RadoBuransky/sonar-scoverage-plugin/tree/sonar35) +## Installation ## + +Download and copy [sonar-scoverage-plugin-5.1.1.jar] [PluginJar] to the Sonar plugins directory +(usually /extensions/plugins). Restart Sonar. + ## Configure Sonar runner ## Set location of the **scoverage.xml** file in the **sonar-project.properties** located in your project's @@ -77,16 +77,16 @@ Take a look at a sample SBT multi-module project located in this repository in t ## Screenshots ## Project dashboard with Scoverage plugin: -![Project dashboard with Scoverage plugin](/doc/img/01_dashboard.png "Project dashboard with Scoverage plugin") +![Project dashboard with Scoverage plugin](/doc/img/01.png "Project dashboard with Scoverage plugin") Multi-module project overview: -![Multi-module project overview](/doc/img/02_detail.png "Multi-module project overview") +![Multi-module project overview](/doc/img/02.png "Multi-module project overview") Columns with statement coverage, total number of statements and number of covered statements: -![Columns](/doc/img/03_columns.png "Columns") +![Columns](/doc/img/03.png "Columns") Source code markup with covered and uncovered lines: -![Source code markup](/doc/img/04_coverage.png "Source code markup") +![Source code markup](/doc/img/04.png "Source code markup") ## Changelog ## diff --git a/doc/img/01.png b/doc/img/01.png new file mode 100644 index 0000000000000000000000000000000000000000..3a5ff19600bf8dcce08cd6415b36c0f31514a242 GIT binary patch literal 68358 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%0DAIlR% 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 literal 0 HcmV?d00001 diff --git a/doc/img/01_dashboard.png b/doc/img/01_dashboard.png deleted file mode 100644 index 5f1d893b73261044eca7edf22937e1b5aab45d3a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51803 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 L6)6$c|N8#{8}%_= diff --git a/doc/img/02.png b/doc/img/02.png new file mode 100644 index 0000000000000000000000000000000000000000..563cb3518b12f8309a1779ba8489244aa4c50fae GIT binary patch literal 65058 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℞_4@Kgkj5 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≪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 TSWPR#K0zqRD1(b$z5n!I0(VwP diff --git a/doc/img/03.png b/doc/img/03.png new file mode 100644 index 0000000000000000000000000000000000000000..d4b7ba3ae07c7086a08ca29f083fb1046bd110b5 GIT binary patch literal 73068 zcmY&iomN(F8EFwX-pycQSG`F|l| z2cq^fi;rH?5iXlACjRyZ_K%~pRbv{6 zh`rjjq96-?yy&b=aT4*l33(>`Ar2rXJ{CiPjM}XieAg%~R3<;83@S!0Kwm6V)qsoq zt)bZ75RklBcFWcrSLAvym!OR~VNg#sF!zaUI~KiBjG%@_t(bY`BULe16QG^b@%uilf`EW8b@i2>w=&qS$YH~s|!QagqQIj}%zfKMQvBkZ5v}djv z+N78r=JM~#!>S>u_hIk-3`N!hP1o26Jn2b;>W-t;^Y`Fbe0+RFE_N;dAck0gYsF0zWk5!X+G_j&Y>2cQ8$pf9$z9aZKtGVpz{<~(ef zPlyg0+X&JR6}(Tg@cU2> zkCc?ug(rgQ|7J*x4G)gV@HjBh($X?IDtUFZibD#CD|+K)z88Rhx^iHh$z)LAo6pM6 zavQn>ixvHuvn|O{A&&2=z)Q^14W6o=L6bSEzRdsN7X~-m@CUR4x)E723?Rkz;(mwm zy)OvDIKJX;i?lc;Z#c3RC24vn!QxuyofZ3H9ZU*8aKlHgn5(VO;g`Q7yW1~tynQt# z31_CBL<%_u?}yF5f9-{TDJ1Qy9=UJrUFGD@$rCf^6k9yMR(|={A^v6tW7QGw+zr6am~yW=+E3vM_V2b< zGCMnG85}RZND6A64{gMz{4V{?}6%vi1SF1>C+KAEgH`1$Nn78(&F#4Q@Nh z&Ao)94^sop_7%bQtm?_`FSkEN<3m8t7Bz%|z3clUkE)VdD9h&-eFW_}j|^ zceZ>8iB-m@e|K;eX@d-Z5VmXk8W8*`AaDM}EvM817rtjy@-WK9k#Bs)H?8EvELUTg z$*jIksfnyF4P}-Qj78`5#MbFsW)6KMvyB1Q?lp;Dje>#SZrem-#q*<3qR(sdCKcqR zG7-TX<>8NT@#uOMKLQzI0q&&x)!}nRQW@H(sPPgwqhub zbD--k^z*kAqBM@S5z@lvBro^TkxzxsGPcSH8DZ8@s~O3@?>F$5)|*EFu-7;HW7_wm z7^eEv8A_&IA#R^sLeUlD#C5DO%6K*`i3vUWhHxLMq5)}f13E{??)B*kYx_x?eMj%Z zneC~4>u6n+2h`BzR z48u!VPB#B#0G~!!@L_OFk)g8h=!?x(%c*)k?q^HYph*9jLe}k1`_bWK`uxJepa-Nd z3RQ#%tQTP`U057LlrZdxGmnBGgkR(0C*?LOWpmAun}ELhi%;IdeQ8+WQTguCj>st| zzrUrJP;M|&vwTtP1E-oG8Y)8zI*K|T`(!_PP|ZKrVv=k0^Uvo8B<4+?ytByMH?PO0 zBmbtL)Ly;?b{4*3wWQpNK_CXPk>$|ln8jq)HwWJUdZb@xdu1=$1HVXWk@bDc30ArH zotdFxX9#%zqC|7N!gO~3!s(peaNJBOF(?D=Yyj@{lp#UVdjo4_29cK?=(n_7qgvP= z7Juxgc9tO4%tmyDOsIBBzUV$FY%2(l6L_rxrm-gnVfkJil*G z=IwpjHvf!{jy^s43u^0Ro&ozuTyj>PE_2i#xS2uq>TWp5E*vXfz_+B?<&NmSP5S?}ts-X_U zR(AZ{X5(!RN{;V;_pi-L(~8kz>~z~G>HSMA)|ifCdb>JPlmA$5{2jdrL_=Vcn`g5* z_I=Ugz(-H)hUoL>EJbFK!V(v_9u{bQ#2;De-DYm;b&E7O-gd3WX3|Gkp~wAunnCW= zFdUu9V)UMHFk?If2Hl6<>UA^lX7O96MEIf02_0hiSAjYP|8b09>K_--o9i*>&)I*E zF4d)gPQhH;JQ(_Q+KAo4T*hpH1$6OwgA(B%uNR4h*hWzVxgT#a25*JriSO^(ct8C) zOOQ|H=M4-DX5SYot-bwM)jhP;u#^Pc&H1J z^T>7!8;v(S0F3mwJ@=d#+4Y$0kJcNpn$fF2+o~i=d~#GUobZm`PO8~xdO@e$8qR&a z>bwLWR;$4L3-Te=8ttb6c*Zb-chD=}%_M7b@_JJv^$;}O4z*HAOZ#S55CGWN*wk>C zjrek8?}I$aoBpj!*KTCn;J*?gC{CY_3P%n6yLJycRLJo^_jcjQ-n4zFv&rPJ>ALLM zaKHHz85ITk>%~jJU*zwC(zn*;=BZ512ci$R@dHP2{S=cs*_8xxnSq}_iQIPHOLN~P zR8^OXq!NezKKvSn^yZAp5uk zT&5G)Z}vKcCTf!53ElNu+y|!v`~iuc)jAV3?f#5|!l@m7dev?^+H|ge?>f%=BL3<_ zTzI$M*qIWI^Yl=-2bbc@#)FEB+)yqKxGv8zgQ>1wSc99+iazT22{_hPwtP_1s=ZVFa)*0}o)!JOs1Ow^A=619)P!qyQ>oajHL;@ZS4 zVmp6aWqTZD6UH9>L>4S^M%RA?#RHI;|7h8^i@jZ&i^iZ3a@p(m4h|Md#0G_iDj33H z!}+v)Z1jErt?S-7(wj#&E@!5O*xfTn-ELYAsQ)$uZJMY>Tz9U4jzm6>@q(|r{h+9o zNcm0R?N-pIW%0o< z%j)WSQnF?G0+_FMIG@xEO8n^2vxk5H3YDVo!5Xo8yX=M&A?g8OcyLx}INDVI)*R3J z;-)WD3y(;S4FY9ELT$521&_)cnlc zdHM5N-02X#lF)vFQbunz^?CCBzzGdZzYW8?(O_;I5$2j`2)oZ*YGxdNYP5R{0Y8-; z1pq?UQ>>{#bD6HU&zr4xS)J24eA2VlP8bIOc-rWR9<0m1wS0@3+A19G2O)t^u@n`B z5GeCj$D<3Gw?JSh633fOf5x32e)~y`8+(F;2a$jMdE`h22^@P5tJ!oBmF3=WG{9uD z)|8c%g)%E*sg5|E?(i^Inp;2>QQ%jA0)BhLC`d_(jPM?i1Hq8msMlM>@9BDWNA?Nf zpN0`Os8RHoN`@2)pjE4>s;ZhCAID`ioowA!z3DFJ(ntxJ`ass;h412n$oepHycR{ zPo>Ny)9Q^8-aq&|EhTkle74)x{E4f?TJ1r-9Odp-g3)4KZP5=Aje7&KxP2aK#W^K2 z2SgjPt$K^S#>rAgAQBU_iMlsP~lWUcy>RfHa++P0A z+DhyIK*^c$C-bag&< zM(YGu@$X|ll8mNk=I@8G;CEa*3kwQ>z6)2{*+0N|stArA89>PEdOrW*7IEYrB`muv zNm5aNB556^z;zi7oh5IUE8zX{vi&i~CbyJ~_U{(wqTOF$yr8*TT3RX@9_HL~Ga3v- zec1N>XfXNqPn8U6uRI=iLy-x1*bD}O%9Tp5vmQVt2P!J+rsp6#I|uNKSmq2AQ&mOIgK^spklECfK^$X+_AKkU~>fjq~GtdcVW&!BCv z{E00QRG3BLQ0u$NbW`%!yeJ2BkG$Xu z$m$mS0KkM$hppZ>*@M;CnCgB=r}tKiidj;8{$3v`tc5HYNqsPsVJQ9s5}Va@JxLcqaj)KxMM6^%ww>>1PG&Mdi4`X6_SHroG?K5vN%;!d@ggf<)+%dY< z`$<^#?C74zV6e^&1b4*ae{WUq6rsjQWMH5^Q5~JzKCW zFK_Hw%xW-4TAu}vCKE3U>n6-Id|FL1^5g0HcQ)@2`D`8x0924&+H+yjsqcIg|Hab_ zB=LBCywYm%8esAOq@&_KKs}1YX2RCm8r)GR*T?B1ZGL$kd;t}lQPunjD19FkbmaOxpZI>f=m`t=PcZg+oi3E8rKPpB zaA<}i<8#%4YRC~n&#I!LL1UJwH0Hvlrlg~lX*|(jIyj&J?%K^;sZ?$RTSnKTib_w= zuA1x$J)Y7RKiimZ>+}Y0pv=n9O$y5}4wP)y?k`G zTI-aUnO~z;QtD28$SBf3mdUb8894aYmOB1I^}q4AyOlp z_(?9;q~-d8UL2z+|M;p3r$_?-SX=tqz(Hr)Krmtse!Gx%(}Rj1GSbLv*N!#_`UoBq zVa6fVS|jq1>6QyjEhmNXZ( z2$rEnQDA2;B^0dIa?zc3j-a1^zMhd~9X=0HMgTuo7A7$r*om9xnLsUzOKBl=A}NaP z+ROzB6kB`ly-f37dfhFmfp$j*&)sx6$f#@EuD8aSV{%~CrH@8a`(Kjia7qO*?I|3MDS-KnGe9sHwN&5g@pxW6_u$R{?3Mm zYmm|C7#QC76Tcl!Wtz3FLv4*XoPYkt=K%5*Q2FnAvDU0wdDrI;0W`Vg?6ocHdS2A} zzFBE#tUlkJfvWxhB1%~B;IbWVQCQ9*wCTPpuiUL}7(`sM?GhKKKniD#E_mhonkP=C zt-srP^52^8zS}Cl{s`68ZrKuk{IO zRf_t49KtBxGS-MD=Ap)XB&8W^aQg`+>H5lO)HDu%3)6OnCfk#)k%Df;tgdR3-n4Pe zI9?S@6!WnX(P(oO-FT(ZoSKPN(QGt9C(}%VE}uJ#Qd+77dqcE~D+Md#M8TCnWHz=H zfo*U>LP2thc4oT6Lin(eU)yAG`{884nEP;nc4mEjMbS8zA}2O0)}h8+WMjkN_-3?= zAZxn21|~H!X;>{p)}meSRlbpAFYL$ z?PifDGaL#40B{w|F{&G zni?r*BBRL8x7eJy50rX?aqHgNvD?wuy>k1#=XrYPk%Q&jfn=KVUpEfx6BIoAa{nhf zDhgp&Xt3w}+&br`O@kpI-&^2v&-aNHu;=@booxWY4Y@qoF?(DQ1zfs+J=WyeI2m+0 zlxH6Th?|>zV2B((Fi``}~d(@7%pvFMA{$ zYot5j!|O*Cf7?jKJzcPn+3*Aj(Xju*Z-1ixuOun_yp*4@Qh0vL(nCFAwWyg(=c)}XsajWDh+}g$NPcGC}*DS^RmyM5`52WlJr41{NC# z&8_kNSd~N4QQx@3ae8;%w%G)O$V)r3>pbW?)WJ|$dQW`DuAOMZ@$Ibr)0q)A8WvWT z#y}Az8f#~4yl;ck`YE6d6U+z<8IMEzWTg+$`XJ6F>S{9mhv36TBU|-n*0En~-ha(H zU3Qv}Y8Sxrs;6A!6hFLQ?oXYKb*TTVs;1F!P#tcMXM=(uQzwcY3S`0zXiyOlfhHut z?LcV9omm2&5vYdXVNs;6Rjr|M1cTSrlEx;Wg`oInSo zgAJ&h($RP-?$_r^Etx>wx=%c()#Kn}<^BY`wnERLC&Gu;eBBI_=+cX7fHoN*z9cEC zDnxI^P*mlTei-kB3D4hk+;8uijlJb+*AnslWqVmjKKQ2?3r1qK?GdN$bcb@)^rA&t zz43G9Tsro0ug}o}GIa0LRRYN_*2ds>(WU;!79K5$O-CnV<05%VTbc+y!rE1QLGxdE zH{X^e6_l&$WmZG(phXwlRmJ{Gahy;yH@hCW#FJa8J%#hWsAhkF$jmYirtX zFUU+^C6ul)E02b$L-(>S$^Qpmij~e^WibZKQc{9?(y%z@Gb{{Y6!j(V>wT%z2@J1f zB-b(sV0h>1xnw(e>P94QL&XRm{VwFKIA8CzjU-F@8;clwa9!iG_-4z5nQmZw&nu7>H_>CK+T3<(Fy~# zx*QHdgjnBYc&AP+S)kD)bQ8=F`W{QrorhCCS{n65+N;_xzA6V*aZ*@1FRsO(M5`># zgJTcgay4cclnhViFQ0p_>35&)O%6ddPU=Lvkwh{VUeJwXV_9L@=x7*g>VJT(5bVH7 zmwr0~1KsiAHsa^lq&BQ5<`IYbdL4oH{f`Cj1mpBdILqk2V*WJ?GN|x#@}p5776=v7 zsnesG?~;E^6z(E*je9A+m|*UWNz1`{lr9=3k<4;S2=;!BmTQ zHImNMx7tP*LqvKJ1uP?5KBq~fbab)^eyC8LNr1BKw%Z@#hODH2$*lpEkeY9ox_RTy zD6#7s=Yhxq&N|Z4_Hj@t{KXl0#_k4WfMkx&Rby7?#<$Og)O}x=N<^ZkzNa!-W}Upl zKvF1Ydr;PW=U{j<`J!of`7Gq(EIqBoZL~n%!R@!`tJIJh@))1XH*?-z1NE z7Gd1$?E0^Z3K47fe@BOc%@+==84Bo|2w#FhCggL!PU5AID~W6KP;||$;2nq1SD2C( zCXKjG{w8BsoBTbs?*stVN`Hq&qP*Jg9VxI2{DXf3IW8{8|wG{FqaqG zy}e4+WdF_X@gl#F>Z^kJkjw{evOtDG+T)`e01FTQ8B$nt_lLZ*B1>ht0w5HFhP7Ea z=fYklf*1`(-5il5B|g1bwixOoZyzo5iXgm?6(rxxUly0!s%_J>z!sM}B^^#3-{JlF zI?_u5+lq`ep-3>8!T$FCDYjO=w2*52?1uU-{;E4;Sgk|;-N!$Q5l62blcfhR_L?DL z=`qtfR^#u1B>_ci$M(316ZGhQ!yFVc96}~yV68<52NLO12+&*4B2eL}<{WE8Jq%U; z1Kf`zm;|l&^b`xHa_BXCwM*@Put{so=_X=li@GJ5fC2^naWyrGW0=u5imO$) z^T(wvm-Fb}T~4`*S?po#X;p0{{MY!BHooG8!AvlH>8W%lso_*&k^g^!YV5nayWO4- z@={V@zifFwMIa0|s4IIO>uC`EpB4Z;kc2w9Ljlg87Z4&rPL?>mX)6O z@r~{#*J^a+omp`FEBTJ0-<+x`sk%eHRSApwID8EIJb}p^Nl)spbF!lJ6-gM_1*j?Y zoMU}_HY7&{3ju)S0Xtbv1i+>;=ikgoNHAnc4^#Y)#t>?|zIyUvle=7R#Zoq)x|jpzT;iC0>PKG<8>K=rf0M^i%Fpeon`P>ZaN@N#9`bXN`6>B_zm(?+6LBPV_a` zQ-!>%3IqHg5s1mzv68#d_rE;aeIe^vilZGlt%RfIja7yM@544gNGovm=*}5+{vQOc z2L;0IK^F>~u_t6$RqU9|zvw74!AgF|DBQuat)8t_ucW>rpPy-}TCKOvr3eupV=uI( zWrQ;&XOlpjMz+8(@7g9%Us~lv%!h}1DDnWWytwLxvP$_%wPY2F0!G|mMe*MX57fdr zz&OLkm0mVo;W99pur# z#6A>R@;&HdUs7IbSZZ1bs%-K5~~9#esEy1RRtF zdl?!(J!P0)llg3umK}ljUUYx*YcByW92{HlbcM%;2e=yMD=UQTQ&x6B@dvvSk z1CIR&(LXp^i)S+`_9yO6^Ri~79jnjHGu1Zo-c*7=orNbQ1#30`MYYUBL3mb5N(!jj zIzB!|im({0xD~y&pZKjlTL%>hR}$UhavRM_n9BslM`%N?97|}l zYoFd^NzgTMD}&M1Jed;sQhqOxW3{yPDJgEk3XRuMhnYaVzTV9EzCRs-VAL#r&lX}> z#~j>pBO!V2a^tXUYeee*HxbY zur}=)EIqpS!%RB_Xi;NhGBE{Eyj<&WIRi05bMx~zSo^&4RZmV0gb=v|e3Z<$B+zJ8PhjVbr=pKm&AP64*v4MO1{s;pfX;9DU2r|@aRM&&Bi;h?uepy`Ii9A;$u?a@ z>ykQOacLvH>X!v8XH)YuwU+*Yg0KLubU%OtXkJCtSrSdl3Uc0 zb1un0DZps@LWpELEvYRotYv4b%vjE(%@NYjP0qCO>Ff&%Wfk@qE=IUCPfr-Y{S%04 zMAks<0|OQSa-@IICFw8B9|lzAWnQ|VeV?RIXp6^E#7!$>&m|8xpczumlHMTjv|qWh zqQ?D)YUZXuok)Q1EJX{GUUR=zcsJ@92N3u;cPgA+srD`?D4=Pz6?3e)j(gU?E8Swi z-unl10<8yTDQ*t9Q7n_|A zYyPT}@O~(mMXiWCt+gp!cYsCf7a&SlLV>aSHeuo*A~yDNsd}B@$A-gRH0K=K#my6F zsXJo@p2G~Vb7z*jJ%7ZGkp6I?>RgZY0Rx0VoQM(0&g=Q=LzQOp^E7X%w(89DP-t~+ znd`5mtc`G0=sr_+?#N|TwT04|2A001R5kW_Dem_9Dri3v%o;Zv6?Yv?+lXC!#<7{# zDYu206xt%QCeu2XGu5Oep7bCxB~kv_(|?^A1)tYd*3-IPeT_C8C4oa}NPS}jX5a6V z0?Yedc(3WET_CB>}M|KS7b=6x2R~gaJL?E=$IheqtKuzPZ>Y zq<=`r%PgFH9dJG49g6ygry1+Pgaw>;eTDXUdc#_7toGdb+Jg@6Ur2u~YPP z4CD3V^%T*`Voy&)Cfl6u#2oI93K|+Y?R<>*FuY!t$yZ@CRm=lF~WSc?vIOf@4$BVhu5 z%*91!WZae&mS?!xKom7ODWz2y_A-Gw%UB9C^H0kpiH5!BOaeL>plQ}{Vsu(TqALtc)L2GJax%re*2oVqwj@jpaKMj*ol$UJzm{rUTjx?A%v^LJE^9-Un?-z` zbvh&AXuNImHB>R$tf8Xb^yV;P4Vn@(hM^-k)pqeOXx=kvwWOtVio+QKLlEWcqjR;! zBO~toQ(24rwE1t@m9OO1ts6VC=h&{PrInm0U)l%)L}+NF zYnMYqLt!?)P@?x^*LD3yP0FJ9eaZYYG&hY~$=nv&Z&P4<aU3lQk2H5)3Y~g9_V~n~5VV6eFIz6P*qC9X;jn zd%^|$0wrn?ePlEevv^S}BQKBXz2d*qmy0fDv)(dg&IUfbHxe5g5pnIo5BPy8z*g&a zcQiO^d3o{qr9lwU=JQ|AY1!m>(na2*gxyoAwr`%_p8TLzYqeheedRV?wm@*u-r`V6 z5ZFlM%e<(rrrPBdQXNxGJJSqsjXNogRKIh(Uk8|+V464a(wPFo)^aoSlZ_eqHC4yy z2dpepEgiv2J05r9TDCqtQ0K|yGjzELW9Jpr4a@|5HT7(ijd^qTeUY^DQ@_-e%s6N> z=-vCr2b7`xnAhm43dh0lH_suNSfQc)Y)rwvCX3@mA*>L{Bl+1l+w0d+1tfA%VCS{8 z&wi4+<<>!q-aXgMpI)k^)zCU)qd**!WNC;oogVy^K$Oa=p~Q-IeAsb%+K^!SCx3%P zd?(}V3@`hWD2t+OQ)7NfMN3yx3bU3C4oBd;u98Zpdol@$;Jj?0_FRf7JF9hLcS}cw zvzXJ8Xrr|_m(yEI6MCb+RZVS8jc#iM2Y-@930RiPnY8p-8>E(aMtXJan(o!JrZ{>h z!6p=-kI%yVa{v@{IU1UaLD@=<8s~F0v^^UK9>RF)M6Vf}vvO~t>BX6eD^qDv2?aL( z{aS{pSb3{iT|bY5q2gP|b%UmrEu0Obp4tStTxMAnIV{u9w1jwQ0RO7=AKmfp2AAW0 z*!;=ulNX5?@$lp0)t=!BkC+0ej)xA+H=D_MH(8K9ox{1a1p!&-MIXU?&#{EvXMxDz zIoBl<>|Au`C4cSfi&WVS5eUxA>d8hN@J9?>EGRIKP{E$LiDgNJ1m`P0biJ{|6fqRc zpCTFmtag`z$S)9u89?myd{PRkSzWEHtb|5RoI5}i|5OIM3<{(R1eHzm@9F&C(&})F zi;DyTj*xX}ZDK6&D08gO=AzD^xQPm$IOYtZVxFI$t!moEy|rI-J)^GjMMDvw1Bqb+ zVUQ2GsDGsv{DL_XcznI721WEz3r*)liLYNr`x&LJqmz`yy-{}Vy-u->inU0;q=mtX z3&iwc)5hB7b|kzm%6VgXDXCJtaAaK#LYDKr%-BU4TO4cbRq{9@`4+4mBsRN(`Ejf+!0eqyC zyCVkuq0DW;u z317MmcX3-d!O2YIWo6*JB7ZNl_BWZS1ZT6B986)5t|^6*b?YjhLIOKHOe=8-?DZXO zAi+Wihw_4bN);VD#|grJos@lw^TpcYu6@w`;xj2M_=p22hpJ3NQqU+T)n=6QK|YsJ z1&%=i@j4x_e6^iVhk{k=SC>%;l@rCU57T=El&kA0PPIY`nolOlYH8Mhac_uq=pm}6 z-b{fc_DqM|F(np25Ea?q-#$udM^7Mm2gtXf*k&(WU+(JhFKV0xMalxX?)o5O-_>UP zx*MDY$rvY;n%c-IeHUJFl1*HyifMKfnNpN12`$fKTrs_*)0F1-@`_68e~t(nm|C7! z;@vn#L_}UUR1p|^lt!CMeVr@0)%cvLW>cfCqMfg88Z`+LaBGVuXB#Du5gb?5I{7iv zFU~~bJI|#hq7ouK3n@g)jX zEFYxIv$A>!3sAA<{&@^`3-*l#8-Y|J20e8FS{7QwuYSnT!p3G%Ug9gBTh5VtPD}(^ z>Y^il>aWUFo|*3;s7EoS)DVG+g$AVAYi??C<0m8_pkRmW+qS&o&;X(K`W#k4%tjm|B65YKgX92UPN$dFYOnGHD%VgtdoR-o z2biKz`K(7`A&gWl4#rOv4TyDF6v*y((XCBJ6U4Jwq65~tO~>MLC|w=npUc@p7G@$Saye|) zKq96eWnsJb>#RIWGi<62lr+@JLU6;VZa(+to?AXI7yO?0U%!5JcqD4y z>@g6>DQt*`_sxIS8o}Wca89nVms5_@oaZK*vWO5f9;^h1JM)Mhm+TG`3u1^;v=q~| zl$A7-avvtMl$Q+kl6?+4z{~^3tO5NqnKN~!)dU1UpDEM4j+4t6i~Y#l9-J%<5%{Mv zA*EHs zk=9v887E%jBeVJwH#xjEK4qekspc^JY9S>nG~+Dmr(#{GWCQ=GW4WzwCa2*}ny8T&Xy>4AbRYlctDLMx#NMUSd!=RZXkjmEUvil*1 zh?k-UAHz-mwjYMb3j~>E%a>}$YGafMCY`5Ui1q4IKSZLu3LaQWVgvdbm;l{L z`dD3WQIR;3etjyYUEsrjC2KCJ$+So7xZt($cXEPxp}2|R;8)u9Uk7i zz>zVrsFtjPh6Dxsii$4OuZY{`Kg>^e@S~$+gzT7>wHs}92qub**FfjkoCD~=+MX*s zR2w=ga-broCbQ1SrC=x!74D~*)#;$^7nIq}ZJ5;)qISy46%*=sEn7JOBPs66DIN%i z1r<*4Y<>2*Bh_9II_cLqYa#i|kZ1nfpJNV~KHQPS9?nWgPc(sU$=~XSdXS}zUq}0* z4vD@=}N0*4?s$RB*(!ik8;LCDiA9e83sHucglXx&miHV&xp(k+oV}LWdxcS8yI0 zPwn79Kop-#p1TNsb1aX@^1%L~^_MOOijRmId~-Tn1%o4njCOK5%T6Yt&hU~YHo z8pKn}v<+Y5g(`Kk(&|RcL$cXEj-^F*HqM3Rr6u-;C~{U#8Qrr{bylTla+TuRTA4;1 zf@T$$DrR<3SyV^)RAYiN9%~CH4^zd9j?-l|u8< zj;w8=*9TcI_$?(RS%g#$I#`xWLk`7FC25qJyH~B5@yu)LeLP8jVMfLy{YjS>$(*Mw zS*WSj=o~O;)R=;vR$x5xIqB1H@wqwlSmU9cANUo=Jf+%MQBh?nshe@7J@qd&n*aIH ztLMHz&n@TS$;+T2M>TFJv&|a?&4_Yv1q)27emb+&F86U5kMgoJ_T{AX?B~E%uAuMz zy4^$12?emzy)}L?(S6w~m}UU@5i;eSS=HKX^$@&v*MnLNAP?mZ6s%R1otp}jOXuaX zd0fs{>JvN=igPU#7--j1BH)ctRZ~X|qd=A1=8tVe07NGJS#30Pak<-QcM6m{CuWt= zCv7?!&|RZV+3{UP}IBa)?rL?!^Kmv zn8{Q7Nz0@PyVj&TyiAU}GyCyNjOJfkDTNpi4?f9V%r0x-vs-Zhg$H))@YAUgPDYw@ z;ecLSg~^N^5VF?3{_C6>4+h9(!>_wD0Dkq^BlT5<^$q)rYTv;81Z?f^8?F=RNP!j1 zly3D@rANztFTb{3co4_K5|E9#S(#Pn;iSPnfjuQca>BWV48~t(9>EK zR0l#eT!>^wPp@4Z&;dpbru)Xl;kHgcxfksu$J`~^nhTHs^FVI$a6}{<#~-y`aL>lN$i0usDODOYXd7(t2re?^=9q2;|b0$L(u`PDnuLt+!-!Px4 zUAP*3KoV*;+-MS|#7L0)GWI+gPNvmgluID^A+fU&o1hk_g+PJmkyGxkB;Ib1&&YyR zH8t`wG83b1I(-3Y!P;~Xl5m&e{Z8;?5?5tlI~fF_03alxASWkRs`(AisCeZTsf3t7 zNC6%Cb@v(B%F>dLFqRmWZ$=5d7}S4zfP8C<%Vv!&<_OAPqF``&IylboOGHG}M(2aC zXeiSB*;~p?4v&lF!MF-MCf{5R*hK1w)pXVdh-GnN%Gp6S(|g#c=m^sA&A>>?{cD)F z$$boWg!V9rVf$~PU85i`Ufn3j)=yxzXfTV;= zRW53FH?;rsx_|Q>Im`rC^ve`WBl$0Z9cO4q4 zQpI21`V<))drv}|Jd{uev6%)8OE9>>BC#`OK6!y~ZqKu4d%mM9x8z66)n7D$iNLXQ69iF!umsT%gp;lA&V(!l4a~eRQ;|hw_q5On~34IjCBch$S@9 z;MkYu4~pv|v9NCnk_Lex1_mUfIWF%)CGkbrhfpzCZ$pj#md|h`SzLv0OR7EY|y5^6#ZrKa8$;`VW&kF#3A)X zqKX6R-|vku;7Jcm0=kB%Mx|ER4b(x5IaakWoPQzoh<@yc(?yxidQ<=#Aq2z> z=7KbvMa$aKr3_G|Mp5zSNU0E%`)QbYOnrc3r62Goi`J2ismsZEg;d;la$S zQ$+2Gj>*vN5k*>m=`)>gc5=SQYxCEBtP-@>s^^}+97Y#1A3(cPKtc5)33q*JCm=SE;-AnJ_ZzU&4URgMFgB8 z5C47WWklMQPPy$$V^okQCJnZpSky*)lcmL36OIathp>ZeFS4#dqo@^kHRG1DJ zU;~Pn;@=Z2q~p)2oO=U(aitiDF^sE(QhcksbQ#DoB_A&fZ_IhD-Ub0Iq1+6dCRsKEc9&L-jN?r09AM>;VyGTQt7 zTU$QW52}|1%gBqd$46X{fQ#6cJV~o&`2X22_(rYrUCRL+Gjh=P?@)1Fd85KMNQurSW`>HFh-(Bb|BTjLD zf%tehFVLmIZ|YlZ|Lb0VHSVIbjdc5|9LnVQ>S4v+o z3@0Ya!2m||a0JfWnaaFu_@9H?aDW=$D2P&SN+xx>!rv3tYa5)!4i+#B7Bfmk$y4Js zaIjHK@fJxb-oDF2)q>Bw5_T!iNvbdeMq|J2|D_~>T%q%m187s>M?=yqTsE{yUnjWqgq9_6~PQ>LOZI>Gahiu!g_IxG9MByJ7qM!uOLPxnoul#{3;W^siK*|4^xr* zA>wuF*@$N1kFJV>Ww(^6FYkJkBKEI)$p5!zD7R$>u^Bh-r`p|TGsYPHIXnMl!72yB zAmTW-+WprMNuHbb4Qe)>(@8G2>u6FgoeTmR{Q_3I&br^$4xCG^WS9d7=N*`FU_P|1 zSvSU?aQydxgZm=W|4Mv&wpi~o^_RxUoD~4DputdO&r2U9(X4<;80SmVow*4b1k^(R zITAmN+ZM)V%>Q=divh8Ly1IOZr^JMQCm%hXA;kOOo-2Wvs|Vj;62s3Z%rVi?ou_R_ zT_qJAwpI_-zHtI&Ltn!3ACcd4Ek2zVlRXybGM4{`m;nx>TYvxD(dIWv^-zdu0ki;| z(kFt6yY^i?LOJw8BmiZtPk87SPT-8+oYQP&-oszYRl3a+#z$9T4%KKAqr#0j4`x*_J6U1P4L##dWpKt-WJTAD$~GegH%6k;T-PfG}VB^uL*r@0HTNjNT zth=85)qj`G-^>+1)umM~j;r;hQ|6&1-M+)9{|v11&f0DTLjd%#F|Vt=?(3-tFxwC# zuMis^W4=oN6$O}?J0+cD0)T~W=Qrh}*TT7rlcJ(ngU5#?SbzU*r8}m^Rfp_l5C3qS zq|-;EAtna_a~$RlaLoqV(**p(V-8T+4e!kw|Euc~bZ!GlN&zRVnz#w#D&5^@0@d?R zgZK{Kd~1Sy_lh!+9Mul&rF-A#on@>I0yMHDnm$} zFPzXhm`AHqbs~c;|C5{b{iNaZX$>n{4>NymGTB;|JrJ3tX?z-ZDd*H=XEZ8YYi zBe2}{QqN#!^Q!lQwQjyuliZL13(I}~Pc9f$9l2hEasJiQ`h-^FpIJdUT^ys6;z|Pg>UEn{3e6c z5?=`gUpmpVH*e*qOZQXujjr?q_M*e6mtiP=wD*gO4Y(d;{c-rSFLgJLhwGAaITB)* z>Axb!xpv;Zz8ZyM&FvMx-7@s{YyqZ}{+@3#4l;7SoqE_eFB{kP4-O&_yy=94It-P~ zS)N+Gikkxx-a%GY}V3OkA!jE2T(bjRk!l<-d09l zn_v3}JE^-Zue~8i_?*3qcGgb!5ixaz^1>Lr-zo(^oFReL@iw#qz8*J+$aJ@vFu=Fk z)hFl0x@B1l6=LCUR}bE|he_Gl>zxvJb93q;KmRN(S$|4>We)K7_3a2V8`#H=e=1Zl zj6F)>B#ul!ckEqNAoS%{7@b|%yJG@v?q?>CyYZ8c(H<^<&Tp%=Smz&x>;niPSMt8q z-#;)-JYx1Zgn@n6=fpIv=JsvrSSjw!<{5OiA(cS79&Ko%5!GNS50sE(I4W^+!^-q-K%1ynHv*oUx*!EfjN7k6pWd*(?*Fu=#mC!B_?{ENMzoilNO zIr@1!J}gwshc9Q|=eAP6?K&ak1RqtP;=Gb3dqt(@YRV5cA9h0jatNO4JWwB*&_lW9 zCf0#pm-X)Fgf;~5&*~gCrJQUihz7QEHUGrW<#lV3 zvf4$Lq|bepB)1rsr03oBmUQX=wz2|6=!>jPN0hPrbEp1kBzWZVZc0GOZ;R+SA1W}} zAO_3U#%iPRO59~7q1of8*rce~?QJXH1ok0@wpBSO_c^S{6G>GvzPZ+;tAJmkcT$r1qI_Hy{7RZh*vPw3CFad-B93v8^LyLjh z2+mfOA@oy!1OPlS2xil_srV>}+5<}Y*O!n3np{thL5rUl_jhC;ed2V|FyQx0i%lLH6?pS)MQdZ>b6R_k ze4sHghShCFhqOoE8sB9FY=*qGSl*|+yZd&Fc}&o1I$7gVcjg@J3}nsi`g4;v_7U*= zMpWLk?A2*q)8)+nS~jGP>aKM0zl!1RyW2o`NYh1hEe!!nG&cAiD#VMR0`qGRh}XF~ z0Ti1IU#peFPO9e%`N?!7cDk&$zswZ!G$6%7{|6`6l1_8c#)BUKpd?(JC(6GDw3STm zKvhtv58*`myGjk1^x&XpKWYW?SBW^hFSgy3 zV%OVOuMoM__8|h#lgY366r3v6yeZGTwJD`OqX$eW0=LiO>HskPOOVmWvSw|2Zy}P1 zui8>cykRJR-iJ0T;8>7X2cj32$(w)gK1d(kHVc{9&vESMk7Wb{PfwBzfmQDfl6}e$ zIdW_UpI>EfZ^xZ}x+)rNcYbcN?jK1>5lIK?w{w3!kt%Xw?*GWpo@{4P!PVxW?4%o= z`NlMmil;;;nr%OT)GkJI{$)8gaDO?xq~lOir;*Kq#q3} z*p&M4WeABh4)}awwp(#FBxrhe<6LP|*?8wr?RSESA2LQ#=E1j{q?-IOpMoyv9U{lF zulvyMZ;tndo>Ns@t^~G-93mc;S25nf{m{5&3_N?kF>SOSHqSdRrTL{=IT;87Aw=Cy zW3+>PF~(S$XA_jQOdF@PFk~{Y78i5g-&uyFZ+G6brwkhBWtJ6qKgct{11pK>Pli`z z+T=Cyv?KWq;*&=$@(=n|gSVP#2Qa16?krRHh<_3iH20A>KPzR_20|c)UL40{pz9UP z+xy{Y!doe6n|-g3F6e5K%&cP#TGq75=;u3oC8o1qH<~1Eog!Opb@8*sIHrrOSgKXQ z*RPN+PjQ&j1OT0ys~PWT-@by#7>ZoG5Zeq0y{L;!Gb08~wfH-p5pxq}`VV5=p`6VIjN%|T!Rg~MOXK_a=Fl!Aeoq^M7%lk5UlcV`xvr`i6y zbykem`lh8kMN2AarzP~8Tu#+CF9+Q+y{e1Hi4L(nYdV9T9?AZC`{tLdYv&uEk~u&q zpdLoK!1KkD$q{|31@8lk_e)*}k-*_io_Fwdi&LEb%Iidwp%$7dwi@*gC0yB!_x!o+ zVk1-LArt?55c>OJxwlCnj7^5H7y4zPL5!Cz?^x~qc^r!6YP*p|J}RISS1diQG9J{%>DHSU# zD;Fr29;w>dRjQZ20|ZKcqM!Wg%mM5b^`tz12Pz&PZmA}A!NrncihH{h3k zGq*Su;KgfE@}yw8eisrTfZoA%GgtBUids6ozaYh>?qEvsw=hB4%NifP;)`*TaK&Op zZB0Q0MxJC>Ya}#Zp|gRssf!slFc=+mC_AQz7u0B*ZS8lwsLKG~vk|%+p>EosBvp_7 zD{@KFeyWQ8H2hB8iarotoseEF!B zLjoX+-qwBAr!-pJFx)*W@3y#~Y`Xq@=|lNpbRfeo6*KA#Hx=rYg5TA5Oh&ObnU&lU zpOnh^neUIZ;L}#rW}yv(qx#zQs}0mviPMvi%+4yOv;!L}?j6N#`{XO{Mm0@uyIf@^ zeae^U7lQd_$LHB`8+S|7t^%2DE2&)%tZIL#AX61M;+HkRBOEG)wO3Fu-5!Uk-9T^9 z?5GlZop@A7XI99&C&H~IbkB-I8h(!$nv&&g*-DfYcKKC|cF|z`?&yne(qJ$+-nFD6 zud^=#{q=>&H)yGm+q>!>!&B>pCX!CJbwojVWiewYQ_mwETy!ht$A zy!VDm)e@-II~YhC5z}b4c#ZD@chd7Rq6(v`5*cC*`vl*JNfjLBsKoCbj!@{%GvkfnQs%-9#3oqhkZz zgI>_|IJRwT?)dJ{V{5vJROcXfcjt|^&JGo2m2~O4Q0G_xLC9EM3oHYm~ZUM;eS_);39c<=o#1Tjkd98`4*E z%ZZn&mG4?cZfC!Zf>xeVzAi#XBO_8tNk^lswRekbL zis85|I7~Xg}R;3yGM9Q4)r?7Q1b%5^@ybE?SltBN>y+7|RJ=eJGU z$1YVZQ!Xt~wo+A11e5W@rl7&j&?HH#DJyUJOBX6Dk>L+r9RKIq+A^l)P1-20?&@_S z08BqEIs`oF)N0aWMV21>+#U~)jJQmsXJzT@>obBM(JPl;gSW;Ho9gN5frrEd-<2%r zp@CWo1ID#LAec%YFy`=CvBeG-WwcqeZIUkZ-u#MPrQ2#;tEpbCzdAD`nuG{=d3o*Z z?97mVHZd`AcL#4u8>ODh+TFxIA)RUjnD6z_pTxo)y0UON@R`9h`{c2wg9AQTdwwvJ z)~X8wEJQ(*D`N~m0(l(dvtEQG(M#6WPODQzEZbZ=i0Mi$O$pEd30_(Bd(lmt8Z6Ml zkl1wqK+?}HN$J{~3#OpT+XadJOe(J&y;r2fgR4MEr#zR60;`L@qh4EPc2awNNnzH8 zbhO11A_gMPPONnMU3RViUQ9jAUGfd*@h^T}pA@^8Wd~y#@+3Y8s7NK;W^e4KBP=MQQp~Ph94#-6oAO7635%WhxmcEM%~zHwuL| z6Iqi0z&Dsi7UeGrQ)l}4yrm|t7DuI!L^`Y@qFy^m;6-hX*S&_TTzSo)i>x4NoLy<% z1OTND6|h$HKIRNJ5QsP%+z8B}DFqm7FM@=oHgwEHDW_{Vx48*Y9ot<*Px&M#VfemvTo zDX|cfYSnJ6?`Au!nk?2yuWjv`g`CmhFzF6;#+xh)`gI--wJ^f$D0rlXBm2?lSb2NI z_afItZ-16s410*b{zzbg#@jI!3GjeDg#;WSEnO!PTr7PLMW%U)4xdiXFJuH)K=Pe7 zvL2qNV(KZT&Q;6``ak}Z`s+>WdfOMCQIe0c$sM|FvSeO2vxPEm7Q($#8v0t7mw8{_ zU2FwJVs_RT8vX5lp&)xK40yW}V?MzyTwENF?sIW=h6|39E}SXz0&PH&0uqQExSW(A z784UIEG~u;l9GaNw3a8uii8jTW8J7+nh09h;y)$}k^@6SVZKYMf(I){`l78ncxU8* zw1NT?1EEev5kf(aQ%6h7J!r?vIu4y;Ne0^5v`Nw(j2x6%#{*kt|uWrbqvbHa@1LOv?MLw3eN|DB`p+mSD@@9;}%RrTq-+GL2C!+xp%mwxvdG?MsOc%F1L>MzC} z7&Gm;s!K$y(sBm1Id?htN<$Z|7E1%O_-JdwkU#Rv8L$I$g;jg@QXAHIno=XShhyS# zJQK{qj=B{es~foA>ezwZ&h<`qcLZI1f8DX;c`UnMZlM|sbOIf$V(Ba|bvT2)BIjT9 zcF4+JY?}%Z%$KIDNPog)k$unDN;{l=o1UN6UM`Z1HDkY%!N!>_%wjy5|Cl+lMRdb$ zRMV1kMEsI)JP$FSBIJ1XAA^JDv$N0_hFh>jCg%Q_>ysJ>B|N6KR9^4}i z33Ags)a=Jgpw*|s`)ot&yGngdY7sdApvHApV|Pvil1j75Y<}}&VModI5!-cUZDdij zH#C({rG(Ed6%gHLUf*htF6G@C=7S4!y61}u zb2_mkFq&e;JKt&QcN2=K=x;rb#sTaLya{#HU*zthabFe_Z|tD_pni6`H}Gj6I6 zoQC~exP^l`W0U6p^eO%knX-i@70Le7BUsuGT1R4DA<}QtPmS7gnT?wCf^QEK3|Q|A z%OC=^2dUtU`$9j^W6^#3bO~lOLqbAkrdn*^GxADGO3KO*fXmCv1=}WFMvnl*oIHcN zx;kB%Z56{>O8sv1Ce!>O7a=DckTYdd3P5La^()EXG_hklt@LyH! zW=ZYEenaB~4m}^~N(H544|MshSYN4dz;T4z(XmBFlO zTk`{A^hI2p3mfV%@8`yrA=`3bQk+J7&SFjJHxdZ+6IDttEW`TLpn+Syn8hXQNRD~s z#|qO$AABlVg*^W&+M1+l3xcIM*7r&Y=(MnYou8VC0x5sEqQ+HU%Vc`jx_p1n<;bddJM9ae&vAyBt|H6|CrN#CBn&>pMYo2Jt zu#aqd%1O?Q{Kj8-T~nKiKLhQnn|F37a?F^mQGXqCZ1~R3A?N$X?|O$Y_Dzz2a!^rO z(wE6}Mk`WVT}G&k_E^uP{DhX(ObPE;&kjQ+nh{4`Rs94Qpl4&NBC({?&aMD#=!?%m zwbpXn+SO=7RJ*8`?qQg~jHjiKe_C930>x)U=h z7(fKd;Myl;gDQ{n4*9ORQ`DhD)b~XZ+)F(rm3Chj7HXlI#+O9Qw4EVzXg_$Uh-Dt` zO4LxVvQ08EmVz!D9zK5QuS-y_TUrw;P^O)ejcZhnMQ*>hkiR z{AU)Q*ZIb0fq)fqcrgtPY|3c}5?b1_^70XWn9RJ^R$RX8P7Pg{7RRqM5@KShW4qu+ zN4J-?{6@Xo?F(YR8W24!3!KXaiFKljUU?kWX4DiJpDlSAfIu;-LPe&&8p*F^d|%%K zLTEhs8W+dGKQSp49oAdm>(1q)W1wZ0wens0F~fsEAGCF;o(By_cV%u#O3`UX%Gja$ zX;}UP>Jj3m;ed30339I1$ZdHB@9YmZ9(=m~WobYu`)N)m-Qo=`D*RG)ywjYaO`bJm zpchc!_4P~wpn&*4s2m)T5Wp^Da3;zKB(M>oA8T3<5jc}+*Ed}-?my_5v18*x1eag5 z^rJxxby0ow@DxTSed(i|hyB;sT1Q5hwWn1Z6REse0!D-(06>>i;nPnMcs1w2jb2Zi z6BbrMrjZ&y+hM5+jP!Ci5HXrmcVkriC2sRG0fVL=KH5q7s^c*rJ`}PgRl>cjapfBE z`+&@Tq{8JZLB#*K0CAa(Z40gz4x3ZgntFD{$n}FqAHAdOuQ|GzvBueLcuSc?3hN{} zd=La4ms)ekB)E;bUw92 z-)#RSp44@akueZjmB%V5{@hute6j0}Of<{C#S$`5G~j+B=|`8eRXIbbS8G*^Gixw4 zulIs^V8S_(V}90a_&n-76!x$svM2v#M#I6A&JD|y2ME0BwoiJS*8|`l=UXvLH`}qo z%)bf0o%O#tv9WMvM96nP$;L_iXnT+DWHVyb*Eiw{09zHxC$CyR#*A6;qF=W25g#XL zZEorn7e$M-R3?UpS<=U5jJdNXn81sUF8}%>%CYjRO734uN=j8xA8;FE!ix+{wU}_E z7vE148YH$+s|FHJR^M3OQWHY}i~Az7{XL}k`A)o!q=iP1Vtw65G$qOWQz4WkBP;hXnP%wd(p z0)dQ6BO{YSvqvvaSZ!?yy}QOkI6VN!=OucXG@vHT%ZLOaZy_2N~d{;0xc#!DNG<-p+gk$x$Uz;~We4WZ8*3MPGI zmL@aX$D4KHspCq!rnWLoGe8luoN-BYQCxXKjMVD8S~{`o!Au^*Qf-c@fn3@L97L18 z7=m?*?q;RGZasV$32l?h&tx|@YJ<+R8VBZJ<;!G4YO&{5>b`hqQC6;`75(KWdMUpf zbF;xJKlY>2m)~P{VI?KzXPx5lnHG&JXtUv8rdBee!7u`~u~k}qtt$<&2BLA(rz#Bq zn5N^PN!TpxV3D&?)Xo-^Dw`YT+-coQY1w~|iA}={p;$zgp-aop1}>V<97N6ZlXd95 zn8TWcEX60eyAOR4m@M~D-vJjD0-rgjo>t>TtQ7Qe1Y1b55tF$nJL=0UkByCHRNbl+ zQ0p7;wrrK`hF?|KIqJtsj;6*QsX7ZdjhOw-qZQ8jd@l-Jh#B0+-tjlx8*sui_dM%A z68kenXSTtflfxl0dJ;NMtUZp96DwlbP41RyCb`JC88S%@tA7JLUQy71z|*TSUUR43 z)(crt(f)>TP5L=dd$Yc-4(Kstrq^kzXm3yFq5LlS5mR2wA4&*B^|ULCx_-@AH#)aw zNFV&ZOvrxz3yK)90(o5!-A;yPSi@+TPh*+ z*2QJOj(02z(FALT;y5o;&33>j+z+U~ggTro=K}^65Vb}FO!p7~;+nr_?duz6v26A? zO69~eowK5G2^LP`r5df$n-{s>?&6a;N+wR^<+IiHfV;I9pPBBbyD*7gAT4~o^NjD( z1Z!I%ppEZkjfpEphaO7<$HUN&q-CJZ)XYq2b2o7Y2GEFmG;K-lfr5nm2f`QtHBp<3 z#)Xrl_~Bc=MMaH{jO6DTm)tnea)kv)2zZd9h3f`!0)g8tk`#HS{5KJXpd6~o@&U(AP?vd~@m%-rtvoFiNNk|olVjyh|qPku$R=r6*4Fo#H#X)-c01I8?o10eQt zn)YK!d6hMBfUr^>tbUBy_7%w1*7X2m#m>+<%SP?_p?vGcw56}&R0&GisgPiTJ!HUG zN{d02&Z@+$aBSo+=AgN8d2LT{2Y- zA(pCY>+13~x}3&nW@lpayWv4YUbqU6zal>wy~o&TFEG7r~ zqMrMVG*^IKQ}uR}4;3o#Cv!H#((^|mKGbi;sXhUd{@L#*&b)sjo4ddcmXby<9gEOD%xmD`6UN(2XRLUgk&?lFb(cal%6>oqZKbPk@2`2g4 z>};WWIdw&fkFvJ5H8W0Kef{0>;^e^ueK`Y9fiJA>d*Ni?wJecbK%r> z1A>sI>CS4037`(BXWq(Fq&oH62tkX>{9}Qfm+L)7F^JE@1v{>jPQHsA$h(!lSovY5 z?MPrztI0Yh$&asCeo343+m=Iy0xhK=t5-FpaihsB+5(azonOoSzm$dkiVdBN$L*eC zuA4;@ai>F<;E<4FhVLZbhSgJ~;wj*IEq&we2|$Pdz@;IzuhoPL-ne=Ib@WI{Nvm2O zXk^{Ac)y}hOF4tea{oR)+EI8Zf#c^ea*8#Ph1>IwH(bhRVveWIr#qu#;=om> zlt*N@zgjS3hArG<*o6w*#D3SPpHUc7WB#Qx1qnS*GymrcFqc5~ozIHyUzIKrly8fB z#6EI_|D`Q!3RpGJelkb@e{mLufkpb|2P zxS9`E?HJ~UtiDHfwN-dTF8f_v+1~s@H^h4rMEZ3KWiv-SDJX^h&yX+HA(v!X<3gHY zUV}IR|egpc8jt z*i7KwuQ-=n>A&VUNRt??=Vbvm_(> zECHdyF@xa&E@GnRA;W@Uo2~~JXX1S{P}pYXGA;V@p`G<&rxzp}`*!L9lQXQI9`fh! zrOrB91CL7YLzh&O2=nd&4u0EMzcw5ri!fCY*>LtK4%I4H!&j@xj{J%)Vwy>D&@_@a z{PLGDl`D~B^Y6hfeuzg9Zjpruk@;PqICq@b4fSz9jN4-50lRog#(LYns0Sd`F=vLMsALzypO%rg7^Ud>VDJT zBMxQwU)*!TMCp^ws$^VuNtPo63bi{dDjT(LDnZ&{+Aoq?-CB2BhLJKsj{;#osdb0u z>6eR^{>D^ii))0^>R&v8d-et8$TJPsljxTtpT3^b84BRe>1inC+P|)u!oI~>4!1=+ zUo6lr2}PD|?YG+W*JPf?4pCH%6F0X>z&t(`k{eT}?)@Jfn`_8{K6EW9Rkg}U?o zitxpqXYXO46o0xV!tMktG~-$3))Lz$(LwMp)bG-n%U5o`)2dWQKc`Gxyu_J6A*nN4 z#+%)$dkHrwemtz>V6Wy^$2qI0c^QGiv}RzDy&Kv#T|9zjUmovJN+ynlh+F+_K;zYp zJp2HtkIhxcl={Tqlv!8)1ZPTeZA*55$3aQLwE0p4bMhhewk=JjzemU*J2Rz5y_O?r zdR|pYsknJ5lbmcCH_TAc%KJ+vstQQmK9~a8vTAb`jlX@g3#0yP+FmAKpuMVIx$pOnGpRhOW|zKiD-&bO|^va z=Et#X0H1ghhfn-IgUwxQL`2%|nVfr$OIECp$?9OU#8|3v6t1ckWn5)Xw;KBatgN4( zy1eh<;IZgRfs$H=6>*KW90eNI>tRbhGZdIQ+2Eo6Y{1x zVrt&ky1Xp5#o&(1qSzFgo~xW!b`hIbweW_ifTD`WP2l55EUj(ud1#D?knyJLs;W># zW3&JJ6ecS9H}w+QVz}T{QawoR;%Sl_($t{-5MU2`79kr{aH^gT$g$E<7n`VAMQYcm z%3(FByW648@#KcyIv_Rd`Z#kTCsQ4wFw6|=bOZCH63z#B5!?PUw#QdO*!1(e(0ARg zk6=CU1~zk*;cI$&6XlKn)j_RJ7A|3^gMdRFqjvoc<#=V0$=ih(>+37s3cOWlRWr^7 zC6Qg7n+-gy;3y4>+-gcVEZdsV{qD@p_Y`#ptzOc@Sie^>m4&D3)`myLu*Vs-K^t7| zNr+BtwIKyR^x6ZtSIK4U!&AdWhrd@6JRsxLjfdW6z*o>=%r$Doo!P)s;uY@{77)U3 zb|$JZ9BX<=Zcclf>Bt7ywZ_F`V z5yrc&be?(5u`Kwwpa2`G#9&3qs36yM>%kh!O5ux7u%nJDU{eRZOKc5K0>PKWqksYR zjy>gZP$JRrx%uj<^ez68so9QyVq%oPNdkl&yGrvskD6Zjef~o3bR3jW|pJuWqXK4=2EuDVdXbj82Gah=CPeV^AL7p-`gSjDq7>Cy0q zwasUll|g>$0X{^SrEXQ)W?CbIDlRwxCM_u)`C1C&3G;$4s?5vfV$iR3i-g|TE8|5d zL15PGo#yg{>s7MFB}Cw{KwV>wM8$&3r>roo4{7A@-Yx&)e_G##0v6;LB}SjSD}V_*-NZN&~+%fJbY&zJgf8alk# z#y!bWcRl%BJ~*Ia)c^|v!?)F9VmaYgxfF|UYvl%_gWcw^C`7`sO=+oOSzG022TwU0 zyK3$pUlWs8A|e>W6LmYfkKD}x3VF>ru+y@K>d1HzB9*FlD5hn>QBhfNN= zOuttAJ_W^o_ZutQim!ZWr{;@S-o?N!@lxffIZUTTKxi^DdrI({j$#4o6kJppMn-8c z41t2zkxHHS(v?n7qTo6mc>;fmY1XtIF^J3S&vZ+96t4IzcH#arFl4)W+#*dgC0r_|JjT`$Y0La~PecIhZ_Kt~4C zEW&daW7kB1cH8?|hYFKR$P{K-D<-Y&OX^=68rf$f0XFSF0AP&32)%Yf8 zE0z9t(tm@Q!%usXF#Eup0;kn)1oc^%Z#@>qDDPo3MkfRbN9{0%6n!HyS=7afZDXh# zXXSad5_MzE^5BDJ96e&rXQ68}qv<^j2kzZ4uY_czqm6F{{Vpqk026iRW8dO?BLR&~ zOps17`RpLj&9aXIE5BF1X#899ig?Uu)M=sPx2wnLy34{zkD)Ajqdn`(+0qd)r#ll< zdz)ol0QEI7=j$7HC3#jWi;=lMUI|Af6PI(9nBYdOg_wbgSTsMo0cq}Oy=;)^cc!p5 zC^^4zXm_7Dde+HOZvMo=lVQf6fD~tmBqz3QG+3cW=k6U& z!9d$+&wEAKZ!Dc?8GUlY{%6anGJu$ATLjczG>!$t%V%B_7e7o#RV2&~XehQnQDr z2^3Y-wRKdj6DX*4wl3AGG8D2$H4<1IOB?Y1KBUeBvkanFk0i0Omzl>_)gMmWoll8l zC#OP6nKv!?SNZ@&xF)J2`bq6Y*(6?i88^z&G+wLwLx~;G5BQo_saccP6z68C!}(`r zp75!u!S1u%v2p>w?9~PualVpi;fSUtwkV-0sFvTX!F}XkgoEAxO3a9SzWc}xpq#N1MwN#OtChMD^DtCaA9wJ_6`z^cFEmZ9oM#h!c(K)5 z_%pjCxPtb(`R}thW5}rsM`OaU{POOE>*WT25BT9nz{+VgOT6E!-@B98CBf^!hHi*R-sM!*|ABBB#<#4%4lfBk{Gf5(IV z@a_-6T)24bCu6)e-s{!9fBL5TNc?7X4sv8fwma^3BeKU!5UROkSC@`B#-D|jT)Wcv zi`3^`p1t0FBVWFLE%y1e>tgiQo2vj)BrQ9*_F5-cx4SYg*B>Z$qpN-{>m&3)4?hh3 z#)zy%YW!rg(|*)USYmU4-#LcVkUjt2G~Q_>%{N^3k4M*lmqNkS){2QLvp)2Yv|=xh zWCzQgUC?qJ)*#}~3aBcbPyC`dtOM!gG#~13*>F9FjqlvJ%w@~?QbD)(@oHX(-UZnl zFpsijdC^YDg6@GH8TjMuuj;C~e01$wMj>+54AslILQKf4OAqbWtWxm!zQ4uC#dvO~ z3cp$4_h04{s&Yc0;?dzlhlYWh2Z0I-%hJ*TEgozp4V-iJmu}6`8G}!wmgGWHj-{}X zI;U&6vY%y9{Nn$*`meMUeCC>-*?wq%1_GVFw4~=B_(Fs+>K-TKZx1+DJO)X0wv%OI zpR92bK%7p5W4A^lQz@XB9^#NdRIKi`ha`8iRP6=#Td}B*Q9^O~QTn@eRBut;%oZT( z@>cIL5p#-qv9OGGA;nhZUy*V7$&Hx|rCvmQqO~+BV-bHD#%>uSjl%BB->w#Kxq(yY zA^i}lbZ6p<N+mr21%8E@mMT!VNl<~loPLKxahCkcVEKuprQgg*&zXD>gz&FfuUF!q5L2dOUGZR zC{)AuV`*ac*a9jZBMjqY;c+7QPI;7M@(z(In=HL~eUN}vLZFc{T(a@S{W`nvj9q=pr|(ZEBNR7h|csQvezaf)8*~nf&e9z8fs0 zpIMCLakJrR+(Tl+M3W2k8KfzYFTueg>b;fIFfYlKKry&VUCP8b#GWxc{Jm-lq#STD6bp}g{uAVQHRR8&kDg}%~_-^vmF*~dANn1%}&WCfT@ z+uox|{}m1=Fcex}QT20GS$%N(+KNVGHKmivC9pZmmC0nN-Oc%Z!;PH)ir12-L#t2p>w8y^+}jDWeams{s$1pN?(BG3<^*UZ zdza>6GdQW>ecMQ_UtfRYF(x~D)Hj%?e>d>S{pJ?4gkPi+#pB_Jv~%oSFhsGlgtiaRQ@?Ly9djW^ zf^!||(>fn#g6X#6X?kOerqjKyr>K=7;1P$B*oAh?40v*SF*J3w?C82inPCItF%G4- z4=;;($r0UaIPs3eLiRj8B4@bWPvb0t1L-{Of$P4guVVv92`KaKLf1@L`F@<%N!#kn z2M!IKvPgJb6&|3L1KV@qgJW4VGkiBIjA(JoP%pAu#$(5JYvL$-FEBmJq?$b+X5PU~ zx?tXa9({Pd0qGU$C_a|=ReJ3$?$NdjsD~vW0j8?pS>J2N6n}sD00=t;% zIc`QnjgY-$cDo;Gn_fDyFc2yk_TWF)*|_2NX*?3(ekj-+IZ2;jR;jJYJF)?sc$$fvl&%&x%Z*3cQ`X6t59^{2hKKn3{e zYy;uCzD;bZtTV1kJN&lKk+1I>zrvCCILI%gr>`{MI$pP#;j$pHh}9v&+x4Gk)6VYQ z+!t|X(pdQi`Fm93hZzdM0^(8R8`wBPP|K+-v}z9SpuFz|6%KHn|Jr zKyZnR_b~mLkh|da*5M)O_2{sL6=C;%r8By@S&G4*f7#l}WgGGs*X5MYZnM;@Gfx1K zv>A0R?^J7?*D2q$N9&Os780ICzu6V1@)H=j7V%#<`JeY?BF`g>m!o28YNM5Vj5;0% z_6R42Kk2&=UB5pM`Ti3%5M&ABI{*3MzsBJDgD0&THqrp0kN^tUIG3Ax)csm5yH&2$ zKSVfr#!ax6GIO>_y@=9`*bCJ{uB{R4P2+c7$~2xhdq2Qm_`4{`YH*>#kP}xD(ISQ^bNpwi+68QIgTI5> zWku|1|Q402b&r^e1{`X&K_mNigvH+7TyJtfog+p_9RnGmmX!oin&7oQ9NWg50N9 zRwNS{PKG6&{mAl7vdwR|M@JBOmd#1lL*K?ezen8pW4Irx?sHT{cOhdG)2&bS{DsMJ zFbM|-&+ueUpOc$_d@!c`3j^S>?rB=`)KCUbamhK9JpIzUB@(v8=z+{lP;@%7isbv# z^d|*5XT!Shsx6LF6i$`?Y2nwgJ=y$b!Xo_n4s#ErR(+UsiJY~)ON9A zWvtw$Pq@)T2uY3A7JA>kslVvdd&qh$WSUR zx?GO0&n?yDuxYa=t{28cpo!EQ9f0mpymeyScBvyfi3Y0Sz0G=s#AsB?L+Z z4LC_~urZKzI($H6;fGt$w}loMn8Uh}eA>cqD)lB#>x z6p2Ymnf*L;v*-Uf9LJin&Z`-f;*r$uA-WyR)<-brJnbLpr7 z+aN<)V@h}%8+sgGkHWQi(&4pSM!35C7R$fFI<#g_*PwLjJY|M8VZ)Wya z)Z!6o>05O+t<~Ok2q0?7;nliv^}a0_0bru9u*DA!d`I(XvvN8+F)`84M32PBrA}|y zDmUIEnOR%aN9LAj{xMvmgJboS0ZC=#KqY`gyl0Qrcc{GD9Cba5;&e&;l>6<89Oh~0 znD_(okI+)3EgY(|7qA*q$|8uw$$Tlba=AHm?zfq-5ekeD(ti3ZCgw@6+tWK7K%tK1 z;khXoYIMa74FbIaX6$>swU6=U<$*|92-%hwX4zpfoR=>O;(q!>$;^q+-9_VlL#R=F zV?66SO4=AQ_N=gI?fGXWnPZNbIrJINbC3JpcA~-f%zZu* zIy$roa3&e#eos~pS*{Nq@#RhiVGeup%W7RVt%Ozw_$>AXD)(Z_R_T9T-hI?w@WGWs zrFzntR2037CV5>acdEyX<&?WLdndi*gCfrXqx@qof0o#YStce{6_u|(1RoImrFcax zA@A=oNKz`a-W=8N#s$59hv28>D1J0(SG^ZNq^{sV{j6o{8ZM_#;wtM+xF$Os6BO zu`p_hzwQuPG^8)#pQoR{YyE7H5)#5P^7=JaOPVHs&w0A5p@}CHsXX-Ly6 zdWJD9lk$dn<}zf-NBNYZR{Xy^zqj&kv+G*iQo=wxU{ze7sb8$`N>99;rv294 zeQ>LI{KNz>{S>{>Aep$f_iM7fCp>wq+xIwSId)>c#@PUT9VEZKxJ7T&Of-LhVLX+J zAJ>=giuvwTdj3JD2_>oU`+k1T#%mNAy6)q7!rb_{Z}!A*F6LnfP-}EH#Vl(-j0?GD zS_>4Bz4xnF(5)D@(c8C9OMlpb&_7IQ<(Z`$_)h}7o=#6l8U1X3_=(fjnP6srK}0t~ z$ZQ_zK3Rl&GMb*_TJf}tzu6Q*s^WZvX;kV!t-7>*e2rcl^ZTeDUO~IvA7y6q26&3$ z#i1C!(PCl7;85H}1-PFq+2Vb^eWS+wJo`*(OP8ikbxng=NJqnOL{|#vgrHJunSH`D%%DCJ|DTNhV+_fJ2cDwx(W?1p& zvLFe=BTQHrITT(t<2$ylzVxQnu7na^=&+hel~7~i{Pj6p>$OkajEyH^!@u+AGh_U^peHg* zi4f0nOyQ=yveDF{TGg!eUQj8#yD(v-yc!51Qxfc>!FXtLb!uzoE0waRXqe72N^g** zJ44Kf8$WY=dN!;v?}gu7C_BKrz6Zq56_JV>j8Q_5|K1)`a)r@0K@Xk?d!u|Ac4mWy4_n?ju<*d zXRqzrw;*$PnIpVp#i+*7yO|C&7Rv(!yxw8#o^c0kMSEV8GcS)PRZ`g$bR8bSr{@t$ zE1Lu7pTM*VKrx~fFxJeK5(lPrbl)*R5n&5TPWk+?e0{CZ;i2FMl&3h z(#}cyW$Ci?qQ=yl$U+7;?v_Cp)2$aK4yO7m-GKsaG-a<<9`1&2pU+?8Q_G&O2jkLa zCI0WxO3>ve1R3P}zU~A|wKffvZrno4Q$|Yyz9KzwL5!TPao=^aQs>Wn=k=7@wq@bj z^pDS8|6mr)*gI+kqD$hsFbLhT3FKR4hZyj=WW|t|R%2EzcV&zipWw>*y?fO?b%R~* zv`?OJ?4{}zt#X$=Q*c+|8 z+mJ`!F78cqQJLGi4?<4Q7RuKJXJ<4Cm_@%y(j+C<%A%fTQ4;(k_&1&z@A6Jgzhpi} z`(LH`WCNcF4Yc3zzc^y>Mtby2M(TH_*squ?l?+72!2-6YF$T@tmx~{7Z4!kUpkRp4dDCt9 z8Wcm%p@)94wHSL(LfG0O^lWHe=QY9-{jG%+g+lKekHB^(8KyC&u8l$N2F0-)6&adi zzZ%l6(C4$SF{GX|5Gur0j(>rC)Yp0$hCi0_*=Ar|be8QC((xqccXclKK?E#oTloj6 z>a=Cyy7nI=kH?HsYk|ayU(xn78!8MGj@E?PTA4XbwDuVLv9-j0W2 zRqnJ)InV^(bO;|oa3yqAowzOEL;1Qpya*y#BKnxvy*zU3F^2D2P6AuSHrgX*O&UCn zwLF`U#ecctHnBjcxIQWug`JnkL3yLN3vXzk9C1PYoMr0kw*!R{)jaG z8*lr*z9kUIey!n7mJxi~Io_3_SkJ;{&3QQb4m(=KFyo9jTat|O{DP7p@A2vmUce&F zZCnE7JYe(jH}pOi3uQaUEBd#Ma3}A|6cw!xkOueRLUFurPauou#4=}IbSrHRW$MVi zHPOGhjSV==iACU`BWyH~2~#)pW`0I~(;X?!-&H3^Lqjdm%Xl6Fmn=0`^XzDyHB*p+ zU4dUZGX(SNZJ*vk*W@0@WtMC z7M_Y^AmWwy@f`W+^QS9#XF$EU#b;S|(9@*Kz`d8lyGw=pq3>7gGnb>!cBdA;lhL^g z(ERf~_olC}FHKRSnDigF)p;H0_kSAL{477sJKQ(5)XzVvph4H-%6aRC40Z@W-+yNF zKHtC3nm-}SU?Pp$rt_z}Wb^0;?4qH&((bN{CTMVI; z>8c?L>xD@Gb-Q5H%7XcQ=Nuat&3guIc9;flOnJJHwfQ(ydY$eVfz?rGmAUqrx2s>i z*;kH93h!+l*MKilhl7%!E1^N1jiN3eFz!Z&#od2hIrYE(hTN@By9^!7CHJwsZa5LJ zHRPLj{VjPi4`DMTSHao}3cvzP5Iai>Yjzmyf^Rf-=5s5F?AGy=2UopMx2}R30e&r4 zP97i5U_i#0Y) zC+=7P4aKJDWpPA42!T5JIH}-vG&o*?>wWbE<6CZHT1J~Mtn=I;EIOipR-_fYlDPiX z#EPK$EUx`1$c4+W=)Nofa=RtHLu(0KDHH!BxO_H?K#b!F-~om?BrgXMz$PE)b%Jr# zx+ZPeaTE&95;EkTAMECXIKbS>yLmv>t9w2oY6Pnr)``?@T^Hhlv(ymezkNNQuYYpC z-!{*f#KXAsl)U@g?!vhmVec)i5fPysDCMwO5pXl*NvtnY0e5$4-+|C?7bNDT;xi;nq`CvEh-AtoyV+Q-(&-#}fy+oxl}gmdyI}nh2aVqPU0_n z1Fj~F3|}pIXIoCJEtiCx2Gzcvc?5NX*}sa^p@91<>ab_*;a@gKXfp;Q4DWL_O0ckV z_of~_4pQ8GGy-aH;H2Jn&NclGBXdz1B%+`cPr~~HP*QreV%6?2Z3k<2xDk*PcBM0_qWKa@!b~xA0D{^7sLfuCi zvFNz$%scy+<1EEHQdM%hGnV^0jUPOxIC?S3t5F#53Ilu>#DtWxTHoaUlB*6CmAAMX zjfiUR{QIYmRQLLjHpUJx667z~qNTs5)k9EL@|6=* zU*y|EThK<``&C=f1y@p$lYh)=nwn84G}Kj5|EI@!^l1zT;_C{nwacHP*WN~XxuxRb z{lfNZ__5|#;Y)@QsdH%ejD_<#=C8Rt{c1LJ;lJDK8&t>KWy2i{2|YEB2y}je z?5q%VUmQuw=iHt;a3*E`FzuRT-|~z?+{b+E4uI9FGhmchyJPwAk(TRU6XYzW8(rTn zB0^9%(Qk@Y3&(E1B(X94C#O#14kg$0nVWuL>tCOUq1(YFUU!CMqRsWz7Zb#3#`Dsa zOmsDmt7J8Dbv>MuWNB{LY4yC38XsLa>739FhyI$DH6Iaz?ykM}a9i8=Mr(5l z;82XZIq1A~`8C}qcEP@rc6N=0CF#9~_(i?n7-Hq$sShPCfeV z+@8*cgn#f>HZVW24DkOR6jofU_(Xh4?4`>_Do5$;;nFJP=B1QrQbC#A?qlCfr85bn zW`>hueg|`0s^OqyWLZ^h!-B@jBHnHp`A9bg}Wv81^)1Ai=HAd=ZnUl)=+VhF$7#ME6 zft;XMZFl-dpCkX6Y#pD*2kGy-MJE{NlHZ$VxoYo@yWy!kINo4Ue-=V*+-~K{KHYj| z^m!#U{VIEVT5|2#w+Zubj^bJxWAxquv_aT4$fuA&L)97NT;7gon)En*?|bx`!-a+v z<8f}%bJffi2VM8L*6r11G=q!44DEN)sCC93zl)(JdU2;Htz8aLj1kaFyo9JdJ1Jma zTQgL~|1cQ4yHPsng@w$-4OR%2XoU;e`)lw*>qS#RMH~2!5q7i9d!}Dc`HMqAD^&_D zzw4xi)fLK!Wb*^`8XKfQj;xo3r;(e}-+iBEg;Vzy?{l$|*o5g#ON5ti*5!S*;hzhq z|APDV9bM7$W!zyH{;MZ}u1`&oZAPdKgK;O-G34$_oT6zCi6ya4;TgjmWQg{iXV8_L zjUrG}%`59!SP+1SRzc!^llsNUI%b2jxM^7hP zMM#ay*dS3Q8{{2>0&`o>1*xLT(LGIktFi`Y&`h9`xdF}{Z_J|PhLwkq6$n}k$y2=% za@qIJFU>Kz2NBy8S6M}rYk*7Ml-Z2f^&+PfDEl9!zgoW$LSlW=i}qvVu0fLa;&@n0 zmT-+Bz{@J&QsWW{3=E9R=rIe3kmuML*L@v4i=|?%w5UnPLx90*oF8@h=Q$NU{W5oD zWb+jasY7k_)kpT%^tO&ZEMvhvr|SEF1FS?{_lwE*4{5nHj25>J(SL#nNgzn=jl;Kp zeoSAJKGVND=)*YM58(xU(>F|!xLw=Wj;v*Rmh&Qt$JB_%vZnCc7NeilLOb!D$`>57 zi8lyp)=kKzR1)nu>D!k&Z0HAkS4y-L-o<mc-m%C_bF*_d66yWKcTRvs%KNzcLhdVw)T zJRN|+5jh8|&?d0_{q0eM*=zdftuOxOgZA?$glfIB#E@0{0akEo;TCR+lCH_?k~hs| zVx`>4V)v4kM+=bsLUF5g15T?V>E0_H>-0WRr=?;LUjI+l@bD9UMmx>?3^Z%G70%AF zq{~KTcFuO!)q|bKu);iNVBFqvX*aD`vI-FRq)(L=40AYCrE>fn0T6%k;VxP*efaw1 zD<-+Z*pS)$Z3M{j8}BAb_3akQF@a{HT^jF_u=v`*edgz8{mAe$VjA^dDty0rP56%&<6a@{cw+OnuC0tq zcccFh2;Y1NOtMqG+h47tIE39QEDYI=^wrmMjC{I4)Zfjl!xK(}#utcU-?f~VhaX!_ z!kFXow>;KE8^y5dL=%_uzuNpJeA;oC+GKy6+NE_w8kIsy*e1^Z_Rs-W4Lq;2rg!}j zYle~sp*@L+tY=dXv$>Aj3siNdiyj7T=rk5GRV?U8Cq{1>;bw_`m%dcm&lya+D#Keh z9i|1)9DmVW_@R3+RQ2zJ?O19yV!-?^V$Gq-)F@i#=lbY5 zGfGM>;fbwN#P)0$_IxFR>a%`+bVi=(-ALD|PD6oR^lj#CtJgob8ia(7UPvjIJ~!Ym z%Lm>hBTt#%R@};8zg?FP{5)PJ)`3>ir5OX*^@ZJ^7S1Lyj3s-r?Hw<6h##&h(5D&G zOv#uWPF}u7_qS_R`1FBGj0irMJ-91!mewFMvuf9PoDi$FFt*&V)W>So>%NN!op9;Q zu4{hk;L#Nu4qDWp-WBeO`*3)~R=>)?HNkDxfq9eawta!=@rI3Y!Uz`*c$Nl=WIBSaJ4FaQt#o-ua+i52;KM!Vv- z9&-4$+2FZj-)|HiYERWH|7MG!)Sdia32J)Q-}wIAIQ*MIGKV5*lH|3KL=xaWfE2AU zih+Up!F7igNI;8=QPH90Y|#goZ0O9td5jR!yZ-$F!#Pln`tSLFCJD{+a-Wp-^zs{M z?pVlTjn54E-@Ly`6lxVPG7gWwOe9`x_6!b`32Jg~brvg?s8Q@p2=+Kb1t>X)Gi+kI zNfm^I%)d;(Hy-uB*J#nJsRN(f?4j<)8CHdWn$Cx?2OOlqkG5Sc2nq-^dxKg&L^gvB zPiHS0@T0X0*`w!VVfx%tLN_Ifq}t^l^JtpEF#|Wvv857cpbcADV3Nd*e%-BK#7sv2 zy^EC;yX1wOYsBn0k{7xy)k>3C#Q&cfP$d%pAD!>BzhAh;DGn{w8l0<$jx=+#k2`O5 z41etTp=Cnxu=mI+T^Wd@%6Q_K2j1ImoOah4(R~Iux;m=$(|K0op;g$DR34gp6ywxvlFfafo`!olkDin8Y$cy?s%&73wLE?_ zwGnvIZaS_`M@W(n$OK%iR9hDR7)@g7DKsNZB<^1{eXpbxDm_=zJBSHPO2jJu9_Jn$ zV^%j5-^93hWAdcm_$MPh^GbxQCL>79X!q5&cG1UI@!WRr)d=ZjrlQlo^EhFLhZ}A5 zGL-u)t{BAqyb8p2o*#?UBujN3I8zZL*cz)kWRO1-t$8sdG!~bqc9ckgdq=5&a{M#L z1!<=B6e1vOj$S|I^ghXrOS5nC<%afn)mvoDsgTSJeeIw7mke&i4jOzXO$W!o7lPhhvOJoM zMy=Xofi`R4%QRcXCf(}TgTp@4h=@Q%0oa`Y!(97*+CqYE&b~NS$71!j4tcpT{>+TA zrpi0SK3cjqhGsMrUdQ0?QV~k$tFm(5vm&oXz7Abyf16aU{PB>M%DBl}r9ZT9QS#H4 zkXengxt-dU6?wno!-b{y4QKCSc!jniJl}a1h_v*=|MICJQw(wQ2$)oXG@Nacon7^o zk->c!f(g*-Xsy~05g!IwiWm`BxS{>l9gvI@HqpS0Yv)m)4ef!gV>WI?*>@Bq2Ru49?3BGe5V-mAuR zxwVSa64tN$8$C-c-1*N+AbuO0HLJbmt#4*+pZzx$AYyoLxF+pqOA;-#rQNZ;@$jmG zX^uY``S@>h1{#wE(;ePo>s(J=dkA>yth6z5*q5owc_ioU?2Cf5i*g?G}Y|G;T>V4R7jMntR3(5N#08HmU9dJ8drjFSd&Jv(2y@s@D5BqL@xS-LmD zf)$NRnS3heVdC*JPrvAZo8%rx27Hq(dCYO+5EgpbYu-aF>~a-SVB@D?;}ol7<1@`p zRZ&a5cw@gt%EaT!I`(@I0fhh#ztl-@rA~qNSZy=68dp*z&%i#M51v=ej=>FuUq-(| z&Dca-A5I6oOZ)I%=aeWWsjM+Lxl`LI<4%e`+^F#y@n;tPwSPRvAtEm>{vGW9NcNKe zew>am_6%OYfo0lEiiy@TMD8?aKsz_{J%Cl1Tvl3!fgBKMW88OGwQs=6G0cVM(^~MH zML&2bVn$C~vF9qW$!cfNiC?koR_?q)%D=~Gp`HCe3uMZB`TmHWrVSo@M6=mRv(7p^ zOI*ElpU%L#b;Cnd=x%}6?07k_J{G@HT-4!wd>#T)StWzti+1bbN`s=Y&u_3-MdpKU zwi3rHy|{metFSA0yDnC%YEzy18M1jb7L_VDN3pRu1n zx^CX@1c|zFk|=D=lk_riU@P$a=wA4<(DUQu*YW}(vZGqYzW&uORp5z>O6nY1)Vj`{(HEbrmho)K81gy93e{dw?^ZOkAyi5r zSXIJd%xU(8%%P;8u+%nG!UtLKTWhVIM{kWEd3bb0Yw(}BKIZ!%#AoJiP5&;)#p^fn zg|vZzudRWvK{w>~HXI#xtLSLxaKs&7JKUDXv_MnAqvvVj&|xlL$~<@4|N0&$OIBmM zmpdkH$yM-oe!%z~{a+tqXm@Z8@S}HkbVgQr|I_)2AkeQ zYNh@lA_@A&ra4J^Vpp6Eh(ky+m^8Y6NCmOaNLBU6u5gRcsgl z#uj-}WYykjf$MFC84~t`cfV^ZQbE`OvT6+UHWoXCc5`-1q<)o9>@F$O(t;XB#f0X%8XC`Y4A~nYES-W zRgnWgqIjAV5bJ%iP+NEK#dmB-_<0-LdzZ$D3K8YV^Sx?Sl&QDRpf2OL^Lx#B&6ySH z#xi<(dXrh%pG0nE2CNJ<#2#{8XjGwR)KzqUVJXZ`Bm2JmqN_$c_0?>F``SUAMB%b> z(7E8KwX;EkvO*SV;xP?`r67BA@m~2Nk@UkcNY#-ycbu&79ndtFSK`8WIetL@s3YRC zxh62sAY5vzK^eG|UhX(}e$o4^#`(gKfRK*F5k6G!JXgbgH|NbyA`CuitGb|TZS;Jz zbVDXkxh)h4$B)q6=(fkP5LsF*YNnJ19V){owBgPC{v9115eP$Ym6&OF?_u9)%e3`l zUteG8Mk9L(skr8W`FTNyQ7eNB*41kKzZcep_4SO5eCW8bs#9LE-+j180_0a3r&OH~lQ=Ya=iwKe!>oQ%%RHvegf| zwdfzwU{qOI*>P?41q~E4)p3~>8EtN-9r?!xprL^-xsxq&|5BQnv4rDnT-q{(MDQqE zX(cU)m*KR%J0G3_D{FPS-kSJ5p$alIs>?sot71O>R%15*T>0<}B-#OKM(#&}sop9{ zl&yKZmVEEF6F1U!4FG`C9j%TU=UmZ;HEqA;q0b7ASPrpjxOB(5(azgIz2=ZMLu0>)~rANxl!d@ z1_;!Tg-9BKWiB*GduO z9q7;?bY{7`DG2CETFAJ!53-$LHD|48>@i}UvK+>atVj=t(fqMIlR#)jkyUB z#PDe2q7O?J&-}rtLsAXwxzZ&h=BYv6k){ELr&{EZOsKY%%-};iBES$@*cw#T%)>M< z`fYuYI&P1oV@6oiSjTT3=4%tHH%{Eje^o4nHj63Sn`|5_5=E_SF1~(v^NO#B%4hs@ zwm>PJ=6vaPvzUEY6`Q|Esg}99_03vNFj+s4q`LP;FS_jME9n_PJwyG@)_ND885N+K{rW2$iG>HiGSvK1@_~|6r7D zp>qHFnMDh+>7eqaaySx`J=QR#VpIM;V-YKi;sw#30QJR^g1LtMkRu}IALDI#T^WO@ zS>3sJIMcg^gEc^$*h#NoE2xL(=I6#7_MjHwkM=Fo6l1c2Nv0}NrYvjvEx(#$$;%k^ zSfRhdag^U4I%^AL?jyHu@xBgHxt13HFl5sB`H6nBS-vOlkg@GhO@rIlAe79XZ>W`> z>22Y({m)glNz-rUVbi;vKBjSkQ*}K}nBF|Bo)wvL842ZrFJ_eeaME<10pvBCWq^FR z91RyK{3<`_8hZcXT~e>?0w{=fk;t+Wva2lY(9Zcph1c+MI*Ky{0wY_2J(PQpjz3?`Z%)`h9>1y8td+uvfmtNhcfXetrcw?C`kslHN^5S)=E zuM~4t(_BN<{zhtqY17O5oX0GjKCBLzz0r_CrU*29?GGF>$gG%>-8aV^>s0emm)O4% zvWxX@z$A--@~R3&~%G-dVkvsB>RiKAs7oL`8;e~vw zVNU;}?jN0>O_=KMsrUfgQelFE>X~RVCG#U0V|>5IR_CS*ZR%M7P^%ano$Sh~^^BWj zEk2Q1SW|hb(2_c=Guf-(;}IctlkW4H%|3J2hPEF=h!D)1n;XpjOu=}zG?pN=D{NJ| zN3WTkP5S-X2LRfn+b90&DW$(9rd08fiimE5WI*dUlJ-EWCf%v~=K8awq$H#Y*{k_x z_+*lRtNSVuP2OKqV-{pbXV_NptT^(^`@p8x!ZOA7#F5ELz=2s-?6f+NO=oAYI4 zZD^sS(L`IS78~}U=T9`XZV$2IYMrj`;<7?#JuUx^c2*M)dKwZ| z+6%(v6F!ou-GMR6p<0H3`}_Ov^tHpdd)@-NB_YZ|O`BC*?^U`gzDFOnftb8-xW?!! zI)43Xe!8>;8`ozBY=)&5e^Oh}CtlJwC!ALvV4^qqpwQOLs+*{v?VjXzW5Bc$i=v}v z{pfb~h_rNU&>S-=i<@2wbZ8qoBgzmDyq&)33Ko_3OKh9)JlS0($8`gG*LgX0qZ@qf zU0e-$ab9U7CdRjrpuIAk0!FC%tAnrw`h0!06&HMa=}HBipR$+HeJ3_oyUk<0=VkYq zOh<1?ihjDk3~!;KCg5M;mBHK_7FmxAB^*t7;T%U4T?7Z-`rJfc(hGtKlLDu1=v~$I zTT0|A=XYu4!xL&H6x%6e_nXjNL+wL&IN*)b*9|dv3iEWOA<+ApEaxzw`T3R2-ExB>QdYmVDGy6cGC3y zzNAr}iu{kA!?4T4Em60Zr|m3f&71kCk+Y^eh8w;14!IPXr1+zT&wkUtYA4py3KbSB zN!&~Q$V~=%PTQZ~-ZMO}z8i$x@O}tvm|5mRE69b$^2Z5~PulNXBMN($$vfW)mnLdpxAy#~eUbCp=4uR&hW81^C58@FtuQN-u$8WZ zqp%}Y-z~6DHaRmaTdQEti7Bl;P&q(N?BKq-hWh*-lWpl{M-y#Xr-7y)t5VT7ux$r7 zIfKMg1Ya_a!4+-vHa(bW{^4C4xd{7|$}QQfXigm5Q#sAT$bqfG_XBPFcJ{eG5~JV5 zEL+ykjBj#7MF+d9^xNQp9oBbS6noQodx=!H*5(`}Lm5`HU zC*Z^?`;FccAJXZa2lHb=qj#Z?!p6-pBMOjOR_Nq4~l)_+*dmKJA z9K0svenXmdayJY}Xm|XIfkEHU2HhRyiG+26PaMB0W+#>KDcnWEa0uFGFIMXpEV?!i;nl0t1HgF^wJ~5k( zq;76clapVQ@tmE;W?3ZE#R2d1M)5x>_Y&-639JW)UTIb+CtX>BM+cF&5jci28yJ-Uv0nb#AcP=x@Ua4>fWVBR+XDh>pbp>(@ zTQ3nLSX@rdU?2UlAMV6cZO;UflAf~ zTSc2gj)&xNOMHkoA1Cw4#Rlj>gr-`=r4I-)xlrHgV9z2>qu{%@)7GCUIF4)d`y#Ra zoA!O4p^i0IRn(J1TsBsPdasB_b5wmb_FZs_&1kBIJJ+@8bJ4Zxvr%oq`(6e#@A-!;4?{?iZ~9{EZWt_zr6}$iTMwqIjnbT zaRMRHl-+c!tu&Cm$g+WVbJ6xAx6?z80(m#)UJ3`;xu0$lP+8?(j;w`ofq3SqsxzGo zHL1MTlJKz+E+Mj+dsxS;PJfi72ntGjq6lt%Ji*{HD{%wj(Nh2 z@QV+XxEY&PzM{9pGVnZqPsj?~P{22T8ablW+h^p;t_wN+Y3cL(vv~@r=IJJwqH&a*x90N z;yVsgbyoUB(AbJlR~L|o{{}4I#;!|vnN(s_P`Com0_yH5@|m$@z@09Iay?WohA-on z0g*M=ZyPmD53YAto=1oCW*NO@=>XkJRTqkTo25Zv6^}w5Q-yvdd7EVdA^sJ^K>8WRBrirBtw36lJWPdmxzp6NgQ%shq1x|>Tjc8To z9okQjnx*(jb~652KA#fO?Du}W?R2RQ2*6?+1=Om;RMBG3O*w4!Za&T@byu==p$skV z{J0#;I6Af5$0KW9tTi-mGT89d_C!_lnvg#{V6_j1G*Mp0Y(0gylVEGKY zH-J32&5$yog&)Rwv@@~U-DP;^q&W5(<_JCLBcU>}uVvRF!=cJalhqhk_7f#D_F6Zw zw=m+1#Lp65^QXgtObp%$n6I1l$Ml(hqxln=b}^>}nRElXnZUA(vFsA!Zh(ir{0Di_ z?K(b3!MC^dr8;1UA)5G7PuH@p{)7A794;JQs#A%q;B3oo)>uoc?OB9D*gh4$Nza-> ziNWKCk6s2DuUjcb#ZU^_h&-SkEQ)IQ6M$w+E-?=m(<+*Gch$s46~cD<{Gyha>a|t76l_8a{oXqp``R(7?PN!4u;^!sQn4v}7Yp!SK*w>7v`U8f9-ry3 zi4{7D$nQJNg~2J3qDn1j!S;`x^$64Y)pw#6>U_N3zGiXj>zgz`($Gp8G`?j_YnyI# z1?IJK0IJg@DkQbmLRHjOQ_s)O7UtJ#J6`#I%w?c2Y5C3$1h zL2E?c28U@Br|L}^7$rHbqRiK~9(JyntvEFa3G1CRnfZ5;IQsKZwqs|4H=gUPh_wT# zNU~DbC#^+T#W?lta(X3_V;c)lm#Jr<)vIzdo$bm?WB_|OGAcY5^Q|>C^3JL#7uBBI z$a9Y1<~3CGo=YX?=h&+}IUaj^xWCO@d5s(qx8!VUJDmlOt_X$Mx#r)vX{OXm(uyTm z9iUvH&`Qh8afzaVIkoy*n{DoH%HcfWQZbfSnFj7H>7k*Ae7F8=>uH|%?a!4?QrurY zF=g1J0y{ex&ZXF(NB>gDu*BVYtZVJG*kU`mB|19K{p8qSbXPzlA*En|;(BDhx)zc- z`vC02PvNjwOQOBiaVyE+kN9*TDZV&9t(k&k6F&rQR%@UiOFHp9a?7hzPN0JEJt~}Ih1~ZAtsM? z-Lhd`f~w)<(E8Iw5GXfCeK4nBAY;v7fOW;WJq^pV_}IrY_w^yO&m$a?kwGvLDl{Wn%>x@?<-)c z#(vr>bA*KAJ3M+qOThyczaCz*a+H-YQ$7e5g85C8i9^VF<@eQ{AGD=yz8-K{5B;G- zyvksXcdd5XWFszE0xh&g>Wf={ZxlbM*g=}T^|22RAnCU_942NyoIU@!w4o8m>Hfm6 zg3-XZbH~48EkMql1IFUyxjsFZIlaC<9s6Ro@oQ)(@2rMUpMNzy#mRyDcKcgmTXraB zSMkb_81(_)ykCRQZM$U#)D|ix9>PDeE9jW{YLeG7a$quZz^|t%NTo&EGs7i?Iqpkm z&BLXd8<5Ye=W~BaYpV7}WqwBEZ}e*&#-q%Rd4^|$h1@h`m-qSbw``@T(^s}h=2sJw z7TO6C%YD8xE`w&1G{%uV0yFW^h$2#A&H7_u|w!^QSFE zs>Bsnd-MJ|preYQ+ge$t@wkeJ_>crDTm%g$;X6=|8TW~?Q5a}l)^-TM>I%H#@7PKE zhWZYo2+o!($gaN2emBR}G$xY}b-N=it~PXsD9MeQ4xTJFuF<&KO}A!evo)Y3Zps@$ zqrcvWcIp1CsJ+@2Mp zwdOlR{=I)?6iHZX8Pd_pZ-3vFVgV03sa*uT5>S!WzqR7K5{b@bx52_Cw;4a!0T8Hi zzf#q8)xXrkCQb_5A|TkNCeQ@sXg`Q=g~+(KK~mqRvPRIY1p9I{an8l-%$nHzWDgB3 zr=y=T7&dSRB1=2w!g2EXsr`(sS?FZ#Oy5bGtt8U{nGO|dUWo{-h_fH-vs>G15k)y< z*kyo=s?u-rM;ycZ9dd{am_V*~84H(vaUlHJMy2xGlhJWJL$T~)`kbkf7Shm?;gZm! z%>}1{=9bzEzG}B>u!2#SwuWq6iW2i=G#-nLt{zvSDmM`LDw01oy8;W~R6I6}xX~e0 z?8&%3kF}!CTKT*gel+ZwIM>~JA-#adHf^#QnWSTx5v6=z6EEta{o5HPOfVa7Ni7!V zwIg9IFXEelLRR=mq0hml#fx#_1%GADZ8{Yb`h|-=9H>Jg|VM!e&@@f70;N=v2BKGkW z=fl81s#c02!sH5@#4%Q(Wl)Zy5`D`repPY>C9KATnjEsw(=ada9R#DLvgL zGOE>Dn#wc1RwUZF?Ie!M*r}A%C&S#1z2(;ACA0OJmnJzg`0CIi9&7v6@{Ik5^KA0^ zM$Sy#_{8JQ^Fz&E^jy3l$$(^Dnn9(flCRb~iGS44(vyi!1S;Pgpl10foc0inETiSD z^px}^Q~QTa^t06vtJS|V5^L?UR!11S;xMH`X_U7-UDbws14*&`7_HQL| z#3ww07bc2=Kt$i45fk2lmXd-O*@Uo6V9$D)qSQb1`+C~)(2&9DTFoqX5J&>(1KT7M@{U{Z=w>t+?Ku8YvZZ3xljw3RFqbY9NP5 z6w=v4zg!FHoFuFe&sCrTLPMdLb{eG>zNjPQSNhcqO4*7ApRM}rhPq%X1FG~Qgtpz< zb6;x0#nz_iiWbBTz`SM1DM)IS>mo1i;>X6G5nm%tJ(K62B%lV7X?A&^Y%1vb*>)}A zZWh>LjVY-!+mO^FZNj}W`=PMIKmlkjxSiUfomS2s#?e|?44sU)7cQs}n(vMC2Ezd6 z=zF>-J#jBml@eNXLlSi17driGH@kft0YTzP-T5Mkq2*=fP-+7kxx!|IjB!#H<;;p? zyf!d4@7X*>JL4*Zr^f9+Ya_(7rSGPQCQXm1Prq$ozK83 z*LgnHS1{iBq(rlSkUTO;DE(>usf7K8{up?cAfs-}!>HvY(KXXgZinY%lHSz`lsY!_cBJ=7_hPe0hIZ@p36(& zrqdK%E;(uI(AKT1x!NaTp{BzGA=1yD=sB;mLOfg3(TjKye)!NkRR~(CN3U~hS4I0U z@mN=IIIP{5^C;YS|N6bfOyVtS72sNbtfXC8>iH79nTi5<)PZ znq^zkw*DmKN^)!#w505P)4MQGo5$6Dxy6QWdeBW&$oH3rxV(x%JS%>2oUGhRCg_%w8U zeojLM*{o@OE5>n-j5pO<6QzA9Dz5hHd{DJ-{X~Y_wIq)cqZ5S}jaykX9ZiJkM|`Ds z;!5c0Hs-$1-&<=!bZ5;QR=GQ#xo_0n|JH316ZJX-SU$9sBT6CB2ZMvk`i^N=yDFhd zqKB>{n^ykA`+f2X1q!QWxkGgqSu7~e@#1+?Fr#9t6$&syVb*}fc&Y5;GV%-tagaO!c8VBd*9X_bKtly``kxXJwUuRkRT_sVhS6T>d$mUN7 zLi-?;lr2^oE6Z3o>F+m#)UqpZwkT*w>a4t>T>zXOaz0N)h<%J@sNJrO!pC5{n5(Z> zm;}-RrY50uHEGB0ujyFRg}N>MgjOQ$V5;A0oUN)s@$NWi=g-~h!bh>OZ!|UDZZb3W zX$=*hnlirpc>^I)Ls{^`gF?|>K=(_AarCsmbCokgUgc(^T1%f!F*a(#T$?BFFy?V=vX^&4)NERrAhN3IFQJtr-aettWd zy65o)9Smvttyav^7>=HgjN|!Eo+f@k129>VwM(hFO@Q8@_5UAG6B(litB)n-iv?WB zx52~CQ2S}IG9wKuF7i%EA6EkpaXIAHnXH9(l!~uGHYZ>*!Q8i!lUk@w5JjU#%lOE* zKu-tFZdjO3HRAplEr#~nw6M|=GBc|=M+Dzm5D^kVH13GW@Hu>wu2%t-%xcZfRq>lG zFRNCTK`n5Kb+1%sTgKeBpFiEoTzLdvnzF_`ib3WT)1jOGaa*i3%`oGy%6J3Acn?J4 zyUQ3gk9#yc>vZDs21+U&6kT6eZ@MI=U0FP(1FzlCgO6st{TBeckBRSyP|SMBi8r;j zzddWp=Wi5+<(@d}Twdcnnqd3jIC?j~jrN1a$gM-}b{vB~|AnSu+^VDRKr`B)j~0yo z2Ic<0uo#i7!iw3icdu#xP4KxaPOd#b2Q9FPKHkAP|M#BM|2qiW{}~+b|0)-=n^CP`(G4} z?d>L$=nK%v4ndvg_zOa5pv<}XMWaYR48*ca6pI+Rb0!AVuOw{|fY+4=5&+ z7ZZGdt}$;2R_YXvgKDH<5Kp6cIW~}(Qik3>gbROG82No`q9^$AFrNcgWNI7&l9 zLqEY|^8%@LlZHZ}PifGwqI?s>t@f?6|A)P|jB2a>{yu4Yx76?g#Y=E6Uc4;(wK zhuZa|#(e)ae(BA;71ElinmQW4O+qBtpo_*0AEnyZjq=E`=Nj z{2*i4?6jX;ZP$-L87s>SE{XE&d3PV!(+0c!xFQ-e+Cz^=Y#Iet$ZN$W}$%JK}~d_V?PATBcXp4*%yAeSo=!=*@+bAmy}K! ziBwt2E{g>T>`z42@49K^BQAILHf?RQCI%&J*qKWeQ8B7k*J!j5D5Ntbkw+TUfmdl& zBx2>AKFc*?mYKh-& zTJG9icDVqz+S$VEa~p19bGXTA?=G@y63Yu6ezEr15<`|y-@XxeeKr<5MQ)$ph$Vc) zesRvYzK`;}nS>vL5okC6;}%uE)76%y8^_M<;w58)GG@FMR(o8tEg_h)Y9x8}LY7XC zX45YwTC|;qcN>lTgiy#CKWBW`X;PG=&8#Dw4JwmLq7j>f?;O2yIwH)ARCf~V)wQ*A z2vp*F8uIjI57R0<`a2gP86D4Xlzt5s%AqA$9o*t;ijX8KC*E{dfX7~@stXqzj>Pje zE5!Dpp1UC#LD()sq*vFcsJLIhF2QujWs>~+L>{}rH_k0}HN zgB?-ySk!r6{EUCB_6~hu7)vDIu2jNVTfR_GSy}Dyr#J|qjOA8{iAd9U4&GZh$~^*h zC`=a>cZH#!Rb}6eV;`UY3_VStPeESjA90ip+Et@q@bMP(ZsEBwp98(xVh3A~2wIz} zN+BT_$#onTjP;K1XL4*UGmx$exo*hJTtAahKQ_Bv#>1iI3H<=D3}<2N(URdLI`+|S z{~l_0ciXYqS5f7l?gx|Nsm)DqNNNwMu)u30vnx^RLE$e$+`861e(<;ss!Yc$p}gn} zu$o`(4g)>~9~=$0c znow#+BKqjmpBARmMX0MrJ%Do;+175ogTp|%(2&*IH+y1)P;Gp&#KVDyYp`~2pKtoA zp&xcT)_bYmySi%EXfy6(>#96ajvn_IPRLw}D_*9@L9L0VGPzJCEW+wbjSwCp4Cc@e z3?V4NLLFWX>P~%@Sz5hL9;u5A9sn!_y|9Y>l_Cet`)iZZf>>B;yV`?8m}ck84U}uTbU1|ZKs-HHxb#y?#u`=pj!Gz# zFie>3oV*O^Y0(hOatyJgW3*xYkUZzoZ_5#*+|1TBBL(;#!D2z`DyVIx_kdR^$&0Cv zm=`FEq{t~>+kgcu1XsM+-F(};qhkApZov>P@+2(9-Fe4kgc zGN~lto`xweei$QOBirlATd(&7V2!xQ`ce3b*Up%tiBWBXjA%rlYI@LW^3A)zX)=cX z&x*~vPcSk&2Mdh}%)Gq3j*gDZkhNfj*vDindnW|82w9mKinzK|qW6GKBY-K1IeW90 z*{M*OAtayGTFx!vj=I)2eol}DQLO)Wj@PJZW43`(P)p?%>>DCPFN18x&0h!A`EM_|IPTJMgP8@#B zf?f@6cm@gH@x}17-Tsy>ceLuKjDb(E*K&u$7dBd~@`qhWkKZJBR*+2Du8m}7VauX9 za~Jga%#0k^vvhhjL*1L9^qm8s{-Rj6YGcWL{OR+&{HkW|u(_AARRZsuuwDXt!Pr%n z5zollSCj5fugfF#eeo0HUZ%GvQLzn8h!s|#GRknJz6t!bdHzsbu~F*Jp9~I{f;Nq5 zGq_JGfb-hQ)b{xo;xMyaF~~txSy@>{1-1igMS1y(Q$4r^<0f`}avwnUrIOgZBdl-N z&VxQ?pWxZa>FG1L1ejGvNcAZLV6`C#TJ5B#9q8W_W=xF*YFWllzSnhn#2co+VSL}p z@w0H2LTa?oTx28g^!Sf2b}_jQ$^RI;kV*e3@Q}-OFvh~7=jp=xCU+w|b&3)H$Byru zg4LIpU-BkEe->(y30>4;-4}kH&20)RB2_=cFAu>&ZL-KyMyo(5lqI&o8r#loqh1`= zGCZ!Q%!DP3np3I^?)!LYwI2b+*)=6M8EO`+*|P>BQiP+e`}|r7SS9EdJ=Nl71+iTBXT2-xrUnRsQnOQDFq0lr8L$r7 ze)Bu&b;+JoJo^NO0bdOzAQ#Z|PYhzK#aRnI)Cg<5tZ>520jx174Yd!mk2%*z4j8xQ{9#RXxz(M)_qg>Z-M?`vuxF_mD7OB_f=@I@>Lfj1 zU!2-ch2=BROhR0a;v@v$DZraoK+b{qwE66cC(^4^J0y{@4_PR?RhH{Ntu^4#5*=fS zA8u6zLoq+OHnChYp&+R`u=7H;>0p8>EY}f?eH7EIPc^L*CO27G_eIy;$7L7Q(Cx1> zkXg+eC}+c#rueVn0o@!H6@_J?RKd5?E+``~yJptU{D<`At2qXA$D0Zd`$Xe8Co*$J z&l~p6gctkcfRFlG&!wV?C79jsA%$XD7>n>#xLM$pK`ut#(4SBshftSYn4+TgP=89s+A(D*gaNX=fXaJR}U(Q>_jB_^!V{4Y9pY33SS%8o%mMX^V8r(85 z{u{4tGyQkmC~ahk)n2!)vR2eX{g`pk_g^mQwcr1fD$(&CkYqt#-gc7rzk9p6H$6Km zS)}nlL%#p3i1NRepjr~J#ig3q>dJ|4X}5u&+`;2NR~B==2Xq-n z`(gl7>*u@3JKw8AbolLcb)3EEnc1BziHHcrS(rIIl_2eF-p_*U+NS z&xPTw_kg$-N&h+1{of<^6dBq;E)ExExMI;MO8Rl$>B*nBizY@&!{5ry_f7X1EMbni z?Ew|nV_o@bBuVUsetgU>m>-)9vZX4D?u%!yUcL=*n;YHeACOPTb%-UgI^rLTlS7D% zaO#gTz{d*@7bmBPurR`fO8r{dVBiF*b`V8AkYBz@q9z58nRxAJ0Ye`gTH&f5?fb6A z@Xdg!f_HZ=v1&S>oMw!i4Fe%}U52qZpJ54;}I3FSjCj{nzh!_J)cSoeY!Aj>T|WvN7?YHZB`1;ZhGJk zr$5JQtDHIY83Lr)`KW(g)szul%R%r}scRBTj;tzgz~)&u0M(%Xz@XUn<@zT^TpA&^;*h( z3VfgMteVBGLg$8&!ZDD7y&oi!o&#uP|Ff1=9Pp5QeEB)-8+qzB<+JmXTEJ~Wr;7qTIAv<4_`kaG@#^Ubap zZX&go+&9$tX2|u{T5yb%v=-Au1%|T-mIsmo0JL+9OAn_oMVfRH-aB9&+wCA*n!~O} z`9>y{;?2qE-Q=jdT|rHVhhuC0k>;PAJP|6pgB#V>J0ExN%atj2VZTd<4vNjTi}#z) zOT;fr!8&%8VwWqxsoo^I+|9+rWsG0xY5rS^^U@NFu`gShEmm|?$E$xV-k>{Hp_XbvZvOp5;2sg{AUcs#)ggEy?bx>ggLy&I^sZRT%iBd=q?)KWT4Od{J3u zp~yY++^ePr0g@`n4Hlc7&os;}N4ZvGQg?4~^9eMO7oaWgs&-5^JU2rp?tAVc;^V$R z@rHwR=V_1=)V-A=Ks5zI=z(GZ82|birTyhlQjhvBzklapPx4=`NLtjS3*8hT-1S3i zgQn04&HC}pc;M&$?{PFw;jTy<+ZK>>hY0P;Z}IW?IKw1ui%{RS z)?n9X;exyHM^oC9&c8=h#d2p~PthjPY}KPr4&31rji`r5rvxAe4#g&%{s z$@fIHumo55h057pdGgeLxqF5Dy10-yQf$F9-Ta5q27QCKiG$ zLuk=KKUB7cK&dzRp_NqSn% z7b09!C1+0NgY#&I&F*NrGgXubR&9ZrFKy=bu&bTt_T)oWPJ(FEROcjBlSz)Y)Oyp+|dl7!bmTgW=xRFnwTOm_WGN4ayh5Bcq+-^M7;;rbpR zvBh0Y5%q2GZpvwB)?4hQ_Kt$l(iz@e<S3n``UQK3`Xi ztM6yk^AOM;hBn zrDy&#A|hqcfPYU-$9Q?kr&eq?j;X~nB7fe`GDGgJRpQg_8MaX^&EgtoK=_BF;&`bA z)Zw)<(P0zH|Er|KP`5DMfLjb3Et$yRZe@0G&_cXCndO$Jc$VG)$<bPW%r=<2@nc zotqOy%5ADL*QKP%Med92^wB|8O1~+|w@n*u1TG-2{M!kunz^LrFim!7;-$eG#T$5`%n@HZ0?j2-W>o6j64ULOQMjJ z8yq|Z6mS;VenJjQ*j9ZH`L1LqG@pp&yfy1>m z7$vSe=g&E*7{2MH!{th_65oYZ2? z#b-+?4^GW}`P*-i(0SS`3vVm^ALsI1Ygc7MQKNbwLZb2&M^(97ba(_snrCkl1Noo{ zPnZ$;T1o@JB z`zJE#I^+`KL0kwB!d|FkXx;hJW|O^wL<4fC-kE0bEs)Wu<|?ke0+>kN#^DSyYD{bY zW6iU8Bj8pSL>oN+)24gjZuf40DKZbhCnTp>tM2$;EP&}n8x1e*9LQ`n-$~JX3FZ0f7$Oy}P#tw?Qe;9M zG}*O6*m)-GLPP`uX2=_3W(-sU}^CIt;`Hti?2{o{9mx7bRmaJ^oO_;(g)o`GKj!`D5Bh9yte zr&a`IIVb1=X-_3e^|=$27HaO@QmQ&1qF&bY&3~qoJbf#@smVVG${YXm_Fr93!b6pOnHW8Z!SgL&f~#UwIImSHKVd zsV#s1{l8Xd%&UoBK{)^Q?SD)7^ZrNqh5r42G+_tJF(Zh9<^Y+A7e&|;2o`O=OX)0ebQw~Wu>+M;;en2qqe+qstHdhxd(C zeM-VlaNb^W3%_WN@>>lF5r>h|^Elkf^fPc0VVYT2V%EszHX~#Yc~m{j`w#CHTM!q6 zVu!xI5iC0werLTr#-Lqgm#gqbp{=|8xDVwgQj2g8V3{8eE#dr2r=wfK{x{FUk(YaHYksf#fUK9v`%qcK?$XSrRqaXI~WW}Z$nY1&OR z+JcZ5Mtvi@0=vQ&d`p=Kex6z z*PKg+O+tJozfEs8`52P6_<|DZ)G8?=qC&=DP-t@U=TF0uLvm)#uT92Nm81r|G*OLd zo~kb$ukGoCP-Pf12=P)q*J+ESuhRZ0>Ixm=;}CM=cU78Ub|{EhF-@BT++j*3XRak_)hV z*9Q*24O!Vy6)Iei70&D{D?VGpcG2h>h-yBI>gG~~dUCrpN}$)1?pR$JoV@mqw>EuT z2fK+PMR9&-dGh#nh+nNHLVq~JHXq3ZeUL)W=`^2h-z>sI1TKO?$UVKp3`4JXfe$X) z=QwCn6a^sGt5S6Orz2Z59BIjl<9|pZVi-*8PiKwj*hZfcj#=G@7&jUzO5JP)-R7F) zB;Efqr3`MOYO+E5Z8a9@lsaL~k4v3WKu9qdG}*F}Q_O17-%pp-q;s$pw4Zrz5avh^ zU}14`a%z|yv#1qY@YN{sy=-)*YFXcj_C2{09~|>TEc;xO84|y=T3aK! zJ6#Lf>1&w41s5mWbo3id_( z#C_E8uzuVN5f`Etao$$zR;;p|rCQ{naiexC7oJ1P{htehQBD@XTy_ZP#(1^3oB;pL0HxG* zd&&8Y6-VP~b#B|1lB7Y`E@sKwapF2>W?faB(+Vmc%}bDCed2zjCk!~bjEIytv49W^ z+_uedmoiD+=Fk7=-uRLB4|O*>Wb!gVa}IPn%yPZa!nOg(=(N0GEQgD_Cv;=<{1BT@a=SV|Im?V z6LIAu$e!q4~9B@5#8D3!pMUn=XQi=a`t z>=9OL(!)|1N7;aARnnS8rO$fhHC0;0KfRH=_>gv`rbQPCGH`SdKPILL| z>yz8kILvzJn#DbwWlO^@Sgra}Hu~XAHOuQcXRa| zQXj`)3d^g_jN3FW{Xx;YV?9?d-4mA?(s#F02#GvZy(@ctAGJH?e{DO{=vSF|SF z6qsApe3Hpy%*FwC^zOU&8!{Z~eHu|Yy+yQP*tNDaXdKSU0^O0uz7x@xa@_hA>J*e0 z%@(#^h;ldID<-@%l)=D7PIav!dR{$T(Fz$(aSanryDZI_45AH*5Nqt2xOb*}d}mCb zVN2dpLgxt;Y^FjQjn~@_Yy}MceLfB;3e3#i_E#()z@y2wkqJ7W;w$&BZ z^IM`-61{ot_fjo3=au!@?VYSr&~=~EgfPKT(;7YM9=rur-53=Z4sv# zl^?q3&UP6SNHo3Q>0NZWf_u0lk?9H9ENLDo45oCLE*8iwgOTl(z-b7AL-UTObc$eT zlLXykDOI|&&y4dM^6X+t`pVUkW8ZH>?mCEtOQn$2_!u}l$AhTkkSOtIDF>{_?3ar7 z5XfK;_U;gFuTT zo4O+CYo0FwLDxM?@l{kA9+fVnc>KGm14Kf;FU5Mv8Hq@J9na4`iv#XT=!cr$lc=!r z=E~YTeLA^=3(x0X@VPsqZu@zSM#lmQiRj%f-e3O~m5w=BMxr?m&T<>BP7Wc!`plv3h+lLL6Q7#+#+1Ksevky` zB>j6d5E8`P$@we7zyI+2EIwUK;a^|)llp&imH+pOzXtn1LWlotkN<6t|9=gKb@4I0 zztmp+Enlm-_v&{J-J+|EE_Z z^BC5>9<%@V?7t?73?&JJbLRg2gd;4C!cJvlNnt|z|H#ok{u|8vzq;Zp&QUS$*eL$) z6~*+~Lwi{Tl_>j1WQc@pZZRgL`PPEoOtbU5q=?9|*9}9lvwP7+`hSF}ah4mqJaVwv z9{#+_{G0BPhS*eJC)9bk_fGbfj|{{WL)LuW9T7EPB#jLB+wnlv8cO-S%jG|hNUCL# zrbRrBC@!AZ+(-5pru~`JH<(M;r>*#!V9s@a;~?(m>i(wW*g!mf4tmvpHF47+6)36N zQKabejth}gC@dUHN~R-Zvu2~ki{eIC6yFtN0fGhkKIWcUGz4}R8MnDX%JHD%}c zJz7I@NlsSlbLh|_;riG){8u6Uo$#+I-9%3R6# zLUV3Ox@x;HHCw^w>T0dnXyH(bzUu8lcH1z1ULim7x@#L*zuh`8MYzS5U~lAwHno&7 z!v10DK);w7BBW4wYHwPtqiEj9_s0F|0BAh`-%}VfGQ^}f%+9;Cz9OS4Kpt9x?k)QU z@w$G+C0m-9XFD+{JT`~El+pDzEUc*vVu5t#Op(PT+wJT?Vx*#eMiQ>~=-O2B+f0}o zZ0(*IHEl%|sz>>Y!@4KYCi3zY&N?lXwSMy)wryk|k$*xnB>iOMjDqfxp2V_;SnlV% z1`!eE$Cdizd$dI;U``l2>XY>`g7m?*j}p1_Li95=7L1?T~RYiw$AO&mcd;-Py8- z&xvNv=*VdD?REeJE%DLO8&74|S5`JV-Rox83kIWRqZlvF>P9jLqh<`kAt;TWz~`92 zy?;QK^Q-NkJc%aIwTB#CeLi3+r#(*wIKDKcp5S$+sTf{)GZS6)PgvvH?FsoVew?Pa z!=Cf`QHZDYzB2h^PUQ_5IVqvDpGi)7-U^0a#fXl{cNL!Y4!K4I`NbDxe)D7Ff19Ls z5F4A6WeEjK)X~>+aI-L&yT4pek4sS_jZA5tj{=M7sb@({O&sp6kXO6y=PA;7V{Jlw zav>zNE#`iK6tkJ9XYd&bB(B91dGa);Qpn3$Yj{p%i09X{+0f$R@_MTK@ikFQOta23 zm^QC(6t2sB6>BeQa)L85bDv8LE~+v&Rj`}ilO{9r@WswaNeXAT)E4{A_l$#WCpSM! z37^%oW7TFuG$2~$cJ}c3RO}&jt0!SBv%!`s{-)a+kF9L@U=PL~m~#>phAJ74sax)D ze(2SAwGi=$8CYogJTkf-y8e;EwnYEVshat{J}9%Xy)U+?Pk|tY$g#7ai6%R(`ZQGM zt)VCSnGJ95ke5f0pssZh?<25cB_)OOJaK(IbUnQ=Dk;R(5^m0EmDk_LcD+YDbSYjf zDj}iXqd#P4sFgG0^IA>Cym+Z@_owB>AD}fRRx2n=(LBtm80O_iI;cf@z$!N9zyJd& zdCF$UDz#IvpPuwe+HQfBGFDP|sKJR)B~;AjvMcg(HQ3YHeZ4B|_`z=o_1_y#H`FvC zu#|(X)VBHvxK2^QzT@#wIyNUVL!X)1c?n@;>tldVu8AKOZ~>|E=(5}*-&gn|rc*L; z3iK|l$s4WVbRI(N7p|i-gu~?auN%<; zlUUJ5gu;wxRaQ+Dl+*Rt6IL8Ad4Z;>4emHw3{Ey#L!M&97hxgEt>dU z)%5y1Wr|u%A$$@dytPImcsF)Zs;`)~n$~I$=V~PeGt;y3u!kUOs|epFEnHlTN}@Ek zK3IkUeN7PFibhnb*c+_+DEPG$?gV#AHPd136?huE$3#Cl3;Bs@4kGWkZ?M^DP!)^J zgua^koID(#BJ;8OwEh_xKvB^MSC2AL495AWxkId5$J~~^XF@-!hiVGl>iNRkL3@<(n?%P({6qd88agZZE&k5yR*`LvQF zRC>W)__Aox>kKn$F7M-*0B7LSK47&cSkyOGOC*RujkBb_)XK0?bALUj zX{bB%c@_GBGC8?ic7QmSV~`sOpVl*0L%8PW3pDBuDZ8_u#64e)PrDQ1uxC;6~O8I`H8`A z!urA+i7Pla710`1R7#`C)qSEhtZKrS0~_QD3~H!la{ClQ)OLzl?+-26_>>G|&R29q z8IWZW!o&kIVm(~Ot984b4zm78FfyCpV^IIjc~K)!>qg9r;2q+4IrJ<3m8)*6lk!KU zpIiSRsah_7z#ATgfL*dmp+s!0#tP8U=RWW++MLghUCHaO11}qAW<@>ZFdIR!Vd{Qom!%)qvXqmM3OQrpd*l7*-GamIWi%q*=!!KiSbuj4agiBx)#F3LdKYq}vKj8;H=YtzzG5AX6HZ1h%h9}PIO^%5Imyezfpi)* z@9AB@1BDdjQc`ugzF#$jhzky(y+T_kk$cT{37s122UKL4n@LE~ZZXF0=FFOo_cfCZ zLJ=US>3G5`yus;4Yc^2x+D6^9pu#GVmgYOZ*H zgEK;tgmW#7nIl5nW@}{uX2Z*&#Wevf;#!ba)60TA)>i1?gbC(*)pou{b(D5=Fx#`0 z@an>#w)rJn{v?^vrrJeth3Ct=g?J>guALmO3>o5r>(3J_7Yf>6R>zZaFla>SJc9NV z6xwB^llDlPjW;0>m`+b>heT+%J6eD%>|;fdlME}!x%16P>^+>$_o!#T9}F0sdPi=0 ztnzHLhrifo&DayL&>^U2pAU43=^h9ucRddapAsjEC@xITXey`=V5vZzg}gAcGI?pW zyCugX%H5zk&I%UZMV3WmWQI4W*ei<)iZ8Ue8KAZ#*2rsJk3*9_*F8yn*Czq{JpNU1 zt^YM#O7RF-!%eluEBkhf}XSg4#Z3k^-`t3zbv+0nt% z3xj%^c*hI3tvOC{62jF(V}rTb9khI5H;=O8m}A@{#82AG`;MKR!L2k@kl{tn>ind3 zg`VNZRl4kEMa;-c)&U|SD;rLZl04yfSl(Qx)W$DTQ&;odnr_X(tU~U=xw?*qYn>Hs z5}wKDrKwqbyx{Frv`aLY*M&N&QZ`aZESUNd_mKA%@wjCH*N zyC=swLdHw*#{`$O6a2;rE7-)^blQ8ULG6ru)_3UvoniC_Lj#T<*K7N0Z`}9022&PELi3{b z-^X27RO|7E7Ngqc=Wm>r<{3e9UB4&CVY6nBbz+?qRov!J7Af`5-Ln^EZ+?i{KPc4`+J&pDwpc8t z#Kb6*r5@UiYP+(x-pu!m4J|F`X4~&p&CF7&?8p@~%IsPM4~DB!usq-OmeW#GQL|49 zd#M8<;x94J!?tGzuXu!%R8+JrfHp+_(3>>{yolW-WB#xqe^5XuyjHJr_zD#GAQ9mr%saxzSDg7bgn*1^cj&~y57>p zGJF)Sk?WEoq-bluA!1CF^$Gfch?vo)M6|2$eJ4Cm(PFZ*qBU4HDhA3gs5J#qjOLRq z3Le(A?H!~P8Jy4K;_rSRCrazO*&rgD5vd5{pG^Bq>6aqIN$+D}V%aQehQMUvg-s}R#KcEGS#XNYm|-8x(@nlipE*V$TRmTiv?RU928@H_@H<}g_XEW& zy+-R8Y`ci!QV1%dGKtUJ-X_$a{;7V_sJ7Fcz7jRJ8NeaWSH=9C^r{HBNBPNIpXW&>b%bYMufANk^vDfa(H?bMjAM7FwI7D zfmRpz>H_2z)cd;-4VhdZ+e^SuS#-6%jGS;wLt3(gX5X4mzzYot`}r}c(U#rr$zpFy zw`9|s)!_K?y0&X)Q0Xq@{5)J<{v*FjApzX{g<)m;VxE%V-4oRK`5HHz4p zn&R#5q*82D?KCqR{aO{EPZ&cyr(NZDiX5c%%?$oKco2hJHLqKql#sqgINHXM)AV>? zFuqP2lGp0Vo_il^D)UNh+jP3VZ%x` zXMC+@3yfqi07;ChI`mqty?AHF9`{m4)5bqLYkMF|(&_v>#Iq;dJHS{!o8!%iWiOHN zkF?&odgP{1W5LZ;Z*eIVI(cKLNZ}bJ`gx{1b9ZKeHig^R!+VaO(1WfP9UVv#?ipxA zUh7h~f@TFUeTM?HwO$;;!8nqU-J|E|1dt;Th%WtLs1CYxa>)mh(w0R0oDmf=soY&p zN$L$&6lUHlBa$cM(rf0@Ya_OvG-o1Pw^L%>ph(Llu8f@Ej1XrQd>?nJZ9XgoiYU>6YE;^Lx`4UdMTv5T@?YiZU zETEit^?Gz@D@(2xOa(0hwH)ZJ(4(yGhwBDd(vFA$d$4VZjp*Whwa)OltNmYjFH>xz zg`A)dG;s+e@&^Q(pDVX1XXj&{PNg;|NU%+M@&lV>K-u;vsQ1ob!C?!(``nL5_B4la zn`P~^*wOwFn=?|P>hvjCjk$ilG~s>np*TG^Z9ckB-S{bCL@}F`nO_okFYxEJcwViR z!-8j|hvmNFV=S%$!Jpv4b|>;dVC&ox9Q%YyD zaQ;t$m9LJRt}|?UEMkWfyB>;LKc3a(Zv}|`*?G3Q=InBLE4LTi_}j2j^1NDM%%0Q4 zexA5-cR1ANF0Y)E&CRUYb(f+gjN*Xd_k%}}=vGW?4#(`r_R<06XD@GqA&6pehFmUY z7KU}VwCCIZCW_pGhDe%@OePJwRn=T@SCs}&9k>KMw z!hx`$lc%Zz5R$c1O;bl3B#doY!>MJgO=lF^*y*kg>Yjn7Q+@r!P_^h+g z(uOaDxNgBm5RnQwfAJYn+O~PV&H+B0t+vHh?94ct(J?nKU+o3pLOS|dI^@hXwA?Hc z1ObI-m&d`QXG}SxTo1$Aq(6u1yk9w9>k6lnGEOA2WAEce?5}7d6PjoQSKC{I*ES3S z=K7r2Lp3;m?}XH@3$jOHb8Drn$^-(j4s3Q!T@O4pi24L6v*V#})^?umTP;9@PP#q- z+%&1Be4D&4D6IpQsJYZ0UAi8VEGamUY&V zoGd@djczgq0{|5@<5f}0keU<*KuPl=qjo-HZL_QjC{b^oINn&_u_;5q42F$vKSJaV zC&3ePg(d28#LlRMkru;)(LvlgI(jSy5pZm@*;NGxx*{?Uk_R9OX<~nqZkv`_j7-_) z>uGjtFl^>1B~r`xjTjSCZBHj;lBQolQ9(gr_NPXA<#7Bid6IEbBRvxv8yna47wN6; ziWySUD++5ic{V*~{^c6K-(Ogs9G zXC;ynNjCi%mY6B%*Agz-GeFk1Gt=ACx9ki>3t$+S}# zV8L0gu55ThTz<>XAZI=iW3NX_MMFc=f>`eV=pYcw)B)z~o~vUQ5*77T)H7Qv=aN4j z89AmR*ePn`(v8t5+o-SQhgM9``mj}DT#5LpbE*{6_27}L09lEhEFtg?P zlK!EDeF0PEMX@`*DlcKw=OIOCvl;pFk&%&dd6Ae$gN_=xyOKl+NCJC9Qi*Xd2047s zuCRk>kNzA`Up}8x^2)Gnk_u!_vQJ6Zum4R0`Un5ydU1F-91d6RmCUIe9fs}~Bujf> zk6TWzbk<2pO2&Nc`7FD}TPK@3b*1)Re;u#_T&=t*Gd78RszuEg2A*kV&v)016&9v) zw*c1ZV}Em>WJ-qBBPyoIN~n`L29E~c7Bf7QED6?9H+DR?{d=ACA+U#mKo+!G6sz0ojC@n%;Lvbd z>x17g)^#uPS!WxkI-U0}m zJeQ@(H3X!211IeSA~~1Q+mwYW67H|{1`?5{P6gBL3#6pkJ2plp=iS3i13<+}{1#Ay zyxFh=7ozG!M~>sESz!nIHu&--DO(?FT3JD|}kOS7T24U?zZu28O9L`_GB|wP36g3G2>e zwLy-cxQHD-W5g$;TRcDnCqGQA)EuYvuhkRz8u6914{J=+!35sDQ6HYufrzw>Tlu>vHGQ>mvB0s-7tcxg zoEIMX?$oqlkDik^hFmp*$|DLEcX?$fMW^fGu33jwEZeBa1MRCEnQDTmCEqk`E7UM* zTCxs^X@jeXi3*LQBB;epOo$=!L+$(GXx}SlWkqY_YHNQONwT$f;~%Sm#WuaW8~?Ug zrH$A{Nnmoxo7}{Zd3}9^uv(EV?VT|l1WU@T`cqmCd%kmLc0y4Qi?5f4oX;;C``Ef*!T&Fj{hqoDTxr!G|h7; zCyvmAG#HT)d1r2Rj`SRUu@{ao$deSv zD8Y{J!4!eIp!V;apMb$XHx^?zCshCdtHYnKs&k?APRy&RWJtS;6$IJs8U zVz2APTn8~x0y`)K`<%iqJTGav*Lh+}sS=}q`sl~TSIhEyyoq=ucUhG-nE>NY7J>$} z1wB<2@i{Ko-u^D!dv!FiqCwg^E340+Se>oTg0E@_u+p>sWSBAPV(ESs8u75O1E{WC zm(2K$h@H2*;%(UUxVV7mfP4NpU#xE0Z=5G=4A{HIbbwfd0*@pv^>qc)H>cPS1y5p+ zJ7L`S$_k)wWkL4K-SfI;C&6{a*z{zu?rKqTrmtgQuk2G}iu4zirmmVcPU9IcsX3)d zpS7Lca~VgDfb1g8O2GS3-^ouY6_6rP$HSFkH78zk)^{0@tQIZi+}ML#zc1`+8x)ORj6WHf`kH z0(Nw|+fOAR3mYGpX>#_oF|;cSXQ1~Fyuac;K4ATE{Ur18Dfx!)yrnZ((n_Q2IvARl z`@gb~;X(FxZe$O$vRq?qPlc&BBrptSYiTWN(+hA)fL}hBrk1i~!8UbiE1SA|SL-~7^t+&^G=iRmLz5DEaZWl2yCHpfn*G&4KbhoLpn2|d)(bmbx_93x6J51 ze&V&q0rLUGrUOowpW$dZTf4f5>3)daw!5;Cd0LyOZ_FhcG`JNcWy#+Anx;JBgA7VE zlS^Yucm>(rWjB-TwFbFT#yV9t^x{P6H(EY6B|H>^E$Xg5Tx!J$w&I*1`O>ln4j z_R0(w+RgCi~DBzdV!a z(T?CfGPgL{@{)ZyV3%fFA3fvQKi3I+XP+KHcwU#ZAEGZ00(tNF`|Tyu`gwL5mGVlL zI%##B7eU_-~%V1f*B_Z2HQCBl7Ho8~sP z+V8%UicX&o75kD?C21cEZp;cRmI&>dE#7IK@%6>C&)3d#bmT6v8@o0pOGnG7!++spv!4T0N zX^0D-m6SfC*q(S5^kj9WO34~p)#QE4|7H^1I2W!lSCX1Z+V@3*`k%qfJ#w9$11-v_ z?#U_gskm-*1$6K&@4cz}9qoCY=X+j=$x`@GL?F??rfp>EKj+-Anw_B`-vP}~0|VbT zTm;!BMi&$>$@Ok8*K~1tCP<|2<-UC{6qO@JT6LA-7B}zKV?53DrF=5d&abTP3=Bh! z`9jpdT{*%tRnA$bU7dI?1N#USN=T@|dUVu-9+|e0&@h`<({2zt!FAkYPSsYJ^D3Bk zBu~{+E<=&B=__rym!9v@EdCqUle1@K?eV1WFnyw6aHx1y{IAHhUIMozsH%4B-EAe# zEp|)>fV1}UVP7J$I|(aI;;a11D4B=90Xio{vs?|#fp@}x5lfe3hG=cv4?7a%=vde; zHRvdE@k`yDRr)$H*nNF$oNHvnvd=8ZQ09cR&AA$gqk6@Iq*wsXb~50+`VKJ!k%9iY z+a(qpTS~;ps=fX?a^pEopM~gBMl7H!ly*7256Md;%T3xYN zZT0v`)nQKba0Q!oraq-#3jaYjBYm+cym4ks>s$ukO7HJ8mI#aCtkgVwQyRzhZ6n(S z-76kxGiAUM+@_(D`eGA;Q87u(k~vJO#=-ZNLHSCqpPo-&^RCa)HmFM$j7nqImJ%eR zsk-;hr_}a8G2isgPr6VONhK~;5_D{KR}0qQJMs3(6F0?8<&CCuiT81uL*S zJ-m)V8hL*GEdt}!vN}RwFruw%;oUI=kQcafEM@jUSy4%vVgmaJZ2_tG*d#6PU~=fT@$7D%amLC~tF znD#HH+p9Ub`~ydve9Qk?;De0CR6XT-cuf`R}+eM)~yZ3#pkxSip8LF7m4kSjY+?is-Li3hP;xX?K1-}1)B3WAqr;I zdY5*1GGkGUXIdRW3R!kP6l8jr)c#4yd*3{5dgwjLLsw|e_}SX5Ge7Gt^!(C%rkPnY#u~}-c<*S2o$Y#*DqKc0+yGm!i7sfHPjkWhmD*ay{FZij`o_=GyLH$YTvdROD zE_Zj)t2Khx`(@nh{($crW0j?85QM!&~nlu zr`*eL9u!ZxQ+WHmbNx0QlmNj`w83ly{`BukR8~+e=I^+Vk_rAAwcXzu}U{ENuFBjtafmnhg>jDq{ z6GIvnl_VM)8-r4u`$e*jkHdkQ-5W(W$5ZY_5ZBC+aahFh;rsVm#Drd2#IcWnKo&kMq|~fCqfnNN zqKOj1Tg`=jDB{a9_)~V=P*o$VbUNY*U_tbu{Y?q2FE2+01W>>W>WYerpCd2)r`*!v7Oy&_99^c_u@4a+pfuZg|fsHX27lD6i`s1K)7R7tWKA{UDFJXfN zZNor#{#H~R7#actkRf|+q=|w!1`uN7Ylm?^-Kg2@Mp~NWXwQ zpcNg>@fJM4TYq(k8#tXD&;b8=w_&{e`^3!g;fg|VNC=jFxQ5Knt+OArv+N=oad@!0 b@%{U7Cgh?r(HgKVv#{vv7;8V*bol3gpk4s+ literal 0 HcmV?d00001 diff --git a/doc/img/03_columns.png b/doc/img/03_columns.png deleted file mode 100644 index 74003c47a1af183d3b3e63272b1d0d3c5ad4977d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54122 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%0Dx;?4a3?*2;Da3&V48G1S*Wnlbex|{_`$9w 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 X`*~d_@8Ajwje$d*IOW6h4!`ve!rbQ@ diff --git a/doc/img/04.png b/doc/img/04.png new file mode 100644 index 0000000000000000000000000000000000000000..7695fd54314740e8417c02a482a0d39b6c002ebf GIT binary patch literal 109527 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 z zs_j_{LT>$M;U=UM{NU)&zX>Vw*dPsTRcpM6iB=fE0R|3D`4&G#-?`3KSj}uwowu26 z446FgAJj6hspes1l+7EHCn9oZ0z-$ZmZ71T}TjqW#ee4Y{KiH@`u;KIn4*odK3VBdk)U6LzemFy?;mB zhI_xj_9Y$mRBz-$`(`iR6A8(N*!gNau3xSGVOIHcJ^q8Rp8;>g{|JD0jfc4!-VkcGfM=sW`B4je5GLcAw~43qFy=%42aXQ`u=TzHXvuUhvw! zJ(yGdl^OW{chtADc=eZ}rae!-Cta}=dKFuaR8&=Q%dI}It=FhJJ^A)ix!cTbYnRv? zE~z7jl3k*xYEJ8uR|>q!)D#Dl3(O_b5GjD_gs-zDj+FHXKObn2(imd<KaFkBD?Ahz$Cwkd;I<9;rr+tfPnzhB(FQzNL)M`Mk%IJ zaU@w&$zWtz*o^{MLS2JZ@!ztZo<_{xT6aT2%^g2iekgn^lfzUDM~ivero{Cg;X9DQ zPQxV8%9lu#Y@3`s_FFaU6;x@tTv|NEzO5&;fyxmdMAX;*uFXAJ#fH+!I!?8 z3ubNJloeLvPdq@AYPECYn8L)qQ)P6|c4>)RWr_AiP9@ofgGQ&$G-0p)aR zYUG@w6*#>vE*Fcg_^Vp!km=AwdF|Z(dDMH9Rs%&7Ik#&)iVcUjnYnq}*PoZ+03T__ zJIAD%J!`s{gc7LS@|71V6D`?$Up#(CrB3yoynMYx1bjM@?7SE`ua(pG*r@IlBWhz@ z9}DIwjs6WThiVp_k8%Y9^TsVUfrqt07@{XSKK3X2*Gb{{PW-a91*8M($>%Q7@5s{( zSrcT-wdrG&>RN9qh5Ejm{RD)KyObvv%pH7NE$x5X+v~pu%=ERhM%4}Q<=pgY>O2Gx zfedBx6vb-_IG4b=^;Mtf_RV^_Uf0}V3I;}bB^Qarpr`nN!rO#|r-+DvqF8{)u$kRg zYT^fte)0x7cX+uG!sTxY9>=D?u-jImHx(VR0XnXC5h+fzw&#_7$$~;EB2O|0dP67x zpl$v|gfkYo|9ZW!rp>;SRwD}V$3HmZp!#S{(-G1>fqHE3hJd?>DdnxK%Hn(T}pcT=gsJ%wq8{96XV9-yE2INO&8@6WqMY;`?ZO)Ki zT2p}b2G3`f|1k6JxUo{!0XY~6+2dG_YF%@;e#0{{JgryC!5B}6zTde=?VgB<;F%wU?!qmzF;4kH8vmTfkkgK@vq~;)m;k^%^D_ ziIM)WG3CDb>=$hl$(V41(XsV5F3FPU4=g4|)wM((mDNwgxFFxs7%urNFfTdRFm%(% z5co-jZBbd{Q3e2u`Dey?QSf9az6gey<)uXa$aSxiJL zlU^SuFm$Yu@_5chKGY3!=Ncq*N{NgkfqevgejQ$tSGx{8O$?j2`H;_@xPCJ8Zn&*Q z_V^Dch405}wQN|aoYB@$1iPFBA#r!0^cfZ4!&p$m8(8!$JY(x?)m^q4iExBcVjr$) zdGXQFRCY$8nA)H$%1Bvx9j4C)e0+P%PV8vXSi#3Rt;O4umyD LB4> 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 BOxpke From 88993074dfa2dfc3c7b210734890d5e07fd6b6e3 Mon Sep 17 00:00:00 2001 From: Tim Soethout Date: Fri, 8 May 2015 15:35:52 +0200 Subject: [PATCH 082/101] added maven samples with README --- samples/maven/.gitignore | 32 +++++ samples/maven/README | 3 + .../module1/pom.xml | 12 ++ .../src/main/java/module1/HelloWorld.java | 16 +++ .../src/main/scala/module1/HelloScala.scala | 10 ++ .../src/test/java/module1/HelloWorldTest.java | 11 ++ .../src/test/scala/HelloScalaTest.scala | 16 +++ .../module2/pom.xml | 12 ++ .../src/main/java/module2/HelloWorld2.java | 16 +++ .../src/main/scala/module2/HelloScala2.scala | 10 ++ .../src/test/java/HelloWorld2Test.java | 11 ++ .../src/test/scala/HelloScala2Test.scala | 16 +++ .../pom.xml | 117 ++++++++++++++++++ .../maven/combined-scala-java-sonar/pom.xml | 103 +++++++++++++++ .../src/main/java/module1/HelloWorld.java | 16 +++ .../src/main/scala/module1/HelloScala.scala | 10 ++ .../src/test/java/module1/HelloWorldTest.java | 11 ++ .../src/test/scala/HelloScalaTest.scala | 16 +++ 18 files changed, 438 insertions(+) create mode 100644 samples/maven/.gitignore create mode 100644 samples/maven/README create mode 100644 samples/maven/combined-scala-java-multi-module-sonar/module1/pom.xml create mode 100644 samples/maven/combined-scala-java-multi-module-sonar/module1/src/main/java/module1/HelloWorld.java create mode 100644 samples/maven/combined-scala-java-multi-module-sonar/module1/src/main/scala/module1/HelloScala.scala create mode 100644 samples/maven/combined-scala-java-multi-module-sonar/module1/src/test/java/module1/HelloWorldTest.java create mode 100644 samples/maven/combined-scala-java-multi-module-sonar/module1/src/test/scala/HelloScalaTest.scala create mode 100644 samples/maven/combined-scala-java-multi-module-sonar/module2/pom.xml create mode 100644 samples/maven/combined-scala-java-multi-module-sonar/module2/src/main/java/module2/HelloWorld2.java create mode 100644 samples/maven/combined-scala-java-multi-module-sonar/module2/src/main/scala/module2/HelloScala2.scala create mode 100644 samples/maven/combined-scala-java-multi-module-sonar/module2/src/test/java/HelloWorld2Test.java create mode 100644 samples/maven/combined-scala-java-multi-module-sonar/module2/src/test/scala/HelloScala2Test.scala create mode 100644 samples/maven/combined-scala-java-multi-module-sonar/pom.xml create mode 100644 samples/maven/combined-scala-java-sonar/pom.xml create mode 100644 samples/maven/combined-scala-java-sonar/src/main/java/module1/HelloWorld.java create mode 100644 samples/maven/combined-scala-java-sonar/src/main/scala/module1/HelloScala.scala create mode 100644 samples/maven/combined-scala-java-sonar/src/test/java/module1/HelloWorldTest.java create mode 100644 samples/maven/combined-scala-java-sonar/src/test/scala/HelloScalaTest.scala diff --git a/samples/maven/.gitignore b/samples/maven/.gitignore new file mode 100644 index 0000000..ab7ac4c --- /dev/null +++ b/samples/maven/.gitignore @@ -0,0 +1,32 @@ +*.class +*.log + +# Package files +*.war +*.jar +*.ear + +# Maven +out/ +target/ + +# Eclipse +.project +.classpath +.settings/ + +# IDEA +*.iml +.idea + +# OSX +.DS_STORE +.Trashes + +# Windows +Desktop.ini +Thumbs.db + +# Python +*.pyc + diff --git a/samples/maven/README b/samples/maven/README new file mode 100644 index 0000000..73907c2 --- /dev/null +++ b/samples/maven/README @@ -0,0 +1,3 @@ +Run with: + +`mvn clean scoverage:report sonar:sonar` \ No newline at end of file diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module1/pom.xml b/samples/maven/combined-scala-java-multi-module-sonar/module1/pom.xml new file mode 100644 index 0000000..3d2929f --- /dev/null +++ b/samples/maven/combined-scala-java-multi-module-sonar/module1/pom.xml @@ -0,0 +1,12 @@ + + 4.0.0 + + combined-scala-java-multi-module-sonar + test + 1.0.0 + + module1 + jar + 1.0.0 + diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module1/src/main/java/module1/HelloWorld.java b/samples/maven/combined-scala-java-multi-module-sonar/module1/src/main/java/module1/HelloWorld.java new file mode 100644 index 0000000..056006e --- /dev/null +++ b/samples/maven/combined-scala-java-multi-module-sonar/module1/src/main/java/module1/HelloWorld.java @@ -0,0 +1,16 @@ +package module1; + +/** + * Created by tim on 01/05/15. + */ +public class HelloWorld { + + public String hello() { + return "Hello"; + } + + public void notCovered() { + System.out.println("YOLO"); + } + +} diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module1/src/main/scala/module1/HelloScala.scala b/samples/maven/combined-scala-java-multi-module-sonar/module1/src/main/scala/module1/HelloScala.scala new file mode 100644 index 0000000..c7057ca --- /dev/null +++ b/samples/maven/combined-scala-java-multi-module-sonar/module1/src/main/scala/module1/HelloScala.scala @@ -0,0 +1,10 @@ +package module1 + +class HelloScala { + + case class TryOut(some: String, fields: List[String]) + + def test = "Hello" + + def someOther = 42 +} \ No newline at end of file diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module1/src/test/java/module1/HelloWorldTest.java b/samples/maven/combined-scala-java-multi-module-sonar/module1/src/test/java/module1/HelloWorldTest.java new file mode 100644 index 0000000..ebd0817 --- /dev/null +++ b/samples/maven/combined-scala-java-multi-module-sonar/module1/src/test/java/module1/HelloWorldTest.java @@ -0,0 +1,11 @@ +package module1; + +import static org.junit.Assert.assertEquals; + +public class HelloWorldTest { + + @org.junit.Test + public void testHello() throws Exception { + assertEquals("Hello", new HelloWorld().hello()); + } +} \ No newline at end of file diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module1/src/test/scala/HelloScalaTest.scala b/samples/maven/combined-scala-java-multi-module-sonar/module1/src/test/scala/HelloScalaTest.scala new file mode 100644 index 0000000..30dc4da --- /dev/null +++ b/samples/maven/combined-scala-java-multi-module-sonar/module1/src/test/scala/HelloScalaTest.scala @@ -0,0 +1,16 @@ +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner +import org.scalatest.{FlatSpec, ShouldMatchers} +import module1.HelloScala + +@RunWith(classOf[JUnitRunner]) +class HelloScalaTest extends FlatSpec with ShouldMatchers { + + "it" should "work" in { + val scala: HelloScala = new HelloScala() + scala.test should equal("Hello") + + scala.TryOut("String", List()) should not equal(true) + } + +} diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module2/pom.xml b/samples/maven/combined-scala-java-multi-module-sonar/module2/pom.xml new file mode 100644 index 0000000..511f9a4 --- /dev/null +++ b/samples/maven/combined-scala-java-multi-module-sonar/module2/pom.xml @@ -0,0 +1,12 @@ + + 4.0.0 + + combined-scala-java-multi-module-sonar + test + 1.0.0 + + module2 + jar + 1.0.0 + diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module2/src/main/java/module2/HelloWorld2.java b/samples/maven/combined-scala-java-multi-module-sonar/module2/src/main/java/module2/HelloWorld2.java new file mode 100644 index 0000000..c4632f8 --- /dev/null +++ b/samples/maven/combined-scala-java-multi-module-sonar/module2/src/main/java/module2/HelloWorld2.java @@ -0,0 +1,16 @@ +package module2; + +/** + * Created by tim on 01/05/15. + */ +public class HelloWorld2 { + + public String hello() { + return "Hello"; + } + + public void notCovered() { + System.out.println("YOLO"); + } + +} diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module2/src/main/scala/module2/HelloScala2.scala b/samples/maven/combined-scala-java-multi-module-sonar/module2/src/main/scala/module2/HelloScala2.scala new file mode 100644 index 0000000..7047194 --- /dev/null +++ b/samples/maven/combined-scala-java-multi-module-sonar/module2/src/main/scala/module2/HelloScala2.scala @@ -0,0 +1,10 @@ +package module2 + +class HelloScala2 { + + case class TryOut(some: String, fields: List[String]) + + def test = "Hello" + + def someOther = 42 +} \ No newline at end of file diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module2/src/test/java/HelloWorld2Test.java b/samples/maven/combined-scala-java-multi-module-sonar/module2/src/test/java/HelloWorld2Test.java new file mode 100644 index 0000000..329eb10 --- /dev/null +++ b/samples/maven/combined-scala-java-multi-module-sonar/module2/src/test/java/HelloWorld2Test.java @@ -0,0 +1,11 @@ +import module2.HelloWorld2; + +import static org.junit.Assert.assertEquals; + +public class HelloWorld2Test { + + @org.junit.Test + public void testHello() throws Exception { + assertEquals("Hello", new HelloWorld2().hello()); + } +} \ No newline at end of file diff --git a/samples/maven/combined-scala-java-multi-module-sonar/module2/src/test/scala/HelloScala2Test.scala b/samples/maven/combined-scala-java-multi-module-sonar/module2/src/test/scala/HelloScala2Test.scala new file mode 100644 index 0000000..9364c6e --- /dev/null +++ b/samples/maven/combined-scala-java-multi-module-sonar/module2/src/test/scala/HelloScala2Test.scala @@ -0,0 +1,16 @@ +import module2.HelloScala2 +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner +import org.scalatest.{FlatSpec, ShouldMatchers} + +@RunWith(classOf[JUnitRunner]) +class HelloScala2Test extends FlatSpec with ShouldMatchers { + + "it" should "work" in { + val scala: HelloScala2 = new HelloScala2() + scala.test should equal("Hello") + + scala.TryOut("String", List()) should not equal(true) + } + +} diff --git a/samples/maven/combined-scala-java-multi-module-sonar/pom.xml b/samples/maven/combined-scala-java-multi-module-sonar/pom.xml new file mode 100644 index 0000000..234cf6e --- /dev/null +++ b/samples/maven/combined-scala-java-multi-module-sonar/pom.xml @@ -0,0 +1,117 @@ + + 4.0.0 + + test + combined-scala-java-multi-module-sonar + pom + 1.0.0 + + + module1 + module2 + + + + 2.11.6 + + scoverage + jacoco + target/surefire-reports + target/scoverage.xml + target/notthere.xml + src + target/jacoco.exec + src/test/** + UTF-8 + + + + + + + com.google.code.sbt-compiler-maven-plugin + sbt-compiler-maven-plugin + 1.0.0-beta5 + + + + compile + testCompile + addScalaSources + + default-sbt-compile + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.2 + + true + true + + + + + org.jacoco + jacoco-maven-plugin + 0.7.4.201502262128 + + + pre-test + + prepare-agent + + + + + + + org.scoverage + scoverage-maven-plugin + 1.0.4 + + true + + + + scoverage-report + + report + + prepare-package + + + + + + + + + org.scalatest + scalatest_2.11 + 2.2.1 + test + + + org.mockito + mockito-all + 1.9.5 + test + + + junit + junit + 4.11 + + + + ch.qos.logback + logback-classic + 1.0.13 + + + + diff --git a/samples/maven/combined-scala-java-sonar/pom.xml b/samples/maven/combined-scala-java-sonar/pom.xml new file mode 100644 index 0000000..1a0a890 --- /dev/null +++ b/samples/maven/combined-scala-java-sonar/pom.xml @@ -0,0 +1,103 @@ + + 4.0.0 + + test + combined-scala-java-sonar-single-module + jar + 1.0.0 + + + 2.11.6 + + scoverage + jacoco + target/surefire-reports + target/scoverage.xml + target/notthere.xml + src + target/jacoco.exec + src/test/java/**,src/test/scala/** + UTF-8 + + + + + + + com.google.code.sbt-compiler-maven-plugin + sbt-compiler-maven-plugin + 1.0.0-beta5 + + + + compile + testCompile + addScalaSources + + default-sbt-compile + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.2 + + true + true + + + + + org.jacoco + jacoco-maven-plugin + 0.7.4.201502262128 + + + pre-test + + prepare-agent + + + + + + + org.scoverage + scoverage-maven-plugin + 1.0.4 + + true + + + + + + + + org.scalatest + scalatest_2.11 + 2.2.1 + test + + + org.mockito + mockito-all + 1.9.5 + test + + + junit + junit + 4.11 + + + + ch.qos.logback + logback-classic + 1.0.13 + + + + diff --git a/samples/maven/combined-scala-java-sonar/src/main/java/module1/HelloWorld.java b/samples/maven/combined-scala-java-sonar/src/main/java/module1/HelloWorld.java new file mode 100644 index 0000000..056006e --- /dev/null +++ b/samples/maven/combined-scala-java-sonar/src/main/java/module1/HelloWorld.java @@ -0,0 +1,16 @@ +package module1; + +/** + * Created by tim on 01/05/15. + */ +public class HelloWorld { + + public String hello() { + return "Hello"; + } + + public void notCovered() { + System.out.println("YOLO"); + } + +} diff --git a/samples/maven/combined-scala-java-sonar/src/main/scala/module1/HelloScala.scala b/samples/maven/combined-scala-java-sonar/src/main/scala/module1/HelloScala.scala new file mode 100644 index 0000000..c7057ca --- /dev/null +++ b/samples/maven/combined-scala-java-sonar/src/main/scala/module1/HelloScala.scala @@ -0,0 +1,10 @@ +package module1 + +class HelloScala { + + case class TryOut(some: String, fields: List[String]) + + def test = "Hello" + + def someOther = 42 +} \ No newline at end of file diff --git a/samples/maven/combined-scala-java-sonar/src/test/java/module1/HelloWorldTest.java b/samples/maven/combined-scala-java-sonar/src/test/java/module1/HelloWorldTest.java new file mode 100644 index 0000000..ebd0817 --- /dev/null +++ b/samples/maven/combined-scala-java-sonar/src/test/java/module1/HelloWorldTest.java @@ -0,0 +1,11 @@ +package module1; + +import static org.junit.Assert.assertEquals; + +public class HelloWorldTest { + + @org.junit.Test + public void testHello() throws Exception { + assertEquals("Hello", new HelloWorld().hello()); + } +} \ No newline at end of file diff --git a/samples/maven/combined-scala-java-sonar/src/test/scala/HelloScalaTest.scala b/samples/maven/combined-scala-java-sonar/src/test/scala/HelloScalaTest.scala new file mode 100644 index 0000000..30dc4da --- /dev/null +++ b/samples/maven/combined-scala-java-sonar/src/test/scala/HelloScalaTest.scala @@ -0,0 +1,16 @@ +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner +import org.scalatest.{FlatSpec, ShouldMatchers} +import module1.HelloScala + +@RunWith(classOf[JUnitRunner]) +class HelloScalaTest extends FlatSpec with ShouldMatchers { + + "it" should "work" in { + val scala: HelloScala = new HelloScala() + scala.test should equal("Hello") + + scala.TryOut("String", List()) should not equal(true) + } + +} From 4c7f646c7040032f3c74f4719a88daa384630917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=20Buransk=C3=BD?= Date: Sat, 9 May 2015 14:52:49 +0200 Subject: [PATCH 083/101] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ac4f271..6601ead 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ just plain average of coverage rates for sub-projects. ### Support for older versions of Sonar ### - [SonarQube 4.5] (https://github.com/RadoBuransky/sonar-scoverage-plugin/tree/sonar45) -- [SonarQube 4.2] (https://github.com/RadoBuransky/sonar-scoverage-plugin/tree/sonar45) +- [SonarQube 4.2] (https://github.com/RadoBuransky/sonar-scoverage-plugin/tree/sonar42) - [SonarQube 3.5] (https://github.com/RadoBuransky/sonar-scoverage-plugin/tree/sonar35) ## Installation ## @@ -101,4 +101,4 @@ Source code markup with covered and uncovered lines: [LatestPluginJar]: https://github.com/RadoBuransky/sonar-scoverage-plugin/releases/download/v5.1.1/sonar-scoverage-plugin-5.1.1.jar [SonarQube]: http://www.sonarqube.org/ "SonarQube" [Scoverage]: https://github.com/scoverage/scalac-scoverage-plugin "Scoverage" -[sbt-scoverage]: https://github.com/scoverage/sbt-scoverage \ No newline at end of file +[sbt-scoverage]: https://github.com/scoverage/sbt-scoverage From 1c0daf2ae92658bfbf47c2f294de0fc0151d7ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=20Buransk=C3=BD?= Date: Sat, 9 May 2015 14:53:56 +0200 Subject: [PATCH 084/101] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6601ead..71596c3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ #Scoverage Plugin for Sonar# [![Build Status](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin.png)](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin) +[![License](http://img.shields.io/:license-Apache%202-red.svg)](http://www.apache.org/licenses/LICENSE-2.0.txt) [![Analytics](https://ga-beacon.appspot.com/UA-55603212-2/sonar-scoverage-plugin)](https://github.com/igrigorik/ga-beacon) Plugin for [SonarQube] that imports statement coverage generated by [Scoverage] for Scala projects. From a8e03558efe0aa7129319e0b8ff9473b6272b42b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rado=20Buransk=C3=BD?= Date: Sat, 9 May 2015 14:57:51 +0200 Subject: [PATCH 085/101] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71596c3..3386ac4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ #Scoverage Plugin for Sonar# [![Build Status](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin.png)](https://travis-ci.org/RadoBuransky/sonar-scoverage-plugin) -[![License](http://img.shields.io/:license-Apache%202-red.svg)](http://www.apache.org/licenses/LICENSE-2.0.txt) +[![License](https://img.shields.io/badge/license-LGPL-orange.svg)](http://www.gnu.org/licenses/lgpl.txt) [![Analytics](https://ga-beacon.appspot.com/UA-55603212-2/sonar-scoverage-plugin)](https://github.com/igrigorik/ga-beacon) Plugin for [SonarQube] that imports statement coverage generated by [Scoverage] for Scala projects. From f9e5ea0c88126e3991d9c82c89a1d521cd447068 Mon Sep 17 00:00:00 2001 From: Tim Soethout Date: Mon, 11 May 2015 14:45:24 +0200 Subject: [PATCH 086/101] Fixed coverage reporting in sonar - instrumentation should now also work correctly for maven package --- .../combined-scala-java-multi-module-sonar/pom.xml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/samples/maven/combined-scala-java-multi-module-sonar/pom.xml b/samples/maven/combined-scala-java-multi-module-sonar/pom.xml index 234cf6e..000db69 100644 --- a/samples/maven/combined-scala-java-multi-module-sonar/pom.xml +++ b/samples/maven/combined-scala-java-multi-module-sonar/pom.xml @@ -14,12 +14,12 @@ 2.11.6 + 0.13.8 scoverage jacoco target/surefire-reports target/scoverage.xml - target/notthere.xml src target/jacoco.exec src/test/** @@ -76,10 +76,20 @@ true + + instrument + + + pre-compile + + post-compile + + scoverage-report - report + + report-only prepare-package From 8b78a992f85d583f211ff69eef155cf2999003c7 Mon Sep 17 00:00:00 2001 From: Michael Zinsmaier Date: Tue, 20 Oct 2015 00:23:42 +0200 Subject: [PATCH 087/101] Removed unused class The stub parser is not used anymore, removed it --- .../xml/StubScoverageReportParser.scala | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 plugin/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala deleted file mode 100644 index adcb548..0000000 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/StubScoverageReportParser.scala +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Sonar Scoverage Plugin - * Copyright (C) 2013 Rado Buransky - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scoverage.xml - -import com.buransky.plugins.scoverage._ -import com.buransky.plugins.scoverage.ProjectStatementCoverage -import com.buransky.plugins.scoverage.CoveredStatement -import com.buransky.plugins.scoverage.StatementPosition -import com.buransky.plugins.scoverage.FileStatementCoverage -import com.buransky.plugins.scoverage.DirectoryStatementCoverage -import scala.io.Source - -/** - * Stub with some dummy data so that we don't have to parse XML for testing. - * - * @author Rado Buransky - */ -class StubScoverageReportParser extends ScoverageReportParser { - def parse(reportFilePath: String): ProjectStatementCoverage = { - val errorCodeFile = FileStatementCoverage("ErrorCode.scala", 17, 13, - List(simpleStatement(10, 2), simpleStatement(11, 0), - simpleStatement(25, 1))) - - val graphFile = FileStatementCoverage("Graph.scala", 42, 0, - List(simpleStatement(33, 0), simpleStatement(3, 1), simpleStatement(1, 0), simpleStatement(2, 2))) - - val file2 = FileStatementCoverage("file2.scala", 2, 1, Nil) - val bbbDir = DirectoryStatementCoverage("bbb", Seq(file2)) - - val file1 = FileStatementCoverage("file1.scala", 100, 33, Nil) - val aaaDir = DirectoryStatementCoverage("aaa", Seq(file1, errorCodeFile, graphFile, bbbDir)) - - val project = ProjectStatementCoverage("project", Seq(aaaDir)) - - project - } - - def parse(source: Source): ProjectStatementCoverage = { - ProjectStatementCoverage("x", Nil) - } - - private def simpleStatement(line: Int, hitCount: Int): CoveredStatement = - CoveredStatement(StatementPosition(line, 0), StatementPosition(line, 0), hitCount) - -} From 6d1afc970a095ffd4ed3abb27e3cf9673ef06106 Mon Sep 17 00:00:00 2001 From: Michael Zinsmaier Date: Tue, 20 Oct 2015 21:43:24 +0200 Subject: [PATCH 088/101] rewrote PathUtil to work with file.getParentFile Justification: relying on getParentFile gets the path splitting working under windows and unix - adapted the tests to the new behavior - added a windows / unix switch in the tests to get them working on both systems PathUtils now converts: - the empty path to an empty List - an absolute path to a list folders (not including the drive name under windows) - a relative path to a list of folders --- .../plugins/scoverage/util/PathUtil.scala | 21 +++++++++---- .../plugins/scoverage/util/PathUtilSpec.scala | 31 ++++++++++++------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala index d4a8938..1981fcf 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala @@ -20,15 +20,24 @@ package com.buransky.plugins.scoverage.util import java.io.File +import scala.Iterator /** * File path helper. * * @author Rado Buransky */ object PathUtil { - def splitPath(filePath: String, separator: String = File.separator): List[String] = - filePath.split(separator.replaceAllLiterally("\\", "\\\\")).toList match { - case "" :: tail if tail.nonEmpty => separator :: tail - case other => other - } -} + + def splitPath(filePath: String): List[String] = { + new FileParentIterator(new File(filePath)).toList.reverse + } + + class FileParentIterator(private var f: File) extends Iterator[String] { + def hasNext: Boolean = f != null && !f.getName().isEmpty() + def next(): String = { + val name = f.getName() + f = f.getParentFile + name + } + } +} \ No newline at end of file diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala index bae563d..4f095ef 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala @@ -24,23 +24,30 @@ import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner @RunWith(classOf[JUnitRunner]) -class UnixPathUtilSpec extends ParamPathUtilSpec("Unix", "/") - -@RunWith(classOf[JUnitRunner]) -class WindowsPathUtilSpec extends ParamPathUtilSpec("Windows", "\\") - -abstract class ParamPathUtilSpec(osName: String, separator: String) extends FlatSpec with Matchers { +class PathUtilSpec extends FlatSpec with Matchers { + + val osName = System.getProperty("os.name") + val separator = System.getProperty("file.separator") + behavior of s"splitPath for $osName" - - it should "work for empty path" in { - PathUtil.splitPath("", separator) should equal(List("")) + + it should "ignore the empty path" in { + PathUtil.splitPath("") should equal(List.empty[String]) } - it should "work with separator at the beginning" in { - PathUtil.splitPath(s"${separator}a", separator) should equal(List(separator, "a")) + it should "ignore a separator at the beginning" in { + PathUtil.splitPath(s"${separator}a") should equal(List("a")) } it should "work with separator in the middle" in { - PathUtil.splitPath(s"a${separator}b", separator) should equal(List("a", "b")) + PathUtil.splitPath(s"a${separator}b") should equal(List("a", "b")) + } + + it should "work with an OS dependent absolute path" in { + if (osName.startsWith("Windows")) { + PathUtil.splitPath("C:\\test\\2") should equal(List("test", "2")) + } else { + PathUtil.splitPath("/test/2") should equal(List("test", "2")) + } } } \ No newline at end of file From 1209dfaff0bd514e732d3bac2b942e325e766921 Mon Sep 17 00:00:00 2001 From: Michael Zinsmaier Date: Tue, 20 Oct 2015 22:19:19 +0200 Subject: [PATCH 089/101] Switched to source directory relative pathes, sanitizing reported pathes Justification: - sanitizing the reported file pathes to a common format makes the sensor simpler - it is a precondition for implementing proper directory coverage - tests run now under unix and windows, easier to develop The sonar.sources property can be read from the settings. By mapping all reported pathes (absolute / base dir relative / source dir relative) against the actual file tree it is possible to converte all of them to the same source dir relative format. For instance /home/src/main/scala/folder/test0.scala => folder/test0.scala folder/test1.scala => folder/test1.scala /src/main/scala/test2.scala => test2.scala ... --- .../scoverage/ScoverageReportParser.scala | 4 +- .../BruteForceSequenceMatcher.scala | 86 +++++++++++++++++++ .../scoverage/pathcleaner/PathSanitizer.scala | 34 ++++++++ .../scoverage/sensor/ScoverageSensor.scala | 28 ++++-- ...XmlScoverageReportConstructingParser.scala | 56 +++++++----- .../xml/XmlScoverageReportParser.scala | 5 +- .../sensor/ScoverageSensorSpec.scala | 10 +-- ...coverageReportConstructingParserSpec.scala | 31 +++++-- .../xml/XmlScoverageReportParserSpec.scala | 6 +- 9 files changed, 212 insertions(+), 48 deletions(-) create mode 100644 plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala create mode 100644 plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/PathSanitizer.scala diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala index b3c84c6..72d40d0 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala @@ -19,13 +19,15 @@ */ package com.buransky.plugins.scoverage +import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer + /** * Interface for Scoverage report parser. * * @author Rado Buransky */ trait ScoverageReportParser { - def parse(reportFilePath: String): ProjectStatementCoverage + def parse(reportFilePath: String, pathSanitizer: PathSanitizer): ProjectStatementCoverage } /** diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala new file mode 100644 index 0000000..5acd4f7 --- /dev/null +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala @@ -0,0 +1,86 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scoverage.pathcleaner + +import java.io.File +import org.apache.commons.io.FileUtils +import org.apache.commons.io.FileUtils +import BruteForceSequenceMatcher._ +import com.buransky.plugins.scoverage.util.PathUtil +import scala.collection.JavaConversions._ +import org.sonar.api.utils.log.Loggers + +object BruteForceSequenceMatcher { + + val extensions = Array[String]("java", "scala") + + type PathSeq = Seq[String] +} + +/** + * Helper that allows to convert a report path into a source folder relative path by testing it against + * the tree of source files. + * + * Assumes that all report paths of a given report have a common root. Dependent of the scoverage + * report this root is either something outside the actual project (absolute path), the base dir of the project + * (report path relative to base dir) or some sub folder of the project. + * + * By reverse mapping a report path against the tree of all file children of the source folder the correct filesystem file + * can be found and the report path can be converted into a source dir relative path. * + * + * @author Michael Zinsmaier + */ +class BruteForceSequenceMatcher(baseDir: File, sourcePath: String) extends PathSanitizer { + + private val sourceDir = initSourceDir() + require(sourceDir.isAbsolute) + require(sourceDir.isDirectory) + + private val log = Loggers.get(classOf[BruteForceSequenceMatcher]) + private val sourcePathLength = PathUtil.splitPath(sourceDir.getAbsolutePath).size + private val filesMap = initFilesMap() + + + def getSourceRelativePath(reportPath: PathSeq): Option[PathSeq] = { + // match with file system map of files + val relPathOption = for { + absPathCandidates <- filesMap.get(reportPath.last) + path <- absPathCandidates.find(absPath => absPath.endsWith(reportPath)) + } yield path.drop(sourcePathLength) + + relPathOption + } + + // mock able helpers that allow us to remove the dependency to the real file system during tests + + private[pathcleaner] def initSourceDir(): File = { + val sourceDir = new File(baseDir, sourcePath) + sourceDir + } + + private[pathcleaner] def initFilesMap(): Map[String, Seq[PathSeq]] = { + val srcFiles = FileUtils.iterateFiles(sourceDir, extensions, true) + val paths = srcFiles.map(file => PathUtil.splitPath(file.getAbsolutePath)).toSeq + + // group them by filename, in case multiple files have the same name + paths.groupBy(path => path.last) + } + +} \ No newline at end of file diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/PathSanitizer.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/PathSanitizer.scala new file mode 100644 index 0000000..d4a1b79 --- /dev/null +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/PathSanitizer.scala @@ -0,0 +1,34 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scoverage.pathcleaner + +/** + * @author Michael Zinsmaier + */ +trait PathSanitizer { + + /** tries to convert the given path such that it is relative to the + * projects/modules source directory. + * + * @return Some(source folder relative path) or None if the path cannot be converted + */ + def getSourceRelativePath(path: Seq[String]): Option[Seq[String]] + +} \ No newline at end of file diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala index bad6f98..e488410 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala @@ -35,6 +35,8 @@ import org.sonar.api.scan.filesystem.PathResolver import org.sonar.api.utils.log.Loggers import scala.collection.JavaConversions._ +import com.buransky.plugins.scoverage.pathcleaner.BruteForceSequenceMatcher +import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer /** * Main sensor for importing Scoverage report to Sonar. @@ -53,7 +55,16 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem scoverageReportPath match { case Some(reportPath) => // Single-module project - processProject(scoverageReportParser.parse(reportPath), project, context) + val srcOption = Option(settings.getString(project.getName() + ".sonar.sources")) + val sonarSources = srcOption match { + case Some(src) => src + case None => { + log.warn(s"could not find settings key ${project.getName()}.sonar.sources assuming src/main/scala.") + "src/main/scala" + } + } + val pathSanitizer = createPathSanitizer(sonarSources) + processProject(scoverageReportParser.parse(reportPath, pathSanitizer), project, context, sonarSources) case None => // Multi-module project has report path set for each module individually @@ -63,6 +74,9 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem override val toString = getClass.getSimpleName + protected def createPathSanitizer(sonarSources: String): PathSanitizer + = new BruteForceSequenceMatcher(fileSystem.baseDir(), sonarSources) + private lazy val scoverageReportPath: Option[String] = { settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY) match { case null => None @@ -127,14 +141,14 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem } } - private def processProject(projectCoverage: ProjectStatementCoverage, project: Project, context: SensorContext) { + private def processProject(projectCoverage: ProjectStatementCoverage, project: Project, context: SensorContext, sonarSources: String) { // Save measures saveMeasures(context, project, projectCoverage) log.info(LogUtil.f("Statement coverage for " + project.getKey + " is " + ("%1.2f" format projectCoverage.rate))) // Process children - processChildren(projectCoverage.children, context, "") + processChildren(projectCoverage.children, context, sonarSources) } private def processDirectory(directoryCoverage: DirectoryStatementCoverage, context: SensorContext, @@ -147,9 +161,8 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem val path = appendFilePath(directory, fileCoverage.name) val p = fileSystem.predicates() - val pathPredicate = if (new io.File(path).isAbsolute) p.hasAbsolutePath(path) else p.matchesPathPattern("**/" + path) val files = fileSystem.inputFiles(p.and( - pathPredicate, + p.hasRelativePath(path), p.hasLanguage(scala.getKey), p.hasType(InputFile.Type.MAIN))).toList @@ -164,10 +177,7 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem saveLineCoverage(fileCoverage.statements, scalaSourceFile, context) case None => { - fileSystem.inputFiles(p.all()).foreach { inputFile => - log.debug(inputFile.absolutePath()) - } - log.warn(s"File not found in file system! [$pathPredicate]") + log.warn(s"File not found in file system! [$path]") } } } diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala index d597aa7..77390db 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala @@ -30,13 +30,14 @@ import scala.collection.mutable import scala.io.Source import scala.xml.parsing.ConstructingParser import scala.xml.{MetaData, NamespaceBinding, Text} +import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer /** * Scoverage XML parser based on ConstructingParser provided by standard Scala library. * * @author Rado Buransky */ -class XmlScoverageReportConstructingParser(source: Source) extends ConstructingParser(source, false) { +class XmlScoverageReportConstructingParser(source: Source, pathSanitizer: PathSanitizer) extends ConstructingParser(source, false) { private val log = Loggers.get(classOf[XmlScoverageReportConstructingParser]) private val CLASS_ELEMENT = "class" @@ -163,7 +164,7 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP val files = fileStatementCoverage(statementsInFile) // Transform file paths to chain of case classes - val chained = files.map(fsc => pathToChain(fsc._1, fsc._2)) + val chained = files.map(fsc => pathToChain(fsc._1, fsc._2)).flatten // Merge chains into one tree val root = DirOrFile("", Nil, None) @@ -173,31 +174,42 @@ class XmlScoverageReportConstructingParser(source: Source) extends ConstructingP root.toProjectStatementCoverage } - private def pathToChain(filePath: String, coverage: FileStatementCoverage): DirOrFile = { + private def pathToChain(filePath: String, coverage: FileStatementCoverage): Option[DirOrFile] = { + // helper + def convertToDirOrFile(relPath: Seq[String]) = { + // Get directories + val dirs = for (i <- 0 to relPath.length - 2) + yield DirOrFile(relPath(i), Nil, None) + + // Chain directories + for (i <- 0 to dirs.length - 2) + dirs(i).children = List(dirs(i + 1)) + + // Get file + val file = DirOrFile(relPath(relPath.length - 1).toString, Nil, Some(coverage)) + + if (dirs.isEmpty) { + // File in root dir + file + } else { + // Append file + dirs.last.children = List(file) + dirs.head + } + } + + // processing val path = PathUtil.splitPath(filePath) if (path.length < 1) throw new ScoverageException("Path cannot be empty!") - // Get directories - val dirs = for (i <- 0 to path.length - 2) - yield DirOrFile(path(i), Nil, None) - - // Chain directories - for (i <- 0 to dirs.length - 2) - dirs(i).children = List(dirs(i + 1)) - - // Get file - val file = DirOrFile(path(path.length - 1).toString, Nil, Some(coverage)) - - if (dirs.isEmpty) { - // File in root dir - file - } - else { - // Append file - dirs.last.children = List(file) - dirs.head + pathSanitizer.getSourceRelativePath(path) match { + case Some(relPath) => Some(convertToDirOrFile(relPath)) + case None => { + log.warn(s"skipping file coverage results for $path, was not able to retrieve the file in the configured source dir") + None + } } } diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala index b5f1c5a..fe2036a 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala @@ -24,6 +24,7 @@ import com.buransky.plugins.scoverage.{ProjectStatementCoverage, ScoverageExcept import org.sonar.api.utils.log.Loggers import scala.io.Source +import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer /** * Bridge between parser implementation and coverage provider. @@ -33,13 +34,13 @@ import scala.io.Source class XmlScoverageReportParser extends ScoverageReportParser { private val log = Loggers.get(classOf[XmlScoverageReportParser]) - def parse(reportFilePath: String): ProjectStatementCoverage = { + def parse(reportFilePath: String, pathSanitizer: PathSanitizer): ProjectStatementCoverage = { require(reportFilePath != null) require(!reportFilePath.trim.isEmpty) log.debug(LogUtil.f("Reading report. [" + reportFilePath + "]")) - val parser = new XmlScoverageReportConstructingParser(sourceFromFile(reportFilePath)) + val parser = new XmlScoverageReportConstructingParser(sourceFromFile(reportFilePath), pathSanitizer) parser.parse() } diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala index 1e0c2ec..8748cda 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala @@ -36,6 +36,8 @@ import org.sonar.api.resources.Project.AnalysisType import org.sonar.api.scan.filesystem.PathResolver import scala.collection.JavaConversions._ +import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer +import org.mockito.Matchers.any @RunWith(classOf[JUnitRunner]) @@ -94,15 +96,12 @@ class ScoverageSensorSpec extends FlatSpec with Matchers with MockitoSugar { when(settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY)).thenReturn(pathToScoverageReport) when(fileSystem.baseDir).thenReturn(moduleBaseDir) when(fileSystem.predicates).thenReturn(filePredicates) - when(fileSystem.inputFiles(org.mockito.Matchers.any[FilePredicate]())).thenReturn(Nil) + when(fileSystem.inputFiles(any[FilePredicate]())).thenReturn(Nil) when(pathResolver.relativeFile(moduleBaseDir, pathToScoverageReport)).thenReturn(reportFile) - when(scoverageReportParser.parse(reportAbsolutePath)).thenReturn(projectStatementCoverage) + when(scoverageReportParser.parse(any[String](), any[PathSanitizer]())).thenReturn(projectStatementCoverage) // Execute analyse(project, context) - - verify(filePredicates).hasAbsolutePath("/home/a.scala") - verify(filePredicates).matchesPathPattern("**/x/b.scala") } class AnalyseScoverageSensorScope extends ScoverageSensorScope { @@ -110,6 +109,7 @@ class ScoverageSensorSpec extends FlatSpec with Matchers with MockitoSugar { val context = new TestSensorContext override protected lazy val scoverageReportParser = mock[ScoverageReportParser] + override protected def createPathSanitizer(sonarSources: String) = mock[PathSanitizer] } class ScoverageSensorScope extends { diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala index aa4325e..3244847 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala @@ -26,33 +26,52 @@ import scala.io.Source import com.buransky.plugins.scoverage.xml.data.XmlReportFile1 import scala._ import com.buransky.plugins.scoverage.{ProjectStatementCoverage, FileStatementCoverage, DirectoryStatementCoverage} +import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer @RunWith(classOf[JUnitRunner]) class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { behavior of "parse source" it must "parse old broken Scoverage 0.95 file correctly" in { - assertReportFile(XmlReportFile1.scoverage095Data, 24.53)(assertScoverage095Data) + val sanitizer = new PathSanitizer() { + def getSourceRelativePath(path: Seq[String]): Option[Seq[String]] = { + // do nothing + Some(path) + } + } + assertReportFile(XmlReportFile1.scoverage095Data, 24.53, sanitizer)(assertScoverage095Data) } it must "parse new fixed Scoverage 1.0.4 file correctly" in { - assertReportFile(XmlReportFile1.scoverage104Data, 50.0) { projectCoverage => + val sanitizer = new PathSanitizer() { + def getSourceRelativePath(path: Seq[String]): Option[Seq[String]] = { + // drop first 6 = /a1b2c3/workspace/sonar-test/src/main/scala + Some(path.drop(6)) + } + } + assertReportFile(XmlReportFile1.scoverage104Data, 50.0, sanitizer) { projectCoverage => assert(projectCoverage.name === "") assert(projectCoverage.children.size.toInt === 1) projectCoverage.children.head match { case rootDir: DirectoryStatementCoverage => - assert(rootDir.name == "a1b2c3") + assert(rootDir.name == "com") case other => fail(s"This is not a directory statement coverage! [$other]") } } } it must "parse file1 correctly even without XML declaration" in { - assertReportFile(XmlReportFile1.dataWithoutDeclaration, 24.53)(assertScoverage095Data) + val sanitizer = new PathSanitizer() { + def getSourceRelativePath(path: Seq[String]): Option[Seq[String]] = { + // do nothing + Some(path) + } + } + assertReportFile(XmlReportFile1.dataWithoutDeclaration, 24.53, sanitizer)(assertScoverage095Data) } - private def assertReportFile(data: String, expectedCoverage: Double)(f: (ProjectStatementCoverage) => Unit) { - val parser = new XmlScoverageReportConstructingParser(Source.fromString(data)) + private def assertReportFile(data: String, expectedCoverage: Double, pathSanitizer: PathSanitizer)(f: (ProjectStatementCoverage) => Unit) { + val parser = new XmlScoverageReportConstructingParser(Source.fromString(data), pathSanitizer) val projectCoverage = parser.parse() // Assert coverage diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala index c049737..5d457c7 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala @@ -29,14 +29,14 @@ class XmlScoverageReportParserSpec extends FlatSpec with Matchers { behavior of "parse file path" it must "fail for null path" in { - the[IllegalArgumentException] thrownBy XmlScoverageReportParser().parse(null.asInstanceOf[String]) + the[IllegalArgumentException] thrownBy XmlScoverageReportParser().parse(null.asInstanceOf[String], null) } it must "fail for empty path" in { - the[IllegalArgumentException] thrownBy XmlScoverageReportParser().parse("") + the[IllegalArgumentException] thrownBy XmlScoverageReportParser().parse("", null) } it must "fail for not existing path" in { - the[ScoverageException] thrownBy XmlScoverageReportParser().parse("/x/a/b/c/1/2/3/4.xml") + the[ScoverageException] thrownBy XmlScoverageReportParser().parse("/x/a/b/c/1/2/3/4.xml", null) } } From f6654ce3aecc476b06206ab5a945f3be851c3041 Mon Sep 17 00:00:00 2001 From: Michael Zinsmaier Date: Tue, 20 Oct 2015 22:27:25 +0200 Subject: [PATCH 090/101] added some tests for the brute force path sanitizer - testing absolute report file paths - testing base dir relative report file paths - testing source dir relative report file paths --- .../BruteForceSequenceMatcherSpec.scala | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 plugin/src/test/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcherSpec.scala diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcherSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcherSpec.scala new file mode 100644 index 0000000..5fec3f6 --- /dev/null +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcherSpec.scala @@ -0,0 +1,117 @@ +/* + * Sonar Scoverage Plugin + * Copyright (C) 2013 Rado Buransky + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package com.buransky.plugins.scoverage.pathcleaner + +import org.junit.runner.RunWith +import org.scalatest.mock.MockitoSugar +import org.scalatest.junit.JUnitRunner +import org.scalatest.FlatSpec +import com.buransky.plugins.scoverage.pathcleaner.BruteForceSequenceMatcher.PathSeq +import org.scalatest.Matchers +import java.io.File +import org.mockito.Mockito._ + +@RunWith(classOf[JUnitRunner]) +class BruteForceSequenceMatcherSpec extends FlatSpec with Matchers with MockitoSugar { + + // file-map of all files under baseDir/sonar.sources organized by their filename + val filesMap: Map[String, Seq[PathSeq]] = Map ( + "rootTestFile.scala" -> List(List("testProject", "main", "rootTestFile.scala")), + "nestedTestFile.scala" -> List(List("testProject", "main", "some", "folders", "nestedTestFile.scala")), + "multiFile.scala" -> List( + List("testProject", "main", "some", "multiFile.scala"), + List("testProject", "main", "some", "folder", "multiFile.scala") + ) + ) + + // baseDir = testProject sonar.sources = main + val testee = new BruteForceSequenceMatcherTestee("/testProject/main", filesMap) + + + + behavior of "BruteForceSequenceMatcher with absolute report filenames" + + it should "provide just the filename for top level files" in { + testee.getSourceRelativePath(List("testProject", "main", "rootTestFile.scala")).get shouldEqual List("rootTestFile.scala") + } + + it should "provide the filename and the folders for nested files" in { + testee.getSourceRelativePath(List("testProject", "main", "some", "folders", "nestedTestFile.scala")).get shouldEqual List("some", "folders", "nestedTestFile.scala") + } + + it should "find the correct file if multiple files with same name exist" in { + testee.getSourceRelativePath(List("testProject", "main", "some", "multiFile.scala")).get shouldEqual List("some", "multiFile.scala") + testee.getSourceRelativePath(List("testProject", "main", "some", "folder", "multiFile.scala")).get shouldEqual List("some", "folder", "multiFile.scala") + } + + + + + behavior of "BruteForceSequenceMatcher with filenames relative to the base dir" + + it should "provide just the filename for top level files" in { + testee.getSourceRelativePath(List("main", "rootTestFile.scala")).get shouldEqual List("rootTestFile.scala") + } + + it should "provide the filename and the folders for nested files" in { + testee.getSourceRelativePath(List("main", "some", "folders", "nestedTestFile.scala")).get shouldEqual List("some", "folders", "nestedTestFile.scala") + } + + it should "find the correct file if multiple files with same name exist" in { + testee.getSourceRelativePath(List("main", "some", "multiFile.scala")).get shouldEqual List("some", "multiFile.scala") + testee.getSourceRelativePath(List("main", "some", "folder", "multiFile.scala")).get shouldEqual List("some", "folder", "multiFile.scala") + } + + + + + behavior of "BruteForceSequenceMatcher with filenames relative to the src dir" + + it should "provide just the filename for top level files" in { + testee.getSourceRelativePath(List("rootTestFile.scala")).get shouldEqual List("rootTestFile.scala") + } + + it should "provide the filename and the folders for nested files" in { + testee.getSourceRelativePath(List("some", "folders", "nestedTestFile.scala")).get shouldEqual List("some", "folders", "nestedTestFile.scala") + } + + it should "find the correct file if multiple files with same name exist" in { + testee.getSourceRelativePath(List("some", "multiFile.scala")).get shouldEqual List("some", "multiFile.scala") + testee.getSourceRelativePath(List("some", "folder", "multiFile.scala")).get shouldEqual List("some", "folder", "multiFile.scala") + } + + + + + class BruteForceSequenceMatcherTestee(absoluteSrcPath: String, filesMap: Map[String, Seq[PathSeq]]) + extends BruteForceSequenceMatcher(mock[File], "") { + + def srcDir = { + val dir = mock[File] + when(dir.isAbsolute).thenReturn(true) + when(dir.isDirectory).thenReturn(true) + when(dir.getAbsolutePath).thenReturn(absoluteSrcPath) + dir + } + + override private[pathcleaner] def initSourceDir(): File = srcDir + override private[pathcleaner] def initFilesMap(): Map[String, Seq[PathSeq]] = filesMap + } +} From 80e787b8108a66d28021c730a92562980b5d792f Mon Sep 17 00:00:00 2001 From: Michael Zinsmaier Date: Tue, 20 Oct 2015 22:41:19 +0200 Subject: [PATCH 091/101] Added total statements metric to avoid overlaps with coremetrics If the core metric STATEMENTS value is already set scoverage will not overwrite the value but the existing value will also not align with the covered statement values. Using a separated value solves this problem - Added totalStatements to the plugin metrics --- .../plugins/scoverage/measure/ScalaMetrics.scala | 11 ++++++++++- .../plugins/scoverage/sensor/ScoverageSensor.scala | 9 ++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala index 4b7434d..dd08c7b 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala @@ -30,12 +30,13 @@ import scala.collection.mutable.ListBuffer * @author Rado Buransky */ class ScalaMetrics extends Metrics { - override def getMetrics = ListBuffer(ScalaMetrics.statementCoverage, ScalaMetrics.coveredStatements).toList + override def getMetrics = ListBuffer(ScalaMetrics.statementCoverage, ScalaMetrics.coveredStatements, ScalaMetrics.totalStatements).toList } object ScalaMetrics { private val STATEMENT_COVERAGE_KEY = "scoverage" private val COVERED_STATEMENTS_KEY = "covered_statements" + private val TOTAL_STATEMENTS_KEY = "total_statements" lazy val statementCoverage = new Metric.Builder(STATEMENT_COVERAGE_KEY, "Statement coverage", ValueType.PERCENT) @@ -55,4 +56,12 @@ object ScalaMetrics { .setDomain(CoreMetrics.DOMAIN_SIZE) .setFormula(new org.sonar.api.measures.SumChildValuesFormula(false)) .create[java.lang.Integer]() + + lazy val totalStatements = new Metric.Builder(TOTAL_STATEMENTS_KEY, + "Total statements", Metric.ValueType.INT) + .setDescription("Number of all statements covered by tests and uncovered") + .setDirection(Metric.DIRECTION_BETTER) + .setQualitative(false) + .setDomain(CoreMetrics.DOMAIN_SIZE) + .create[java.lang.Integer]() } \ No newline at end of file diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala index bad6f98..a89dab7 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala @@ -114,7 +114,7 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem private def analyseStatementCountForModule(module: Project, context: SensorContext): Long = { // Aggregate modules - context.getMeasure(module, CoreMetrics.STATEMENTS) match { + context.getMeasure(module, ScalaMetrics.totalStatements) match { case null => log.debug(LogUtil.f("Module has no number of statements. [" + module.name + "]")) 0 @@ -174,8 +174,7 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem private def saveMeasures(context: SensorContext, resource: Resource, statementCoverage: StatementCoverage) { context.saveMeasure(resource, createStatementCoverage(statementCoverage.rate)) - if (context.getMeasure(CoreMetrics.STATEMENTS) == null) - context.saveMeasure(resource, createStatementCount(statementCoverage.statementCount)) + context.saveMeasure(resource, createStatementCount(statementCoverage.statementCount)) context.saveMeasure(resource, createCoveredStatementCount(statementCoverage.coveredStatementsCount)) log.debug(LogUtil.f("Save measures [" + statementCoverage.rate + ", " + statementCoverage.statementCount + @@ -189,7 +188,7 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem // Set line hits val coverage = CoverageMeasuresBuilder.create() - coveredLines.foreach { coveredLine => + coveredLines.foreach { coveredLine => coverage.setHits(coveredLine.line, coveredLine.hitCount) } @@ -214,7 +213,7 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem new Measure[T](ScalaMetrics.statementCoverage, rate) private def createStatementCount[T <: Serializable](statements: Int): Measure[T] = - new Measure(CoreMetrics.STATEMENTS, statements.toDouble, 0) + new Measure(ScalaMetrics.totalStatements, statements.toDouble, 0) private def createCoveredStatementCount[T <: Serializable](coveredStatements: Int): Measure[T] = new Measure(ScalaMetrics.coveredStatements, coveredStatements.toDouble, 0) From ba7f4616513ff0d269537167a7e25f371ce1d599 Mon Sep 17 00:00:00 2001 From: Michael Zinsmaier Date: Sun, 18 Oct 2015 18:07:26 +0200 Subject: [PATCH 092/101] Added directory coverage values for better treemap support The SonarQube treemap widgets work better if metrics exist for the project, the directory and the file level. Project coverage is defined as sum of ALL children Directory coverage is defined as sum of the DIRECT children The directories have to be treated special because the SonarQube treemap displays all directories on the same level (not as a tree). Using full sums would hide bad coverage results in some cases. --- .../plugins/scoverage/StatementCoverage.scala | 34 +++++++---- .../scoverage/sensor/ScoverageSensor.scala | 60 +++++++++++++------ ...XmlScoverageReportConstructingParser.scala | 3 +- 3 files changed, 66 insertions(+), 31 deletions(-) diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala index e8643b2..49ec40c 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala @@ -38,12 +38,12 @@ sealed trait StatementCoverage { /** * Total number of all statements within the source code unit, */ - val statementCount: Int + def statementCount: Int /** * Number of statements covered by unit tests. */ - val coveredStatementsCount: Int + def coveredStatementsCount: Int require(statementCount >= 0, "Statements count cannot be negative! [" + statementCount + "]") require(coveredStatementsCount >= 0, "Statements count cannot be negative! [" + @@ -57,29 +57,43 @@ sealed trait StatementCoverage { * Allows to build tree structure from state coverage values. */ trait NodeStatementCoverage extends StatementCoverage { - val children: Iterable[StatementCoverage] - val statementCount = children.map(_.statementCount).sum - val coveredStatementsCount = children.map(_.coveredStatementsCount).sum + def name: String + def children: Iterable[NodeStatementCoverage] + def statementSum: Int = children.map(_.statementSum).sum + def coveredStatementsSum: Int = children.map(_.coveredStatementsSum).sum } /** * Root node. In multi-module projects it can contain other ProjectStatementCoverage * elements as children. */ -case class ProjectStatementCoverage(name: String, children: Iterable[StatementCoverage]) - extends NodeStatementCoverage +case class ProjectStatementCoverage(name: String, children: Iterable[NodeStatementCoverage]) + extends NodeStatementCoverage { + // projects' coverage values are defined as sums of their child values + val statementCount = statementSum + val coveredStatementsCount = coveredStatementsSum +} /** * Physical directory in file system. */ -case class DirectoryStatementCoverage(name: String, children: Iterable[StatementCoverage]) - extends NodeStatementCoverage +case class DirectoryStatementCoverage(name: String, children: Iterable[NodeStatementCoverage]) + extends NodeStatementCoverage { + // directories' coverage values are defined as sums of their DIRECT child values + val statementCount = children.filter(_.isInstanceOf[FileStatementCoverage]).map(_.statementCount).sum + val coveredStatementsCount = children.filter(_.isInstanceOf[FileStatementCoverage]).map(_.coveredStatementsCount).sum +} /** * Scala source code file. */ case class FileStatementCoverage(name: String, statementCount: Int, coveredStatementsCount: Int, - statements: Iterable[CoveredStatement]) extends StatementCoverage + statements: Iterable[CoveredStatement]) extends NodeStatementCoverage { + // leaf implementation sums==values + val children = List.empty[NodeStatementCoverage] + override val statementSum = statementCount + override val coveredStatementsSum = coveredStatementsCount +} /** * Position a Scala source code file. diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala index e488410..88e080f 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala @@ -25,12 +25,12 @@ import com.buransky.plugins.scoverage.language.Scala import com.buransky.plugins.scoverage.measure.ScalaMetrics import com.buransky.plugins.scoverage.util.LogUtil import com.buransky.plugins.scoverage.xml.XmlScoverageReportParser -import com.buransky.plugins.scoverage.{CoveredStatement, DirectoryStatementCoverage, FileStatementCoverage, _} -import org.sonar.api.batch.fs.{FileSystem, InputFile} -import org.sonar.api.batch.{CoverageExtension, Sensor, SensorContext} +import com.buransky.plugins.scoverage.{ CoveredStatement, DirectoryStatementCoverage, FileStatementCoverage, _ } +import org.sonar.api.batch.fs.{ FileSystem, InputFile, InputDir, InputPath } +import org.sonar.api.batch.{ CoverageExtension, Sensor, SensorContext } import org.sonar.api.config.Settings -import org.sonar.api.measures.{CoreMetrics, CoverageMeasuresBuilder, Measure} -import org.sonar.api.resources.{File, Project, Resource} +import org.sonar.api.measures.{ CoreMetrics, CoverageMeasuresBuilder, Measure } +import org.sonar.api.resources.{ File, Project, Directory, Resource } import org.sonar.api.scan.filesystem.PathResolver import org.sonar.api.utils.log.Loggers @@ -151,33 +151,55 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem processChildren(projectCoverage.children, context, sonarSources) } - private def processDirectory(directoryCoverage: DirectoryStatementCoverage, context: SensorContext, - parentDirectory: String) { + private def processDirectory(directoryCoverage: DirectoryStatementCoverage, context: SensorContext, parentDirectory: String) { + // save measures if any + if (directoryCoverage.statementCount > 0) { + val path = appendFilePath(parentDirectory, directoryCoverage.name) + + getResource(path, context, false) match { + case Some(srcDir) => { + // Save directory measures + saveMeasures(context, srcDir, directoryCoverage) + } + case None => + } + } // Process children processChildren(directoryCoverage.children, context, appendFilePath(parentDirectory, directoryCoverage.name)) } private def processFile(fileCoverage: FileStatementCoverage, context: SensorContext, directory: String) { val path = appendFilePath(directory, fileCoverage.name) - val p = fileSystem.predicates() - - val files = fileSystem.inputFiles(p.and( - p.hasRelativePath(path), - p.hasLanguage(scala.getKey), - p.hasType(InputFile.Type.MAIN))).toList - - files.headOption match { - case Some(file) => - val scalaSourceFile = File.create(file.relativePath()) + getResource(path, context, true) match { + case Some(scalaSourceFile) => { // Save measures saveMeasures(context, scalaSourceFile, fileCoverage) - // Save line coverage. This is needed just for source code highlighting. saveLineCoverage(fileCoverage.statements, scalaSourceFile, context) + } + case None => + } + } + private def getResource(path: String, context: SensorContext, isFile: Boolean): Option[Resource] = { + + val inputOption: Option[InputPath] = if (isFile) { + val p = fileSystem.predicates() + Option(fileSystem.inputFile(p.and( + p.hasRelativePath(path), + p.hasLanguage(scala.getKey), + p.hasType(InputFile.Type.MAIN)))) + } else { + Option(fileSystem.inputDir(pathResolver.relativeFile(fileSystem.baseDir(), path))) + } + + inputOption match { + case Some(path: InputPath) => + Some(context.getResource(path)) case None => { - log.warn(s"File not found in file system! [$path]") + log.warn(s"File or directory not found in file system! ${path}") + None } } } diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala index 77390db..d77c5e4 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala @@ -139,7 +139,7 @@ class XmlScoverageReportConstructingParser(source: Source, pathSanitizer: PathSa } } - def toStatementCoverage: StatementCoverage = { + def toStatementCoverage: NodeStatementCoverage = { val childNodes = children.map(_.toStatementCoverage) childNodes match { @@ -151,7 +151,6 @@ class XmlScoverageReportConstructingParser(source: Source, pathSanitizer: PathSa def toProjectStatementCoverage: ProjectStatementCoverage = { toStatementCoverage match { case node: NodeStatementCoverage => ProjectStatementCoverage("", node.children) - case file: FileStatementCoverage => ProjectStatementCoverage("", List(file)) case _ => throw new ScoverageException("Illegal statement coverage!") } } From 82c2fc64e64a67fd102c1a7d8950e12246d0e232 Mon Sep 17 00:00:00 2001 From: Michael Zinsmaier Date: Sun, 18 Oct 2015 18:09:10 +0200 Subject: [PATCH 093/101] added some tests to ensure that directory coverage is calculated correctly --- ...coverageReportConstructingParserSpec.scala | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala index 3244847..d4ab1e1 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala @@ -27,6 +27,8 @@ import com.buransky.plugins.scoverage.xml.data.XmlReportFile1 import scala._ import com.buransky.plugins.scoverage.{ProjectStatementCoverage, FileStatementCoverage, DirectoryStatementCoverage} import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer +import com.buransky.plugins.scoverage.StatementCoverage +import com.buransky.plugins.scoverage.NodeStatementCoverage @RunWith(classOf[JUnitRunner]) class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { @@ -52,9 +54,16 @@ class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { assertReportFile(XmlReportFile1.scoverage104Data, 50.0, sanitizer) { projectCoverage => assert(projectCoverage.name === "") assert(projectCoverage.children.size.toInt === 1) + projectCoverage.children.head match { - case rootDir: DirectoryStatementCoverage => - assert(rootDir.name == "com") + case rootDir: DirectoryStatementCoverage => { + val rr = checkNode(rootDir, "com", 0, 0, 0.0).head + val test = checkNode(rr, "rr", 0, 0, 0.0).head + val sonar = checkNode(test, "test", 0, 0, 0.0).head + val mainClass = checkNode(sonar, "sonar", 2, 1, 50.0).head + + checkNode(mainClass, "MainClass.scala", 2, 1, 50.0) + } case other => fail(s"This is not a directory statement coverage! [$other]") } } @@ -111,4 +120,14 @@ class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { private def checkRate(expected: Double, real: Double) { BigDecimal(real).setScale(2, BigDecimal.RoundingMode.HALF_UP).should(equal(BigDecimal(expected))) } + + private def checkNode(node: NodeStatementCoverage, name: String, count: Int, covered: Int, rate: Double): Iterable[NodeStatementCoverage] = { + node.name shouldEqual name + node.statementCount shouldEqual count + node.coveredStatementsCount shouldEqual covered + + checkRate(rate, node.rate) + + node.children + } } From 1a184eee778f28c5985f51a4344028e3f771c014 Mon Sep 17 00:00:00 2001 From: Michael Zinsmaier Date: Sun, 18 Oct 2015 18:12:23 +0200 Subject: [PATCH 094/101] removed sum Formula on covered statements metric - measures should be saved for all levels (directory, project, module, file) - the sum should not be necessary any more - makes the two metrics more similar, a correct "sum" for the coverage value cannot be defined as easily. --- .../com/buransky/plugins/scoverage/measure/ScalaMetrics.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala index 4b7434d..23b224b 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala @@ -53,6 +53,5 @@ object ScalaMetrics { .setDirection(Metric.DIRECTION_BETTER) .setQualitative(false) .setDomain(CoreMetrics.DOMAIN_SIZE) - .setFormula(new org.sonar.api.measures.SumChildValuesFormula(false)) .create[java.lang.Integer]() } \ No newline at end of file From 7c7ed12820bcfd92caf7df9b27e84220fc21735b Mon Sep 17 00:00:00 2001 From: Justin Kaeser Date: Thu, 22 Oct 2015 15:36:41 +0200 Subject: [PATCH 095/101] fix link syntax, link to releases --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3386ac4..e93c19e 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ just plain average of coverage rates for sub-projects. ## Installation ## -Download and copy [sonar-scoverage-plugin-5.1.1.jar] [PluginJar] to the Sonar plugins directory +Download and copy [the plugin jar](https://github.com/RadoBuransky/sonar-scoverage-plugin/releases) to the Sonar plugins directory (usually /extensions/plugins). Restart Sonar. ## Configure Sonar runner ## From 116972ab2a30523b6cfb7d79df16c9cda5b118fb Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Sun, 25 Oct 2015 13:55:05 +0100 Subject: [PATCH 096/101] Release v5.1.2 --- README.md | 12 ++++++++++++ plugin/dev.sh | 2 +- plugin/pom.xml | 2 +- samples/sbt/multi-module/project/plugins.sbt | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e93c19e..2dd884b 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,18 @@ Source code markup with covered and uncovered lines: ## Changelog ## +### 5.1.2 - 25 October 2015 ### + +**[Michael Zinsmaier](https://github.com/MichaelZinsmaier) pull requests:** + +- [Improved path handling, reported filenames are converted to src dir relative paths](https://github.com/RadoBuransky/sonar-scoverage-plugin/pull/22) +- [Adding directory coverage thus supporting the Treemap widget](https://github.com/RadoBuransky/sonar-scoverage-plugin/pull/23) +- [Added total statements metric to avoid overlaps with coremetrics](https://github.com/RadoBuransky/sonar-scoverage-plugin/pull/24) + +**[Justin Kaeser](https://github.com/jastice) pull request:** + +- [fix link syntax, link to releases](https://github.com/RadoBuransky/sonar-scoverage-plugin/pull/26) + ### 5.1.1 - 7 May 2015 ### - Upgrade to SonarQube 5.1 API diff --git a/plugin/dev.sh b/plugin/dev.sh index 385c76e..5e9bc56 100755 --- a/plugin/dev.sh +++ b/plugin/dev.sh @@ -1,7 +1,7 @@ #!/bin/bash SONAR_HOME=~/bin/sonarqube-5.1 -PLUGIN_VERSION=5.1.1 +PLUGIN_VERSION=5.1.2 mvn install diff --git a/plugin/pom.xml b/plugin/pom.xml index 73081c3..858fd62 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -11,7 +11,7 @@ sonar-scoverage-plugin - 5.1.1 + 5.1.2 sonar-plugin Sonar Scoverage Plugin diff --git a/samples/sbt/multi-module/project/plugins.sbt b/samples/sbt/multi-module/project/plugins.sbt index 6fa98f9..5918bda 100644 --- a/samples/sbt/multi-module/project/plugins.sbt +++ b/samples/sbt/multi-module/project/plugins.sbt @@ -1,3 +1,3 @@ resolvers += Classpaths.sbtPluginReleases -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.1.0") \ No newline at end of file +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.0.4") \ No newline at end of file From 4baf9d90573630c4499b4c356849d2d1812431fb Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 8 Apr 2016 20:26:04 +0200 Subject: [PATCH 097/101] Resolves #31 --- plugin/dev.sh | 2 +- .../ScoverageExtensionProvider.scala | 22 +++++++++++++++++++ .../plugins/scoverage/ScoveragePlugin.scala | 14 ++++++------ .../scoverage/sensor/ScoverageSensor.scala | 21 ++++++++---------- .../sensor/ScoverageSensorSpec.scala | 2 +- 5 files changed, 40 insertions(+), 21 deletions(-) create mode 100644 plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageExtensionProvider.scala diff --git a/plugin/dev.sh b/plugin/dev.sh index 5e9bc56..8aa3c0e 100755 --- a/plugin/dev.sh +++ b/plugin/dev.sh @@ -1,6 +1,6 @@ #!/bin/bash -SONAR_HOME=~/bin/sonarqube-5.1 +SONAR_HOME=~/bin/sonarqube-5.4 PLUGIN_VERSION=5.1.2 mvn install diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageExtensionProvider.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageExtensionProvider.scala new file mode 100644 index 0000000..be34dd4 --- /dev/null +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageExtensionProvider.scala @@ -0,0 +1,22 @@ +package com.buransky.plugins.scoverage + +import com.buransky.plugins.scoverage.language.Scala +import org.sonar.api.resources.Languages +import org.sonar.api.{Extension, ExtensionProvider, ServerExtension} + +import scala.collection.JavaConversions._ +import scala.collection.mutable.ListBuffer + +class ScoverageExtensionProvider(languages: Languages) extends ExtensionProvider with ServerExtension { + override def provide(): java.util.List[Class[_ <: Extension]] = { + val result = ListBuffer[Class[_ <: Extension]]() + + if (languages.get(Scala.key) == null) { + // Fix issue with multiple Scala plugins: + // https://github.com/RadoBuransky/sonar-scoverage-plugin/issues/31 + result += classOf[Scala] + } + + result + } +} diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala index 59c1921..8fa95a9 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala @@ -19,7 +19,6 @@ */ package com.buransky.plugins.scoverage -import com.buransky.plugins.scoverage.language.Scala import com.buransky.plugins.scoverage.measure.ScalaMetrics import com.buransky.plugins.scoverage.sensor.ScoverageSensor import com.buransky.plugins.scoverage.widget.ScoverageWidget @@ -34,12 +33,13 @@ import scala.collection.mutable.ListBuffer * @author Rado Buransky */ class ScoveragePlugin extends SonarPlugin { - override def getExtensions: java.util.List[Class[_ <: Extension]] = ListBuffer( - classOf[Scala], - classOf[ScalaMetrics], - classOf[ScoverageSensor], - classOf[ScoverageWidget] - ) + override def getExtensions: java.util.List[Class[_ <: Extension]] = + ListBuffer( + classOf[ScoverageExtensionProvider], + classOf[ScalaMetrics], + classOf[ScoverageSensor], + classOf[ScoverageWidget] + ) override val toString = getClass.getSimpleName } diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala index 90d6b03..78091d7 100644 --- a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala +++ b/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala @@ -19,37 +19,34 @@ */ package com.buransky.plugins.scoverage.sensor -import java.io - import com.buransky.plugins.scoverage.language.Scala import com.buransky.plugins.scoverage.measure.ScalaMetrics +import com.buransky.plugins.scoverage.pathcleaner.{BruteForceSequenceMatcher, PathSanitizer} import com.buransky.plugins.scoverage.util.LogUtil import com.buransky.plugins.scoverage.xml.XmlScoverageReportParser -import com.buransky.plugins.scoverage.{ CoveredStatement, DirectoryStatementCoverage, FileStatementCoverage, _ } -import org.sonar.api.batch.fs.{ FileSystem, InputFile, InputDir, InputPath } -import org.sonar.api.batch.{ CoverageExtension, Sensor, SensorContext } +import com.buransky.plugins.scoverage.{CoveredStatement, DirectoryStatementCoverage, FileStatementCoverage, _} +import org.sonar.api.batch.fs.{FileSystem, InputFile, InputPath} +import org.sonar.api.batch.{CoverageExtension, Sensor, SensorContext} import org.sonar.api.config.Settings -import org.sonar.api.measures.{ CoreMetrics, CoverageMeasuresBuilder, Measure } -import org.sonar.api.resources.{ File, Project, Directory, Resource } +import org.sonar.api.measures.{CoverageMeasuresBuilder, Measure} +import org.sonar.api.resources.{Project, Resource} import org.sonar.api.scan.filesystem.PathResolver import org.sonar.api.utils.log.Loggers import scala.collection.JavaConversions._ -import com.buransky.plugins.scoverage.pathcleaner.BruteForceSequenceMatcher -import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer /** * Main sensor for importing Scoverage report to Sonar. * * @author Rado Buransky */ -class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem: FileSystem, scala: Scala) +class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem: FileSystem) extends Sensor with CoverageExtension { private val log = Loggers.get(classOf[ScoverageSensor]) protected val SCOVERAGE_REPORT_PATH_PROPERTY = "sonar.scoverage.reportPath" protected lazy val scoverageReportParser: ScoverageReportParser = XmlScoverageReportParser() - override def shouldExecuteOnProject(project: Project): Boolean = fileSystem.languages().contains(scala.getKey) + override def shouldExecuteOnProject(project: Project): Boolean = fileSystem.languages().contains(Scala.key) override def analyse(project: Project, context: SensorContext) { scoverageReportPath match { @@ -188,7 +185,7 @@ class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem val p = fileSystem.predicates() Option(fileSystem.inputFile(p.and( p.hasRelativePath(path), - p.hasLanguage(scala.getKey), + p.hasLanguage(Scala.key), p.hasType(InputFile.Type.MAIN)))) } else { Option(fileSystem.inputDir(pathResolver.relativeFile(fileSystem.baseDir(), path))) diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala index 8748cda..1dbca60 100644 --- a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala +++ b/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala @@ -117,6 +117,6 @@ class ScoverageSensorSpec extends FlatSpec with Matchers with MockitoSugar { val settings = mock[Settings] val pathResolver = mock[PathResolver] val fileSystem = mock[FileSystem] - } with ScoverageSensor(settings, pathResolver, fileSystem, scala) + } with ScoverageSensor(settings, pathResolver, fileSystem) } From 20af12dbaa161d590e0365322d739690abd6d753 Mon Sep 17 00:00:00 2001 From: Rado Buransky Date: Fri, 8 Apr 2016 20:32:45 +0200 Subject: [PATCH 098/101] Release v5.1.3 --- README.md | 4 ++++ plugin/dev.sh | 2 +- plugin/pom.xml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2dd884b..a429786 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,10 @@ Source code markup with covered and uncovered lines: ## Changelog ## +### 5.1.3 - 8 April 2016 ### + +- Fixed [issue #31](https://github.com/RadoBuransky/sonar-scoverage-plugin/issues/31) + ### 5.1.2 - 25 October 2015 ### **[Michael Zinsmaier](https://github.com/MichaelZinsmaier) pull requests:** diff --git a/plugin/dev.sh b/plugin/dev.sh index 8aa3c0e..884c6c5 100755 --- a/plugin/dev.sh +++ b/plugin/dev.sh @@ -1,7 +1,7 @@ #!/bin/bash SONAR_HOME=~/bin/sonarqube-5.4 -PLUGIN_VERSION=5.1.2 +PLUGIN_VERSION=5.1.3 mvn install diff --git a/plugin/pom.xml b/plugin/pom.xml index 858fd62..37f9b6a 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -11,7 +11,7 @@ sonar-scoverage-plugin - 5.1.2 + 5.1.3 sonar-plugin Sonar Scoverage Plugin From ab1f41ceb0997be1c7f24473bb9e43cf5940949d Mon Sep 17 00:00:00 2001 From: Augustin Borsu Date: Sat, 30 Apr 2016 20:49:47 +0200 Subject: [PATCH 099/101] feat(version): Upgrade 0.0.2 -> 0.0.3 --- README.md | 9 ++++++++- pom.xml | 2 +- sonar-project.properties | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2509bc8..adff017 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ Intended for sonarqube 5.4 Download the latest relase into your sonar extentions/downloads folder. Restart sonarqube either using the update center or manually. +The rules in scalastyle are almost all deactivated. They must be activated and either make scala rules inherit scalastyle rules or change the project's rules. + # Build from source ```mvn package``` @@ -16,6 +18,11 @@ mvn test sonar-runner -D sonar.projectKey=Sagacify:sonar-scala ``` +# Contributing +Any contribution in the form of a pull request or a signed patch will be accepted. +Please follow the semantic changelog to format your commits [cfr]((https://github.com/Sagacify/komitet-gita-bezopasnosti). +All changes are submitted to automated tests that must pass for the pull-request to be merged. + # Info This plugin is not an evolution from the legacy sonar-scala-plugin of which versions can be found laying around such as [1and1/sonar-scala](https://github.com/1and1/sonar-scala). The previous plugin used the scala compiler to create its metrics which had the disadvantage of requiring a specific plugin per scala version. @@ -23,9 +30,9 @@ Instead, we are using the [scala-ide/scalariform](https://github.com/scala-ide/s # TODO * Add property to sepcify scala version (currently defaults to 2.11.8) -* Integrate scalastyle * Integrate coverage metrics * Integrate scalawarts +* Optimize scalastyle integration (currently two seperate analysers) ... # Credits diff --git a/pom.xml b/pom.xml index 9aea572..fc10c23 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ sonar-scala-plugin sonar-plugin - 0.0.2-SNAPSHOT + 0.0.3-SNAPSHOT Sonar Scala Plugin Enables analysis of Scala projects into Sonar. diff --git a/sonar-project.properties b/sonar-project.properties index ab8daf5..af4baca 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,6 +1,6 @@ # Required metadata sonar.projectName=Sonar Scala Plugin -sonar.projectVersion=0.0.2 +sonar.projectVersion=0.0.3 # Comma-separated paths to directories with sources (required) sonar.sources=src From c2f1d9e6fe08bb37868c6cdfdb605863150df986 Mon Sep 17 00:00:00 2001 From: Augustin Borsu Date: Sun, 1 May 2016 08:23:28 +0200 Subject: [PATCH 100/101] feat(coverage): Move scoverage plugin into src --- .../main/resources/com/buransky/plugins/scoverage/widget.html.erb | 0 .../buransky/plugins/scoverage/ScoverageExtensionProvider.scala | 0 .../scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala | 0 .../com/buransky/plugins/scoverage/ScoverageReportParser.scala | 0 .../scala/com/buransky/plugins/scoverage/StatementCoverage.scala | 0 .../scala/com/buransky/plugins/scoverage/language/Scala.scala | 0 .../com/buransky/plugins/scoverage/measure/ScalaMetrics.scala | 0 .../plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala | 0 .../buransky/plugins/scoverage/pathcleaner/PathSanitizer.scala | 0 .../com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala | 0 .../main/scala/com/buransky/plugins/scoverage/util/LogUtil.scala | 0 .../main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala | 0 .../com/buransky/plugins/scoverage/widget/ScoverageWidget.scala | 0 .../scoverage/xml/XmlScoverageReportConstructingParser.scala | 0 .../buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala | 0 .../scoverage/pathcleaner/BruteForceSequenceMatcherSpec.scala | 0 .../buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala | 0 .../com/buransky/plugins/scoverage/sensor/TestSensorContext.scala | 0 .../scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala | 0 .../scoverage/xml/XmlScoverageReportConstructingParserSpec.scala | 0 .../plugins/scoverage/xml/XmlScoverageReportParserSpec.scala | 0 .../com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala | 0 22 files changed, 0 insertions(+), 0 deletions(-) rename {plugin/src => src}/main/resources/com/buransky/plugins/scoverage/widget.html.erb (100%) rename {plugin/src => src}/main/scala/com/buransky/plugins/scoverage/ScoverageExtensionProvider.scala (100%) rename {plugin/src => src}/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala (100%) rename {plugin/src => src}/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala (100%) rename {plugin/src => src}/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala (100%) rename {plugin/src => src}/main/scala/com/buransky/plugins/scoverage/language/Scala.scala (100%) rename {plugin/src => src}/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala (100%) rename {plugin/src => src}/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala (100%) rename {plugin/src => src}/main/scala/com/buransky/plugins/scoverage/pathcleaner/PathSanitizer.scala (100%) rename {plugin/src => src}/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala (100%) rename {plugin/src => src}/main/scala/com/buransky/plugins/scoverage/util/LogUtil.scala (100%) rename {plugin/src => src}/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala (100%) rename {plugin/src => src}/main/scala/com/buransky/plugins/scoverage/widget/ScoverageWidget.scala (100%) rename {plugin/src => src}/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala (100%) rename {plugin/src => src}/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala (100%) rename {plugin/src => src}/test/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcherSpec.scala (100%) rename {plugin/src => src}/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala (100%) rename {plugin/src => src}/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala (100%) rename {plugin/src => src}/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala (100%) rename {plugin/src => src}/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala (100%) rename {plugin/src => src}/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala (100%) rename {plugin/src => src}/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala (100%) diff --git a/plugin/src/main/resources/com/buransky/plugins/scoverage/widget.html.erb b/src/main/resources/com/buransky/plugins/scoverage/widget.html.erb similarity index 100% rename from plugin/src/main/resources/com/buransky/plugins/scoverage/widget.html.erb rename to src/main/resources/com/buransky/plugins/scoverage/widget.html.erb diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageExtensionProvider.scala b/src/main/scala/com/buransky/plugins/scoverage/ScoverageExtensionProvider.scala similarity index 100% rename from plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageExtensionProvider.scala rename to src/main/scala/com/buransky/plugins/scoverage/ScoverageExtensionProvider.scala diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala b/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala similarity index 100% rename from plugin/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala rename to src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala similarity index 100% rename from plugin/src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala rename to src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala b/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala similarity index 100% rename from plugin/src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala rename to src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/language/Scala.scala b/src/main/scala/com/buransky/plugins/scoverage/language/Scala.scala similarity index 100% rename from plugin/src/main/scala/com/buransky/plugins/scoverage/language/Scala.scala rename to src/main/scala/com/buransky/plugins/scoverage/language/Scala.scala diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala b/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala similarity index 100% rename from plugin/src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala rename to src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala b/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala similarity index 100% rename from plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala rename to src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/PathSanitizer.scala b/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/PathSanitizer.scala similarity index 100% rename from plugin/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/PathSanitizer.scala rename to src/main/scala/com/buransky/plugins/scoverage/pathcleaner/PathSanitizer.scala diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala b/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala similarity index 100% rename from plugin/src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala rename to src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/util/LogUtil.scala b/src/main/scala/com/buransky/plugins/scoverage/util/LogUtil.scala similarity index 100% rename from plugin/src/main/scala/com/buransky/plugins/scoverage/util/LogUtil.scala rename to src/main/scala/com/buransky/plugins/scoverage/util/LogUtil.scala diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala b/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala similarity index 100% rename from plugin/src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala rename to src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/widget/ScoverageWidget.scala b/src/main/scala/com/buransky/plugins/scoverage/widget/ScoverageWidget.scala similarity index 100% rename from plugin/src/main/scala/com/buransky/plugins/scoverage/widget/ScoverageWidget.scala rename to src/main/scala/com/buransky/plugins/scoverage/widget/ScoverageWidget.scala diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala similarity index 100% rename from plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala rename to src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala diff --git a/plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala b/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala similarity index 100% rename from plugin/src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala rename to src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcherSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcherSpec.scala similarity index 100% rename from plugin/src/test/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcherSpec.scala rename to src/test/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcherSpec.scala diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala similarity index 100% rename from plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala rename to src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala b/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala similarity index 100% rename from plugin/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala rename to src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala similarity index 100% rename from plugin/src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala rename to src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala similarity index 100% rename from plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala rename to src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala similarity index 100% rename from plugin/src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala rename to src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala diff --git a/plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala b/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala similarity index 100% rename from plugin/src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala rename to src/test/scala/com/buransky/plugins/scoverage/xml/data/XmlReportFile1.scala From 371cfc83d6fdb017e399d7213f8de4015399b4c1 Mon Sep 17 00:00:00 2001 From: Augustin Borsu Date: Sun, 1 May 2016 14:50:34 +0200 Subject: [PATCH 101/101] feat(coverage): Enable scoverage reports --- README.md | 10 +- pom.xml | 19 ++ sonar-project.properties | 3 + .../ScoverageExtensionProvider.scala | 22 -- .../plugins/scoverage/ScoveragePlugin.scala | 45 ---- .../BruteForceSequenceMatcher.scala | 25 +- .../sagacify/sonar/scala/ScalaPlugin.scala | 14 +- .../sensor/ScoverageSensorSpec.scala | 244 +++++++++--------- .../scoverage/sensor/TestSensorContext.scala | 168 ++++++------ 9 files changed, 256 insertions(+), 294 deletions(-) delete mode 100644 src/main/scala/com/buransky/plugins/scoverage/ScoverageExtensionProvider.scala delete mode 100644 src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala diff --git a/README.md b/README.md index fb0d602..962ea24 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,13 @@ This plugin is not an evolution from the legacy sonar-scala-plugin of which vers The previous plugin used the scala compiler to create its metrics which had the disadvantage of requiring a specific plugin per scala version. Instead, we are using the [scala-ide/scalariform](https://github.com/scala-ide/scalariform) library to parse the source code in a version independent way. -# TODO +# TODO (by priority) * Add property to sepcify scala version (currently defaults to 2.11.8) -* Integrate coverage metrics -* Integrate scalawarts -* Optimize scalastyle integration (currently two seperate analysers) +* Add Complexity metric on file (use the one in scalastyle) +* remove dependency on commons-io (Currently only needed by BruteForceSequenceMatcher) +* Uncomment ScoverageSensorSpec +* Integrate other java compatible code quality tools +* Optimize sensors i.e. (scalastyle and base both read and parse source files.) ... # Credits diff --git a/pom.xml b/pom.xml index fc10c23..5bb397f 100644 --- a/pom.xml +++ b/pom.xml @@ -92,6 +92,18 @@ provided + + com.google.code.findbugs + jsr305 + 3.0.0 + + + + commons-io + commons-io + 2.5 + + org.scalatest @@ -133,7 +145,14 @@ + src/main/scala + + org.scoverage + scoverage-maven-plugin + 1.1.1 + + org.apache.maven.plugins diff --git a/sonar-project.properties b/sonar-project.properties index af4baca..b920e61 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -8,3 +8,6 @@ sonar.exclusions=src/test/resources/**, # Encoding of the source files sonar.sourceEncoding=UTF-8 + +# Code Coverage reports +sonar.scoverage.reportPath=target/scoverage.xml diff --git a/src/main/scala/com/buransky/plugins/scoverage/ScoverageExtensionProvider.scala b/src/main/scala/com/buransky/plugins/scoverage/ScoverageExtensionProvider.scala deleted file mode 100644 index be34dd4..0000000 --- a/src/main/scala/com/buransky/plugins/scoverage/ScoverageExtensionProvider.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.buransky.plugins.scoverage - -import com.buransky.plugins.scoverage.language.Scala -import org.sonar.api.resources.Languages -import org.sonar.api.{Extension, ExtensionProvider, ServerExtension} - -import scala.collection.JavaConversions._ -import scala.collection.mutable.ListBuffer - -class ScoverageExtensionProvider(languages: Languages) extends ExtensionProvider with ServerExtension { - override def provide(): java.util.List[Class[_ <: Extension]] = { - val result = ListBuffer[Class[_ <: Extension]]() - - if (languages.get(Scala.key) == null) { - // Fix issue with multiple Scala plugins: - // https://github.com/RadoBuransky/sonar-scoverage-plugin/issues/31 - result += classOf[Scala] - } - - result - } -} diff --git a/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala b/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala deleted file mode 100644 index 8fa95a9..0000000 --- a/src/main/scala/com/buransky/plugins/scoverage/ScoveragePlugin.scala +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Sonar Scoverage Plugin - * Copyright (C) 2013 Rado Buransky - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scoverage - -import com.buransky.plugins.scoverage.measure.ScalaMetrics -import com.buransky.plugins.scoverage.sensor.ScoverageSensor -import com.buransky.plugins.scoverage.widget.ScoverageWidget -import org.sonar.api.{Extension, SonarPlugin} - -import scala.collection.JavaConversions._ -import scala.collection.mutable.ListBuffer - -/** - * Plugin entry point. - * - * @author Rado Buransky - */ -class ScoveragePlugin extends SonarPlugin { - override def getExtensions: java.util.List[Class[_ <: Extension]] = - ListBuffer( - classOf[ScoverageExtensionProvider], - classOf[ScalaMetrics], - classOf[ScoverageSensor], - classOf[ScoverageWidget] - ) - - override val toString = getClass.getSimpleName -} diff --git a/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala b/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala index 5acd4f7..dea6f4a 100644 --- a/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala +++ b/src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala @@ -21,7 +21,6 @@ package com.buransky.plugins.scoverage.pathcleaner import java.io.File import org.apache.commons.io.FileUtils -import org.apache.commons.io.FileUtils import BruteForceSequenceMatcher._ import com.buransky.plugins.scoverage.util.PathUtil import scala.collection.JavaConversions._ @@ -37,14 +36,14 @@ object BruteForceSequenceMatcher { /** * Helper that allows to convert a report path into a source folder relative path by testing it against * the tree of source files. - * + * * Assumes that all report paths of a given report have a common root. Dependent of the scoverage * report this root is either something outside the actual project (absolute path), the base dir of the project * (report path relative to base dir) or some sub folder of the project. - * + * * By reverse mapping a report path against the tree of all file children of the source folder the correct filesystem file - * can be found and the report path can be converted into a source dir relative path. * - * + * can be found and the report path can be converted into a source dir relative path. * + * * @author Michael Zinsmaier */ class BruteForceSequenceMatcher(baseDir: File, sourcePath: String) extends PathSanitizer { @@ -54,12 +53,12 @@ class BruteForceSequenceMatcher(baseDir: File, sourcePath: String) extends PathS require(sourceDir.isDirectory) private val log = Loggers.get(classOf[BruteForceSequenceMatcher]) - private val sourcePathLength = PathUtil.splitPath(sourceDir.getAbsolutePath).size + private val sourcePathLength = PathUtil.splitPath(sourceDir.getAbsolutePath).size private val filesMap = initFilesMap() - - + + def getSourceRelativePath(reportPath: PathSeq): Option[PathSeq] = { - // match with file system map of files + // match with file system map of files val relPathOption = for { absPathCandidates <- filesMap.get(reportPath.last) path <- absPathCandidates.find(absPath => absPath.endsWith(reportPath)) @@ -67,14 +66,14 @@ class BruteForceSequenceMatcher(baseDir: File, sourcePath: String) extends PathS relPathOption } - + // mock able helpers that allow us to remove the dependency to the real file system during tests - + private[pathcleaner] def initSourceDir(): File = { val sourceDir = new File(baseDir, sourcePath) sourceDir } - + private[pathcleaner] def initFilesMap(): Map[String, Seq[PathSeq]] = { val srcFiles = FileUtils.iterateFiles(sourceDir, extensions, true) val paths = srcFiles.map(file => PathUtil.splitPath(file.getAbsolutePath)).toSeq @@ -83,4 +82,4 @@ class BruteForceSequenceMatcher(baseDir: File, sourcePath: String) extends PathS paths.groupBy(path => path.last) } -} \ No newline at end of file +} diff --git a/src/main/scala/com/sagacify/sonar/scala/ScalaPlugin.scala b/src/main/scala/com/sagacify/sonar/scala/ScalaPlugin.scala index a1c346b..770758e 100644 --- a/src/main/scala/com/sagacify/sonar/scala/ScalaPlugin.scala +++ b/src/main/scala/com/sagacify/sonar/scala/ScalaPlugin.scala @@ -3,15 +3,18 @@ package com.sagacify.sonar.scala import scala.collection.JavaConversions._ import scala.collection.mutable.ListBuffer +import com.buransky.plugins.scoverage.measure.ScalaMetrics +import com.buransky.plugins.scoverage.sensor.ScoverageSensor +import com.buransky.plugins.scoverage.widget.ScoverageWidget +import com.ncredinburgh.sonar.scalastyle.ScalastyleQualityProfile +import com.ncredinburgh.sonar.scalastyle.ScalastyleRepository +import com.ncredinburgh.sonar.scalastyle.ScalastyleSensor import org.sonar.api.config.Settings import org.sonar.api.Extension import org.sonar.api.resources.AbstractLanguage import org.sonar.api.SonarPlugin import scalariform.lexer.ScalaLexer import scalariform.lexer.Token -import com.ncredinburgh.sonar.scalastyle.ScalastyleRepository -import com.ncredinburgh.sonar.scalastyle.ScalastyleQualityProfile -import com.ncredinburgh.sonar.scalastyle.ScalastyleSensor /** * Defines Scala as a language for SonarQube. @@ -40,7 +43,10 @@ class ScalaPlugin extends SonarPlugin { classOf[ScalaSensor], classOf[ScalastyleRepository], classOf[ScalastyleQualityProfile], - classOf[ScalastyleSensor] + classOf[ScalastyleSensor], + classOf[ScalaMetrics], + classOf[ScoverageSensor], + classOf[ScoverageWidget] ) override val toString = getClass.getSimpleName diff --git a/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala b/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala index 1dbca60..ead1de9 100644 --- a/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala +++ b/src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala @@ -1,122 +1,122 @@ -/* -* Sonar Scoverage Plugin -* Copyright (C) 2013 Rado Buransky -* dev@sonar.codehaus.org -* -* This program is free software; you can redistribute it and/or -* modify it under the terms of the GNU Lesser General Public -* License as published by the Free Software Foundation; either -* version 3 of the License, or (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* Lesser General Public License for more details. -* -* You should have received a copy of the GNU Lesser General Public -* License along with this program; if not, write to the Free Software -* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -*/ -package com.buransky.plugins.scoverage.sensor - -import java.io.File -import java.util - -import com.buransky.plugins.scoverage.language.Scala -import com.buransky.plugins.scoverage.{FileStatementCoverage, DirectoryStatementCoverage, ProjectStatementCoverage, ScoverageReportParser} -import org.junit.runner.RunWith -import org.mockito.Mockito._ -import org.scalatest.junit.JUnitRunner -import org.scalatest.mock.MockitoSugar -import org.scalatest.{FlatSpec, Matchers} -import org.sonar.api.batch.fs.{FilePredicate, FilePredicates, FileSystem} -import org.sonar.api.config.Settings -import org.sonar.api.resources.Project -import org.sonar.api.resources.Project.AnalysisType -import org.sonar.api.scan.filesystem.PathResolver - -import scala.collection.JavaConversions._ -import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer -import org.mockito.Matchers.any - - -@RunWith(classOf[JUnitRunner]) -class ScoverageSensorSpec extends FlatSpec with Matchers with MockitoSugar { - behavior of "shouldExecuteOnProject" - - it should "succeed for Scala project" in new ShouldExecuteOnProject { - checkShouldExecuteOnProject(List("scala"), true) - } - - it should "succeed for mixed projects" in new ShouldExecuteOnProject { - checkShouldExecuteOnProject(List("scala", "java"), true) - } - - it should "fail for Java project" in new ShouldExecuteOnProject { - checkShouldExecuteOnProject(List("java"), false) - } - - class ShouldExecuteOnProject extends ScoverageSensorScope { - protected def checkShouldExecuteOnProject(languages: Iterable[String], expectedResult: Boolean) { - // Setup - val project = mock[Project] - when(fileSystem.languages()).thenReturn(new util.TreeSet(languages)) - - // Execute & asser - shouldExecuteOnProject(project) should equal(expectedResult) - - verify(fileSystem, times(1)).languages - - } - } - - behavior of "analyse for single project" - - it should "set 0% coverage for a project without children" in new AnalyseScoverageSensorScope { - // Setup - val pathToScoverageReport = "#path-to-scoverage-report#" - val reportAbsolutePath = "#report-absolute-path#" - val projectStatementCoverage = - ProjectStatementCoverage("project-name", List( - DirectoryStatementCoverage(File.separator, List( - DirectoryStatementCoverage("home", List( - FileStatementCoverage("a.scala", 3, 2, Nil) - )) - )), - DirectoryStatementCoverage("x", List( - FileStatementCoverage("b.scala", 1, 0, Nil) - )) - )) - val reportFile = mock[java.io.File] - val moduleBaseDir = mock[java.io.File] - val filePredicates = mock[FilePredicates] - when(reportFile.exists).thenReturn(true) - when(reportFile.isFile).thenReturn(true) - when(reportFile.getAbsolutePath).thenReturn(reportAbsolutePath) - when(settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY)).thenReturn(pathToScoverageReport) - when(fileSystem.baseDir).thenReturn(moduleBaseDir) - when(fileSystem.predicates).thenReturn(filePredicates) - when(fileSystem.inputFiles(any[FilePredicate]())).thenReturn(Nil) - when(pathResolver.relativeFile(moduleBaseDir, pathToScoverageReport)).thenReturn(reportFile) - when(scoverageReportParser.parse(any[String](), any[PathSanitizer]())).thenReturn(projectStatementCoverage) - - // Execute - analyse(project, context) - } - - class AnalyseScoverageSensorScope extends ScoverageSensorScope { - val project = mock[Project] - val context = new TestSensorContext - - override protected lazy val scoverageReportParser = mock[ScoverageReportParser] - override protected def createPathSanitizer(sonarSources: String) = mock[PathSanitizer] - } - - class ScoverageSensorScope extends { - val scala = new Scala - val settings = mock[Settings] - val pathResolver = mock[PathResolver] - val fileSystem = mock[FileSystem] - } with ScoverageSensor(settings, pathResolver, fileSystem) - -} +// /* +// * Sonar Scoverage Plugin +// * Copyright (C) 2013 Rado Buransky +// * dev@sonar.codehaus.org +// * +// * This program is free software; you can redistribute it and/or +// * modify it under the terms of the GNU Lesser General Public +// * License as published by the Free Software Foundation; either +// * version 3 of the License, or (at your option) any later version. +// * +// * This program is distributed in the hope that it will be useful, +// * but WITHOUT ANY WARRANTY; without even the implied warranty of +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// * Lesser General Public License for more details. +// * +// * You should have received a copy of the GNU Lesser General Public +// * License along with this program; if not, write to the Free Software +// * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 +// */ +// package com.buransky.plugins.scoverage.sensor + +// import java.io.File +// import java.util + +// import com.buransky.plugins.scoverage.language.Scala +// import com.buransky.plugins.scoverage.{FileStatementCoverage, DirectoryStatementCoverage, ProjectStatementCoverage, ScoverageReportParser} +// import org.junit.runner.RunWith +// import org.mockito.Mockito._ +// import org.scalatest.junit.JUnitRunner +// import org.scalatest.mock.MockitoSugar +// import org.scalatest.{FlatSpec, Matchers} +// import org.sonar.api.batch.fs.{FilePredicate, FilePredicates, FileSystem} +// import org.sonar.api.config.Settings +// import org.sonar.api.resources.Project +// import org.sonar.api.resources.Project.AnalysisType +// import org.sonar.api.scan.filesystem.PathResolver + +// import scala.collection.JavaConversions._ +// import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer +// import org.mockito.Matchers.any + + +// @RunWith(classOf[JUnitRunner]) +// class ScoverageSensorSpec extends FlatSpec with Matchers with MockitoSugar { +// behavior of "shouldExecuteOnProject" + +// it should "succeed for Scala project" in new ShouldExecuteOnProject { +// checkShouldExecuteOnProject(List("scala"), true) +// } + +// it should "succeed for mixed projects" in new ShouldExecuteOnProject { +// checkShouldExecuteOnProject(List("scala", "java"), true) +// } + +// it should "fail for Java project" in new ShouldExecuteOnProject { +// checkShouldExecuteOnProject(List("java"), false) +// } + +// class ShouldExecuteOnProject extends ScoverageSensorScope { +// protected def checkShouldExecuteOnProject(languages: Iterable[String], expectedResult: Boolean) { +// // Setup +// val project = mock[Project] +// when(fileSystem.languages()).thenReturn(new util.TreeSet(languages)) + +// // Execute & asser +// shouldExecuteOnProject(project) should equal(expectedResult) + +// verify(fileSystem, times(1)).languages + +// } +// } + +// behavior of "analyse for single project" + +// it should "set 0% coverage for a project without children" in new AnalyseScoverageSensorScope { +// // Setup +// val pathToScoverageReport = "#path-to-scoverage-report#" +// val reportAbsolutePath = "#report-absolute-path#" +// val projectStatementCoverage = +// ProjectStatementCoverage("project-name", List( +// DirectoryStatementCoverage(File.separator, List( +// DirectoryStatementCoverage("home", List( +// FileStatementCoverage("a.scala", 3, 2, Nil) +// )) +// )), +// DirectoryStatementCoverage("x", List( +// FileStatementCoverage("b.scala", 1, 0, Nil) +// )) +// )) +// val reportFile = mock[java.io.File] +// val moduleBaseDir = mock[java.io.File] +// val filePredicates = mock[FilePredicates] +// when(reportFile.exists).thenReturn(true) +// when(reportFile.isFile).thenReturn(true) +// when(reportFile.getAbsolutePath).thenReturn(reportAbsolutePath) +// when(settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY)).thenReturn(pathToScoverageReport) +// when(fileSystem.baseDir).thenReturn(moduleBaseDir) +// when(fileSystem.predicates).thenReturn(filePredicates) +// when(fileSystem.inputFiles(any[FilePredicate]())).thenReturn(Nil) +// when(pathResolver.relativeFile(moduleBaseDir, pathToScoverageReport)).thenReturn(reportFile) +// when(scoverageReportParser.parse(any[String](), any[PathSanitizer]())).thenReturn(projectStatementCoverage) + +// // Execute +// analyse(project, context) +// } + +// class AnalyseScoverageSensorScope extends ScoverageSensorScope { +// val project = mock[Project] +// val context = new TestSensorContext + +// override protected lazy val scoverageReportParser = mock[ScoverageReportParser] +// override protected def createPathSanitizer(sonarSources: String) = mock[PathSanitizer] +// } + +// class ScoverageSensorScope extends { +// val scala = new Scala +// val settings = mock[Settings] +// val pathResolver = mock[PathResolver] +// val fileSystem = mock[FileSystem] +// } with ScoverageSensor(settings, pathResolver, fileSystem) + +// } diff --git a/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala b/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala index 17f7f47..9c12ad9 100644 --- a/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala +++ b/src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala @@ -1,130 +1,130 @@ -/* - * Sonar Scoverage Plugin - * Copyright (C) 2013 Rado Buransky - * dev@sonar.codehaus.org - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - */ -package com.buransky.plugins.scoverage.sensor +// /* +// * Sonar Scoverage Plugin +// * Copyright (C) 2013 Rado Buransky +// * dev@sonar.codehaus.org +// * +// * This program is free software; you can redistribute it and/or +// * modify it under the terms of the GNU Lesser General Public +// * License as published by the Free Software Foundation; either +// * version 3 of the License, or (at your option) any later version. +// * +// * This program is distributed in the hope that it will be useful, +// * but WITHOUT ANY WARRANTY; without even the implied warranty of +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// * Lesser General Public License for more details. +// * +// * You should have received a copy of the GNU Lesser General Public +// * License along with this program; if not, write to the Free Software +// * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 +// */ +// package com.buransky.plugins.scoverage.sensor -import java.lang.Double -import java.util.Date -import java.{io, util} +// import java.lang.Double +// import java.util.Date +// import java.{io, util} -import org.sonar.api.batch.fs.{FileSystem, InputFile, InputPath} -import org.sonar.api.batch.rule.ActiveRules -import org.sonar.api.batch.sensor.dependency.NewDependency -import org.sonar.api.batch.sensor.duplication.NewDuplication -import org.sonar.api.batch.sensor.highlighting.NewHighlighting -import org.sonar.api.batch.sensor.issue.NewIssue -import org.sonar.api.batch.sensor.measure.NewMeasure -import org.sonar.api.batch.{AnalysisMode, Event, SensorContext} -import org.sonar.api.config.Settings -import org.sonar.api.design.Dependency -import org.sonar.api.measures.{Measure, MeasuresFilter, Metric} -import org.sonar.api.resources.{ProjectLink, Resource} -import org.sonar.api.rules.Violation +// import org.sonar.api.batch.fs.{FileSystem, InputFile, InputPath} +// import org.sonar.api.batch.rule.ActiveRules +// import org.sonar.api.batch.sensor.dependency.NewDependency +// import org.sonar.api.batch.sensor.duplication.NewDuplication +// import org.sonar.api.batch.sensor.highlighting.NewHighlighting +// import org.sonar.api.batch.sensor.issue.NewIssue +// import org.sonar.api.batch.sensor.measure.NewMeasure +// import org.sonar.api.batch.{AnalysisMode, Event, SensorContext} +// import org.sonar.api.config.Settings +// import org.sonar.api.design.Dependency +// import org.sonar.api.measures.{Measure, MeasuresFilter, Metric} +// import org.sonar.api.resources.{ProjectLink, Resource} +// import org.sonar.api.rules.Violation -import scala.collection.mutable +// import scala.collection.mutable -class TestSensorContext extends SensorContext { +// class TestSensorContext extends SensorContext { - private val measures = mutable.Map[String, Measure[_ <: io.Serializable]]() +// private val measures = mutable.Map[String, Measure[_ <: io.Serializable]]() - override def saveDependency(dependency: Dependency): Dependency = ??? +// override def saveDependency(dependency: Dependency): Dependency = ??? - override def isExcluded(reference: Resource): Boolean = ??? +// override def isExcluded(reference: Resource): Boolean = ??? - override def deleteLink(key: String): Unit = ??? +// override def deleteLink(key: String): Unit = ??? - override def isIndexed(reference: Resource, acceptExcluded: Boolean): Boolean = ??? +// override def isIndexed(reference: Resource, acceptExcluded: Boolean): Boolean = ??? - override def saveViolations(violations: util.Collection[Violation]): Unit = ??? +// override def saveViolations(violations: util.Collection[Violation]): Unit = ??? - override def getParent(reference: Resource): Resource = ??? +// override def getParent(reference: Resource): Resource = ??? - override def getOutgoingDependencies(from: Resource): util.Collection[Dependency] = ??? +// override def getOutgoingDependencies(from: Resource): util.Collection[Dependency] = ??? - override def saveSource(reference: Resource, source: String): Unit = ??? +// override def saveSource(reference: Resource, source: String): Unit = ??? - override def getMeasures[M](filter: MeasuresFilter[M]): M = ??? +// override def getMeasures[M](filter: MeasuresFilter[M]): M = ??? - override def getMeasures[M](resource: Resource, filter: MeasuresFilter[M]): M = ??? +// override def getMeasures[M](resource: Resource, filter: MeasuresFilter[M]): M = ??? - override def deleteEvent(event: Event): Unit = ??? +// override def deleteEvent(event: Event): Unit = ??? - override def saveViolation(violation: Violation, force: Boolean): Unit = ??? +// override def saveViolation(violation: Violation, force: Boolean): Unit = ??? - override def saveViolation(violation: Violation): Unit = ??? +// override def saveViolation(violation: Violation): Unit = ??? - override def saveResource(resource: Resource): String = ??? +// override def saveResource(resource: Resource): String = ??? - override def getEvents(resource: Resource): util.List[Event] = ??? +// override def getEvents(resource: Resource): util.List[Event] = ??? - override def getDependencies: util.Set[Dependency] = ??? +// override def getDependencies: util.Set[Dependency] = ??? - override def getIncomingDependencies(to: Resource): util.Collection[Dependency] = ??? +// override def getIncomingDependencies(to: Resource): util.Collection[Dependency] = ??? - override def index(resource: Resource): Boolean = ??? +// override def index(resource: Resource): Boolean = ??? - override def index(resource: Resource, parentReference: Resource): Boolean = ??? +// override def index(resource: Resource, parentReference: Resource): Boolean = ??? - override def saveLink(link: ProjectLink): Unit = ??? +// override def saveLink(link: ProjectLink): Unit = ??? - override def getMeasure[G <: io.Serializable](metric: Metric[G]): Measure[G] = measures.get(metric.getKey).orNull.asInstanceOf[Measure[G]] +// override def getMeasure[G <: io.Serializable](metric: Metric[G]): Measure[G] = measures.get(metric.getKey).orNull.asInstanceOf[Measure[G]] - override def getMeasure[G <: io.Serializable](resource: Resource, metric: Metric[G]): Measure[G] = ??? +// override def getMeasure[G <: io.Serializable](resource: Resource, metric: Metric[G]): Measure[G] = ??? - override def getChildren(reference: Resource): util.Collection[Resource] = ??? +// override def getChildren(reference: Resource): util.Collection[Resource] = ??? - override def createEvent(resource: Resource, name: String, description: String, category: String, date: Date): Event = ??? +// override def createEvent(resource: Resource, name: String, description: String, category: String, date: Date): Event = ??? - override def getResource[R <: Resource](reference: R): R = ??? +// override def getResource[R <: Resource](reference: R): R = ??? - override def getResource(inputPath: InputPath): Resource = ??? +// override def getResource(inputPath: InputPath): Resource = ??? - override def saveMeasure(measure: Measure[_ <: io.Serializable]): Measure[_ <: io.Serializable] = ??? +// override def saveMeasure(measure: Measure[_ <: io.Serializable]): Measure[_ <: io.Serializable] = ??? - override def saveMeasure(metric: Metric[_ <: io.Serializable], value: Double): Measure[_ <: io.Serializable] = ??? +// override def saveMeasure(metric: Metric[_ <: io.Serializable], value: Double): Measure[_ <: io.Serializable] = ??? - override def saveMeasure(resource: Resource, metric: Metric[_ <: io.Serializable], value: Double): Measure[_ <: io.Serializable] = ??? +// override def saveMeasure(resource: Resource, metric: Metric[_ <: io.Serializable], value: Double): Measure[_ <: io.Serializable] = ??? - override def saveMeasure(resource: Resource, measure: Measure[_ <: io.Serializable]): Measure[_ <: io.Serializable] = { - measures.put(resource.getKey, measure) - measure - } +// override def saveMeasure(resource: Resource, measure: Measure[_ <: io.Serializable]): Measure[_ <: io.Serializable] = { +// measures.put(resource.getKey, measure) +// measure +// } - override def saveMeasure(inputFile: InputFile, metric: Metric[_ <: io.Serializable], value: Double): Measure[_ <: io.Serializable] = ??? +// override def saveMeasure(inputFile: InputFile, metric: Metric[_ <: io.Serializable], value: Double): Measure[_ <: io.Serializable] = ??? - override def saveMeasure(inputFile: InputFile, measure: Measure[_ <: io.Serializable]): Measure[_ <: io.Serializable] = ??? +// override def saveMeasure(inputFile: InputFile, measure: Measure[_ <: io.Serializable]): Measure[_ <: io.Serializable] = ??? - override def newDuplication(): NewDuplication = ??? +// override def newDuplication(): NewDuplication = ??? - override def activeRules(): ActiveRules = ??? +// override def activeRules(): ActiveRules = ??? - override def newHighlighting(): NewHighlighting = ??? +// override def newHighlighting(): NewHighlighting = ??? - override def analysisMode(): AnalysisMode = ??? +// override def analysisMode(): AnalysisMode = ??? - override def fileSystem(): FileSystem = ??? +// override def fileSystem(): FileSystem = ??? - override def newDependency(): NewDependency = ??? +// override def newDependency(): NewDependency = ??? - override def settings(): Settings = ??? +// override def settings(): Settings = ??? - override def newMeasure[G <: io.Serializable](): NewMeasure[G] = ??? +// override def newMeasure[G <: io.Serializable](): NewMeasure[G] = ??? - override def newIssue(): NewIssue = ??? -} \ No newline at end of file +// override def newIssue(): NewIssue = ??? +// }