├── .gitignore ├── LICENSE ├── README.md ├── circle.yml ├── pom.xml ├── project ├── .gitignore ├── build.properties ├── build.scala └── plugins.sbt ├── samples ├── maven │ ├── .gitignore │ ├── README │ ├── combined-scala-java-multi-module-sonar │ │ ├── module1 │ │ │ ├── pom.xml │ │ │ └── src │ │ │ │ ├── main │ │ │ │ ├── java │ │ │ │ │ └── module1 │ │ │ │ │ │ └── HelloWorld.java │ │ │ │ └── scala │ │ │ │ │ └── module1 │ │ │ │ │ └── HelloScala.scala │ │ │ │ └── test │ │ │ │ ├── java │ │ │ │ └── module1 │ │ │ │ │ └── HelloWorldTest.java │ │ │ │ └── scala │ │ │ │ └── HelloScalaTest.scala │ │ ├── module2 │ │ │ ├── pom.xml │ │ │ └── src │ │ │ │ ├── main │ │ │ │ ├── java │ │ │ │ │ └── module2 │ │ │ │ │ │ └── HelloWorld2.java │ │ │ │ └── scala │ │ │ │ │ └── module2 │ │ │ │ │ └── HelloScala2.scala │ │ │ │ └── test │ │ │ │ ├── java │ │ │ │ └── HelloWorld2Test.java │ │ │ │ └── scala │ │ │ │ └── HelloScala2Test.scala │ │ └── pom.xml │ └── combined-scala-java-sonar │ │ ├── pom.xml │ │ └── src │ │ ├── main │ │ ├── java │ │ │ └── module1 │ │ │ │ └── HelloWorld.java │ │ └── scala │ │ │ └── module1 │ │ │ └── HelloScala.scala │ │ └── test │ │ ├── java │ │ └── module1 │ │ │ └── HelloWorldTest.java │ │ └── scala │ │ └── HelloScalaTest.scala └── sbt │ └── multi-module │ ├── .gitignore │ ├── README.md │ ├── build.sbt │ ├── module1 │ ├── build.sbt │ └── src │ │ ├── main │ │ └── scala │ │ │ └── com │ │ │ └── buransky │ │ │ └── plugins │ │ │ └── scoverage │ │ │ └── samples │ │ │ └── sbt │ │ │ └── multiModule │ │ │ └── module1 │ │ │ ├── Beer.scala │ │ │ └── Pub.scala │ │ └── test │ │ └── scala │ │ └── com │ │ └── buransky │ │ └── plugins │ │ └── scoverage │ │ └── samples │ │ └── sbt │ │ └── multiModule │ │ └── module1 │ │ ├── BeerSpec.scala │ │ └── PubSpec.scala │ ├── module2 │ ├── build.sbt │ └── src │ │ ├── main │ │ └── scala │ │ │ └── com │ │ │ └── buransky │ │ │ └── plugins │ │ │ └── scoverage │ │ │ └── samples │ │ │ └── sbt │ │ │ └── multiModule │ │ │ └── module2 │ │ │ └── Animal.scala │ │ └── test │ │ └── scala │ │ └── com │ │ └── buransky │ │ └── plugins │ │ └── scoverage │ │ └── samples │ │ └── sbt │ │ └── multiModule │ │ └── module2 │ │ └── AnimalSpec.scala │ ├── project │ ├── Common.scala │ ├── build.properties │ └── plugins.sbt │ └── sonar-project.properties ├── sonar-project.properties └── src ├── main ├── resources │ └── com │ │ └── buransky │ │ └── plugins │ │ └── scoverage │ │ └── widget.html.erb └── scala │ └── com │ ├── buransky │ └── plugins │ │ └── scoverage │ │ ├── ScoverageReportParser.scala │ │ ├── StatementCoverage.scala │ │ ├── language │ │ └── Scala.scala │ │ ├── measure │ │ └── ScalaMetrics.scala │ │ ├── pathcleaner │ │ ├── BruteForceSequenceMatcher.scala │ │ └── PathSanitizer.scala │ │ ├── sensor │ │ └── ScoverageSensor.scala │ │ ├── util │ │ ├── LogUtil.scala │ │ └── PathUtil.scala │ │ ├── widget │ │ └── ScoverageWidget.scala │ │ └── xml │ │ ├── XmlScoverageReportConstructingParser.scala │ │ └── XmlScoverageReportParser.scala │ ├── ncredinburgh │ └── sonar │ │ └── scalastyle │ │ ├── Constants.scala │ │ ├── RepositoryRule.scala │ │ ├── ScalastyleQualityProfile.scala │ │ ├── ScalastyleRepository.scala │ │ ├── ScalastyleResources.scala │ │ ├── ScalastyleRunner.scala │ │ └── ScalastyleSensor.scala │ └── sagacify │ └── sonar │ └── scala │ ├── Measures.scala │ ├── ScalaPlugin.scala │ └── ScalaSensor.scala └── test ├── resources ├── ScalaFile1.scala └── ScalaFile2.scala └── scala └── com ├── buransky └── plugins │ └── scoverage │ ├── pathcleaner │ └── BruteForceSequenceMatcherSpec.scala │ ├── sensor │ ├── ScoverageSensorSpec.scala │ └── TestSensorContext.scala │ ├── util │ └── PathUtilSpec.scala │ └── xml │ ├── XmlScoverageReportConstructingParserSpec.scala │ ├── XmlScoverageReportParserSpec.scala │ └── data │ └── XmlReportFile1.scala ├── ncredinburgh └── sonar │ └── scalastyle │ ├── ScalastyleAdaptedQualityProfileSpec.scala │ ├── ScalastyleDefaultQualityProfileSpec.scala │ ├── ScalastyleRepositorySpec.scala │ ├── ScalastyleResourcesSpec.scala │ ├── ScalastyleRunnerSpec.scala │ ├── ScalastyleSensorSpec.scala │ └── testUtils │ ├── TestRuleFinder.scala │ └── TestRuleFinderWithTemplates.scala └── sagacify └── sonar └── scala ├── BaseMetricSensorSpec.scala ├── MeasuresSpec.scala └── ScalaPluginSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache/ 6 | .history/ 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | .classpath 19 | .project 20 | .settings 21 | .cache 22 | 23 | # Intellij IDEA Specific 24 | .idea/* 25 | *.iml 26 | *.iws 27 | *.ipr 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sonar-scala 2 | Sonarqube plugin for scala analysis 3 | 4 | # Set-up 5 | Intended for sonarqube 5.4 6 | 7 | Download the latest relase into your sonar extentions/downloads folder. 8 | Restart sonarqube either using the update center or manually. 9 | 10 | 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. 11 | 12 | For more information about either scalastyle rules or scoverage results please consult their upstream documentation first: 13 | 14 | * [NCR-CoDE/sonar-scalastyle](https://github.com/NCR-CoDE/sonar-scalastyle) 15 | * [RadoBuransky/sonar-scoverage-plugin](https://github.com/RadoBuransky/sonar-scoverage-plugin) 16 | 17 | # Build from source 18 | ```mvn package``` 19 | 20 | # Test 21 | ``` 22 | mvn test 23 | sonar-runner -D sonar.projectKey=Sagacify:sonar-scala 24 | ``` 25 | 26 | # Contributing 27 | Contributions wre accepted in the form of a pull request or a signed patch. 28 | Please follow the semantic changelog to format your commits [cfr](https://github.com/Sagacify/komitet-gita-bezopasnosti). 29 | All changes are submitted to automated tests that must pass for the pull-request to be merged. 30 | 31 | # Info 32 | 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). 33 | The previous plugin used the scala compiler to create its metrics which had the disadvantage of requiring a specific plugin per scala version. 34 | 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. 35 | 36 | # TODO (by priority) 37 | * Add property to sepcify scala version (currently defaults to 2.11.8) 38 | * Add Complexity metric on file (use the one in scalastyle) 39 | * remove dependency on commons-io (Currently only needed by BruteForceSequenceMatcher) 40 | * Uncomment ScoverageSensorSpec 41 | * Integrate other java compatible code quality tools 42 | * Optimize sensors i.e. (scalastyle and base both read and parse source files.) 43 | ... 44 | 45 | # Credits 46 | Many existing projects have been used as inspiration. 47 | Here is a list of the main ones. 48 | 49 | [1and1/sonar-scala](https://github.com/1and1/sonar-scala) 50 | 51 | [SonarSource/sonar-java](https://github.com/SonarSource/sonar-java) 52 | 53 | [SonarSource/sonar-examples](https://github.com/SonarSource/sonar-examples) 54 | 55 | [NCR-CoDE/sonar-scalastyle](https://github.com/NCR-CoDE/sonar-scalastyle) 56 | 57 | [RadoBuransky/sonar-scoverage-plugin](https://github.com/RadoBuransky/sonar-scoverage-plugin) 58 | 59 | # Integration 60 | For ease of use, Sonar Scala directly integrates the latest code from the [Sonar Scalastyle Plugin](https://github.com/NCR-CoDE/sonar-scalastyle) and [Sonar Scoverage Plugin](https://github.com/RadoBuransky/sonar-scoverage-plugin). This is possible as all three projects are released under the LGPL3 license. Nevertheless, all merged files are to keep their original copyright, classpath, and commit history. Any further change upstream should be incorporated using cherry-picks or merges. 61 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | # Launch the build of the docker image 2 | dependencies: 3 | override: 4 | - wget https://raw.githubusercontent.com/Sagacify/ci-tools/master/run-sonar.sh 5 | - chmod +x run-sonar.sh 6 | - ./run-sonar.sh check 7 | - ./run-sonar.sh install 8 | - mvn dependency:resolve 9 | 10 | # Launch the test into the docker image 11 | test: 12 | override: 13 | - mvn scoverage:report 14 | - ./run-sonar.sh run 15 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.sonarsource.parent 7 | parent 8 | 31 9 | 10 | 11 | sonar-scala-plugin 12 | sonar-plugin 13 | 0.0.3-SNAPSHOT 14 | 15 | Sonar Scala Plugin 16 | Enables analysis of Scala projects into Sonar. 17 | http://github.com/sagacify/sonar-scala 18 | 2016 19 | 20 | 21 | 22 | GNU LGPL 3 23 | http://www.gnu.org/licenses/lgpl.txt 24 | repo 25 | 26 | 27 | 28 | 29 | Sagacify 30 | https://www.sagacify.com 31 | 32 | 33 | 34 | 35 | aborsu 36 | Augustin Borsu 37 | a.borsu@gmail.com 38 | http://www.acelpb.com/ 39 | 40 | 41 | 42 | 43 | scm:git:git@github.com:sagacify/sonar-scala.git 44 | scm:git:git@github.com:sagacify/sonar-scala.git 45 | https://github.com/sagacify/sonar-scala 46 | HEAD 47 | 48 | 49 | 50 | https://github.com/sagacify/sonar-scala/issues 51 | 52 | 53 | 54 | 5.4 55 | scala 56 | Scala 57 | com.sagacify.sonar.scala.ScalaPlugin 58 | 59 | 2.11.8 60 | 2.11 61 | 62 | 63 | 64 | 65 | 66 | org.sonarsource.sonarqube 67 | sonar-plugin-api 68 | ${sonar.version} 69 | provided 70 | 71 | 72 | org.scala-lang 73 | scala-library 74 | ${scala.version} 75 | 76 | 77 | 78 | org.scalariform 79 | scalariform_${scala.major.version} 80 | 0.1.8 81 | 82 | 83 | org.scala-lang 84 | scala-library 85 | 86 | 87 | 88 | 89 | org.slf4j 90 | slf4j-api 91 | 1.6.2 92 | provided 93 | 94 | 95 | 96 | com.google.code.findbugs 97 | jsr305 98 | 3.0.0 99 | 100 | 101 | 102 | commons-io 103 | commons-io 104 | 2.5 105 | 106 | 107 | 108 | 109 | org.scalatest 110 | scalatest_${scala.major.version} 111 | 2.2.6 112 | test 113 | 114 | 115 | org.scala-lang 116 | scala-library 117 | 118 | 119 | 120 | 121 | org.sonarsource.sonarqube 122 | sonar-testing-harness 123 | ${sonar.version} 124 | test 125 | 126 | 127 | 128 | org.scalastyle 129 | scalastyle_${scala.major.version} 130 | 0.8.0 131 | 132 | 133 | org.scala-lang 134 | scala-library 135 | 136 | 137 | 138 | 139 | 140 | org.sonarsource.sonarqube 141 | sonar-core 142 | ${sonar.version} 143 | test 144 | 145 | 146 | 147 | 148 | src/main/scala 149 | 150 | 151 | org.scoverage 152 | scoverage-maven-plugin 153 | 1.1.1 154 | 155 | 156 | 157 | 158 | org.apache.maven.plugins 159 | maven-surefire-plugin 160 | 161 | true 162 | 163 | 164 | 165 | 166 | org.scalatest 167 | scalatest-maven-plugin 168 | 1.0 169 | 170 | ${project.build.directory}/surefire-reports 171 | . 172 | WDF TestSuite.txt 173 | 174 | 175 | 176 | test 177 | 178 | test 179 | 180 | 181 | 182 | 183 | 184 | 185 | net.alchim31.maven 186 | scala-maven-plugin 187 | 3.2.2 188 | 189 | 190 | 191 | compile 192 | testCompile 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | -------------------------------------------------------------------------------- /project/.gitignore: -------------------------------------------------------------------------------- 1 | project 2 | target 3 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.8 -------------------------------------------------------------------------------- /project/build.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | object MyBuild extends com.typesafe.sbt.pom.PomBuild 3 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.sbt" % "sbt-pom-reader" % "2.0.0") -------------------------------------------------------------------------------- /samples/maven/.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # Package files 5 | *.war 6 | *.jar 7 | *.ear 8 | 9 | # Maven 10 | out/ 11 | target/ 12 | 13 | # Eclipse 14 | .project 15 | .classpath 16 | .settings/ 17 | 18 | # IDEA 19 | *.iml 20 | .idea 21 | 22 | # OSX 23 | .DS_STORE 24 | .Trashes 25 | 26 | # Windows 27 | Desktop.ini 28 | Thumbs.db 29 | 30 | # Python 31 | *.pyc 32 | 33 | -------------------------------------------------------------------------------- /samples/maven/README: -------------------------------------------------------------------------------- 1 | Run with: 2 | 3 | `mvn clean scoverage:report sonar:sonar` -------------------------------------------------------------------------------- /samples/maven/combined-scala-java-multi-module-sonar/module1/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | combined-scala-java-multi-module-sonar 6 | test 7 | 1.0.0 8 | 9 | module1 10 | jar 11 | 1.0.0 12 | 13 | -------------------------------------------------------------------------------- /samples/maven/combined-scala-java-multi-module-sonar/module1/src/main/java/module1/HelloWorld.java: -------------------------------------------------------------------------------- 1 | package module1; 2 | 3 | /** 4 | * Created by tim on 01/05/15. 5 | */ 6 | public class HelloWorld { 7 | 8 | public String hello() { 9 | return "Hello"; 10 | } 11 | 12 | public void notCovered() { 13 | System.out.println("YOLO"); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /samples/maven/combined-scala-java-multi-module-sonar/module1/src/main/scala/module1/HelloScala.scala: -------------------------------------------------------------------------------- 1 | package module1 2 | 3 | class HelloScala { 4 | 5 | case class TryOut(some: String, fields: List[String]) 6 | 7 | def test = "Hello" 8 | 9 | def someOther = 42 10 | } -------------------------------------------------------------------------------- /samples/maven/combined-scala-java-multi-module-sonar/module1/src/test/java/module1/HelloWorldTest.java: -------------------------------------------------------------------------------- 1 | package module1; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | public class HelloWorldTest { 6 | 7 | @org.junit.Test 8 | public void testHello() throws Exception { 9 | assertEquals("Hello", new HelloWorld().hello()); 10 | } 11 | } -------------------------------------------------------------------------------- /samples/maven/combined-scala-java-multi-module-sonar/module1/src/test/scala/HelloScalaTest.scala: -------------------------------------------------------------------------------- 1 | import org.junit.runner.RunWith 2 | import org.scalatest.junit.JUnitRunner 3 | import org.scalatest.{FlatSpec, ShouldMatchers} 4 | import module1.HelloScala 5 | 6 | @RunWith(classOf[JUnitRunner]) 7 | class HelloScalaTest extends FlatSpec with ShouldMatchers { 8 | 9 | "it" should "work" in { 10 | val scala: HelloScala = new HelloScala() 11 | scala.test should equal("Hello") 12 | 13 | scala.TryOut("String", List()) should not equal(true) 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /samples/maven/combined-scala-java-multi-module-sonar/module2/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | combined-scala-java-multi-module-sonar 6 | test 7 | 1.0.0 8 | 9 | module2 10 | jar 11 | 1.0.0 12 | 13 | -------------------------------------------------------------------------------- /samples/maven/combined-scala-java-multi-module-sonar/module2/src/main/java/module2/HelloWorld2.java: -------------------------------------------------------------------------------- 1 | package module2; 2 | 3 | /** 4 | * Created by tim on 01/05/15. 5 | */ 6 | public class HelloWorld2 { 7 | 8 | public String hello() { 9 | return "Hello"; 10 | } 11 | 12 | public void notCovered() { 13 | System.out.println("YOLO"); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /samples/maven/combined-scala-java-multi-module-sonar/module2/src/main/scala/module2/HelloScala2.scala: -------------------------------------------------------------------------------- 1 | package module2 2 | 3 | class HelloScala2 { 4 | 5 | case class TryOut(some: String, fields: List[String]) 6 | 7 | def test = "Hello" 8 | 9 | def someOther = 42 10 | } -------------------------------------------------------------------------------- /samples/maven/combined-scala-java-multi-module-sonar/module2/src/test/java/HelloWorld2Test.java: -------------------------------------------------------------------------------- 1 | import module2.HelloWorld2; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | public class HelloWorld2Test { 6 | 7 | @org.junit.Test 8 | public void testHello() throws Exception { 9 | assertEquals("Hello", new HelloWorld2().hello()); 10 | } 11 | } -------------------------------------------------------------------------------- /samples/maven/combined-scala-java-multi-module-sonar/module2/src/test/scala/HelloScala2Test.scala: -------------------------------------------------------------------------------- 1 | import module2.HelloScala2 2 | import org.junit.runner.RunWith 3 | import org.scalatest.junit.JUnitRunner 4 | import org.scalatest.{FlatSpec, ShouldMatchers} 5 | 6 | @RunWith(classOf[JUnitRunner]) 7 | class HelloScala2Test extends FlatSpec with ShouldMatchers { 8 | 9 | "it" should "work" in { 10 | val scala: HelloScala2 = new HelloScala2() 11 | scala.test should equal("Hello") 12 | 13 | scala.TryOut("String", List()) should not equal(true) 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /samples/maven/combined-scala-java-multi-module-sonar/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | test 6 | combined-scala-java-multi-module-sonar 7 | pom 8 | 1.0.0 9 | 10 | 11 | module1 12 | module2 13 | 14 | 15 | 16 | 2.11.6 17 | 0.13.8 18 | 19 | scoverage 20 | jacoco 21 | target/surefire-reports 22 | target/scoverage.xml 23 | src 24 | target/jacoco.exec 25 | src/test/** 26 | UTF-8 27 | 28 | 29 | 30 | 31 | 32 | 33 | com.google.code.sbt-compiler-maven-plugin 34 | sbt-compiler-maven-plugin 35 | 1.0.0-beta5 36 | 37 | 38 | 39 | compile 40 | testCompile 41 | addScalaSources 42 | 43 | default-sbt-compile 44 | 45 | 46 | 47 | 48 | org.apache.maven.plugins 49 | maven-compiler-plugin 50 | 3.2 51 | 52 | true 53 | true 54 | 55 | 56 | 57 | 58 | org.jacoco 59 | jacoco-maven-plugin 60 | 0.7.4.201502262128 61 | 62 | 63 | pre-test 64 | 65 | prepare-agent 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.scoverage 73 | scoverage-maven-plugin 74 | 1.3.0 75 | 76 | true 77 | 78 | 79 | 80 | instrument 81 | 82 | 83 | pre-compile 84 | 85 | post-compile 86 | 87 | 88 | 89 | scoverage-report 90 | 91 | 92 | report-only 93 | 94 | prepare-package 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | org.scalatest 104 | scalatest_2.11 105 | 2.2.1 106 | test 107 | 108 | 109 | org.mockito 110 | mockito-all 111 | 1.9.5 112 | test 113 | 114 | 115 | junit 116 | junit 117 | 4.11 118 | 119 | 120 | 121 | ch.qos.logback 122 | logback-classic 123 | 1.0.13 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /samples/maven/combined-scala-java-sonar/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | test 6 | combined-scala-java-sonar-single-module 7 | jar 8 | 1.0.0 9 | 10 | 11 | 2.11.6 12 | 13 | scoverage 14 | jacoco 15 | target/surefire-reports 16 | target/scoverage.xml 17 | target/notthere.xml 18 | src 19 | target/jacoco.exec 20 | src/test/java/**,src/test/scala/** 21 | UTF-8 22 | 23 | 24 | 25 | 26 | 27 | 28 | com.google.code.sbt-compiler-maven-plugin 29 | sbt-compiler-maven-plugin 30 | 1.0.0-beta5 31 | 32 | 33 | 34 | compile 35 | testCompile 36 | addScalaSources 37 | 38 | default-sbt-compile 39 | 40 | 41 | 42 | 43 | org.apache.maven.plugins 44 | maven-compiler-plugin 45 | 3.2 46 | 47 | true 48 | true 49 | 50 | 51 | 52 | 53 | org.jacoco 54 | jacoco-maven-plugin 55 | 0.7.4.201502262128 56 | 57 | 58 | pre-test 59 | 60 | prepare-agent 61 | 62 | 63 | 64 | 65 | 66 | 67 | org.scoverage 68 | scoverage-maven-plugin 69 | 1.3.0 70 | 71 | true 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | org.scalatest 80 | scalatest_2.11 81 | 2.2.1 82 | test 83 | 84 | 85 | org.mockito 86 | mockito-all 87 | 1.9.5 88 | test 89 | 90 | 91 | junit 92 | junit 93 | 4.11 94 | 95 | 96 | 97 | ch.qos.logback 98 | logback-classic 99 | 1.0.13 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /samples/maven/combined-scala-java-sonar/src/main/java/module1/HelloWorld.java: -------------------------------------------------------------------------------- 1 | package module1; 2 | 3 | /** 4 | * Created by tim on 01/05/15. 5 | */ 6 | public class HelloWorld { 7 | 8 | public String hello() { 9 | return "Hello"; 10 | } 11 | 12 | public void notCovered() { 13 | System.out.println("YOLO"); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /samples/maven/combined-scala-java-sonar/src/main/scala/module1/HelloScala.scala: -------------------------------------------------------------------------------- 1 | package module1 2 | 3 | class HelloScala { 4 | 5 | case class TryOut(some: String, fields: List[String]) 6 | 7 | def test = "Hello" 8 | 9 | def someOther = 42 10 | } -------------------------------------------------------------------------------- /samples/maven/combined-scala-java-sonar/src/test/java/module1/HelloWorldTest.java: -------------------------------------------------------------------------------- 1 | package module1; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | public class HelloWorldTest { 6 | 7 | @org.junit.Test 8 | public void testHello() throws Exception { 9 | assertEquals("Hello", new HelloWorld().hello()); 10 | } 11 | } -------------------------------------------------------------------------------- /samples/maven/combined-scala-java-sonar/src/test/scala/HelloScalaTest.scala: -------------------------------------------------------------------------------- 1 | import org.junit.runner.RunWith 2 | import org.scalatest.junit.JUnitRunner 3 | import org.scalatest.{FlatSpec, ShouldMatchers} 4 | import module1.HelloScala 5 | 6 | @RunWith(classOf[JUnitRunner]) 7 | class HelloScalaTest extends FlatSpec with ShouldMatchers { 8 | 9 | "it" should "work" in { 10 | val scala: HelloScala = new HelloScala() 11 | scala.test should equal("Hello") 12 | 13 | scala.TryOut("String", List()) should not equal(true) 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /samples/sbt/multi-module/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .idea_modules 3 | target -------------------------------------------------------------------------------- /samples/sbt/multi-module/README.md: -------------------------------------------------------------------------------- 1 | # Multi-module SBT sample project for Sonar Scoverage plugin # 2 | 3 | 1. Create quality profile for Scala language and set it to be used by default. 4 | 5 | 2. Run scoverage to generate coverage reports: 6 | 7 | $ sbt clean coverage test 8 | 9 | 3. And then run Sonar runner to upload data from reports to the Sonar server: 10 | 11 | $ sonar-runner 12 | 13 | ## Requirements ## 14 | 15 | - Installed Sonar Scoverage plugin 16 | - Installed SBT 17 | - Installed Sonar runner -------------------------------------------------------------------------------- /samples/sbt/multi-module/build.sbt: -------------------------------------------------------------------------------- 1 | organization in ThisBuild := "com.buransky" 2 | 3 | scalaVersion in ThisBuild := "2.11.6" 4 | 5 | version in ThisBuild := "5.1.0" 6 | 7 | lazy val module1 = project 8 | 9 | lazy val module2 = project 10 | -------------------------------------------------------------------------------- /samples/sbt/multi-module/module1/build.sbt: -------------------------------------------------------------------------------- 1 | name := Common.baseName + "-module1" 2 | 3 | libraryDependencies += Common.scalatest -------------------------------------------------------------------------------- /samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Beer.scala: -------------------------------------------------------------------------------- 1 | package com.buransky.plugins.scoverage.samples.sbt.multiModule.module1 2 | 3 | import scala.util.Random 4 | 5 | trait Beer { 6 | val volume: Double 7 | def isGood: Boolean = (volume > 0.0) 8 | } 9 | 10 | case object EmptyBeer extends { 11 | val volume = 0.0 12 | } with Beer 13 | 14 | trait SlovakBeer extends Beer { 15 | override def isGood = Random.nextBoolean 16 | } 17 | 18 | trait BelgianBeer extends Beer { 19 | if (volume > 0.25) 20 | throw new IllegalArgumentException("Too big beer for belgian beer!") 21 | 22 | override def isGood = true 23 | } 24 | 25 | case class HordonBeer(volume: Double) extends SlovakBeer { 26 | override def isGood = false 27 | } 28 | 29 | case class ChimayBeer(volume: Double) extends BelgianBeer 30 | -------------------------------------------------------------------------------- /samples/sbt/multi-module/module1/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/Pub.scala: -------------------------------------------------------------------------------- 1 | package com.buransky.plugins.scoverage.samples.sbt.multiModule.module1 2 | 3 | trait Pub { 4 | def offer: Iterable[_ <: Beer] 5 | def giveMeGreat: Beer 6 | } 7 | 8 | object Delirium extends Pub { 9 | def offer = List(HordonBeer(0.5), ChimayBeer(0.2)) 10 | def giveMeGreat = offer.filter(_.isGood).filter(_.volume > 0.3).head 11 | } -------------------------------------------------------------------------------- /samples/sbt/multi-module/module1/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/BeerSpec.scala: -------------------------------------------------------------------------------- 1 | package com.buransky.plugins.scoverage.samples.sbt.multiModule.module1 2 | 3 | import org.scalatest.{Matchers, FlatSpec} 4 | 5 | class BeerSpec extends FlatSpec with Matchers { 6 | behavior of "Beer" 7 | 8 | "isGood" must "be true if not empty" in { 9 | val beer = new Beer { val volume = 0.1 } 10 | beer.isGood should equal(true) 11 | } 12 | 13 | behavior of "EmptyBeer" 14 | 15 | it must "be empty" in { 16 | EmptyBeer.volume should equal(0.0) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /samples/sbt/multi-module/module1/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module1/PubSpec.scala: -------------------------------------------------------------------------------- 1 | package com.buransky.plugins.scoverage.samples.sbt.multiModule.module1 2 | 3 | import org.scalatest.{FlatSpec, Matchers} 4 | 5 | class PubSpec extends FlatSpec with Matchers { 6 | behavior of "Delirium" 7 | 8 | it must "give me what I want" in { 9 | the[NoSuchElementException] thrownBy Delirium.giveMeGreat 10 | } 11 | } -------------------------------------------------------------------------------- /samples/sbt/multi-module/module2/build.sbt: -------------------------------------------------------------------------------- 1 | name := Common.baseName + "-module2" 2 | 3 | libraryDependencies += Common.scalatest -------------------------------------------------------------------------------- /samples/sbt/multi-module/module2/src/main/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module2/Animal.scala: -------------------------------------------------------------------------------- 1 | package com.buransky.plugins.scoverage.samples.sbt.multiModule.module2 2 | 3 | trait Animal { 4 | val legs: Int 5 | val eyes: Int 6 | val canFly: Boolean 7 | val canSwim: Boolean 8 | } 9 | 10 | object Animal { 11 | def fancy(farm: Iterable[Animal]): Iterable[Animal] = 12 | farm.filter(_.legs > 10).filter(_.canFly).filter(_.canSwim) 13 | } -------------------------------------------------------------------------------- /samples/sbt/multi-module/module2/src/test/scala/com/buransky/plugins/scoverage/samples/sbt/multiModule/module2/AnimalSpec.scala: -------------------------------------------------------------------------------- 1 | package com.buransky.plugins.scoverage.samples.sbt.multiModule.module2 2 | 3 | import org.scalatest.{Matchers, FlatSpec} 4 | 5 | class AnimalSpec extends FlatSpec with Matchers { 6 | behavior of "fancy" 7 | 8 | it should "do nothing" in { 9 | Animal.fancy(Nil) should equal(Nil) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /samples/sbt/multi-module/project/Common.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Common { 4 | val baseName = "multi-module" 5 | val scalatest = "org.scalatest" % "scalatest_2.11" % "2.2.4" 6 | } -------------------------------------------------------------------------------- /samples/sbt/multi-module/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.6 -------------------------------------------------------------------------------- /samples/sbt/multi-module/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Classpaths.sbtPluginReleases 2 | 3 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.0.4") -------------------------------------------------------------------------------- /samples/sbt/multi-module/sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=com.buransky:multi-module 2 | sonar.projectName=Sonar Scoverage plugin multi-module sample project 3 | sonar.projectVersion=5.1.0 4 | 5 | sonar.language=scala 6 | 7 | sonar.modules=module1,module2 8 | 9 | module1.sonar.sources=src/main/scala 10 | module1.sonar.tests=src/test/scala 11 | module1.sonar.scoverage.reportPath=target/scala-2.11/scoverage-report/scoverage.xml 12 | 13 | module2.sonar.sources=src/main/scala 14 | module2.sonar.tests=src/test/scala 15 | module2.sonar.scoverage.reportPath=target/scala-2.11/scoverage-report/scoverage.xml 16 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | # Required metadata 2 | sonar.projectName=Sonar Scala Plugin 3 | sonar.projectVersion=0.0.3 4 | 5 | # Comma-separated paths to directories with sources (required) 6 | sonar.sources=src 7 | sonar.exclusions=src/test/resources/**, 8 | 9 | # Encoding of the source files 10 | sonar.sourceEncoding=UTF-8 11 | 12 | # Code Coverage reports 13 | sonar.scoverage.reportPath=target/scoverage.xml 14 | -------------------------------------------------------------------------------- /src/main/resources/com/buransky/plugins/scoverage/widget.html.erb: -------------------------------------------------------------------------------- 1 | <% measure=measure('scoverage') 2 | if measure 3 | %> 4 |
5 |

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

9 |
10 | <% end %> -------------------------------------------------------------------------------- /src/main/scala/com/buransky/plugins/scoverage/ScoverageReportParser.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scoverage Plugin 3 | * Copyright (C) 2013 Rado Buransky 4 | * dev@sonar.codehaus.org 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | */ 20 | package com.buransky.plugins.scoverage 21 | 22 | import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer 23 | 24 | /** 25 | * Interface for Scoverage report parser. 26 | * 27 | * @author Rado Buransky 28 | */ 29 | trait ScoverageReportParser { 30 | def parse(reportFilePath: String, pathSanitizer: PathSanitizer): ProjectStatementCoverage 31 | } 32 | 33 | /** 34 | * Common Scoverage exception. 35 | * 36 | * @author Rado Buransky 37 | */ 38 | case class ScoverageException(message: String, source: Throwable = null) 39 | extends Exception(message, source) 40 | -------------------------------------------------------------------------------- /src/main/scala/com/buransky/plugins/scoverage/StatementCoverage.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scoverage Plugin 3 | * Copyright (C) 2013 Rado Buransky 4 | * dev@sonar.codehaus.org 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | */ 20 | package com.buransky.plugins.scoverage 21 | 22 | /** 23 | * Statement coverage represents rate at which are statements of a certain source code unit 24 | * being covered by tests. 25 | * 26 | * @author Rado Buransky 27 | */ 28 | sealed trait StatementCoverage { 29 | /** 30 | * Percentage rate ranging from 0 up to 100%. 31 | */ 32 | lazy val rate: Double = 33 | if (statementCount == 0) 34 | 0.0 35 | else 36 | (coveredStatementsCount.toDouble / statementCount.toDouble) * 100.0 37 | 38 | /** 39 | * Total number of all statements within the source code unit, 40 | */ 41 | def statementCount: Int 42 | 43 | /** 44 | * Number of statements covered by unit tests. 45 | */ 46 | def coveredStatementsCount: Int 47 | 48 | require(statementCount >= 0, "Statements count cannot be negative! [" + statementCount + "]") 49 | require(coveredStatementsCount >= 0, "Statements count cannot be negative! [" + 50 | coveredStatementsCount + "]") 51 | require(coveredStatementsCount <= statementCount, 52 | "Number of covered statements cannot be more than total number of statements! [" + 53 | statementCount + ", " + coveredStatementsCount + "]") 54 | } 55 | 56 | /** 57 | * Allows to build tree structure from state coverage values. 58 | */ 59 | trait NodeStatementCoverage extends StatementCoverage { 60 | def name: String 61 | def children: Iterable[NodeStatementCoverage] 62 | def statementSum: Int = children.map(_.statementSum).sum 63 | def coveredStatementsSum: Int = children.map(_.coveredStatementsSum).sum 64 | } 65 | 66 | /** 67 | * Root node. In multi-module projects it can contain other ProjectStatementCoverage 68 | * elements as children. 69 | */ 70 | case class ProjectStatementCoverage(name: String, children: Iterable[NodeStatementCoverage]) 71 | extends NodeStatementCoverage { 72 | // projects' coverage values are defined as sums of their child values 73 | val statementCount = statementSum 74 | val coveredStatementsCount = coveredStatementsSum 75 | } 76 | 77 | /** 78 | * Physical directory in file system. 79 | */ 80 | case class DirectoryStatementCoverage(name: String, children: Iterable[NodeStatementCoverage]) 81 | extends NodeStatementCoverage { 82 | // directories' coverage values are defined as sums of their DIRECT child values 83 | val statementCount = children.filter(_.isInstanceOf[FileStatementCoverage]).map(_.statementCount).sum 84 | val coveredStatementsCount = children.filter(_.isInstanceOf[FileStatementCoverage]).map(_.coveredStatementsCount).sum 85 | } 86 | 87 | /** 88 | * Scala source code file. 89 | */ 90 | case class FileStatementCoverage(name: String, statementCount: Int, coveredStatementsCount: Int, 91 | statements: Iterable[CoveredStatement]) extends NodeStatementCoverage { 92 | // leaf implementation sums==values 93 | val children = List.empty[NodeStatementCoverage] 94 | override val statementSum = statementCount 95 | override val coveredStatementsSum = coveredStatementsCount 96 | } 97 | 98 | /** 99 | * Position a Scala source code file. 100 | */ 101 | case class StatementPosition(line: Int, pos: Int) 102 | 103 | /** 104 | * Coverage information about the Scala statement. 105 | * 106 | * @param start Starting position of the statement. 107 | * @param end Ending position of the statement. 108 | * @param hitCount How many times has the statement been hit by unit tests. Zero means 109 | * that the statement is not covered. 110 | */ 111 | case class CoveredStatement(start: StatementPosition, end: StatementPosition, hitCount: Int) 112 | 113 | /** 114 | * Aggregated statement coverage for a given source code line. 115 | */ 116 | case class CoveredLine(line: Int, hitCount: Int) 117 | 118 | object StatementCoverage { 119 | /** 120 | * Utility method to transform statement coverage to line coverage. Pessimistic logic is used 121 | * meaning that line hit count is minimum of hit counts of all statements on the given line. 122 | * 123 | * Example: If a line contains two statements, one is covered by 3 hits, the other one is 124 | * without any hits, then the whole line is treated as uncovered. 125 | * 126 | * @param statements Statement coverage. 127 | * @return Line coverage. 128 | */ 129 | def statementCoverageToLineCoverage(statements: Iterable[CoveredStatement]): Iterable[CoveredLine] = { 130 | // Handle statements that end on a different line than start 131 | val multilineStatements = statements.filter { s => s.start.line != s.end.line } 132 | val extraStatements = multilineStatements.flatMap { s => 133 | for (i <- (s.start.line + 1) to s.end.line) 134 | yield CoveredStatement(StatementPosition(i, 0), StatementPosition(i, 0), s.hitCount) 135 | } 136 | 137 | // Group statements by starting line 138 | val lineStatements = (statements ++ extraStatements).groupBy(_.start.line) 139 | 140 | // Pessimistic approach: line hit count is a minimum of hit counts of all statements on the line 141 | lineStatements.map { lineStatement => 142 | CoveredLine(lineStatement._1, lineStatement._2.map(_.hitCount).min) 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /src/main/scala/com/buransky/plugins/scoverage/language/Scala.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scoverage Plugin 3 | * Copyright (C) 2013 Rado Buransky 4 | * dev@sonar.codehaus.org 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | */ 20 | package com.buransky.plugins.scoverage.language 21 | 22 | import org.sonar.api.resources.AbstractLanguage 23 | 24 | /** 25 | * Scala language. 26 | * 27 | * @author Rado Buransky 28 | */ 29 | class Scala extends AbstractLanguage(Scala.key, Scala.name) { 30 | val getFileSuffixes = Array(Scala.fileExtension) 31 | } 32 | 33 | object Scala { 34 | val key = "scala" 35 | val name = "Scala" 36 | val fileExtension = "scala" 37 | } -------------------------------------------------------------------------------- /src/main/scala/com/buransky/plugins/scoverage/measure/ScalaMetrics.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scoverage Plugin 3 | * Copyright (C) 2013 Rado Buransky 4 | * dev@sonar.codehaus.org 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | */ 20 | package com.buransky.plugins.scoverage.measure 21 | 22 | import org.sonar.api.measures.{CoreMetrics, Metric, Metrics} 23 | import org.sonar.api.measures.Metric.ValueType 24 | import scala.collection.JavaConversions._ 25 | import scala.collection.mutable.ListBuffer 26 | 27 | /** 28 | * Statement coverage metric definition. 29 | * 30 | * @author Rado Buransky 31 | */ 32 | class ScalaMetrics extends Metrics { 33 | override def getMetrics = ListBuffer(ScalaMetrics.statementCoverage, ScalaMetrics.coveredStatements, ScalaMetrics.totalStatements).toList 34 | } 35 | 36 | object ScalaMetrics { 37 | private val STATEMENT_COVERAGE_KEY = "scoverage" 38 | private val COVERED_STATEMENTS_KEY = "covered_statements" 39 | private val TOTAL_STATEMENTS_KEY = "total_statements" 40 | 41 | lazy val statementCoverage = new Metric.Builder(STATEMENT_COVERAGE_KEY, 42 | "Statement coverage", ValueType.PERCENT) 43 | .setDescription("Statement coverage by tests") 44 | .setDirection(Metric.DIRECTION_BETTER) 45 | .setQualitative(true) 46 | .setDomain(CoreMetrics.DOMAIN_TESTS) 47 | .setWorstValue(0.0) 48 | .setBestValue(100.0) 49 | .create[java.lang.Double]() 50 | 51 | lazy val coveredStatements = new Metric.Builder(COVERED_STATEMENTS_KEY, 52 | "Covered statements", Metric.ValueType.INT) 53 | .setDescription("Number of statements covered by tests") 54 | .setDirection(Metric.DIRECTION_BETTER) 55 | .setQualitative(false) 56 | .setDomain(CoreMetrics.DOMAIN_SIZE) 57 | .create[java.lang.Integer]() 58 | 59 | lazy val totalStatements = new Metric.Builder(TOTAL_STATEMENTS_KEY, 60 | "Total statements", Metric.ValueType.INT) 61 | .setDescription("Number of all statements covered by tests and uncovered") 62 | .setDirection(Metric.DIRECTION_BETTER) 63 | .setQualitative(false) 64 | .setDomain(CoreMetrics.DOMAIN_SIZE) 65 | .create[java.lang.Integer]() 66 | } -------------------------------------------------------------------------------- /src/main/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcher.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scoverage Plugin 3 | * Copyright (C) 2013 Rado Buransky 4 | * dev@sonar.codehaus.org 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | */ 20 | package com.buransky.plugins.scoverage.pathcleaner 21 | 22 | import java.io.File 23 | import org.apache.commons.io.FileUtils 24 | import BruteForceSequenceMatcher._ 25 | import com.buransky.plugins.scoverage.util.PathUtil 26 | import scala.collection.JavaConversions._ 27 | import org.sonar.api.utils.log.Loggers 28 | 29 | object BruteForceSequenceMatcher { 30 | 31 | val extensions = Array[String]("java", "scala") 32 | 33 | type PathSeq = Seq[String] 34 | } 35 | 36 | /** 37 | * Helper that allows to convert a report path into a source folder relative path by testing it against 38 | * the tree of source files. 39 | * 40 | * Assumes that all report paths of a given report have a common root. Dependent of the scoverage 41 | * report this root is either something outside the actual project (absolute path), the base dir of the project 42 | * (report path relative to base dir) or some sub folder of the project. 43 | * 44 | * By reverse mapping a report path against the tree of all file children of the source folder the correct filesystem file 45 | * can be found and the report path can be converted into a source dir relative path. * 46 | * 47 | * @author Michael Zinsmaier 48 | */ 49 | class BruteForceSequenceMatcher(baseDir: File, sourcePath: String) extends PathSanitizer { 50 | 51 | private val sourceDir = initSourceDir() 52 | require(sourceDir.isAbsolute) 53 | require(sourceDir.isDirectory) 54 | 55 | private val log = Loggers.get(classOf[BruteForceSequenceMatcher]) 56 | private val sourcePathLength = PathUtil.splitPath(sourceDir.getAbsolutePath).size 57 | private val filesMap = initFilesMap() 58 | 59 | 60 | def getSourceRelativePath(reportPath: PathSeq): Option[PathSeq] = { 61 | // match with file system map of files 62 | val relPathOption = for { 63 | absPathCandidates <- filesMap.get(reportPath.last) 64 | path <- absPathCandidates.find(absPath => absPath.endsWith(reportPath)) 65 | } yield path.drop(sourcePathLength) 66 | 67 | relPathOption 68 | } 69 | 70 | // mock able helpers that allow us to remove the dependency to the real file system during tests 71 | 72 | private[pathcleaner] def initSourceDir(): File = { 73 | sourcePath.split(",").headOption.map { first => 74 | val firstFile = new File(first) 75 | if (firstFile.isAbsolute) { 76 | firstFile 77 | } else { 78 | val sourceDir = new File(baseDir, first) 79 | sourceDir 80 | } 81 | }.orNull 82 | } 83 | 84 | private[pathcleaner] def initFilesMap(): Map[String, Seq[PathSeq]] = { 85 | val srcFiles = FileUtils.iterateFiles(sourceDir, extensions, true) 86 | val paths = srcFiles.map(file => PathUtil.splitPath(file.getAbsolutePath)).toSeq 87 | 88 | // group them by filename, in case multiple files have the same name 89 | paths.groupBy(path => path.last) 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/scala/com/buransky/plugins/scoverage/pathcleaner/PathSanitizer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scoverage Plugin 3 | * Copyright (C) 2013 Rado Buransky 4 | * dev@sonar.codehaus.org 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | */ 20 | package com.buransky.plugins.scoverage.pathcleaner 21 | 22 | /** 23 | * @author Michael Zinsmaier 24 | */ 25 | trait PathSanitizer { 26 | 27 | /** tries to convert the given path such that it is relative to the 28 | * projects/modules source directory. 29 | * 30 | * @return Some(source folder relative path) or None if the path cannot be converted 31 | */ 32 | def getSourceRelativePath(path: Seq[String]): Option[Seq[String]] 33 | 34 | } -------------------------------------------------------------------------------- /src/main/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensor.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scoverage Plugin 3 | * Copyright (C) 2013 Rado Buransky 4 | * dev@sonar.codehaus.org 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | */ 20 | package com.buransky.plugins.scoverage.sensor 21 | 22 | import java.io.File 23 | 24 | import com.buransky.plugins.scoverage.language.Scala 25 | import com.buransky.plugins.scoverage.measure.ScalaMetrics 26 | import com.buransky.plugins.scoverage.pathcleaner.{BruteForceSequenceMatcher, PathSanitizer} 27 | import com.buransky.plugins.scoverage.util.LogUtil 28 | import com.buransky.plugins.scoverage.xml.XmlScoverageReportParser 29 | import com.buransky.plugins.scoverage.{CoveredStatement, DirectoryStatementCoverage, FileStatementCoverage, _} 30 | import org.sonar.api.batch.fs.{FileSystem, InputFile, InputPath} 31 | import org.sonar.api.batch.{CoverageExtension, Sensor, SensorContext} 32 | import org.sonar.api.config.Settings 33 | import org.sonar.api.measures.{CoverageMeasuresBuilder, Measure} 34 | import org.sonar.api.resources.{Project, Resource} 35 | import org.sonar.api.scan.filesystem.PathResolver 36 | import org.sonar.api.utils.log.Loggers 37 | 38 | import scala.collection.JavaConversions._ 39 | 40 | /** 41 | * Main sensor for importing Scoverage report to Sonar. 42 | * 43 | * @author Rado Buransky 44 | */ 45 | class ScoverageSensor(settings: Settings, pathResolver: PathResolver, fileSystem: FileSystem) 46 | extends Sensor with CoverageExtension { 47 | private val log = Loggers.get(classOf[ScoverageSensor]) 48 | protected val SCOVERAGE_REPORT_PATH_PROPERTY = "sonar.scoverage.reportPath" 49 | protected lazy val scoverageReportParser: ScoverageReportParser = XmlScoverageReportParser() 50 | 51 | override def shouldExecuteOnProject(project: Project): Boolean = fileSystem.languages().contains(Scala.key) 52 | 53 | override def analyse(project: Project, context: SensorContext) { 54 | scoverageReportPath match { 55 | case Some(reportPath) => 56 | // Single-module project 57 | val srcOption = Option(settings.getString("sonar.sources")) 58 | val sonarSources = srcOption match { 59 | case Some(src) => src 60 | case None => { 61 | log.warn(s"could not find settings key sonar.sources assuming src/main/scala.") 62 | "src/main/scala" 63 | } 64 | } 65 | val pathSanitizer = createPathSanitizer(sonarSources) 66 | processProject(scoverageReportParser.parse(reportPath, pathSanitizer), project, context, sonarSources) 67 | 68 | case None => 69 | // Multi-module project has report path set for each module individually 70 | analyseMultiModuleProject(project, context) 71 | } 72 | } 73 | 74 | override val toString = getClass.getSimpleName 75 | 76 | protected def createPathSanitizer(sonarSources: String): PathSanitizer 77 | = new BruteForceSequenceMatcher(fileSystem.baseDir(), sonarSources) 78 | 79 | private lazy val scoverageReportPath: Option[String] = { 80 | settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY) match { 81 | case null => None 82 | case path: String => 83 | pathResolver.relativeFile(fileSystem.baseDir, path) match { 84 | case report: java.io.File if !report.exists || !report.isFile => 85 | log.error(LogUtil.f("Report not found at {}"), report) 86 | None 87 | 88 | case report: java.io.File => Some(report.getAbsolutePath) 89 | } 90 | } 91 | } 92 | 93 | private def analyseMultiModuleProject(project: Project, context: SensorContext) { 94 | project.isModule match { 95 | case true => log.warn(LogUtil.f("Report path not set for " + project.name + " module! [" + 96 | project.name + "." + SCOVERAGE_REPORT_PATH_PROPERTY + "]")) 97 | case _ => 98 | // Compute overall statement coverage from submodules 99 | val totalStatementCount = project.getModules.map(analyseStatementCountForModule(_, context)).sum 100 | val coveredStatementCount = project.getModules.map(analyseCoveredStatementCountForModule(_, context)).sum 101 | 102 | if (totalStatementCount > 0) { 103 | // Convert to percentage 104 | val overall = (coveredStatementCount.toDouble / totalStatementCount.toDouble) * 100.0 105 | 106 | // Set overall statement coverage 107 | context.saveMeasure(project, createStatementCoverage(overall)) 108 | 109 | log.info(LogUtil.f("Overall statement coverage is " + ("%1.2f" format overall))) 110 | } 111 | } 112 | } 113 | 114 | private def analyseCoveredStatementCountForModule(module: Project, context: SensorContext): Long = { 115 | // Aggregate modules 116 | context.getMeasure(module, ScalaMetrics.coveredStatements) match { 117 | case null => 118 | log.debug(LogUtil.f("Module has no statement coverage. [" + module.name + "]")) 119 | 0 120 | case moduleCoveredStatementCount: Measure[_] => 121 | log.debug(LogUtil.f("Covered statement count for " + module.name + " module. [" + 122 | moduleCoveredStatementCount.getValue + "]")) 123 | 124 | moduleCoveredStatementCount.getValue.toLong 125 | } 126 | } 127 | 128 | private def analyseStatementCountForModule(module: Project, context: SensorContext): Long = { 129 | // Aggregate modules 130 | context.getMeasure(module, ScalaMetrics.totalStatements) match { 131 | case null => 132 | log.debug(LogUtil.f("Module has no number of statements. [" + module.name + "]")) 133 | 0 134 | 135 | case moduleStatementCount: Measure[_] => 136 | log.debug(LogUtil.f("Statement count for " + module.name + " module. [" + 137 | moduleStatementCount.getValue + "]")) 138 | 139 | moduleStatementCount.getValue.toLong 140 | } 141 | } 142 | 143 | private def processProject(projectCoverage: ProjectStatementCoverage, project: Project, context: SensorContext, sonarSources: String) { 144 | // Save measures 145 | saveMeasures(context, project, projectCoverage) 146 | 147 | log.info(LogUtil.f("Statement coverage for " + project.getKey + " is " + ("%1.2f" format projectCoverage.rate))) 148 | 149 | // Process children 150 | processChildren(projectCoverage.children, context, sonarSources) 151 | } 152 | 153 | private def processDirectory(directoryCoverage: DirectoryStatementCoverage, context: SensorContext, parentDirectory: String) { 154 | // save measures if any 155 | if (directoryCoverage.statementCount > 0) { 156 | val path = appendFilePath(parentDirectory, directoryCoverage.name) 157 | 158 | getResource(path, context, false) match { 159 | case Some(srcDir) => { 160 | // Save directory measures 161 | saveMeasures(context, srcDir, directoryCoverage) 162 | } 163 | case None => 164 | } 165 | } 166 | // Process children 167 | processChildren(directoryCoverage.children, context, appendFilePath(parentDirectory, directoryCoverage.name)) 168 | } 169 | 170 | private def processFile(fileCoverage: FileStatementCoverage, context: SensorContext, directory: String) { 171 | val path = appendFilePath(directory, fileCoverage.name) 172 | 173 | getResource(path, context, true) match { 174 | case Some(scalaSourceFile) => { 175 | // Save measures 176 | saveMeasures(context, scalaSourceFile, fileCoverage) 177 | // Save line coverage. This is needed just for source code highlighting. 178 | saveLineCoverage(fileCoverage.statements, scalaSourceFile, context) 179 | } 180 | case None => 181 | } 182 | } 183 | 184 | private def getResource(path: String, context: SensorContext, isFile: Boolean): Option[Resource] = { 185 | 186 | val inputOption: Option[InputPath] = if (isFile) { 187 | val p = fileSystem.predicates() 188 | val pathPredicate = if (new File(path).isAbsolute) p.hasAbsolutePath(path) else p.hasRelativePath(path) 189 | Option(fileSystem.inputFile(p.and( 190 | pathPredicate, 191 | p.hasLanguage(Scala.key), 192 | p.hasType(InputFile.Type.MAIN)))) 193 | } else { 194 | Option(fileSystem.inputDir(pathResolver.relativeFile(fileSystem.baseDir(), path))) 195 | } 196 | 197 | inputOption match { 198 | case Some(path: InputPath) => 199 | Some(context.getResource(path)) 200 | case None => { 201 | log.warn(s"File or directory not found in file system! ${path}") 202 | None 203 | } 204 | } 205 | } 206 | 207 | private def saveMeasures(context: SensorContext, resource: Resource, statementCoverage: StatementCoverage) { 208 | context.saveMeasure(resource, createStatementCoverage(statementCoverage.rate)) 209 | context.saveMeasure(resource, createStatementCount(statementCoverage.statementCount)) 210 | context.saveMeasure(resource, createCoveredStatementCount(statementCoverage.coveredStatementsCount)) 211 | 212 | log.debug(LogUtil.f("Save measures [" + statementCoverage.rate + ", " + statementCoverage.statementCount + 213 | ", " + statementCoverage.coveredStatementsCount + ", " + resource.getKey + "]")) 214 | } 215 | 216 | private def saveLineCoverage(coveredStatements: Iterable[CoveredStatement], resource: Resource, 217 | context: SensorContext) { 218 | // Convert statements to lines 219 | val coveredLines = StatementCoverage.statementCoverageToLineCoverage(coveredStatements) 220 | 221 | // Set line hits 222 | val coverage = CoverageMeasuresBuilder.create() 223 | coveredLines.foreach { coveredLine => 224 | coverage.setHits(coveredLine.line, coveredLine.hitCount) 225 | } 226 | 227 | // Save measures 228 | coverage.createMeasures().toList.foreach(context.saveMeasure(resource, _)) 229 | } 230 | 231 | private def processChildren(children: Iterable[StatementCoverage], context: SensorContext, directory: String) { 232 | children.foreach(processChild(_, context, directory)) 233 | } 234 | 235 | private def processChild(dirOrFile: StatementCoverage, context: SensorContext, directory: String) { 236 | dirOrFile match { 237 | case dir: DirectoryStatementCoverage => processDirectory(dir, context, directory) 238 | case file: FileStatementCoverage => processFile(file, context, directory) 239 | case _ => throw new IllegalStateException("Not a file or directory coverage! [" + 240 | dirOrFile.getClass.getName + "]") 241 | } 242 | } 243 | 244 | private def createStatementCoverage[T <: Serializable](rate: Double): Measure[T] = 245 | new Measure[T](ScalaMetrics.statementCoverage, rate) 246 | 247 | private def createStatementCount[T <: Serializable](statements: Int): Measure[T] = 248 | new Measure(ScalaMetrics.totalStatements, statements.toDouble, 0) 249 | 250 | private def createCoveredStatementCount[T <: Serializable](coveredStatements: Int): Measure[T] = 251 | new Measure(ScalaMetrics.coveredStatements, coveredStatements.toDouble, 0) 252 | 253 | private def appendFilePath(src: String, name: String) = { 254 | val result = src match { 255 | case java.io.File.separator => java.io.File.separator 256 | case empty if empty.isEmpty => "" 257 | case other => other + java.io.File.separator 258 | } 259 | 260 | result + name 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/main/scala/com/buransky/plugins/scoverage/util/LogUtil.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scoverage Plugin 3 | * Copyright (C) 2013 Rado Buransky 4 | * dev@sonar.codehaus.org 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | */ 20 | package com.buransky.plugins.scoverage.util 21 | 22 | /** 23 | * Logging helper. 24 | * 25 | * @author Rado Buransky 26 | */ 27 | object LogUtil { 28 | def f(msg: String) = "[scoverage] " + msg 29 | } 30 | -------------------------------------------------------------------------------- /src/main/scala/com/buransky/plugins/scoverage/util/PathUtil.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scoverage Plugin 3 | * Copyright (C) 2013 Rado Buransky 4 | * dev@sonar.codehaus.org 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | */ 20 | package com.buransky.plugins.scoverage.util 21 | 22 | import java.io.File 23 | import scala.Iterator 24 | /** 25 | * File path helper. 26 | * 27 | * @author Rado Buransky 28 | */ 29 | object PathUtil { 30 | 31 | def splitPath(filePath: String): List[String] = { 32 | new FileParentIterator(new File(filePath)).toList.reverse 33 | } 34 | 35 | class FileParentIterator(private var f: File) extends Iterator[String] { 36 | def hasNext: Boolean = f != null && !f.getName().isEmpty() 37 | def next(): String = { 38 | val name = f.getName() 39 | f = f.getParentFile 40 | name 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/scala/com/buransky/plugins/scoverage/widget/ScoverageWidget.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scoverage Plugin 3 | * Copyright (C) 2013 Rado Buransky 4 | * dev@sonar.codehaus.org 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | */ 20 | package com.buransky.plugins.scoverage.widget 21 | 22 | import org.sonar.api.web.{RubyRailsWidget, AbstractRubyTemplate} 23 | 24 | /** 25 | * UI widget that can be added to the main dashboard to display overall statement coverage for the project. 26 | * 27 | * @author Rado Buransky 28 | */ 29 | class ScoverageWidget extends AbstractRubyTemplate with RubyRailsWidget { 30 | val getId = "scoverage" 31 | val getTitle = "Statement coverage" 32 | override val getTemplatePath = "/com/buransky/plugins/scoverage/widget.html.erb" 33 | } 34 | -------------------------------------------------------------------------------- /src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParser.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scoverage Plugin 3 | * Copyright (C) 2013 Rado Buransky 4 | * dev@sonar.codehaus.org 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | */ 20 | package com.buransky.plugins.scoverage.xml 21 | 22 | import java.io.File 23 | 24 | import com.buransky.plugins.scoverage._ 25 | import com.buransky.plugins.scoverage.util.PathUtil 26 | import org.sonar.api.utils.log.Loggers 27 | 28 | import scala.annotation.tailrec 29 | import scala.collection.mutable 30 | import scala.io.Source 31 | import scala.xml.parsing.ConstructingParser 32 | import scala.xml.{MetaData, NamespaceBinding, Text} 33 | import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer 34 | 35 | /** 36 | * Scoverage XML parser based on ConstructingParser provided by standard Scala library. 37 | * 38 | * @author Rado Buransky 39 | */ 40 | class XmlScoverageReportConstructingParser(source: Source, pathSanitizer: PathSanitizer) extends ConstructingParser(source, false) { 41 | private val log = Loggers.get(classOf[XmlScoverageReportConstructingParser]) 42 | 43 | private val CLASS_ELEMENT = "class" 44 | private val FILENAME_ATTRIBUTE = "filename" 45 | private val STATEMENT_ELEMENT = "statement" 46 | private val START_ATTRIBUTE = "start" 47 | private val LINE_ATTRIBUTE = "line" 48 | private val INVOCATION_COUNT_ATTRIBUTE = "invocation-count" 49 | 50 | val statementsInFile: mutable.HashMap[String, List[CoveredStatement]] = mutable.HashMap.empty 51 | var currentFilePath: Option[String] = None 52 | 53 | def parse(): ProjectStatementCoverage = { 54 | // Initialize 55 | nextch() 56 | 57 | // Parse 58 | document() 59 | 60 | // Transform map to project 61 | projectFromMap(statementsInFile.toMap) 62 | } 63 | 64 | override def elemStart(pos: Int, pre: String, label: String, attrs: MetaData, scope: NamespaceBinding) { 65 | label match { 66 | case CLASS_ELEMENT => 67 | currentFilePath = Some(fixLeadingSlash(getText(attrs, FILENAME_ATTRIBUTE))) 68 | log.debug("Current file path: " + currentFilePath.get) 69 | 70 | case STATEMENT_ELEMENT => 71 | currentFilePath match { 72 | case Some(cfp) => 73 | val start = getInt(attrs, START_ATTRIBUTE) 74 | val line = getInt(attrs, LINE_ATTRIBUTE) 75 | val hits = getInt(attrs, INVOCATION_COUNT_ATTRIBUTE) 76 | 77 | // Add covered statement to the mutable map 78 | val pos = StatementPosition(line, start) 79 | addCoveredStatement(cfp, CoveredStatement(pos, pos, hits)) 80 | 81 | log.debug("Statement added: " + line + ", " + hits + ", " + start) 82 | 83 | case None => throw new ScoverageException("Current file path not set!") 84 | } 85 | case _ => // Nothing to do 86 | } 87 | 88 | super.elemStart(pos, pre, label, attrs, scope) 89 | } 90 | 91 | private def addCoveredStatement(filePath: String, coveredStatement: CoveredStatement) { 92 | statementsInFile.get(filePath) match { 93 | case None => statementsInFile.put(filePath, List(coveredStatement)) 94 | case Some(s) => statementsInFile.update(filePath, coveredStatement :: s) 95 | } 96 | } 97 | 98 | /** 99 | * Remove this when scoverage is fixed! It's just a hack. 100 | * Old Scoverage has incorrectly added leading '/' to relative file paths. 101 | */ 102 | private def fixLeadingSlash(filePath: String) = { 103 | if (filePath.startsWith(File.separator) && !new File(filePath).exists()) { 104 | filePath.drop(File.separator.length) 105 | } 106 | else 107 | filePath 108 | } 109 | 110 | private def getInt(attrs: MetaData, name: String) = getText(attrs, name).toInt 111 | 112 | private def getText(attrs: MetaData, name: String): String = { 113 | attrs.get(name) match { 114 | case Some(attr) => 115 | attr match { 116 | case text: Text => text.toString() 117 | case _ => throw new ScoverageException("Not a text attribute!") 118 | } 119 | case None => throw new ScoverageException("Attribute doesn't exit! [" + name + "]") 120 | } 121 | } 122 | 123 | private case class DirOrFile(name: String, var children: List[DirOrFile], 124 | coverage: Option[FileStatementCoverage]) { 125 | def get(name: String) = children.find(_.name == name) 126 | 127 | @tailrec 128 | final def add(chain: DirOrFile) { 129 | get(chain.name) match { 130 | case None => children = chain :: children 131 | case Some(child) => 132 | chain.children match { 133 | case h :: t => 134 | if (t != Nil) 135 | throw new IllegalStateException("This is not a linear chain!") 136 | child.add(h) 137 | case _ => // Duplicate file? Should not happen. 138 | } 139 | } 140 | } 141 | 142 | def toStatementCoverage: NodeStatementCoverage = { 143 | val childNodes = children.map(_.toStatementCoverage) 144 | 145 | childNodes match { 146 | case Nil => coverage.get 147 | case _ => DirectoryStatementCoverage(name, childNodes) 148 | } 149 | } 150 | 151 | def toProjectStatementCoverage: ProjectStatementCoverage = { 152 | toStatementCoverage match { 153 | case node: NodeStatementCoverage => ProjectStatementCoverage("", node.children) 154 | case _ => throw new ScoverageException("Illegal statement coverage!") 155 | } 156 | } 157 | } 158 | 159 | private def projectFromMap(statementsInFile: Map[String, List[CoveredStatement]]): 160 | ProjectStatementCoverage = { 161 | 162 | // Transform to file statement coverage 163 | val files = fileStatementCoverage(statementsInFile) 164 | 165 | // Transform file paths to chain of case classes 166 | val chained = files.map(fsc => pathToChain(fsc._1, fsc._2)).flatten 167 | 168 | // Merge chains into one tree 169 | val root = DirOrFile("", Nil, None) 170 | chained.foreach(root.add) 171 | 172 | // Transform file system tree into coverage structure tree 173 | root.toProjectStatementCoverage 174 | } 175 | 176 | private def pathToChain(filePath: String, coverage: FileStatementCoverage): Option[DirOrFile] = { 177 | // helper 178 | def convertToDirOrFile(relPath: Seq[String]) = { 179 | // Get directories 180 | val dirs = for (i <- 0 to relPath.length - 2) 181 | yield DirOrFile(relPath(i), Nil, None) 182 | 183 | // Chain directories 184 | for (i <- 0 to dirs.length - 2) 185 | dirs(i).children = List(dirs(i + 1)) 186 | 187 | // Get file 188 | val file = DirOrFile(relPath(relPath.length - 1).toString, Nil, Some(coverage)) 189 | 190 | if (dirs.isEmpty) { 191 | // File in root dir 192 | file 193 | } else { 194 | // Append file 195 | dirs.last.children = List(file) 196 | dirs.head 197 | } 198 | } 199 | 200 | // processing 201 | val path = PathUtil.splitPath(filePath) 202 | 203 | if (path.length < 1) 204 | throw new ScoverageException("Path cannot be empty!") 205 | 206 | pathSanitizer.getSourceRelativePath(path) match { 207 | case Some(relPath) => Some(convertToDirOrFile(relPath)) 208 | case None => { 209 | log.warn(s"skipping file coverage results for $path, was not able to retrieve the file in the configured source dir") 210 | None 211 | } 212 | } 213 | } 214 | 215 | private def fileStatementCoverage(statementsInFile: Map[String, List[CoveredStatement]]): 216 | Map[String, FileStatementCoverage] = { 217 | statementsInFile.map { sif => 218 | val fileStatementCoverage = FileStatementCoverage(PathUtil.splitPath(sif._1).last, 219 | sif._2.length, coveredStatements(sif._2), sif._2) 220 | 221 | (sif._1, fileStatementCoverage) 222 | } 223 | } 224 | 225 | private def coveredStatements(statements: Iterable[CoveredStatement]) = 226 | statements.count(_.hitCount > 0) 227 | } -------------------------------------------------------------------------------- /src/main/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParser.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scoverage Plugin 3 | * Copyright (C) 2013 Rado Buransky 4 | * dev@sonar.codehaus.org 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | */ 20 | package com.buransky.plugins.scoverage.xml 21 | 22 | import com.buransky.plugins.scoverage.util.LogUtil 23 | import com.buransky.plugins.scoverage.{ProjectStatementCoverage, ScoverageException, ScoverageReportParser} 24 | import org.sonar.api.utils.log.Loggers 25 | 26 | import scala.io.Source 27 | import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer 28 | 29 | /** 30 | * Bridge between parser implementation and coverage provider. 31 | * 32 | * @author Rado Buransky 33 | */ 34 | class XmlScoverageReportParser extends ScoverageReportParser { 35 | private val log = Loggers.get(classOf[XmlScoverageReportParser]) 36 | 37 | def parse(reportFilePath: String, pathSanitizer: PathSanitizer): ProjectStatementCoverage = { 38 | require(reportFilePath != null) 39 | require(!reportFilePath.trim.isEmpty) 40 | 41 | log.debug(LogUtil.f("Reading report. [" + reportFilePath + "]")) 42 | 43 | val parser = new XmlScoverageReportConstructingParser(sourceFromFile(reportFilePath), pathSanitizer) 44 | parser.parse() 45 | } 46 | 47 | private def sourceFromFile(scoverageReportPath: String) = { 48 | try { 49 | Source.fromFile(scoverageReportPath) 50 | } 51 | catch { 52 | case ex: Exception => throw ScoverageException("Cannot parse file! [" + scoverageReportPath + "]", ex) 53 | } 54 | } 55 | } 56 | 57 | object XmlScoverageReportParser { 58 | def apply() = new XmlScoverageReportParser 59 | } -------------------------------------------------------------------------------- /src/main/scala/com/ncredinburgh/sonar/scalastyle/Constants.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scalastyle Plugin 3 | * Copyright (C) 2014 All contributors 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 3 of the License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 18 | */ 19 | package com.ncredinburgh.sonar.scalastyle 20 | 21 | object Constants { 22 | val ScalaKey = "scala" 23 | val RepositoryKey = "Scalastyle" 24 | val RepositoryName = "Scalastyle Rules" 25 | val ProfileName = "Scalastyle" 26 | 27 | /** the class of the checker that should be executed by the sonar rule */ 28 | val ClazzParam = "scalastyle-checker" 29 | } 30 | -------------------------------------------------------------------------------- /src/main/scala/com/ncredinburgh/sonar/scalastyle/RepositoryRule.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scalastyle Plugin 3 | * Copyright (C) 2014 All contributors 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 3 of the License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 18 | */ 19 | package com.ncredinburgh.sonar.scalastyle 20 | 21 | import org.sonar.api.server.rule.RuleParamType 22 | 23 | case class RepositoryRule(clazz : String, id : String, description : String, params : List[Param]) 24 | 25 | case class Param(name: String, `type`: RuleParamType, desc: String, defaultVal: String) 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleQualityProfile.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scalastyle Plugin 3 | * Copyright (C) 2014 All contributors 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 3 of the License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 18 | */ 19 | package com.ncredinburgh.sonar.scalastyle 20 | 21 | import org.slf4j.LoggerFactory 22 | import org.sonar.api.profiles.{ProfileDefinition, RulesProfile} 23 | import org.sonar.api.rules.{RuleFinder, ActiveRule} 24 | import org.sonar.api.utils.ValidationMessages 25 | import org.scalastyle.ScalastyleError 26 | import scala.xml.XML 27 | import collection.JavaConversions._ 28 | import org.sonar.api.rules.RuleQuery 29 | import org.sonar.api.rules.Rule 30 | 31 | /** 32 | * This class creates the default "Scalastyle" quality profile from Scalastyle's default_config.xml 33 | */ 34 | class ScalastyleQualityProfile(ruleFinder: RuleFinder) extends ProfileDefinition { 35 | 36 | private val log = LoggerFactory.getLogger(classOf[ScalastyleQualityProfile]) 37 | private val defaultConfigRules = xmlFromClassPath("/default_config.xml") \\ "scalastyle" \ "check" 38 | 39 | override def createProfile(validation: ValidationMessages): RulesProfile = { 40 | val profile = RulesProfile.create(Constants.ProfileName, Constants.ScalaKey) 41 | val enabledRules = defaultConfigRules filter (x => (x \ "@enabled").text.equals("true")) 42 | val defaultRuleClasses = enabledRules map (x => (x \ "@class").text) 43 | 44 | // currently findAll is buggy (sonar 4.5-5.1 https://jira.sonarsource.com/browse/SONAR-6390) 45 | // will still work but won't add all possible rule to the default profile 46 | val query = RuleQuery.create().withRepositoryKey(Constants.RepositoryKey) 47 | val repoRules = ruleFinder.findAll(query) 48 | 49 | for {clazz <- defaultRuleClasses} { 50 | val ruleOption = repoRules.find(clazzMatch(_, clazz)) 51 | 52 | ruleOption match { 53 | case None => validation.addWarningText(s"Rule for $clazz not found in ${Constants.RepositoryKey} repository! Rule won't be activated.") 54 | case Some(rule) => { 55 | if (!rule.isTemplate()) { 56 | val activated = profile.activateRule(rule, rule.getSeverity) 57 | setParameters(activated, clazz) 58 | } 59 | } 60 | } 61 | } 62 | 63 | profile 64 | } 65 | 66 | def setParameters(activeRule: ActiveRule, clazz: String) { 67 | // set parameters 68 | defaultConfigRules.find(x => (x \ "@class").text.equals(clazz)) match { 69 | case Some(rule) => { 70 | val params = (rule \ "parameters" \ "parameter").map(n => ((n \ "@name").text, n.text)).toMap 71 | params foreach { case (key, value) => activeRule.setParameter(key, value) } 72 | } 73 | case _ => log.warn("Default rule with key " + activeRule.getRuleKey + " could not found in default_config.xml") 74 | } 75 | 76 | // set synthetic parameter 77 | activeRule.setParameter(Constants.ClazzParam, clazz) 78 | } 79 | 80 | private def clazzMatch(rule: Rule, clazz: String): Boolean = { 81 | Option(rule.getParam(Constants.ClazzParam)) match { 82 | case Some(param) => { 83 | param.getDefaultValue.equals(clazz) 84 | } 85 | case None => { 86 | log.warn(s"Could not find required parameter ${Constants.ClazzParam}, rule for $clazz cannot be activated") 87 | false 88 | } 89 | } 90 | } 91 | 92 | private def xmlFromClassPath(s: String) = XML.load(classOf[ScalastyleError].getResourceAsStream(s)) 93 | } 94 | -------------------------------------------------------------------------------- /src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleRepository.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scalastyle Plugin 3 | * Copyright (C) 2014 All contributors 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 3 of the License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 18 | */ 19 | package com.ncredinburgh.sonar.scalastyle 20 | 21 | import org.sonar.api.rule.Severity 22 | import org.sonar.api.server.rule.RulesDefinition 23 | import org.sonar.api.server.rule.RuleParamType 24 | import org.slf4j.LoggerFactory 25 | import org.sonar.api.server.rule.RulesDefinition.NewRepository 26 | import com.ncredinburgh.sonar.scalastyle.ScalastyleRepository.getStandardKey 27 | import scala.annotation.tailrec 28 | 29 | object ScalastyleRepository { 30 | 31 | def getStandardKey(clazz: String) = { 32 | val simpleClazz = clazz.reverse.takeWhile(_ != '.').reverse 33 | s"scalastyle_${simpleClazz}" 34 | } 35 | } 36 | 37 | /** 38 | * Scalastyle rules repository - creates a rule for each checker shipped with Scalastyle based 39 | * on the scalastyle_definition.xml file that ships with the Scalastyle jar. 40 | */ 41 | class ScalastyleRepository extends RulesDefinition { 42 | 43 | override def define(context: RulesDefinition.Context): Unit = { 44 | val repository = context 45 | .createRepository(Constants.RepositoryKey, Constants.ScalaKey) 46 | .setName(Constants.RepositoryName) 47 | 48 | ScalastyleResources.allDefinedRules foreach { 49 | repoRule => 50 | { 51 | val ruleKey = determineFreeRuleKey(repoRule.clazz, repository) 52 | 53 | // define the rule 54 | val rule = repository.createRule(ruleKey) 55 | rule.setName(ScalastyleResources.label(repoRule.id)) 56 | rule.setHtmlDescription(repoRule.description) 57 | 58 | // currently all rules comes with "warning" default level so we can treat with major severity 59 | rule.setSeverity(Severity.MAJOR) 60 | 61 | // add parameters 62 | repoRule.params foreach { 63 | param => 64 | { 65 | rule 66 | .createParam(param.name) 67 | .setDefaultValue(param.defaultVal) 68 | .setType(param.`type`) 69 | .setDescription(param.desc) 70 | } 71 | } 72 | 73 | // add synthetic parameter as reference to the class 74 | rule.createParam(Constants.ClazzParam) 75 | .setDefaultValue(repoRule.clazz) 76 | .setType(RuleParamType.STRING) 77 | .setDescription("Scalastyle checker that validates the rule.") 78 | 79 | // if a rule has at least one real parameter make it a template 80 | rule.setTemplate(repoRule.params.size > 0) 81 | 82 | } 83 | } 84 | 85 | repository.done() 86 | } 87 | 88 | /** 89 | * determines a free rule key in the repo, in case the key scalastyle- is already 90 | * in use the name scalastyle__ is tried i = 1, 2, .... 91 | */ 92 | private def determineFreeRuleKey(clazz: String, repo: NewRepository): String = { 93 | @tailrec 94 | def getFreeRuleKey(key: String, count: Int, repo: NewRepository): String = { 95 | val testee = if (count == 0) key else "$key_$count" 96 | if (repo.rule(testee) == null) { 97 | testee 98 | } else { 99 | getFreeRuleKey(key, (count + 1), repo) 100 | } 101 | } 102 | 103 | getFreeRuleKey(getStandardKey(clazz), 0, repo) 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleResources.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scalastyle Plugin 3 | * Copyright (C) 2014 All contributors 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 3 of the License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 18 | */ 19 | package com.ncredinburgh.sonar.scalastyle 20 | 21 | import java.io.InputStream 22 | import com.typesafe.config.ConfigFactory 23 | import org.scalastyle.ScalastyleError 24 | import org.sonar.api.server.rule.RuleParamType 25 | import scala.io.Source 26 | import scala.xml.{Elem, XML, Node} 27 | 28 | /** 29 | * Provides access to the various .property and XML files that Scalastyle provides 30 | * to describe its checkers. 31 | */ 32 | object ScalastyleResources { 33 | 34 | // accessing scalastyles definition and documentation.xml files 35 | private val definitions = xmlFromClassPath("/scalastyle_definition.xml") 36 | private val documentation = xmlFromClassPath("/scalastyle_documentation.xml") 37 | 38 | // accessing scalastyles reference.conf (includes additional data such as key.label) 39 | private val cfg = ConfigFactory.load(this.getClass.getClassLoader) 40 | 41 | def allDefinedRules: Seq[RepositoryRule] = for { 42 | checker <- definitions \\ "checker" 43 | clazz = (checker \ "@class").text 44 | id = (checker \ "@id").text 45 | desc = description(id) 46 | params = nodeToParams(checker, id) 47 | } yield RepositoryRule(clazz, id, desc, params) 48 | 49 | def nodeToParams(checker: Node, id: String): List[Param] = for { 50 | parameter <- (checker \\ "parameter").toList 51 | ruleParamKey = nodeToRuleParamKey(parameter) 52 | ruleParamType = nodeToRuleParamType(parameter) 53 | description = nodeToPropertyDescription(parameter, id) 54 | defaultValue = nodeToDefaultValue(parameter) 55 | } yield Param(ruleParamKey, ruleParamType, description, defaultValue) 56 | 57 | def description(key: String): String = descriptionFromDocumentation(key) getOrElse cfg.getConfig(key).getString("description") 58 | 59 | def label(key: String): String = cfg.getConfig(key).getString("label") 60 | 61 | private def descriptionFromDocumentation(key: String): Option[String] = { 62 | documentation \\ "scalastyle-documentation" \ "check" find { _ \\ "@id" exists (_.text == key) } match { 63 | case Some(node) => 64 | val justification = { 65 | val text = (node \ "justification").text 66 | if (text.trim != "") Some(ScalastyleDocFormatter.format(text)) else None 67 | } 68 | val extraDescription = { 69 | val text = (node \ "extra-description").text 70 | if (text.trim != "") Some(ScalastyleDocFormatter.format(text)) else None 71 | } 72 | (justification, extraDescription) match { 73 | case (Some(j), Some(ed)) => Some(s"$j\n$ed") 74 | case (Some(j), None) => Some(j) 75 | case _ => None 76 | } 77 | case None => None 78 | } 79 | } 80 | 81 | private def nodeToRuleParamKey(n: Node): String = (n \ "@name").text.trim 82 | 83 | private def nodeToRuleParamType(n: Node): RuleParamType = (n \ "@type").text.trim match { 84 | case "string" => if ((n \ "@name").text == "regex") { 85 | RuleParamType.STRING 86 | } else if ((n \ "@name").text == "header") { 87 | RuleParamType.TEXT 88 | } else { 89 | RuleParamType.STRING 90 | } 91 | case "integer" => RuleParamType.INTEGER 92 | case "boolean" => RuleParamType.BOOLEAN 93 | case _ => RuleParamType.STRING 94 | } 95 | 96 | private def nodeToPropertyDescription(node: Node, id: String): String = { 97 | val key = nodeToRuleParamKey(node) 98 | description(s"$id.$key") 99 | } 100 | 101 | private def nodeToDefaultValue(n: Node): String = (n \ "@default").text.trim 102 | 103 | private def xmlFromClassPath(s: String): Elem = XML.load(fromClassPath(s)) 104 | 105 | private def fromClassPath(s: String): InputStream = classOf[ScalastyleError].getResourceAsStream(s) 106 | } 107 | 108 | object ScalastyleDocFormatter { 109 | 110 | private case class Out(pre: Boolean, appended: Boolean, text: String) 111 | private case class LineWithLeadingSpaces(spaceCount: Int, empty: Boolean, line: String) 112 | private case class DocLine(pre: Boolean, empty: Boolean, line: String) 113 | 114 | private def empty(line: String) = line.trim == "" 115 | private def countLeadingSpaces(line: String) = { 116 | val count = line.takeWhile(_ == ' ').length 117 | LineWithLeadingSpaces(count, empty(line), line) 118 | } 119 | private val margin = 2 120 | 121 | def format(in: String): String = { 122 | val linesWithLeadingSpaces = Source.fromString(in).getLines().map(countLeadingSpaces).toList 123 | val docLines = linesWithLeadingSpaces.map(l => DocLine(l.spaceCount > margin, l.empty, l.line)) 124 | 125 | docLines.foldLeft(Out(pre = false, appended = false, "")) { 126 | case (out @ Out(false, false, text), line) => 127 | if (line.empty) out 128 | else if (line.pre) Out(pre = true, appended = true, text + s"

${line.line}\n")
129 |         else Out(pre = false, appended = true, text + s"

${line.line.trim}\n") 130 | 131 | case (out @ Out(false, true, text), line) => 132 | if (line.empty) out.copy(appended = false, text = text.trim + "

\n") 133 | else if (line.pre) Out(pre = true, appended = true, text + s"

\n

${line.line}\n")
134 |         else Out(pre = false, appended = true, text + s"${line.line.trim}\n")
135 | 
136 |       case (out @ Out(true, false, text), line) =>
137 |         if (line.empty) out.copy(text = text + "\n")
138 |         else if (line.pre) Out(pre = true, appended = true, text + s"${line.line}\n")
139 |         else Out(pre = false, appended = true, text.trim + s"

\n

${line.line.trim}\n") 140 | 141 | case (out @ Out(true, true, text), line) => 142 | if (line.empty) out.copy(appended = false, text = text + "\n") 143 | else if (line.pre) Out(pre = true, appended = true, text + s"${line.line}\n") 144 | else Out(pre = false, appended = true, text + s"

\n

${line.line.trim}\n") 145 | 146 | } match { 147 | case Out(true, _, text) => 148 | text.trim + "

" 149 | case Out(false, true, text) => 150 | text.trim + "

" 151 | case Out(false, false, text) => 152 | text.trim 153 | } 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleRunner.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scalastyle Plugin 3 | * Copyright (C) 2014 All contributors 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 3 of the License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 18 | */ 19 | 20 | package com.ncredinburgh.sonar.scalastyle 21 | 22 | import java.io.File 23 | 24 | import org.scalastyle.StyleError 25 | import org.scalastyle.StyleException 26 | import org.scalastyle.Message 27 | import org.scalastyle.FileSpec 28 | import org.scalastyle.Directory 29 | import org.scalastyle.ScalastyleChecker 30 | import org.scalastyle.ErrorLevel 31 | import org.scalastyle.ScalastyleConfiguration 32 | import org.scalastyle.ConfigurationChecker 33 | import org.slf4j.LoggerFactory 34 | import org.sonar.api.profiles.RulesProfile 35 | import org.sonar.api.rules.ActiveRule 36 | import scala.collection.JavaConversions._ 37 | 38 | /** 39 | * Runs Scalastyle based on active rules in the given RulesProfile 40 | */ 41 | class ScalastyleRunner(rp: RulesProfile) { 42 | private val log = LoggerFactory.getLogger(classOf[ScalastyleRunner]) 43 | 44 | def run(encoding: String, files: java.util.List[File]): List[Message[FileSpec]] = { 45 | log.debug("Using config " + config) 46 | 47 | val fileSpecs = Directory.getFilesAsJava(Some(encoding), files) 48 | val messages = new ScalastyleChecker[FileSpec]().checkFiles(config, fileSpecs) 49 | 50 | // only errors and exceptions are of interest 51 | messages.collect { _ match { 52 | case e: StyleError[_] => e 53 | case ex: StyleException[_] => ex 54 | }} 55 | 56 | } 57 | 58 | def config: ScalastyleConfiguration = { 59 | val sonarRules = rp.getActiveRulesByRepository(Constants.RepositoryKey) 60 | val checkers = sonarRules.map(ruleToChecker).toList 61 | new ScalastyleConfiguration("sonar", true, checkers) 62 | } 63 | 64 | private def ruleToChecker(activeRule: ActiveRule): ConfigurationChecker = { 65 | val sonarParams = activeRule.getActiveRuleParams.map(p => (p.getKey, p.getValue)).toMap 66 | 67 | val checkerParams = sonarParams.filterNot(keyVal => keyVal._1 == Constants.ClazzParam) 68 | val className = sonarParams(Constants.ClazzParam) 69 | val sonarKey = activeRule.getRuleKey 70 | 71 | ConfigurationChecker(className, ErrorLevel, true, sonarParams, None, Some(sonarKey)) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleSensor.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scalastyle Plugin 3 | * Copyright (C) 2014 All contributors 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 3 of the License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 18 | */ 19 | package com.ncredinburgh.sonar.scalastyle 20 | 21 | import org.scalastyle._ 22 | import org.slf4j.LoggerFactory 23 | import org.sonar.api.batch.fs.{InputFile, FileSystem} 24 | import org.sonar.api.batch.{Sensor, SensorContext} 25 | import org.sonar.api.component.ResourcePerspectives 26 | import org.sonar.api.issue.{Issuable, Issue} 27 | import org.sonar.api.profiles.RulesProfile 28 | import org.sonar.api.resources.Project 29 | import org.sonar.api.rule.RuleKey 30 | import org.sonar.api.rules.{Rule, RuleFinder, RuleQuery} 31 | 32 | 33 | import scala.collection.JavaConversions._ 34 | 35 | 36 | /** 37 | * Main sensor for return Scalastyle issues to Sonar. 38 | */ 39 | class ScalastyleSensor(resourcePerspectives: ResourcePerspectives, 40 | runner: ScalastyleRunner, 41 | fileSystem: FileSystem, 42 | ruleFinder: RuleFinder) 43 | extends Sensor { 44 | 45 | def this(resourcePerspectives: ResourcePerspectives, 46 | rulesProfile: RulesProfile, 47 | fileSystem: FileSystem, 48 | ruleFinder: RuleFinder) = this(resourcePerspectives, new ScalastyleRunner(rulesProfile), fileSystem, ruleFinder) 49 | 50 | 51 | private val log = LoggerFactory.getLogger(classOf[ScalastyleSensor]) 52 | 53 | private def predicates = fileSystem.predicates() 54 | 55 | private def scalaFilesPredicate = predicates.and(predicates.hasType(InputFile.Type.MAIN), predicates.hasLanguage(Constants.ScalaKey)) 56 | 57 | 58 | override def shouldExecuteOnProject(project: Project): Boolean = { 59 | fileSystem.files(scalaFilesPredicate).nonEmpty 60 | } 61 | 62 | override def analyse(project: Project, context: SensorContext): Unit = { 63 | val files = fileSystem.files(scalaFilesPredicate) 64 | val encoding = fileSystem.encoding.name 65 | val messages = runner.run(encoding, files.toList) 66 | 67 | messages foreach (processMessage(_)) 68 | } 69 | 70 | private def processMessage(message: Message[FileSpec]): Unit = message match { 71 | case error: StyleError[FileSpec] => processError(error) 72 | case exception: StyleException[FileSpec] => processException(exception) 73 | case _ => Unit 74 | } 75 | 76 | private def processError(error: StyleError[FileSpec]): Unit = { 77 | log.debug("Error message for rule " + error.clazz.getName) 78 | 79 | val inputFile = fileSystem.inputFile(predicates.hasPath(error.fileSpec.name)) 80 | val issuable = Option(resourcePerspectives.as(classOf[Issuable], inputFile)) 81 | val rule = findSonarRuleForError(error) 82 | 83 | log.debug("Matched to sonar rule " + rule) 84 | 85 | if (issuable.isDefined) { 86 | addIssue(issuable.get, error, rule) 87 | } else { 88 | log.error("issuable is null, cannot add issue") 89 | } 90 | } 91 | 92 | private def addIssue(issuable: Issuable, error: StyleError[FileSpec], rule: Rule): Unit = { 93 | val lineNum = sanitiseLineNum(error.lineNumber) 94 | val messageStr = error.customMessage getOrElse rule.getName 95 | 96 | val issue: Issue = issuable.newIssueBuilder.ruleKey(rule.ruleKey) 97 | .line(lineNum).message(messageStr).build 98 | issuable.addIssue(issue) 99 | } 100 | 101 | private def findSonarRuleForError(error: StyleError[FileSpec]): Rule = { 102 | val key = Constants.RepositoryKey 103 | val errorKey = error.key // == scalastyle ConfigurationChecker.customId 104 | log.debug("Looking for sonar rule for " + errorKey) 105 | ruleFinder.find(RuleQuery.create.withKey(errorKey).withRepositoryKey(key)) 106 | } 107 | 108 | private def processException(exception: StyleException[FileSpec]): Unit = { 109 | log.error("Got exception message from Scalastyle. " + 110 | "Check you have valid parameters configured for all rules. Exception message was: " + exception.message) 111 | } 112 | 113 | // sonar claims to accept null or a non zero lines, however if it is passed 114 | // null it blows up at runtime complaining it was passed 0 115 | private def sanitiseLineNum(maybeLine: Option[Int]) = if ((maybeLine getOrElse 0) != 0) { 116 | maybeLine.get 117 | } else { 118 | 1 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/scala/com/sagacify/sonar/scala/Measures.scala: -------------------------------------------------------------------------------- 1 | package com.sagacify.sonar.scala 2 | 3 | import scala.annotation.tailrec 4 | 5 | import scalariform.lexer.ScalaLexer 6 | import scalariform.lexer.Token 7 | import scalariform.lexer.Tokens.LINE_COMMENT 8 | import scalariform.lexer.Tokens.MULTILINE_COMMENT 9 | import scalariform.lexer.Tokens.XML_COMMENT 10 | import scalariform.lexer.Tokens.WS 11 | import scalariform.lexer.Tokens.EOF 12 | 13 | object Measures { 14 | 15 | /* applied on raw source code */ 16 | 17 | /* applied on lines of code */ 18 | 19 | /* applied on tokenised code */ 20 | 21 | @tailrec 22 | final def count_comment_lines(tokens: List[Token], i: Int = 0): Int = { 23 | tokens match { 24 | case Nil => i 25 | case token :: tail if token.tokenType.isComment => { 26 | token.tokenType match { 27 | case LINE_COMMENT => 28 | count_comment_lines(tail, i + 1) 29 | case MULTILINE_COMMENT => 30 | count_comment_lines(tail, i + token.rawText.count(_ == '\n') + 1) 31 | case XML_COMMENT => 32 | new scala.NotImplementedError("XML ?!"); i 33 | } 34 | } 35 | case _ :: tail => count_comment_lines(tail, i) 36 | } 37 | } 38 | 39 | @tailrec 40 | final def count_ncloc(tokens: List[Token], i: Int = 0): Int = { 41 | 42 | @tailrec 43 | def get_next_line(tokens: List[Token]): List[Token] = { 44 | tokens match { 45 | case Nil => Nil 46 | case token :: tail if token.tokenType == WS && 47 | token.text.contains('\n') => tail 48 | case token :: tail if token.tokenType == LINE_COMMENT => tail 49 | case token :: tail => get_next_line(tail) 50 | } 51 | } 52 | 53 | tokens match { 54 | case Nil => i 55 | case token :: tail if token.tokenType == WS => count_ncloc(tail, i) 56 | case token :: tail if token.tokenType == EOF => i 57 | case token :: tail => 58 | if( !token.tokenType.isNewline & !token.tokenType.isComment) { 59 | count_ncloc(get_next_line(tail), i + 1) 60 | } else { 61 | count_ncloc(tail, i) 62 | } 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/scala/com/sagacify/sonar/scala/ScalaPlugin.scala: -------------------------------------------------------------------------------- 1 | package com.sagacify.sonar.scala 2 | 3 | import scala.collection.JavaConversions._ 4 | import scala.collection.mutable.ListBuffer 5 | 6 | import com.buransky.plugins.scoverage.measure.ScalaMetrics 7 | import com.buransky.plugins.scoverage.sensor.ScoverageSensor 8 | import com.buransky.plugins.scoverage.widget.ScoverageWidget 9 | import com.ncredinburgh.sonar.scalastyle.ScalastyleQualityProfile 10 | import com.ncredinburgh.sonar.scalastyle.ScalastyleRepository 11 | import com.ncredinburgh.sonar.scalastyle.ScalastyleSensor 12 | import org.sonar.api.config.Settings 13 | import org.sonar.api.Extension 14 | import org.sonar.api.resources.AbstractLanguage 15 | import org.sonar.api.SonarPlugin 16 | import scalariform.lexer.ScalaLexer 17 | import scalariform.lexer.Token 18 | 19 | /** 20 | * Defines Scala as a language for SonarQube. 21 | */ 22 | class Scala(s: Settings) extends AbstractLanguage("scala", "Scala") { 23 | 24 | override def getFileSuffixes: Array[String] = Array("scala") 25 | 26 | } 27 | 28 | object Scala { 29 | 30 | def tokenize(sourceCode: String, scalaVersion: String): List[Token] = 31 | ScalaLexer.createRawLexer(sourceCode, false, scalaVersion).toList 32 | 33 | } 34 | 35 | /** 36 | * Plugin entry point. 37 | */ 38 | class ScalaPlugin extends SonarPlugin { 39 | 40 | override def getExtensions: java.util.List[Class[_]] = 41 | ListBuffer[Class[_]] ( 42 | classOf[Scala], 43 | classOf[ScalaSensor], 44 | classOf[ScalastyleRepository], 45 | classOf[ScalastyleQualityProfile], 46 | classOf[ScalastyleSensor], 47 | classOf[ScalaMetrics], 48 | classOf[ScoverageSensor], 49 | classOf[ScoverageWidget] 50 | ) 51 | 52 | override val toString = getClass.getSimpleName 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/scala/com/sagacify/sonar/scala/ScalaSensor.scala: -------------------------------------------------------------------------------- 1 | package com.sagacify.sonar.scala 2 | 3 | import scala.io.Source 4 | import scala.collection.JavaConversions._ 5 | 6 | import org.sonar.api.batch.fs.FileSystem 7 | import org.sonar.api.batch.Sensor 8 | import org.sonar.api.batch.SensorContext 9 | import org.sonar.api.measures.{CoreMetrics => CM} 10 | import org.sonar.api.resources.Project 11 | 12 | 13 | class ScalaSensor(scala: Scala, fs: FileSystem) extends Sensor { 14 | 15 | def shouldExecuteOnProject(project: Project): Boolean = { 16 | return fs.hasFiles(fs.predicates().hasLanguage(scala.getKey())); 17 | } 18 | 19 | def analyse(project: Project, context: SensorContext): Unit = { 20 | 21 | val charset = fs.encoding().toString() 22 | val version = "2.11.8" 23 | 24 | val inputFiles = fs.inputFiles(fs.predicates().hasLanguage(scala.getKey())) 25 | 26 | inputFiles.foreach{ inputFile => 27 | context.saveMeasure(inputFile, CM.FILES, 1.0); 28 | 29 | val sourceCode = Source.fromFile(inputFile.file, charset).mkString 30 | val tokens = Scala.tokenize(sourceCode, version) 31 | 32 | context.saveMeasure(inputFile, 33 | CM.COMMENT_LINES, 34 | Measures.count_comment_lines(tokens)) 35 | context.saveMeasure(inputFile, 36 | CM.NCLOC, 37 | Measures.count_ncloc(tokens)) 38 | 39 | // context.saveMeasure(input, CM.CLASSES, classes) 40 | // context.saveMeasure(input, CM.FUNCTIONS, methods) 41 | // context.saveMeasure(input, CM.ACCESSORS, accessors) 42 | // context.saveMeasure(input, CM.COMPLEXITY_IN_FUNCTIONS, complexityInMethods) 43 | // context.saveMeasure(input, CM.COMPLEXITY_IN_CLASSES, fileComplexity) 44 | // context.saveMeasure(input, CM.COMPLEXITY, fileComplexity) 45 | // context.saveMeasure(input, CM.PUBLIC_API, publicApiChecker.getPublicApi()) 46 | // context.saveMeasure(input, CM.PUBLIC_DOCUMENTED_API_DENSITY, publicApiChecker.getDocumentedPublicApiDensity()) 47 | // context.saveMeasure(input, CM.PUBLIC_UNDOCUMENTED_API, publicApiChecker.getUndocumentedPublicApi()) 48 | 49 | } 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /src/test/resources/ScalaFile1.scala: -------------------------------------------------------------------------------- 1 | class ScalaFile1 { 2 | val value = "value" 3 | 4 | def function: Unit = { 5 | println("function called.") 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/test/resources/ScalaFile2.scala: -------------------------------------------------------------------------------- 1 | // Expected Header Comment 2 | 3 | class ScalaFile2 { 4 | val value = "value" 5 | 6 | def function: Unit = { 7 | println("function called.") 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/test/scala/com/buransky/plugins/scoverage/pathcleaner/BruteForceSequenceMatcherSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scoverage Plugin 3 | * Copyright (C) 2013 Rado Buransky 4 | * dev@sonar.codehaus.org 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | */ 20 | package com.buransky.plugins.scoverage.pathcleaner 21 | 22 | import org.junit.runner.RunWith 23 | import org.scalatest.mock.MockitoSugar 24 | import org.scalatest.junit.JUnitRunner 25 | import org.scalatest.FlatSpec 26 | import com.buransky.plugins.scoverage.pathcleaner.BruteForceSequenceMatcher.PathSeq 27 | import org.scalatest.Matchers 28 | import java.io.File 29 | import org.mockito.Mockito._ 30 | 31 | @RunWith(classOf[JUnitRunner]) 32 | class BruteForceSequenceMatcherSpec extends FlatSpec with Matchers with MockitoSugar { 33 | 34 | // file-map of all files under baseDir/sonar.sources organized by their filename 35 | val filesMap: Map[String, Seq[PathSeq]] = Map ( 36 | "rootTestFile.scala" -> List(List("testProject", "main", "rootTestFile.scala")), 37 | "nestedTestFile.scala" -> List(List("testProject", "main", "some", "folders", "nestedTestFile.scala")), 38 | "multiFile.scala" -> List( 39 | List("testProject", "main", "some", "multiFile.scala"), 40 | List("testProject", "main", "some", "folder", "multiFile.scala") 41 | ) 42 | ) 43 | 44 | // baseDir = testProject sonar.sources = main 45 | val testee = new BruteForceSequenceMatcherTestee("/testProject/main", filesMap) 46 | 47 | 48 | 49 | behavior of "BruteForceSequenceMatcher with absolute report filenames" 50 | 51 | it should "provide just the filename for top level files" in { 52 | testee.getSourceRelativePath(List("testProject", "main", "rootTestFile.scala")).get shouldEqual List("rootTestFile.scala") 53 | } 54 | 55 | it should "provide the filename and the folders for nested files" in { 56 | testee.getSourceRelativePath(List("testProject", "main", "some", "folders", "nestedTestFile.scala")).get shouldEqual List("some", "folders", "nestedTestFile.scala") 57 | } 58 | 59 | it should "find the correct file if multiple files with same name exist" in { 60 | testee.getSourceRelativePath(List("testProject", "main", "some", "multiFile.scala")).get shouldEqual List("some", "multiFile.scala") 61 | testee.getSourceRelativePath(List("testProject", "main", "some", "folder", "multiFile.scala")).get shouldEqual List("some", "folder", "multiFile.scala") 62 | } 63 | 64 | 65 | 66 | 67 | behavior of "BruteForceSequenceMatcher with filenames relative to the base dir" 68 | 69 | it should "provide just the filename for top level files" in { 70 | testee.getSourceRelativePath(List("main", "rootTestFile.scala")).get shouldEqual List("rootTestFile.scala") 71 | } 72 | 73 | it should "provide the filename and the folders for nested files" in { 74 | testee.getSourceRelativePath(List("main", "some", "folders", "nestedTestFile.scala")).get shouldEqual List("some", "folders", "nestedTestFile.scala") 75 | } 76 | 77 | it should "find the correct file if multiple files with same name exist" in { 78 | testee.getSourceRelativePath(List("main", "some", "multiFile.scala")).get shouldEqual List("some", "multiFile.scala") 79 | testee.getSourceRelativePath(List("main", "some", "folder", "multiFile.scala")).get shouldEqual List("some", "folder", "multiFile.scala") 80 | } 81 | 82 | 83 | 84 | 85 | behavior of "BruteForceSequenceMatcher with filenames relative to the src dir" 86 | 87 | it should "provide just the filename for top level files" in { 88 | testee.getSourceRelativePath(List("rootTestFile.scala")).get shouldEqual List("rootTestFile.scala") 89 | } 90 | 91 | it should "provide the filename and the folders for nested files" in { 92 | testee.getSourceRelativePath(List("some", "folders", "nestedTestFile.scala")).get shouldEqual List("some", "folders", "nestedTestFile.scala") 93 | } 94 | 95 | it should "find the correct file if multiple files with same name exist" in { 96 | testee.getSourceRelativePath(List("some", "multiFile.scala")).get shouldEqual List("some", "multiFile.scala") 97 | testee.getSourceRelativePath(List("some", "folder", "multiFile.scala")).get shouldEqual List("some", "folder", "multiFile.scala") 98 | } 99 | 100 | 101 | 102 | 103 | class BruteForceSequenceMatcherTestee(absoluteSrcPath: String, filesMap: Map[String, Seq[PathSeq]]) 104 | extends BruteForceSequenceMatcher(mock[File], "") { 105 | 106 | def srcDir = { 107 | val dir = mock[File] 108 | when(dir.isAbsolute).thenReturn(true) 109 | when(dir.isDirectory).thenReturn(true) 110 | when(dir.getAbsolutePath).thenReturn(absoluteSrcPath) 111 | dir 112 | } 113 | 114 | override private[pathcleaner] def initSourceDir(): File = srcDir 115 | override private[pathcleaner] def initFilesMap(): Map[String, Seq[PathSeq]] = filesMap 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/scala/com/buransky/plugins/scoverage/sensor/ScoverageSensorSpec.scala: -------------------------------------------------------------------------------- 1 | // /* 2 | // * Sonar Scoverage Plugin 3 | // * Copyright (C) 2013 Rado Buransky 4 | // * dev@sonar.codehaus.org 5 | // * 6 | // * This program is free software; you can redistribute it and/or 7 | // * modify it under the terms of the GNU Lesser General Public 8 | // * License as published by the Free Software Foundation; either 9 | // * version 3 of the License, or (at your option) any later version. 10 | // * 11 | // * This program is distributed in the hope that it will be useful, 12 | // * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | // * Lesser General Public License for more details. 15 | // * 16 | // * You should have received a copy of the GNU Lesser General Public 17 | // * License along with this program; if not, write to the Free Software 18 | // * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | // */ 20 | // package com.buransky.plugins.scoverage.sensor 21 | 22 | // import java.io.File 23 | // import java.util 24 | 25 | // import com.buransky.plugins.scoverage.language.Scala 26 | // import com.buransky.plugins.scoverage.{FileStatementCoverage, DirectoryStatementCoverage, ProjectStatementCoverage, ScoverageReportParser} 27 | // import org.junit.runner.RunWith 28 | // import org.mockito.Mockito._ 29 | // import org.scalatest.junit.JUnitRunner 30 | // import org.scalatest.mock.MockitoSugar 31 | // import org.scalatest.{FlatSpec, Matchers} 32 | // import org.sonar.api.batch.fs.{FilePredicate, FilePredicates, FileSystem} 33 | // import org.sonar.api.config.Settings 34 | // import org.sonar.api.resources.Project 35 | // import org.sonar.api.resources.Project.AnalysisType 36 | // import org.sonar.api.scan.filesystem.PathResolver 37 | 38 | // import scala.collection.JavaConversions._ 39 | // import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer 40 | // import org.mockito.Matchers.any 41 | 42 | 43 | // @RunWith(classOf[JUnitRunner]) 44 | // class ScoverageSensorSpec extends FlatSpec with Matchers with MockitoSugar { 45 | // behavior of "shouldExecuteOnProject" 46 | 47 | // it should "succeed for Scala project" in new ShouldExecuteOnProject { 48 | // checkShouldExecuteOnProject(List("scala"), true) 49 | // } 50 | 51 | // it should "succeed for mixed projects" in new ShouldExecuteOnProject { 52 | // checkShouldExecuteOnProject(List("scala", "java"), true) 53 | // } 54 | 55 | // it should "fail for Java project" in new ShouldExecuteOnProject { 56 | // checkShouldExecuteOnProject(List("java"), false) 57 | // } 58 | 59 | // class ShouldExecuteOnProject extends ScoverageSensorScope { 60 | // protected def checkShouldExecuteOnProject(languages: Iterable[String], expectedResult: Boolean) { 61 | // // Setup 62 | // val project = mock[Project] 63 | // when(fileSystem.languages()).thenReturn(new util.TreeSet(languages)) 64 | 65 | // // Execute & asser 66 | // shouldExecuteOnProject(project) should equal(expectedResult) 67 | 68 | // verify(fileSystem, times(1)).languages 69 | 70 | // } 71 | // } 72 | 73 | // behavior of "analyse for single project" 74 | 75 | // it should "set 0% coverage for a project without children" in new AnalyseScoverageSensorScope { 76 | // // Setup 77 | // val pathToScoverageReport = "#path-to-scoverage-report#" 78 | // val reportAbsolutePath = "#report-absolute-path#" 79 | // val projectStatementCoverage = 80 | // ProjectStatementCoverage("project-name", List( 81 | // DirectoryStatementCoverage(File.separator, List( 82 | // DirectoryStatementCoverage("home", List( 83 | // FileStatementCoverage("a.scala", 3, 2, Nil) 84 | // )) 85 | // )), 86 | // DirectoryStatementCoverage("x", List( 87 | // FileStatementCoverage("b.scala", 1, 0, Nil) 88 | // )) 89 | // )) 90 | // val reportFile = mock[java.io.File] 91 | // val moduleBaseDir = mock[java.io.File] 92 | // val filePredicates = mock[FilePredicates] 93 | // when(reportFile.exists).thenReturn(true) 94 | // when(reportFile.isFile).thenReturn(true) 95 | // when(reportFile.getAbsolutePath).thenReturn(reportAbsolutePath) 96 | // when(settings.getString(SCOVERAGE_REPORT_PATH_PROPERTY)).thenReturn(pathToScoverageReport) 97 | // when(fileSystem.baseDir).thenReturn(moduleBaseDir) 98 | // when(fileSystem.predicates).thenReturn(filePredicates) 99 | // when(fileSystem.inputFiles(any[FilePredicate]())).thenReturn(Nil) 100 | // when(pathResolver.relativeFile(moduleBaseDir, pathToScoverageReport)).thenReturn(reportFile) 101 | // when(scoverageReportParser.parse(any[String](), any[PathSanitizer]())).thenReturn(projectStatementCoverage) 102 | 103 | // // Execute 104 | // analyse(project, context) 105 | // } 106 | 107 | // class AnalyseScoverageSensorScope extends ScoverageSensorScope { 108 | // val project = mock[Project] 109 | // val context = new TestSensorContext 110 | 111 | // override protected lazy val scoverageReportParser = mock[ScoverageReportParser] 112 | // override protected def createPathSanitizer(sonarSources: String) = mock[PathSanitizer] 113 | // } 114 | 115 | // class ScoverageSensorScope extends { 116 | // val scala = new Scala 117 | // val settings = mock[Settings] 118 | // val pathResolver = mock[PathResolver] 119 | // val fileSystem = mock[FileSystem] 120 | // } with ScoverageSensor(settings, pathResolver, fileSystem) 121 | 122 | // } 123 | -------------------------------------------------------------------------------- /src/test/scala/com/buransky/plugins/scoverage/sensor/TestSensorContext.scala: -------------------------------------------------------------------------------- 1 | // /* 2 | // * Sonar Scoverage Plugin 3 | // * Copyright (C) 2013 Rado Buransky 4 | // * dev@sonar.codehaus.org 5 | // * 6 | // * This program is free software; you can redistribute it and/or 7 | // * modify it under the terms of the GNU Lesser General Public 8 | // * License as published by the Free Software Foundation; either 9 | // * version 3 of the License, or (at your option) any later version. 10 | // * 11 | // * This program is distributed in the hope that it will be useful, 12 | // * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | // * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | // * Lesser General Public License for more details. 15 | // * 16 | // * You should have received a copy of the GNU Lesser General Public 17 | // * License along with this program; if not, write to the Free Software 18 | // * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | // */ 20 | // package com.buransky.plugins.scoverage.sensor 21 | 22 | // import java.lang.Double 23 | // import java.util.Date 24 | // import java.{io, util} 25 | 26 | // import org.sonar.api.batch.fs.{FileSystem, InputFile, InputPath} 27 | // import org.sonar.api.batch.rule.ActiveRules 28 | // import org.sonar.api.batch.sensor.dependency.NewDependency 29 | // import org.sonar.api.batch.sensor.duplication.NewDuplication 30 | // import org.sonar.api.batch.sensor.highlighting.NewHighlighting 31 | // import org.sonar.api.batch.sensor.issue.NewIssue 32 | // import org.sonar.api.batch.sensor.measure.NewMeasure 33 | // import org.sonar.api.batch.{AnalysisMode, Event, SensorContext} 34 | // import org.sonar.api.config.Settings 35 | // import org.sonar.api.design.Dependency 36 | // import org.sonar.api.measures.{Measure, MeasuresFilter, Metric} 37 | // import org.sonar.api.resources.{ProjectLink, Resource} 38 | // import org.sonar.api.rules.Violation 39 | 40 | // import scala.collection.mutable 41 | 42 | // class TestSensorContext extends SensorContext { 43 | 44 | // private val measures = mutable.Map[String, Measure[_ <: io.Serializable]]() 45 | 46 | // override def saveDependency(dependency: Dependency): Dependency = ??? 47 | 48 | // override def isExcluded(reference: Resource): Boolean = ??? 49 | 50 | // override def deleteLink(key: String): Unit = ??? 51 | 52 | // override def isIndexed(reference: Resource, acceptExcluded: Boolean): Boolean = ??? 53 | 54 | // override def saveViolations(violations: util.Collection[Violation]): Unit = ??? 55 | 56 | // override def getParent(reference: Resource): Resource = ??? 57 | 58 | // override def getOutgoingDependencies(from: Resource): util.Collection[Dependency] = ??? 59 | 60 | // override def saveSource(reference: Resource, source: String): Unit = ??? 61 | 62 | // override def getMeasures[M](filter: MeasuresFilter[M]): M = ??? 63 | 64 | // override def getMeasures[M](resource: Resource, filter: MeasuresFilter[M]): M = ??? 65 | 66 | // override def deleteEvent(event: Event): Unit = ??? 67 | 68 | // override def saveViolation(violation: Violation, force: Boolean): Unit = ??? 69 | 70 | // override def saveViolation(violation: Violation): Unit = ??? 71 | 72 | // override def saveResource(resource: Resource): String = ??? 73 | 74 | // override def getEvents(resource: Resource): util.List[Event] = ??? 75 | 76 | // override def getDependencies: util.Set[Dependency] = ??? 77 | 78 | // override def getIncomingDependencies(to: Resource): util.Collection[Dependency] = ??? 79 | 80 | // override def index(resource: Resource): Boolean = ??? 81 | 82 | // override def index(resource: Resource, parentReference: Resource): Boolean = ??? 83 | 84 | // override def saveLink(link: ProjectLink): Unit = ??? 85 | 86 | // override def getMeasure[G <: io.Serializable](metric: Metric[G]): Measure[G] = measures.get(metric.getKey).orNull.asInstanceOf[Measure[G]] 87 | 88 | // override def getMeasure[G <: io.Serializable](resource: Resource, metric: Metric[G]): Measure[G] = ??? 89 | 90 | // override def getChildren(reference: Resource): util.Collection[Resource] = ??? 91 | 92 | // override def createEvent(resource: Resource, name: String, description: String, category: String, date: Date): Event = ??? 93 | 94 | // override def getResource[R <: Resource](reference: R): R = ??? 95 | 96 | // override def getResource(inputPath: InputPath): Resource = ??? 97 | 98 | // override def saveMeasure(measure: Measure[_ <: io.Serializable]): Measure[_ <: io.Serializable] = ??? 99 | 100 | // override def saveMeasure(metric: Metric[_ <: io.Serializable], value: Double): Measure[_ <: io.Serializable] = ??? 101 | 102 | // override def saveMeasure(resource: Resource, metric: Metric[_ <: io.Serializable], value: Double): Measure[_ <: io.Serializable] = ??? 103 | 104 | // override def saveMeasure(resource: Resource, measure: Measure[_ <: io.Serializable]): Measure[_ <: io.Serializable] = { 105 | // measures.put(resource.getKey, measure) 106 | // measure 107 | // } 108 | 109 | // override def saveMeasure(inputFile: InputFile, metric: Metric[_ <: io.Serializable], value: Double): Measure[_ <: io.Serializable] = ??? 110 | 111 | // override def saveMeasure(inputFile: InputFile, measure: Measure[_ <: io.Serializable]): Measure[_ <: io.Serializable] = ??? 112 | 113 | // override def newDuplication(): NewDuplication = ??? 114 | 115 | // override def activeRules(): ActiveRules = ??? 116 | 117 | // override def newHighlighting(): NewHighlighting = ??? 118 | 119 | // override def analysisMode(): AnalysisMode = ??? 120 | 121 | // override def fileSystem(): FileSystem = ??? 122 | 123 | // override def newDependency(): NewDependency = ??? 124 | 125 | // override def settings(): Settings = ??? 126 | 127 | // override def newMeasure[G <: io.Serializable](): NewMeasure[G] = ??? 128 | 129 | // override def newIssue(): NewIssue = ??? 130 | // } 131 | -------------------------------------------------------------------------------- /src/test/scala/com/buransky/plugins/scoverage/util/PathUtilSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scoverage Plugin 3 | * Copyright (C) 2013 Rado Buransky 4 | * dev@sonar.codehaus.org 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | */ 20 | package com.buransky.plugins.scoverage.util 21 | 22 | import org.scalatest.{FlatSpec, Matchers} 23 | import org.junit.runner.RunWith 24 | import org.scalatest.junit.JUnitRunner 25 | 26 | @RunWith(classOf[JUnitRunner]) 27 | class PathUtilSpec extends FlatSpec with Matchers { 28 | 29 | val osName = System.getProperty("os.name") 30 | val separator = System.getProperty("file.separator") 31 | 32 | behavior of s"splitPath for $osName" 33 | 34 | it should "ignore the empty path" in { 35 | PathUtil.splitPath("") should equal(List.empty[String]) 36 | } 37 | 38 | it should "ignore a separator at the beginning" in { 39 | PathUtil.splitPath(s"${separator}a") should equal(List("a")) 40 | } 41 | 42 | it should "work with separator in the middle" in { 43 | PathUtil.splitPath(s"a${separator}b") should equal(List("a", "b")) 44 | } 45 | 46 | it should "work with an OS dependent absolute path" in { 47 | if (osName.startsWith("Windows")) { 48 | PathUtil.splitPath("C:\\test\\2") should equal(List("test", "2")) 49 | } else { 50 | PathUtil.splitPath("/test/2") should equal(List("test", "2")) 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportConstructingParserSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scoverage Plugin 3 | * Copyright (C) 2013 Rado Buransky 4 | * dev@sonar.codehaus.org 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | */ 20 | package com.buransky.plugins.scoverage.xml 21 | 22 | import org.junit.runner.RunWith 23 | import org.scalatest.junit.JUnitRunner 24 | import org.scalatest.{Matchers, FlatSpec} 25 | import scala.io.Source 26 | import com.buransky.plugins.scoverage.xml.data.XmlReportFile1 27 | import scala._ 28 | import com.buransky.plugins.scoverage.{ProjectStatementCoverage, FileStatementCoverage, DirectoryStatementCoverage} 29 | import com.buransky.plugins.scoverage.pathcleaner.PathSanitizer 30 | import com.buransky.plugins.scoverage.StatementCoverage 31 | import com.buransky.plugins.scoverage.NodeStatementCoverage 32 | 33 | @RunWith(classOf[JUnitRunner]) 34 | class XmlScoverageReportConstructingParserSpec extends FlatSpec with Matchers { 35 | behavior of "parse source" 36 | 37 | it must "parse old broken Scoverage 0.95 file correctly" in { 38 | val sanitizer = new PathSanitizer() { 39 | def getSourceRelativePath(path: Seq[String]): Option[Seq[String]] = { 40 | // do nothing 41 | Some(path) 42 | } 43 | } 44 | assertReportFile(XmlReportFile1.scoverage095Data, 24.53, sanitizer)(assertScoverage095Data) 45 | } 46 | 47 | it must "parse new fixed Scoverage 1.0.4 file correctly" in { 48 | val sanitizer = new PathSanitizer() { 49 | def getSourceRelativePath(path: Seq[String]): Option[Seq[String]] = { 50 | // drop first 6 = /a1b2c3/workspace/sonar-test/src/main/scala 51 | Some(path.drop(6)) 52 | } 53 | } 54 | assertReportFile(XmlReportFile1.scoverage104Data, 50.0, sanitizer) { projectCoverage => 55 | assert(projectCoverage.name === "") 56 | assert(projectCoverage.children.size.toInt === 1) 57 | 58 | projectCoverage.children.head match { 59 | case rootDir: DirectoryStatementCoverage => { 60 | val rr = checkNode(rootDir, "com", 0, 0, 0.0).head 61 | val test = checkNode(rr, "rr", 0, 0, 0.0).head 62 | val sonar = checkNode(test, "test", 0, 0, 0.0).head 63 | val mainClass = checkNode(sonar, "sonar", 2, 1, 50.0).head 64 | 65 | checkNode(mainClass, "MainClass.scala", 2, 1, 50.0) 66 | } 67 | case other => fail(s"This is not a directory statement coverage! [$other]") 68 | } 69 | } 70 | } 71 | 72 | it must "parse file1 correctly even without XML declaration" in { 73 | val sanitizer = new PathSanitizer() { 74 | def getSourceRelativePath(path: Seq[String]): Option[Seq[String]] = { 75 | // do nothing 76 | Some(path) 77 | } 78 | } 79 | assertReportFile(XmlReportFile1.dataWithoutDeclaration, 24.53, sanitizer)(assertScoverage095Data) 80 | } 81 | 82 | private def assertReportFile(data: String, expectedCoverage: Double, pathSanitizer: PathSanitizer)(f: (ProjectStatementCoverage) => Unit) { 83 | val parser = new XmlScoverageReportConstructingParser(Source.fromString(data), pathSanitizer) 84 | val projectCoverage = parser.parse() 85 | 86 | // Assert coverage 87 | checkRate(expectedCoverage, projectCoverage.rate) 88 | 89 | f(projectCoverage) 90 | } 91 | 92 | private def assertScoverage095Data(projectCoverage: ProjectStatementCoverage): Unit = { 93 | // Assert structure 94 | projectCoverage.name should equal("") 95 | 96 | val projectChildren = projectCoverage.children.toList 97 | projectChildren.length should equal(1) 98 | projectChildren.head shouldBe a [DirectoryStatementCoverage] 99 | 100 | val aaa = projectChildren.head.asInstanceOf[DirectoryStatementCoverage] 101 | aaa.name should equal("aaa") 102 | checkRate(24.53, aaa.rate) 103 | 104 | val aaaChildren = aaa.children.toList.sortBy(_.statementCount) 105 | aaaChildren.length should equal(2) 106 | 107 | aaaChildren(1) shouldBe a [FileStatementCoverage] 108 | val errorCode = aaaChildren(1).asInstanceOf[FileStatementCoverage] 109 | errorCode.name should equal("ErrorCode.scala") 110 | errorCode.statementCount should equal (46) 111 | errorCode.coveredStatementsCount should equal (13) 112 | 113 | aaaChildren.head shouldBe a [FileStatementCoverage] 114 | val graph = aaaChildren.head.asInstanceOf[FileStatementCoverage] 115 | graph.name should equal("Graph.scala") 116 | graph.statementCount should equal (7) 117 | graph.coveredStatementsCount should equal (0) 118 | } 119 | 120 | private def checkRate(expected: Double, real: Double) { 121 | BigDecimal(real).setScale(2, BigDecimal.RoundingMode.HALF_UP).should(equal(BigDecimal(expected))) 122 | } 123 | 124 | private def checkNode(node: NodeStatementCoverage, name: String, count: Int, covered: Int, rate: Double): Iterable[NodeStatementCoverage] = { 125 | node.name shouldEqual name 126 | node.statementCount shouldEqual count 127 | node.coveredStatementsCount shouldEqual covered 128 | 129 | checkRate(rate, node.rate) 130 | 131 | node.children 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/test/scala/com/buransky/plugins/scoverage/xml/XmlScoverageReportParserSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scoverage Plugin 3 | * Copyright (C) 2013 Rado Buransky 4 | * dev@sonar.codehaus.org 5 | * 6 | * This program is free software; you can redistribute it and/or 7 | * modify it under the terms of the GNU Lesser General Public 8 | * License as published by the Free Software Foundation; either 9 | * version 3 of the License, or (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | * Lesser General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Lesser General Public 17 | * License along with this program; if not, write to the Free Software 18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 19 | */ 20 | package com.buransky.plugins.scoverage.xml 21 | 22 | import org.scalatest.{FlatSpec, Matchers} 23 | import org.scalatest.junit.JUnitRunner 24 | import org.junit.runner.RunWith 25 | import com.buransky.plugins.scoverage.ScoverageException 26 | 27 | @RunWith(classOf[JUnitRunner]) 28 | class XmlScoverageReportParserSpec extends FlatSpec with Matchers { 29 | behavior of "parse file path" 30 | 31 | it must "fail for null path" in { 32 | the[IllegalArgumentException] thrownBy XmlScoverageReportParser().parse(null.asInstanceOf[String], null) 33 | } 34 | 35 | it must "fail for empty path" in { 36 | the[IllegalArgumentException] thrownBy XmlScoverageReportParser().parse("", null) 37 | } 38 | 39 | it must "fail for not existing path" in { 40 | the[ScoverageException] thrownBy XmlScoverageReportParser().parse("/x/a/b/c/1/2/3/4.xml", null) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleAdaptedQualityProfileSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scalastyle Plugin 3 | * Copyright (C) 2014 All contributors 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 3 of the License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 18 | */ 19 | package com.ncredinburgh.sonar.scalastyle 20 | 21 | import com.ncredinburgh.sonar.scalastyle.testUtils.TestRuleFinderWithTemplates 22 | import org.scalatest._ 23 | import org.scalatest.mock.MockitoSugar 24 | import org.sonar.api.profiles.RulesProfile 25 | import org.sonar.api.utils.ValidationMessages 26 | 27 | import scala.collection.JavaConversions._ 28 | 29 | /** 30 | * Tests an adapted ScalastyleQualityProfile, assuming the user instantiated all templates once 31 | */ 32 | class ScalastyleQualityProfileSpec extends FlatSpec with Matchers with MockitoSugar { 33 | trait Fixture { 34 | val validationMessages = ValidationMessages.create 35 | val testee = new ScalastyleQualityProfile(TestRuleFinderWithTemplates) 36 | } 37 | 38 | val rulesCount = 38 39 | val parametersCount = 21 40 | 41 | "a scalastyle quality profile" should "create a default profile" in new Fixture { 42 | val rulesProfile = testee.createProfile(validationMessages) 43 | 44 | rulesProfile.getClass shouldEqual classOf[RulesProfile] 45 | rulesProfile.getName shouldEqual Constants.ProfileName 46 | rulesProfile.getLanguage shouldEqual Constants.ScalaKey 47 | } 48 | 49 | "the default quality profile" should "have all the rules in default config" in new Fixture { 50 | val rulesProfile = testee.createProfile(validationMessages) 51 | 52 | rulesProfile.getActiveRules.size shouldBe rulesCount 53 | } 54 | 55 | it should "have all the parameters in default config" in new Fixture { 56 | val totalParameters = parametersCount + (rulesCount * 1) 57 | 58 | val rulesProfile = testee.createProfile(validationMessages) 59 | 60 | rulesProfile.getActiveRules.flatMap(_.getActiveRuleParams).size shouldBe totalParameters 61 | } 62 | 63 | it should "have correct values for parameters" in new Fixture { 64 | val ruleKey = "scalastyle_NumberOfMethodsInTypeChecker" 65 | 66 | val rulesProfile = testee.createProfile(validationMessages) 67 | val rule = rulesProfile.getActiveRule(Constants.RepositoryKey, ruleKey) 68 | val param = rule.getActiveRuleParams.head 69 | 70 | param.getKey shouldBe "maxMethods" 71 | param.getValue shouldBe "30" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleDefaultQualityProfileSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scalastyle Plugin 3 | * Copyright (C) 2014 All contributors 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 3 of the License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 18 | */ 19 | package com.ncredinburgh.sonar.scalastyle 20 | 21 | import com.ncredinburgh.sonar.scalastyle.testUtils.TestRuleFinderWithTemplates 22 | import org.scalatest._ 23 | import org.scalatest.mock.MockitoSugar 24 | import org.sonar.api.profiles.RulesProfile 25 | import org.sonar.api.utils.ValidationMessages 26 | import scala.collection.JavaConversions._ 27 | import com.ncredinburgh.sonar.scalastyle.testUtils.TestRuleFinder 28 | 29 | /** 30 | * Tests the default ScalastyleQualityProfile, only rules without parameters, no templates 31 | */ 32 | class ScalastyleDefaultQualityProfileSpec extends FlatSpec with Matchers with MockitoSugar { 33 | trait Fixture { 34 | val validationMessages = ValidationMessages.create 35 | val testee = new ScalastyleQualityProfile(TestRuleFinder) 36 | } 37 | 38 | val rulesCount = 19 // rules without templates 39 | 40 | "a scalastyle quality profile" should "create a default profile" in new Fixture { 41 | val rulesProfile = testee.createProfile(validationMessages) 42 | 43 | rulesProfile.getClass shouldEqual classOf[RulesProfile] 44 | rulesProfile.getName shouldEqual Constants.ProfileName 45 | rulesProfile.getLanguage shouldEqual Constants.ScalaKey 46 | } 47 | 48 | "the default quality profile" should "have all the rules in default config" in new Fixture { 49 | val rulesProfile = testee.createProfile(validationMessages) 50 | 51 | rulesProfile.getActiveRules.size shouldBe rulesCount 52 | } 53 | 54 | it should "have all the parameters in default config" in new Fixture { 55 | val totalParameters = (rulesCount * 1) 56 | 57 | val rulesProfile = testee.createProfile(validationMessages) 58 | 59 | rulesProfile.getActiveRules.flatMap(_.getActiveRuleParams).size shouldBe totalParameters 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleRepositorySpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scalastyle Plugin 3 | * Copyright (C) 2014 All contributors 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 3 of the License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 18 | */ 19 | package com.ncredinburgh.sonar.scalastyle 20 | 21 | import org.scalatest._ 22 | import org.sonar.api.rules.RulePriority 23 | import org.sonar.api.server.rule.{RuleParamType, RulesDefinition} 24 | 25 | 26 | import scala.collection.JavaConversions._ 27 | 28 | /** 29 | * Tests ScalastyleRepository 30 | */ 31 | class ScalastyleRepositorySpec extends FlatSpec with Matchers with Inspectors with BeforeAndAfterAll { 32 | 33 | val testee = new ScalastyleRepository 34 | val ctx = new RulesDefinition.Context() 35 | 36 | def rules = ctx.repository(Constants.RepositoryKey).rules() 37 | 38 | override def beforeAll() { 39 | testee.define(ctx) 40 | } 41 | 42 | "a scalastyle repository" should "return a list of rules" in { 43 | rules should not be empty 44 | } 45 | 46 | it should "use the same repository for all rules" in { 47 | forAll(rules) { 48 | r: RulesDefinition.Rule => 49 | r.repository().key() shouldEqual Constants.RepositoryKey 50 | r.repository().name() shouldEqual Constants.RepositoryName 51 | } 52 | } 53 | 54 | it should "consist of 63 rules" in { 55 | rules.size() shouldEqual 63 56 | } 57 | 58 | it should "set default severity to major" in { 59 | forAll(rules) {r: RulesDefinition.Rule => r.severity() shouldEqual RulePriority.MAJOR.name()} 60 | } 61 | 62 | it should "give a name to every rule" in { 63 | rules.filter(_.name == null) should be(empty) 64 | } 65 | 66 | it should "set the ClazzParam for every rule" in { 67 | rules.filter(_.param(Constants.ClazzParam) == null) should be(empty) 68 | } 69 | 70 | it should "name the rule properly" in { 71 | val rule = rules.find(_.key == "scalastyle_MagicNumberChecker") 72 | rule.get.name shouldEqual "Magic Number" 73 | } 74 | 75 | it should "describe the rule properly" in { 76 | val rule = rules.find(_.key == "scalastyle_MagicNumberChecker") 77 | rule.get.htmlDescription shouldEqual 78 | "

Replacing a magic number with a named constant can make code easier to read and understand," + 79 | " and can avoid some subtle bugs.

\n" + 80 | "

A simple assignment to a val is not considered to be a magic number, for example:

\n" + 81 | "

    val foo = 4

\n

is not a magic number, but

\n" + 82 | "

    var foo = 4

\n

is considered to be a magic number.

" 83 | } 84 | 85 | it should "determine the parameter of a rule with a parameter" in { 86 | val rule = rules.find(_.key == "scalastyle_ParameterNumberChecker") 87 | rule.get.params map (_.key) shouldEqual List("maxParameters", Constants.ClazzParam) 88 | } 89 | 90 | it should "determine parameters of a rule with multiple parameters" in { 91 | val rule = rules.find(_.key == "scalastyle_MethodNamesChecker") 92 | rule.get.params map (_.key) should contain theSameElementsAs List("regex", "ignoreRegex", "ignoreOverride", Constants.ClazzParam) 93 | } 94 | 95 | it should "determine correct type of integer parameters" in { 96 | val rule = rules.find(_.key == "scalastyle_ParameterNumberChecker") 97 | rule.get.param("maxParameters").`type` shouldEqual RuleParamType.INTEGER 98 | } 99 | 100 | it should "determine correct type of boolean parameters" in { 101 | val rule = rules.find(_.key == "scalastyle_MethodNamesChecker") 102 | rule.get.param("ignoreOverride").`type` shouldEqual RuleParamType.BOOLEAN 103 | } 104 | 105 | it should "determine correct type of regex parameters" in { 106 | val rule = rules.find(_.key == "scalastyle_ClassTypeParameterChecker") 107 | rule.get.param("regex").`type` shouldEqual RuleParamType.STRING 108 | } 109 | 110 | it should "describe the parameter properly" in { 111 | val rule = rules.find(_.key == "scalastyle_ClassTypeParameterChecker") 112 | rule.get.param("regex").description shouldEqual "Standard Scala regular expression syntax" 113 | } 114 | 115 | it should "provide default parameters to scalastyle preferred defaults for rules with a parameter" in { 116 | val rule = rules.find(_.key == "scalastyle_ParameterNumberChecker") 117 | rule.get.param("maxParameters").defaultValue.toInt shouldEqual 8 118 | } 119 | 120 | it should "provide default parameters to scalastyle preferred defaults for rules with multiple parameters" in { 121 | val rule = rules.find(_.key == "scalastyle_MethodNamesChecker") 122 | rule.get.param("regex").defaultValue shouldEqual "^[a-z][A-Za-z0-9]*(_=)?$" 123 | rule.get.param("ignoreRegex").defaultValue shouldEqual "^$" 124 | rule.get.param("ignoreOverride").defaultValue.toBoolean shouldEqual false 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleResourcesSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scalastyle Plugin 3 | * Copyright (C) 2014 All contributors 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 3 of the License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 18 | */ 19 | package com.ncredinburgh.sonar.scalastyle 20 | 21 | import org.scalatest.{FlatSpec, Inspectors, Matchers, PrivateMethodTester} 22 | import org.sonar.api.PropertyType 23 | import org.sonar.api.server.rule.RuleParamType 24 | 25 | import scala.xml.Elem 26 | 27 | /** 28 | * Tests ScalastyleResources 29 | */ 30 | class ScalastyleResourcesSpec extends FlatSpec with Matchers with Inspectors with PrivateMethodTester { 31 | 32 | it should "get default_config.xml from Scalastyle jar" in { 33 | val xmlFromClassPath = PrivateMethod[Elem]('xmlFromClassPath) 34 | val definitions = ScalastyleResources invokePrivate xmlFromClassPath("/scalastyle_definition.xml") 35 | assert(definitions.isInstanceOf[Elem]) 36 | } 37 | 38 | it should "get scalastyle_definition.xml from Scalastyle jar" in { 39 | val xmlFromClassPath = PrivateMethod[Elem]('xmlFromClassPath) 40 | val scalastyleDefinitions = ScalastyleResources invokePrivate xmlFromClassPath("/scalastyle_definition.xml") 41 | assert(scalastyleDefinitions.isInstanceOf[Elem]) 42 | } 43 | 44 | it should "get scalastyle_documentation.xml from Scalastyle jar" in { 45 | val xmlFromClassPath = PrivateMethod[Elem]('xmlFromClassPath) 46 | val scalastyleDocumentation = ScalastyleResources invokePrivate xmlFromClassPath("/scalastyle_documentation.xml") 47 | assert(scalastyleDocumentation.isInstanceOf[Elem]) 48 | } 49 | 50 | "the configuration" should "allow access to description in documentation for a checker" in { 51 | ScalastyleResources.description("line.size.limit") shouldEqual 52 | "

Lines that are too long can be hard to read and horizontal scrolling is annoying.

" 53 | } 54 | 55 | it should "return all defined checkers" in { 56 | ScalastyleResources.allDefinedRules.size shouldEqual 63 57 | } 58 | 59 | it should "give rules a description" in { 60 | forAll(ScalastyleResources.allDefinedRules) {r: RepositoryRule => r.description.length should be > 0} 61 | } 62 | 63 | it should "give rules an id" in { 64 | forAll(ScalastyleResources.allDefinedRules) {r: RepositoryRule => r.id should not be empty} 65 | } 66 | 67 | it should "get all parameters of rules with a parameter" in { 68 | val rule = ScalastyleResources.allDefinedRules.find(_.clazz == "org.scalastyle.scalariform.ParameterNumberChecker") 69 | rule.get.params map (_.name) shouldEqual List("maxParameters") 70 | } 71 | 72 | it should "get all parameters of rules with multiple parameters" in { 73 | val rule = ScalastyleResources.allDefinedRules.find(_.clazz == "org.scalastyle.scalariform.MethodNamesChecker") 74 | rule.get.params map (_.name) shouldEqual List("regex", "ignoreRegex", "ignoreOverride") 75 | } 76 | 77 | it should "get labels from configuration" in { 78 | ScalastyleResources.label("disallow.space.after.token") shouldEqual "Space after tokens" 79 | ScalastyleResources.label("no.whitespace.before.left.bracket") shouldEqual "No whitespace before left bracket ''[''" 80 | } 81 | 82 | it should "get description from configuration" in { 83 | ScalastyleResources.description("magic.number") shouldEqual 84 | "

Replacing a magic number with a named constant can make code easier to read and understand," + 85 | " and can avoid some subtle bugs.

\n" + 86 | "

A simple assignment to a val is not considered to be a magic number, for example:

\n" + 87 | "

    val foo = 4

\n

is not a magic number, but

\n" + 88 | "

    var foo = 4

\n

is considered to be a magic number.

" 89 | 90 | // In case no long description found, return the short description 91 | ScalastyleResources.label("disallow.space.after.token") shouldEqual "Space after tokens" 92 | } 93 | 94 | it should "get parameter key from node" in { 95 | val xmlFromClassPath = PrivateMethod[Elem]('xmlFromClassPath) 96 | val nodeToRuleParamKey = PrivateMethod[String]('nodeToRuleParamKey) 97 | 98 | val key = "org.scalastyle.scalariform.ParameterNumberChecker" 99 | val definitions = ScalastyleResources invokePrivate xmlFromClassPath("/scalastyle_definition.xml") 100 | val ruleNodes = definitions \\ "scalastyle-definition" \ "checker" 101 | val ruleNode = ruleNodes find { _ \\ "@class" exists (_.text == key) } 102 | 103 | ruleNode match { 104 | case Some(node) => { 105 | val parameter = (node \ "parameters" \ "parameter").head 106 | ScalastyleResources invokePrivate nodeToRuleParamKey(parameter) shouldEqual "maxParameters" 107 | } 108 | case _ => fail("rule with key " + key + "could not found") 109 | } 110 | } 111 | 112 | it should "get property type from node" in { 113 | val xmlFromClassPath = PrivateMethod[Elem]('xmlFromClassPath) 114 | val nodeToRuleParamType = PrivateMethod[PropertyType]('nodeToRuleParamType) 115 | 116 | val key = "org.scalastyle.scalariform.ParameterNumberChecker" 117 | val definitions = ScalastyleResources invokePrivate xmlFromClassPath("/scalastyle_definition.xml") 118 | val ruleNodes = definitions \\ "scalastyle-definition" \ "checker" 119 | val ruleNode = ruleNodes find { _ \\ "@class" exists (_.text == key) } 120 | 121 | ruleNode match { 122 | case Some(node) => { 123 | val parameter = (node \ "parameters" \ "parameter").head 124 | ScalastyleResources invokePrivate nodeToRuleParamType(parameter) shouldEqual RuleParamType.INTEGER 125 | } 126 | case _ => fail("rule with key " + key + "could not found") 127 | } 128 | } 129 | 130 | it should "get default value from node" in { 131 | val xmlFromClassPath = PrivateMethod[Elem]('xmlFromClassPath) 132 | val nodeToDefaultValue = PrivateMethod[String]('nodeToDefaultValue) 133 | 134 | val key = "org.scalastyle.scalariform.ParameterNumberChecker" 135 | val definitions = ScalastyleResources invokePrivate xmlFromClassPath("/scalastyle_definition.xml") 136 | val ruleNodes = definitions \\ "scalastyle-definition" \ "checker" 137 | val ruleNode = ruleNodes find { _ \\ "@class" exists (_.text == key) } 138 | 139 | ruleNode match { 140 | case Some(node) => { 141 | val parameter = (node \ "parameters" \ "parameter").head 142 | ScalastyleResources invokePrivate nodeToDefaultValue(parameter) shouldEqual "8" 143 | } 144 | case _ => fail("rule with key " + key + "could not found") 145 | } 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleRunnerSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scalastyle Plugin 3 | * Copyright (C) 2014 All contributors 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 3 of the License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 18 | */ 19 | package com.ncredinburgh.sonar.scalastyle 20 | 21 | import java.io.File 22 | import java.nio.charset.StandardCharsets 23 | 24 | import org.mockito.Mockito._ 25 | import org.scalastyle._ 26 | import org.scalastyle.StyleError 27 | import org.scalatest.mock.MockitoSugar 28 | import org.scalatest.{FlatSpec, Matchers, PrivateMethodTester} 29 | import org.sonar.api.profiles.RulesProfile 30 | import org.sonar.api.rules.{Rule, RulePriority} 31 | 32 | import scala.collection.JavaConversions._ 33 | 34 | /** 35 | * Tests ScalastyleRunner 36 | */ 37 | class ScalastyleRunnerSpec extends FlatSpec with Matchers with MockitoSugar with PrivateMethodTester { 38 | 39 | trait Fixture { 40 | val checker1 = ConfigurationChecker("org.scalastyle.scalariform.MultipleStringLiteralsChecker", ErrorLevel, true, Map(), None, None) 41 | val checker2 = ConfigurationChecker("org.scalastyle.file.HeaderMatchesChecker", ErrorLevel, true, Map("header" -> "// Expected Header Comment"), None, None) 42 | val configuration = ScalastyleConfiguration("sonar", true, List(checker1, checker2)) 43 | val testeeSpy = spy(new ScalastyleRunner(mock[RulesProfile])) 44 | doReturn(configuration).when(testeeSpy).config 45 | val charset = StandardCharsets.UTF_8.name 46 | } 47 | 48 | 49 | "a scalastyle runner" should "report StyleError messages if there are rule violations" in new Fixture { 50 | val files = List(new File("src/test/resources/ScalaFile1.scala")) 51 | 52 | val messages = testeeSpy.run(charset, files).map(_.toString) 53 | 54 | messages should contain ("StyleError key=header.matches args=List() lineNumber=Some(1) column=None customMessage=None") 55 | 56 | } 57 | 58 | it should "not report StyleError messages if there are no violations" in new Fixture { 59 | val files = List(new File("src/test/resources/ScalaFile2.scala")) 60 | 61 | val messages = testeeSpy.run(charset, files) 62 | 63 | messages.length shouldEqual 0 64 | } 65 | 66 | it should "scan multiple files" in new Fixture { 67 | val files = List(new File("src/test/resources/ScalaFile1.scala"), new File("src/test/resources/ScalaFile2.scala")) 68 | 69 | val messages = testeeSpy.run(charset, files) 70 | 71 | messages.length shouldEqual 1 72 | } 73 | 74 | it should "convert rules to checker" in { 75 | val ruleToChecker = PrivateMethod[ConfigurationChecker]('ruleToChecker) 76 | val profile = RulesProfile.create(Constants.ProfileName, Constants.ScalaKey) 77 | val testee = new ScalastyleRunner(profile) 78 | val key = "multiple.string.literals" 79 | val className = "org.scalastyle.scalariform.MultipleStringLiteralsChecker" 80 | val rule = Rule.create 81 | rule.setRepositoryKey(Constants.RepositoryKey) 82 | .setKey(className) 83 | .setName(ScalastyleResources.label(key)) 84 | .setDescription(ScalastyleResources.description(key)) 85 | .setConfigKey(key) 86 | .setSeverity(RulePriority.MAJOR) 87 | rule.createParameter 88 | .setKey("allowed") 89 | .setDescription("") 90 | .setType("integer") 91 | .setDefaultValue("1") 92 | rule.createParameter 93 | .setKey("ignoreRegex") 94 | .setDescription("") 95 | .setType("integer") 96 | .setDefaultValue("^""$") 97 | 98 | // add synthetic parameter as reference to the class 99 | rule.createParameter 100 | .setKey(Constants.ClazzParam) 101 | .setDescription("Scalastyle checker that validates the rule.") 102 | .setType("string") 103 | .setDefaultValue("org.scalastyle.scalariform.MultipleStringLiteralsChecker") 104 | 105 | val activeRule = profile.activateRule(rule, rule.getSeverity) 106 | activeRule.setParameter("allowed", "1") 107 | activeRule.setParameter("ignoreRegex", "^""$") 108 | activeRule.setParameter(Constants.ClazzParam, "org.scalastyle.scalariform.MultipleStringLiteralsChecker") 109 | 110 | val checker = testee invokePrivate ruleToChecker(activeRule) 111 | val expectedParameters = Map("allowed" -> "1", "ignoreRegex" -> "^""$", Constants.ClazzParam -> "org.scalastyle.scalariform.MultipleStringLiteralsChecker") 112 | val expectedChecker = ConfigurationChecker(className, ErrorLevel, true, expectedParameters, None, Some(className)) 113 | 114 | checker shouldEqual expectedChecker 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/test/scala/com/ncredinburgh/sonar/scalastyle/ScalastyleSensorSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scalastyle Plugin 3 | * Copyright (C) 2014 All contributors 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 3 of the License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 18 | */ 19 | package com.ncredinburgh.sonar.scalastyle 20 | 21 | import java.io.File 22 | import java.nio.charset.StandardCharsets 23 | 24 | import org.mockito.Matchers._ 25 | import org.mockito.Mockito._ 26 | import org.scalastyle._ 27 | import org.scalastyle.file.FileLengthChecker 28 | import org.scalastyle.scalariform.{ForBraceChecker, IfBraceChecker} 29 | import org.scalatest._ 30 | import org.scalatest.mock.MockitoSugar 31 | import org.sonar.api.batch.SensorContext 32 | import org.sonar.api.batch.fs._ 33 | import org.sonar.api.component.ResourcePerspectives 34 | import org.sonar.api.issue.{Issuable, Issue} 35 | import org.sonar.api.resources.Project 36 | import org.sonar.api.rules.{Rule, RuleFinder, RuleQuery} 37 | import org.sonar.core.issue.DefaultIssueBuilder 38 | 39 | import scala.collection.JavaConversions._ 40 | 41 | 42 | class ScalastyleSensorSpec extends FlatSpec with Matchers with MockitoSugar with PrivateMethodTester { 43 | 44 | trait Fixture { 45 | val fs = mock[FileSystem] 46 | val predicates = mock[FilePredicates] 47 | val project = mock[Project] 48 | val runner = mock[ScalastyleRunner] 49 | val perspective = mock[ResourcePerspectives] 50 | val issuable = mock[Issuable] 51 | val issueBuilder = new DefaultIssueBuilder().componentKey("foo").projectKey("bar") 52 | val rf = mock[RuleFinder] 53 | val aRule = Rule.create("repo", "key") 54 | 55 | val testee = new ScalastyleSensor(perspective, runner, fs, rf) 56 | val context = mock[SensorContext] 57 | 58 | when(runner.run(anyString, anyListOf(classOf[File]))).thenReturn(List()) 59 | when(fs.encoding).thenReturn(StandardCharsets.UTF_8) 60 | when(fs.predicates()).thenReturn(predicates) 61 | when(perspective.as(any(), any(classOf[InputPath]))).thenReturn(issuable) 62 | when(issuable.newIssueBuilder()).thenReturn(issueBuilder) 63 | when(rf.find(any[RuleQuery])).thenReturn(aRule) 64 | 65 | def mockScalaPredicate(scalaFiles: java.lang.Iterable[File]): Unit= { 66 | val scalaFilesPred = mock[FilePredicate] 67 | val hasTypePred = mock[FilePredicate] 68 | val langPred = mock[FilePredicate] 69 | when(predicates.hasType(InputFile.Type.MAIN)).thenReturn(hasTypePred) 70 | when(predicates.hasLanguage(Constants.ScalaKey)).thenReturn(langPred) 71 | when(predicates.and(hasTypePred, langPred)).thenReturn(scalaFilesPred) 72 | when(fs.files(scalaFilesPred)).thenReturn(scalaFiles) 73 | } 74 | } 75 | 76 | "A Scalastyle Sensor" should "execute when the project have Scala files" in new Fixture { 77 | mockScalaPredicate(List(new File("foo.scala"), new File("bar.scala"))) 78 | 79 | testee.shouldExecuteOnProject(project) shouldBe true 80 | } 81 | 82 | it should "not execute when there isn't any Scala files" in new Fixture { 83 | mockScalaPredicate(List()) 84 | 85 | testee.shouldExecuteOnProject(project) shouldBe false 86 | } 87 | 88 | 89 | 90 | it should "analyse all scala source files in project" in new Fixture { 91 | val scalaFiles = List(new File("foo.scala"), new File("bar.scala")) 92 | mockScalaPredicate(scalaFiles) 93 | 94 | testee.analyse(project, context) 95 | 96 | verify(runner).run(StandardCharsets.UTF_8.name(), scalaFiles) 97 | } 98 | 99 | it should "not create SonarQube issues when there isn't any scalastyle errors" in new Fixture { 100 | mockScalaPredicate(List(new File("foo.scala"), new File("bar.scala"))) 101 | when(runner.run(anyString, anyListOf(classOf[File]))).thenReturn(List()) 102 | 103 | testee.analyse(project, context) 104 | 105 | verify(issuable, never).addIssue(any[Issue]) 106 | } 107 | 108 | it should "report a scalastyle error as a SonarQube issue" in new Fixture { 109 | mockScalaPredicate(List(new File("foo.scala"), new File("bar.scala"))) 110 | 111 | val error = new StyleError[FileSpec]( 112 | new RealFileSpec("foo.scala", None), 113 | classOf[ForBraceChecker], 114 | "org.scalastyle.scalariform.ForBraceChecker", 115 | WarningLevel, 116 | List(), 117 | None 118 | ) 119 | when(runner.run(anyString, anyListOf(classOf[File]))).thenReturn(List(error)) 120 | 121 | testee.analyse(project, context) 122 | 123 | verify(issuable, times(1)).addIssue(any[Issue]) 124 | } 125 | 126 | it should "report scalastyle errors as SonarQube issues" in new Fixture { 127 | mockScalaPredicate(List(new File("foo.scala"), new File("bar.scala"))) 128 | 129 | val error1 = new StyleError[FileSpec](new RealFileSpec("foo.scala", None), classOf[FileLengthChecker], 130 | "org.scalastyle.file.FileLengthChecker", WarningLevel, List(), None) 131 | val error2 = new StyleError[FileSpec](new RealFileSpec("bar.scala", None), classOf[IfBraceChecker], 132 | "org.scalastyle.scalariform.IfBraceChecker", WarningLevel, List(), None) 133 | when(runner.run(anyString, anyListOf(classOf[File]))).thenReturn(List(error1, error2)) 134 | 135 | testee.analyse(project, context) 136 | 137 | verify(issuable, times(2)).addIssue(any[Issue]) 138 | } 139 | 140 | it should "find sonar rule for error" in new Fixture { 141 | val findSonarRuleForError = PrivateMethod[Rule]('findSonarRuleForError) 142 | val error = new StyleError[FileSpec](new RealFileSpec("foo.scala", None), classOf[FileLengthChecker], 143 | "org.scalastyle.file.FileLengthChecker", WarningLevel, List(), None) 144 | 145 | val rule = testee invokePrivate findSonarRuleForError(error) 146 | 147 | rule.getKey shouldEqual "key" 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/test/scala/com/ncredinburgh/sonar/scalastyle/testUtils/TestRuleFinder.scala: -------------------------------------------------------------------------------- 1 | package com.ncredinburgh.sonar.scalastyle.testUtils 2 | 3 | import java.util 4 | import com.ncredinburgh.sonar.scalastyle.{Constants, ScalastyleResources} 5 | import org.sonar.api.rule.RuleKey 6 | import org.sonar.api.rules.{RulePriority, Rule, RuleQuery, RuleFinder} 7 | import scala.collection.JavaConversions._ 8 | import com.ncredinburgh.sonar.scalastyle.ScalastyleRepository 9 | import org.sonar.api.server.rule.RuleParamType 10 | import com.ncredinburgh.sonar.scalastyle.RepositoryRule 11 | 12 | object TestRuleFinder extends RuleFinder { 13 | 14 | override def findByKey(repositoryKey: String, key: String): Rule = findAll(RuleQuery.create()).find(r => r.getRepositoryKey == repositoryKey && r.getKey == key).orNull 15 | 16 | override def findByKey(key: RuleKey): Rule = findAll(RuleQuery.create()).find(r => r.getRepositoryKey == key.repository() && r.getKey == key.rule()).orNull 17 | 18 | override def findById(ruleId: Int): Rule = findAll(RuleQuery.create()).find(r => r.getId == ruleId).orNull 19 | 20 | override def findAll(query: RuleQuery): util.Collection[Rule] = { 21 | ScalastyleResources.allDefinedRules.filterNot(r => isTemplate(r)) map { 22 | defRule => 23 | val rule = Rule.create() 24 | val key = defRule.id 25 | rule.setRepositoryKey(Constants.RepositoryKey) 26 | rule.setLanguage(Constants.ScalaKey) 27 | rule.setKey(ScalastyleRepository.getStandardKey(defRule.clazz)) 28 | rule.setName(ScalastyleResources.label(key)) 29 | rule.setDescription(defRule.description) 30 | rule.setConfigKey(ScalastyleRepository.getStandardKey(defRule.clazz)) 31 | 32 | // currently all rules comes with "warning" default level so we can treat with major severity 33 | rule.setSeverity(RulePriority.MAJOR) 34 | 35 | // add parameters 36 | defRule.params foreach { 37 | param => 38 | rule 39 | .createParameter 40 | .setDefaultValue(param.defaultVal) 41 | .setType(param.`type`.`type`()) 42 | .setKey(param.name) 43 | .setDescription(param.desc) 44 | } 45 | 46 | // add synthetic parameter as reference to the class 47 | rule.createParameter(Constants.ClazzParam) 48 | .setDefaultValue(defRule.clazz) 49 | .setType(RuleParamType.STRING.`type`()) 50 | .setDescription("Scalastyle checker that validates the rule.") 51 | 52 | rule 53 | } 54 | } 55 | 56 | override def find(query: RuleQuery): Rule = ??? 57 | 58 | private def isTemplate(rule: RepositoryRule): Boolean = { 59 | rule.params.size() > 0 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/scala/com/ncredinburgh/sonar/scalastyle/testUtils/TestRuleFinderWithTemplates.scala: -------------------------------------------------------------------------------- 1 | package com.ncredinburgh.sonar.scalastyle.testUtils 2 | 3 | import java.util 4 | import com.ncredinburgh.sonar.scalastyle.{Constants, ScalastyleResources} 5 | import org.sonar.api.rule.RuleKey 6 | import org.sonar.api.rules.{RulePriority, Rule, RuleQuery, RuleFinder} 7 | import scala.collection.JavaConversions._ 8 | import com.ncredinburgh.sonar.scalastyle.ScalastyleRepository 9 | import org.sonar.api.server.rule.RuleParamType 10 | import com.ncredinburgh.sonar.scalastyle.RepositoryRule 11 | 12 | object TestRuleFinderWithTemplates extends RuleFinder { 13 | 14 | override def findByKey(repositoryKey: String, key: String): Rule = findAll(RuleQuery.create()).find(r => r.getRepositoryKey == repositoryKey && r.getKey == key).orNull 15 | 16 | override def findByKey(key: RuleKey): Rule = findAll(RuleQuery.create()).find(r => r.getRepositoryKey == key.repository() && r.getKey == key.rule()).orNull 17 | 18 | override def findById(ruleId: Int): Rule = findAll(RuleQuery.create()).find(r => r.getId == ruleId).orNull 19 | 20 | override def findAll(query: RuleQuery): util.Collection[Rule] = { 21 | ScalastyleResources.allDefinedRules map { 22 | defRule => 23 | val rule = Rule.create() 24 | val key = defRule.id 25 | rule.setRepositoryKey(Constants.RepositoryKey) 26 | rule.setLanguage(Constants.ScalaKey) 27 | rule.setKey(ScalastyleRepository.getStandardKey(defRule.clazz)) 28 | rule.setName(ScalastyleResources.label(key)) 29 | rule.setDescription(defRule.description) 30 | rule.setConfigKey(ScalastyleRepository.getStandardKey(defRule.clazz)) 31 | 32 | // currently all rules comes with "warning" default level so we can treat with major severity 33 | rule.setSeverity(RulePriority.MAJOR) 34 | 35 | // add parameters 36 | defRule.params foreach { 37 | param => 38 | rule 39 | .createParameter 40 | .setDefaultValue(param.defaultVal) 41 | .setType(param.`type`.`type`()) 42 | .setKey(param.name) 43 | .setDescription(param.desc) 44 | } 45 | 46 | // add synthetic parameter as reference to the class 47 | rule.createParameter(Constants.ClazzParam) 48 | .setDefaultValue(defRule.clazz) 49 | .setType(RuleParamType.STRING.`type`()) 50 | .setDescription("Scalastyle checker that validates the rule.") 51 | 52 | rule 53 | } 54 | } 55 | 56 | override def find(query: RuleQuery): Rule = ??? 57 | 58 | private def isTemplate(rule: RepositoryRule): Boolean = { 59 | rule.params.size() > 0 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/scala/com/sagacify/sonar/scala/BaseMetricSensorSpec.scala: -------------------------------------------------------------------------------- 1 | package com.sagacify.sonar.scala; 2 | 3 | import org.scalatest._ 4 | import org.mockito.Matchers.any; 5 | import org.mockito.Matchers.eq; 6 | import org.mockito.Mockito.mock; 7 | import org.mockito.Mockito.times; 8 | import org.mockito.Mockito.verify; 9 | import org.mockito.Mockito.verifyNoMoreInteractions; 10 | 11 | import java.io.IOException; 12 | import java.nio.file.Paths 13 | import scala.collection.JavaConversions._ 14 | 15 | import org.junit.Before; 16 | import org.junit.Test; 17 | import org.sonar.api.batch.SensorContext; 18 | import org.sonar.api.batch.fs.internal.DefaultFileSystem; 19 | import org.sonar.api.config.Settings; 20 | import org.sonar.api.measures.{CoreMetrics => CM} 21 | import org.sonar.api.measures.Measure; 22 | import org.sonar.api.measures.Metric; 23 | import org.sonar.api.resources.Project; 24 | 25 | import org.sonar.api.batch.fs.internal.DefaultInputFile; 26 | 27 | class ScalaSensorSpec extends FlatSpec with Matchers { 28 | 29 | val NUMBER_OF_FILES = 3; 30 | 31 | val scala = new Scala(new Settings()) 32 | 33 | def context = new { 34 | val fs = new DefaultFileSystem(Paths.get("./src/test/resources")) 35 | val project = mock(classOf[Project]) 36 | val sensor = new ScalaSensor(scala, fs) 37 | } 38 | 39 | // val project = mock(classOf[Project]) 40 | // val sensorContext = mock(classOf[SensorContext]) 41 | // val sensor = new BaseMetricsSensor(new Scala(new Settings()), fs) 42 | 43 | "A ScalaSensor" should "execute on a scala project" in { 44 | val c = context 45 | c.fs.add(new DefaultInputFile("p", "fake.scala").setLanguage("scala")); 46 | assert(c.sensor.shouldExecuteOnProject(c.project)) 47 | } 48 | 49 | it should "only execute on a scala project" in { 50 | val c = context 51 | c.fs.add(new DefaultInputFile("p", "fake.php").setLanguage("php")); 52 | assert(! c.sensor.shouldExecuteOnProject(c.project)) 53 | } 54 | 55 | it should "correctly measure ScalaFile1" in { 56 | val c = context 57 | c.fs.add( 58 | new DefaultInputFile("p", "ScalaFile1.scala").setLanguage("scala")); 59 | val sensorContext = mock(classOf[SensorContext]) 60 | c.sensor.analyse(c.project, sensorContext) 61 | 62 | val inputFiles = c.fs.inputFiles( 63 | c.fs.predicates().hasLanguage(scala.getKey())) 64 | 65 | inputFiles.foreach{ file => 66 | verify(sensorContext, times(1)) 67 | .saveMeasure(file, CM.FILES, 1) 68 | verify(sensorContext, times(1)) 69 | .saveMeasure(file, CM.COMMENT_LINES, 0) 70 | 71 | } 72 | } 73 | 74 | it should "correctly measure ScalaFile2" in { 75 | 76 | val c = context 77 | c.fs.add( 78 | new DefaultInputFile("p", "ScalaFile2.scala").setLanguage("scala")); 79 | val sensorContext = mock(classOf[SensorContext]) 80 | c.sensor.analyse(c.project, sensorContext) 81 | 82 | val inputFiles = c.fs.inputFiles( 83 | c.fs.predicates().hasLanguage(scala.getKey())) 84 | 85 | inputFiles.foreach{ file => 86 | verify(sensorContext, times(1)) 87 | .saveMeasure(file, CM.FILES, 1) 88 | verify(sensorContext, times(1)) 89 | .saveMeasure(file, CM.COMMENT_LINES, 1) 90 | 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/test/scala/com/sagacify/sonar/scala/MeasuresSpec.scala: -------------------------------------------------------------------------------- 1 | package com.sagacify.sonar.scala 2 | 3 | import org.scalatest._ 4 | // import scalariform.lexer.ScalaLexer 5 | // import scalariform.lexer.Token 6 | // import scalariform.lexer.Tokens.LINE_COMMENT 7 | // import scalariform.lexer.Tokens.MULTILINE_COMMENT 8 | // import scalariform.lexer.Tokens.XML_COMMENT 9 | // import scala.annotation.tailrec 10 | 11 | class MeasurersSpec extends FlatSpec with Matchers { 12 | 13 | val exampleSourceFile = """/* 14 | * Sonar Scala Plugin 15 | * Copyright (C) 2011-2016 SonarSource SA 16 | * mailto:contact AT sonarsource DOT com 17 | * 18 | * This program is free software; you can redistribute it and/or 19 | * modify it under the terms of the GNU Lesser General Public 20 | * License as published by the Free Software Foundation; either 21 | * version 3 of the License, or (at your option) any later version. 22 | * 23 | * This program is distributed in the hope that it will be useful, 24 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 25 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 26 | * Lesser General Public License for more details. 27 | * 28 | * You should have received a copy of the GNU Lesser General Public License 29 | * along with this program; if not, write to the Free Software Foundation, 30 | * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 31 | */ 32 | package com.sagacify.example 33 | 34 | import collection.mutable.Stack 35 | import org.scalatest._ 36 | 37 | class ScalaSensorSpec extends FlatSpec with Matchers { 38 | 39 | // Example test 40 | "A Stack" should "pop values in last-in-first-out order" in { 41 | val stack = new Stack[Int] 42 | stack.push(1) // This is 43 | stack.push(2) // a pointless 44 | stack.pop() should be (2) // example 45 | stack.pop() should be (1) 46 | } 47 | 48 | it should "throw NoSuchElementException if an empty stack is popped" in { 49 | val emptyStack = new Stack[Int] 50 | a [NoSuchElementException] should be thrownBy { 51 | emptyStack.pop() 52 | } 53 | } 54 | } 55 | """ 56 | 57 | "A Comment lines counter" should "count line comments" in { 58 | val tokens = Scala.tokenize("// this is a test", "2.11.8") 59 | val count = Measures.count_comment_lines(tokens) 60 | assert(count == 1) 61 | } 62 | 63 | it should "count multiline comments" in { 64 | val tokens = Scala.tokenize("/* this\n *is\n *a\n *test*/", "2.11.8") 65 | val count = Measures.count_comment_lines(tokens) 66 | assert(count == 4) 67 | } 68 | 69 | it should "count trailing comments." in { 70 | val tokens = Scala.tokenize("case class Test() // this is a test", "2.11.8") 71 | val count = Measures.count_comment_lines(tokens) 72 | assert(count == 1) 73 | } 74 | 75 | it should "count the correct number of comments" in { 76 | val tokens = Scala.tokenize(exampleSourceFile, "2.11.8") 77 | val count = Measures.count_comment_lines(tokens) 78 | assert(count == 23) 79 | } 80 | 81 | "A Non-Comment lines counter" should "count non-comment lines of codes" in { 82 | val tokens = Scala.tokenize("package com.example", "2.11.8") 83 | println(tokens) 84 | val count = Measures.count_ncloc(tokens) 85 | assert(count == 1) 86 | } 87 | 88 | it should "count lines of code with a trailing comment" in { 89 | val tokens = Scala.tokenize("case class Test() /*\n * test\n */", "2.11.8") 90 | val count = Measures.count_ncloc(tokens) 91 | assert(count == 1) 92 | } 93 | 94 | it should "count trailing code." in { 95 | val tokens = Scala.tokenize("/* this is a test */ case class Test()", "2.11.8") 96 | val count = Measures.count_ncloc(tokens) 97 | assert(count == 1) 98 | } 99 | 100 | it should "count the correct number of comments" in { 101 | val tokens = Scala.tokenize(exampleSourceFile, "2.11.8") 102 | val count = Measures.count_ncloc(tokens) 103 | assert(count == 18) 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/test/scala/com/sagacify/sonar/scala/ScalaPluginSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Sonar Scalastyle Plugin 3 | * Copyright (C) 2014 All contributors 4 | * 5 | * This program is free software; you can redistribute it and/or 6 | * modify it under the terms of the GNU Lesser General Public 7 | * License as published by the Free Software Foundation; either 8 | * version 3 of the License, or (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | * Lesser General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Lesser General Public 16 | * License along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 18 | */ 19 | package com.sagacify.sonar.scala 20 | 21 | import com.ncredinburgh.sonar.scalastyle.ScalastyleSensor 22 | import com.ncredinburgh.sonar.scalastyle.ScalastyleRepository 23 | import com.ncredinburgh.sonar.scalastyle.ScalastyleQualityProfile 24 | import org.scalatest.{FlatSpec, Matchers} 25 | 26 | /** 27 | * Tests ScalastylePlugin 28 | */ 29 | class ScalastylePluginSpec extends FlatSpec with Matchers { 30 | 31 | val testee = new ScalaPlugin 32 | 33 | "a scala plugin" should "provide a scala sensor" in { 34 | assert(testee.getExtensions.contains(classOf[ScalaSensor])) 35 | } 36 | 37 | it should "provide a scalastyle sensor" in { 38 | assert(testee.getExtensions.contains(classOf[ScalastyleSensor])) 39 | } 40 | 41 | it should "provide a scalastyle repository" in { 42 | assert(testee.getExtensions.contains(classOf[ScalastyleRepository])) 43 | } 44 | 45 | it should "provide a scala language" in { 46 | assert(testee.getExtensions.contains(classOf[Scala])) 47 | } 48 | 49 | it should "provide a scalastyle quality profile" in { 50 | assert(testee.getExtensions.contains(classOf[ScalastyleQualityProfile])) 51 | } 52 | } 53 | --------------------------------------------------------------------------------