├── .git-blame-ignore-revs ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.yml ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── ci.yml │ ├── release-drafter.yml │ └── release.yml ├── .gitignore ├── .scalafix.conf ├── .scalafmt.conf ├── 2.10.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin └── test-release.sh ├── build.sbt ├── domain └── src │ ├── main │ └── scala │ │ └── scoverage │ │ └── domain │ │ ├── Builders.scala │ │ ├── CodeGrid.scala │ │ ├── Constants.scala │ │ ├── CoverageMetrics.scala │ │ ├── DoubleFormat.scala │ │ ├── Location.scala │ │ ├── Statement.scala │ │ ├── StatementStatus.scala │ │ └── coverage.scala │ └── test │ └── scala │ └── scoverage │ └── domain │ └── CoverageTest.scala ├── misc ├── logo1.png ├── logo2.png ├── scales.png ├── scales2.png ├── screenshot1.png └── screenshot2.png ├── package-lock.json ├── package.json ├── plugin └── src │ ├── main │ ├── resources │ │ └── scalac-plugin.xml │ └── scala │ │ └── scoverage │ │ ├── CoverageFilter.scala │ │ ├── Location.scala │ │ ├── ScoverageOptions.scala │ │ └── ScoveragePlugin.scala │ └── test │ ├── scala-2.11+ │ └── scoverage │ │ └── macrosupport │ │ └── TesterMacro.scala │ ├── scala-2.13+ │ └── scoverage │ │ └── Scala213PluginCoverageTest.scala │ └── scala │ └── scoverage │ ├── LocationCompiler.scala │ ├── LocationTest.scala │ ├── MacroSupport.scala │ ├── PluginASTSupportTest.scala │ ├── PluginCoverageScalaJsTest.scala │ ├── PluginCoverageTest.scala │ ├── RegexCoverageFilterTest.scala │ ├── ScoverageCompiler.scala │ ├── ScoverageOptionsTest.scala │ └── macrosupport │ └── Tester.scala ├── project ├── build.properties └── plugins.sbt ├── reporter └── src │ ├── main │ ├── resources │ │ └── scoverage │ │ │ ├── index.html │ │ │ └── pure-min.css │ └── scala │ │ └── scoverage │ │ └── reporter │ │ ├── BaseReportWriter.scala │ │ ├── CoberturaXmlWriter.scala │ │ ├── CoverageAggregator.scala │ │ ├── IOUtils.scala │ │ ├── ScoverageHtmlWriter.scala │ │ ├── ScoverageXmlWriter.scala │ │ └── StatementWriter.scala │ └── test │ ├── resources │ └── scoverage │ │ └── reporter │ │ ├── cobertura.sample.xml │ │ ├── coverage-04.dtd │ │ └── forHtmlWriter │ │ └── src │ │ └── main │ │ └── scala │ │ ├── ClassContainingHtml.scala │ │ ├── ClassInMainDir.scala │ │ └── subdir │ │ └── ClassInSubDir.scala │ └── scala │ └── scoverage │ └── reporter │ ├── CoberturaXmlWriterTest.scala │ ├── CoverageAggregatorTest.scala │ ├── CoverageMetricsTest.scala │ ├── CoverageTest.scala │ ├── IOUtilsTest.scala │ └── ScoverageHtmlWriterTest.scala ├── runtime ├── js │ └── src │ │ └── main │ │ └── scala │ │ ├── scalajssupport │ │ ├── File.scala │ │ ├── FileWriter.scala │ │ ├── JsFile.scala │ │ ├── NodeFile.scala │ │ ├── PhantomFile.scala │ │ ├── RhinoFile.scala │ │ └── Source.scala │ │ └── scoverage │ │ └── Platform.scala ├── jvm │ └── src │ │ ├── main │ │ └── scala │ │ │ └── scoverage │ │ │ └── Platform.scala │ │ └── test │ │ └── scala │ │ └── scoverage │ │ └── InvokerConcurrencyTest.scala ├── native │ └── src │ │ └── main │ │ └── scala │ │ └── scoverage │ │ └── Platform.scala └── shared │ └── src │ ├── main │ └── scala │ │ └── scoverage │ │ └── Invoker.scala │ └── test │ └── scala │ └── scoverage │ └── InvokerMultiModuleTest.scala ├── runtimeJSDOMTest └── src │ └── test │ └── scala │ └── scoverage │ └── InvokerMultiModuleJSDOMTest.scala └── serializer └── src ├── main └── scala │ └── scoverage │ └── serialize │ └── Serializer.scala └── test └── scala └── scoverage └── serialize └── SerializerTest.scala /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # It's a good idea to ignore the following commits from git blame. 2 | # You can read more about this here if you're unfamiliar: 3 | # https://www.moxio.com/blog/43/ignoring-bulk-change-commits-with-git-blame 4 | # 5 | # Added scallafix and scalafmt 6 | 03aeb373675b76d3fd021854fda776aafef07bd7 7 | 8 | # Scala Steward: Reformat with scalafmt 3.8.5 9 | 6c5f5c6d874d7c9e15bfc78f3c847829297fb8ae 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a bug report for Scala 2 or reports. For Scala 3 issues please report to https://github.com/lampepfl/dotty. 3 | body: 4 | - type: textarea 5 | id: what-happened 6 | attributes: 7 | label: Describe the bug 8 | description: A clear and concise description of what the bug is. 9 | placeholder: | 10 | Description ... 11 | 12 | Reproduction steps 13 | 1. Go to ... 14 | 2. Click on ... 15 | 3. Scroll down to ... 16 | 4. See error 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: expectation 22 | attributes: 23 | label: Expected behavior 24 | description: A clear and concise description of what you expected to happen. 25 | 26 | - type: dropdown 27 | id: build-tool 28 | attributes: 29 | label: What build tool are you using? 30 | options: 31 | - sbt 32 | - Mill 33 | - Gradle 34 | - Maven 35 | - Other 36 | validations: 37 | required: true 38 | 39 | - type: input 40 | id: version 41 | attributes: 42 | label: Version of scoverage 43 | placeholder: v2.0.0 44 | validations: 45 | required: true 46 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## What’s Changed 3 | 4 | $CHANGES 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'doc/**' 7 | - 'docs/**' 8 | - '*.md' 9 | branches: 10 | - main 11 | pull_request: 12 | 13 | jobs: 14 | test-plugin: 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [ 'ubuntu-latest', 'windows-latest' ] 20 | java: ['8', '17'] 21 | scala: [ 22 | { version: '2.12.20' }, 23 | { version: '2.12.19' }, 24 | { version: '2.12.18' }, 25 | { version: '2.13.16' }, 26 | { version: '2.13.15' }, 27 | { version: '2.13.14' } 28 | ] 29 | steps: 30 | - name: checkout the repo 31 | uses: actions/checkout@v4 32 | with: 33 | fetch-depth: 0 34 | 35 | - uses: sbt/setup-sbt@v1 36 | 37 | - name: Set up JVM 38 | uses: actions/setup-java@v4 39 | with: 40 | distribution: 'temurin' 41 | java-version: ${{ matrix.java }} 42 | 43 | - name: run tests 44 | run: sbt ++${{ matrix.scala.version }} plugin/test 45 | 46 | test-the-rest: 47 | runs-on: ${{ matrix.os }} 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | os: [ 'ubuntu-latest', 'windows-latest' ] 52 | java: ['8', '17' ] 53 | module: ['runtime', 'runtimeJS', 'runtimeJSDOMTest', 'runtimeNative', 'reporter', 'domain', 'serializer'] 54 | steps: 55 | - name: checkout the repo 56 | uses: actions/checkout@v4 57 | with: 58 | fetch-depth: 0 59 | 60 | - uses: sbt/setup-sbt@v1 61 | 62 | - name: Set up JVM 63 | uses: actions/setup-java@v4 64 | with: 65 | distribution: 'temurin' 66 | java-version: ${{ matrix.java }} 67 | 68 | - name: Install JSDOM 69 | run: npm install 70 | if: matrix.module == 'runtimeJSDOMTest' 71 | 72 | - name: run tests 73 | run: sbt +${{ matrix.module }}/test 74 | 75 | style-check: 76 | runs-on: ubuntu-latest 77 | 78 | steps: 79 | - name: checkout the repo 80 | uses: actions/checkout@v4 81 | with: 82 | fetch-depth: 0 83 | 84 | - uses: sbt/setup-sbt@v1 85 | 86 | - name: Set up JVM 87 | uses: actions/setup-java@v4 88 | with: 89 | distribution: 'temurin' 90 | java-version: '17' 91 | 92 | - name: styleCheck 93 | run: sbt styleCheck 94 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: release-drafter/release-drafter@v6 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: ["*"] 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | 15 | - uses: sbt/setup-sbt@v1 16 | 17 | - name: Set up JVM 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: 'temurin' 21 | java-version: 17 22 | 23 | - run: sbt ci-release 24 | env: 25 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 26 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 27 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 28 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | 3 | # Build Server Protocol 4 | .bsp/ 5 | .metals/ 6 | metals.sbt 7 | 8 | # SBT specific 9 | target/ 10 | project/boot/ 11 | project/plugins/project/ 12 | credentials.sbt 13 | 14 | # Eclipse specific 15 | .classpath 16 | .project 17 | .settings/ 18 | 19 | # Scala-IDE specific 20 | .cache-main 21 | .cache-tests 22 | 23 | # IntelliJ IDEA specific 24 | .idea 25 | .idea_modules 26 | *.iml 27 | 28 | # npm specific 29 | node_modules/ 30 | -------------------------------------------------------------------------------- /.scalafix.conf: -------------------------------------------------------------------------------- 1 | rules = [ 2 | OrganizeImports 3 | ] 4 | 5 | OrganizeImports.groupedImports = Explode 6 | OrganizeImports.expandRelative = true 7 | OrganizeImports.removeUnused = true 8 | OrganizeImports.groups = [ 9 | "re:javax?\\." 10 | "scala." 11 | "*" 12 | ] 13 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.9.4" 2 | project.git = true 3 | runner.dialect = "scala213" 4 | assumeStandardLibraryStripMargin = true 5 | xmlLiterals.assumeFormatted = true 6 | docstrings.wrap = no 7 | -------------------------------------------------------------------------------- /2.10.md: -------------------------------------------------------------------------------- 1 | ## Where has 2.10 support gone ? 2 | 3 | This is a long answer, but an interesting one. 4 | 5 | As part of the compilation step, scalac builds a data structure of the code, called an abstract syntax tree. This AST 6 | can then be modified by further compilation steps. For example, one of the steps is called `uncurry` and changes 7 | curried functions into the non-curried version that the JVM supports. 8 | 9 | Each node in the scala AST has an associated position. This position can be in one of several states: `no-position`, 10 | meaning positional information was not available; `offset` which is the number of characters from the start of the 11 | file; `range` which gives you the start and end rather than just an offset. It is this range information that 12 | scoverage leverages to provide the pretty HTML output of code highlighted on a per-statement level. Without the range 13 | information it would not be able to highlight properly. 14 | 15 | Range information is not enabled by default. To enable it one must use a compiler switch. Scoverage enables this 16 | switch as part of the build. Now, there is an interesting line of code in the compiler: 17 | 18 | ` if (global.settings.Yrangepos && !global.reporter.hasErrors) global.validatePositions(unit.body)` 19 | 20 | This can be found in the typer phase of the compiler and it says: if range positions are enabled and there has been no 21 | errors reported, validate the tree. 22 | 23 | What does validating the tree entail? 24 | 25 | A number of checks, but the important one is checking that no range-enabled 26 | node is nested inside a non-range node. Why is this important? It wouldn't be, but for the kicker. Lots of macro code 27 | does not set range positions properly. The macro expanded code is inserted as part of this typer phase, 28 | and because of the lack of range information, the 2.10 validator fails. 29 | 30 | When scoverage for Scala 2.10 was first released about a year ago, macros were fairly new, 31 | and almost no one used them. Over the past year macros have become commonplace, such as ScalaLogging, ReactiveMongo, 32 | Salat and other libraries using macros extensively. 33 | 34 | This means scoverage falls foul of this issue. I don't know if one could call it a bug, 35 | since range positions are not activated by default, and if it is a bug, whether to blame macro writers, 36 | or the compiler, but the end result is that it doesn't work in 2.10 but does in 2.11[1] 37 | 38 | Unfortunately this means 2.10 won't be supported any longer. 39 | 40 | [1] https://issues.scala-lang.org/browse/SI-6743 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We are committed to providing a friendly, safe and welcoming environment for 4 | all, regardless of level of experience, gender, gender identity and expression, 5 | sexual orientation, disability, personal appearance, body size, race, ethnicity, 6 | age, religion, nationality, or other such characteristics. 7 | 8 | Everyone is expected to follow the [Scala Code of 9 | Conduct](https://www.scala-lang.org/conduct/) when discussing the project on the 10 | available communication channels. 11 | 12 | ## Moderation 13 | 14 | Any questions, concerns, or moderation requests please contact a member of the project. 15 | 16 | - Chris Kipp | [twitter](https://twitter.com/ckipp01) | [email](mailto:open-source@chris-kipp.io) 17 | - Stephen Samuel | [twitter](https://twitter.com/_sksamuel) | [email](mailto:sam@sksamuel.com) 18 | 19 | _The text for this was borrowed from 20 | [typelevel/cats-effect](https://github.com/typelevel/cats-effect/blob/series/3.x/CODE_OF_CONDUCT.md)_ 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | To run the tests, you will first need to run `npm install`. This will 4 | install the JSDOM dependency used to run some of the Scala.js tests. 5 | 6 | When working in the code base it's a good idea to utilize the 7 | `.git-blame-ignore-revs` file at the root of this project. You can add it 8 | locally by doing a: 9 | 10 | ```sh 11 | git config blame.ignoreRevsFile .git-blame-ignore-revs 12 | ``` 13 | 14 | This will ensure that when you are using `git blame` functionality that the 15 | listed commit in that file are ignored. 16 | 17 | ## Making a release 18 | 19 | scalac-scoverage-plugin relies on 20 | [sbt-ci-release](https://github.com/olafurpg/sbt-ci-release) for an automated 21 | release process. In order to make this clear for anyone in the future that may 22 | need to cut a release, I've outlined the steps below: 23 | 24 | 1. Tag a new release locally. `git tag -a vX.X.X -m "v.X.X.X"` 25 | 2. Push the new tag upstream. `git push upstream --tags` The tag will trigger a 26 | release via GitHub Actions. You can see this if you look in 27 | `.github/workflows/release.yml`. 28 | 3. Once the CI has ran, everything should be available pretty much right away. 29 | You can verify this with the script in `bin/test-release.sh`. Keep in mind 30 | that if you add support for a new Scala version, add it to the 31 | `test-release.sh` script. 32 | 4. Once the release is verified, update the draft release in 33 | [here](https://github.com/scoverage/scalac-scoverage-plugin/releases) and 34 | "publish" the release. This will notify everyone that follows the repo that a 35 | release was made and also serve as the release notes. 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scalac-scoverage-plugin 2 | 3 | ![build](https://github.com/scoverage/scalac-scoverage-plugin/workflows/build/badge.svg) 4 | [![Gitter](https://img.shields.io/gitter/room/scoverage/scoverage.svg)](https://gitter.im/scoverage/scoverage) 5 | [![Maven Central](https://img.shields.io/maven-central/v/org.scoverage/scalac-scoverage-plugin_2.11.12.svg?label=latest%202.11%20Scala%20support%20[2.11.12]%20and%20latest%20version)](http://search.maven.org/#search|ga|1|g%3A%22org.scoverage%22%20AND%20a%3A%22scalac-scoverage-plugin_2.11.12%22) 6 | [![Maven Central](https://img.shields.io/maven-central/v/org.scoverage/scalac-scoverage-plugin_2.12.16.svg?label=2.12%20Scala%20support%20)](http://search.maven.org/#search|ga|1|g%3A%22org.scoverage%22%20AND%20a%3A%22scalac-scoverage-plugin_2.12.16%22) 7 | [![Maven Central](https://img.shields.io/maven-central/v/org.scoverage/scalac-scoverage-plugin_2.13.8.svg?label=2.13%20Scala%20support%20)](http://search.maven.org/#search|ga|1|g%3A%22org.scoverage%22%20AND%20a%3A%22scalac-scoverage-plugin_2.13.8%22) 8 | [![Maven Central](https://img.shields.io/maven-central/v/org.scoverage/scalac-scoverage-domain_3.svg?label=3%20Scala%20support%20)](http://search.maven.org/#search|ga|1|g%3A%22org.scoverage%22%20AND%20a%3A%22scalac-scoverage-domain_3%22) 9 | [![License](http://img.shields.io/:license-Apache%202-red.svg)](http://www.apache.org/licenses/LICENSE-2.0.txt) 10 | 11 | scoverage is a free Apache licensed code coverage tool for Scala that offers 12 | statement and branch coverage. scoverage is available for 13 | [sbt](https://github.com/scoverage/sbt-scoverage), 14 | [Maven](https://github.com/scoverage/scoverage-maven-plugin), 15 | [Mill](https://com-lihaoyi.github.io/mill/mill/Plugin_Scoverage.html), and 16 | [Gradle](https://github.com/scoverage/gradle-scoverage). 17 | 18 | 19 | **NOTE**: That this repository contains the Scala compiler plugin for Code coverage 20 | in Scala 2 and other coverage utilities for generating reports. For Scala 3 code 21 | coverage the [compiler](https://github.com/lampepfl/dotty) natively produces 22 | code coverage output, but the reporting logic utilities are then shared with the 23 | Scala 2 code coverage utilities in this repo. 24 | 25 | ![Screenshot of scoverage report html](misc/screenshot2.png) 26 | 27 | ### Statement Coverage 28 | 29 | In traditional code coverage tools, line coverage has been the main metric. 30 | This is fine for languages such as Java which are very verbose and very rarely have more than one 31 | statement per line, and more usually have one statement spread across multiple lines. 32 | 33 | In powerful, expressive languages like Scala, quite often multiple statements, or even branches 34 | are included on a single line, eg a very simple example: 35 | 36 | ``` 37 | val status = if (Color == Red) Stop else Go 38 | ``` 39 | 40 | If you had a unit test that ran through the Color Red you would get 100% line coverage 41 | yet you only have 50% statement coverage. 42 | 43 | Let's expand this example out to be multifaceted, albeit somewhat contrived: 44 | 45 | ``` 46 | val status = if (Color == Red) Stop else if (Sign == Stop) Stop else Go 47 | ``` 48 | 49 | Now we would get 100% code coverage for passing in the values (Green, SpeedLimit). 50 | 51 | That's why in scoverage we focus on statement coverage, and don't even include line coverage as a metric. 52 | This is a paradigm shift that we hope will take hold. 53 | 54 | ### Branch Coverage 55 | 56 | Branch coverage is very useful to ensure all code paths are covered. Scoverage produces branch coverage metrics 57 | as a percentage of the total branches. Symbols that are deemed as branch statements are: 58 | 59 | * If / else statements 60 | * Match statements 61 | * Partial function cases 62 | * Try / catch / finally clauses 63 | 64 | In this screenshot you can see the coverage HTML report that shows one branch of the if statement was not 65 | executed during the test run. In addition two of the cases in the partial function were not executed. 66 | ![Screenshot of scoverage report html](misc/screenshot1.png) 67 | 68 | ### How to use 69 | 70 | This project is the base library for instrumenting code via a scalac compiler plugin. To use scoverage in your 71 | project you will need to use one of the build plugins: 72 | 73 | * [scoverage-maven-plugin](https://github.com/scoverage/scoverage-maven-plugin) 74 | * [sbt-scoverage](https://github.com/scoverage/sbt-scoverage) 75 | * [gradle-scoverage](https://github.com/scoverage/gradle-scoverage) 76 | * [sbt-coveralls](https://github.com/scoverage/sbt-coveralls) 77 | * [mill-contrib-scoverage](https://www.lihaoyi.com/mill/page/contrib-modules.html#scoverage) 78 | * Upload report to [Codecov](https://codecov.io): [Example Scala Repository](https://github.com/codecov/example-scala) 79 | * Upload report to [Codacy](https://www.codacy.com/): [Documentation](https://support.codacy.com/hc/en-us/articles/207279819-Coverage) 80 | 81 | Scoverage support is available for the following tools: 82 | 83 | * [Sonar](https://github.com/RadoBuransky/sonar-scoverage-plugin) 84 | * [Jenkins](https://github.com/jenkinsci/scoverage-plugin) 85 | 86 | If you want to write a tool that uses this code coverage library then it is available on maven central. 87 | Search for scalac-scoverage-plugin. 88 | 89 | #### Excluding code from coverage stats 90 | 91 | You can exclude whole classes or packages by name. Pass a semicolon separated 92 | list of regexes to the `excludedPackages` option. 93 | 94 | For example: 95 | 96 | -P:scoverage:excludedPackages:.*\.utils\..*;.*\.SomeClass;org\.apache\..* 97 | 98 | The regular expressions are matched against the fully qualified class name, and must match the entire string to take effect. 99 | 100 | Any matched classes will not be instrumented or included in the coverage report. 101 | 102 | You can also exclude files from being considered for instrumentation. 103 | 104 | -P:scoverage:excludedFiles:.*\/two\/GoodCoverage;.*\/three\/.* 105 | 106 | Note: The `.scala` file extension needs to be omitted from the filename, if one is given. 107 | 108 | Note: These two options only work for Scala2. Right now Scala3 does not support 109 | a way to exclude packages or files from being instrumented. 110 | 111 | You can also mark sections of code with comments like: 112 | 113 | // $COVERAGE-OFF$ 114 | ... 115 | // $COVERAGE-ON$ 116 | 117 | Any code between two such comments will not be instrumented or included in the coverage report. 118 | 119 | Further details are given in the plugin readme's. 120 | 121 | ### Release History 122 | 123 | For a full release history please see the [releases 124 | page](https://github.com/scoverage/scalac-scoverage-plugin/releases). 125 | -------------------------------------------------------------------------------- /bin/test-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eux 3 | 4 | version=$1 5 | 6 | coursier fetch \ 7 | org.scoverage:scalac-scoverage-plugin_2.12.16:$version \ 8 | org.scoverage:scalac-scoverage-plugin_2.12.17:$version \ 9 | org.scoverage:scalac-scoverage-plugin_2.12.18:$version \ 10 | org.scoverage:scalac-scoverage-plugin_2.12.19:$version \ 11 | org.scoverage:scalac-scoverage-plugin_2.12.20:$version \ 12 | org.scoverage:scalac-scoverage-plugin_2.13.11:$version \ 13 | org.scoverage:scalac-scoverage-plugin_2.13.12:$version \ 14 | org.scoverage:scalac-scoverage-plugin_2.13.13:$version \ 15 | org.scoverage:scalac-scoverage-plugin_2.13.14:$version \ 16 | org.scoverage:scalac-scoverage-plugin_2.13.15:$version \ 17 | org.scoverage:scalac-scoverage-plugin_2.13.16:$version \ 18 | org.scoverage:scalac-scoverage-runtime_2.12:$version \ 19 | org.scoverage:scalac-scoverage-runtime_2.13:$version \ 20 | org.scoverage:scalac-scoverage-runtime_sjs1_2.12:$version \ 21 | org.scoverage:scalac-scoverage-runtime_sjs1_2.13:$version \ 22 | org.scoverage:scalac-scoverage-runtime_native0.4_2.12:$version \ 23 | org.scoverage:scalac-scoverage-runtime_native0.4_2.13:$version \ 24 | org.scoverage:scalac-scoverage-domain_2.12:$version \ 25 | org.scoverage:scalac-scoverage-domain_2.13:$version \ 26 | org.scoverage:scalac-scoverage-domain_3:$version \ 27 | org.scoverage:scalac-scoverage-reporter_2.12:$version \ 28 | org.scoverage:scalac-scoverage-reporter_2.13:$version \ 29 | org.scoverage:scalac-scoverage-reporter_3:$version \ 30 | org.scoverage:scalac-scoverage-serializer_2.12:$version \ 31 | org.scoverage:scalac-scoverage-serializer_2.13:$version \ 32 | org.scoverage:scalac-scoverage-serializer_3:$version \ 33 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import sbtcrossproject.CrossProject 2 | import sbtcrossproject.CrossType 3 | 4 | lazy val munitVersion = "1.1.1" 5 | lazy val scalametaVersion = "4.9.9" 6 | lazy val defaultScala212 = "2.12.20" 7 | lazy val defaultScala213 = "2.13.16" 8 | lazy val defaultScala3 = "3.3.0" 9 | lazy val bin212 = 10 | Seq( 11 | defaultScala212, 12 | "2.12.19", 13 | "2.12.18", 14 | "2.12.17", 15 | "2.12.16" 16 | ) 17 | lazy val bin213 = 18 | Seq( 19 | defaultScala213, 20 | "2.13.15", 21 | "2.13.14", 22 | "2.13.13", 23 | "2.13.12", 24 | "2.13.11" 25 | ) 26 | 27 | inThisBuild( 28 | List( 29 | organization := "org.scoverage", 30 | homepage := Some(url("http://scoverage.org/")), 31 | developers := List( 32 | Developer( 33 | "sksamuel", 34 | "Stephen Samuel", 35 | "sam@sksamuel.com", 36 | url("https://github.com/sksamuel") 37 | ), 38 | Developer( 39 | "gslowikowski", 40 | "Grzegorz Slowikowski", 41 | "gslowikowski@gmail.com", 42 | url("https://github.com/gslowikowski") 43 | ), 44 | Developer( 45 | "ckipp01", 46 | "Chris Kipp", 47 | "open-source@chris-kipp.io", 48 | url("https://www.chris-kipp.io/") 49 | ) 50 | ), 51 | licenses := Seq( 52 | "Apache-2.0" -> url("http://www.apache.org/license/LICENSE-2.0") 53 | ), 54 | scalaVersion := defaultScala213, 55 | versionScheme := Some("early-semver"), 56 | Test / fork := false, 57 | Test / publishArtifact := false, 58 | Test / parallelExecution := false, 59 | Global / concurrentRestrictions += Tags.limit(Tags.Test, 1), 60 | scalacOptions := Seq( 61 | "-unchecked", 62 | "-deprecation", 63 | "-feature", 64 | "-encoding", 65 | "utf8" 66 | ), 67 | semanticdbEnabled := true, 68 | semanticdbVersion := scalametaVersion, 69 | scalafixScalaBinaryVersion := scalaBinaryVersion.value 70 | ) 71 | ) 72 | 73 | lazy val sharedSettings = List( 74 | scalacOptions := { 75 | if (scalaVersion.value == defaultScala213) { 76 | scalacOptions.value :+ "-Wunused:imports" 77 | } else { 78 | scalacOptions.value 79 | } 80 | }, 81 | libraryDependencies += "org.scalameta" %%% "munit" % munitVersion % Test 82 | ) 83 | 84 | lazy val root = Project("scalac-scoverage", file(".")) 85 | .settings( 86 | name := "scalac-scoverage", 87 | publishArtifact := false, 88 | publishLocal := {} 89 | ) 90 | .aggregate( 91 | plugin, 92 | runtime.jvm, 93 | runtime.js, 94 | runtime.native, 95 | runtimeJSDOMTest, 96 | reporter, 97 | domain, 98 | serializer, 99 | buildInfo 100 | ) 101 | 102 | lazy val runtime = CrossProject( 103 | "runtime", 104 | file("runtime") 105 | )(JVMPlatform, JSPlatform, NativePlatform) 106 | .crossType(CrossType.Full) 107 | .withoutSuffixFor(JVMPlatform) 108 | .settings( 109 | name := "scalac-scoverage-runtime", 110 | crossScalaVersions := Seq(defaultScala212, defaultScala213), 111 | crossTarget := target.value / s"scala-${scalaVersion.value}", 112 | sharedSettings 113 | ) 114 | .jvmSettings( 115 | Test / fork := true 116 | ) 117 | 118 | lazy val `runtimeJVM` = runtime.jvm 119 | lazy val `runtimeJS` = runtime.js 120 | 121 | lazy val runtimeJSDOMTest = 122 | project 123 | .enablePlugins(ScalaJSPlugin) 124 | .dependsOn(runtimeJS % "test->test") 125 | .settings( 126 | publishArtifact := false, 127 | publishLocal := {}, 128 | jsEnv := new org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv(), 129 | sharedSettings 130 | ) 131 | 132 | lazy val plugin = 133 | project 134 | // we need both runtimes compiled prior to running tests 135 | .dependsOn(runtimeJVM % Test, runtimeJS % Test) 136 | .settings( 137 | name := "scalac-scoverage-plugin", 138 | crossTarget := target.value / s"scala-${scalaVersion.value}", 139 | crossScalaVersions := bin212 ++ bin213, 140 | crossVersion := CrossVersion.full, 141 | libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value % Provided, 142 | sharedSettings 143 | ) 144 | .settings( 145 | Test / unmanagedSourceDirectories += (Test / sourceDirectory).value / "scala-2.12+", 146 | Test / unmanagedSourceDirectories ++= { 147 | val sourceDir = (Test / sourceDirectory).value 148 | CrossVersion.partialVersion(scalaVersion.value) match { 149 | case Some((2, n)) if n >= 13 => Seq(sourceDir / "scala-2.13+") 150 | case _ => Seq.empty 151 | } 152 | } 153 | ) 154 | .dependsOn(domain, reporter % "test->compile", serializer, buildInfo % Test) 155 | 156 | lazy val reporter = 157 | project 158 | .settings( 159 | name := "scalac-scoverage-reporter", 160 | libraryDependencies += "org.scala-lang.modules" %% "scala-xml" % "2.3.0", 161 | sharedSettings, 162 | crossScalaVersions := Seq(defaultScala212, defaultScala213, defaultScala3) 163 | ) 164 | .dependsOn(domain, serializer) 165 | 166 | lazy val buildInfo = 167 | project 168 | .settings( 169 | crossScalaVersions := bin212 ++ bin213, 170 | buildInfoKeys += BuildInfoKey("scalaJSVersion", scalaJSVersion), 171 | publishArtifact := false, 172 | publishLocal := {} 173 | ) 174 | .enablePlugins(BuildInfoPlugin) 175 | 176 | lazy val domain = 177 | project 178 | .settings( 179 | name := "scalac-scoverage-domain", 180 | sharedSettings, 181 | crossScalaVersions := Seq(defaultScala212, defaultScala213, defaultScala3) 182 | ) 183 | 184 | lazy val serializer = 185 | project 186 | .settings( 187 | name := "scalac-scoverage-serializer", 188 | sharedSettings, 189 | crossScalaVersions := Seq(defaultScala212, defaultScala213, defaultScala3) 190 | ) 191 | .dependsOn(domain) 192 | 193 | addCommandAlias( 194 | "styleFix", 195 | "scalafixAll ; scalafmtAll ; scalafmtSbt" 196 | ) 197 | 198 | addCommandAlias( 199 | "styleCheck", 200 | "scalafmtCheckAll ; scalafmtSbtCheck ; scalafix --check" 201 | ) 202 | -------------------------------------------------------------------------------- /domain/src/main/scala/scoverage/domain/Builders.scala: -------------------------------------------------------------------------------- 1 | package scoverage.domain 2 | 3 | trait MethodBuilders { 4 | def statements: Iterable[Statement] 5 | def methods: Seq[MeasuredMethod] = { 6 | statements 7 | .groupBy(stmt => 8 | stmt.location.packageName + "/" + stmt.location.className + "/" + stmt.location.method 9 | ) 10 | .map(arg => MeasuredMethod(arg._1, arg._2)) 11 | .toSeq 12 | } 13 | def methodCount = methods.size 14 | } 15 | 16 | trait PackageBuilders { 17 | def statements: Iterable[Statement] 18 | def packageCount = packages.size 19 | def packages: Seq[MeasuredPackage] = { 20 | statements 21 | .groupBy(_.location.packageName) 22 | .map(arg => MeasuredPackage(arg._1, arg._2)) 23 | .toSeq 24 | .sortBy(_.name) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /domain/src/main/scala/scoverage/domain/CodeGrid.scala: -------------------------------------------------------------------------------- 1 | package scoverage.domain 2 | 3 | import scala.io.Codec 4 | import scala.io.Source 5 | 6 | /** @author Stephen Samuel */ 7 | class CodeGrid(mFile: MeasuredFile, sourceEncoding: Option[String]) { 8 | 9 | // for backward compatibility only 10 | def this(mFile: MeasuredFile) = { 11 | this(mFile, None); 12 | } 13 | 14 | case class Cell(char: Char, var status: StatementStatus) 15 | 16 | private val lineBreak = "\n" 17 | 18 | // Array of lines, each line is an array of cells, where a cell is a character + coverage info for that position 19 | // All cells default to NoData until the highlighted information is applied 20 | // note: we must re-include the line sep to keep source positions correct. 21 | private val lines = source(mFile) 22 | .split(lineBreak) 23 | .map(line => (line.toCharArray ++ lineBreak).map(Cell(_, NoData))) 24 | 25 | // useful to have a single array to write into the cells 26 | private val cells = lines.flatten 27 | 28 | // apply the instrumentation data to the cells updating their coverage info 29 | mFile.statements.foreach(stmt => { 30 | for (k <- stmt.start until stmt.end) { 31 | if (k < cells.size) { 32 | // if the cell is set to Invoked, then it be changed to NotInvoked, as an inner statement will override 33 | // outer containing statements. If a cell is NotInvoked then it can not be changed further. 34 | // in that block were executed 35 | cells(k).status match { 36 | case Invoked => if (!stmt.isInvoked) cells(k).status = NotInvoked 37 | case NoData => 38 | if (!stmt.isInvoked) cells(k).status = NotInvoked 39 | else if (stmt.isInvoked) cells(k).status = Invoked 40 | case NotInvoked => 41 | } 42 | } 43 | } 44 | }) 45 | 46 | val highlighted: String = { 47 | var lineNumber = 1 48 | val code = lines map (line => { 49 | var style = cellStyle(NoData) 50 | val sb = new StringBuilder 51 | sb append lineNumber append " " 52 | lineNumber = lineNumber + 1 53 | sb append spanStart(NoData) 54 | line.map(cell => { 55 | val style2 = cellStyle(cell.status) 56 | if (style != style2) { 57 | sb append "" 58 | sb append spanStart(cell.status) 59 | style = style2 60 | } 61 | // escape xml characters 62 | cell.char match { 63 | case '<' => sb.append("<") 64 | case '>' => sb.append(">") 65 | case '&' => sb.append("&") 66 | case '"' => sb.append(""") 67 | case c => sb.append(c) 68 | } 69 | }) 70 | sb append "" 71 | sb.toString 72 | }) mkString "" 73 | s"
$code
" 74 | } 75 | 76 | private def source(mfile: MeasuredFile): String = { 77 | val src = sourceEncoding match { 78 | case Some(enc) => Source.fromFile(mfile.source, enc) 79 | case None => Source.fromFile(mfile.source, Codec.UTF8.name) 80 | } 81 | try src.mkString 82 | finally src.close() 83 | } 84 | 85 | private def spanStart(status: StatementStatus): String = 86 | s"" 87 | 88 | private def cellStyle(status: StatementStatus): String = { 89 | val GREEN = "#AEF1AE" 90 | val RED = "#F0ADAD" 91 | status match { 92 | case Invoked => s"background: $GREEN" 93 | case NotInvoked => s"background: $RED" 94 | case NoData => "" 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /domain/src/main/scala/scoverage/domain/Constants.scala: -------------------------------------------------------------------------------- 1 | package scoverage.domain 2 | 3 | object Constants { 4 | // the file that contains the statement mappings 5 | val CoverageFileName = "scoverage.coverage" 6 | // the final scoverage report 7 | val XMLReportFilename = "scoverage.xml" 8 | val XMLReportFilenameWithDebug = "scoverage-debug.xml" 9 | // directory that contains all the measurement data but not reports 10 | val DataDir = "scoverage-data" 11 | // the prefix the measurement files have 12 | val MeasurementsPrefix = "scoverage.measurements." 13 | 14 | val CoverageDataFormatVersion = "3.0" 15 | } 16 | -------------------------------------------------------------------------------- /domain/src/main/scala/scoverage/domain/CoverageMetrics.scala: -------------------------------------------------------------------------------- 1 | package scoverage.domain 2 | 3 | import java.io.File 4 | 5 | trait CoverageMetrics { 6 | def statements: Iterable[Statement] 7 | def statementCount: Int = statements.size 8 | 9 | def ignoredStatements: Iterable[Statement] 10 | def ignoredStatementCount: Int = ignoredStatements.size 11 | 12 | def invokedStatements: Iterable[Statement] = statements.filter(_.count > 0) 13 | def invokedStatementCount = invokedStatements.size 14 | def statementCoverage: Double = if (statementCount == 0) 1 15 | else invokedStatementCount / statementCount.toDouble 16 | def statementCoveragePercent = statementCoverage * 100 17 | def statementCoverageFormatted: String = DoubleFormat.twoFractionDigits( 18 | statementCoveragePercent 19 | ) 20 | def branches: Iterable[Statement] = statements.filter(_.branch) 21 | def branchCount: Int = branches.size 22 | def branchCoveragePercent = branchCoverage * 100 23 | def invokedBranches: Iterable[Statement] = branches.filter(_.count > 0) 24 | def invokedBranchesCount = invokedBranches.size 25 | 26 | /** @see http://stackoverflow.com/questions/25184716/scoverage-ambiguous-measurement-from-branch-coverage 27 | */ 28 | def branchCoverage: Double = { 29 | // if there are zero branches, then we have a single line of execution. 30 | // in that case, if there is at least some coverage, we have covered the branch. 31 | // if there is no coverage then we have not covered the branch 32 | if (branchCount == 0) { 33 | if (statementCoverage > 0) 1 34 | else 0 35 | } else { 36 | invokedBranchesCount / branchCount.toDouble 37 | } 38 | } 39 | def branchCoverageFormatted: String = 40 | DoubleFormat.twoFractionDigits(branchCoveragePercent) 41 | } 42 | 43 | case class MeasuredMethod(name: String, statements: Iterable[Statement]) 44 | extends CoverageMetrics { 45 | override def ignoredStatements: Iterable[Statement] = Seq() 46 | } 47 | 48 | case class MeasuredClass(fullClassName: String, statements: Iterable[Statement]) 49 | extends CoverageMetrics 50 | with MethodBuilders { 51 | 52 | def source: String = statements.head.source 53 | def loc = statements.map(_.line).max 54 | 55 | /** The class name for display is the FQN minus the package, 56 | * for example "com.a.Foo.Bar.Baz" should display as "Foo.Bar.Baz" 57 | * and "com.a.Foo" should display as "Foo". 58 | * 59 | * This is used in the class lists in the package and overview pages. 60 | */ 61 | def displayClassName = statements.headOption 62 | .map(_.location) 63 | .map { location => 64 | location.fullClassName.stripPrefix(location.packageName + ".") 65 | } 66 | .getOrElse(fullClassName) 67 | 68 | override def ignoredStatements: Iterable[Statement] = Seq() 69 | } 70 | 71 | case class MeasuredPackage(name: String, statements: Iterable[Statement]) 72 | extends CoverageMetrics 73 | with ClassCoverage 74 | with ClassBuilders 75 | with FileBuilders { 76 | override def ignoredStatements: Iterable[Statement] = Seq() 77 | } 78 | 79 | case class MeasuredFile(source: String, statements: Iterable[Statement]) 80 | extends CoverageMetrics 81 | with ClassCoverage 82 | with ClassBuilders { 83 | def filename = new File(source).getName 84 | def loc = statements.map(_.line).max 85 | 86 | override def ignoredStatements: Iterable[Statement] = Seq() 87 | } 88 | 89 | trait ClassCoverage { 90 | this: ClassBuilders => 91 | val statements: Iterable[Statement] 92 | def invokedClasses: Int = classes.count(_.statements.count(_.count > 0) > 0) 93 | def classCoverage: Double = invokedClasses / classes.size.toDouble 94 | } 95 | -------------------------------------------------------------------------------- /domain/src/main/scala/scoverage/domain/DoubleFormat.scala: -------------------------------------------------------------------------------- 1 | package scoverage.domain 2 | 3 | import java.text.DecimalFormat 4 | import java.text.DecimalFormatSymbols 5 | import java.util.Locale 6 | 7 | object DoubleFormat { 8 | private[this] val twoFractionDigitsFormat: DecimalFormat = { 9 | val fmt = new DecimalFormat() 10 | fmt.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.US)) 11 | fmt.setMinimumIntegerDigits(1) 12 | fmt.setMinimumFractionDigits(2) 13 | fmt.setMaximumFractionDigits(2) 14 | fmt.setGroupingUsed(false) 15 | fmt 16 | } 17 | 18 | def twoFractionDigits(d: Double) = twoFractionDigitsFormat.format(d) 19 | 20 | } 21 | -------------------------------------------------------------------------------- /domain/src/main/scala/scoverage/domain/Location.scala: -------------------------------------------------------------------------------- 1 | package scoverage.domain 2 | 3 | /** @param packageName the name of the enclosing package 4 | * @param className the name of the closest enclosing class 5 | * @param fullClassName the fully qualified name of the closest enclosing class 6 | */ 7 | case class Location( 8 | packageName: String, 9 | className: String, 10 | fullClassName: String, 11 | classType: ClassType, 12 | method: String, 13 | sourcePath: String 14 | ) 15 | -------------------------------------------------------------------------------- /domain/src/main/scala/scoverage/domain/Statement.scala: -------------------------------------------------------------------------------- 1 | package scoverage.domain 2 | 3 | import scala.collection.mutable 4 | 5 | case class Statement( 6 | location: Location, 7 | id: Int, 8 | start: Int, 9 | end: Int, 10 | line: Int, 11 | desc: String, 12 | symbolName: String, 13 | treeName: String, 14 | branch: Boolean, 15 | var count: Int = 0, 16 | ignored: Boolean = false, 17 | tests: mutable.Set[String] = mutable.Set[String]() 18 | ) extends java.io.Serializable { 19 | def source = location.sourcePath 20 | def invoked(test: String): Unit = { 21 | count = count + 1 22 | if (test != "") tests += test 23 | } 24 | def isInvoked = count > 0 25 | } 26 | -------------------------------------------------------------------------------- /domain/src/main/scala/scoverage/domain/StatementStatus.scala: -------------------------------------------------------------------------------- 1 | package scoverage.domain 2 | 3 | /** @author Stephen Samuel */ 4 | sealed trait StatementStatus 5 | case object Invoked extends StatementStatus 6 | case object NotInvoked extends StatementStatus 7 | case object NoData extends StatementStatus 8 | -------------------------------------------------------------------------------- /domain/src/main/scala/scoverage/domain/coverage.scala: -------------------------------------------------------------------------------- 1 | package scoverage.domain 2 | 3 | import scala.collection.mutable 4 | 5 | /** @author Stephen Samuel 6 | */ 7 | case class Coverage() 8 | extends CoverageMetrics 9 | with MethodBuilders 10 | with java.io.Serializable 11 | with ClassBuilders 12 | with PackageBuilders 13 | with FileBuilders { 14 | 15 | private val statementsById = mutable.Map[Int, Statement]() 16 | override def statements = statementsById.values 17 | def add(stmt: Statement): Unit = statementsById.put(stmt.id, stmt) 18 | 19 | private val ignoredStatementsById = mutable.Map[Int, Statement]() 20 | override def ignoredStatements = ignoredStatementsById.values 21 | def addIgnoredStatement(stmt: Statement): Unit = 22 | ignoredStatementsById.put(stmt.id, stmt) 23 | 24 | def avgClassesPerPackage = classCount / packageCount.toDouble 25 | def avgClassesPerPackageFormatted: String = DoubleFormat.twoFractionDigits( 26 | avgClassesPerPackage 27 | ) 28 | 29 | def avgMethodsPerClass = methodCount / classCount.toDouble 30 | def avgMethodsPerClassFormatted: String = DoubleFormat.twoFractionDigits( 31 | avgMethodsPerClass 32 | ) 33 | 34 | def loc = files.map(_.loc).sum 35 | def linesPerFile = loc / fileCount.toDouble 36 | def linesPerFileFormatted: String = 37 | DoubleFormat.twoFractionDigits(linesPerFile) 38 | 39 | // returns the classes by least coverage 40 | def risks(limit: Int) = classes.toSeq 41 | .sortBy(_.statementCount) 42 | .reverse 43 | .sortBy(_.statementCoverage) 44 | .take(limit) 45 | 46 | def apply(ids: Iterable[(Int, String)]): Unit = ids foreach invoked 47 | def invoked(id: (Int, String)): Unit = 48 | statementsById.get(id._1).foreach(_.invoked(id._2)) 49 | } 50 | 51 | trait ClassBuilders { 52 | def statements: Iterable[Statement] 53 | def classes = statements 54 | .groupBy(_.location.fullClassName) 55 | .map(arg => MeasuredClass(arg._1, arg._2)) 56 | def classCount: Int = classes.size 57 | } 58 | 59 | trait FileBuilders { 60 | def statements: Iterable[Statement] 61 | def files: Iterable[MeasuredFile] = 62 | statements.groupBy(_.source).map(arg => MeasuredFile(arg._1, arg._2)) 63 | def fileCount: Int = files.size 64 | } 65 | 66 | sealed trait ClassType 67 | object ClassType { 68 | case object Object extends ClassType 69 | case object Class extends ClassType 70 | case object Trait extends ClassType 71 | def fromString(str: String): ClassType = { 72 | str.toLowerCase match { 73 | case "object" => Object 74 | case "trait" => Trait 75 | case _ => Class 76 | } 77 | } 78 | } 79 | 80 | case class ClassRef(name: String) { 81 | lazy val simpleName = name.split(".").last 82 | lazy val getPackage = name.split(".").dropRight(1).mkString(".") 83 | } 84 | 85 | object ClassRef { 86 | def fromFilepath(path: String) = ClassRef(path.replace('/', '.')) 87 | def apply(_package: String, className: String): ClassRef = ClassRef( 88 | _package.replace('/', '.') + "." + className 89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /domain/src/test/scala/scoverage/domain/CoverageTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage.domain 2 | 3 | import munit.FunSuite 4 | 5 | /** @author Stephen Samuel */ 6 | class CoverageTest extends FunSuite { 7 | 8 | test("coverage for no statements is 1") { 9 | val coverage = Coverage() 10 | assertEquals(1.0, coverage.statementCoverage) 11 | } 12 | 13 | test("coverage for no invoked statements is 0") { 14 | val coverage = Coverage() 15 | coverage.add( 16 | Statement( 17 | Location("", "", "", ClassType.Object, "", ""), 18 | 1, 19 | 2, 20 | 3, 21 | 4, 22 | "", 23 | "", 24 | "", 25 | false, 26 | 0 27 | ) 28 | ) 29 | assertEquals(0.0, coverage.statementCoverage) 30 | } 31 | 32 | test("coverage for invoked statements") { 33 | val coverage = Coverage() 34 | coverage.add( 35 | Statement( 36 | Location("", "", "", ClassType.Object, "", ""), 37 | 1, 38 | 2, 39 | 3, 40 | 4, 41 | "", 42 | "", 43 | "", 44 | false, 45 | 3 46 | ) 47 | ) 48 | coverage.add( 49 | Statement( 50 | Location("", "", "", ClassType.Object, "", ""), 51 | 2, 52 | 2, 53 | 3, 54 | 4, 55 | "", 56 | "", 57 | "", 58 | false, 59 | 0 60 | ) 61 | ) 62 | coverage.add( 63 | Statement( 64 | Location("", "", "", ClassType.Object, "", ""), 65 | 3, 66 | 2, 67 | 3, 68 | 4, 69 | "", 70 | "", 71 | "", 72 | false, 73 | 0 74 | ) 75 | ) 76 | coverage.add( 77 | Statement( 78 | Location("", "", "", ClassType.Object, "", ""), 79 | 4, 80 | 2, 81 | 3, 82 | 4, 83 | "", 84 | "", 85 | "", 86 | false, 87 | 0 88 | ) 89 | ) 90 | assertEquals(0.25, coverage.statementCoverage) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /misc/logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scoverage/scalac-scoverage-plugin/e2e59ed9f625323eb6b196e7e936134bbaa9d5dd/misc/logo1.png -------------------------------------------------------------------------------- /misc/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scoverage/scalac-scoverage-plugin/e2e59ed9f625323eb6b196e7e936134bbaa9d5dd/misc/logo2.png -------------------------------------------------------------------------------- /misc/scales.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scoverage/scalac-scoverage-plugin/e2e59ed9f625323eb6b196e7e936134bbaa9d5dd/misc/scales.png -------------------------------------------------------------------------------- /misc/scales2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scoverage/scalac-scoverage-plugin/e2e59ed9f625323eb6b196e7e936134bbaa9d5dd/misc/scales2.png -------------------------------------------------------------------------------- /misc/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scoverage/scalac-scoverage-plugin/e2e59ed9f625323eb6b196e7e936134bbaa9d5dd/misc/screenshot1.png -------------------------------------------------------------------------------- /misc/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scoverage/scalac-scoverage-plugin/e2e59ed9f625323eb6b196e7e936134bbaa9d5dd/misc/screenshot2.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "jsdom": "^26.0.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /plugin/src/main/resources/scalac-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | scoverage 3 | scoverage.ScoveragePlugin 4 | -------------------------------------------------------------------------------- /plugin/src/main/scala/scoverage/CoverageFilter.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import scala.collection.mutable 4 | import scala.reflect.internal.util.Position 5 | import scala.reflect.internal.util.SourceFile 6 | import scala.tools.nsc.reporters.Reporter 7 | import scala.util.matching.Regex 8 | 9 | /** Methods related to filtering the instrumentation and coverage. 10 | * 11 | * @author Stephen Samuel 12 | */ 13 | trait CoverageFilter { 14 | def isClassIncluded(className: String): Boolean 15 | def isFileIncluded(file: SourceFile): Boolean 16 | def isLineIncluded(position: Position): Boolean 17 | def isSymbolIncluded(symbolName: String): Boolean 18 | def getExcludedLineNumbers(sourceFile: SourceFile): List[Range] 19 | } 20 | 21 | object AllCoverageFilter extends CoverageFilter { 22 | override def getExcludedLineNumbers(sourceFile: SourceFile): List[Range] = Nil 23 | override def isLineIncluded(position: Position): Boolean = true 24 | override def isClassIncluded(className: String): Boolean = true 25 | override def isFileIncluded(file: SourceFile): Boolean = true 26 | override def isSymbolIncluded(symbolName: String): Boolean = true 27 | } 28 | 29 | class RegexCoverageFilter( 30 | excludedPackages: Seq[String], 31 | excludedFiles: Seq[String], 32 | excludedSymbols: Seq[String], 33 | reporter: Reporter 34 | ) extends CoverageFilter { 35 | if (excludedPackages.nonEmpty) 36 | reporter.echo(s"scoverage excludedPackages: ${excludedPackages}") 37 | if (excludedFiles.nonEmpty) 38 | reporter.echo(s"scoverage excludedFiles: ${excludedFiles}") 39 | if (excludedSymbols.nonEmpty) 40 | reporter.echo(s"scoverage excludedSymbols: ${excludedSymbols}") 41 | 42 | val excludedClassNamePatterns = excludedPackages.map(_.r.pattern) 43 | val excludedFilePatterns = excludedFiles.map(_.r.pattern) 44 | val excludedSymbolPatterns = excludedSymbols.map(_.r.pattern) 45 | 46 | /** We cache the excluded ranges to avoid scanning the source code files 47 | * repeatedly. For a large project there might be a lot of source code 48 | * data, so we only hold a weak reference. 49 | */ 50 | val linesExcludedByScoverageCommentsCache 51 | : mutable.Map[SourceFile, List[Range]] = mutable.WeakHashMap.empty 52 | 53 | final val scoverageExclusionCommentsRegex = 54 | """(?ms)^\s*//\s*(\$COVERAGE-OFF\$).*?(^\s*//\s*\$COVERAGE-ON\$|\Z)""".r 55 | 56 | /** True if the given className has not been excluded by the 57 | * `excludedPackages` option. 58 | */ 59 | override def isClassIncluded(className: String): Boolean = { 60 | excludedClassNamePatterns.isEmpty || !excludedClassNamePatterns.exists( 61 | _.matcher(className).matches 62 | ) 63 | } 64 | 65 | override def isFileIncluded(file: SourceFile): Boolean = { 66 | def isFileMatch(file: SourceFile) = excludedFilePatterns.exists( 67 | _.matcher(file.path.replace(".scala", "")).matches 68 | ) 69 | excludedFilePatterns.isEmpty || !isFileMatch(file) 70 | } 71 | 72 | /** True if the line containing `position` has not been excluded by a magic comment. 73 | */ 74 | def isLineIncluded(position: Position): Boolean = { 75 | if (position.isDefined) { 76 | val excludedLineNumbers = getExcludedLineNumbers(position.source) 77 | val lineNumber = position.line 78 | !excludedLineNumbers.exists(_.contains(lineNumber)) 79 | } else { 80 | true 81 | } 82 | } 83 | 84 | override def isSymbolIncluded(symbolName: String): Boolean = { 85 | excludedSymbolPatterns.isEmpty || !excludedSymbolPatterns.exists( 86 | _.matcher(symbolName).matches 87 | ) 88 | } 89 | 90 | /** Provides overloads to paper over 2.12.13+ SourceFile incompatibility 91 | */ 92 | def compatFindAllIn( 93 | regexp: Regex, 94 | pattern: Array[Char] 95 | ): Regex.MatchIterator = regexp.findAllIn(new String(pattern)) 96 | def compatFindAllIn(regexp: Regex, pattern: String): Regex.MatchIterator = 97 | regexp.findAllIn(pattern) 98 | 99 | /** Checks the given sourceFile for any magic comments which exclude lines 100 | * from coverage. Returns a list of Ranges of lines that should be excluded. 101 | * 102 | * The line numbers returned are conventional 1-based line numbers (i.e. the 103 | * first line is line number 1) 104 | */ 105 | def getExcludedLineNumbers(sourceFile: SourceFile): List[Range] = { 106 | linesExcludedByScoverageCommentsCache.get(sourceFile) match { 107 | case Some(lineNumbers) => lineNumbers 108 | case None => 109 | val lineNumbers = compatFindAllIn( 110 | scoverageExclusionCommentsRegex, 111 | sourceFile.content 112 | ).matchData.map { m => 113 | // Asking a SourceFile for the line number of the char after 114 | // the end of the file gives an exception 115 | val endChar = math.min(m.end(2), sourceFile.content.length - 1) 116 | // Most of the compiler API appears to use conventional 117 | // 1-based line numbers (e.g. "Position.line"), but it appears 118 | // that the "offsetToLine" method in SourceFile uses 0-based 119 | // line numbers 120 | Range( 121 | 1 + sourceFile.offsetToLine(m.start(1)), 122 | 1 + sourceFile.offsetToLine(endChar) 123 | ) 124 | }.toList 125 | linesExcludedByScoverageCommentsCache.put(sourceFile, lineNumbers) 126 | lineNumbers 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /plugin/src/main/scala/scoverage/Location.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import scala.tools.nsc.Global 4 | 5 | import scoverage.domain.ClassType 6 | 7 | object Location { 8 | 9 | def fromGlobal(global: Global): global.Tree => Option[domain.Location] = { 10 | tree => 11 | def packageName(s: global.Symbol): String = { 12 | s.enclosingPackage.fullName 13 | } 14 | 15 | def className(s: global.Symbol): String = { 16 | // anon functions are enclosed in proper classes. 17 | if (s.enclClass.isAnonymousFunction || s.enclClass.isAnonymousClass) 18 | className(s.owner) 19 | else s.enclClass.nameString 20 | } 21 | 22 | def classType(s: global.Symbol): ClassType = { 23 | if (s.enclClass.isTrait) ClassType.Trait 24 | else if (s.enclClass.isModuleOrModuleClass) ClassType.Object 25 | else ClassType.Class 26 | } 27 | 28 | def fullClassName(s: global.Symbol): String = { 29 | // anon functions are enclosed in proper classes. 30 | if (s.enclClass.isAnonymousFunction || s.enclClass.isAnonymousClass) 31 | fullClassName(s.owner) 32 | else s.enclClass.fullNameString 33 | } 34 | 35 | def enclosingMethod(s: global.Symbol): String = { 36 | // check if we are in a proper method and return that, otherwise traverse up 37 | if (s.enclClass.isAnonymousFunction) enclosingMethod(s.owner) 38 | else if (s.enclMethod.isPrimaryConstructor) "" 39 | else Option(s.enclMethod.nameString).getOrElse("") 40 | } 41 | 42 | def sourcePath(symbol: global.Symbol): String = { 43 | Option(symbol.sourceFile).map(_.canonicalPath).getOrElse("") 44 | } 45 | 46 | Option(tree.symbol) map { symbol => 47 | domain.Location( 48 | packageName(symbol), 49 | className(symbol), 50 | fullClassName(symbol), 51 | classType(symbol), 52 | enclosingMethod(symbol), 53 | sourcePath(symbol) 54 | ) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /plugin/src/main/scala/scoverage/ScoverageOptions.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | /** Base options that can be passed into scoverage 4 | * 5 | * @param excludedPackages packages to be excluded in coverage 6 | * @param excludedFiles files to be excluded in coverage 7 | * @param excludedSymbols symbols to be excluded in coverage 8 | * @param dataDir the directory that the coverage files should be written to 9 | * @param reportTestName whether or not the test names should be reported 10 | * @param sourceRoot the source root of your project 11 | */ 12 | case class ScoverageOptions( 13 | excludedPackages: Seq[String], 14 | excludedFiles: Seq[String], 15 | excludedSymbols: Seq[String], 16 | dataDir: String, 17 | reportTestName: Boolean, 18 | sourceRoot: String 19 | ) 20 | 21 | object ScoverageOptions { 22 | 23 | private[scoverage] val help = Some( 24 | Seq( 25 | "-P:scoverage:dataDir: where the coverage files should be written\n", 26 | "-P:scoverage:sourceRoot: the root dir of your sources, used for path relativization\n", 27 | "-P:scoverage:excludedPackages:; semicolon separated list of regexs for packages to exclude", 28 | "-P:scoverage:excludedFiles:; semicolon separated list of regexs for paths to exclude", 29 | "-P:scoverage:excludedSymbols:; semicolon separated list of regexs for symbols to exclude", 30 | "-P:scoverage:extraAfterPhase: phase after which scoverage phase runs (must be after typer phase)", 31 | "-P:scoverage:extraBeforePhase: phase before which scoverage phase runs (must be before patmat phase)", 32 | " Any classes whose fully qualified name matches the regex will", 33 | " be excluded from coverage." 34 | ).mkString("\n") 35 | ) 36 | 37 | private def parseExclusionOption( 38 | inOption: String 39 | ): Seq[String] = 40 | inOption 41 | .split(";") 42 | .collect { 43 | case value if value.trim().nonEmpty => value.trim() 44 | } 45 | .toIndexedSeq 46 | 47 | private val ExcludedPackages = "excludedPackages:(.*)".r 48 | private val ExcludedFiles = "excludedFiles:(.*)".r 49 | private val ExcludedSymbols = "excludedSymbols:(.*)".r 50 | private val DataDir = "dataDir:(.*)".r 51 | private val SourceRoot = "sourceRoot:(.*)".r 52 | private val ExtraAfterPhase = "extraAfterPhase:(.*)".r 53 | private val ExtraBeforePhase = "extraBeforePhase:(.*)".r 54 | 55 | /** Default that is _only_ used for initializing purposes. dataDir and 56 | * sourceRoot are both just empty strings here, but we nevery actually 57 | * allow for this to be the case when the plugin runs, and this is checked 58 | * before it does. 59 | */ 60 | def default() = ScoverageOptions( 61 | excludedPackages = Seq.empty, 62 | excludedFiles = Seq.empty, 63 | excludedSymbols = Seq( 64 | "scala.reflect.api.Exprs.Expr", 65 | "scala.reflect.api.Trees.Tree", 66 | "scala.reflect.macros.Universe.Tree" 67 | ), 68 | dataDir = "", 69 | reportTestName = false, 70 | sourceRoot = "" 71 | ) 72 | 73 | def processPhaseOptions( 74 | opts: List[String] 75 | ): (Option[String], Option[String]) = { 76 | 77 | val afterPhase: Option[String] = 78 | opts.collectFirst { case ExtraAfterPhase(phase) => phase } 79 | val beforePhase: Option[String] = 80 | opts.collectFirst { case ExtraBeforePhase(phase) => phase } 81 | 82 | (afterPhase, beforePhase) 83 | } 84 | 85 | def parse( 86 | scalacOptions: List[String], 87 | errFn: String => Unit, 88 | base: ScoverageOptions 89 | ): ScoverageOptions = { 90 | 91 | var options = base 92 | 93 | scalacOptions.foreach { 94 | case ExcludedPackages(packages) => 95 | options = 96 | options.copy(excludedPackages = parseExclusionOption(packages)) 97 | case ExcludedFiles(files) => 98 | options = options.copy(excludedFiles = parseExclusionOption(files)) 99 | case ExcludedSymbols(symbols) => 100 | options = options.copy(excludedSymbols = parseExclusionOption(symbols)) 101 | case DataDir(dir) => 102 | options = options.copy(dataDir = dir) 103 | case SourceRoot(root) => options = options.copy(sourceRoot = root) 104 | // NOTE that both the extra phases are actually parsed out early on, so 105 | // we just ignore them here 106 | case ExtraAfterPhase(afterPhase) => () 107 | case ExtraBeforePhase(beforePhase) => () 108 | case "reportTestName" => 109 | options = options.copy(reportTestName = true) 110 | case opt => errFn("Unknown option: " + opt) 111 | } 112 | 113 | options 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /plugin/src/test/scala-2.11+/scoverage/macrosupport/TesterMacro.scala: -------------------------------------------------------------------------------- 1 | package scoverage.macrosupport 2 | 3 | import scala.reflect.macros.blackbox.Context 4 | 5 | private object TesterMacro { 6 | 7 | type TesterContext = Context { type PrefixType = Tester.type } 8 | 9 | def test(c: TesterContext) = { 10 | import c.universe._ 11 | q"""println("macro test")""" 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /plugin/src/test/scala-2.13+/scoverage/Scala213PluginCoverageTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import munit.FunSuite 4 | 5 | class Scala213PluginCoverageTest extends FunSuite with MacroSupport { 6 | 7 | test( 8 | "scoverage should ignore synthetic lazy definitions generated by compiler from by-name implicits" 9 | ) { 10 | val compiler = ScoverageCompiler.noPositionValidation 11 | compiler.compileCodeSnippet( 12 | """ 13 | |object test { 14 | | 15 | | trait Foo { 16 | | def next: Foo 17 | | } 18 | | 19 | | object Foo { 20 | | implicit def foo(implicit rec: => Foo): Foo = 21 | | new Foo { def next = rec } 22 | | } 23 | | 24 | | val foo = implicitly[Foo] 25 | | 26 | |} 27 | | 28 | """.stripMargin 29 | ) 30 | assert(!compiler.reporter.hasErrors) 31 | assert(!compiler.reporter.hasWarnings) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /plugin/src/test/scala/scoverage/LocationCompiler.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import java.io.File 4 | 5 | import scala.tools.nsc.Global 6 | import scala.tools.nsc.plugins.PluginComponent 7 | import scala.tools.nsc.transform.Transform 8 | import scala.tools.nsc.transform.TypingTransformers 9 | 10 | import scoverage.reporter.IOUtils 11 | 12 | private[scoverage] class LocationCompiler( 13 | settings: scala.tools.nsc.Settings, 14 | reporter: scala.tools.nsc.reporters.Reporter 15 | ) extends scala.tools.nsc.Global(settings, reporter) { 16 | 17 | val locations = List.newBuilder[(String, domain.Location)] 18 | private val locationSetter = new LocationSetter(this) 19 | 20 | def compile(code: String): Unit = { 21 | val files = writeCodeSnippetToTempFile(code) 22 | val command = 23 | new scala.tools.nsc.CompilerCommand(List(files.getAbsolutePath), settings) 24 | new Run().compile(command.files) 25 | } 26 | 27 | def writeCodeSnippetToTempFile(code: String): File = { 28 | val file = File.createTempFile("code_snippet", ".scala") 29 | IOUtils.writeToFile(file, code, None) 30 | file.deleteOnExit() 31 | file 32 | } 33 | 34 | class LocationSetter(val global: Global) 35 | extends PluginComponent 36 | with TypingTransformers 37 | with Transform { 38 | 39 | override val phaseName = "location-setter" 40 | override val runsAfter = List("typer") 41 | override val runsBefore = List("patmat") 42 | 43 | override protected def newTransformer( 44 | unit: global.CompilationUnit 45 | ): global.Transformer = new Transformer(unit) 46 | class Transformer(unit: global.CompilationUnit) 47 | extends TypingTransformer(unit) { 48 | 49 | override def transform(tree: global.Tree) = { 50 | for (location <- Location.fromGlobal(global)(tree)) { 51 | locations += (tree.getClass.getSimpleName -> location) 52 | } 53 | super.transform(tree) 54 | } 55 | } 56 | } 57 | 58 | override def computeInternalPhases(): Unit = { 59 | super.computeInternalPhases() 60 | addToPhasesSet(locationSetter, "sets locations") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /plugin/src/test/scala/scoverage/LocationTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import munit.FunSuite 4 | import scoverage.domain.ClassType 5 | 6 | class LocationTest extends FunSuite { 7 | 8 | test("top level for classes") { 9 | val compiler = ScoverageCompiler.locationCompiler 10 | compiler.compile("package com.test\nclass Sammy") 11 | val loc = compiler.locations.result().find(_._1 == "Template").get._2 12 | assertEquals(loc.packageName, "com.test") 13 | assertEquals(loc.className, "Sammy") 14 | assertEquals(loc.fullClassName, "com.test.Sammy") 15 | assertEquals(loc.method, "") 16 | assertEquals(loc.classType, ClassType.Class) 17 | assert(loc.sourcePath.endsWith(".scala")) 18 | } 19 | test("top level for objects") { 20 | val compiler = ScoverageCompiler.locationCompiler 21 | compiler.compile( 22 | "package com.test\nobject Bammy { def foo = Symbol(\"boo\") } " 23 | ) 24 | val loc = compiler.locations.result().find(_._1 == "Template").get._2 25 | assertEquals(loc.packageName, "com.test") 26 | assertEquals(loc.className, "Bammy") 27 | assertEquals(loc.fullClassName, "com.test.Bammy") 28 | assertEquals(loc.method, "") 29 | assertEquals(loc.classType, ClassType.Object) 30 | assert(loc.sourcePath.endsWith(".scala")) 31 | } 32 | test("top level for traits") { 33 | val compiler = ScoverageCompiler.locationCompiler 34 | compiler.compile( 35 | "package com.test\ntrait Gammy { def goo = Symbol(\"hoo\") } " 36 | ) 37 | val loc = compiler.locations.result().find(_._1 == "Template").get._2 38 | assertEquals(loc.packageName, "com.test") 39 | assertEquals(loc.className, "Gammy") 40 | assertEquals(loc.fullClassName, "com.test.Gammy") 41 | assertEquals(loc.method, "") 42 | assertEquals(loc.classType, ClassType.Trait) 43 | assert(loc.sourcePath.endsWith(".scala")) 44 | } 45 | test("should correctly process methods") { 46 | val compiler = ScoverageCompiler.locationCompiler 47 | compiler.compile( 48 | "package com.methodtest \n class Hammy { def foo = Symbol(\"boo\") } " 49 | ) 50 | val loc = compiler.locations.result().find(_._2.method == "foo").get._2 51 | assertEquals(loc.packageName, "com.methodtest") 52 | assertEquals(loc.className, "Hammy") 53 | assertEquals(loc.fullClassName, "com.methodtest.Hammy") 54 | assertEquals(loc.classType, ClassType.Class) 55 | assert(loc.sourcePath.endsWith(".scala")) 56 | } 57 | test("should correctly process nested methods") { 58 | val compiler = ScoverageCompiler.locationCompiler 59 | compiler.compile( 60 | "package com.methodtest \n class Hammy { def foo = { def goo = { getClass; 3 }; goo } } " 61 | ) 62 | val loc = compiler.locations.result().find(_._2.method == "goo").get._2 63 | assertEquals(loc.packageName, "com.methodtest") 64 | assertEquals(loc.className, "Hammy") 65 | assertEquals(loc.fullClassName, "com.methodtest.Hammy") 66 | assertEquals(loc.classType, ClassType.Class) 67 | assert(loc.sourcePath.endsWith(".scala")) 68 | } 69 | test("should process anon functions as inside the enclosing method") { 70 | val compiler = ScoverageCompiler.locationCompiler 71 | compiler.compile( 72 | "package com.methodtest \n class Jammy { def moo = { Option(\"bat\").map(_.length) } } " 73 | ) 74 | val loc = compiler.locations.result().find(_._1 == "Function").get._2 75 | assertEquals(loc.packageName, "com.methodtest") 76 | assertEquals(loc.className, "Jammy") 77 | assertEquals(loc.fullClassName, "com.methodtest.Jammy") 78 | assertEquals(loc.method, "moo") 79 | assertEquals(loc.classType, ClassType.Class) 80 | assert(loc.sourcePath.endsWith(".scala")) 81 | } 82 | test("should use outer package for nested classes") { 83 | val compiler = ScoverageCompiler.locationCompiler 84 | compiler.compile( 85 | "package com.methodtest \n class Jammy { class Pammy } " 86 | ) 87 | val loc = 88 | compiler.locations.result().find(_._2.className == "Pammy").get._2 89 | assertEquals(loc.packageName, "com.methodtest") 90 | assertEquals(loc.className, "Pammy") 91 | assertEquals(loc.fullClassName, "com.methodtest.Jammy.Pammy") 92 | assertEquals(loc.method, "") 93 | assertEquals(loc.classType, ClassType.Class) 94 | assert(loc.sourcePath.endsWith(".scala")) 95 | } 96 | test("for nested objects") { 97 | val compiler = ScoverageCompiler.locationCompiler 98 | compiler.compile( 99 | "package com.methodtest \n class Jammy { object Zammy } " 100 | ) 101 | val loc = 102 | compiler.locations.result().find(_._2.className == "Zammy").get._2 103 | assertEquals(loc.packageName, "com.methodtest") 104 | assertEquals(loc.className, "Zammy") 105 | assertEquals(loc.fullClassName, "com.methodtest.Jammy.Zammy") 106 | assertEquals(loc.method, "") 107 | assertEquals(loc.classType, ClassType.Object) 108 | assert(loc.sourcePath.endsWith(".scala")) 109 | } 110 | test("for nested traits") { 111 | val compiler = ScoverageCompiler.locationCompiler 112 | compiler.compile( 113 | "package com.methodtest \n class Jammy { trait Mammy } " 114 | ) 115 | val loc = 116 | compiler.locations.result().find(_._2.className == "Mammy").get._2 117 | assertEquals(loc.packageName, "com.methodtest") 118 | assertEquals(loc.className, "Mammy") 119 | assertEquals(loc.fullClassName, "com.methodtest.Jammy.Mammy") 120 | assertEquals(loc.method, "") 121 | assertEquals(loc.classType, ClassType.Trait) 122 | assert(loc.sourcePath.endsWith(".scala")) 123 | } 124 | test("should support nested packages for classes") { 125 | val compiler = ScoverageCompiler.locationCompiler 126 | compiler.compile( 127 | "package com.a \n " + 128 | "package b \n" + 129 | "class Kammy " 130 | ) 131 | val loc = compiler.locations.result().find(_._1 == "Template").get._2 132 | assertEquals(loc.packageName, "com.a.b") 133 | assertEquals(loc.className, "Kammy") 134 | assertEquals(loc.fullClassName, "com.a.b.Kammy") 135 | assertEquals(loc.method, "") 136 | assertEquals(loc.classType, ClassType.Class) 137 | assert(loc.sourcePath.endsWith(".scala")) 138 | } 139 | test("for objects") { 140 | val compiler = ScoverageCompiler.locationCompiler 141 | compiler.compile( 142 | "package com.a \n " + 143 | "package b \n" + 144 | "object Kammy " 145 | ) 146 | val loc = compiler.locations.result().find(_._1 == "Template").get._2 147 | assertEquals(loc.packageName, "com.a.b") 148 | assertEquals(loc.className, "Kammy") 149 | assertEquals(loc.fullClassName, "com.a.b.Kammy") 150 | assertEquals(loc.method, "") 151 | assertEquals(loc.classType, ClassType.Object) 152 | assert(loc.sourcePath.endsWith(".scala")) 153 | } 154 | test("for traits") { 155 | val compiler = ScoverageCompiler.locationCompiler 156 | compiler.compile( 157 | "package com.a \n " + 158 | "package b \n" + 159 | "trait Kammy " 160 | ) 161 | val loc = compiler.locations.result().find(_._1 == "Template").get._2 162 | assertEquals(loc.packageName, "com.a.b") 163 | assertEquals(loc.className, "Kammy") 164 | assertEquals(loc.fullClassName, "com.a.b.Kammy") 165 | assertEquals(loc.method, "") 166 | assertEquals(loc.classType, ClassType.Trait) 167 | assert(loc.sourcePath.endsWith(".scala")) 168 | } 169 | test("should use method name for class constructor body") { 170 | val compiler = ScoverageCompiler.locationCompiler 171 | compiler.compile( 172 | "package com.b \n class Tammy { val name = Symbol(\"sam\") } " 173 | ) 174 | val loc = compiler.locations.result().find(_._1 == "ValDef").get._2 175 | assertEquals(loc.packageName, "com.b") 176 | assertEquals(loc.className, "Tammy") 177 | assertEquals(loc.fullClassName, "com.b.Tammy") 178 | assertEquals(loc.method, "") 179 | assertEquals(loc.classType, ClassType.Class) 180 | assert(loc.sourcePath.endsWith(".scala")) 181 | } 182 | test("for object constructor body") { 183 | val compiler = ScoverageCompiler.locationCompiler 184 | compiler.compile( 185 | "package com.b \n object Yammy { val name = Symbol(\"sam\") } " 186 | ) 187 | val loc = compiler.locations.result().find(_._1 == "ValDef").get._2 188 | assertEquals(loc.packageName, "com.b") 189 | assertEquals(loc.className, "Yammy") 190 | assertEquals(loc.fullClassName, "com.b.Yammy") 191 | assertEquals(loc.method, "") 192 | assertEquals(loc.classType, ClassType.Object) 193 | assert(loc.sourcePath.endsWith(".scala")) 194 | } 195 | test("for trait constructor body") { 196 | val compiler = ScoverageCompiler.locationCompiler 197 | compiler.compile( 198 | "package com.b \n trait Wammy { val name = Symbol(\"sam\") } " 199 | ) 200 | val loc = compiler.locations.result().find(_._1 == "ValDef").get._2 201 | assertEquals(loc.packageName, "com.b") 202 | assertEquals(loc.className, "Wammy") 203 | assertEquals(loc.fullClassName, "com.b.Wammy") 204 | assertEquals(loc.method, "") 205 | assertEquals(loc.classType, ClassType.Trait) 206 | assert(loc.sourcePath.endsWith(".scala")) 207 | } 208 | test("anon class should report enclosing class") { 209 | val compiler = ScoverageCompiler.locationCompiler 210 | compiler 211 | .compile( 212 | "package com.a; object A { def foo(b : B) : Unit = b.invoke }; trait B { def invoke : Unit }; class C { A.foo(new B { def invoke = () }) }" 213 | ) 214 | val loc = compiler.locations.result().filter(_._1 == "Template").last._2 215 | assertEquals(loc.packageName, "com.a") 216 | assertEquals(loc.className, "C") 217 | assertEquals(loc.fullClassName, "com.a.C") 218 | assertEquals(loc.method, "") 219 | assertEquals(loc.classType, ClassType.Class) 220 | assert(loc.sourcePath.endsWith(".scala")) 221 | } 222 | test("anon class implemented method should report enclosing method") { 223 | val compiler = ScoverageCompiler.locationCompiler 224 | compiler.compile( 225 | "package com.a; object A { def foo(b : B) : Unit = b.invoke }; trait B { def invoke : Unit }; class C { A.foo(new B { def invoke = () }) }" 226 | ) 227 | val loc = compiler.locations.result().filter(_._1 == "DefDef").last._2 228 | assertEquals(loc.packageName, "com.a") 229 | assertEquals(loc.className, "C") 230 | assertEquals(loc.fullClassName, "com.a.C") 231 | assertEquals(loc.method, "invoke") 232 | assertEquals(loc.classType, ClassType.Class) 233 | assert(loc.sourcePath.endsWith(".scala")) 234 | } 235 | test("doubly nested classes should report correct fullClassName") { 236 | val compiler = ScoverageCompiler.locationCompiler 237 | compiler.compile( 238 | "package com.a \n object Foo { object Boo { object Moo { val name = Symbol(\"sam\") } } }" 239 | ) 240 | val loc = compiler.locations.result().find(_._1 == "ValDef").get._2 241 | assertEquals(loc.packageName, "com.a") 242 | assertEquals(loc.className, "Moo") 243 | assertEquals(loc.fullClassName, "com.a.Foo.Boo.Moo") 244 | assertEquals(loc.method, "") 245 | assertEquals(loc.classType, ClassType.Object) 246 | assert(loc.sourcePath.endsWith(".scala")) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /plugin/src/test/scala/scoverage/MacroSupport.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import java.io.File 4 | 5 | trait MacroSupport { 6 | 7 | val macroContextPackageName: String = 8 | if (ScoverageCompiler.ShortScalaVersion == "2.10") { 9 | "scala.reflect.macros" 10 | } else { 11 | "scala.reflect.macros.blackbox" 12 | } 13 | 14 | val macroSupportDeps = Seq(testClasses) 15 | 16 | private def testClasses: File = new File( 17 | s"./plugin/target/scala-${ScoverageCompiler.ScalaVersion}/test-classes" 18 | ) 19 | 20 | } 21 | -------------------------------------------------------------------------------- /plugin/src/test/scala/scoverage/PluginASTSupportTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import munit.FunSuite 4 | 5 | /** @author Stephen Samuel */ 6 | class PluginASTSupportTest extends FunSuite with MacroSupport { 7 | 8 | override def afterEach(context: AfterEach): Unit = { 9 | val compiler = ScoverageCompiler.default 10 | assert(!compiler.reporter.hasErrors) 11 | } 12 | 13 | // https://github.com/scoverage/sbt-scoverage/issues/203 14 | test("should support final val literals in traits") { 15 | val compiler = ScoverageCompiler.default 16 | compiler.compileCodeSnippet(""" 17 | |trait TraitWithFinalVal { 18 | | final val FOO = "Bar" 19 | |} """.stripMargin) 20 | compiler.assertNoErrors() 21 | compiler.assertNMeasuredStatements(0) 22 | } 23 | 24 | test("should support final val literals in objects") { 25 | val compiler = ScoverageCompiler.default 26 | compiler.compileCodeSnippet(""" 27 | |object TraitWithFinalVal { 28 | | final val FOO = "Bar" 29 | |} """.stripMargin) 30 | compiler.assertNoErrors() 31 | compiler.assertNMeasuredStatements(0) 32 | } 33 | 34 | test("should support final val literals in classes") { 35 | val compiler = ScoverageCompiler.default 36 | compiler.compileCodeSnippet(""" 37 | |class TraitWithFinalVal { 38 | | final val FOO = "Bar" 39 | |} """.stripMargin) 40 | compiler.assertNoErrors() 41 | compiler.assertNMeasuredStatements(0) 42 | } 43 | 44 | test("should support final val blocks in traits") { 45 | val compiler = ScoverageCompiler.default 46 | compiler.compileCodeSnippet(""" 47 | |trait TraitWithFinalVal { 48 | | final val FOO = { println("boo"); "Bar" } 49 | |} """.stripMargin) 50 | compiler.assertNoErrors() 51 | compiler.assertNMeasuredStatements(2) 52 | } 53 | 54 | test("should support final val blocks in objects") { 55 | val compiler = ScoverageCompiler.default 56 | compiler.compileCodeSnippet(""" 57 | |object TraitWithFinalVal { 58 | | final val FOO = { println("boo"); "Bar" } 59 | |} """.stripMargin) 60 | compiler.assertNoErrors() 61 | compiler.assertNMeasuredStatements(2) 62 | } 63 | 64 | test("should support final val blocks in classes") { 65 | val compiler = ScoverageCompiler.default 66 | compiler.compileCodeSnippet(""" 67 | |class TraitWithFinalVal { 68 | | final val FOO = { println("boo"); "Bar" } 69 | |} """.stripMargin) 70 | compiler.assertNoErrors() 71 | compiler.assertNMeasuredStatements(2) 72 | } 73 | 74 | test("scoverage component should ignore basic macros") { 75 | val compiler = ScoverageCompiler.default 76 | compiler.compileCodeSnippet(s""" 77 | | object MyMacro { 78 | | import scala.language.experimental.macros 79 | | import ${macroContextPackageName}.Context 80 | | def test: Unit = macro testImpl 81 | | def testImpl(c: Context): c.Expr[Unit] = { 82 | | import c.universe._ 83 | | reify { 84 | | println("macro test") 85 | | } 86 | | } 87 | |} """.stripMargin) 88 | assert(!compiler.reporter.hasErrors) 89 | } 90 | 91 | test("scoverage component should ignore complex macros #11") { 92 | val compiler = ScoverageCompiler.default 93 | compiler.compileCodeSnippet( 94 | s""" object ComplexMacro { 95 | | 96 | | import scala.language.experimental.macros 97 | | import ${macroContextPackageName}.Context 98 | | 99 | | def debug(params: Any*): Unit = macro debugImpl 100 | | 101 | | def debugImpl(c: Context)(params: c.Expr[Any]*) = { 102 | | import c.universe._ 103 | | 104 | | val trees = params map {param => (param.tree match { 105 | | case Literal(Constant(_)) => reify { print(param.splice) } 106 | | case _ => reify { 107 | | val variable = c.Expr[String](Literal(Constant(show(param.tree)))).splice 108 | | print(s"$$variable = $${param.splice}") 109 | | } 110 | | }).tree 111 | | } 112 | | 113 | | val separators = (1 until trees.size).map(_ => (reify { print(", ") }).tree) :+ (reify { println() }).tree 114 | | val treesWithSeparators = trees zip separators flatMap {p => List(p._1, p._2)} 115 | | 116 | | c.Expr[Unit](Block(treesWithSeparators.toList, Literal(Constant(())))) 117 | | } 118 | |} """.stripMargin 119 | ) 120 | assert(!compiler.reporter.hasErrors) 121 | } 122 | 123 | // https://github.com/scoverage/scalac-scoverage-plugin/issues/32 124 | test("exhaustive warnings should not be generated for @unchecked") { 125 | val compiler = ScoverageCompiler.default 126 | compiler.compileCodeSnippet( 127 | """object PartialMatchObject { 128 | | def partialMatchExample(s: Option[String]): Unit = { 129 | | (s: @unchecked) match { 130 | | case Some(str) => println(str) 131 | | } 132 | | } 133 | |} """.stripMargin 134 | ) 135 | assert(!compiler.reporter.hasErrors) 136 | assert(!compiler.reporter.hasWarnings) 137 | } 138 | 139 | // https://github.com/skinny-framework/skinny-framework/issues/97 140 | test("macro range positions should not break plugin".ignore) { 141 | val compiler = ScoverageCompiler.default 142 | macroSupportDeps.foreach(compiler.addToClassPath(_)) 143 | compiler.compileCodeSnippet(s"""import scoverage.macrosupport.Tester 144 | | 145 | |object MacroTest { 146 | | Tester.test 147 | |} """.stripMargin) 148 | assert(!compiler.reporter.hasErrors) 149 | assert(!compiler.reporter.hasWarnings) 150 | } 151 | 152 | // https://github.com/scoverage/scalac-scoverage-plugin/issues/45 153 | test("compile final vals in annotations") { 154 | val compiler = ScoverageCompiler.default 155 | compiler.compileCodeSnippet("""object Foo { 156 | | final val foo = 1L 157 | |} 158 | |@SerialVersionUID(Foo.foo) 159 | |case class Bar() 160 | |""".stripMargin) 161 | assert(!compiler.reporter.hasErrors) 162 | assert(!compiler.reporter.hasWarnings) 163 | } 164 | 165 | test("type param with default arg supported") { 166 | val compiler = ScoverageCompiler.default 167 | compiler.compileCodeSnippet( 168 | """class TypeTreeObjects { 169 | | class Container { 170 | | def typeParamAndDefaultArg[C](name: String = "sammy"): String = name 171 | | } 172 | | new Container().typeParamAndDefaultArg[Any]() 173 | |} """.stripMargin 174 | ) 175 | assert(!compiler.reporter.hasErrors) 176 | assert(!compiler.reporter.hasWarnings) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /plugin/src/test/scala/scoverage/PluginCoverageScalaJsTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import munit.FunSuite 4 | 5 | /** https://github.com/scoverage/scalac-scoverage-plugin/issues/196 6 | */ 7 | class PluginCoverageScalaJsTest extends FunSuite with MacroSupport { 8 | 9 | test("scoverage should ignore default undefined parameter") { 10 | val compiler = ScoverageCompiler.defaultJS 11 | compiler.compileCodeSnippet( 12 | """import scala.scalajs.js 13 | | 14 | |object JSONHelper { 15 | | def toJson(value: String): String = js.JSON.stringify(value) 16 | |}""".stripMargin 17 | ) 18 | assert(!compiler.reporter.hasErrors) 19 | compiler.assertNMeasuredStatements(2) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /plugin/src/test/scala/scoverage/PluginCoverageTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import munit.FunSuite 4 | 5 | /** @author Stephen Samuel */ 6 | class PluginCoverageTest extends FunSuite with MacroSupport { 7 | 8 | test("scoverage should instrument default arguments with methods") { 9 | val compiler = ScoverageCompiler.default 10 | compiler.compileCodeSnippet( 11 | """ object DefaultArgumentsObject { 12 | | val defaultName = "world" 13 | | def makeGreeting(name: String = defaultName): String = { 14 | | "Hello, " + name 15 | | } 16 | |} """.stripMargin 17 | ) 18 | assert(!compiler.reporter.hasErrors) 19 | // we expect: 20 | // instrumenting the default-param which becomes a method call invocation 21 | // the method makeGreeting is entered. 22 | compiler.assertNMeasuredStatements(2) 23 | } 24 | 25 | test("scoverage should skip macros") { 26 | val compiler = ScoverageCompiler.default 27 | val code = 28 | if (ScoverageCompiler.ShortScalaVersion == "2.10") 29 | """ 30 | import scala.language.experimental.macros 31 | import scala.reflect.macros.Context 32 | object Impl { 33 | def poly[T: c.WeakTypeTag](c: Context) = c.literal(c.weakTypeOf[T].toString) 34 | } 35 | 36 | object Macros { 37 | def poly[T]: String = macro Impl.poly[T] 38 | }""" 39 | else 40 | s""" 41 | import scala.language.experimental.macros 42 | import scala.reflect.macros.blackbox.Context 43 | class Impl(val c: Context) { 44 | import c.universe._ 45 | def poly[T: c.WeakTypeTag] = q"$${c.weakTypeOf[T].toString}" 46 | } 47 | object Macros { 48 | def poly[T]: String = macro Impl.poly[T] 49 | }""" 50 | compiler.compileCodeSnippet(code) 51 | assert(!compiler.reporter.hasErrors) 52 | compiler.assertNMeasuredStatements(0) 53 | } 54 | 55 | test("scoverage should instrument final vals") { 56 | val compiler = ScoverageCompiler.default 57 | compiler.compileCodeSnippet(""" object FinalVals { 58 | | final val name = { 59 | | val name = "sammy" 60 | | if (System.currentTimeMillis() > 0) { 61 | | println(name) 62 | | } 63 | | } 64 | | println(name) 65 | |} """.stripMargin) 66 | assert(!compiler.reporter.hasErrors) 67 | // we should have 3 statements - initialising the val, executing println, and executing the parameter 68 | compiler.assertNMeasuredStatements(8) 69 | } 70 | 71 | test("scoverage should not instrument the match as a statement") { 72 | val compiler = ScoverageCompiler.default 73 | compiler.compileCodeSnippet(""" object A { 74 | | System.currentTimeMillis() match { 75 | | case x => println(x) 76 | | } 77 | |} """.stripMargin) 78 | assert(!compiler.reporter.hasErrors) 79 | assert(!compiler.reporter.hasWarnings) 80 | 81 | /** should have the following statements instrumented: 82 | * the selector, clause/skip 1 83 | */ 84 | compiler.assertNMeasuredStatements(3) 85 | } 86 | test("scoverage should instrument match guards") { 87 | val compiler = ScoverageCompiler.default 88 | compiler.compileCodeSnippet(""" object A { 89 | | System.currentTimeMillis() match { 90 | | case l if l < 1000 => println("a") 91 | | case l if l > 1000 => println("b") 92 | | case _ => println("c") 93 | | } 94 | |} """.stripMargin) 95 | assert(!compiler.reporter.hasErrors) 96 | assert(!compiler.reporter.hasWarnings) 97 | 98 | /** should have the following statements instrumented: 99 | * the selector, guard 1, clause 1, guard 2, clause 2, clause 3 100 | */ 101 | compiler.assertNMeasuredStatements(9) 102 | } 103 | 104 | test("scoverage should instrument non basic selector") { 105 | val compiler = ScoverageCompiler.default 106 | compiler.compileCodeSnippet(""" trait A { 107 | | def someValue = "sammy" 108 | | def foo(a:String) = someValue match { 109 | | case any => "yes" 110 | | } 111 | |} """.stripMargin) 112 | assert(!compiler.reporter.hasErrors) 113 | // should instrument: 114 | // the someValue method entry 115 | // the selector call 116 | // case block "yes" literal 117 | // skip case block 118 | compiler.assertNMeasuredStatements(4) 119 | } 120 | 121 | test("scoverage should instrument conditional selectors in a match") { 122 | val compiler = ScoverageCompiler.default 123 | compiler.compileCodeSnippet( 124 | """ trait A { 125 | | def foo(a:String) = (if (a == "hello") 1 else 2) match { 126 | | case any => "yes" 127 | | } 128 | |} """.stripMargin 129 | ) 130 | assert(!compiler.reporter.hasErrors) 131 | // should instrument: 132 | // the if clause, 133 | // then block, 134 | // then literal "1", 135 | // else block, 136 | // else literal "2", 137 | // case block "yes" literal 138 | // skip case block "yes" literal 139 | compiler.assertNMeasuredStatements(7) 140 | } 141 | 142 | test( 143 | "scoverage should instrument anonymous function with pattern matching body" 144 | ) { 145 | val compiler = ScoverageCompiler.default 146 | compiler.compileCodeSnippet( 147 | """ object A { 148 | | def foo(a: List[Option[Int]]) = a.map { 149 | | case Some(value) => value + 1 150 | | case None => 0 151 | | } 152 | |} """.stripMargin 153 | ) 154 | assert(!compiler.reporter.hasErrors) 155 | // should instrument: 156 | // the def method entry, 157 | // case Some, 158 | // case block expression 159 | // case none, 160 | // case block literal "0" 161 | 162 | // account for canbuildfrom statement 163 | val expectedStatementsCount = 164 | if (ScoverageCompiler.ShortScalaVersion < "2.13") 6 else 5 165 | compiler.assertNMeasuredStatements(expectedStatementsCount) 166 | } 167 | 168 | // https://github.com/scoverage/sbt-scoverage/issues/16 169 | test( 170 | "scoverage should instrument for-loops but not the generated scaffolding" 171 | ) { 172 | val compiler = ScoverageCompiler.default 173 | compiler.compileCodeSnippet( 174 | """ trait A { 175 | | def print1(list: List[String]) = for (string: String <- list) println(string) 176 | |} """.stripMargin 177 | ) 178 | assert(!compiler.reporter.hasErrors) 179 | assert(!compiler.reporter.hasWarnings) 180 | // should instrument: 181 | // the def method entry 182 | // foreach body 183 | // specifically we want to avoid the withFilter partial function added by the compiler 184 | compiler.assertNMeasuredStatements(2) 185 | } 186 | 187 | // We ignore here becuase we end up getting an error in the compiler. 188 | // ``` 189 | // scala.reflect.internal.Positions$ValidateException: Enclosing tree [165] does not include tree [160] 190 | // ``` 191 | // When you do have this code it doesn't seem to actually impact the coverage data that is generated 192 | // so we just made note of this and ignored it. You can see more of the conversation in: 193 | // https://github.com/scoverage/scalac-scoverage-plugin/pull/641 194 | test("scoverage should instrument for-loop guards".ignore) { 195 | val compiler = ScoverageCompiler.default 196 | 197 | compiler.compileCodeSnippet( 198 | """object A { 199 | | def foo(list: List[String]) = for (string: String <- list if string.length > 5) 200 | | println(string) 201 | |} """.stripMargin 202 | ) 203 | assert(!compiler.reporter.hasErrors) 204 | assert(!compiler.reporter.hasWarnings) 205 | // should instrument: 206 | // foreach body 207 | // the guard 208 | // but we want to avoid the withFilter partial function added by the compiler 209 | compiler.assertNMeasuredStatements(3) 210 | } 211 | 212 | test( 213 | "scoverage should correctly handle new with args (apply with list of args)" 214 | ) { 215 | val compiler = ScoverageCompiler.default 216 | compiler.compileCodeSnippet(""" object A { 217 | | new String(new String(new String)) 218 | | } """.stripMargin) 219 | assert(!compiler.reporter.hasErrors) 220 | assert(!compiler.reporter.hasWarnings) 221 | // should have 3 statements, one for each of the nested strings 222 | compiler.assertNMeasuredStatements(3) 223 | } 224 | 225 | test( 226 | "scoverage should correctly handle no args new (apply, empty list of args)" 227 | ) { 228 | val compiler = ScoverageCompiler.default 229 | compiler.compileCodeSnippet(""" object A { 230 | | new String 231 | | } """.stripMargin) 232 | assert(!compiler.reporter.hasErrors) 233 | assert(!compiler.reporter.hasWarnings) 234 | // should have 1. the apply that wraps the select. 235 | compiler.assertNMeasuredStatements(1) 236 | } 237 | 238 | test("scoverage should correctly handle new that invokes nested statements") { 239 | val compiler = ScoverageCompiler.default 240 | compiler.compileCodeSnippet( 241 | """ 242 | | object A { 243 | | val value = new java.util.concurrent.CountDownLatch(if (System.currentTimeMillis > 1) 5 else 10) 244 | | } """.stripMargin 245 | ) 246 | assert(!compiler.reporter.hasErrors) 247 | assert(!compiler.reporter.hasWarnings) 248 | // should have 6 statements - the apply/new statement, two literals, the if cond, if elsep, if thenp 249 | compiler.assertNMeasuredStatements(6) 250 | } 251 | 252 | test("scoverage should instrument val RHS") { 253 | val compiler = ScoverageCompiler.default 254 | compiler.compileCodeSnippet("""object A { 255 | | val name = BigDecimal(50.0) 256 | |} """.stripMargin) 257 | assert(!compiler.reporter.hasErrors) 258 | assert(!compiler.reporter.hasWarnings) 259 | compiler.assertNMeasuredStatements(1) 260 | } 261 | 262 | test("scoverage should not instrument function tuple wrapping") { 263 | val compiler = ScoverageCompiler.default 264 | compiler.compileCodeSnippet( 265 | """ 266 | | sealed trait Foo 267 | | case class Bar(s: String) extends Foo 268 | | case object Baz extends Foo 269 | | 270 | | object Foo { 271 | | implicit val fooOrdering: Ordering[Foo] = Ordering.fromLessThan { 272 | | case (Bar(_), Baz) => true 273 | | case (Bar(a), Bar(b)) => a < b 274 | | case (_, _) => false 275 | | } 276 | | } 277 | """.stripMargin 278 | ) 279 | 280 | assert(!compiler.reporter.hasErrors) 281 | assert(!compiler.reporter.hasWarnings) 282 | // should have 7 profiled statements: the outer apply, and three pairs of case patterns & blocks 283 | // we are testing that we don't instrument the tuple2 call used here 284 | compiler.assertNMeasuredStatements(7) 285 | } 286 | 287 | test("scoverage should instrument all case statements in an explicit match") { 288 | val compiler = ScoverageCompiler.default 289 | compiler.compileCodeSnippet(""" trait A { 290 | | def foo(name: Any) = name match { 291 | | case i : Int => 1 292 | | case b : Boolean => println("boo") 293 | | case _ => 3 294 | | } 295 | |} """.stripMargin) 296 | assert(!compiler.reporter.hasErrors) 297 | assert(!compiler.reporter.hasWarnings) 298 | // should have one statement for each case body 299 | // and one statement for each case skipped 300 | // selector is a constant so would be ignored. 301 | compiler.assertNMeasuredStatements(6) 302 | } 303 | 304 | test("plugin should support yields") { 305 | val compiler = ScoverageCompiler.default 306 | compiler.compileCodeSnippet( 307 | """ 308 | | object Yielder { 309 | | val holidays = for ( name <- Seq("sammy", "clint", "lee"); 310 | | place <- Seq("london", "philly", "iowa") ) yield { 311 | | name + " has been to " + place 312 | | } 313 | | }""".stripMargin 314 | ) 315 | assert(!compiler.reporter.hasErrors) 316 | // 2 statements for the two applies in Seq, one for each literal which is 6, one for the operation passed to yield. 317 | // Depending on the collections api version, there can be additional implicit canBuildFrom statements. 318 | val expectedStatementsCount = 319 | if (ScoverageCompiler.ShortScalaVersion < "2.13") 11 else 9 320 | compiler.assertNMeasuredStatements(expectedStatementsCount) 321 | } 322 | 323 | test("plugin should not instrument local macro implementation") { 324 | val compiler = ScoverageCompiler.default 325 | compiler.compileCodeSnippet(s""" 326 | | object MyMacro { 327 | | import scala.language.experimental.macros 328 | | import ${macroContextPackageName}.Context 329 | | def test: Unit = macro testImpl 330 | | def testImpl(c: Context): c.Expr[Unit] = { 331 | | import c.universe._ 332 | | reify { 333 | | println("macro test") 334 | | } 335 | | } 336 | |} """.stripMargin) 337 | assert(!compiler.reporter.hasErrors) 338 | compiler.assertNoCoverage() 339 | } 340 | 341 | test( 342 | "plugin should not instrument expanded macro code http://github.com/skinny-framework/skinny-framework/issues/97".ignore 343 | ) { 344 | val compiler = ScoverageCompiler.default 345 | macroSupportDeps.foreach(compiler.addToClassPath(_)) 346 | compiler.compileCodeSnippet(s"""import scoverage.macrosupport.Tester 347 | | 348 | |class MacroTest { 349 | | Tester.test 350 | |} """.stripMargin) 351 | assert(!compiler.reporter.hasErrors) 352 | assert(!compiler.reporter.hasWarnings) 353 | compiler.assertNoCoverage() 354 | } 355 | 356 | test( 357 | "plugin should handle return inside catch github.com/scoverage/scalac-scoverage-plugin/issues/93".ignore 358 | ) { 359 | val compiler = ScoverageCompiler.default 360 | compiler.compileCodeSnippet( 361 | """ 362 | | object bob { 363 | | def fail(): Boolean = { 364 | | try { 365 | | true 366 | | } catch { 367 | | case _: Throwable => 368 | | Option(true) match { 369 | | case Some(bool) => return recover(bool) // comment this return and instrumentation succeeds 370 | | case _ => 371 | | } 372 | | false 373 | | } 374 | | } 375 | | def recover(it: Boolean): Boolean = it 376 | | } 377 | """.stripMargin 378 | ) 379 | assert(!compiler.reporter.hasErrors) 380 | assert(!compiler.reporter.hasWarnings) 381 | compiler.assertNMeasuredStatements(11) 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /plugin/src/test/scala/scoverage/RegexCoverageFilterTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import scala.reflect.internal.util.BatchSourceFile 4 | import scala.reflect.internal.util.NoFile 5 | import scala.reflect.internal.util.SourceFile 6 | import scala.reflect.io.VirtualFile 7 | import scala.tools.nsc.Settings 8 | import scala.tools.nsc.reporters.ConsoleReporter 9 | 10 | import munit.FunSuite 11 | 12 | class RegexCoverageFilterTest extends FunSuite { 13 | 14 | val reporter = new ConsoleReporter(new Settings()) 15 | 16 | test("isClassIncluded should return true for empty excludes") { 17 | assert( 18 | new RegexCoverageFilter(Nil, Nil, Nil, reporter).isClassIncluded("x") 19 | ) 20 | } 21 | 22 | test("should not crash for empty input") { 23 | assert(new RegexCoverageFilter(Nil, Nil, Nil, reporter).isClassIncluded("")) 24 | } 25 | 26 | test("should exclude scoverage -> scoverage") { 27 | assert( 28 | !new RegexCoverageFilter(Seq("scoverage"), Nil, Nil, reporter) 29 | .isClassIncluded("scoverage") 30 | ) 31 | } 32 | 33 | test("should include scoverage -> scoverageeee") { 34 | assert( 35 | new RegexCoverageFilter(Seq("scoverage"), Nil, Nil, reporter) 36 | .isClassIncluded("scoverageeee") 37 | ) 38 | } 39 | 40 | test("should exclude scoverage* -> scoverageeee") { 41 | assert( 42 | !new RegexCoverageFilter(Seq("scoverage*"), Nil, Nil, reporter) 43 | .isClassIncluded("scoverageeee") 44 | ) 45 | } 46 | 47 | test("should include eee -> scoverageeee") { 48 | assert( 49 | new RegexCoverageFilter(Seq("eee"), Nil, Nil, reporter) 50 | .isClassIncluded("scoverageeee") 51 | ) 52 | } 53 | 54 | test("should exclude .*eee -> scoverageeee") { 55 | assert( 56 | !new RegexCoverageFilter(Seq(".*eee"), Nil, Nil, reporter) 57 | .isClassIncluded("scoverageeee") 58 | ) 59 | } 60 | 61 | val abstractFile = new VirtualFile("sammy.scala") 62 | 63 | test("isFileIncluded should return true for empty excludes") { 64 | val file = new BatchSourceFile(abstractFile, Array.emptyCharArray) 65 | assert( 66 | new RegexCoverageFilter(Nil, Nil, Nil, reporter).isFileIncluded(file) 67 | ) 68 | } 69 | 70 | test("should exclude by filename") { 71 | val file = new BatchSourceFile(abstractFile, Array.emptyCharArray) 72 | assert( 73 | !new RegexCoverageFilter(Nil, Seq("sammy"), Nil, reporter) 74 | .isFileIncluded(file) 75 | ) 76 | } 77 | 78 | test("should exclude by regex wildcard") { 79 | val file = new BatchSourceFile(abstractFile, Array.emptyCharArray) 80 | assert( 81 | !new RegexCoverageFilter(Nil, Seq("sam.*"), Nil, reporter) 82 | .isFileIncluded(file) 83 | ) 84 | } 85 | 86 | test("should not exclude non matching regex") { 87 | val file = new BatchSourceFile(abstractFile, Array.emptyCharArray) 88 | assert( 89 | new RegexCoverageFilter(Nil, Seq("qweqeqwe"), Nil, reporter) 90 | .isFileIncluded(file) 91 | ) 92 | } 93 | 94 | val options = ScoverageOptions.default() 95 | 96 | test("isSymbolIncluded should return true for empty excludes") { 97 | assert( 98 | new RegexCoverageFilter(Nil, Nil, Nil, reporter).isSymbolIncluded("x") 99 | ) 100 | } 101 | 102 | test("should not crash for empty input") { 103 | assert( 104 | new RegexCoverageFilter(Nil, Nil, Nil, reporter).isSymbolIncluded("") 105 | ) 106 | } 107 | 108 | test("should exclude scoverage -> scoverage") { 109 | assert( 110 | !new RegexCoverageFilter(Nil, Nil, Seq("scoverage"), reporter) 111 | .isSymbolIncluded("scoverage") 112 | ) 113 | } 114 | 115 | test("should include scoverage -> scoverageeee") { 116 | assert( 117 | new RegexCoverageFilter(Nil, Nil, Seq("scoverage"), reporter) 118 | .isSymbolIncluded("scoverageeee") 119 | ) 120 | } 121 | test("should exclude scoverage* -> scoverageeee") { 122 | assert( 123 | !new RegexCoverageFilter(Nil, Nil, Seq("scoverage*"), reporter) 124 | .isSymbolIncluded("scoverageeee") 125 | ) 126 | } 127 | 128 | test("should include eee -> scoverageeee") { 129 | assert( 130 | new RegexCoverageFilter(Nil, Nil, Seq("eee"), reporter) 131 | .isSymbolIncluded("scoverageeee") 132 | ) 133 | } 134 | 135 | test("should exclude .*eee -> scoverageeee") { 136 | assert( 137 | !new RegexCoverageFilter(Nil, Nil, Seq(".*eee"), reporter) 138 | .isSymbolIncluded("scoverageeee") 139 | ) 140 | } 141 | test("should exclude scala.reflect.api.Exprs.Expr") { 142 | assert( 143 | !new RegexCoverageFilter(Nil, Nil, options.excludedSymbols, reporter) 144 | .isSymbolIncluded("scala.reflect.api.Exprs.Expr") 145 | ) 146 | } 147 | test("should exclude scala.reflect.macros.Universe.Tree") { 148 | assert( 149 | !new RegexCoverageFilter(Nil, Nil, options.excludedSymbols, reporter) 150 | .isSymbolIncluded("scala.reflect.macros.Universe.Tree") 151 | ) 152 | } 153 | test("should exclude scala.reflect.api.Trees.Tree") { 154 | assert( 155 | !new RegexCoverageFilter(Nil, Nil, options.excludedSymbols, reporter) 156 | .isSymbolIncluded("scala.reflect.api.Trees.Tree") 157 | ) 158 | } 159 | test( 160 | "getExcludedLineNumbers should exclude no lines if no magic comments are found" 161 | ) { 162 | val file = 163 | """1 164 | |2 165 | |3 166 | |4 167 | |5 168 | |6 169 | |7 170 | |8 171 | """.stripMargin 172 | 173 | val numbers = new RegexCoverageFilter(Nil, Nil, Nil, reporter) 174 | .getExcludedLineNumbers(mockSourceFile(file)) 175 | assertEquals(numbers, List.empty) 176 | } 177 | test("should exclude lines between magic comments") { 178 | val file = 179 | """1 180 | |2 181 | |3 182 | | // $COVERAGE-OFF$ 183 | |5 184 | |6 185 | |7 186 | |8 187 | | // $COVERAGE-ON$ 188 | |10 189 | |11 190 | | // $COVERAGE-OFF$ 191 | |13 192 | | // $COVERAGE-ON$ 193 | |15 194 | |16 195 | """.stripMargin 196 | 197 | val numbers = new RegexCoverageFilter(Nil, Nil, Nil, reporter) 198 | .getExcludedLineNumbers(mockSourceFile(file)) 199 | assertEquals(numbers, List(Range(4, 9), Range(12, 14))) 200 | } 201 | test("should exclude all lines after an upaired magic comment") { 202 | val file = 203 | """1 204 | |2 205 | |3 206 | | // $COVERAGE-OFF$ 207 | |5 208 | |6 209 | |7 210 | |8 211 | | // $COVERAGE-ON$ 212 | |10 213 | |11 214 | | // $COVERAGE-OFF$ 215 | |13 216 | |14 217 | |15 218 | """.stripMargin 219 | 220 | val numbers = new RegexCoverageFilter(Nil, Nil, Nil, reporter) 221 | .getExcludedLineNumbers(mockSourceFile(file)) 222 | assertEquals(numbers, List(Range(4, 9), Range(12, 16))) 223 | } 224 | test("should allow text comments on the same line as the markers") { 225 | val file = 226 | """1 227 | |2 228 | |3 229 | | // $COVERAGE-OFF$ because the next lines are boring 230 | |5 231 | |6 232 | |7 233 | |8 234 | | // $COVERAGE-ON$ resume coverage here 235 | |10 236 | |11 237 | | // $COVERAGE-OFF$ but ignore this bit 238 | |13 239 | |14 240 | |15 241 | """.stripMargin 242 | 243 | val numbers = new RegexCoverageFilter(Nil, Nil, Nil, reporter) 244 | .getExcludedLineNumbers(mockSourceFile(file)) 245 | assertEquals(numbers, List(Range(4, 9), Range(12, 16))) 246 | } 247 | 248 | private def mockSourceFile(contents: String): SourceFile = { 249 | new BatchSourceFile(NoFile, contents.toCharArray) 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /plugin/src/test/scala/scoverage/ScoverageCompiler.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import java.io.File 4 | import java.io.FileNotFoundException 5 | import java.net.URL 6 | 7 | import scala.collection.mutable.ListBuffer 8 | import scala.tools.nsc.Global 9 | import scala.tools.nsc.Settings 10 | import scala.tools.nsc.plugins.PluginComponent 11 | import scala.tools.nsc.transform.Transform 12 | import scala.tools.nsc.transform.TypingTransformers 13 | 14 | import buildinfo.BuildInfo 15 | import scoverage.reporter.IOUtils 16 | 17 | private[scoverage] object ScoverageCompiler { 18 | 19 | val ScalaVersion: String = scala.util.Properties.versionNumberString 20 | val ShortScalaVersion: String = (ScalaVersion split "[.]").toList match { 21 | case init :+ last if last forall (_.isDigit) => init mkString "." 22 | case _ => ScalaVersion 23 | } 24 | 25 | def classPath: Seq[String] = 26 | getScalaJars.map( 27 | _.getAbsolutePath 28 | ) :+ sbtCompileDir.getAbsolutePath :+ runtimeClasses("jvm").getAbsolutePath 29 | 30 | def jsClassPath: Seq[String] = 31 | getScalaJsJars.map( 32 | _.getAbsolutePath 33 | ) :+ sbtCompileDir.getAbsolutePath :+ runtimeClasses("js").getAbsolutePath 34 | 35 | def settings: Settings = settings(classPath) 36 | 37 | def jsSettings: Settings = { 38 | val s = settings(jsClassPath) 39 | s.plugin.value = List(getScalaJsCompilerJar.getAbsolutePath) 40 | s 41 | } 42 | 43 | def settings(classPath: Seq[String]): Settings = { 44 | val s = new scala.tools.nsc.Settings 45 | s.Xprint.value = List("all", "_") 46 | s.deprecation.value = true 47 | s.Yrangepos.value = true 48 | s.Yposdebug.value = true 49 | s.classpath.value = classPath.mkString(File.pathSeparator) 50 | 51 | val path = 52 | s"./plugin/target/scala-$ScalaVersion/test-generated-classes" 53 | new File(path).mkdirs() 54 | s.outdir.value = path 55 | s 56 | } 57 | 58 | def default: ScoverageCompiler = { 59 | val reporter = new scala.tools.nsc.reporters.ConsoleReporter(settings) 60 | new ScoverageCompiler(settings, reporter, validatePositions = true) 61 | } 62 | 63 | def noPositionValidation: ScoverageCompiler = { 64 | val reporter = new scala.tools.nsc.reporters.ConsoleReporter(settings) 65 | new ScoverageCompiler(settings, reporter, validatePositions = false) 66 | } 67 | 68 | def defaultJS: ScoverageCompiler = { 69 | val reporter = new scala.tools.nsc.reporters.ConsoleReporter(jsSettings) 70 | new ScoverageCompiler(jsSettings, reporter, validatePositions = true) 71 | } 72 | 73 | def locationCompiler: LocationCompiler = { 74 | val reporter = new scala.tools.nsc.reporters.ConsoleReporter(settings) 75 | new LocationCompiler(settings, reporter) 76 | } 77 | 78 | private def getScalaJars: List[File] = { 79 | val scalaJars = List("scala-compiler", "scala-library", "scala-reflect") 80 | scalaJars.map(findScalaJar) 81 | } 82 | 83 | private def getScalaJsJars: List[File] = 84 | findJar( 85 | "org.scala-js", 86 | s"scalajs-library_$ShortScalaVersion", 87 | BuildInfo.scalaJSVersion 88 | ) :: getScalaJars 89 | 90 | private def getScalaJsCompilerJar: File = findJar( 91 | "org.scala-js", 92 | s"scalajs-compiler_$ScalaVersion", 93 | BuildInfo.scalaJSVersion 94 | ) 95 | 96 | private def sbtCompileDir: File = { 97 | val dir = new File( 98 | s"./plugin/target/scala-$ScalaVersion/classes" 99 | ) 100 | if (!dir.exists) 101 | throw new FileNotFoundException( 102 | s"Could not locate SBT compile directory for plugin files [$dir]" 103 | ) 104 | dir 105 | } 106 | 107 | private def runtimeClasses(platform: String): File = new File( 108 | s"./runtime/$platform/target/scala-$ScalaVersion/classes" 109 | ) 110 | 111 | private def findScalaJar(artifactId: String): File = 112 | findJar("org.scala-lang", artifactId, ScalaVersion) 113 | 114 | private def findJar( 115 | groupId: String, 116 | artifactId: String, 117 | version: String 118 | ): File = 119 | findIvyJar(groupId, artifactId, version) 120 | .orElse(findCoursierJar(groupId, artifactId, version)) 121 | .getOrElse { 122 | throw new FileNotFoundException( 123 | s"Could not locate $groupId:$artifactId:$version" 124 | ) 125 | } 126 | 127 | private def findCoursierJar( 128 | groupId: String, 129 | artifactId: String, 130 | version: String 131 | ): Option[File] = { 132 | val userHome = System.getProperty("user.home") 133 | val jarPaths = Iterator( 134 | ".cache/coursier", // Linux 135 | "Library/Caches/Coursier", // MacOSX 136 | "AppData/Local/Coursier/cache" // Windows 137 | ).map { loc => 138 | val gid = groupId.replace('.', '/') 139 | s"$userHome/$loc/v1/https/repo1.maven.org/maven2/$gid/$artifactId/$version/$artifactId-$version.jar" 140 | } 141 | jarPaths.map(new File(_)).find(_.exists()) 142 | } 143 | 144 | private def findIvyJar( 145 | groupId: String, 146 | artifactId: String, 147 | version: String, 148 | packaging: String = "jar" 149 | ): Option[File] = { 150 | val userHome = System.getProperty("user.home") 151 | val jarPath = 152 | s"$userHome/.ivy2/cache/$groupId/$artifactId/${packaging}s/$artifactId-$version.jar" 153 | val file = new File(jarPath) 154 | if (file.exists()) Some(file) else None 155 | } 156 | } 157 | 158 | class ScoverageCompiler( 159 | settings: scala.tools.nsc.Settings, 160 | rep: scala.tools.nsc.reporters.Reporter, 161 | validatePositions: Boolean 162 | ) extends scala.tools.nsc.Global(settings, rep) { 163 | 164 | def addToClassPath(file: File): Unit = { 165 | settings.classpath.value = 166 | settings.classpath.value + File.pathSeparator + file.getAbsolutePath 167 | } 168 | 169 | val instrumentationComponent = 170 | new ScoverageInstrumentationComponent(this, None, None) 171 | 172 | val coverageOptions = ScoverageOptions 173 | .default() 174 | .copy(dataDir = IOUtils.getTempPath) 175 | .copy(sourceRoot = IOUtils.getTempPath) 176 | 177 | instrumentationComponent.setOptions(coverageOptions) 178 | val testStore = new ScoverageTestStoreComponent(this) 179 | val validator = new PositionValidator(this) 180 | 181 | def compileSourceFiles(files: File*): ScoverageCompiler = { 182 | val command = new scala.tools.nsc.CompilerCommand( 183 | files.map(_.getAbsolutePath).toList, 184 | settings 185 | ) 186 | new Run().compile(command.files) 187 | this 188 | } 189 | 190 | def writeCodeSnippetToTempFile(code: String): File = { 191 | val file = File.createTempFile("scoverage_snippet", ".scala") 192 | IOUtils.writeToFile(file, code, None) 193 | file.deleteOnExit() 194 | file 195 | } 196 | 197 | def compileCodeSnippet(code: String): ScoverageCompiler = compileSourceFiles( 198 | writeCodeSnippetToTempFile(code) 199 | ) 200 | def compileSourceResources(urls: URL*): ScoverageCompiler = { 201 | compileSourceFiles(urls.map(_.getFile).map(new File(_)): _*) 202 | } 203 | 204 | def assertNoErrors() = 205 | assert(!reporter.hasErrors, "There are compilation errors") 206 | 207 | def assertNoCoverage() = assert( 208 | !testStore.sources.mkString(" ").contains(s"scoverage.Invoker.invoked"), 209 | "There are scoverage.Invoker.invoked instructions added to the code" 210 | ) 211 | 212 | def assertNMeasuredStatements(n: Int): Unit = { 213 | for (k <- 1 to n) { 214 | assert( 215 | testStore.sources 216 | .mkString(" ") 217 | .contains(s"scoverage.Invoker.invoked($k,"), 218 | s"Should be $n invoked statements but missing #$k" 219 | ) 220 | } 221 | assert( 222 | !testStore.sources 223 | .mkString(" ") 224 | .contains(s"scoverage.Invoker.invoked(${n + 1},"), 225 | s"Found statement ${n + 1} but only expected $n" 226 | ) 227 | } 228 | 229 | class PositionValidator(val global: Global) 230 | extends PluginComponent 231 | with TypingTransformers 232 | with Transform { 233 | 234 | override val phaseName = "scoverage-validator" 235 | override val runsAfter = List("typer") 236 | override val runsBefore = List("scoverage-instrumentation") 237 | 238 | override protected def newTransformer( 239 | unit: global.CompilationUnit 240 | ): global.Transformer = new Transformer(unit) 241 | class Transformer(unit: global.CompilationUnit) 242 | extends TypingTransformer(unit) { 243 | 244 | override def transform(tree: global.Tree) = { 245 | global.validatePositions(tree) 246 | tree 247 | } 248 | } 249 | } 250 | 251 | class ScoverageTestStoreComponent(val global: Global) 252 | extends PluginComponent 253 | with TypingTransformers 254 | with Transform { 255 | 256 | val sources = new ListBuffer[String] 257 | 258 | override val phaseName = "scoverage-teststore" 259 | override val runsAfter = List("jvm") 260 | override val runsBefore = List("terminal") 261 | 262 | override protected def newTransformer( 263 | unit: global.CompilationUnit 264 | ): global.Transformer = new Transformer(unit) 265 | class Transformer(unit: global.CompilationUnit) 266 | extends TypingTransformer(unit) { 267 | 268 | override def transform(tree: global.Tree) = { 269 | sources += tree.toString 270 | tree 271 | } 272 | } 273 | } 274 | 275 | override def computeInternalPhases(): Unit = { 276 | super.computeInternalPhases() 277 | if (validatePositions) 278 | addToPhasesSet(validator, "scoverage validator") 279 | addToPhasesSet( 280 | instrumentationComponent, 281 | "scoverage instrumentationComponent" 282 | ) 283 | addToPhasesSet(testStore, "scoverage teststore") 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /plugin/src/test/scala/scoverage/ScoverageOptionsTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import munit.FunSuite 4 | 5 | class ScoverageOptionsTest extends FunSuite { 6 | 7 | val initalOptions = ScoverageOptions.default() 8 | val fakeOptions = List( 9 | "dataDir:myFakeDir", 10 | "sourceRoot:myFakeSourceRoot", 11 | "excludedPackages:some.package;another.package*", 12 | "excludedFiles:*.proto;iHateThisFile.scala", 13 | "excludedSymbols:someSymbol;anotherSymbol;aThirdSymbol", 14 | "extraAfterPhase:extarAfter", 15 | "extraBeforePhase:extraBefore", 16 | "reportTestName" 17 | ) 18 | 19 | val parsed = ScoverageOptions.parse(fakeOptions, (_) => (), initalOptions) 20 | 21 | test("should be able to parse all options") { 22 | assertEquals( 23 | parsed.excludedPackages, 24 | Seq("some.package", "another.package*") 25 | ) 26 | assertEquals(parsed.excludedFiles, Seq("*.proto", "iHateThisFile.scala")) 27 | assertEquals( 28 | parsed.excludedSymbols, 29 | Seq("someSymbol", "anotherSymbol", "aThirdSymbol") 30 | ) 31 | assertEquals(parsed.dataDir, "myFakeDir") 32 | assertEquals(parsed.reportTestName, true) 33 | assertEquals(parsed.sourceRoot, "myFakeSourceRoot") 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /plugin/src/test/scala/scoverage/macrosupport/Tester.scala: -------------------------------------------------------------------------------- 1 | package scoverage.macrosupport 2 | 3 | object Tester { 4 | 5 | // def test: Unit = macro TesterMacro.test 6 | 7 | } 8 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.6.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.19.0") 2 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2") 3 | 4 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.7") 5 | addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") 6 | 7 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.3") 8 | 9 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") 10 | 11 | addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.2") 12 | 13 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") 14 | 15 | libraryDependencies += "org.scala-js" %% "scalajs-env-jsdom-nodejs" % "1.1.0" 16 | -------------------------------------------------------------------------------- /reporter/src/main/resources/scoverage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scoverage Code Coverage 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /reporter/src/main/scala/scoverage/reporter/BaseReportWriter.scala: -------------------------------------------------------------------------------- 1 | package scoverage.reporter 2 | 3 | import java.io.File 4 | 5 | class BaseReportWriter( 6 | sourceDirectories: Seq[File], 7 | outputDir: File, 8 | sourceEncoding: Option[String] 9 | ) { 10 | 11 | // Source paths in canonical form WITH trailing file separator 12 | private val formattedSourcePaths: Seq[String] = 13 | sourceDirectories 14 | .filter(_.isDirectory) 15 | .map(_.getCanonicalPath + File.separator) 16 | 17 | /** Converts absolute path to relative one if any of the source directories is it's parent. 18 | * If there is no parent directory, the path is returned unchanged (absolute). 19 | * 20 | * @param src absolute file path in canonical form 21 | */ 22 | def relativeSource(src: String): String = 23 | relativeSource(src, formattedSourcePaths) 24 | 25 | private def relativeSource(src: String, sourcePaths: Seq[String]): String = { 26 | // We need the canonical path for the given src because our formattedSourcePaths are canonical 27 | val canonicalSrc = new File(src).getCanonicalPath 28 | val sourceRoot: Option[String] = 29 | sourcePaths.find(sourcePath => canonicalSrc.startsWith(sourcePath)) 30 | sourceRoot match { 31 | case Some(path: String) => canonicalSrc.replace(path, "") 32 | case _ => 33 | val fmtSourcePaths: String = sourcePaths.mkString("'", "', '", "'") 34 | throw new RuntimeException( 35 | s"No source root found for '$canonicalSrc' (source roots: $fmtSourcePaths)" 36 | ); 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /reporter/src/main/scala/scoverage/reporter/CoberturaXmlWriter.scala: -------------------------------------------------------------------------------- 1 | package scoverage.reporter 2 | 3 | import java.io.File 4 | 5 | import scala.xml.Node 6 | import scala.xml.PrettyPrinter 7 | 8 | import scoverage.domain.Coverage 9 | import scoverage.domain.DoubleFormat 10 | import scoverage.domain.MeasuredClass 11 | import scoverage.domain.MeasuredMethod 12 | import scoverage.domain.MeasuredPackage 13 | 14 | /** @author Stephen Samuel */ 15 | class CoberturaXmlWriter( 16 | sourceDirectories: Seq[File], 17 | outputDir: File, 18 | sourceEncoding: Option[String] 19 | ) extends BaseReportWriter(sourceDirectories, outputDir, sourceEncoding) { 20 | 21 | def this(baseDir: File, outputDir: File, sourceEncoding: Option[String]) = { 22 | this(Seq(baseDir), outputDir, sourceEncoding) 23 | } 24 | 25 | def write(coverage: Coverage): Unit = { 26 | val file = new File(outputDir, "cobertura.xml") 27 | IOUtils.writeToFile( 28 | file, 29 | "\n\n" + 30 | new PrettyPrinter(120, 4).format(xml(coverage)), 31 | sourceEncoding 32 | ) 33 | } 34 | 35 | def method(method: MeasuredMethod): Node = { 36 | 41 | 42 | { 43 | method.statements.map(stmt => ) 47 | } 48 | 49 | 50 | } 51 | 52 | def klass(klass: MeasuredClass): Node = { 53 | 58 | 59 | {klass.methods.map(method)} 60 | 61 | 62 | { 63 | klass.statements.map(stmt => ) 67 | } 68 | 69 | 70 | } 71 | 72 | def pack(pack: MeasuredPackage): Node = { 73 | 77 | 78 | {pack.classes.map(klass)} 79 | 80 | 81 | } 82 | 83 | def source(src: File): Node = { 84 | {src.getCanonicalPath.replace(File.separator, "/")} 85 | } 86 | 87 | def xml(coverage: Coverage): Node = { 88 | 101 | 102 | --source 103 | {sourceDirectories.filter(_.isDirectory).map(source)} 104 | 105 | 106 | {coverage.packages.map(pack)} 107 | 108 | 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /reporter/src/main/scala/scoverage/reporter/CoverageAggregator.scala: -------------------------------------------------------------------------------- 1 | package scoverage.reporter 2 | 3 | import java.io.File 4 | 5 | import scoverage.domain.Coverage 6 | import scoverage.serialize.Serializer 7 | 8 | object CoverageAggregator { 9 | 10 | // to be used by gradle-scoverage plugin 11 | def aggregate(dataDirs: Array[File], sourceRoot: File): Option[Coverage] = 12 | aggregate( 13 | dataDirs.toSeq, 14 | sourceRoot 15 | ) 16 | 17 | def aggregate(dataDirs: Seq[File], sourceRoot: File): Option[Coverage] = { 18 | println( 19 | s"[info] Found ${dataDirs.size} subproject scoverage data directories [${dataDirs.mkString(",")}]" 20 | ) 21 | if (dataDirs.size > 0) { 22 | Some(aggregatedCoverage(dataDirs, sourceRoot)) 23 | } else { 24 | None 25 | } 26 | } 27 | 28 | def aggregatedCoverage(dataDirs: Seq[File], sourceRoot: File): Coverage = { 29 | var id = 0 30 | val coverage = Coverage() 31 | dataDirs foreach { dataDir => 32 | val coverageFile: File = Serializer.coverageFile(dataDir) 33 | if (coverageFile.exists) { 34 | val subcoverage: Coverage = 35 | Serializer.deserialize(coverageFile, sourceRoot) 36 | val measurementFiles: Array[File] = 37 | IOUtils.findMeasurementFiles(dataDir) 38 | val measurements = IOUtils.invoked(measurementFiles.toIndexedSeq) 39 | subcoverage.apply(measurements) 40 | subcoverage.statements foreach { stmt => 41 | // need to ensure all the ids are unique otherwise the coverage object will have stmt collisions 42 | id = id + 1 43 | coverage add stmt.copy(id = id) 44 | } 45 | } 46 | } 47 | coverage 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /reporter/src/main/scala/scoverage/reporter/IOUtils.scala: -------------------------------------------------------------------------------- 1 | package scoverage.reporter 2 | 3 | import java.io._ 4 | 5 | import scala.collection.Set 6 | import scala.collection.mutable 7 | import scala.io.Codec 8 | import scala.io.Source 9 | 10 | import scoverage.domain.Constants 11 | 12 | /** @author Stephen Samuel */ 13 | object IOUtils { 14 | 15 | def getTempDirectory: File = new File(getTempPath) 16 | def getTempPath: String = System.getProperty("java.io.tmpdir") 17 | 18 | def readStreamAsString(in: InputStream): String = 19 | Source.fromInputStream(in).mkString 20 | 21 | private val UnixSeperator: Char = '/' 22 | private val WindowsSeperator: Char = '\\' 23 | 24 | def getName(path: String): Any = { 25 | val index = { 26 | val lastUnixPos = path.lastIndexOf(UnixSeperator) 27 | val lastWindowsPos = path.lastIndexOf(WindowsSeperator) 28 | Math.max(lastUnixPos, lastWindowsPos) 29 | } 30 | path.drop(index + 1) 31 | } 32 | 33 | def reportFile(outputDir: File, debug: Boolean = false): File = debug match { 34 | case true => new File(outputDir, Constants.XMLReportFilenameWithDebug) 35 | case false => new File(outputDir, Constants.XMLReportFilename) 36 | } 37 | 38 | def clean(dataDir: File): Unit = 39 | findMeasurementFiles(dataDir).foreach(_.delete) 40 | def clean(dataDir: String): Unit = clean(new File(dataDir)) 41 | 42 | def writeToFile( 43 | file: File, 44 | str: String, 45 | encoding: Option[String] 46 | ) = { 47 | val writer = new BufferedWriter( 48 | new OutputStreamWriter( 49 | new FileOutputStream(file), 50 | encoding.getOrElse(Codec.UTF8.name) 51 | ) 52 | ) 53 | try { 54 | writer.write(str) 55 | } finally { 56 | writer.close() 57 | } 58 | } 59 | 60 | /** Returns the measurement file for the current thread. 61 | */ 62 | def measurementFile(dataDir: File): File = measurementFile( 63 | dataDir.getAbsolutePath 64 | ) 65 | def measurementFile(dataDir: String): File = 66 | new File(dataDir, Constants.MeasurementsPrefix + Thread.currentThread.getId) 67 | 68 | def findMeasurementFiles(dataDir: String): Array[File] = findMeasurementFiles( 69 | new File(dataDir) 70 | ) 71 | def findMeasurementFiles(dataDir: File): Array[File] = 72 | dataDir.listFiles(new FileFilter { 73 | override def accept(pathname: File): Boolean = 74 | pathname.getName.startsWith(Constants.MeasurementsPrefix) 75 | }) 76 | 77 | def scoverageDataDirsSearch(baseDir: File): Seq[File] = { 78 | def directoryFilter = new FileFilter { 79 | override def accept(pathname: File): Boolean = pathname.isDirectory 80 | } 81 | def search(file: File): Seq[File] = file match { 82 | case dir if dir.isDirectory && dir.getName == Constants.DataDir => 83 | Seq(dir) 84 | case dir if dir.isDirectory => 85 | dir.listFiles(directoryFilter).toSeq.flatMap(search) 86 | case _ => Nil 87 | } 88 | search(baseDir) 89 | } 90 | 91 | val isMeasurementFile = (file: File) => 92 | file.getName.startsWith(Constants.MeasurementsPrefix) 93 | val isReportFile = (file: File) => file.getName == Constants.XMLReportFilename 94 | val isDebugReportFile = (file: File) => 95 | file.getName == Constants.XMLReportFilenameWithDebug 96 | 97 | // loads all the invoked statement ids from the given files 98 | def invoked( 99 | files: Seq[File], 100 | encoding: String = Codec.UTF8.name 101 | ): Set[(Int, String)] = { 102 | val acc = mutable.Set[(Int, String)]() 103 | files.foreach { file => 104 | val reader = 105 | Source.fromFile(file, encoding) 106 | for (line <- reader.getLines()) { 107 | if (!line.isEmpty) { 108 | acc += (line.split(" ").toList match { 109 | case List(idx, clazz) => (idx.toInt, clazz) 110 | case List(idx) => (idx.toInt, "") 111 | // This should never really happen but to avoid a match error we'll default to a 0 112 | // index here since we start with 1 anyways. 113 | case _ => (0, "") 114 | }) 115 | } 116 | } 117 | reader.close() 118 | } 119 | acc 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /reporter/src/main/scala/scoverage/reporter/ScoverageXmlWriter.scala: -------------------------------------------------------------------------------- 1 | package scoverage.reporter 2 | 3 | import java.io.File 4 | 5 | import scala.xml.Node 6 | import scala.xml.PrettyPrinter 7 | 8 | import scoverage.domain.Coverage 9 | import scoverage.domain.MeasuredClass 10 | import scoverage.domain.MeasuredMethod 11 | import scoverage.domain.MeasuredPackage 12 | import scoverage.domain.Statement 13 | 14 | /** @author Stephen Samuel */ 15 | class ScoverageXmlWriter( 16 | sourceDirectories: Seq[File], 17 | outputDir: File, 18 | debug: Boolean, 19 | sourceEncoding: Option[String] 20 | ) extends BaseReportWriter(sourceDirectories, outputDir, sourceEncoding) { 21 | 22 | def this( 23 | sourceDir: File, 24 | outputDir: File, 25 | debug: Boolean, 26 | sourceEncoding: Option[String] 27 | ) = { 28 | this(Seq(sourceDir), outputDir, debug, sourceEncoding) 29 | } 30 | 31 | def write(coverage: Coverage): Unit = { 32 | val file = IOUtils.reportFile(outputDir, debug) 33 | IOUtils.writeToFile( 34 | file, 35 | new PrettyPrinter(120, 4).format(xml(coverage)), 36 | sourceEncoding 37 | ) 38 | } 39 | 40 | private def xml(coverage: Coverage): Node = { 41 | 47 | 48 | {coverage.packages.map(pack)} 49 | 50 | 51 | } 52 | 53 | private def statement(stmt: Statement): Node = { 54 | debug match { 55 | case true => 56 | 70 | {escape(stmt.desc)} 71 | 72 | case false => 73 | 85 | } 86 | } 87 | 88 | private def method(method: MeasuredMethod): Node = { 89 | 94 | 95 | {method.statements.map(statement)} 96 | 97 | 98 | } 99 | 100 | private def klass(klass: MeasuredClass): Node = { 101 | 107 | 108 | {klass.methods.map(method)} 109 | 110 | 111 | } 112 | 113 | private def pack(pack: MeasuredPackage): Node = { 114 | 118 | 119 | {pack.classes.map(klass)} 120 | 121 | 122 | } 123 | 124 | /** This method ensures that the output String has only 125 | * valid XML unicode characters as specified by the 126 | * XML 1.0 standard. For reference, please see 127 | * the 128 | * standard. This method will return an empty 129 | * String if the input is null or empty. 130 | * 131 | * @param in The String whose non-valid characters we want to remove. 132 | * @return The in String, stripped of non-valid characters. 133 | * @see http://blog.mark-mclaren.info/2007/02/invalid-xml-characters-when-valid-utf8_5873.html 134 | */ 135 | def escape(in: String): String = { 136 | val out = new StringBuilder() 137 | for (current <- Option(in).getOrElse("").toCharArray) { 138 | if ( 139 | (current == 0x9) || (current == 0xa) || (current == 0xd) || 140 | ((current >= 0x20) && (current <= 0xd7ff)) || 141 | ((current >= 0xe000) && (current <= 0xfffd)) || 142 | ((current >= 0x10000) && (current <= 0x10ffff)) 143 | ) 144 | out.append(current) 145 | } 146 | out.mkString 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /reporter/src/main/scala/scoverage/reporter/StatementWriter.scala: -------------------------------------------------------------------------------- 1 | package scoverage.reporter 2 | 3 | import scala.xml.Node 4 | 5 | import scoverage.domain.MeasuredFile 6 | 7 | /** @author Stephen Samuel */ 8 | class StatementWriter(mFile: MeasuredFile) { 9 | 10 | val GREEN = "#AEF1AE" 11 | val RED = "#F0ADAD" 12 | 13 | def output: Node = { 14 | 15 | def cellStyle(invoked: Boolean): String = invoked match { 16 | case true => s"background: $GREEN" 17 | case false => s"background: $RED" 18 | } 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | { 30 | mFile.statements.toSeq 31 | .sortBy(_.line) 32 | .map(stmt => { 33 | 34 | 37 | 40 | 45 | 48 | 51 | 54 | 57 | 58 | }) 59 | } 60 |
LineStmt IdPosTreeSymbolTestsCode
35 | {stmt.line} 36 | 38 | {stmt.id} 39 | 41 | {stmt.start.toString} 42 | - 43 | {stmt.end.toString} 44 | 46 | {stmt.treeName} 47 | 49 | {stmt.symbolName} 50 | 52 | {stmt.tests.mkString(",")} 53 | 55 | {stmt.desc} 56 |
61 | } 62 | } 63 | -------------------------------------------------------------------------------- /reporter/src/test/resources/scoverage/reporter/cobertura.sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | C:/local/mvn-coverage-example/src/main/java 7 | --source 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /reporter/src/test/resources/scoverage/reporter/coverage-04.dtd: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /reporter/src/test/resources/scoverage/reporter/forHtmlWriter/src/main/scala/ClassContainingHtml.scala: -------------------------------------------------------------------------------- 1 | package coverage.sample 2 | 3 | class ClassContainingHtml { 4 | def some_html =
HTML content
5 | } 6 | -------------------------------------------------------------------------------- /reporter/src/test/resources/scoverage/reporter/forHtmlWriter/src/main/scala/ClassInMainDir.scala: -------------------------------------------------------------------------------- 1 | package coverage.sample 2 | 3 | class ClassInMainDir { 4 | def msg_coverage = println("measure coverage of code") 5 | } 6 | -------------------------------------------------------------------------------- /reporter/src/test/resources/scoverage/reporter/forHtmlWriter/src/main/scala/subdir/ClassInSubDir.scala: -------------------------------------------------------------------------------- 1 | package coverage.sample 2 | 3 | class ClassInSubDir { 4 | def msg_test = println("test code") 5 | } 6 | -------------------------------------------------------------------------------- /reporter/src/test/scala/scoverage/reporter/CoberturaXmlWriterTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage.reporter 2 | 3 | import java.io.File 4 | import java.util.UUID 5 | import javax.xml.parsers.DocumentBuilderFactory 6 | import javax.xml.parsers.SAXParserFactory 7 | 8 | import scala.xml.Elem 9 | import scala.xml.XML 10 | import scala.xml.factory.XMLLoader 11 | 12 | import munit.FunSuite 13 | import org.xml.sax.ErrorHandler 14 | import org.xml.sax.SAXParseException 15 | import scoverage.domain.ClassType 16 | import scoverage.domain.Coverage 17 | import scoverage.domain.Location 18 | import scoverage.domain.Statement 19 | 20 | /** @author Stephen Samuel */ 21 | class CoberturaXmlWriterTest extends FunSuite { 22 | 23 | def tempDir(): File = { 24 | val dir = new File(IOUtils.getTempDirectory, UUID.randomUUID.toString) 25 | dir.mkdirs() 26 | dir.deleteOnExit() 27 | dir 28 | } 29 | 30 | def fileIn(dir: File) = new File(dir, "cobertura.xml") 31 | 32 | // Let current directory be our source root 33 | private val sourceRoot = new File(".") 34 | private def canonicalPath(fileName: String) = 35 | new File(sourceRoot, fileName).getCanonicalPath 36 | 37 | test("cobertura output has relative file path") { 38 | 39 | val dir = tempDir() 40 | 41 | val coverage = Coverage() 42 | coverage.add( 43 | Statement( 44 | Location( 45 | "com.sksamuel.scoverage", 46 | "A", 47 | "com.sksamuel.scoverage.A", 48 | ClassType.Object, 49 | "create", 50 | canonicalPath("a.scala") 51 | ), 52 | 1, 53 | 2, 54 | 3, 55 | 12, 56 | "", 57 | "", 58 | "", 59 | false, 60 | 3 61 | ) 62 | ) 63 | coverage.add( 64 | Statement( 65 | Location( 66 | "com.sksamuel.scoverage.A", 67 | "B", 68 | "com.sksamuel.scoverage.A.B", 69 | ClassType.Object, 70 | "create", 71 | canonicalPath("a/b.scala") 72 | ), 73 | 2, 74 | 2, 75 | 3, 76 | 12, 77 | "", 78 | "", 79 | "", 80 | false, 81 | 3 82 | ) 83 | ) 84 | 85 | val writer = new CoberturaXmlWriter(sourceRoot, dir, None) 86 | writer.write(coverage) 87 | 88 | // Needed to acount for https://github.com/scala/scala-xml/pull/177 89 | val customXML: XMLLoader[Elem] = XML.withSAXParser { 90 | val factory = SAXParserFactory.newInstance() 91 | factory.setFeature( 92 | "http://apache.org/xml/features/nonvalidating/load-external-dtd", 93 | false 94 | ) 95 | factory.newSAXParser() 96 | } 97 | 98 | val xml = customXML.loadFile(fileIn(dir)) 99 | 100 | assertEquals( 101 | ((xml \\ "coverage" \ "packages" \ "package" \ "classes" \ "class")( 102 | 0 103 | ) \ "@filename").text, 104 | new File("a.scala").getPath() 105 | ) 106 | 107 | assertEquals( 108 | ((xml \\ "coverage" \ "packages" \ "package" \ "classes" \ "class")( 109 | 1 110 | ) \ "@filename").text, 111 | new File("a", "b.scala").getPath() 112 | ) 113 | } 114 | 115 | // This is failing with 116 | // ==> X scoverage.reporter.CoberturaXmlWriterTest.cobertura output validates 0.375s java.io.FileNotFoundException: https://cobertura.sourceforge.net/xml/coverage-04.dtd 117 | // which seems to indicated that when we are reaching out for the schema it fails to fetch it, which is sort of outo f our control. We could try to have it in this repo 118 | // but my motivation to do this is quite low unless someone else wants to pick it up. 119 | test("cobertura output validates".ignore) { 120 | 121 | val dir = tempDir() 122 | 123 | val coverage = Coverage() 124 | coverage 125 | .add( 126 | Statement( 127 | Location( 128 | "com.sksamuel.scoverage", 129 | "A", 130 | "com.sksamuel.scoverage.A", 131 | ClassType.Object, 132 | "create", 133 | canonicalPath("a.scala") 134 | ), 135 | 1, 136 | 2, 137 | 3, 138 | 12, 139 | "", 140 | "", 141 | "", 142 | false, 143 | 3 144 | ) 145 | ) 146 | coverage 147 | .add( 148 | Statement( 149 | Location( 150 | "com.sksamuel.scoverage", 151 | "A", 152 | "com.sksamuel.scoverage.A", 153 | ClassType.Object, 154 | "create2", 155 | canonicalPath("a.scala") 156 | ), 157 | 2, 158 | 2, 159 | 3, 160 | 16, 161 | "", 162 | "", 163 | "", 164 | false, 165 | 3 166 | ) 167 | ) 168 | coverage 169 | .add( 170 | Statement( 171 | Location( 172 | "com.sksamuel.scoverage2", 173 | "B", 174 | "com.sksamuel.scoverage2.B", 175 | ClassType.Object, 176 | "retrieve", 177 | canonicalPath("b.scala") 178 | ), 179 | 3, 180 | 2, 181 | 3, 182 | 21, 183 | "", 184 | "", 185 | "", 186 | false, 187 | 0 188 | ) 189 | ) 190 | coverage 191 | .add( 192 | Statement( 193 | Location( 194 | "com.sksamuel.scoverage2", 195 | "B", 196 | "B", 197 | ClassType.Object, 198 | "retrieve2", 199 | canonicalPath("b.scala") 200 | ), 201 | 4, 202 | 2, 203 | 3, 204 | 9, 205 | "", 206 | "", 207 | "", 208 | false, 209 | 3 210 | ) 211 | ) 212 | coverage 213 | .add( 214 | Statement( 215 | Location( 216 | "com.sksamuel.scoverage3", 217 | "C", 218 | "com.sksamuel.scoverage3.C", 219 | ClassType.Object, 220 | "update", 221 | canonicalPath("c.scala") 222 | ), 223 | 5, 224 | 2, 225 | 3, 226 | 66, 227 | "", 228 | "", 229 | "", 230 | true, 231 | 3 232 | ) 233 | ) 234 | coverage 235 | .add( 236 | Statement( 237 | Location( 238 | "com.sksamuel.scoverage3", 239 | "C", 240 | "com.sksamuel.scoverage3.C", 241 | ClassType.Object, 242 | "update2", 243 | canonicalPath("c.scala") 244 | ), 245 | 6, 246 | 2, 247 | 3, 248 | 6, 249 | "", 250 | "", 251 | "", 252 | true, 253 | 3 254 | ) 255 | ) 256 | coverage 257 | .add( 258 | Statement( 259 | Location( 260 | "com.sksamuel.scoverage4", 261 | "D", 262 | "com.sksamuel.scoverage4.D", 263 | ClassType.Object, 264 | "delete", 265 | canonicalPath("d.scala") 266 | ), 267 | 7, 268 | 2, 269 | 3, 270 | 4, 271 | "", 272 | "", 273 | "", 274 | false, 275 | 0 276 | ) 277 | ) 278 | coverage 279 | .add( 280 | Statement( 281 | Location( 282 | "com.sksamuel.scoverage4", 283 | "D", 284 | "com.sksamuel.scoverage4.D", 285 | ClassType.Object, 286 | "delete2", 287 | canonicalPath("d.scala") 288 | ), 289 | 8, 290 | 2, 291 | 3, 292 | 14, 293 | "", 294 | "", 295 | "", 296 | false, 297 | 0 298 | ) 299 | ) 300 | 301 | val writer = new CoberturaXmlWriter(sourceRoot, dir, None) 302 | writer.write(coverage) 303 | 304 | val domFactory = DocumentBuilderFactory.newInstance() 305 | domFactory.setValidating(true) 306 | val builder = domFactory.newDocumentBuilder() 307 | builder.setErrorHandler(new ErrorHandler() { 308 | @Override 309 | def error(e: SAXParseException): Unit = { 310 | fail(e.getMessage(), e.getCause()) 311 | } 312 | @Override 313 | def fatalError(e: SAXParseException): Unit = { 314 | fail(e.getMessage(), e.getCause()) 315 | } 316 | 317 | @Override 318 | def warning(e: SAXParseException): Unit = { 319 | fail(e.getMessage(), e.getCause()) 320 | } 321 | }) 322 | builder.parse(fileIn(dir)) 323 | } 324 | 325 | test( 326 | "coverage rates are written as 2dp decimal values rather than percentage" 327 | ) { 328 | 329 | val dir = tempDir() 330 | 331 | val coverage = Coverage() 332 | coverage 333 | .add( 334 | Statement( 335 | Location( 336 | "com.sksamuel.scoverage", 337 | "A", 338 | "com.sksamuel.scoverage.A", 339 | ClassType.Object, 340 | "create", 341 | canonicalPath("a.scala") 342 | ), 343 | 1, 344 | 2, 345 | 3, 346 | 12, 347 | "", 348 | "", 349 | "", 350 | false 351 | ) 352 | ) 353 | coverage 354 | .add( 355 | Statement( 356 | Location( 357 | "com.sksamuel.scoverage", 358 | "A", 359 | "com.sksamuel.scoverage.A", 360 | ClassType.Object, 361 | "create2", 362 | canonicalPath("a.scala") 363 | ), 364 | 2, 365 | 2, 366 | 3, 367 | 16, 368 | "", 369 | "", 370 | "", 371 | true 372 | ) 373 | ) 374 | coverage 375 | .add( 376 | Statement( 377 | Location( 378 | "com.sksamuel.scoverage", 379 | "A", 380 | "com.sksamuel.scoverage.A", 381 | ClassType.Object, 382 | "create3", 383 | canonicalPath("a.scala") 384 | ), 385 | 3, 386 | 3, 387 | 3, 388 | 20, 389 | "", 390 | "", 391 | "", 392 | true, 393 | 1 394 | ) 395 | ) 396 | 397 | val writer = new CoberturaXmlWriter(sourceRoot, dir, None) 398 | writer.write(coverage) 399 | 400 | // Needed to acount for https://github.com/scala/scala-xml/pull/177 401 | val customXML: XMLLoader[Elem] = XML.withSAXParser { 402 | val factory = SAXParserFactory.newInstance() 403 | factory.setFeature( 404 | "http://apache.org/xml/features/nonvalidating/load-external-dtd", 405 | false 406 | ) 407 | factory.newSAXParser() 408 | } 409 | 410 | val xml = customXML.loadFile(fileIn(dir)) 411 | 412 | assertEquals((xml \\ "coverage" \ "@line-rate").text, "0.33", "line-rate") 413 | assertEquals( 414 | (xml \\ "coverage" \ "@branch-rate").text, 415 | "0.50", 416 | "branch-rate" 417 | ) 418 | 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /reporter/src/test/scala/scoverage/reporter/CoverageAggregatorTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage.reporter 2 | 3 | import java.io.File 4 | import java.io.FileWriter 5 | import java.util.UUID 6 | 7 | import munit.FunSuite 8 | import scoverage.domain.ClassType 9 | import scoverage.domain.Constants 10 | import scoverage.domain.Coverage 11 | import scoverage.domain.Location 12 | import scoverage.domain.Statement 13 | import scoverage.serialize.Serializer 14 | 15 | class CoverageAggregatorTest extends FunSuite { 16 | 17 | // Let current directory be our source root 18 | private val sourceRoot = new File(".").getCanonicalPath() 19 | private def canonicalPath(fileName: String) = 20 | new File(sourceRoot, fileName).getCanonicalPath() 21 | 22 | test("should merge coverage objects with same id") { 23 | 24 | val source = canonicalPath("com/scoverage/class.scala") 25 | val location = Location( 26 | "com.scoverage.foo", 27 | "ServiceState", 28 | "com.scoverage.foo.Service.ServiceState", 29 | ClassType.Trait, 30 | "methlab", 31 | source 32 | ) 33 | 34 | val cov1Stmt1 = Statement(location, 1, 155, 176, 4, "", "", "", true, 1) 35 | val cov1Stmt2 = Statement(location, 2, 200, 300, 5, "", "", "", false, 1) 36 | val coverage1 = Coverage() 37 | coverage1.add(cov1Stmt1.copy(count = 0)) 38 | coverage1.add(cov1Stmt2.copy(count = 0)) 39 | val dir1 = new File(IOUtils.getTempPath, UUID.randomUUID.toString) 40 | dir1.mkdir() 41 | Serializer.serialize( 42 | coverage1, 43 | Serializer.coverageFile(dir1), 44 | new File(sourceRoot) 45 | ) 46 | val measurementsFile1 = 47 | new File(dir1, s"${Constants.MeasurementsPrefix}1") 48 | val measurementsFile1Writer = new FileWriter(measurementsFile1) 49 | measurementsFile1Writer.write("1\n2\n") 50 | measurementsFile1Writer.close() 51 | 52 | val cov2Stmt1 = Statement(location, 1, 95, 105, 19, "", "", "", false, 0) 53 | val coverage2 = Coverage() 54 | coverage2.add(cov2Stmt1) 55 | val dir2 = new File(IOUtils.getTempPath, UUID.randomUUID.toString) 56 | dir2.mkdir() 57 | Serializer.serialize( 58 | coverage2, 59 | Serializer.coverageFile(dir2), 60 | new File(sourceRoot) 61 | ) 62 | 63 | val cov3Stmt1 = 64 | Statement(location, 2, 14, 1515, 544, "", "", "", false, 1) 65 | val coverage3 = Coverage() 66 | coverage3.add(cov3Stmt1.copy(count = 0)) 67 | val dir3 = new File(IOUtils.getTempPath, UUID.randomUUID.toString) 68 | dir3.mkdir() 69 | Serializer.serialize( 70 | coverage3, 71 | Serializer.coverageFile(dir3), 72 | new File(sourceRoot) 73 | ) 74 | val measurementsFile3 = 75 | new File(dir3, s"${Constants.MeasurementsPrefix}1") 76 | val measurementsFile3Writer = new FileWriter(measurementsFile3) 77 | measurementsFile3Writer.write("2\n") 78 | measurementsFile3Writer.close() 79 | 80 | val aggregatedForGradle = CoverageAggregator.aggregate( 81 | Array(dir1, dir2, dir3), 82 | new File(sourceRoot) 83 | ) 84 | 85 | assert(aggregatedForGradle.nonEmpty) 86 | 87 | val aggregated = 88 | CoverageAggregator.aggregatedCoverage( 89 | Seq(dir1, dir2, dir3), 90 | new File(sourceRoot) 91 | ) 92 | 93 | assertEquals(aggregated.statements.toSet.size, 4) 94 | assertEquals( 95 | aggregated.statements.map(_.copy(id = 0)).toSet, 96 | Set(cov1Stmt1, cov1Stmt2, cov2Stmt1, cov3Stmt1).map(_.copy(id = 0)) 97 | ) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /reporter/src/test/scala/scoverage/reporter/CoverageMetricsTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage.reporter 2 | 3 | import munit.FunSuite 4 | import scoverage.domain.CoverageMetrics 5 | import scoverage.domain.Statement 6 | 7 | class CoverageMetricsTest extends FunSuite { 8 | 9 | test( 10 | "no branches with at least one invoked statement should have 100% branch coverage" 11 | ) { 12 | val metrics = new CoverageMetrics { 13 | override def statements: Iterable[Statement] = 14 | Seq(Statement(null, 0, 0, 0, 0, null, null, null, false, 1)) 15 | 16 | override def ignoredStatements: Iterable[Statement] = Seq() 17 | } 18 | assertEquals(metrics.branchCount, 0) 19 | assertEqualsDouble(metrics.branchCoverage, 1.0, 0.0001) 20 | } 21 | 22 | test( 23 | "no branches with no invoked statements should have 0% branch coverage" 24 | ) { 25 | val metrics = new CoverageMetrics { 26 | override def statements: Iterable[Statement] = 27 | Seq(Statement(null, 0, 0, 0, 0, null, null, null, false, 0)) 28 | 29 | override def ignoredStatements: Iterable[Statement] = Seq() 30 | } 31 | assertEquals(metrics.branchCount, 0) 32 | assertEquals(metrics.branchCoverage, 0.0) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /reporter/src/test/scala/scoverage/reporter/CoverageTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage.domain 2 | 3 | import munit.FunSuite 4 | 5 | /** @author Stephen Samuel */ 6 | class CoverageTest extends FunSuite { 7 | 8 | test("coverage for no statements is 1") { 9 | val coverage = Coverage() 10 | assertEquals(1.0, coverage.statementCoverage) 11 | } 12 | 13 | test("coverage for no invoked statements is 0") { 14 | val coverage = Coverage() 15 | coverage.add( 16 | Statement( 17 | Location("", "", "", ClassType.Object, "", ""), 18 | 1, 19 | 2, 20 | 3, 21 | 4, 22 | "", 23 | "", 24 | "", 25 | false, 26 | 0 27 | ) 28 | ) 29 | assertEquals(0.0, coverage.statementCoverage) 30 | } 31 | 32 | test("coverage for invoked statements") { 33 | val coverage = Coverage() 34 | coverage.add( 35 | Statement( 36 | Location("", "", "", ClassType.Object, "", ""), 37 | 1, 38 | 2, 39 | 3, 40 | 4, 41 | "", 42 | "", 43 | "", 44 | false, 45 | 3 46 | ) 47 | ) 48 | coverage.add( 49 | Statement( 50 | Location("", "", "", ClassType.Object, "", ""), 51 | 2, 52 | 2, 53 | 3, 54 | 4, 55 | "", 56 | "", 57 | "", 58 | false, 59 | 0 60 | ) 61 | ) 62 | coverage.add( 63 | Statement( 64 | Location("", "", "", ClassType.Object, "", ""), 65 | 3, 66 | 2, 67 | 3, 68 | 4, 69 | "", 70 | "", 71 | "", 72 | false, 73 | 0 74 | ) 75 | ) 76 | coverage.add( 77 | Statement( 78 | Location("", "", "", ClassType.Object, "", ""), 79 | 4, 80 | 2, 81 | 3, 82 | 4, 83 | "", 84 | "", 85 | "", 86 | false, 87 | 0 88 | ) 89 | ) 90 | assertEquals(0.25, coverage.statementCoverage) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /reporter/src/test/scala/scoverage/reporter/IOUtilsTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage.reporter 2 | 3 | import java.io.File 4 | import java.io.FileWriter 5 | import java.util.UUID 6 | 7 | import munit.FunSuite 8 | import scoverage.domain.Constants 9 | 10 | /** @author Stephen Samuel */ 11 | class IOUtilsTest extends FunSuite { 12 | 13 | test("should parse measurement files") { 14 | val file = File.createTempFile("scoveragemeasurementtest", "txt") 15 | val writer = new FileWriter(file) 16 | writer.write("1\n5\n9\n\n10\n") 17 | writer.close() 18 | val invoked = IOUtils.invoked(Seq(file)) 19 | assertEquals(invoked, Set((1, ""), (5, ""), (9, ""), (10, ""))) 20 | 21 | file.delete() 22 | } 23 | 24 | test("should parse multiple measurement files") { 25 | // clean up any existing measurement files 26 | for (file <- IOUtils.findMeasurementFiles(IOUtils.getTempDirectory)) 27 | file.delete() 28 | 29 | val file1 = File.createTempFile("scoverage.measurements.1", "txt") 30 | val writer1 = new FileWriter(file1) 31 | writer1.write("1\n5\n9\n\n10\n") 32 | writer1.close() 33 | 34 | val file2 = File.createTempFile("scoverage.measurements.2", "txt") 35 | val writer2 = new FileWriter(file2) 36 | writer2.write("1\n7\n14\n\n2\n") 37 | writer2.close() 38 | 39 | val files = IOUtils.findMeasurementFiles(file1.getParent) 40 | val invoked = IOUtils.invoked(files.toIndexedSeq) 41 | assertEquals( 42 | invoked, 43 | Set( 44 | (1, ""), 45 | (2, ""), 46 | (5, ""), 47 | (7, ""), 48 | (9, ""), 49 | (10, ""), 50 | (14, "") 51 | ) 52 | ) 53 | 54 | file1.delete() 55 | file2.delete() 56 | } 57 | test("should deep search for scoverage-data directories") { 58 | // create new folder to hold all our data 59 | val base = new File(IOUtils.getTempDirectory, UUID.randomUUID.toString) 60 | 61 | val dataDir1 = new File(base, Constants.DataDir) 62 | assertEquals(dataDir1.mkdirs(), true) 63 | 64 | val subDir = new File(base, UUID.randomUUID.toString) 65 | val dataDir2 = new File(subDir, Constants.DataDir) 66 | assertEquals(dataDir2.mkdirs(), true) 67 | 68 | val subSubDir = new File(subDir, UUID.randomUUID.toString) 69 | val dataDir3 = new File(subSubDir, Constants.DataDir) 70 | assertEquals(dataDir3.mkdirs(), true) 71 | 72 | val dataDirs = IOUtils.scoverageDataDirsSearch(base) 73 | assert(dataDirs.contains(dataDir1)) 74 | assert(dataDirs.contains(dataDir2)) 75 | assert(dataDirs.contains(dataDir3)) 76 | assertEquals(dataDirs.size, 3) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /reporter/src/test/scala/scoverage/reporter/ScoverageHtmlWriterTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage.reporter 2 | 3 | import java.io._ 4 | import java.util.UUID 5 | 6 | import scala.io.Source 7 | import scala.xml.XML 8 | 9 | import munit.FunSuite 10 | import scoverage.domain.ClassType 11 | import scoverage.domain.Coverage 12 | import scoverage.domain.Location 13 | import scoverage.domain.Statement 14 | 15 | class ScoverageHtmlWriterTest extends FunSuite { 16 | 17 | val rootDirForClasses = new File( 18 | getClass.getResource("forHtmlWriter/src/main/scala/").getFile 19 | ) 20 | 21 | def pathToClassFile(classLocation: String): String = 22 | new File(rootDirForClasses, classLocation).getCanonicalPath 23 | 24 | val pathToClassContainingHtml = pathToClassFile("ClassContainingHtml.scala") 25 | val pathToClassInSubDir = pathToClassFile("subdir/ClassInSubDir.scala") 26 | val pathToClassInMainDir = pathToClassFile("ClassInMainDir.scala") 27 | 28 | val statementForClassContainingHtml = Statement( 29 | Location( 30 | "coverage.sample", 31 | "ClassContainingHtml", 32 | "ClassContainingHtml", 33 | ClassType.Class, 34 | "some_html", 35 | pathToClassContainingHtml 36 | ), 37 | 3, 38 | 74, 39 | 97, 40 | 4, 41 | "
HTML content
", 42 | "scala.Predef.println", 43 | "Apply", 44 | false, 45 | 0 46 | ) 47 | val statementForClassInSubDir = Statement( 48 | Location( 49 | "coverage.sample", 50 | "ClassInSubDir", 51 | "ClassInSubDir", 52 | ClassType.Class, 53 | "msg_test", 54 | pathToClassInSubDir 55 | ), 56 | 2, 57 | 64, 58 | 84, 59 | 4, 60 | "scala.this.Predef.println(\"test code\")", 61 | "scala.Predef.println", 62 | "Apply", 63 | false, 64 | 0 65 | ) 66 | val statementForClassInMainDir = Statement( 67 | Location( 68 | "coverage.sample", 69 | "ClassInMainDir", 70 | "ClassInMainDir", 71 | ClassType.Class, 72 | "msg_coverage", 73 | pathToClassInMainDir 74 | ), 75 | 1, 76 | 69, 77 | 104, 78 | 4, 79 | "scala.this.Predef.println(\"measure coverage of code\")", 80 | "scala.Predef.println", 81 | "Apply", 82 | false, 83 | 0 84 | ) 85 | 86 | def createTemporaryDir(): File = { 87 | val dir = new File(IOUtils.getTempDirectory, UUID.randomUUID.toString) 88 | dir.mkdirs() 89 | dir.deleteOnExit() 90 | dir 91 | } 92 | 93 | def writeCoverageToTemporaryDir(coverage: Coverage): File = { 94 | val outputDir = createTemporaryDir() 95 | val htmlWriter = new ScoverageHtmlWriter(rootDirForClasses, outputDir) 96 | htmlWriter.write(coverage) 97 | outputDir 98 | } 99 | 100 | test("HTML coverage report contains correct links") { 101 | 102 | val coverage = Coverage() 103 | coverage.add(statementForClassInSubDir) 104 | coverage.add(statementForClassInMainDir) 105 | 106 | val outputDir = writeCoverageToTemporaryDir(coverage) 107 | 108 | val htmls = List("overview.html", "coverage.sample.html") 109 | 110 | for (html <- htmls) { 111 | val xml = XML.loadString( 112 | Source.fromFile(new File(outputDir, html)).getLines().mkString 113 | ) 114 | val links = for (node <- xml \\ "a") yield { 115 | node.attribute("href") match { 116 | case Some(url) => url.toString 117 | case None => fail("href isn't a url") 118 | } 119 | } 120 | 121 | assert( 122 | links.toSet == Set( 123 | "ClassInMainDir.scala.html", 124 | "subdir/ClassInSubDir.scala.html" 125 | ) 126 | ) 127 | } 128 | } 129 | 130 | test("HTML coverage report escapes HTML") { 131 | 132 | val coverage = Coverage() 133 | coverage.add(statementForClassContainingHtml) 134 | val outputDir = writeCoverageToTemporaryDir(coverage) 135 | 136 | val contentsOfFileWithEmbeddedHtml = Source 137 | .fromFile(new File(outputDir, "ClassContainingHtml.scala.html")) 138 | .getLines() 139 | .mkString 140 | assert(!contentsOfFileWithEmbeddedHtml.contains("
HTML content
")) 141 | assert( 142 | contentsOfFileWithEmbeddedHtml.contains( 143 | "<div>HTML content</div>" 144 | ) 145 | ) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /runtime/js/src/main/scala/scalajssupport/File.scala: -------------------------------------------------------------------------------- 1 | package scalajssupport 2 | 3 | import scala.scalajs.js 4 | 5 | /** This wraps RhinoFile, NodeFile, or PhantomFile depending on which javascript 6 | * environment is being used, and emulates a subset of the java.io.File API. 7 | */ 8 | class File(path: String) { 9 | import File._ 10 | 11 | val _file = jsFile(path) 12 | 13 | def this(path: String, child: String) = { 14 | this(File.pathJoin(path, child)) 15 | } 16 | 17 | def delete(): Unit = { 18 | _file.delete() 19 | } 20 | def getAbsolutePath(): String = { 21 | _file.getAbsolutePath() 22 | } 23 | 24 | def getName(): String = { 25 | _file.getName() 26 | } 27 | 28 | def getPath(): String = { 29 | _file.getPath() 30 | } 31 | 32 | def isDirectory(): Boolean = { 33 | _file.isDirectory() 34 | } 35 | 36 | def mkdirs(): Unit = { 37 | _file.mkdirs() 38 | } 39 | 40 | def listFiles(): Array[File] = { 41 | _file.listFiles().toArray 42 | } 43 | 44 | def listFiles(filter: FileFilter): Array[File] = { 45 | _file.listFiles().filter(filter.accept).toArray 46 | } 47 | 48 | def readFile(): String = { 49 | _file.readFile() 50 | } 51 | 52 | override def toString: String = { 53 | getPath() 54 | } 55 | } 56 | 57 | object File { 58 | val globalObject: js.Dynamic = { 59 | import js.Dynamic.{global => g} 60 | if (js.typeOf(g.global) != "undefined" && (g.global.Object eq g.Object)) { 61 | // Node.js environment detected 62 | g.global 63 | } else { 64 | // In all other well-known environment, we can use the global `this` 65 | js.special.fileLevelThis.asInstanceOf[js.Dynamic] 66 | } 67 | } 68 | 69 | val jsFile: JsFileObject = 70 | if (globalObject.hasOwnProperty("Packages").asInstanceOf[Boolean]) 71 | RhinoFile 72 | else if (globalObject.hasOwnProperty("callPhantom").asInstanceOf[Boolean]) 73 | PhantomFile 74 | else if ( 75 | globalObject 76 | .hasOwnProperty("navigator") 77 | .asInstanceOf[Boolean] && globalObject.navigator.userAgent 78 | .asInstanceOf[js.UndefOr[String]] 79 | .exists(_.contains("jsdom")) 80 | ) 81 | JSDOMFile 82 | else 83 | NodeFile 84 | // Factorize this 85 | 86 | def pathJoin(path: String, child: String): String = 87 | jsFile.pathJoin(path, child) 88 | 89 | def write(path: String, data: String, mode: String = "a") = 90 | jsFile.write(path, data, mode) 91 | } 92 | -------------------------------------------------------------------------------- /runtime/js/src/main/scala/scalajssupport/FileWriter.scala: -------------------------------------------------------------------------------- 1 | package scalajssupport 2 | 3 | /** Emulates a subset of the java.io.FileWriter API required for scoverage to work. 4 | */ 5 | class FileWriter(file: File, append: Boolean) { 6 | def this(file: File) = this(file, false) 7 | def this(file: String) = this(new File(file), false) 8 | def this(file: String, append: Boolean) = this(new File(file), append) 9 | 10 | def append(csq: CharSequence) = { 11 | File.write(file.getPath(), csq.toString) 12 | this 13 | } 14 | 15 | def close(): Unit = { 16 | // do nothing as we don't open a FD to the file, as phantomJS does not use FDs 17 | } 18 | 19 | override def finalize(): Unit = close() 20 | 21 | def flush() = {} 22 | } 23 | -------------------------------------------------------------------------------- /runtime/js/src/main/scala/scalajssupport/JsFile.scala: -------------------------------------------------------------------------------- 1 | package scalajssupport 2 | 3 | trait JsFile { 4 | def delete(): Unit 5 | def getAbsolutePath(): String 6 | 7 | def getName(): String 8 | 9 | def getPath(): String 10 | 11 | def isDirectory(): Boolean 12 | 13 | def mkdirs(): Unit 14 | 15 | def listFiles(): Array[File] 16 | 17 | def listFiles(filter: FileFilter): Array[File] = { 18 | listFiles().filter(filter.accept) 19 | } 20 | 21 | def readFile(): String 22 | } 23 | 24 | trait FileFilter { 25 | def accept(file: File): Boolean 26 | } 27 | 28 | trait JsFileObject { 29 | def write(path: String, data: String, mode: String = "a"): Unit 30 | def pathJoin(path: String, child: String): String 31 | def apply(path: String): JsFile 32 | } 33 | -------------------------------------------------------------------------------- /runtime/js/src/main/scala/scalajssupport/NodeFile.scala: -------------------------------------------------------------------------------- 1 | package scalajssupport 2 | 3 | import scala.scalajs.js 4 | 5 | class NodeFile(path: String)(implicit fs: FS, nodePath: NodePath) 6 | extends JsFile { 7 | def this(path: String, child: String)(implicit fs: FS, nodePath: NodePath) = { 8 | this(nodePath.join(path, child)) 9 | } 10 | 11 | def delete(): Unit = { 12 | if (isDirectory()) fs.rmdirSync(path) 13 | else fs.unlinkSync(path) 14 | } 15 | 16 | def getAbsolutePath(): String = { 17 | fs.realpathSync(path) 18 | } 19 | 20 | def getName(): String = { 21 | nodePath.basename(path) 22 | } 23 | 24 | def getPath(): String = { 25 | path 26 | } 27 | 28 | def isDirectory(): Boolean = { 29 | try { 30 | fs.lstatSync(path).isDirectory() 31 | } catch { 32 | // return false if the file does not exist 33 | case e: Exception => false 34 | } 35 | } 36 | 37 | def mkdirs(): Unit = { 38 | path 39 | .split("/") 40 | .foldLeft("")((acc: String, x: String) => { 41 | val new_acc = nodePath.join(acc, x) 42 | try { 43 | fs.mkdirSync(new_acc) 44 | } catch { 45 | case e: Exception => 46 | } 47 | new_acc 48 | }) 49 | } 50 | 51 | def listFiles(): Array[File] = { 52 | val files = fs.readdirSync(path) 53 | val filesArray = new Array[File](files.length) 54 | for ((item, i) <- filesArray.zipWithIndex) { 55 | filesArray(i) = new File(nodePath.join(this.getPath(), files(i))) 56 | } 57 | filesArray 58 | } 59 | 60 | def readFile(): String = { 61 | fs.readFileSync(path, js.Dynamic.literal(encoding = "utf-8")) 62 | } 63 | 64 | } 65 | 66 | @js.native 67 | trait FSStats extends js.Object { 68 | def isDirectory(): Boolean = js.native 69 | } 70 | 71 | @js.native 72 | trait FS extends js.Object { 73 | def closeSync(fd: Int): Unit = js.native 74 | def lstatSync(path: String): FSStats = js.native 75 | def mkdirSync(path: String): Unit = js.native 76 | def openSync(path: String, flags: String): Int = js.native 77 | def realpathSync(path: String): String = js.native 78 | def readdirSync(path: String): js.Array[String] = js.native 79 | def readFileSync(path: String, options: js.Dynamic): String = js.native 80 | def rmdirSync(path: String): Unit = js.native 81 | def unlinkSync(path: String): Unit = js.native 82 | def writeFileSync( 83 | path: String, 84 | data: String, 85 | options: js.Dynamic = js.Dynamic.literal() 86 | ): Unit = js.native 87 | } 88 | 89 | @js.native 90 | trait NodePath extends js.Object { 91 | def basename(path: String): String = js.native 92 | def join(paths: String*): String = js.native 93 | } 94 | 95 | private[scalajssupport] trait NodeLikeFile extends JsFileObject { 96 | def require: js.Dynamic 97 | 98 | implicit lazy val fs: FS = require("fs").asInstanceOf[FS] 99 | implicit lazy val nodePath: NodePath = require("path").asInstanceOf[NodePath] 100 | 101 | def write(path: String, data: String, mode: String = "a") = { 102 | fs.writeFileSync(path, data, js.Dynamic.literal(flag = mode)) 103 | } 104 | 105 | def pathJoin(path: String, child: String) = { 106 | nodePath.join(path, child) 107 | } 108 | 109 | def apply(path: String) = { 110 | new NodeFile(path) 111 | } 112 | } 113 | 114 | private[scalajssupport] object NodeFile extends NodeLikeFile { 115 | lazy val require = js.Dynamic.global.require 116 | } 117 | 118 | private[scalajssupport] object JSDOMFile extends NodeLikeFile { 119 | lazy val require = js.Dynamic.global.Node.constructor("return require")() 120 | } 121 | -------------------------------------------------------------------------------- /runtime/js/src/main/scala/scalajssupport/PhantomFile.scala: -------------------------------------------------------------------------------- 1 | package scalajssupport 2 | 3 | import scala.scalajs.js 4 | import scala.scalajs.js.JSON 5 | 6 | class PhantomFile(path: String) extends JsFile { 7 | def this(path: String, child: String) = { 8 | this(PhantomFile.pathJoin(path, child)) 9 | } 10 | 11 | def delete(): Unit = { 12 | if (isDirectory()) PhantomFile.removeDirectory(path) 13 | else PhantomFile.remove(path) 14 | } 15 | 16 | def getAbsolutePath(): String = { 17 | PhantomFile.absolute(path) 18 | } 19 | 20 | def getName(): String = { 21 | path.split("\\" + PhantomFile.separator).last 22 | } 23 | 24 | def getPath(): String = { 25 | path 26 | } 27 | 28 | def isDirectory(): Boolean = { 29 | PhantomFile.isDirectory(path) 30 | } 31 | 32 | def mkdirs(): Unit = { 33 | PhantomFile.makeTree(path) 34 | } 35 | 36 | def listFiles(): Array[File] = { 37 | val files = PhantomFile.list(path) 38 | val filesArray = new Array[File](files.length) 39 | for ((item, i) <- filesArray.zipWithIndex) { 40 | filesArray(i) = new File(PhantomFile.pathJoin(this.getPath(), files(i))) 41 | } 42 | filesArray 43 | } 44 | 45 | def readFile(): String = { 46 | PhantomFile.read(path) 47 | } 48 | 49 | } 50 | 51 | private[scalajssupport] object PhantomFile extends JsFileObject { 52 | def fsCallArray(method: String, args: js.Array[js.Any]): js.Dynamic = { 53 | val d = js.Dynamic.global.callPhantom( 54 | js.Dynamic.literal( 55 | action = "require.fs", 56 | method = method, 57 | args = args 58 | ) 59 | ) 60 | JSON.parse(d.asInstanceOf[String]) 61 | } 62 | 63 | def fsCall(method: String, arg: js.Any = null): js.Dynamic = { 64 | fsCallArray(method, js.Array(arg)) 65 | 66 | } 67 | 68 | def absolute(path: String): String = 69 | fsCall("absolute", path).asInstanceOf[String] 70 | def isDirectory(path: String): Boolean = 71 | fsCall("isDirectory", path).asInstanceOf[Boolean] 72 | def list(path: String): js.Array[String] = 73 | fsCall("list", path).asInstanceOf[js.Array[String]] 74 | def makeTree(path: String): Boolean = 75 | fsCall("makeTree", path).asInstanceOf[Boolean] 76 | def read(path: String): String = fsCall("read", path).asInstanceOf[String] 77 | def remove(path: String): Boolean = 78 | fsCall("remove", path).asInstanceOf[Boolean] 79 | def removeDirectory(path: String): Boolean = 80 | fsCall("removeDirectory", path).asInstanceOf[Boolean] 81 | val separator: String = fsCall("separator").asInstanceOf[String] 82 | def write(path: String, content: String, mode: String): Unit = 83 | fsCallArray("write", js.Array(path, content, mode)) 84 | def pathJoin(path: String, child: String): String = path + separator + child 85 | 86 | def apply(path: String) = { 87 | new PhantomFile(path) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /runtime/js/src/main/scala/scalajssupport/RhinoFile.scala: -------------------------------------------------------------------------------- 1 | package scalajssupport 2 | 3 | import scala.scalajs.js 4 | import scala.scalajs.js.Dynamic.{global => g} 5 | import scala.scalajs.js.Dynamic.{newInstance => jsnew} 6 | import scala.scalajs.js.annotation.JSGlobal 7 | 8 | @JSGlobal("Packages.java.io.File") 9 | @js.native 10 | class NativeRhinoFile(path: String, child: String) extends js.Object { 11 | def this(path: String) = this("", path) 12 | 13 | def delete(): Unit = js.native 14 | 15 | def getAbsolutePath(): Any = js.native 16 | 17 | def getName(): Any = js.native 18 | 19 | def getPath(): Any = js.native 20 | 21 | def isDirectory(): Boolean = js.native 22 | 23 | def length(): js.Any = js.native 24 | 25 | def mkdirs(): Unit = js.native 26 | 27 | def listFiles(): js.Array[NativeRhinoFile] = js.native 28 | } 29 | 30 | class RhinoFile(_file: NativeRhinoFile) extends JsFile { 31 | def this(path: String) = this(new NativeRhinoFile(path)) 32 | 33 | def this(path: String, child: String) = { 34 | this((new NativeRhinoFile(path, child))) 35 | } 36 | 37 | def delete(): Unit = _file.delete() 38 | 39 | def getAbsolutePath(): String = "" + _file.getAbsolutePath() 40 | 41 | def getName(): String = "" + _file.getName() 42 | 43 | def getPath(): String = { 44 | "" + _file 45 | .getPath() // Rhino bug: doesn't seem to actually returns a string, we have to convert it ourselves 46 | } 47 | 48 | def isDirectory(): Boolean = _file.isDirectory() 49 | 50 | def mkdirs(): Unit = _file.mkdirs() 51 | 52 | def listFiles(): Array[File] = { 53 | val files = _file.listFiles() 54 | val filesArray = new Array[File](files.length) 55 | for ((item, i) <- filesArray.zipWithIndex) { 56 | filesArray(i) = new File("" + files(i).getAbsolutePath()) 57 | } 58 | filesArray 59 | } 60 | 61 | def readFile(): String = { 62 | val fis = jsnew(g.Packages.java.io.FileInputStream)(_file) 63 | val data = g.Packages.java.lang.reflect.Array.newInstance( 64 | g.Packages.java.lang.Byte.TYPE, 65 | _file.length() 66 | ) 67 | fis.read(data) 68 | fis.close() 69 | "" + jsnew(g.Packages.java.lang.String)(data) 70 | } 71 | } 72 | 73 | private[scalajssupport] object RhinoFile extends JsFileObject { 74 | def write(path: String, data: String, mode: String) = { 75 | val outputstream = 76 | jsnew(g.Packages.java.io.FileOutputStream)(path, mode == "a") 77 | val jString = jsnew(g.Packages.java.lang.String)(data) 78 | outputstream.write(jString.getBytes()) 79 | } 80 | 81 | def pathJoin(path: String, child: String) = { 82 | "" + (new NativeRhinoFile(path, child)).getPath() 83 | } 84 | 85 | def apply(path: String) = { 86 | new RhinoFile(path) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /runtime/js/src/main/scala/scalajssupport/Source.scala: -------------------------------------------------------------------------------- 1 | package scalajssupport 2 | 3 | import scala.io.{Source => OrigSource} 4 | 5 | /** This implementation of Source loads the whole file in memory, which is not really efficient, but 6 | * it is not a problem for scoverage operations. 7 | */ 8 | object Source { 9 | def fromFile(file: File) = { 10 | new OrigSource { 11 | 12 | val iter = file.readFile().toCharArray.iterator 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /runtime/js/src/main/scala/scoverage/Platform.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import scala.collection.mutable.HashMap 4 | 5 | import scalajssupport.{File => SupportFile} 6 | import scalajssupport.{FileFilter => SupportFileFilter} 7 | import scalajssupport.{FileWriter => SupportFileWriter} 8 | import scalajssupport.{Source => SupportSource} 9 | 10 | object Platform { 11 | type ThreadSafeMap[A, B] = HashMap[A, B] 12 | lazy val ThreadSafeMap = HashMap 13 | 14 | type File = SupportFile 15 | type FileWriter = SupportFileWriter 16 | type FileFilter = SupportFileFilter 17 | 18 | lazy val Source = SupportSource 19 | 20 | def insecureRandomUUID() = { 21 | import scala.util.Random 22 | var msb = Random.nextLong() 23 | var lsb = Random.nextLong() 24 | msb &= 0xffffffffffff0fffL // clear version 25 | msb |= 0x0000000000004000L // set to version 4 26 | lsb &= 0x3fffffffffffffffL // clear variant 27 | lsb |= 0x8000000000000000L // set to IETF variant 28 | new java.util.UUID(msb, lsb) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /runtime/jvm/src/main/scala/scoverage/Platform.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import java.io.{File => SupportFile} 4 | import java.io.{FileFilter => SupportFileFilter} 5 | import java.io.{FileWriter => SupportFileWriter} 6 | 7 | import scala.collection.concurrent.TrieMap 8 | import scala.io.{Source => SupportSource} 9 | 10 | object Platform { 11 | type ThreadSafeMap[A, B] = TrieMap[A, B] 12 | lazy val ThreadSafeMap = TrieMap 13 | 14 | type File = SupportFile 15 | type FileWriter = SupportFileWriter 16 | type FileFilter = SupportFileFilter 17 | 18 | lazy val Source = SupportSource 19 | 20 | def insecureRandomUUID() = java.util.UUID.randomUUID() 21 | 22 | } 23 | -------------------------------------------------------------------------------- /runtime/jvm/src/test/scala/scoverage/InvokerConcurrencyTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import java.io.File 4 | import java.util.concurrent.Executors 5 | 6 | import scala.concurrent._ 7 | import scala.concurrent.duration._ 8 | 9 | import munit.FunSuite 10 | 11 | /** Verify that [[Invoker.invoked()]] is thread-safe 12 | */ 13 | class InvokerConcurrencyTest extends FunSuite { 14 | 15 | implicit val executor = 16 | ExecutionContext.fromExecutor(Executors.newFixedThreadPool(8)) 17 | 18 | val measurementDir = new File("target/invoker-test.measurement") 19 | 20 | override def beforeAll(): Unit = { 21 | deleteMeasurementFiles() 22 | measurementDir.mkdirs() 23 | } 24 | 25 | test( 26 | "calling Invoker.invoked on multiple threads does not corrupt the measurement file" 27 | ) { 28 | 29 | val testIds: Set[Int] = (1 to 1000).toSet 30 | 31 | // Create 1k "invoked" calls on the common thread pool, to stress test 32 | // the method 33 | val futures: Set[Future[Unit]] = testIds.map { i: Int => 34 | Future { 35 | Invoker.invoked(i, measurementDir.toString) 36 | } 37 | } 38 | 39 | futures.foreach(Await.result(_, 3.second)) 40 | 41 | // Now verify that the measurement file is not corrupted by loading it 42 | val measurementFiles = Invoker.findMeasurementFiles(measurementDir) 43 | val idsFromFile = Invoker.invoked(measurementFiles.toIndexedSeq) 44 | 45 | assertEquals(idsFromFile, testIds) 46 | } 47 | 48 | override def afterAll(): Unit = { 49 | deleteMeasurementFiles() 50 | measurementDir.delete() 51 | } 52 | 53 | private def deleteMeasurementFiles(): Unit = { 54 | if (measurementDir.isDirectory) 55 | measurementDir.listFiles().foreach(_.delete()) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /runtime/native/src/main/scala/scoverage/Platform.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import java.io.{File => SupportFile} 4 | import java.io.{FileFilter => SupportFileFilter} 5 | import java.io.{FileWriter => SupportFileWriter} 6 | 7 | import scala.collection.mutable.HashMap 8 | import scala.io.{Source => SupportSource} 9 | 10 | object Platform { 11 | type ThreadSafeMap[A, B] = HashMap[A, B] 12 | lazy val ThreadSafeMap = HashMap 13 | 14 | type File = SupportFile 15 | type FileWriter = SupportFileWriter 16 | type FileFilter = SupportFileFilter 17 | 18 | lazy val Source = SupportSource 19 | 20 | def insecureRandomUUID() = { 21 | import scala.util.Random 22 | var msb = Random.nextLong() 23 | var lsb = Random.nextLong() 24 | msb &= 0xffffffffffff0fffL // clear version 25 | msb |= 0x0000000000004000L // set to version 4 26 | lsb &= 0x3fffffffffffffffL // clear variant 27 | lsb |= 0x8000000000000000L // set to IETF variant 28 | new java.util.UUID(msb, lsb) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /runtime/shared/src/main/scala/scoverage/Invoker.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import scala.collection.Set 4 | import scala.collection.mutable 5 | 6 | import scoverage.Platform._ 7 | 8 | /** @author Stephen Samuel */ 9 | object Invoker { 10 | 11 | private val runtimeUUID = Platform.insecureRandomUUID() 12 | 13 | private val MeasurementsPrefix = "scoverage.measurements." 14 | private val threadFiles = new ThreadLocal[mutable.HashMap[String, FileWriter]] 15 | 16 | // For each data directory we maintain a thread-safe set tracking the ids that we've already 17 | // seen and recorded. We're using a map as a set, so we only care about its keys and can ignore 18 | // its values. 19 | private val dataDirToIds = 20 | ThreadSafeMap.empty[String, ThreadSafeMap[Int, Any]] 21 | 22 | /** We record that the given id has been invoked by appending its id to the coverage 23 | * data file. 24 | * 25 | * This will happen concurrently on as many threads as the application is using, 26 | * so we use one file per thread, named for the thread id. 27 | * 28 | * This method is not thread-safe if the threads are in different JVMs, because 29 | * the thread IDs may collide. 30 | * You may not use `scoverage` on multiple processes in parallel without risking 31 | * corruption of the measurement file. 32 | * 33 | * @param id the id of the statement that was invoked 34 | * @param dataDir the directory where the measurement data is held 35 | */ 36 | def invoked( 37 | id: Int, 38 | dataDir: String, 39 | reportTestName: Boolean = false 40 | ): Unit = { 41 | // [sam] we can do this simple check to save writing out to a file. 42 | // This won't work across JVMs but since there's no harm in writing out the same id multiple 43 | // times since for coverage we only care about 1 or more, (it just slows things down to 44 | // do it more than once), anything we can do to help is good. This helps especially with code 45 | // that is executed many times quickly, eg tight loops. 46 | if (!dataDirToIds.contains(dataDir)) { 47 | // Guard against SI-7943: "TrieMap method getOrElseUpdate is not thread-safe". 48 | dataDirToIds.synchronized { 49 | if (!dataDirToIds.contains(dataDir)) { 50 | dataDirToIds(dataDir) = ThreadSafeMap.empty[Int, Any] 51 | } 52 | } 53 | } 54 | val ids = dataDirToIds(dataDir) 55 | if (!ids.contains(id)) { 56 | // Each thread writes to a separate measurement file, to reduce contention 57 | // and because file appends via FileWriter are not atomic on Windows. 58 | var files = threadFiles.get() 59 | if (files == null) { 60 | files = mutable.HashMap.empty[String, FileWriter] 61 | threadFiles.set(files) 62 | } 63 | val writer = files.getOrElseUpdate( 64 | dataDir, 65 | new FileWriter(measurementFile(dataDir), true) 66 | ) 67 | 68 | if (reportTestName) 69 | writer 70 | .append(Integer.toString(id)) 71 | .append(" ") 72 | .append(getCallingScalaTest) 73 | .append("\n") 74 | .flush() 75 | else writer.append(Integer.toString(id)).append("\n").flush() 76 | ids.put(id, ()) 77 | } 78 | } 79 | 80 | def getCallingScalaTest: String = 81 | Thread.currentThread.getStackTrace 82 | .map(_.getClassName.toLowerCase) 83 | .find(name => 84 | name.endsWith("suite") || name.endsWith("spec") || name.endsWith("test") 85 | ) 86 | .getOrElse("") 87 | 88 | def measurementFile(dataDir: File): File = measurementFile( 89 | dataDir.getAbsolutePath() 90 | ) 91 | def measurementFile(dataDir: String): File = new File( 92 | dataDir, 93 | MeasurementsPrefix + runtimeUUID + "." + Thread.currentThread.getId 94 | ) 95 | 96 | def findMeasurementFiles(dataDir: String): Array[File] = findMeasurementFiles( 97 | new File(dataDir) 98 | ) 99 | def findMeasurementFiles(dataDir: File): Array[File] = 100 | dataDir.listFiles(new FileFilter { 101 | override def accept(pathname: File): Boolean = 102 | pathname.getName().startsWith(MeasurementsPrefix) 103 | }) 104 | 105 | // loads all the invoked statement ids from the given files 106 | def invoked(files: Seq[File]): Set[Int] = { 107 | val acc = mutable.Set[Int]() 108 | files.foreach { file => 109 | val reader = Source.fromFile(file) 110 | for (line <- reader.getLines()) { 111 | if (!line.isEmpty) { 112 | acc += line.toInt 113 | } 114 | } 115 | reader.close() 116 | } 117 | acc 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /runtime/shared/src/test/scala/scoverage/InvokerMultiModuleTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | import munit.FunSuite 4 | import scoverage.Platform.File 5 | 6 | /** Verify that [[Invoker.invoked()]] can handle a multi-module project 7 | */ 8 | class InvokerMultiModuleTest extends FunSuite { 9 | 10 | val measurementDir = Array( 11 | new File("target/invoker-test.measurement0"), 12 | new File("target/invoker-test.measurement1") 13 | ) 14 | 15 | override def beforeAll(): Unit = { 16 | deleteMeasurementFiles() 17 | measurementDir.foreach(_.mkdirs()) 18 | } 19 | 20 | test( 21 | "calling Invoker.invoked on with different directories puts measurements in different directories" 22 | ) { 23 | 24 | val testIds: Set[Int] = (1 to 10).toSet 25 | 26 | testIds.map((i: Int) => 27 | Invoker.invoked(i, measurementDir(i % 2).toString()) 28 | ) 29 | 30 | // Verify measurements went to correct directory 31 | val measurementFiles0 = Invoker.findMeasurementFiles(measurementDir(0)) 32 | val idsFromFile0 = Invoker.invoked(measurementFiles0.toIndexedSeq) 33 | 34 | assertEquals(idsFromFile0, testIds.filter((i: Int) => i % 2 == 0)) 35 | 36 | val measurementFiles1 = Invoker.findMeasurementFiles(measurementDir(1)) 37 | val idsFromFile1 = Invoker.invoked(measurementFiles1.toIndexedSeq) 38 | 39 | assertEquals(idsFromFile1, testIds.filter((i: Int) => i % 2 == 1)) 40 | } 41 | 42 | override def afterAll(): Unit = { 43 | deleteMeasurementFiles() 44 | measurementDir.foreach(_.delete()) 45 | } 46 | 47 | private def deleteMeasurementFiles(): Unit = { 48 | measurementDir.foreach((md) => { 49 | if (md.isDirectory()) 50 | md.listFiles().foreach(_.delete()) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /runtimeJSDOMTest/src/test/scala/scoverage/InvokerMultiModuleJSDOMTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage 2 | 3 | class InvokerMultiModuleJSDOMTest extends InvokerMultiModuleTest 4 | -------------------------------------------------------------------------------- /serializer/src/main/scala/scoverage/serialize/Serializer.scala: -------------------------------------------------------------------------------- 1 | package scoverage.serialize 2 | 3 | import java.io.BufferedWriter 4 | import java.io.File 5 | import java.io.FileFilter 6 | import java.io.FileOutputStream 7 | import java.io.OutputStreamWriter 8 | import java.io.Writer 9 | 10 | import scala.io.Codec 11 | import scala.io.Source 12 | 13 | import scoverage.domain.ClassType 14 | import scoverage.domain.Constants 15 | import scoverage.domain.Coverage 16 | import scoverage.domain.Location 17 | import scoverage.domain.Statement 18 | 19 | object Serializer { 20 | 21 | def coverageFile(dataDir: File): File = coverageFile(dataDir.getAbsolutePath) 22 | def coverageFile(dataDir: String): File = 23 | new File(dataDir, Constants.CoverageFileName) 24 | 25 | // Write out coverage data to the given data directory, using the default coverage filename 26 | def serialize(coverage: Coverage, dataDir: String, sourceRoot: String): Unit = 27 | serialize(coverage, coverageFile(dataDir), new File(sourceRoot)) 28 | 29 | // Write out coverage data to given file. 30 | def serialize(coverage: Coverage, file: File, sourceRoot: File): Unit = { 31 | val writer: Writer = new BufferedWriter( 32 | new OutputStreamWriter(new FileOutputStream(file), Codec.UTF8.name) 33 | ) 34 | try { 35 | serialize(coverage, writer, sourceRoot) 36 | } finally { 37 | writer.flush() 38 | writer.close() 39 | } 40 | } 41 | 42 | def serialize( 43 | coverage: Coverage, 44 | writer: Writer, 45 | sourceRoot: File 46 | ): Unit = { 47 | def getRelativePath(filePath: String): String = { 48 | val base = sourceRoot.getCanonicalFile().toPath() 49 | // NOTE: In the real world I have no idea if it's likely that you'll end 50 | // up with weird issues on windows where the roots don't match, something 51 | // like your root being D:/ and your file being C:/. If so this blows up. 52 | // This happened on windows CI for me, since I was using a temp dir, and 53 | // then trying to reletavize it off the cwd, which were in different 54 | // drives. For now, we'll let this as is, but if 'other' has different 55 | // root ever shows its, we'll shut that down real quick here... just not 56 | // sure what to do in that situation yet. 57 | val relPath = 58 | base.relativize(new File(filePath).getCanonicalFile().toPath()) 59 | relPath.toString 60 | } 61 | 62 | def writeHeader(writer: Writer): Unit = { 63 | writer.write( 64 | s"""# Coverage data, format version: ${Constants.CoverageDataFormatVersion} 65 | |# Statement data: 66 | |# - id 67 | |# - source path 68 | |# - package name 69 | |# - class name 70 | |# - class type (Class, Object or Trait) 71 | |# - full class name 72 | |# - method name 73 | |# - start offset 74 | |# - end offset 75 | |# - line number 76 | |# - symbol name 77 | |# - tree name 78 | |# - is branch 79 | |# - invocations count 80 | |# - is ignored 81 | |# - description (can be multi-line) 82 | |# '\f' sign 83 | |# ------------------------------------------ 84 | |""".stripMargin 85 | ) 86 | } 87 | 88 | def writeStatement(stmt: Statement, writer: Writer): Unit = { 89 | writer.write(s"""${stmt.id} 90 | |${getRelativePath(stmt.location.sourcePath)} 91 | |${stmt.location.packageName} 92 | |${stmt.location.className} 93 | |${stmt.location.classType} 94 | |${stmt.location.fullClassName} 95 | |${stmt.location.method} 96 | |${stmt.start} 97 | |${stmt.end} 98 | |${stmt.line} 99 | |${stmt.symbolName} 100 | |${stmt.treeName} 101 | |${stmt.branch} 102 | |${stmt.count} 103 | |${stmt.ignored} 104 | |${stmt.desc} 105 | |\f 106 | |""".stripMargin) 107 | } 108 | 109 | writeHeader(writer) 110 | coverage.statements.toSeq 111 | .sortBy(_.id) 112 | .foreach(stmt => writeStatement(stmt, writer)) 113 | } 114 | 115 | def deserialize(file: File, sourceRoot: File): Coverage = { 116 | val source = Source.fromFile(file)(Codec.UTF8) 117 | try deserialize(source.getLines(), sourceRoot) 118 | finally source.close() 119 | } 120 | 121 | def deserialize(lines: Iterator[String], sourceRoot: File): Coverage = { 122 | // To integrate it smoothly with rest of the report writers, 123 | // it is necessary to again convert [sourcePath] into a 124 | // canonical one. 125 | def getAbsolutePath(filePath: String): String = { 126 | new File(sourceRoot, filePath).getCanonicalPath() 127 | } 128 | 129 | def toStatement(lines: Iterator[String]): Statement = { 130 | val id: Int = lines.next().toInt 131 | val sourcePath = lines.next() 132 | val packageName = lines.next() 133 | val className = lines.next() 134 | val classType = lines.next() 135 | val fullClassName = lines.next() 136 | val method = lines.next() 137 | val loc = Location( 138 | packageName, 139 | className, 140 | fullClassName, 141 | ClassType.fromString(classType), 142 | method, 143 | getAbsolutePath(sourcePath) 144 | ) 145 | val start: Int = lines.next().toInt 146 | val end: Int = lines.next().toInt 147 | val lineNo: Int = lines.next().toInt 148 | val symbolName: String = lines.next() 149 | val treeName: String = lines.next() 150 | val branch: Boolean = lines.next().toBoolean 151 | val count: Int = lines.next().toInt 152 | val ignored: Boolean = lines.next().toBoolean 153 | val desc = lines.toList.mkString("\n") 154 | Statement( 155 | loc, 156 | id, 157 | start, 158 | end, 159 | lineNo, 160 | desc, 161 | symbolName, 162 | treeName, 163 | branch, 164 | count, 165 | ignored 166 | ) 167 | } 168 | 169 | val headerFirstLine = lines.next() 170 | require( 171 | headerFirstLine == s"# Coverage data, format version: ${Constants.CoverageDataFormatVersion}", 172 | "Wrong file format" 173 | ) 174 | 175 | val linesWithoutHeader = lines.dropWhile(_.startsWith("#")) 176 | val coverage = Coverage() 177 | while (!linesWithoutHeader.isEmpty) { 178 | val oneStatementLines = linesWithoutHeader.takeWhile(_ != "\f") 179 | val statement = toStatement(oneStatementLines) 180 | if (statement.ignored) 181 | coverage.addIgnoredStatement(statement) 182 | else 183 | coverage.add(statement) 184 | } 185 | coverage 186 | } 187 | 188 | def clean(dataDir: File): Unit = 189 | findMeasurementFiles(dataDir).foreach(_.delete) 190 | def clean(dataDir: String): Unit = clean(new File(dataDir)) 191 | 192 | def findMeasurementFiles(dataDir: File): Array[File] = 193 | dataDir.listFiles(new FileFilter { 194 | override def accept(pathname: File): Boolean = 195 | pathname.getName.startsWith(Constants.MeasurementsPrefix) 196 | }) 197 | 198 | } 199 | -------------------------------------------------------------------------------- /serializer/src/test/scala/scoverage/serialize/SerializerTest.scala: -------------------------------------------------------------------------------- 1 | package scoverage.serialize 2 | 3 | import java.io.File 4 | import java.io.StringWriter 5 | 6 | import munit.FunSuite 7 | import scoverage.domain.ClassType 8 | import scoverage.domain.Constants 9 | import scoverage.domain.Coverage 10 | import scoverage.domain.Location 11 | import scoverage.domain.Statement 12 | 13 | class SerializerTest extends FunSuite { 14 | private val sourceRoot = new File(".").getCanonicalFile() 15 | 16 | test("coverage should be serializable into plain text") { 17 | val coverage = Coverage() 18 | coverage.add( 19 | Statement( 20 | Location( 21 | "org.scoverage", 22 | "test", 23 | "org.scoverage.test", 24 | ClassType.Trait, 25 | "mymethod", 26 | new File(sourceRoot, "mypath").getAbsolutePath() 27 | ), 28 | 14, 29 | 100, 30 | 200, 31 | 4, 32 | "def test : String", 33 | "test", 34 | "DefDef", 35 | true, 36 | 1 37 | ) 38 | ) 39 | val expected = 40 | s"""# Coverage data, format version: ${Constants.CoverageDataFormatVersion} 41 | |# Statement data: 42 | |# - id 43 | |# - source path 44 | |# - package name 45 | |# - class name 46 | |# - class type (Class, Object or Trait) 47 | |# - full class name 48 | |# - method name 49 | |# - start offset 50 | |# - end offset 51 | |# - line number 52 | |# - symbol name 53 | |# - tree name 54 | |# - is branch 55 | |# - invocations count 56 | |# - is ignored 57 | |# - description (can be multi-line) 58 | |# '\f' sign 59 | |# ------------------------------------------ 60 | |14 61 | |mypath 62 | |org.scoverage 63 | |test 64 | |Trait 65 | |org.scoverage.test 66 | |mymethod 67 | |100 68 | |200 69 | |4 70 | |test 71 | |DefDef 72 | |true 73 | |1 74 | |false 75 | |def test : String 76 | |\f 77 | |""".stripMargin 78 | val writer = new StringWriter() 79 | val actual = Serializer.serialize(coverage, writer, sourceRoot) 80 | assertEquals(expected, writer.toString) 81 | } 82 | 83 | test("coverage should be eserializable from plain text") { 84 | val input = 85 | s"""# Coverage data, format version: ${Constants.CoverageDataFormatVersion} 86 | |# Statement data: 87 | |# - id 88 | |# - source path 89 | |# - package name 90 | |# - class name 91 | |# - class type (Class, Object or Trait) 92 | |# - full class name 93 | |# - method name 94 | |# - start offset 95 | |# - end offset 96 | |# - line number 97 | |# - symbol name 98 | |# - tree name 99 | |# - is branch 100 | |# - invocations count 101 | |# - is ignored 102 | |# - description (can be multi-line) 103 | |# '\f' sign 104 | |# ------------------------------------------ 105 | |14 106 | |mypath 107 | |org.scoverage 108 | |test 109 | |Trait 110 | |org.scoverage.test 111 | |mymethod 112 | |100 113 | |200 114 | |4 115 | |test 116 | |DefDef 117 | |true 118 | |1 119 | |false 120 | |def test : String 121 | |\f 122 | |""".stripMargin 123 | .split(System.lineSeparator()) 124 | .iterator 125 | val statements = List( 126 | Statement( 127 | Location( 128 | "org.scoverage", 129 | "test", 130 | "org.scoverage.test", 131 | ClassType.Trait, 132 | "mymethod", 133 | new File(sourceRoot, "mypath").getAbsolutePath() 134 | ), 135 | 14, 136 | 100, 137 | 200, 138 | 4, 139 | "def test : String", 140 | "test", 141 | "DefDef", 142 | true, 143 | 1 144 | ) 145 | ) 146 | val coverage = Serializer.deserialize(input, sourceRoot) 147 | assertEquals(statements, coverage.statements.toList) 148 | } 149 | test("coverage should serialize sourcePath relatively") { 150 | val coverage = Coverage() 151 | coverage.add( 152 | Statement( 153 | Location( 154 | "org.scoverage", 155 | "test", 156 | "org.scoverage.test", 157 | ClassType.Trait, 158 | "mymethod", 159 | new File(sourceRoot, "mypath").getAbsolutePath() 160 | ), 161 | 14, 162 | 100, 163 | 200, 164 | 4, 165 | "def test : String", 166 | "test", 167 | "DefDef", 168 | true, 169 | 1 170 | ) 171 | ) 172 | val expected = 173 | s"""# Coverage data, format version: ${Constants.CoverageDataFormatVersion} 174 | |# Statement data: 175 | |# - id 176 | |# - source path 177 | |# - package name 178 | |# - class name 179 | |# - class type (Class, Object or Trait) 180 | |# - full class name 181 | |# - method name 182 | |# - start offset 183 | |# - end offset 184 | |# - line number 185 | |# - symbol name 186 | |# - tree name 187 | |# - is branch 188 | |# - invocations count 189 | |# - is ignored 190 | |# - description (can be multi-line) 191 | |# '\f' sign 192 | |# ------------------------------------------ 193 | |14 194 | |mypath 195 | |org.scoverage 196 | |test 197 | |Trait 198 | |org.scoverage.test 199 | |mymethod 200 | |100 201 | |200 202 | |4 203 | |test 204 | |DefDef 205 | |true 206 | |1 207 | |false 208 | |def test : String 209 | |\f 210 | |""".stripMargin 211 | val writer = new StringWriter() 212 | val actual = Serializer.serialize(coverage, writer, sourceRoot) 213 | assertEquals(expected, writer.toString) 214 | } 215 | 216 | test("coverage should deserialize sourcePath by prefixing cwd") { 217 | val input = 218 | s"""# Coverage data, format version: ${Constants.CoverageDataFormatVersion} 219 | |# Statement data: 220 | |# - id 221 | |# - source path 222 | |# - package name 223 | |# - class name 224 | |# - class type (Class, Object or Trait) 225 | |# - full class name 226 | |# - method name 227 | |# - start offset 228 | |# - end offset 229 | |# - line number 230 | |# - symbol name 231 | |# - tree name 232 | |# - is branch 233 | |# - invocations count 234 | |# - is ignored 235 | |# - description (can be multi-line) 236 | |# '\f' sign 237 | |# ------------------------------------------ 238 | |14 239 | |mypath 240 | |org.scoverage 241 | |test 242 | |Trait 243 | |org.scoverage.test 244 | |mymethod 245 | |100 246 | |200 247 | |4 248 | |test 249 | |DefDef 250 | |true 251 | |1 252 | |false 253 | |def test : String 254 | |\f 255 | |""".stripMargin.split(System.lineSeparator()).iterator 256 | val statements = List( 257 | Statement( 258 | Location( 259 | "org.scoverage", 260 | "test", 261 | "org.scoverage.test", 262 | ClassType.Trait, 263 | "mymethod", 264 | new File(sourceRoot, "mypath").getCanonicalPath().toString() 265 | ), 266 | 14, 267 | 100, 268 | 200, 269 | 4, 270 | "def test : String", 271 | "test", 272 | "DefDef", 273 | true, 274 | 1 275 | ) 276 | ) 277 | val coverage = Serializer.deserialize(input, sourceRoot) 278 | assertEquals(statements, coverage.statements.toList) 279 | } 280 | } 281 | --------------------------------------------------------------------------------