├── .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 | 
4 | [](https://gitter.im/scoverage/scoverage)
5 | [](http://search.maven.org/#search|ga|1|g%3A%22org.scoverage%22%20AND%20a%3A%22scalac-scoverage-plugin_2.11.12%22)
6 | [](http://search.maven.org/#search|ga|1|g%3A%22org.scoverage%22%20AND%20a%3A%22scalac-scoverage-plugin_2.12.16%22)
7 | [](http://search.maven.org/#search|ga|1|g%3A%22org.scoverage%22%20AND%20a%3A%22scalac-scoverage-plugin_2.13.8%22)
8 | [](http://search.maven.org/#search|ga|1|g%3A%22org.scoverage%22%20AND%20a%3A%22scalac-scoverage-domain_3%22)
9 | [](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 | 
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 | 
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 |
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 |