├── .git-blame-ignore-revs ├── .github ├── CODEOWNERS ├── dependabot.yml ├── release-drafter.yml ├── settings.yml └── workflows │ ├── ci.yml │ ├── format.yml │ ├── release-drafter.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── .scala-steward.conf ├── .scalafmt.conf ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main └── scala │ └── org │ └── scoverage │ └── coveralls │ ├── CIService.scala │ ├── CoberturaMultiSourceReader.scala │ ├── CoverallPayloadWriter.scala │ ├── CoverallsAuth.scala │ ├── CoverallsClient.scala │ ├── CoverallsPlugin.scala │ ├── GitClient.scala │ ├── Utils.scala │ └── XmlHelper.scala └── test ├── resources ├── example-pr-response.json ├── projectA │ └── src │ │ └── main │ │ ├── scala-2.12 │ │ └── bar │ │ │ └── foo │ │ │ └── TestSourceScala212.scala │ │ └── scala │ │ └── bar │ │ └── foo │ │ ├── TestSourceFile.scala │ │ ├── TestSourceFile2.scala │ │ └── TestSourceFileWithKorean.scala ├── projectB │ └── src │ │ └── main │ │ └── scala │ │ ├── foo │ │ └── TestSourceFile.scala │ │ └── project │ │ └── build.properties ├── test_cobertura.xml.template ├── test_cobertura.xml.windows.template ├── test_cobertura_corrupted.xml.template ├── test_cobertura_corrupted.xml.windows.template ├── test_cobertura_dtd.xml.template ├── test_cobertura_dtd.xml.windows.template ├── test_cobertura_multisource.xml.template └── test_cobertura_multisource.xml.windows.template └── scala └── org └── scoverage └── coveralls ├── CIServiceTest.scala ├── CoberturaMultiSourceReaderTest.scala ├── CoverallPayloadWriterTest.scala ├── CoverallsClientTest.scala ├── GitClientTest.scala ├── HttpClientTest.scala ├── UtilsTest.scala └── XmlHelperTest.scala /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # scalafmt 2 | f37dea67f822eeaffb056552035a0ddb69522020 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @rolandtritsch @ckipp01 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | 9 | 10 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## What’s Changed 3 | 4 | $CHANGES 5 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # These settings are synced to GitHub by 2 | # https://probot.github.io/apps/settings/ 3 | 4 | repository: 5 | # See https://docs.github.com/en/rest/reference/repos#update-a-repository 6 | # for all available settings. 7 | 8 | # The name of the repository. Changing this will rename the repository 9 | name: sbt-coveralls 10 | 11 | # A short description of the repository that will show up on GitHub 12 | description: sbt-plugin to upload sbt-scoverage reports to coveralls 13 | 14 | # A URL with more information about the repository 15 | homepage: https://github.com/scoverage 16 | 17 | # A comma-separated list of topics to set on the repository 18 | topics: scala, sbt, sbt-plugin, coverage, coveralls 19 | 20 | # Either `true` to make the repository private, or `false` to make it 21 | # public. 22 | private: false 23 | 24 | # Either `true` to enable issues for this repository, `false` to disable 25 | # them. 26 | has_issues: true 27 | 28 | # Either `true` to enable projects for this repository, or `false` to 29 | # disable them. If projects are disabled for the organization, passing 30 | # `true` will cause an API error. 31 | has_projects: false 32 | 33 | # Either `true` to enable the wiki for this repository, `false` to disable 34 | # it. 35 | has_wiki: false 36 | 37 | # Either `true` to enable downloads for this repository, `false` to 38 | # disable them. 39 | has_downloads: false 40 | 41 | # Updates the default branch for this repository. 42 | default_branch: main 43 | 44 | # Either `true` to allow squash-merging pull requests, or `false` to 45 | # prevent squash-merging. 46 | allow_squash_merge: true 47 | 48 | # Either `true` to allow merging pull requests with a merge commit, or 49 | # `false` to prevent merging pull requests with merge commits. 50 | allow_merge_commit: false 51 | 52 | # Either `true` to allow rebase-merging pull requests, or `false` to 53 | # prevent rebase-merging. 54 | allow_rebase_merge: false 55 | 56 | # Either `true` to enable automatic deletion of branches on merge, or 57 | # `false` to disable 58 | delete_branch_on_merge: true 59 | 60 | # Either `true` to enable automated security fixes, or `false` to disable 61 | # automated security fixes. 62 | enable_automated_security_fixes: true 63 | 64 | # Either `true` to enable vulnerability alerts, or `false` to disable 65 | # vulnerability alerts. 66 | enable_vulnerability_alerts: true 67 | 68 | # Labels: define labels for Issues and Pull Requests 69 | labels: 70 | - name: bug 71 | color: CC0000 72 | description: An issue with the system 🐛. 73 | 74 | - name: feature 75 | # If including a `#`, make sure to wrap it with quotes! 76 | color: '#336699' 77 | description: New functionality. 78 | 79 | - name: Help Wanted 80 | # Provide a new name to rename an existing label 81 | new_name: first-timers-only 82 | 83 | # Milestones: define milestones for Issues and Pull Requests 84 | milestones: 85 | - title: milestone-title 86 | description: milestone-description 87 | # The state of the milestone. Either `open` or `closed` 88 | state: open 89 | 90 | # Collaborators: give specific users access to this repository. See 91 | # https://docs.github.com/en/rest/reference/repos#add-a-repository-collaborator 92 | # for available options 93 | collaborators: 94 | # - username: bkeepers 95 | # permission: push 96 | # - username: hubot 97 | # permission: pull 98 | 99 | # Note: `permission` is only valid on organization-owned repositories. 100 | # The permission to grant the collaborator. Can be one of: 101 | # * `pull` - can pull, but not push to or administer this repository. 102 | # * `push` - can pull and push, but not administer this repository. 103 | # * `admin` - can pull, push and administer this repository. 104 | # * `maintain` - Recommended for project managers who need to manage the 105 | # repository without access to sensitive or destructive actions. 106 | # * `triage` - Recommended for contributors who need to proactively manage 107 | # issues and pull requests without write access. 108 | 109 | # See https://docs.github.com/en/rest/reference/teams#add-or-update-team-repository-permissions for available options 110 | teams: 111 | - name: core 112 | # The permission to grant the team. Can be one of: 113 | # * `pull` - can pull, but not push to or administer this repository. 114 | # * `push` - can pull and push, but not administer this repository. 115 | # * `admin` - can pull, push and administer this repository. 116 | # * `maintain` - Recommended for project managers who need to manage 117 | # the repository without access to sensitive or destructive actions. 118 | # * `triage` - Recommended for contributors who need to proactively 119 | # manage issues and pull requests without write access. 120 | permission: admin 121 | - name: docs 122 | permission: push 123 | 124 | branches: 125 | - name: main 126 | # https://docs.github.com/en/rest/reference/repos#update-branch-protection 127 | # Branch Protection settings. Set to null to disable 128 | protection: 129 | # Required. Require at least one approving review on a pull request, 130 | # before merging. Set to null to disable. 131 | required_pull_request_reviews: 132 | # The number of approvals required. (1-6) 133 | required_approving_review_count: 1 134 | # Dismiss approved reviews automatically when a new commit is 135 | # pushed. 136 | dismiss_stale_reviews: true 137 | # Blocks merge until code owners have reviewed. 138 | require_code_owner_reviews: true 139 | # Specify which users and teams can dismiss pull request 140 | # reviews. Pass an empty dismissal_restrictions object to 141 | # disable. User and team dismissal_restrictions are only available 142 | # for organization-owned repositories. Omit this parameter for 143 | # personal repositories. 144 | dismissal_restrictions: 145 | users: [] 146 | teams: [] 147 | # Required. Require status checks to pass before merging. Set to 148 | # null to disable 149 | required_status_checks: 150 | # Required. Require branches to be up to date before merging. 151 | strict: true 152 | # Required. The list of status checks to require in order to merge 153 | # into this branch 154 | contexts: [] 155 | # Required. Enforce all configured restrictions for 156 | # administrators. Set to true to enforce required status checks 157 | # for repository administrators. Set to null to disable. 158 | enforce_admins: true 159 | # Prevent merge commits from being pushed to matching branches 160 | required_linear_history: true 161 | # Required. Restrict who can push to this branch. Team and user 162 | # restrictions are only available for organization-owned 163 | # repositories. Set to null to disable. 164 | restrictions: 165 | apps: [] 166 | users: [] 167 | teams: [] 168 | 169 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '*.md' 7 | branches: 8 | - main 9 | pull_request: 10 | 11 | jobs: 12 | scala: 13 | strategy: 14 | fail-fast: false # remove when PR is finished, just to make sure we don't make regression 15 | matrix: 16 | JDK: [ 8, 17 ] 17 | os: 18 | - ubuntu-latest 19 | - windows-latest 20 | runs-on: ${{ matrix.os }} 21 | 22 | steps: 23 | - name: Ignore line ending differences in git 24 | if: contains(runner.os, 'windows') 25 | shell: bash 26 | run: git config --global core.autocrlf false 27 | 28 | - name: checkout the repo 29 | uses: actions/checkout@v4 30 | with: 31 | submodules: 'recursive' 32 | 33 | - name: Setup Java 34 | uses: actions/setup-java@v4 35 | with: 36 | java-version: ${{ matrix.JDK }} 37 | distribution: temurin 38 | cache: sbt 39 | 40 | - name: Install sbt 41 | if: matrix.os == 'ubuntu-latest' 42 | run: | 43 | echo "deb https://repo.scala-sbt.org/scalasbt/debian all main" | sudo tee /etc/apt/sources.list.d/sbt.list 44 | echo "deb https://repo.scala-sbt.org/scalasbt/debian /" | sudo tee /etc/apt/sources.list.d/sbt_old.list 45 | curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x99E82A75642AC823" | sudo apt-key add 46 | sudo apt-get update 47 | sudo apt-get install sbt 48 | 49 | - name: run tests 50 | run: sbt generateXMLFiles test 51 | 52 | - name: run sbt-tests 53 | run: sbt prepareScripted scripted 54 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Scalafmt 2 | 3 | permissions: {} 4 | 5 | on: 6 | pull_request: 7 | branches: ['**'] 8 | 9 | jobs: 10 | build: 11 | name: Code is formatted 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout current branch (full) 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | persist-credentials: false 19 | 20 | - name: Check project is formatted 21 | uses: jrouly/scalafmt-native-action@v4 22 | with: 23 | arguments: '--list --mode diff-ref=origin/main' 24 | -------------------------------------------------------------------------------- /.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-20.04 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - uses: actions/setup-java@v4 15 | with: 16 | distribution: 'temurin' 17 | java-version: '8' 18 | - run: sbt ci-release 19 | env: 20 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 21 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 22 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 23 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | 3 | *.iml 4 | *.ipr 5 | *.iws 6 | .idea/ 7 | .vscode/ 8 | .bloop/ 9 | .bsp/ 10 | .metals/ 11 | metals.sbt 12 | 13 | .settings/ 14 | .cache 15 | .classpath 16 | .project 17 | .tool-versions 18 | 19 | # OS X 20 | Icon 21 | Thumbs.db 22 | .DS_Store 23 | 24 | .history 25 | credentials.sbt 26 | 27 | src/test/resources/*.xml 28 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/sbt-test/scoverage/sbt-coveralls-test"] 2 | path = src/sbt-test/scoverage/sbt-coveralls-test 3 | url = https://github.com/rolandtritsch/sbt-coveralls-test.git 4 | [submodule "src/sbt-test/scoverage/sbt-scoverage-samples"] 5 | path = src/sbt-test/scoverage/sbt-scoverage-samples 6 | url = https://github.com/scoverage/sbt-scoverage-samples.git 7 | -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | updates.pin = [ 2 | # Latest version of jgit which was compiled with JDK 8 which this project targets 3 | { groupId = "org.eclipse.jgit", version = "5.13.0.202109080827-r" }, 4 | # Only do updates for Scala 2.12 series 5 | { groupId = "org.scala-lang", artifactId = "scala-library", version = "2.12." } 6 | ] 7 | 8 | updates.ignore = [ 9 | { groupId = "org.eclipse.jgit" } 10 | ] 11 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.8.3" 2 | runner.dialect = scala213 3 | 4 | project { 5 | git = true 6 | excludePaths = [ 7 | "glob:**/src/test/resources/projectA/src/main/scala/bar/foo/TestSourceFile.scala", 8 | "glob:**/src/test/resources/projectA/src/main/scala/bar/foo/TestSourceFile2.scala", 9 | "glob:**/src/test/resources/projectA/src/main/scala/bar/foo/TestSourceFileWithKorean.scala", 10 | "glob:**/src/test/resources/projectB/src/main/scala/foo/TestSourceFile.scala" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Maintainers/Contributers 4 | 5 | * Create an issue (and agree what needs to get solved and how) 6 | * Fork the repo 7 | * Clone the repo ... 8 | * `git clone >repo<` 9 | * `cd >repo<` 10 | * `git submodule update --init` 11 | * Run `sbt generateXMLFiles` (once) 12 | * Run `sbt test` to test the current implementation 13 | * Run `sbt publishLocal` to create a local build that you can use in 14 | your projects `plugins.sbt` file to test something locally 15 | * Run `sbt scripted` to test the plugin with the scripted test-cases 16 | * Create a PR (and work through the feedback on the PR) 17 | 18 | ## Contributing an sbt-test 19 | 20 | * Reference [sbt-coveralls-test][] as an example 21 | * Add a/the `test` file to the root of your repo 22 | * Put the coveralls token into a `.coverallsToken` file in the root of 23 | your repo and reference/use it from the build.sbt file (not a best 24 | practice, but good enough for now) 25 | * Update your `plugins.sbt` file to read the plugin versions from 26 | the system properties (see [sbt-coverage-test][]) 27 | * Run `git submodule add src/sbt-test/scoverage/` 28 | * Run `sbt prepareScripted scripted` 29 | * To update/pull the latest tests run `git submodule update --remote` 30 | 31 | ## Committers 32 | 33 | * Merge the PR(s) 34 | * When ready, tag the current commit with `git tag v` 35 | * Push the tag with `git push --tags` 36 | * (For now) Go into `releases` and promote the draft release to latest 37 | 38 | Note: (Obviously) Be careful to push tags, because this will trigger 39 | a push to sonatype/maven and bad release/tags cannot be overwritten or 40 | deleted. 41 | 42 | [sbt-coveralls-test]: https://github.com/rolandtritsch/sbt-coveralls-test 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Copyright (C) 2011-2012 Ian Forsey 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 6 | use this file except in compliance with the License. You may obtain a copy of 7 | the License at 8 | 9 | [http://www.apache.org/licenses/LICENSE-2.0] 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | License for the specific language governing permissions and limitations under 15 | the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sbt-coveralls 2 | 3 | [![License][license-badge]][license] 4 | [![Join the chat at https://gitter.im/scoverage/sbt-coveralls][gitter-badge]][gitter] 5 | [![Maven Central][maven-badge]][maven] 6 | 7 | SBT plugin that uploads scala code coverage to [coveralls][] and 8 | integrates with [Travis CI](#travis-ci-integration) and [GitHub 9 | Actions](#github-actions-integration). This plugin uses [scoverage][] 10 | to generate the code coverage metrics. 11 | 12 | Please take a look at the [samples project][] to see some [sample 13 | output][]. 14 | 15 | ## Installation 16 | 17 | 1) Add the following to your `project/build.sbt` file 18 | 19 | ```scala 20 | 21 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.8.2") 22 | 23 | addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.1") 24 | ``` 25 | 26 | 2) Setup coveralls configuration options (such as [Specifying Your 27 | Repo Token](#specifying-your-repo-token)) 28 | 29 | 3) Register on [coveralls][] 30 | 31 | 4) Follow the instructions for either [Travis 32 | CI](#travis-ci-integration) or [Manual Usage](#manual-usage) 33 | 34 | ## Travis CI Integration 35 | 36 | `sbt-coveralls` can be run by [Travis CI][travis-docs] 37 | by following these instructions: 38 | 39 | 1) Add the following to your `travis.yml` 40 | 41 | ```yaml 42 | script: "sbt clean coverage test" 43 | after_success: "sbt coverageReport coveralls" 44 | ``` 45 | 46 | If you have a multi-module project, perform `coverageAggregate` 47 | [as a separate command][multi-project-reports]. 48 | 49 | ```yaml 50 | script: 51 | - sbt clean coverage test coverageReport && 52 | sbt coverageAggregate 53 | after_success: 54 | - sbt coveralls 55 | ``` 56 | 57 | 2) Job done! Commit these changes to `travis.yml` to kick off your 58 | Travis build and you should see coverage reports appear on [coveralls][]. 59 | 60 | ## GitHub Actions Integration 61 | 62 | `sbt-coveralls` can be run by [GitHub Actions][] by following these instructions: 63 | 64 | 1) Add the following to your `.github/workflows/ci.yml` 65 | 66 | ```yaml 67 | - name: Git checkout (merge) 68 | uses: actions/checkout@v3 69 | if: github.event_name != 'pull_request' 70 | with: 71 | fetch-depth: 0 72 | 73 | - name: Git checkout (PR) 74 | uses: actions/checkout@v3 75 | if: github.event_name == 'pull_request' 76 | with: 77 | fetch-depth: 0 78 | # see: https://frontside.com/blog/2020-05-26-github-actions-pull_request/#how-does-pull_request-affect-actionscheckout 79 | ref: ${{ github.event.pull_request.head.sha }} 80 | 81 | - name: Run tests 82 | run: sbt clean coverage test 83 | 84 | - name: Upload coverage data to Coveralls 85 | run: sbt coverageReport coveralls 86 | env: 87 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 88 | COVERALLS_FLAG_NAME: Scala ${{ matrix.scala }} 89 | ``` 90 | 91 | Note the separate checkout step for pull requests. 92 | It is needed because of 93 | [the way pull_request affects actions checkout](https://frontside.com/blog/2020-05-26-github-actions-pull_request/#how-does-pull_request-affect-actionscheckout), 94 | so correct commit info could be sent to coveralls.io 95 | 96 | If you have a multi-module project, perform `coverageAggregate` 97 | [as a separate command][multi-project-reports]. 98 | 99 | ```yaml 100 | - name: Upload coverage data to Coveralls 101 | run: sbt coverageAggregate coveralls 102 | env: 103 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 104 | COVERALLS_FLAG_NAME: Scala ${{ matrix.scala }} 105 | ``` 106 | 107 | 2) Job done! Commit these changes to kick off your GitHub Actions 108 | build and you should see coverage reports appear on [coveralls][]. 109 | 110 | ## CircleCI Integration 111 | 112 | Enable CircleCI support in your `build.sbt`: 113 | 114 | ```scala 115 | import org.scoverage.coveralls.Imports.CoverallsKeys._ 116 | import org.scoverage.coveralls.CircleCI 117 | 118 | coverallsService := Some(CircleCI) 119 | ``` 120 | 121 | Add the following step to your `config.yml` right after your test step: 122 | 123 | ```yaml 124 | - run: 125 | name: Generate and upload coverage report 126 | when: always 127 | command: sbt ";coverageReport ;coverageAggregate ;coveralls" 128 | ``` 129 | 130 | ## Manual Usage 131 | 132 | 1) Get the repo token for your repo from [coveralls][]. 133 | 134 | 1) Let `sbt-coveralls` know what your coveralls repo token is. See 135 | [Specifying Your Repo Token](#specifying-your-repo-token) 136 | 137 | 2) In the SBT console, run `coverage` then your tests finishing with 138 | `coveralls`. After running the command, you should see output similar 139 | to the following: 140 | 141 | Uploading to coveralls.io succeeded: Job #17.1 142 | https://coveralls.io/jobs/12207 143 | 144 | ## Specifying Your Repo Token 145 | 146 | There are several ways to tell `sbt-coveralls` your repo token to 147 | support different use cases: 148 | 149 | ### Write your repo token into a file 150 | 151 | Add the following to your `build.sbt`. The path can be absolute and 152 | point to somewhere outside the project or relative and point somewhere 153 | inside the project (such as `src/main/resources/token.txt`). 154 | 155 | Just remember: **Do not store repo tokens inside your project if it is 156 | in a public git repository!** 157 | 158 | ```scala 159 | import org.scoverage.coveralls.Imports.CoverallsKeys._ 160 | 161 | coverallsTokenFile := "/path/to/my/repo/token.txt" 162 | ``` 163 | 164 | ### Put your repo token directly in your `build.sbt` 165 | 166 | **Do not store repo tokens inside your project if it is in a public 167 | git repository!** 168 | 169 | ```scala 170 | import org.scoverage.coveralls.Imports.CoverallsKeys._ 171 | 172 | coverallsToken := Some("my-token") 173 | ``` 174 | 175 | ### Add an environment variable 176 | 177 | Add an environment variable `COVERALLS_REPO_TOKEN`, for example: 178 | 179 | export COVERALLS_REPO_TOKEN=my-token 180 | 181 | ## Specifying Your Coveralls Endpoint 182 | 183 | If you're using [coveralls][] as your endpoint, then you don't need to 184 | set this option. If you're using a hosted (enterprise) instance of 185 | coveralls, you will need to specify your endpoint in one of two ways. 186 | 187 | ### Put your endpoint directly in your `build.sbt` 188 | 189 | ```scala 190 | import org.scoverage.coveralls.Imports.CoverallsKeys._ 191 | 192 | coverallsEndpoint := Some("http://my-instance.com") 193 | ``` 194 | 195 | ### Add an environment variable 196 | 197 | Add an environment variable `COVERALLS_ENDPOINT`, for example: 198 | 199 | export COVERALLS_ENDPOINT=http://my-instance.com 200 | 201 | ## Overriding the current branch 202 | 203 | By default `sbt-coveralls` uses the currently checked-out branch for 204 | reporting. To override the branch name add the `CI_BRANCH` variable, 205 | for example: 206 | 207 | export CI_BRANCH=my-branch-name 208 | 209 | ## Specifying Source File Encoding 210 | 211 | `sbt-coveralls` finds the encoding in `scalacOptions` setting value. 212 | If not defined it assumes source files are encoded using 213 | platform-specific encoding. To specify encoding, add the following to 214 | your `build.sbt` 215 | 216 | ```scala 217 | scalacOptions += Seq("-encoding", "UTF-8") 218 | ``` 219 | 220 | ## Using Travis-Pro 221 | 222 | It is important to set the correct `service` when using Travis-Pro. 223 | The default is to use `travis-ci`. To override this value, add the 224 | following to your `build.sbt` 225 | 226 | ```scala 227 | import org.scoverage.coveralls.Imports.CoverallsKeys._ 228 | import org.scoverage.coveralls.TravisPro 229 | 230 | coverallsService := Some(TravisPro) 231 | ``` 232 | 233 | ## Uploading coverage from parallel CI builds 234 | 235 | [Coveralls supports merging reports from multiple CI builds.](https://docs.coveralls.io/parallel-build-webhook) 236 | Each report must be flagged as coming from a parallel job, then a webhook must be called after all jobs have completed to merge the reports together. 237 | 238 | To mark uploaded reports as parallel, either: 239 | 240 | ### Put the flag directly in `build.sbt` 241 | 242 | ```scala 243 | import org.scoverage.coveralls.Imports.CoverallsKeys._ 244 | 245 | coverallsParallel := true 246 | ``` 247 | 248 | ### Set an environment variable 249 | 250 | ```shell 251 | export COVERALLS_PARALLEL=true 252 | ``` 253 | 254 | # License 255 | 256 | `sbt-coveralls` is open source software released under the [Apache 2 257 | License][license]. 258 | 259 | [Github Actions]: https://github.com/features/actions 260 | [coveralls]: https://coveralls.io 261 | [gitter-badge]: https://badges.gitter.im/Join%20Chat.svg 262 | [gitter]: https://gitter.im/scoverage/sbt-coveralls 263 | [license]: http://www.apache.org/licenses/LICENSE-2.0.txt 264 | [license-badge]: http://img.shields.io/:license-Apache%202-blue.svg 265 | [maven-badge]: https://img.shields.io/github/v/release/scoverage/sbt-coveralls?label=maven-central 266 | [maven]: https://search.maven.org/artifact/org.scoverage/sbt-coveralls 267 | [multi-project-reports]: https://github.com/scoverage/sbt-scoverage#multi-project-reports 268 | [sample output]: https://coveralls.io/r/scoverage/scoverage-samples 269 | [samples project]: https://github.com/scoverage/sbt-scoverage-samples 270 | [scoverage]: https://github.com/scoverage/scalac-scoverage-plugin 271 | [travis-docs]: https://docs.travis-ci.com 272 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "sbt-coveralls" 2 | 3 | import sbt.ScriptedPlugin.autoImport.scriptedLaunchOpts 4 | 5 | import java.io.File 6 | import scala.sys.process._ 7 | 8 | lazy val generateXMLFiles = 9 | taskKey[Unit]("Generate XML files (for test)") 10 | generateXMLFiles := { 11 | val dir = (Test / resourceDirectory).value 12 | val pwd = (run / baseDirectory).value 13 | 14 | val template = 15 | if (System.getProperty("os.name").startsWith("Windows")) 16 | ".xml.windows.template" 17 | else 18 | ".xml.template" 19 | 20 | dir.listFiles { (_, name) => name.endsWith(template) }.foreach { 21 | templateFile => 22 | val newFile = dir / templateFile.getName.replace(template, ".xml") 23 | val content = IO.read(templateFile) 24 | IO.write(newFile, content.replace("{{PWD}}", pwd.absolutePath)) 25 | } 26 | } 27 | 28 | lazy val prepareScripted = 29 | taskKey[Unit]("Update .git files to make scripted work") 30 | prepareScripted := { 31 | val log = streams.value.log 32 | val pwd = (run / baseDirectory).value 33 | 34 | val submodules = "git submodule status" !! log 35 | val submodulePaths = submodules.split('\n').map { x => 36 | x.split(" ")(2) 37 | } 38 | 39 | submodulePaths.foreach { subModulePath => 40 | val path = pwd / ".git" / "modules" / subModulePath 41 | val pathFixedForWindows = 42 | if (System.getProperty("os.name").startsWith("Windows")) 43 | path.absolutePath.replace( 44 | File.separator, 45 | "/" 46 | ) // Git under Windows uses / for path separator 47 | else 48 | path.absolutePath 49 | val destination = file(subModulePath) / ".git" 50 | IO.delete(destination) 51 | IO.write(destination, s"gitdir: $pathFixedForWindows") 52 | } 53 | } 54 | 55 | inThisBuild( 56 | List( 57 | organization := "org.scoverage", 58 | homepage := Some(url("http://scoverage.org")), 59 | developers := List( 60 | Developer( 61 | "sksamuel", 62 | "Stephen Samuel", 63 | "sam@sksamuel.com", 64 | url("https://github.com/sksamuel") 65 | ), 66 | Developer( 67 | "rolandtritsch", 68 | "Roland Tritsch", 69 | "roland@tritsch.email", 70 | url("https://github.com/rolandtritsch") 71 | ) 72 | ), 73 | licenses := Seq( 74 | "Apache-2.0" -> url("http://www.apache.org/license/LICENSE-2.0") 75 | ), 76 | scalaVersion := "2.12.20", 77 | versionScheme := Some("semver-spec") 78 | ) 79 | ) 80 | 81 | lazy val root = Project("sbt-coveralls", file(".")) 82 | .enablePlugins(SbtPlugin) 83 | .settings( 84 | Test / publishArtifact := false, 85 | scalacOptions := Seq( 86 | "-release:8", 87 | "-unchecked", 88 | "-deprecation", 89 | "-feature", 90 | "-encoding", 91 | "utf8" 92 | ), 93 | dependencyOverrides ++= Seq( 94 | "com.jcraft" % "jsch" % "0.1.55" 95 | ), 96 | libraryDependencies ++= Seq( 97 | "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.18.0", 98 | "com.fasterxml.jackson.core" % "jackson-core" % "2.18.2", 99 | "org.eclipse.jgit" % "org.eclipse.jgit" % "5.13.0.202109080827-r", 100 | "org.scalaj" %% "scalaj-http" % "2.4.2", 101 | "io.circe" %% "circe-core" % "0.14.10", 102 | "io.circe" %% "circe-generic" % "0.14.10", 103 | "io.circe" %% "circe-parser" % "0.14.10", 104 | "org.mockito" % "mockito-core" % "5.14.2" % Test, 105 | "org.scalatest" %% "scalatest" % "3.2.19" % Test 106 | ), 107 | scriptedLaunchOpts ++= Seq( 108 | "-Xmx1024M", 109 | "-Dplugin.sbtscoverage.version=" + "2.0.9", 110 | "-Dplugin.sbtcoveralls.version=" + version.value 111 | ) 112 | ) 113 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | libraryDependencies += "org.scala-sbt" % "scripted-plugin_2.12" % sbtVersion.value 2 | 3 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.6.1") 4 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") 5 | -------------------------------------------------------------------------------- /src/main/scala/org/scoverage/coveralls/CIService.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | import io.circe._ 4 | import io.circe.parser 5 | import io.circe.generic.auto._ 6 | 7 | import scala.io.{BufferedSource, Source} 8 | import scala.util.control.NonFatal 9 | 10 | trait CIService { 11 | def name: String 12 | def jobId: Option[String] 13 | def pullRequest: Option[String] 14 | def currentBranch: Option[String] 15 | 16 | // default behavior for CI services is for a repo token to be required 17 | def coverallsAuth(userRepoToken: Option[String]): Option[CoverallsAuth] = 18 | userRepoToken.map(CoverallsRepoToken) 19 | } 20 | 21 | trait TravisBase extends CIService { 22 | val jobId: Option[String] = sys.env.get("TRAVIS_JOB_ID") 23 | val pullRequest: Option[String] = sys.env.get("CI_PULL_REQUEST") 24 | val currentBranch: Option[String] = sys.env.get("CI_BRANCH") 25 | 26 | // If a user token exists, use it; otherwise, Travis doesn't seem to need a token at all 27 | override def coverallsAuth( 28 | userRepoToken: Option[String] 29 | ): Option[CoverallsAuth] = 30 | Some(userRepoToken.fold[CoverallsAuth](NoTokenNeeded)(CoverallsRepoToken)) 31 | } 32 | 33 | case object TravisCI extends TravisBase { 34 | val name = "travis-ci" 35 | } 36 | 37 | case object TravisPro extends TravisBase { 38 | val name = "travis-pro" 39 | } 40 | 41 | case object GitHubActions extends CIService { 42 | val name = "" 43 | val jobId: Option[String] = sys.env.get("GITHUB_RUN_ID") 44 | 45 | // https://github.com/coverallsapp/github-action/blob/master/src/run.ts#L31-L40 46 | val pullRequest: Option[String] = for { 47 | eventName <- sys.env.get("GITHUB_EVENT_NAME") 48 | if eventName.startsWith("pull_request") 49 | payloadPath <- sys.env.get("GITHUB_EVENT_PATH") 50 | prNumber <- getPrNumber(payloadPath) 51 | } yield prNumber 52 | 53 | // https://docs.github.com/en/actions/learn-github-actions/environment-variables 54 | val currentBranch: Option[String] = pullRequest match { 55 | case Some(_) => sys.env.get("GITHUB_HEAD_REF") 56 | case None => sys.env.get("GITHUB_REF_NAME") 57 | } 58 | 59 | def getPrNumber(payloadPath: String): Option[String] = { 60 | var source: BufferedSource = null 61 | val lines = 62 | try { 63 | source = Source.fromFile(payloadPath, "utf-8") 64 | Some(source.getLines.mkString) 65 | } catch { 66 | case NonFatal(_) => None 67 | } finally { 68 | if (source != null) 69 | source.close() 70 | } 71 | 72 | lines match { 73 | case Some(ls) => getFromJson(ls, "number") 74 | case None => None 75 | } 76 | 77 | } 78 | 79 | def getFromJson(lines: String, element: String): Option[String] = { 80 | parser.parse(lines) match { 81 | case Right(json) => 82 | json.findAllByKey(element) match { 83 | case prNumber :: _ => Some(prNumber.toString) 84 | case _ => None 85 | } 86 | case Left(_) => None 87 | } 88 | } 89 | 90 | override def coverallsAuth( 91 | userRepoToken: Option[String] 92 | ): Option[CoverallsAuth] = { 93 | userRepoToken match { 94 | case Some(token) if token.matches("gh._.+") => 95 | // The token passed in COVERALLS_REPO_TOKEN is a GitHub token (legacy behavior) 96 | // (https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats/#identifiable-prefixes) 97 | Some(CIServiceToken(token)) 98 | case Some(token) => 99 | // The token passed is a Coveralls one 100 | Some(CoverallsRepoToken(token)) 101 | case None => 102 | // COVERALLS_REPO_TOKEN is not defined; lookup GITHUB_TOKEN (available on all GitHub 103 | // Actions jobs) instead as a last resort 104 | sys.env.get("GITHUB_TOKEN").map(CIServiceToken) 105 | } 106 | } 107 | } 108 | 109 | object CircleCI extends CIService { 110 | def name: String = "circleci" 111 | def jobId: Option[String] = sys.env.get("CIRCLE_BUILD_NUM") 112 | def pullRequest: Option[String] = 113 | sys.env.get("CIRCLE_PULL_REQUEST").map(_.split("/").last) 114 | def currentBranch: Option[String] = sys.env.get("CIRCLE_BRANCH") 115 | } 116 | -------------------------------------------------------------------------------- /src/main/scala/org/scoverage/coveralls/CoberturaMultiSourceReader.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | import sbt.Logger 4 | 5 | import java.io.File 6 | import scala.io.{BufferedSource, Source} 7 | import scala.util.control.NonFatal 8 | 9 | class CoberturaMultiSourceReader( 10 | coberturaFile: File, 11 | sourceDirs: Seq[File], 12 | sourceEncoding: Option[String] 13 | )(implicit log: Logger) { 14 | log.debug( 15 | s"sbt-coveralls: CobertaMultiSourceReader: coberturaFile: $coberturaFile" 16 | ) 17 | log.debug( 18 | s"sbt-coveralls: CobertaMultiSourceReader: sourceDirs: $sourceDirs" 19 | ) 20 | 21 | require(sourceDirs.nonEmpty, "Given empty sequence of source directories") 22 | 23 | // there must not be nested source directories 24 | val relatedDirs = sourceDirs.combinations(2).find { case Seq(a, b) => 25 | isChild(a, b) 26 | } 27 | 28 | require( 29 | relatedDirs.isEmpty, 30 | "Source directories must not be nested: " + 31 | s"${relatedDirs.get(0).getCanonicalPath} is contained in ${relatedDirs.get(1).getCanonicalPath}" 32 | ) 33 | 34 | /** Checks whether a file belongs to another directory, recursively 35 | * @param child 36 | * \- a child file or directory 37 | * @param parent 38 | * \- a parent directory 39 | * @return 40 | * false if parent is not a directory or if child does not belongs to the 41 | * file tree rooted at parent. It returns false if child and parent points 42 | * to the same directory 43 | */ 44 | def isChild(child: File, parent: File): Boolean = { 45 | val childPath = child.getCanonicalFile.toPath 46 | val parentPath = parent.getCanonicalFile.toPath 47 | childPath != parentPath && childPath.startsWith(parentPath) 48 | } 49 | 50 | val reportXML: xml.Elem = XmlHelper.loadXmlFile(coberturaFile) 51 | 52 | private val lineCoverageMap = { 53 | (reportXML \\ "class").foldLeft(Map.empty[String, Map[Int, Int]]) { 54 | (map, n) => 55 | val relativeFileName = (n \ "@filename").toString() 56 | val lineCoverage = (n \ "lines" \ "line").map(l => 57 | (l \ "@number").toString().toInt -> (l \ "@hits").toString().toInt 58 | ) 59 | map + (relativeFileName -> (map.getOrElse( 60 | relativeFileName, 61 | Map.empty 62 | ) ++ lineCoverage)) 63 | } 64 | } 65 | 66 | /** A sequence of source files paths that are relative to some source 67 | * directory 68 | */ 69 | private def sourceFilesRelative: Set[String] = lineCoverageMap.keySet 70 | 71 | def sourceFiles: Set[File] = { 72 | log.debug( 73 | s"sbt-coveralls: CobertaMultiSourceReader: sourceFiles: sourceFilesRelative: $sourceFilesRelative" 74 | ) 75 | log.debug( 76 | s"sbt-coveralls: CobertaMultiSourceReader: sourceFiles: sourceDirs: $sourceDirs" 77 | ) 78 | val sfs = for { 79 | relativePath <- sourceFilesRelative 80 | sourceDir <- sourceDirs 81 | // only one directory contains the file 82 | sourceFile = new File(sourceDir, relativePath) 83 | if sourceFile.exists 84 | } yield sourceFile 85 | log.debug( 86 | s"sbt-coveralls: CobertaMultiSourceReader: sourceFiles: sourceFiles: $sfs" 87 | ) 88 | sfs 89 | } 90 | 91 | def sourceFilenames: Set[String] = sourceFiles.map(_.getCanonicalPath) 92 | 93 | /** Splits a path to a source file into two parts: 94 | * 1. the absolute path to source directory that contain this sourceFile 2. 95 | * the relative path to the file Note: that paths contains 96 | * File.separator that is dependant on the system 97 | * 98 | * @return 99 | * a tuple (a,b) such that "a/b" is the canonical path to sourceFile throws 100 | * IllegalArgumentException when a given file does not belongs to any of 101 | * the source directories 102 | */ 103 | def splitPath(sourceFile: File): (String, String) = { 104 | val parentDir = sourceDirs.find(p => isChild(sourceFile, p)) 105 | require( 106 | parentDir.isDefined, 107 | s"The file ${sourceFile.getCanonicalPath} does not belong to any of " + 108 | s"the source directories ${sourceDirs.map(_.getCanonicalPath)}" 109 | ) 110 | 111 | val prefix = parentDir.get.getCanonicalPath 112 | val relativePath = sourceFile.getCanonicalPath.substring(prefix.length + 1) 113 | (prefix, relativePath) 114 | } 115 | 116 | protected def lineCoverage(sourceFile: String): Map[Int, Int] = { 117 | val filenamePath = 118 | splitPath(new File(sourceFile))._2 119 | 120 | lineCoverageMap(filenamePath) 121 | } 122 | 123 | def reportForSource(source: String): SourceFileReport = { 124 | var fileSrc: BufferedSource = null 125 | try { 126 | fileSrc = sourceEncoding match { 127 | case Some(enc) => Source.fromFile(source, enc) 128 | case None => Source.fromFile(source) 129 | } 130 | 131 | val lineCount = fileSrc.getLines().size 132 | 133 | val lineHitMap = lineCoverage(source) 134 | val fullLineHit = (0 until lineCount).map(i => lineHitMap.get(i + 1)) 135 | 136 | SourceFileReport(source, fullLineHit.toList) 137 | } catch { 138 | case NonFatal(e) => 139 | throw e 140 | } finally { 141 | if (fileSrc != null) 142 | fileSrc.close() 143 | } 144 | } 145 | } 146 | 147 | case class SourceFileReport(file: String, lineCoverage: List[Option[Int]]) {} 148 | -------------------------------------------------------------------------------- /src/main/scala/org/scoverage/coveralls/CoverallPayloadWriter.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | import com.fasterxml.jackson.core.{JsonEncoding, JsonFactory, JsonGenerator} 4 | import sbt.Logger 5 | 6 | import java.io.{File, FileInputStream} 7 | import java.security.{DigestInputStream, MessageDigest} 8 | 9 | class CoverallPayloadWriter( 10 | repoRootDir: File, 11 | coverallsFile: File, 12 | coverallsAuth: CoverallsAuth, 13 | service: Option[CIService], 14 | parallel: Boolean, 15 | gitClient: GitClient 16 | )(implicit log: Logger) { 17 | import gitClient._ 18 | 19 | val gen: JsonGenerator = generator(coverallsFile) 20 | 21 | def generator(file: File): JsonGenerator = { 22 | if (!file.getParentFile.exists) file.getParentFile.mkdirs 23 | val factory = new JsonFactory 24 | factory.createGenerator(file, JsonEncoding.UTF8) 25 | } 26 | 27 | def start(): Unit = { 28 | gen.writeStartObject() 29 | 30 | def writeOpt(fieldName: String, holder: Option[String]): Unit = 31 | holder foreach { gen.writeStringField(fieldName, _) } 32 | 33 | coverallsAuth match { 34 | case CoverallsRepoToken(token) => 35 | gen.writeStringField("repo_token", token) 36 | case CIServiceToken(token) => 37 | gen.writeStringField("repo_token", token) 38 | case NoTokenNeeded => 39 | } 40 | 41 | writeOpt("service_name", service.map(_.name)) 42 | writeOpt("service_job_id", service.flatMap(_.jobId)) 43 | writeOpt("service_pull_request", service.flatMap(_.pullRequest)) 44 | writeOpt("flag_name", sys.env.get("COVERALLS_FLAG_NAME")) 45 | 46 | gen.writeBooleanField("parallel", parallel) 47 | 48 | addGitInfo() 49 | 50 | gen.writeFieldName("source_files") 51 | gen.writeStartArray() 52 | } 53 | 54 | private def addGitInfo(): Unit = { 55 | gen.writeFieldName("git") 56 | gen.writeStartObject() 57 | 58 | gen.writeFieldName("head") 59 | gen.writeStartObject() 60 | 61 | val commitInfo = lastCommit() 62 | 63 | gen.writeStringField("id", commitInfo.id) 64 | gen.writeStringField("author_name", commitInfo.authorName) 65 | gen.writeStringField("author_email", commitInfo.authorEmail) 66 | gen.writeStringField("committer_name", commitInfo.committerName) 67 | gen.writeStringField("committer_email", commitInfo.committerEmail) 68 | gen.writeStringField("message", commitInfo.shortMessage) 69 | 70 | gen.writeEndObject() 71 | 72 | gen.writeStringField( 73 | "branch", 74 | service.flatMap(_.currentBranch).getOrElse(gitClient.currentBranch) 75 | ) 76 | 77 | gen.writeFieldName("remotes") 78 | gen.writeStartArray() 79 | 80 | addGitRemotes(remotes) 81 | 82 | gen.writeEndArray() 83 | 84 | gen.writeEndObject() 85 | } 86 | 87 | private def addGitRemotes(remotes: Seq[String]): Unit = { 88 | remotes.foreach(remote => { 89 | gen.writeStartObject() 90 | gen.writeStringField("name", remote) 91 | gen.writeStringField("url", remoteUrl(remote)) 92 | gen.writeEndObject() 93 | }) 94 | } 95 | 96 | def addSourceFile(report: SourceFileReport): Unit = { 97 | val repoRootDirStr = repoRootDir.getCanonicalPath + File.separator 98 | 99 | // create a name relative to the project root (rather than the module root) 100 | // this is needed so that coveralls can find the file in git. 101 | val fileName = report.file.replace(repoRootDirStr, "") 102 | 103 | gen.writeStartObject() 104 | gen.writeStringField("name", fileName) 105 | 106 | val sourceDigest = computeSourceDigest(report.file) 107 | 108 | gen.writeStringField("source_digest", sourceDigest) 109 | 110 | gen.writeFieldName("coverage") 111 | gen.writeStartArray() 112 | report.lineCoverage.foreach { 113 | case Some(x) => gen.writeNumber(x) 114 | case _ => gen.writeNull() 115 | } 116 | gen.writeEndArray() 117 | gen.writeEndObject() 118 | } 119 | 120 | private def computeSourceDigest(path: String) = { 121 | val buffer = new Array[Byte](8192) 122 | val md5 = MessageDigest.getInstance("MD5") 123 | 124 | val dis = new DigestInputStream(new FileInputStream(new File(path)), md5) 125 | try { while (dis.read(buffer) != -1) {} } 126 | finally { dis.close() } 127 | 128 | md5.digest.map("%02x".format(_)).mkString.toUpperCase 129 | } 130 | 131 | def end(): Unit = { 132 | gen.writeEndArray() 133 | gen.writeEndObject() 134 | gen.flush() 135 | gen.close() 136 | } 137 | 138 | def flush(): Unit = { 139 | gen.flush() 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/scala/org/scoverage/coveralls/CoverallsAuth.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | /** The strategy to use when authenticating against Coveralls. 4 | */ 5 | sealed trait CoverallsAuth extends Product with Serializable 6 | 7 | /** Auth strategy where a Coveralls-specific token is used. Works with every CI 8 | * service. 9 | */ 10 | final case class CoverallsRepoToken(token: String) extends CoverallsAuth 11 | 12 | /** Auth strategy where a token specific to the CI service is used, such as a 13 | * GitHub token. Works on selected CI services supported by Coveralls. 14 | */ 15 | final case class CIServiceToken(token: String) extends CoverallsAuth 16 | 17 | /** Auth strategy where no token is passed. This seems to work for Travis. 18 | */ 19 | case object NoTokenNeeded extends CoverallsAuth 20 | -------------------------------------------------------------------------------- /src/main/scala/org/scoverage/coveralls/CoverallsClient.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import com.fasterxml.jackson.module.scala.DefaultScalaModule 5 | import scalaj.http.HttpOptions._ 6 | import scalaj.http.{Http, MultiPart} 7 | 8 | import java.io.File 9 | import java.net.{InetAddress, Socket} 10 | import javax.net.ssl.{SSLSocket, SSLSocketFactory} 11 | import scala.io.{BufferedSource, Codec, Source} 12 | import scala.util.control.NonFatal 13 | 14 | class CoverallsClient(endpoint: String, httpClient: HttpClient) { 15 | 16 | val mapper: ObjectMapper = newMapper 17 | def url: String = s"$endpoint/api/v1/jobs" 18 | 19 | def newMapper: ObjectMapper = { 20 | val mapper = new ObjectMapper 21 | mapper.registerModule(DefaultScalaModule) 22 | mapper 23 | } 24 | 25 | def postFile(file: File): CoverallsResponse = { 26 | val codec: Codec = Codec.UTF8 27 | var source: BufferedSource = null 28 | try { 29 | source = Source.fromFile(file)(codec) 30 | // API want newlines encoded as \n, not sure about other escape chars 31 | // https://coveralls.zendesk.com/hc/en-us/articles/201774865-API-Introduction 32 | val bytes = source.getLines().mkString("\\n").getBytes(codec.charSet) 33 | 34 | httpClient.multipart( 35 | url, 36 | "json_file", 37 | "json_file.json", 38 | "application/json; charset=utf-8", 39 | bytes 40 | ) match { 41 | case CoverallHttpResponse(_, body) => 42 | try { 43 | mapper.readValue(body, classOf[CoverallsResponse]) 44 | } catch { 45 | case NonFatal(e) => 46 | CoverallsResponse( 47 | "Failed to parse response: " + e, 48 | error = true, 49 | "" 50 | ) 51 | } 52 | } 53 | } catch { 54 | case NonFatal(e) => 55 | throw e 56 | } finally { 57 | if (source != null) 58 | source.close() 59 | } 60 | } 61 | } 62 | 63 | object CoverallsClient { 64 | val tokenErrorString = "Couldn't find a repository matching this job" 65 | val errorResponseTitleTag = "title" 66 | val defaultErrorMessage = "ERROR (no title found)" 67 | } 68 | 69 | case class CoverallHttpResponse(responseCode: Int, body: String) 70 | 71 | case class CoverallsResponse(message: String, error: Boolean, url: String) 72 | 73 | trait HttpClient { 74 | def multipart( 75 | url: String, 76 | name: String, 77 | filename: String, 78 | mime: String, 79 | data: Array[Byte] 80 | ): CoverallHttpResponse 81 | } 82 | 83 | class ScalaJHttpClient extends HttpClient { 84 | 85 | val openJdkSafeSsl = new OpenJdkSafeSsl 86 | 87 | def multipart( 88 | url: String, 89 | name: String, 90 | filename: String, 91 | mime: String, 92 | data: Array[Byte] 93 | ): CoverallHttpResponse = try { 94 | val request = Http(url) 95 | .postMulti(MultiPart(name, filename, mime, data)) 96 | .option(connTimeout(60000)) 97 | .option(readTimeout(60000)) 98 | .option(sslSocketFactory(openJdkSafeSsl)) 99 | 100 | val response = request.execute() 101 | CoverallHttpResponse(response.code, response.body) 102 | } catch { 103 | case e: Exception => CoverallHttpResponse(500, e.getMessage) 104 | } 105 | } 106 | 107 | class OpenJdkSafeSsl extends SSLSocketFactory { 108 | val child: SSLSocketFactory = 109 | SSLSocketFactory.getDefault.asInstanceOf[SSLSocketFactory] 110 | 111 | val safeCiphers: Array[String] = Array( 112 | "SSL_RSA_WITH_RC4_128_MD5", 113 | "SSL_RSA_WITH_RC4_128_SHA", 114 | "TLS_RSA_WITH_AES_128_CBC_SHA", 115 | "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", 116 | "TLS_DHE_DSS_WITH_AES_128_CBC_SHA", 117 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", 118 | "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", 119 | "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", 120 | "SSL_RSA_WITH_3DES_EDE_CBC_SHA", 121 | "SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA", 122 | "SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA", 123 | "SSL_RSA_WITH_DES_CBC_SHA", 124 | "SSL_DHE_RSA_WITH_DES_CBC_SHA", 125 | "SSL_DHE_DSS_WITH_DES_CBC_SHA", 126 | "SSL_RSA_EXPORT_WITH_RC4_40_MD5", 127 | "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA", 128 | "SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", 129 | "SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", 130 | "TLS_EMPTY_RENEGOTIATION_INFO_SCSV" 131 | ) intersect SSLSocketFactory.getDefault 132 | .asInstanceOf[SSLSocketFactory] 133 | .getSupportedCipherSuites 134 | 135 | def getDefaultCipherSuites: Array[String] = Array.empty 136 | 137 | def getSupportedCipherSuites: Array[String] = Array.empty 138 | 139 | def createSocket(p1: Socket, p2: String, p3: Int, p4: Boolean): Socket = 140 | safeSocket( 141 | child.createSocket(p1, p2, p3, p4) 142 | ) 143 | 144 | def createSocket(p1: String, p2: Int): Socket = safeSocket( 145 | child.createSocket(p1, p2) 146 | ) 147 | 148 | def createSocket(p1: String, p2: Int, p3: InetAddress, p4: Int): Socket = 149 | safeSocket( 150 | child.createSocket(p1, p2, p3, p4) 151 | ) 152 | 153 | def createSocket(p1: InetAddress, p2: Int): Socket = safeSocket( 154 | child.createSocket(p1, p2) 155 | ) 156 | 157 | def createSocket(p1: InetAddress, p2: Int, p3: InetAddress, p4: Int): Socket = 158 | safeSocket(child.createSocket(p1, p2, p3, p4)) 159 | 160 | def safeSocket(sock: Socket): Socket = sock match { 161 | case ssl: SSLSocket => 162 | ssl.setEnabledCipherSuites(safeCiphers); ssl 163 | case other => other 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/scala/org/scoverage/coveralls/CoverallsPlugin.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | import sbt.Keys._ 4 | import sbt.internal.util.ManagedLogger 5 | import sbt.{ScopeFilter, ThisProject, _} 6 | 7 | import java.io.File 8 | import scala.io.{BufferedSource, Source} 9 | import scala.util.control.NonFatal 10 | 11 | object Imports { 12 | object CoverallsKeys { 13 | val coverallsFile = SettingKey[File]("coverallsFile") 14 | val coverallsToken = SettingKey[Option[String]]("coverallsRepoToken") 15 | val coverallsTokenFile = SettingKey[Option[String]]("coverallsTokenFile") 16 | val coverallsService = SettingKey[Option[CIService]]("coverallsService") 17 | val coverallsFailBuildOnError = SettingKey[Boolean]( 18 | "coverallsFailBuildOnError", 19 | "fail build if coveralls step fails" 20 | ) 21 | val coberturaFile = SettingKey[File]("coberturaFile") 22 | @deprecated( 23 | "Read https://github.com/scoverage/sbt-coveralls#custom-source-file-encoding", 24 | "1.2.5" 25 | ) 26 | val coverallsEncoding = SettingKey[String]("encoding") 27 | val coverallsEndpoint = SettingKey[Option[String]]("coverallsEndpoint") 28 | val coverallsGitRepoLocation = 29 | SettingKey[Option[String]]("coveralls-git-repo") 30 | val coverallsParallel = SettingKey[Boolean]("coverallsParallel") 31 | } 32 | } 33 | 34 | object CoverallsPlugin extends AutoPlugin { 35 | override def trigger = allRequirements 36 | 37 | val autoImport = Imports 38 | import autoImport.* 39 | import CoverallsKeys.* 40 | 41 | lazy val coveralls = taskKey[Unit]( 42 | "Uploads scala code coverage to coveralls.io" 43 | ) 44 | 45 | override lazy val projectSettings: Seq[Setting[_]] = Seq( 46 | coveralls := coverallsTask.value, 47 | coveralls / aggregate := false, 48 | coverallsFailBuildOnError := false, 49 | coverallsToken := None, 50 | coverallsTokenFile := None, 51 | coverallsEndpoint := Some("https://coveralls.io"), 52 | coverallsService := { 53 | if (travisJobIdent.isDefined) Some(TravisCI) 54 | else if (githubActionsRunIdent.isDefined) Some(GitHubActions) 55 | else None 56 | }, 57 | coverallsFile := crossTarget.value / "coveralls.json", 58 | coberturaFile := crossTarget.value / "coverage-report" / "cobertura.xml", 59 | coverallsGitRepoLocation := Some("."), 60 | coverallsParallel := sys.env.get("COVERALLS_PARALLEL").contains("true") 61 | ) 62 | 63 | val aggregateFilter = ScopeFilter( 64 | inAggregates(ThisProject), 65 | inConfigurations(Compile) 66 | ) // must be outside of the 'coverageAggregate' task (see: https://github.com/sbt/sbt/issues/1095 or https://github.com/sbt/sbt/issues/780) 67 | 68 | def coverallsTask = Def.task { 69 | implicit val log: ManagedLogger = streams.value.log 70 | 71 | if (!coberturaFile.value.exists) { 72 | sys.error( 73 | "Could not find the cobertura.xml file. Did you call coverageAggregate?" 74 | ) 75 | } 76 | 77 | val repoToken = 78 | userRepoToken(coverallsToken.value, coverallsTokenFile.value) 79 | 80 | val coverallsAuthOpt = coverallsService.value match { 81 | case Some(ciService) => ciService.coverallsAuth(repoToken) 82 | case None => repoToken.map(CoverallsRepoToken) 83 | } 84 | 85 | val coverallsAuth = coverallsAuthOpt.getOrElse { 86 | sys.error(""" 87 | |Could not find any way to authenticate against Coveralls. 88 | | - If running from Travis, make sure the TRAVIS_JOB_ID env variable is set 89 | | - If running from GitHub CI, set the GITHUB_TOKEN env variable to ${{ secrets.GITHUB_TOKEN }} 90 | | - Otherwise, to set up your repo token read https://github.com/scoverage/sbt-coveralls#specifying-your-repo-token 91 | """.stripMargin) 92 | } 93 | 94 | val sourcesEnc = sourceEncoding((Compile / scalacOptions).value) 95 | 96 | val endpoint = userEndpoint(coverallsEndpoint.value).get 97 | 98 | val coverallsClient = new CoverallsClient(endpoint, apiHttpClient) 99 | 100 | val repoRootDirectory = 101 | new File(coverallsGitRepoLocation.value getOrElse ".") 102 | 103 | val writer = new CoverallPayloadWriter( 104 | repoRootDirectory, 105 | coverallsFile.value, 106 | coverallsAuth, 107 | coverallsService.value, 108 | coverallsParallel.value, 109 | new GitClient(repoRootDirectory) 110 | ) 111 | 112 | writer.start() 113 | 114 | // include all of the sources (from all modules) 115 | val allSources = sourceDirectories 116 | .all(aggregateFilter) 117 | .value 118 | .flatten 119 | .filter(_.isDirectory()) 120 | .distinct 121 | 122 | val reader = new CoberturaMultiSourceReader( 123 | coberturaFile.value, 124 | allSources, 125 | sourcesEnc 126 | ) 127 | 128 | log.info( 129 | s"sbt-coveralls: Generating reports for ${reader.sourceFilenames.size} files ..." 130 | ) 131 | 132 | val fileReports = 133 | reader.sourceFilenames.par.map(reader.reportForSource).seq 134 | 135 | log.info( 136 | s"sbt-coveralls: Adding file reports to the coveralls file (${coverallsFile.value.getName}) ..." 137 | ) 138 | 139 | fileReports.foreach(writer.addSourceFile) 140 | 141 | writer.end() 142 | 143 | log.info( 144 | s"sbt-coveralls: Uploading the coveralls file (${coverallsFile.value.getName}) ..." 145 | ) 146 | 147 | val res = coverallsClient.postFile(coverallsFile.value) 148 | val failBuildOnError = coverallsFailBuildOnError.value 149 | 150 | if (res.error) { 151 | val errorMessage = 152 | s""" 153 | |Uploading to $endpoint failed: ${res.message} 154 | |${if (res.message.contains(CoverallsClient.tokenErrorString)) 155 | s"The error message '${CoverallsClient.tokenErrorString}' can mean your repo token is incorrect." 156 | else ""} 157 | """.stripMargin 158 | if (failBuildOnError) 159 | sys.error(errorMessage) 160 | else 161 | log.error(errorMessage) 162 | } else { 163 | log.info( 164 | s"sbt-coveralls: Uploading to $endpoint succeeded (results may not appear immediately): ${res.message}/${res.url}" 165 | ) 166 | } 167 | } 168 | 169 | def apiHttpClient: ScalaJHttpClient = new ScalaJHttpClient 170 | 171 | def travisJobIdent: Option[String] = sys.env.get("TRAVIS_JOB_ID") 172 | 173 | def githubActionsRunIdent: Option[String] = sys.env.get("GITHUB_RUN_ID") 174 | 175 | def repoTokenFromFile(path: String): Option[String] = { 176 | var source: BufferedSource = null 177 | try { 178 | source = Source.fromFile(path) 179 | val repoToken = source.mkString.trim 180 | source.close() 181 | Some(repoToken) 182 | } catch { 183 | case NonFatal(_) => None 184 | } finally { 185 | if (source != null) 186 | source.close() 187 | } 188 | } 189 | 190 | def userRepoToken( 191 | coverallsToken: Option[String], 192 | coverallsTokenFile: Option[String] 193 | ): Option[String] = 194 | sys.env 195 | .get("COVERALLS_REPO_TOKEN") 196 | .orElse(coverallsToken) 197 | .orElse(coverallsTokenFile.flatMap(repoTokenFromFile)) 198 | 199 | def userEndpoint(coverallsEndpoint: Option[String]): Option[String] = 200 | sys.env 201 | .get("COVERALLS_ENDPOINT") 202 | .orElse(coverallsEndpoint) 203 | 204 | private def sourceEncoding(scalacOptions: Seq[String]): Option[String] = { 205 | val i = scalacOptions.indexOf("-encoding") + 1 206 | if (i > 0 && i < scalacOptions.length) Some(scalacOptions(i)) else None 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/main/scala/org/scoverage/coveralls/GitClient.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | import org.eclipse.jgit.api.Git 4 | import org.eclipse.jgit.lib.{Repository, StoredConfig} 5 | import org.eclipse.jgit.storage.file.FileRepositoryBuilder 6 | import org.scoverage.coveralls.GitClient.GitRevision 7 | import sbt.Logger 8 | 9 | import java.io.File 10 | import java.nio.file.Files.lines 11 | import scala.util.matching.Regex 12 | 13 | object GitClient { 14 | case class GitRevision( 15 | id: String, 16 | authorName: String, 17 | authorEmail: String, 18 | committerName: String, 19 | committerEmail: String, 20 | shortMessage: String 21 | ) 22 | } 23 | 24 | class GitClient(cwd: File)(implicit log: Logger) { 25 | 26 | import scala.collection.JavaConverters._ 27 | 28 | val gitDirLineRegex: Regex = """^gitdir: (.*)""".r 29 | 30 | val gitFile = new File(cwd, ".git") 31 | 32 | val resolvedGitDir: File = 33 | if (gitFile.isFile) 34 | lines(gitFile.toPath) 35 | .iterator() 36 | .asScala 37 | .toList match { 38 | case gitDirLineRegex(dir) :: Nil ⇒ 39 | log.info(s"Resolved git submodule file $gitFile to $dir") 40 | new File(dir) 41 | case lines ⇒ 42 | throw new IllegalArgumentException( 43 | s"Expected single 'gitdir' line in .git file, found:\n\t${lines.mkString("\n\t")}" 44 | ) 45 | } 46 | else 47 | gitFile 48 | 49 | val repository: Repository = FileRepositoryBuilder.create(resolvedGitDir) 50 | val storedConfig: StoredConfig = repository.getConfig 51 | log.info("Repository = " + repository.getDirectory) 52 | 53 | def remotes: Seq[String] = { 54 | storedConfig.getSubsections("remote").asScala.to[Seq] 55 | } 56 | 57 | def remoteUrl(remoteName: String): String = { 58 | storedConfig.getString("remote", remoteName, "url") 59 | } 60 | 61 | def currentBranch: String = repository.getBranch 62 | 63 | def lastCommit(): GitRevision = { 64 | val git = new Git(repository) 65 | val headRev = git.log().setMaxCount(1).call().asScala.head 66 | val id = headRev.getId 67 | val author = headRev.getAuthorIdent 68 | val committer = headRev.getCommitterIdent 69 | GitRevision( 70 | id.name, 71 | author.getName, 72 | author.getEmailAddress, 73 | committer.getName, 74 | committer.getEmailAddress, 75 | headRev.getShortMessage 76 | ) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/scala/org/scoverage/coveralls/Utils.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | import java.io.File 4 | import scala.annotation.tailrec 5 | 6 | object Utils { 7 | def mkFileFromPath(path: Seq[String]): File = { 8 | require(path.nonEmpty, "path cannot be empty") 9 | val p :: ps = path 10 | mkFileFromPath(new File(p), ps) 11 | } 12 | 13 | @tailrec 14 | def mkFileFromPath(base: File, path: Seq[String]): File = path match { 15 | case p :: ps => mkFileFromPath(new File(base, p), ps) 16 | case _ => base 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/scala/org/scoverage/coveralls/XmlHelper.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | import org.xml.sax.InputSource 4 | 5 | import java.io.{File, FileInputStream} 6 | import javax.xml.parsers.SAXParserFactory 7 | import scala.util.Try 8 | import scala.xml.XML 9 | 10 | /** A simple utility around XML.loadXml that doesn't depend on external DTD 11 | * fetching and processing. This avoids random failures when coburtura.xml DTD 12 | * clauses point to dead domains 13 | */ 14 | object XmlHelper { 15 | 16 | private[this] val factory: SAXParserFactory = locally { 17 | val f = SAXParserFactory.newInstance() 18 | f.setValidating(false) 19 | f.setFeature("http://xml.org/sax/features/validation", false) 20 | f.setFeature( 21 | "http://apache.org/xml/features/nonvalidating/load-dtd-grammar", 22 | false 23 | ) 24 | f.setFeature( 25 | "http://apache.org/xml/features/nonvalidating/load-external-dtd", 26 | false 27 | ) 28 | f 29 | } 30 | 31 | def loadXmlFile(file: File): xml.Elem = { 32 | val parser = factory.newSAXParser() 33 | val stream = new FileInputStream(file) 34 | try { 35 | XML.loadXML(new InputSource(stream), parser) 36 | } finally { 37 | Try(stream.close()) 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/test/resources/example-pr-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347", 3 | "id": 1, 4 | "node_id": "MDExOlB1bGxSZXF1ZXN0MQ==", 5 | "html_url": "https://github.com/octocat/Hello-World/pull/1347", 6 | "diff_url": "https://github.com/octocat/Hello-World/pull/1347.diff", 7 | "patch_url": "https://github.com/octocat/Hello-World/pull/1347.patch", 8 | "issue_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347", 9 | "commits_url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347/commits", 10 | "review_comments_url": "https://api.github.com/repos/octocat/Hello-World/pulls/1347/comments", 11 | "review_comment_url": "https://api.github.com/repos/octocat/Hello-World/pulls/comments{/number}", 12 | "comments_url": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments", 13 | "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e", 14 | "number": 1347, 15 | "state": "open", 16 | "locked": true, 17 | "title": "Amazing new feature", 18 | "user": { 19 | "login": "octocat", 20 | "id": 1, 21 | "node_id": "MDQ6VXNlcjE=", 22 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 23 | "gravatar_id": "", 24 | "url": "https://api.github.com/users/octocat", 25 | "html_url": "https://github.com/octocat", 26 | "followers_url": "https://api.github.com/users/octocat/followers", 27 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 28 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 29 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 30 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 31 | "organizations_url": "https://api.github.com/users/octocat/orgs", 32 | "repos_url": "https://api.github.com/users/octocat/repos", 33 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 34 | "received_events_url": "https://api.github.com/users/octocat/received_events", 35 | "type": "User", 36 | "site_admin": false 37 | }, 38 | "body": "Please pull these awesome changes in!", 39 | "labels": [ 40 | { 41 | "id": 208045946, 42 | "node_id": "MDU6TGFiZWwyMDgwNDU5NDY=", 43 | "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug", 44 | "name": "bug", 45 | "description": "Something isn't working", 46 | "color": "f29513", 47 | "default": true 48 | } 49 | ], 50 | "milestone": { 51 | "url": "https://api.github.com/repos/octocat/Hello-World/milestones/1", 52 | "html_url": "https://github.com/octocat/Hello-World/milestones/v1.0", 53 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/milestones/1/labels", 54 | "id": 1002604, 55 | "node_id": "MDk6TWlsZXN0b25lMTAwMjYwNA==", 56 | "number": 1, 57 | "state": "open", 58 | "title": "v1.0", 59 | "description": "Tracking milestone for version 1.0", 60 | "creator": { 61 | "login": "octocat", 62 | "id": 1, 63 | "node_id": "MDQ6VXNlcjE=", 64 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 65 | "gravatar_id": "", 66 | "url": "https://api.github.com/users/octocat", 67 | "html_url": "https://github.com/octocat", 68 | "followers_url": "https://api.github.com/users/octocat/followers", 69 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 70 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 71 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 72 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 73 | "organizations_url": "https://api.github.com/users/octocat/orgs", 74 | "repos_url": "https://api.github.com/users/octocat/repos", 75 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 76 | "received_events_url": "https://api.github.com/users/octocat/received_events", 77 | "type": "User", 78 | "site_admin": false 79 | }, 80 | "open_issues": 4, 81 | "closed_issues": 8, 82 | "created_at": "2011-04-10T20:09:31Z", 83 | "updated_at": "2014-03-03T18:58:10Z", 84 | "closed_at": "2013-02-12T13:22:01Z", 85 | "due_on": "2012-10-09T23:39:01Z" 86 | }, 87 | "active_lock_reason": "too heated", 88 | "created_at": "2011-01-26T19:01:12Z", 89 | "updated_at": "2011-01-26T19:01:12Z", 90 | "closed_at": "2011-01-26T19:01:12Z", 91 | "merged_at": "2011-01-26T19:01:12Z", 92 | "merge_commit_sha": "e5bd3914e2e596debea16f433f57875b5b90bcd6", 93 | "assignee": { 94 | "login": "octocat", 95 | "id": 1, 96 | "node_id": "MDQ6VXNlcjE=", 97 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 98 | "gravatar_id": "", 99 | "url": "https://api.github.com/users/octocat", 100 | "html_url": "https://github.com/octocat", 101 | "followers_url": "https://api.github.com/users/octocat/followers", 102 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 103 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 104 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 105 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 106 | "organizations_url": "https://api.github.com/users/octocat/orgs", 107 | "repos_url": "https://api.github.com/users/octocat/repos", 108 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 109 | "received_events_url": "https://api.github.com/users/octocat/received_events", 110 | "type": "User", 111 | "site_admin": false 112 | }, 113 | "assignees": [ 114 | { 115 | "login": "octocat", 116 | "id": 1, 117 | "node_id": "MDQ6VXNlcjE=", 118 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 119 | "gravatar_id": "", 120 | "url": "https://api.github.com/users/octocat", 121 | "html_url": "https://github.com/octocat", 122 | "followers_url": "https://api.github.com/users/octocat/followers", 123 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 124 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 125 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 126 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 127 | "organizations_url": "https://api.github.com/users/octocat/orgs", 128 | "repos_url": "https://api.github.com/users/octocat/repos", 129 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 130 | "received_events_url": "https://api.github.com/users/octocat/received_events", 131 | "type": "User", 132 | "site_admin": false 133 | }, 134 | { 135 | "login": "hubot", 136 | "id": 1, 137 | "node_id": "MDQ6VXNlcjE=", 138 | "avatar_url": "https://github.com/images/error/hubot_happy.gif", 139 | "gravatar_id": "", 140 | "url": "https://api.github.com/users/hubot", 141 | "html_url": "https://github.com/hubot", 142 | "followers_url": "https://api.github.com/users/hubot/followers", 143 | "following_url": "https://api.github.com/users/hubot/following{/other_user}", 144 | "gists_url": "https://api.github.com/users/hubot/gists{/gist_id}", 145 | "starred_url": "https://api.github.com/users/hubot/starred{/owner}{/repo}", 146 | "subscriptions_url": "https://api.github.com/users/hubot/subscriptions", 147 | "organizations_url": "https://api.github.com/users/hubot/orgs", 148 | "repos_url": "https://api.github.com/users/hubot/repos", 149 | "events_url": "https://api.github.com/users/hubot/events{/privacy}", 150 | "received_events_url": "https://api.github.com/users/hubot/received_events", 151 | "type": "User", 152 | "site_admin": true 153 | } 154 | ], 155 | "requested_reviewers": [ 156 | { 157 | "login": "other_user", 158 | "id": 1, 159 | "node_id": "MDQ6VXNlcjE=", 160 | "avatar_url": "https://github.com/images/error/other_user_happy.gif", 161 | "gravatar_id": "", 162 | "url": "https://api.github.com/users/other_user", 163 | "html_url": "https://github.com/other_user", 164 | "followers_url": "https://api.github.com/users/other_user/followers", 165 | "following_url": "https://api.github.com/users/other_user/following{/other_user}", 166 | "gists_url": "https://api.github.com/users/other_user/gists{/gist_id}", 167 | "starred_url": "https://api.github.com/users/other_user/starred{/owner}{/repo}", 168 | "subscriptions_url": "https://api.github.com/users/other_user/subscriptions", 169 | "organizations_url": "https://api.github.com/users/other_user/orgs", 170 | "repos_url": "https://api.github.com/users/other_user/repos", 171 | "events_url": "https://api.github.com/users/other_user/events{/privacy}", 172 | "received_events_url": "https://api.github.com/users/other_user/received_events", 173 | "type": "User", 174 | "site_admin": false 175 | } 176 | ], 177 | "requested_teams": [ 178 | { 179 | "id": 1, 180 | "node_id": "MDQ6VGVhbTE=", 181 | "url": "https://api.github.com/teams/1", 182 | "html_url": "https://github.com/orgs/github/teams/justice-league", 183 | "name": "Justice League", 184 | "slug": "justice-league", 185 | "description": "A great team.", 186 | "privacy": "closed", 187 | "permission": "admin", 188 | "members_url": "https://api.github.com/teams/1/members{/member}", 189 | "repositories_url": "https://api.github.com/teams/1/repos" 190 | } 191 | ], 192 | "head": { 193 | "label": "octocat:new-topic", 194 | "ref": "new-topic", 195 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", 196 | "user": { 197 | "login": "octocat", 198 | "id": 1, 199 | "node_id": "MDQ6VXNlcjE=", 200 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 201 | "gravatar_id": "", 202 | "url": "https://api.github.com/users/octocat", 203 | "html_url": "https://github.com/octocat", 204 | "followers_url": "https://api.github.com/users/octocat/followers", 205 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 206 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 207 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 208 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 209 | "organizations_url": "https://api.github.com/users/octocat/orgs", 210 | "repos_url": "https://api.github.com/users/octocat/repos", 211 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 212 | "received_events_url": "https://api.github.com/users/octocat/received_events", 213 | "type": "User", 214 | "site_admin": false 215 | }, 216 | "repo": { 217 | "id": 1296269, 218 | "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", 219 | "name": "Hello-World", 220 | "full_name": "octocat/Hello-World", 221 | "owner": { 222 | "login": "octocat", 223 | "id": 1, 224 | "node_id": "MDQ6VXNlcjE=", 225 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 226 | "gravatar_id": "", 227 | "url": "https://api.github.com/users/octocat", 228 | "html_url": "https://github.com/octocat", 229 | "followers_url": "https://api.github.com/users/octocat/followers", 230 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 231 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 232 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 233 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 234 | "organizations_url": "https://api.github.com/users/octocat/orgs", 235 | "repos_url": "https://api.github.com/users/octocat/repos", 236 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 237 | "received_events_url": "https://api.github.com/users/octocat/received_events", 238 | "type": "User", 239 | "site_admin": false 240 | }, 241 | "private": false, 242 | "html_url": "https://github.com/octocat/Hello-World", 243 | "description": "This your first repo!", 244 | "fork": false, 245 | "url": "https://api.github.com/repos/octocat/Hello-World", 246 | "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", 247 | "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}", 248 | "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", 249 | "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}", 250 | "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", 251 | "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}", 252 | "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}", 253 | "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", 254 | "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}", 255 | "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors", 256 | "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments", 257 | "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads", 258 | "events_url": "https://api.github.com/repos/octocat/Hello-World/events", 259 | "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks", 260 | "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", 261 | "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", 262 | "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", 263 | "git_url": "git:github.com/octocat/Hello-World.git", 264 | "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", 265 | "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", 266 | "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}", 267 | "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", 268 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}", 269 | "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages", 270 | "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges", 271 | "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}", 272 | "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", 273 | "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}", 274 | "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}", 275 | "ssh_url": "git@github.com:octocat/Hello-World.git", 276 | "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers", 277 | "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", 278 | "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers", 279 | "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription", 280 | "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags", 281 | "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams", 282 | "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", 283 | "clone_url": "https://github.com/octocat/Hello-World.git", 284 | "mirror_url": "git:git.example.com/octocat/Hello-World", 285 | "hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks", 286 | "svn_url": "https://svn.github.com/octocat/Hello-World", 287 | "homepage": "https://github.com", 288 | "language": null, 289 | "forks_count": 9, 290 | "stargazers_count": 80, 291 | "watchers_count": 80, 292 | "size": 108, 293 | "default_branch": "master", 294 | "open_issues_count": 0, 295 | "topics": [ 296 | "octocat", 297 | "atom", 298 | "electron", 299 | "api" 300 | ], 301 | "has_issues": true, 302 | "has_projects": true, 303 | "has_wiki": true, 304 | "has_pages": false, 305 | "has_downloads": true, 306 | "has_discussions": false, 307 | "archived": false, 308 | "disabled": false, 309 | "pushed_at": "2011-01-26T19:06:43Z", 310 | "created_at": "2011-01-26T19:01:12Z", 311 | "updated_at": "2011-01-26T19:14:43Z", 312 | "permissions": { 313 | "admin": false, 314 | "push": false, 315 | "pull": true 316 | }, 317 | "allow_rebase_merge": true, 318 | "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", 319 | "allow_squash_merge": true, 320 | "allow_merge_commit": true, 321 | "allow_forking": true, 322 | "forks": 123, 323 | "open_issues": 123, 324 | "license": { 325 | "key": "mit", 326 | "name": "MIT License", 327 | "url": "https://api.github.com/licenses/mit", 328 | "spdx_id": "MIT", 329 | "node_id": "MDc6TGljZW5zZW1pdA==" 330 | }, 331 | "watchers": 123 332 | } 333 | }, 334 | "base": { 335 | "label": "octocat:master", 336 | "ref": "master", 337 | "sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e", 338 | "user": { 339 | "login": "octocat", 340 | "id": 1, 341 | "node_id": "MDQ6VXNlcjE=", 342 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 343 | "gravatar_id": "", 344 | "url": "https://api.github.com/users/octocat", 345 | "html_url": "https://github.com/octocat", 346 | "followers_url": "https://api.github.com/users/octocat/followers", 347 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 348 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 349 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 350 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 351 | "organizations_url": "https://api.github.com/users/octocat/orgs", 352 | "repos_url": "https://api.github.com/users/octocat/repos", 353 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 354 | "received_events_url": "https://api.github.com/users/octocat/received_events", 355 | "type": "User", 356 | "site_admin": false 357 | }, 358 | "repo": { 359 | "id": 1296269, 360 | "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", 361 | "name": "Hello-World", 362 | "full_name": "octocat/Hello-World", 363 | "owner": { 364 | "login": "octocat", 365 | "id": 1, 366 | "node_id": "MDQ6VXNlcjE=", 367 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 368 | "gravatar_id": "", 369 | "url": "https://api.github.com/users/octocat", 370 | "html_url": "https://github.com/octocat", 371 | "followers_url": "https://api.github.com/users/octocat/followers", 372 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 373 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 374 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 375 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 376 | "organizations_url": "https://api.github.com/users/octocat/orgs", 377 | "repos_url": "https://api.github.com/users/octocat/repos", 378 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 379 | "received_events_url": "https://api.github.com/users/octocat/received_events", 380 | "type": "User", 381 | "site_admin": false 382 | }, 383 | "private": false, 384 | "html_url": "https://github.com/octocat/Hello-World", 385 | "description": "This your first repo!", 386 | "fork": false, 387 | "url": "https://api.github.com/repos/octocat/Hello-World", 388 | "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", 389 | "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}", 390 | "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", 391 | "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}", 392 | "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", 393 | "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}", 394 | "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}", 395 | "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", 396 | "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}", 397 | "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors", 398 | "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments", 399 | "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads", 400 | "events_url": "https://api.github.com/repos/octocat/Hello-World/events", 401 | "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks", 402 | "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", 403 | "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", 404 | "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", 405 | "git_url": "git:github.com/octocat/Hello-World.git", 406 | "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", 407 | "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", 408 | "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}", 409 | "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", 410 | "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}", 411 | "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages", 412 | "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges", 413 | "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}", 414 | "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", 415 | "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}", 416 | "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}", 417 | "ssh_url": "git@github.com:octocat/Hello-World.git", 418 | "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers", 419 | "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", 420 | "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers", 421 | "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription", 422 | "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags", 423 | "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams", 424 | "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", 425 | "clone_url": "https://github.com/octocat/Hello-World.git", 426 | "mirror_url": "git:git.example.com/octocat/Hello-World", 427 | "hooks_url": "https://api.github.com/repos/octocat/Hello-World/hooks", 428 | "svn_url": "https://svn.github.com/octocat/Hello-World", 429 | "homepage": "https://github.com", 430 | "language": null, 431 | "forks_count": 9, 432 | "stargazers_count": 80, 433 | "watchers_count": 80, 434 | "size": 108, 435 | "default_branch": "master", 436 | "open_issues_count": 0, 437 | "topics": [ 438 | "octocat", 439 | "atom", 440 | "electron", 441 | "api" 442 | ], 443 | "has_issues": true, 444 | "has_projects": true, 445 | "has_wiki": true, 446 | "has_pages": false, 447 | "has_downloads": true, 448 | "has_discussions": false, 449 | "archived": false, 450 | "disabled": false, 451 | "pushed_at": "2011-01-26T19:06:43Z", 452 | "created_at": "2011-01-26T19:01:12Z", 453 | "updated_at": "2011-01-26T19:14:43Z", 454 | "permissions": { 455 | "admin": false, 456 | "push": false, 457 | "pull": true 458 | }, 459 | "allow_rebase_merge": true, 460 | "temp_clone_token": "ABTLWHOULUVAXGTRYU7OC2876QJ2O", 461 | "allow_squash_merge": true, 462 | "allow_merge_commit": true, 463 | "forks": 123, 464 | "open_issues": 123, 465 | "license": { 466 | "key": "mit", 467 | "name": "MIT License", 468 | "url": "https://api.github.com/licenses/mit", 469 | "spdx_id": "MIT", 470 | "node_id": "MDc6TGljZW5zZW1pdA==" 471 | }, 472 | "watchers": 123 473 | } 474 | }, 475 | "_links": { 476 | "self": { 477 | "href": "https://api.github.com/repos/octocat/Hello-World/pulls/1347" 478 | }, 479 | "html": { 480 | "href": "https://github.com/octocat/Hello-World/pull/1347" 481 | }, 482 | "issue": { 483 | "href": "https://api.github.com/repos/octocat/Hello-World/issues/1347" 484 | }, 485 | "comments": { 486 | "href": "https://api.github.com/repos/octocat/Hello-World/issues/1347/comments" 487 | }, 488 | "review_comments": { 489 | "href": "https://api.github.com/repos/octocat/Hello-World/pulls/1347/comments" 490 | }, 491 | "review_comment": { 492 | "href": "https://api.github.com/repos/octocat/Hello-World/pulls/comments{/number}" 493 | }, 494 | "commits": { 495 | "href": "https://api.github.com/repos/octocat/Hello-World/pulls/1347/commits" 496 | }, 497 | "statuses": { 498 | "href": "https://api.github.com/repos/octocat/Hello-World/statuses/6dcb09b5b57875f334f61aebed695e2e4193db5e" 499 | } 500 | }, 501 | "author_association": "OWNER", 502 | "auto_merge": null, 503 | "draft": false, 504 | "merged": false, 505 | "mergeable": true, 506 | "rebaseable": true, 507 | "mergeable_state": "clean", 508 | "merged_by": { 509 | "login": "octocat", 510 | "id": 1, 511 | "node_id": "MDQ6VXNlcjE=", 512 | "avatar_url": "https://github.com/images/error/octocat_happy.gif", 513 | "gravatar_id": "", 514 | "url": "https://api.github.com/users/octocat", 515 | "html_url": "https://github.com/octocat", 516 | "followers_url": "https://api.github.com/users/octocat/followers", 517 | "following_url": "https://api.github.com/users/octocat/following{/other_user}", 518 | "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", 519 | "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", 520 | "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", 521 | "organizations_url": "https://api.github.com/users/octocat/orgs", 522 | "repos_url": "https://api.github.com/users/octocat/repos", 523 | "events_url": "https://api.github.com/users/octocat/events{/privacy}", 524 | "received_events_url": "https://api.github.com/users/octocat/received_events", 525 | "type": "User", 526 | "site_admin": false 527 | }, 528 | "comments": 10, 529 | "review_comments": 0, 530 | "maintainer_can_modify": true, 531 | "commits": 3, 532 | "additions": 100, 533 | "deletions": 3, 534 | "changed_files": 5 535 | } 536 | -------------------------------------------------------------------------------- /src/test/resources/projectA/src/main/scala-2.12/bar/foo/TestSourceScala212.scala: -------------------------------------------------------------------------------- 1 | package bar.foo 2 | 3 | class TestSourceScala212 { 4 | val version = "2.12" 5 | } 6 | -------------------------------------------------------------------------------- /src/test/resources/projectA/src/main/scala/bar/foo/TestSourceFile.scala: -------------------------------------------------------------------------------- 1 | package bar.foo 2 | /** 3 | * Test Scala Source File that is 10 lines 4 | */ 5 | class TestSourceFile { 6 | 7 | 8 | 9 | 10 | } -------------------------------------------------------------------------------- /src/test/resources/projectA/src/main/scala/bar/foo/TestSourceFile2.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Test Scala Source File that is 10 lines 3 | */ 4 | class TestSourceFile2 { 5 | 6 | 7 | 8 | 9 | 10 | } -------------------------------------------------------------------------------- /src/test/resources/projectA/src/main/scala/bar/foo/TestSourceFileWithKorean.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * 한글 테스트 3 | */ 4 | class TestSourceFileWithKorean { 5 | 6 | 7 | 8 | 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/test/resources/projectB/src/main/scala/foo/TestSourceFile.scala: -------------------------------------------------------------------------------- 1 | package foo 2 | /** 3 | * Test Scala Source File that is 10 lines 4 | */ 5 | class TestSourceFile { 6 | 7 | 8 | 9 | 10 | } -------------------------------------------------------------------------------- /src/test/resources/projectB/src/main/scala/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.6.2 2 | -------------------------------------------------------------------------------- /src/test/resources/test_cobertura.xml.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | --source 6 | {{PWD}}/src/test/resources/projectA/arc/main/scala 7 | {{PWD}}/src/test/resources/projectA/arc/main/scala-2.12 8 | {{PWD}}/src/test/resources/projectB/arc/main/scala 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/test/resources/test_cobertura.xml.windows.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | --source 6 | {{PWD}}\src\test\resources\projectA\arc\main\scala 7 | {{PWD}}\src\test\resources\projectA\arc\main\scala-2.12 8 | {{PWD}}\src\test\resources\projectB\arc\main\scala 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/test/resources/test_cobertura_corrupted.xml.template: -------------------------------------------------------------------------------- 1 | corrupted file content 2 | -------------------------------------------------------------------------------- /src/test/resources/test_cobertura_corrupted.xml.windows.template: -------------------------------------------------------------------------------- 1 | corrupted file content 2 | -------------------------------------------------------------------------------- /src/test/resources/test_cobertura_dtd.xml.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | --source 6 | {{PWD}}/src/test/resources/projectA/src/main/scala 7 | {{PWD}}/src/test/resources/projectA/src/main/scala-2.12 8 | {{PWD}}/src/test/resources/projectB/src/main/scala 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/test/resources/test_cobertura_dtd.xml.windows.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | --source 6 | {{PWD}}\src\test\resources\projectA\src\main\scala 7 | {{PWD}}\src\test\resources\projectA\src\main\scala-2.12 8 | {{PWD}}\src\test\resources\projectB\src\main\scala 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/test/resources/test_cobertura_multisource.xml.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | --source 4 | {{PWD}}/src/test/resources/projectA/src/main/scala 5 | {{PWD}}/src/test/resources/projectA/src/main/scala-2.12 6 | {{PWD}}/src/test/resources/projectB/src/main/scala 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/test/resources/test_cobertura_multisource.xml.windows.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | --source 4 | {{PWD}}\src\test\resources\projectA\src\main\scala 5 | {{PWD}}\src\test\resources\projectA\src\main\scala-2.12 6 | {{PWD}}\src\test\resources\projectB\src\main\scala 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/test/scala/org/scoverage/coveralls/CIServiceTest.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | import org.scalatest.matchers.should.Matchers 4 | import org.scalatest.wordspec.AnyWordSpec 5 | 6 | class CIServiceTest extends AnyWordSpec with Matchers { 7 | 8 | "CIService" when { 9 | 10 | "getFromJson" should { 11 | "return a valid response" in { 12 | val lines = """ 13 | |{ 14 | | "textField": "textContent", 15 | | "numericField": 123, 16 | | "booleanField": true, 17 | | "nestedObject": { 18 | | "arrayField": [1, 2, 3] 19 | | } 20 | |} 21 | |""".stripMargin 22 | 23 | GitHubActions.getFromJson(lines, "numericField") shouldBe Some("123") 24 | } 25 | } 26 | 27 | "getPrNumber" should { 28 | "return a valid response" in { 29 | val payloadPath = "src/test/resources/example-pr-response.json" 30 | GitHubActions.getPrNumber(payloadPath) shouldBe Some("1347") 31 | } 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/scala/org/scoverage/coveralls/CoberturaMultiSourceReaderTest.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | import org.scalatest.BeforeAndAfterAll 4 | import org.scalatest.matchers.should.Matchers 5 | import org.scalatest.wordspec.AnyWordSpec 6 | import sbt.util.AbstractLogger 7 | 8 | import java.io.{File, FileNotFoundException} 9 | 10 | class CoberturaMultiSourceReaderTest 11 | extends AnyWordSpec 12 | with BeforeAndAfterAll 13 | with Matchers { 14 | implicit val log: AbstractLogger = sbt.Logger.Null 15 | 16 | val resourceDir = Utils.mkFileFromPath(Seq(".", "src", "test", "resources")) 17 | val sourceDirA = 18 | Utils.mkFileFromPath(resourceDir, Seq("projectA", "src", "main", "scala")) 19 | val sourceDirA212 = 20 | Utils.mkFileFromPath( 21 | resourceDir, 22 | Seq("projectA", "src", "main", "scala-2.12") 23 | ) 24 | val sourceDirB = 25 | Utils.mkFileFromPath(resourceDir, Seq("projectB", "src", "main", "scala")) 26 | val sourceDirs = Seq(sourceDirA, sourceDirA212, sourceDirB) 27 | 28 | val reader = new CoberturaMultiSourceReader( 29 | Utils.mkFileFromPath(resourceDir, Seq("test_cobertura_multisource.xml")), 30 | sourceDirs, 31 | Some("UTF-8") 32 | ) 33 | 34 | "CoberturaMultiSourceReader" when { 35 | "reading a Cobertura file" should { 36 | 37 | "list the correct source files" in { 38 | val sourceFiles = Seq( 39 | Utils.mkFileFromPath( 40 | sourceDirA, 41 | Seq("bar", "foo", "TestSourceFile.scala") 42 | ), 43 | Utils.mkFileFromPath( 44 | sourceDirA212, 45 | Seq("bar", "foo", "TestSourceScala212.scala") 46 | ), 47 | Utils.mkFileFromPath(sourceDirB, Seq("foo", "TestSourceFile.scala")) 48 | ) 49 | reader.sourceFiles shouldEqual sourceFiles.toSet 50 | } 51 | 52 | "return a valid SourceFileReport instance" in { 53 | val sourceFile = Utils.mkFileFromPath( 54 | sourceDirA, 55 | Seq("bar", "foo", "TestSourceFile.scala") 56 | ) 57 | val fileReport = reader.reportForSource(sourceFile.getCanonicalPath) 58 | fileReport.file should endWith( 59 | Seq("foo", "TestSourceFile.scala").mkString(File.separator) 60 | ) 61 | fileReport.lineCoverage should equal( 62 | List( 63 | None, 64 | None, 65 | None, 66 | Some(1), 67 | Some(1), 68 | Some(2), 69 | None, 70 | None, 71 | Some(1), 72 | Some(1) 73 | ) 74 | ) 75 | } 76 | 77 | "return a valid SourceFileReport instance if there are some classes which have the same @filename" in { 78 | val sourceFile = Utils.mkFileFromPath( 79 | sourceDirA, 80 | Seq("bar", "foo", "TestSourceFile.scala") 81 | ) 82 | val fileReport = reader.reportForSource(sourceFile.getCanonicalPath) 83 | fileReport.file should endWith( 84 | Seq("bar", "foo", "TestSourceFile.scala").mkString(File.separator) 85 | ) 86 | fileReport.lineCoverage should equal( 87 | List( 88 | None, 89 | None, 90 | None, 91 | Some(1), 92 | Some(1), 93 | Some(2), 94 | None, 95 | None, 96 | Some(1), 97 | Some(1) 98 | ) 99 | ) 100 | } 101 | 102 | "return a valid SourceFileReport instance if files are in version-specific source dirs" in { 103 | val sourceFile = Utils.mkFileFromPath( 104 | sourceDirA212, 105 | Seq("bar", "foo", "TestSourceScala212.scala") 106 | ) 107 | println(sourceFile.getCanonicalPath) 108 | val fileReport = reader.reportForSource(sourceFile.getCanonicalPath) 109 | fileReport.file should endWith( 110 | Seq("foo", "TestSourceScala212.scala").mkString(File.separator) 111 | ) 112 | } 113 | } 114 | } 115 | 116 | "CoberturaMultiSourceReader" should { 117 | 118 | "not blow up when DTD documents can't be fetched" in { 119 | val withoutDTD = new CoberturaMultiSourceReader( 120 | Utils.mkFileFromPath(resourceDir, Seq("test_cobertura_dtd.xml")), 121 | sourceDirs, 122 | Some("UTF-8") 123 | ) 124 | withoutDTD.reportXML shouldEqual reader.reportXML 125 | } 126 | 127 | "correctly determine who is parent file and who is child file" in { 128 | reader.isChild(resourceDir, resourceDir) shouldBe false 129 | reader.isChild(sourceDirA, sourceDirB) shouldBe false 130 | reader.isChild(sourceDirA, resourceDir) shouldBe true 131 | } 132 | 133 | "correctly recognize that paths is not a child only because it is prefix of another path" in { 134 | // this is an edge case that we are ignoring for now 135 | // reader.isChild(Utils.mkFileFromPath(resourceDir, Seq("src", "main", "scala-2.12")), Utils.mkFileFromPath(resourceDir, Seq("src", "main", "scala"))) shouldBe false 136 | // reader.isChild(Utils.mkFileFromPath(resourceDir, Seq("src", "aaab")), Utils.mkFileFromPath(resourceDir, Seq("src", "aaa"))) shouldBe false 137 | reader.isChild( 138 | Utils.mkFileFromPath(resourceDir, Seq("src", "aaa", "b")), 139 | Utils.mkFileFromPath(resourceDir, Seq("src", "aaa")) 140 | ) shouldBe true 141 | } 142 | } 143 | 144 | "CoberturaMultiSourceReader" should { 145 | 146 | "complain when given an empty set of source diectories" in { 147 | intercept[IllegalArgumentException] { 148 | new CoberturaMultiSourceReader(resourceDir, Seq(), Some("UTF-8")) 149 | } 150 | } 151 | 152 | "complain when at least two of the source directories are nested" in { 153 | intercept[IllegalArgumentException] { 154 | new CoberturaMultiSourceReader( 155 | resourceDir, 156 | sourceDirs ++ Seq(resourceDir), 157 | Some("UTF-8") 158 | ) 159 | } 160 | } 161 | 162 | "complain when given a non-existing cobertura file" in { 163 | intercept[FileNotFoundException] { 164 | new CoberturaMultiSourceReader(resourceDir, sourceDirs, Some("UTF-8")) 165 | } 166 | } 167 | 168 | "complain when given am incorrect cobertura file" in { 169 | intercept[Exception] { 170 | new CoberturaMultiSourceReader( 171 | Utils 172 | .mkFileFromPath(resourceDir, Seq("test_cobertura_corrupted.xml")), 173 | sourceDirs, 174 | Some("UTF-8") 175 | ) 176 | } 177 | } 178 | 179 | } 180 | 181 | "CoberturaMultiSourceReader" should { 182 | "correctly split paths to source files" in { 183 | val sourcePath = Seq("bar", "foo", "TestSourceFile.scala") 184 | val sourceFile = Utils.mkFileFromPath(sourceDirA, sourcePath) 185 | val (source, file) = reader.splitPath(sourceFile) 186 | source shouldEqual sourceDirA.getCanonicalPath 187 | file shouldEqual sourcePath.mkString(File.separator) 188 | } 189 | 190 | "complain when given a file that is outside source directories" in { 191 | intercept[IllegalArgumentException] { 192 | reader.splitPath(resourceDir) 193 | } 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/test/scala/org/scoverage/coveralls/CoverallPayloadWriterTest.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | import java.io.{File, StringWriter, Writer} 4 | import com.fasterxml.jackson.core.JsonFactory 5 | import org.scalatest.wordspec.AnyWordSpec 6 | import org.scalatest.matchers.should.Matchers 7 | import org.scalatest.BeforeAndAfterAll 8 | import sbt.util.AbstractLogger 9 | 10 | class CoverallPayloadWriterTest 11 | extends AnyWordSpec 12 | with BeforeAndAfterAll 13 | with Matchers { 14 | implicit val log: AbstractLogger = sbt.Logger.Null 15 | 16 | val resourceDir = Utils.mkFileFromPath(Seq(".", "src", "test", "resources")) 17 | 18 | val testGitClient = new GitClient(new File(".")) { 19 | override def remotes = List("remote") 20 | override def remoteUrl(remoteName: String) = "remoteUrl" 21 | override def currentBranch = "branch" 22 | override def lastCommit() = GitClient.GitRevision( 23 | "lastCommitId", 24 | "authorName", 25 | "authorEmail", 26 | "committerName", 27 | "committerEmail", 28 | "shortMsg" 29 | ) 30 | } 31 | 32 | def coverallsWriter( 33 | writer: Writer, 34 | tokenIn: Option[String], 35 | service: Option[CIService], 36 | parallel: Boolean 37 | ): (CoverallPayloadWriter, Writer) = { 38 | val payloadWriter = new CoverallPayloadWriter( 39 | new File(".").getAbsoluteFile, 40 | new File("."), 41 | service 42 | .flatMap(_.coverallsAuth(tokenIn)) 43 | .getOrElse(CoverallsRepoToken(tokenIn.get)), 44 | service, 45 | parallel, 46 | testGitClient 47 | ) { 48 | override def generator(file: File) = { 49 | val factory = new JsonFactory() 50 | factory.createGenerator(writer) 51 | } 52 | } 53 | (payloadWriter, writer) 54 | } 55 | 56 | val expectedGit = 57 | """"git":{"head":{"id":"lastCommitId","author_name":"authorName","author_email":"authorEmail","committer_name":"committerName","committer_email":"committerEmail","message":"shortMsg"},"branch":"branch","remotes":[{"name":"remote","url":"remoteUrl"}]}""" 58 | 59 | "CoverallPayloadWriter" when { 60 | "generating coveralls API payload" should { 61 | 62 | "generate a correct starting payload with a job id from a CI service" in { 63 | val testService: CIService = new CIService { 64 | override def name = "my-service" 65 | override def jobId = Some("testServiceJob") 66 | override def pullRequest = None 67 | override def currentBranch = None 68 | } 69 | 70 | val (payloadWriter, writer) = coverallsWriter( 71 | new StringWriter(), 72 | Some("testRepoToken"), 73 | Some(testService), 74 | parallel = false 75 | ) 76 | 77 | payloadWriter.start() 78 | payloadWriter.flush() 79 | 80 | println(writer.toString) 81 | 82 | writer.toString should equal( 83 | """{"repo_token":"testRepoToken","service_name":"my-service","service_job_id":"testServiceJob","parallel":false,""" + 84 | expectedGit + 85 | ""","source_files":[""" 86 | ) 87 | } 88 | 89 | "generate a correct starting payload without a CI service" in { 90 | val (payloadWriter, writer) = 91 | coverallsWriter( 92 | new StringWriter(), 93 | Some("testRepoToken"), 94 | None, 95 | parallel = false 96 | ) 97 | 98 | payloadWriter.start() 99 | payloadWriter.flush() 100 | 101 | writer.toString should equal( 102 | """{"repo_token":"testRepoToken","parallel":false,""" + 103 | expectedGit + 104 | ""","source_files":[""" 105 | ) 106 | } 107 | 108 | "generate a correct starting payload with a CI specific auth token" in { 109 | val testService: CIService = new CIService { 110 | override def name = "my-service" 111 | override def jobId = Some("testServiceJob") 112 | override def pullRequest = None 113 | override def currentBranch = None 114 | 115 | override def coverallsAuth(userRepoToken: Option[String]) = 116 | Some(CIServiceToken("hardcodedToken")) 117 | } 118 | 119 | val (payloadWriter, writer) = coverallsWriter( 120 | new StringWriter(), 121 | Some("testRepoToken"), 122 | Some(testService), 123 | parallel = false 124 | ) 125 | 126 | payloadWriter.start() 127 | payloadWriter.flush() 128 | 129 | writer.toString should equal( 130 | """{"repo_token":"hardcodedToken","service_name":"my-service","service_job_id":"testServiceJob","parallel":false,""" + 131 | expectedGit + 132 | ""","source_files":[""" 133 | ) 134 | } 135 | 136 | "add source files correctly" in { 137 | val sourceFile = Utils.mkFileFromPath( 138 | resourceDir, 139 | Seq( 140 | "projectA", 141 | "src", 142 | "main", 143 | "scala", 144 | "bar", 145 | "foo", 146 | "TestSourceFile.scala" 147 | ) 148 | ) 149 | val (payloadWriter, writer) = coverallsWriter( 150 | new StringWriter(), 151 | Some("testRepoToken"), 152 | Some(TravisCI), 153 | parallel = false 154 | ) 155 | payloadWriter.addSourceFile( 156 | SourceFileReport( 157 | sourceFile.getPath, 158 | List(Some(1), None, Some(2)) 159 | ) 160 | ) 161 | payloadWriter.flush() 162 | 163 | val separator = 164 | if (System.getProperty("os.name").startsWith("Windows")) 165 | s"""${File.separator}\\""" // Backwards slash is a special character in JSON so it needs to be escaped 166 | else 167 | File.separator 168 | 169 | val name = List( 170 | ".", 171 | "src", 172 | "test", 173 | "resources", 174 | "projectA", 175 | "src", 176 | "main", 177 | "scala", 178 | "bar", 179 | "foo", 180 | "TestSourceFile.scala" 181 | ) 182 | .mkString(separator) 183 | writer.toString should equal( 184 | s"""{"name":"$name","source_digest":"B77361233B09D69968F8C62491A5085F","coverage":[1,null,2]}""" 185 | ) 186 | } 187 | 188 | "end the file correctly" in { 189 | val (payloadWriter, writer) = coverallsWriter( 190 | new StringWriter(), 191 | Some("testRepoToken"), 192 | Some(TravisCI), 193 | parallel = false 194 | ) 195 | 196 | payloadWriter.start() 197 | payloadWriter.end() 198 | 199 | writer.toString should endWith("]}") 200 | } 201 | 202 | "include parallel correctly" in { 203 | val (payloadWriter, writer) = 204 | coverallsWriter( 205 | new StringWriter(), 206 | Some("testRepoToken"), 207 | None, 208 | parallel = true 209 | ) 210 | 211 | payloadWriter.start() 212 | payloadWriter.flush() 213 | 214 | writer.toString should equal( 215 | """{"repo_token":"testRepoToken","parallel":true,""" + 216 | expectedGit + 217 | ""","source_files":[""" 218 | ) 219 | } 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/test/scala/org/scoverage/coveralls/CoverallsClientTest.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | import org.scalatest.BeforeAndAfterAll 4 | import org.scalatest.matchers.should.Matchers 5 | import org.scalatest.wordspec.AnyWordSpec 6 | import scalaj.http.Http 7 | import scalaj.http.HttpOptions._ 8 | 9 | import scala.util.Try 10 | 11 | class CoverallsClientTest 12 | extends AnyWordSpec 13 | with BeforeAndAfterAll 14 | with Matchers { 15 | val projectDir = Utils.mkFileFromPath(Seq(".")) 16 | val resourceDir = 17 | Utils.mkFileFromPath(projectDir, Seq("src", "test", "resources")) 18 | 19 | val defaultEndpoint = "https://coveralls.io" 20 | 21 | "CoverallsClient" when { 22 | "making API call" should { 23 | 24 | "return a valid response for success" in { 25 | val testHttpClient = new HttpClientTestSuccess() 26 | val coverallsClient = 27 | new CoverallsClient(defaultEndpoint, testHttpClient) 28 | 29 | val sourceFile = Utils.mkFileFromPath( 30 | resourceDir, 31 | Seq( 32 | "projectA", 33 | "src", 34 | "main", 35 | "scala", 36 | "bar", 37 | "foo", 38 | "TestSourceFile.scala" 39 | ) 40 | ) 41 | val response = coverallsClient.postFile(sourceFile) 42 | 43 | testHttpClient.dataIn should equal( 44 | """package bar.foo\n/**\n * Test Scala Source File that is 10 lines\n */\nclass TestSourceFile {\n\n\n\n\n}""" 45 | ) 46 | response.message should equal("test message") 47 | response.error should equal(false) 48 | response.url should equal( 49 | "https://github.com/theon/xsbt-coveralls-plugin" 50 | ) 51 | } 52 | 53 | "return a valid response with Korean for success" in { 54 | val testHttpClient = new HttpClientTestSuccess() 55 | val coverallsClient = 56 | new CoverallsClient(defaultEndpoint, testHttpClient) 57 | 58 | val sourceFile = Utils.mkFileFromPath( 59 | resourceDir, 60 | Seq( 61 | "projectA", 62 | "src", 63 | "main", 64 | "scala", 65 | "bar", 66 | "foo", 67 | "TestSourceFileWithKorean.scala" 68 | ) 69 | ) 70 | val response = coverallsClient.postFile(sourceFile) 71 | 72 | testHttpClient.dataIn should equal( 73 | """/**\n * 한글 테스트\n */\nclass TestSourceFileWithKorean {\n\n\n\n\n\n}""" 74 | ) 75 | response.message should equal("test message") 76 | response.error should equal(false) 77 | response.url should equal( 78 | "https://github.com/theon/xsbt-coveralls-plugin" 79 | ) 80 | } 81 | 82 | "work when there is no title in an error HTTP response" in { 83 | val testHttpClient = HttpClientTestFake( 84 | 500, 85 | """{"message":"Couldn't find a repository matching this job.","error":true}""" 86 | ) 87 | val coverallsClient = 88 | new CoverallsClient(defaultEndpoint, testHttpClient) 89 | 90 | val sourceFile = Utils.mkFileFromPath( 91 | resourceDir, 92 | Seq( 93 | "projectA", 94 | "src", 95 | "main", 96 | "scala", 97 | "bar", 98 | "foo", 99 | "TestSourceFileWithKorean.scala" 100 | ) 101 | ) 102 | val attemptAtResponse = Try { 103 | coverallsClient.postFile(sourceFile) 104 | } 105 | 106 | assert(attemptAtResponse.isSuccess) 107 | assert( 108 | attemptAtResponse.get.message == "Couldn't find a repository matching this job." 109 | ) 110 | assert(attemptAtResponse.get.error) 111 | 112 | } 113 | 114 | "use the endpoint to build the url" in { 115 | val testHttpClient = new HttpClientTestSuccess() 116 | val coverallsClient = 117 | new CoverallsClient("https://test.endpoint", testHttpClient) 118 | 119 | assert(coverallsClient.url == "https://test.endpoint/api/v1/jobs") 120 | } 121 | } 122 | } 123 | 124 | "OpenJdkSafeSsl" when { 125 | val url = "https://coveralls.io/api/v1/jobs" 126 | "connecting to " + url should { 127 | "connect using ssl" in { 128 | val openJdkSafeSsl = new OpenJdkSafeSsl 129 | val request = Http(url) 130 | .method("GET") 131 | .option(connTimeout(60000)) 132 | .option(readTimeout(60000)) 133 | .option(sslSocketFactory(openJdkSafeSsl)) 134 | 135 | val response = request.execute() 136 | response.code should equal(404) 137 | } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/test/scala/org/scoverage/coveralls/GitClientTest.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | import org.eclipse.jgit.api.Git 4 | import org.scalatest.BeforeAndAfterAll 5 | import org.scalatest.matchers.should.Matchers 6 | import org.scalatest.wordspec.AnyWordSpec 7 | import sbt.ConsoleLogger 8 | 9 | import java.io.File 10 | 11 | class GitClientTest extends AnyWordSpec with BeforeAndAfterAll with Matchers { 12 | 13 | implicit val log: ConsoleLogger = ConsoleLogger(System.out) 14 | 15 | var git: GitClient = _ 16 | 17 | override def beforeAll(): Unit = { 18 | // Create local repository 19 | val repoDir = File.createTempFile("test_repo", "") 20 | repoDir.delete() 21 | repoDir.mkdirs() 22 | val gitRepo = Git.init().setDirectory(repoDir).call() 23 | // Add two remotes 24 | val storedConfig = gitRepo.getRepository.getConfig 25 | storedConfig.setString( 26 | "remote", 27 | "origin_test_1", 28 | "url", 29 | "git@origin_test_1" 30 | ) 31 | storedConfig.setString( 32 | "remote", 33 | "origin_test_2", 34 | "url", 35 | "git@origin_test_2" 36 | ) 37 | storedConfig.save() 38 | // Add and commit a file 39 | val readme = new File(repoDir, "README.md") 40 | readme.createNewFile() 41 | gitRepo 42 | .add() 43 | .addFilepattern("README.md") 44 | .call() 45 | gitRepo 46 | .commit() 47 | .setAuthor("test_username", "test_user@test_email.com") 48 | .setCommitter("test_username", "test_user@test_email.com") 49 | .setMessage("Commit message for unit test") 50 | .call() 51 | 52 | git = new GitClient(repoDir) 53 | } 54 | 55 | "GitClient" when { 56 | 57 | "asked for remotes" should { 58 | "return a valid response" in { 59 | git.remotes should contain("origin_test_1") 60 | git.remotes should contain("origin_test_2") 61 | } 62 | } 63 | 64 | "asked for a remote's url" should { 65 | "return a valid response" in { 66 | git.remoteUrl("origin_test_1") should equal("git@origin_test_1") 67 | git.remoteUrl("origin_test_2") should equal("git@origin_test_2") 68 | } 69 | } 70 | 71 | "asked for the current branch" should { 72 | "return a valid response" in { 73 | // git checkout action defaults to master for this, so we'll just check 74 | // to ensure it starts with ma for [main|master] 75 | git.currentBranch should startWith("ma") 76 | } 77 | } 78 | 79 | "asked for the last commit" should { 80 | "return a valid hash" in { 81 | git.lastCommit().id should fullyMatch regex "[0-9a-f]{40}" 82 | } 83 | "return a valid author name" in { 84 | git.lastCommit().authorName should equal("test_username") 85 | } 86 | "return a valid committer name" in { 87 | git.lastCommit().committerName should equal("test_username") 88 | } 89 | "return a valid author email" in { 90 | git.lastCommit().authorEmail should equal("test_user@test_email.com") 91 | } 92 | "return a valid committer email" in { 93 | git.lastCommit().committerEmail should equal("test_user@test_email.com") 94 | } 95 | "return a valid author commit message" in { 96 | git.lastCommit().shortMessage should equal( 97 | "Commit message for unit test" 98 | ) 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/test/scala/org/scoverage/coveralls/HttpClientTest.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | class HttpClientTestSuccess extends HttpClient { 4 | var dataIn: String = _ 5 | 6 | def multipart( 7 | url: String, 8 | name: String, 9 | filename: String, 10 | mime: String, 11 | data: Array[Byte] 12 | ) = { 13 | dataIn = new String(data, "UTF-8") 14 | CoverallHttpResponse( 15 | 200, 16 | """ 17 | { 18 | "message":"test message", 19 | "error": false, 20 | "url": "https://github.com/theon/xsbt-coveralls-plugin" 21 | } 22 | """ 23 | ) 24 | } 25 | } 26 | 27 | class HttpClientTestFailure extends HttpClient { 28 | def multipart( 29 | url: String, 30 | name: String, 31 | filename: String, 32 | mime: String, 33 | data: Array[Byte] 34 | ) = { 35 | CoverallHttpResponse( 36 | 200, 37 | """ 38 | { 39 | "message":"test error message when there was an error", 40 | "error": true 41 | } 42 | """ 43 | ) 44 | } 45 | } 46 | 47 | case class HttpClientTestFake(status: Int, body: String) extends HttpClient { 48 | def multipart( 49 | url: String, 50 | name: String, 51 | filename: String, 52 | mime: String, 53 | data: Array[Byte] 54 | ) = { 55 | CoverallHttpResponse(status, body) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/scala/org/scoverage/coveralls/UtilsTest.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | import org.scalatest.matchers.should.Matchers 4 | import org.scalatest.wordspec.AnyWordSpec 5 | 6 | import java.io.File 7 | 8 | class UtilsTest extends AnyWordSpec with Matchers { 9 | "mkFileFromPath" when { 10 | "getting the right params" should { 11 | 12 | "return the right path" in { 13 | val path = Seq(".", "a", "b") 14 | Utils.mkFileFromPath(path).getPath shouldEqual path.mkString( 15 | File.separator 16 | ) 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/scala/org/scoverage/coveralls/XmlHelperTest.scala: -------------------------------------------------------------------------------- 1 | package org.scoverage.coveralls 2 | 3 | import org.scalatest.matchers.should.Matchers 4 | import org.scalatest.wordspec.AnyWordSpec 5 | 6 | import java.io.File 7 | import scala.xml.XML 8 | 9 | class XmlHelperTest extends AnyWordSpec with Matchers { 10 | 11 | val root = new File(getClass.getResource("/").getFile) 12 | 13 | val invalidDTD = new File(root, "test_cobertura_dtd.xml") 14 | 15 | "XmlHelper" when { 16 | 17 | "parsing XML documents with an invalid DTD" should { 18 | 19 | "not attempt to fetch the DTD and successfully parse" in { 20 | XmlHelper.loadXmlFile(invalidDTD) shouldBe an[xml.Elem] 21 | // Verify the document actually has an unusable DTD 22 | assertThrows[org.xml.sax.SAXParseException](XML.loadFile(invalidDTD)) 23 | } 24 | } 25 | 26 | } 27 | } 28 | --------------------------------------------------------------------------------