├── .github ├── FUNDING.yml └── workflows │ ├── main.yml │ └── reviewdog.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── gitinfo.go ├── gitinfo_test.go ├── go.mod ├── go.sum ├── gocover.go ├── gocover_test.go ├── goveralls.go ├── goveralls_test.go ├── renovate.json ├── tester ├── cov.json ├── tester.go └── tester_test.go ├── usage_ge1.15_test.go └── usage_lt1.15_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: shogo82148 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | os: 11 | - ubuntu-latest 12 | - macos-latest 13 | - windows-latest 14 | go: 15 | - '1.13' # minimum version 16 | - 'oldstable' 17 | - 'stable' 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | ref: ${{ github.event.pull_request.head.sha }} 24 | 25 | - name: Set up Go ${{ matrix.go }} 26 | uses: actions/setup-go@v4 27 | with: 28 | go-version: ${{ matrix.go }} 29 | - run: go version 30 | 31 | - name: build 32 | run: | 33 | go get ./... 34 | go install . 35 | - name: test 36 | run: goveralls -service=github -parallel -flagname="Unit-${{ matrix.os }}-Go-${{ matrix.go }}" 37 | env: 38 | COVERALLS_TOKEN: ${{ github.token }} 39 | GIT_BRANCH: ${{ github.head_ref }} 40 | 41 | finish: 42 | needs: test 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v3 46 | with: 47 | ref: ${{ github.event.pull_request.head.sha }} 48 | - name: Set up Go 49 | uses: actions/setup-go@v4 50 | - name: finish 51 | run: | 52 | go run github.com/mattn/goveralls -parallel-finish 53 | env: 54 | COVERALLS_TOKEN: ${{ github.token }} 55 | GIT_BRANCH: ${{ github.head_ref }} 56 | -------------------------------------------------------------------------------- /.github/workflows/reviewdog.yml: -------------------------------------------------------------------------------- 1 | name: reviewdog 2 | on: [pull_request] 3 | 4 | jobs: 5 | golangci-lint: 6 | name: golangci-lint 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Check out code into the Go module directory 10 | uses: actions/checkout@v2 11 | - name: golangci-lint 12 | uses: reviewdog/action-golangci-lint@v1 13 | with: 14 | github_token: ${{ secrets.github_token }} 15 | level: warning 16 | 17 | misspell: 18 | name: misspell 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | - name: misspell 24 | uses: reviewdog/action-misspell@v1 25 | with: 26 | github_token: ${{ secrets.github_token }} 27 | level: warning 28 | locale: "US" 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | /goveralls 3 | /vendor -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - '1.x' 4 | before_install: 5 | - go get ./... 6 | - go install . 7 | script: 8 | - goveralls 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Yasuhiro Matsumoto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | goveralls 2 | ========= 3 | 4 | [Go](http://golang.org) integration for [Coveralls.io](http://coveralls.io) 5 | continuous code coverage tracking system. 6 | 7 | # Installation 8 | 9 | `goveralls` requires a working Go installation (Go-1.13 or higher). 10 | 11 | ```bash 12 | $ go install github.com/mattn/goveralls@latest 13 | ``` 14 | 15 | 16 | # Usage 17 | 18 | First you will need an API token. It is found at the bottom of your 19 | repository's page when you are logged in to Coveralls.io. Each repo has its 20 | own token. 21 | 22 | ```bash 23 | $ cd $GOPATH/src/github.com/yourusername/yourpackage 24 | $ goveralls -repotoken your_repos_coveralls_token 25 | ``` 26 | 27 | You can set the environment variable `$COVERALLS_TOKEN` to your token so you do 28 | not have to specify it at each invocation. 29 | 30 | 31 | You can also run this reporter for multiple passes with the flag `-parallel` or 32 | by setting the environment variable `COVERALLS_PARALLEL=true` (see [coveralls 33 | docs](https://docs.coveralls.io/parallel-build-webhook) for more details). 34 | 35 | 36 | # Continuous Integration 37 | 38 | There is no need to run `go test` separately, as `goveralls` runs the entire 39 | test suite. 40 | 41 | ## GitHub Actions 42 | 43 | [shogo82148/actions-goveralls](https://github.com/marketplace/actions/actions-goveralls) is available on GitHub Marketplace. 44 | It provides the shorthand of the GitHub Actions YAML configure. 45 | 46 | ```yaml 47 | name: Quality 48 | on: [push, pull_request] 49 | jobs: 50 | test: 51 | name: Test with Coverage 52 | runs-on: ubuntu-latest 53 | steps: 54 | - name: Set up Go 55 | uses: actions/setup-go@v2 56 | with: 57 | go-version: '1.16' 58 | - name: Check out code 59 | uses: actions/checkout@v2 60 | - name: Install dependencies 61 | run: | 62 | go mod download 63 | - name: Run Unit tests 64 | run: | 65 | go test -race -covermode atomic -coverprofile=covprofile ./... 66 | - name: Install goveralls 67 | run: go install github.com/mattn/goveralls@latest 68 | - name: Send coverage 69 | env: 70 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | run: goveralls -coverprofile=covprofile -service=github 72 | # or use shogo82148/actions-goveralls 73 | # - name: Send coverage 74 | # uses: shogo82148/actions-goveralls@v1 75 | # with: 76 | # path-to-profile: covprofile 77 | ``` 78 | 79 | ## Travis CI 80 | 81 | ### GitHub Integration 82 | 83 | Enable Travis-CI on your GitHub repository settings. 84 | 85 | For a **public** GitHub repository put bellow's `.travis.yml`. 86 | 87 | ```yml 88 | language: go 89 | go: 90 | - tip 91 | before_install: 92 | - go install github.com/mattn/goveralls@latest 93 | script: 94 | - $GOPATH/bin/goveralls -service=travis-ci 95 | ``` 96 | 97 | For a **public** GitHub repository, it is not necessary to define your repository key (`COVERALLS_TOKEN`). 98 | 99 | For a **private** GitHub repository put bellow's `.travis.yml`. If you use **travis pro**, you need to specify `-service=travis-pro` instead of `-service=travis-ci`. 100 | 101 | ```yml 102 | language: go 103 | go: 104 | - tip 105 | before_install: 106 | - go install github.com/mattn/goveralls@latest 107 | script: 108 | - $GOPATH/bin/goveralls -service=travis-pro 109 | ``` 110 | 111 | Store your Coveralls API token in `Environment variables`. 112 | 113 | ``` 114 | COVERALLS_TOKEN = your_token_goes_here 115 | ``` 116 | 117 | or you can store token using [travis encryption keys](https://docs.travis-ci.com/user/encryption-keys/). Note that this is the token provided in the page for that specific repository on Coveralls. This is *not* one that was created from the "Personal API Tokens" area under your Coveralls account settings. 118 | 119 | ``` 120 | $ gem install travis 121 | $ travis encrypt COVERALLS_TOKEN=your_token_goes_here --add env.global 122 | ``` 123 | 124 | travis will add `env` block as following example: 125 | 126 | ```yml 127 | env: 128 | global: 129 | secure: xxxxxxxxxxxxx 130 | ``` 131 | 132 | ### For others: 133 | 134 | ``` 135 | $ go install github.com/mattn/goveralls@latest 136 | $ go test -covermode=count -coverprofile=profile.cov 137 | $ goveralls -coverprofile=profile.cov -service=travis-ci 138 | ``` 139 | 140 | ## Drone.io 141 | 142 | Store your Coveralls API token in `Environment Variables`: 143 | 144 | ``` 145 | COVERALLS_TOKEN=your_token_goes_here 146 | ``` 147 | 148 | Replace the `go test` line in your `Commands` with these lines: 149 | 150 | ``` 151 | $ go install github.com/mattn/goveralls@latest 152 | $ goveralls -service drone.io 153 | ``` 154 | 155 | `goveralls` automatically use the environment variable `COVERALLS_TOKEN` as the 156 | default value for `-repotoken`. 157 | 158 | You can use the `-v` flag to see verbose output from the test suite: 159 | 160 | ``` 161 | $ goveralls -v -service drone.io 162 | ``` 163 | 164 | ## CircleCI 165 | 166 | Store your Coveralls API token as an [Environment Variable](https://circleci.com/docs/environment-variables). 167 | 168 | In your `circle.yml` add the following commands under the `test` section. 169 | 170 | ```yml 171 | test: 172 | pre: 173 | - go install github.com/mattn/goveralls@latest 174 | override: 175 | - go test -v -cover -race -coverprofile=/home/ubuntu/coverage.out 176 | post: 177 | - /home/ubuntu/.go_workspace/bin/goveralls -coverprofile=/home/ubuntu/coverage.out -service=circle-ci -repotoken=$COVERALLS_TOKEN 178 | ``` 179 | 180 | For more information, See https://docs.coveralls.io/go 181 | 182 | ## Semaphore 183 | 184 | Store your Coveralls API token in `Environment Variables`: 185 | 186 | ``` 187 | COVERALLS_TOKEN=your_token_goes_here 188 | ``` 189 | 190 | More instructions on how to do this can be found in the [Semaphore documentation](https://semaphoreci.com/docs/exporting-environment-variables.html). 191 | 192 | Replace the `go test` line in your `Commands` with these lines: 193 | 194 | ``` 195 | $ go install github.com/mattn/goveralls@latest 196 | $ goveralls -service semaphore 197 | ``` 198 | 199 | `goveralls` automatically use the environment variable `COVERALLS_TOKEN` as the 200 | default value for `-repotoken`. 201 | 202 | You can use the `-v` flag to see verbose output from the test suite: 203 | 204 | ``` 205 | $ goveralls -v -service semaphore 206 | ``` 207 | 208 | ## Jenkins CI 209 | 210 | Add your Coveralls API token as a credential in Jenkins (see [Jenkins documentation](https://www.jenkins.io/doc/book/using/using-credentials/#configuring-credentials)). 211 | 212 | Then declare it as the environment variable `COVERALLS_TOKEN`: 213 | ```groovy 214 | pipeline { 215 | agent any 216 | stages { 217 | stage('Test with coverage') { 218 | steps { 219 | sh 'go test ./... -coverprofile=coverage.txt -covermode=atomic' 220 | } 221 | } 222 | stage('Upload to coveralls.io') { 223 | environment { 224 | COVERALLS_TOKEN = credentials('coveralls-token') 225 | } 226 | steps { 227 | sh 'goveralls -coverprofile=coverage.txt' 228 | } 229 | } 230 | } 231 | } 232 | ``` 233 | 234 | See also [related Jenkins documentation](https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#for-secret-text-usernames-and-passwords-and-secret-files). 235 | 236 | It is also possible to let goveralls run the code coverage on its own without providing a coverage profile file. 237 | 238 | ## TeamCity 239 | 240 | Store your Coveralls API token in `Environment Variables`: 241 | 242 | ``` 243 | COVERALLS_TOKEN=your_token_goes_here 244 | ``` 245 | 246 | Setup build steps: 247 | 248 | ``` 249 | $ go install github.com/mattn/goveralls@latest 250 | $ export PULL_REQUEST_NUMBER=%teamcity.build.branch% 251 | $ goveralls -service teamcity -jobid %teamcity.build.id% -jobnumber %build.number% 252 | ``` 253 | 254 | `goveralls` will automatically use the environment variable `COVERALLS_TOKEN` as the 255 | default value for `-repotoken`. 256 | 257 | You can use the `-v` flag to see verbose output. 258 | 259 | 260 | ## Gitlab CI 261 | 262 | Store your Coveralls API token as an [Environment Variable](https://docs.gitlab.com/ee/ci/variables/#create-a-custom-variable-in-the-ui) named `COVERALLS_TOKEN`. 263 | 264 | ```yml 265 | test: 266 | timeout: 30m 267 | stage: test 268 | artifacts: 269 | paths: 270 | - coverage.txt 271 | dependencies: 272 | - build:env 273 | when: always 274 | script: 275 | - go test -covermode atomic -coverprofile=coverage.txt ./... 276 | - go install github.com/mattn/goveralls@latest 277 | - goveralls -service=gitlab -coverprofile=coverage.txt 278 | ``` 279 | 280 | ## Coveralls Enterprise 281 | 282 | If you are using Coveralls Enterprise and have a self-signed certificate, you need to skip certificate verification: 283 | 284 | ```shell 285 | $ goveralls -insecure 286 | ``` 287 | 288 | # Authors 289 | 290 | * Yasuhiro Matsumoto (a.k.a. mattn) 291 | * haya14busa 292 | 293 | # License 294 | 295 | under the MIT License: http://mattn.mit-license.org/2016 296 | -------------------------------------------------------------------------------- /gitinfo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | ) 11 | 12 | // A Head object encapsulates information about the HEAD revision of a git repo. 13 | type Head struct { 14 | ID string `json:"id"` 15 | AuthorName string `json:"author_name,omitempty"` 16 | AuthorEmail string `json:"author_email,omitempty"` 17 | CommitterName string `json:"committer_name,omitempty"` 18 | CommitterEmail string `json:"committer_email,omitempty"` 19 | Message string `json:"message"` 20 | } 21 | 22 | // A Git object encapsulates information about a git repo. 23 | type Git struct { 24 | Head Head `json:"head"` 25 | Branch string `json:"branch"` 26 | } 27 | 28 | // collectGitInfo uses either environment variables or git commands to compose a Git metadata object. 29 | func collectGitInfo(ref string) (*Git, error) { 30 | gitCmds := map[string][]string{ 31 | "GIT_ID": {"rev-parse", ref}, 32 | "GIT_BRANCH": {"branch", "--format", "%(refname:short)", "--contains", ref}, 33 | "GIT_AUTHOR_NAME": {"show", "-s", "--format=%aN", ref}, 34 | "GIT_AUTHOR_EMAIL": {"show", "-s", "--format=%aE", ref}, 35 | "GIT_COMMITTER_NAME": {"show", "-s", "--format=%cN", ref}, 36 | "GIT_COMMITTER_EMAIL": {"show", "-s", "--format=%cE", ref}, 37 | "GIT_MESSAGE": {"show", "-s", "--format=%s", ref}, 38 | } 39 | 40 | var gitPath string 41 | 42 | if *allowGitFetch && ref != "HEAD" { 43 | var err error 44 | gitPath, err = exec.LookPath("git") 45 | if err != nil { 46 | return nil, fmt.Errorf("failed to look path of git: %v", err) 47 | } 48 | 49 | // make sure that the commit is in the local 50 | // e.g. shallow cloned repository 51 | _, err = runCommand(gitPath, "fetch", "--depth=1", "origin", ref) 52 | if err != nil { 53 | return nil, fmt.Errorf("failed to fetch git ref %q: %v", ref, err) 54 | } 55 | } 56 | 57 | for key, args := range gitCmds { 58 | // special case for the git branch name: load from multiple environment variables 59 | if key == "GIT_BRANCH" { 60 | if envBranch := loadBranchFromEnv(); envBranch != "" { 61 | err := os.Setenv(key, envBranch) 62 | if err != nil { 63 | return nil, err 64 | } 65 | continue 66 | } 67 | } 68 | if os.Getenv(key) != "" { 69 | // metadata already available via environment variable 70 | continue 71 | } 72 | 73 | if gitPath == "" { 74 | var err error 75 | gitPath, err = exec.LookPath("git") 76 | if err != nil { 77 | log.Printf("fail to look path of git: %v", err) 78 | log.Print("git information is omitted") 79 | return nil, nil 80 | } 81 | } 82 | 83 | ret, err := runCommand(gitPath, args...) 84 | if err != nil { 85 | log.Printf(`fail to run "%s %s": %v`, gitPath, strings.Join(args, " "), err) 86 | log.Print("git information is omitted") 87 | return nil, nil 88 | } 89 | 90 | err = os.Setenv(key, ret) 91 | if err != nil { 92 | return nil, err 93 | } 94 | } 95 | 96 | h := Head{ 97 | ID: os.Getenv("GIT_ID"), 98 | AuthorName: os.Getenv("GIT_AUTHOR_NAME"), 99 | AuthorEmail: os.Getenv("GIT_AUTHOR_EMAIL"), 100 | CommitterName: os.Getenv("GIT_COMMITTER_NAME"), 101 | CommitterEmail: os.Getenv("GIT_COMMITTER_EMAIL"), 102 | Message: os.Getenv("GIT_MESSAGE"), 103 | } 104 | g := &Git{ 105 | Head: h, 106 | Branch: os.Getenv("GIT_BRANCH"), 107 | } 108 | 109 | return g, nil 110 | } 111 | 112 | func runCommand(gitPath string, args ...string) (string, error) { 113 | cmd := exec.Command(gitPath, args...) 114 | ret, err := cmd.CombinedOutput() 115 | if err != nil { 116 | return "", err 117 | } 118 | ret = bytes.TrimRight(ret, "\n") 119 | return string(ret), nil 120 | } 121 | 122 | var varNames = [...]string{ 123 | "GIT_BRANCH", 124 | 125 | // https://help.github.com/en/actions/automating-your-workflow-with-github-actions/using-environment-variables 126 | "GITHUB_HEAD_REF", "GITHUB_REF", 127 | 128 | "CIRCLE_BRANCH", "TRAVIS_BRANCH", 129 | "CI_BRANCH", "APPVEYOR_REPO_BRANCH", 130 | "WERCKER_GIT_BRANCH", "DRONE_BRANCH", 131 | "BUILDKITE_BRANCH", "BRANCH_NAME", 132 | "CI_COMMIT_REF_NAME", 133 | } 134 | 135 | func loadBranchFromEnv() string { 136 | for _, varName := range varNames { 137 | if branch := os.Getenv(varName); branch != "" { 138 | if varName == "GITHUB_REF" { 139 | return strings.TrimPrefix(branch, "refs/heads/") 140 | } 141 | return branch 142 | } 143 | } 144 | 145 | return "" 146 | } 147 | -------------------------------------------------------------------------------- /gitinfo_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestLoadBranchFromEnv(t *testing.T) { 9 | t.Parallel() 10 | 11 | var tests = []struct { 12 | testCase string 13 | envs map[string]string 14 | expectedBranch string 15 | }{ 16 | { 17 | "all vars defined", 18 | map[string]string{ 19 | "GIT_BRANCH": "master", 20 | "CIRCLE_BRANCH": "circle-master", 21 | "TRAVIS_BRANCH": "travis-master", 22 | "CI_BRANCH": "ci-master", 23 | "APPVEYOR_REPO_BRANCH": "appveyor-master", 24 | "WERCKER_GIT_BRANCH": "wercker-master", 25 | "DRONE_BRANCH": "drone-master", 26 | "BUILDKITE_BRANCH": "buildkite-master", 27 | "BRANCH_NAME": "jenkins-master", 28 | "CI_COMMIT_REF_NAME": "gitlab-master", 29 | }, 30 | "master", 31 | }, 32 | { 33 | "all except GIT_BRANCH", 34 | map[string]string{ 35 | "CIRCLE_BRANCH": "circle-master", 36 | "TRAVIS_BRANCH": "travis-master", 37 | "CI_BRANCH": "ci-master", 38 | "APPVEYOR_REPO_BRANCH": "appveyor-master", 39 | "WERCKER_GIT_BRANCH": "wercker-master", 40 | "DRONE_BRANCH": "drone-master", 41 | "BUILDKITE_BRANCH": "buildkite-master", 42 | "BRANCH_NAME": "jenkins-master", 43 | "CI_COMMIT_REF_NAME": "gitlab-master", 44 | }, 45 | "circle-master", 46 | }, 47 | { 48 | "all except GIT_BRANCH and CIRCLE_BRANCH", 49 | map[string]string{ 50 | "TRAVIS_BRANCH": "travis-master", 51 | "CI_BRANCH": "ci-master", 52 | "APPVEYOR_REPO_BRANCH": "appveyor-master", 53 | "WERCKER_GIT_BRANCH": "wercker-master", 54 | "DRONE_BRANCH": "drone-master", 55 | "BUILDKITE_BRANCH": "buildkite-master", 56 | "BRANCH_NAME": "jenkins-master", 57 | "CI_COMMIT_REF_NAME": "gitlab-master", 58 | }, 59 | "travis-master", 60 | }, 61 | { 62 | "only CI_BRANCH defined", 63 | map[string]string{ 64 | "CI_BRANCH": "ci-master", 65 | }, 66 | "ci-master", 67 | }, 68 | { 69 | "only APPVEYOR_REPO_BRANCH defined", 70 | map[string]string{ 71 | "APPVEYOR_REPO_BRANCH": "appveyor-master", 72 | }, 73 | "appveyor-master", 74 | }, 75 | { 76 | "only WERCKER_GIT_BRANCH defined", 77 | map[string]string{ 78 | "WERCKER_GIT_BRANCH": "wercker-master", 79 | }, 80 | "wercker-master", 81 | }, 82 | { 83 | "only BRANCH_NAME defined", 84 | map[string]string{ 85 | "BRANCH_NAME": "jenkins-master", 86 | }, 87 | "jenkins-master", 88 | }, 89 | { 90 | "only BUILDKITE_BRANCH defined", 91 | map[string]string{ 92 | "BUILDKITE_BRANCH": "buildkite-master", 93 | }, 94 | "buildkite-master", 95 | }, 96 | { 97 | "only DRONE_BRANCH defined", 98 | map[string]string{ 99 | "DRONE_BRANCH": "drone-master", 100 | }, 101 | "drone-master", 102 | }, 103 | { 104 | "only CI_COMMIT_REF_NAME defined", 105 | map[string]string{ 106 | "CI_COMMIT_REF_NAME": "gitlab-master", 107 | }, 108 | "gitlab-master", 109 | }, 110 | { 111 | "GitHub Action push event", 112 | map[string]string{ 113 | "GITHUB_REF": "refs/heads/github-master", 114 | }, 115 | "github-master", 116 | }, 117 | { 118 | "no branch var defined", 119 | map[string]string{}, 120 | "", 121 | }, 122 | } 123 | for _, test := range tests { 124 | resetBranchEnvs(test.envs) 125 | envBranch := loadBranchFromEnv() 126 | if envBranch != test.expectedBranch { 127 | t.Errorf("%s: wrong branch returned. Expected %q, but got %q", test.testCase, test.expectedBranch, envBranch) 128 | } 129 | } 130 | } 131 | 132 | func resetBranchEnvs(values map[string]string) { 133 | for _, envVar := range varNames { 134 | os.Unsetenv(envVar) 135 | } 136 | for k, v := range values { 137 | os.Setenv(k, v) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mattn/goveralls 2 | 3 | go 1.13 4 | 5 | require ( 6 | golang.org/x/mod v0.10.0 7 | golang.org/x/tools v0.8.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 2 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 3 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 4 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 5 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 6 | golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= 7 | golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 8 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 9 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 10 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 11 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 12 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 13 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 14 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 15 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 16 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 17 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 18 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 19 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 21 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 22 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 23 | golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= 24 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 26 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 27 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 28 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 29 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 30 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 31 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 32 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 33 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 34 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 35 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 36 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 37 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 38 | golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= 39 | golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= 40 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 41 | -------------------------------------------------------------------------------- /gocover.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Much of the core of this is copied from go's cover tool itself. 4 | 5 | // Copyright 2013 The Go Authors. All rights reserved. 6 | // Use of this source code is governed by a BSD-style 7 | // license that can be found in the LICENSE file. 8 | 9 | // The rest is written by Dustin Sallings 10 | 11 | import ( 12 | "fmt" 13 | "go/build" 14 | "io/ioutil" 15 | "os" 16 | "path/filepath" 17 | "strings" 18 | 19 | "golang.org/x/mod/modfile" 20 | "golang.org/x/tools/cover" 21 | ) 22 | 23 | func findFile(rootPackage string, rootDir string, file string) (string, error) { 24 | // If we find a file that is inside the root package, we already know 25 | // where it should be! 26 | if rootPackage != "" { 27 | if relPath, _ := filepath.Rel(rootPackage, file); !strings.HasPrefix(relPath, "..") { 28 | // The file is inside the root package... 29 | return filepath.Join(rootDir, relPath), nil 30 | } 31 | } 32 | 33 | dir, file := filepath.Split(file) 34 | pkg, err := build.Import(dir, ".", build.FindOnly) 35 | if err != nil { 36 | return "", fmt.Errorf("can't find %q: %v", file, err) 37 | } 38 | return filepath.Join(pkg.Dir, file), nil 39 | } 40 | 41 | // mergeProfs merges profiles for same target packages. 42 | // It assumes each profiles have same sorted FileName and Blocks. 43 | func mergeProfs(pfss [][]*cover.Profile) []*cover.Profile { 44 | if len(pfss) == 0 { 45 | return nil 46 | } 47 | for len(pfss) > 1 { 48 | i := 0 49 | for ; 2*i+1 < len(pfss); i++ { 50 | pfss[i] = mergeTwoProfs(pfss[2*i], pfss[2*i+1]) 51 | } 52 | if 2*i < len(pfss) { 53 | pfss[i] = pfss[2*i] 54 | i++ 55 | } 56 | pfss = pfss[:i] 57 | } 58 | return pfss[0] 59 | } 60 | 61 | func mergeTwoProfs(left, right []*cover.Profile) []*cover.Profile { 62 | ret := make([]*cover.Profile, 0, len(left)+len(right)) 63 | for len(left) > 0 && len(right) > 0 { 64 | if left[0].FileName == right[0].FileName { 65 | profile := &cover.Profile{ 66 | FileName: left[0].FileName, 67 | Mode: left[0].Mode, 68 | Blocks: mergeTwoProfBlock(left[0].Blocks, right[0].Blocks), 69 | } 70 | ret = append(ret, profile) 71 | left = left[1:] 72 | right = right[1:] 73 | } else if left[0].FileName < right[0].FileName { 74 | ret = append(ret, left[0]) 75 | left = left[1:] 76 | } else { 77 | ret = append(ret, right[0]) 78 | right = right[1:] 79 | } 80 | } 81 | ret = append(ret, left...) 82 | ret = append(ret, right...) 83 | return ret 84 | } 85 | 86 | func mergeTwoProfBlock(left, right []cover.ProfileBlock) []cover.ProfileBlock { 87 | ret := make([]cover.ProfileBlock, 0, len(left)+len(right)) 88 | for len(left) > 0 && len(right) > 0 { 89 | a, b := left[0], right[0] 90 | if a.StartLine == b.StartLine && a.StartCol == b.StartCol && a.EndLine == b.EndLine && a.EndCol == b.EndCol { 91 | ret = append(ret, cover.ProfileBlock{ 92 | StartLine: a.StartLine, 93 | StartCol: a.StartCol, 94 | EndLine: a.EndLine, 95 | EndCol: a.EndCol, 96 | NumStmt: a.NumStmt, 97 | Count: a.Count + b.Count, 98 | }) 99 | left = left[1:] 100 | right = right[1:] 101 | } else if a.StartLine < b.StartLine || (a.StartLine == b.StartLine && a.StartCol < b.StartCol) { 102 | ret = append(ret, a) 103 | left = left[1:] 104 | } else { 105 | ret = append(ret, b) 106 | right = right[1:] 107 | } 108 | } 109 | ret = append(ret, left...) 110 | ret = append(ret, right...) 111 | return ret 112 | } 113 | 114 | // toSF converts profiles to sourcefiles for coveralls. 115 | func toSF(profs []*cover.Profile) ([]*SourceFile, error) { 116 | // find root package to reduce build.Import calls when importing files from relative root 117 | // https://github.com/mattn/goveralls/pull/195 118 | rootDirectory, err := os.Getwd() 119 | if err != nil { 120 | return nil, fmt.Errorf("get working dir: %v", err) 121 | } 122 | rootPackage := findRootPackage(rootDirectory) 123 | 124 | var rv []*SourceFile 125 | for _, prof := range profs { 126 | path, err := findFile(rootPackage, rootDirectory, prof.FileName) 127 | if err != nil { 128 | return nil, fmt.Errorf("cannot find file %q: %v", prof.FileName, err) 129 | } 130 | sf := &SourceFile{ 131 | Name: getCoverallsSourceFileName(path), 132 | } 133 | lineLookup := map[int]int{} 134 | maxLineNo := 0 135 | for _, block := range prof.Blocks { 136 | for i := block.StartLine; i <= block.EndLine; i++ { 137 | lineLookup[i] += block.Count 138 | } 139 | if block.EndLine > maxLineNo { 140 | maxLineNo = block.EndLine 141 | } 142 | } 143 | sf.Coverage = make([]interface{}, maxLineNo) 144 | for i := 1; i <= maxLineNo; i++ { 145 | if c, ok := lineLookup[i]; ok { 146 | sf.Coverage[i-1] = c 147 | } 148 | } 149 | if *uploadSource { 150 | fb, err := ioutil.ReadFile(path) 151 | if err != nil { 152 | return nil, fmt.Errorf("cannot read source of file %q: %v", path, err) 153 | } 154 | sf.Source = string(fb) 155 | } 156 | 157 | rv = append(rv, sf) 158 | } 159 | 160 | return rv, nil 161 | } 162 | 163 | func parseCover(fn string) ([]*SourceFile, error) { 164 | var pfss [][]*cover.Profile 165 | for _, p := range strings.Split(fn, ",") { 166 | profs, err := cover.ParseProfiles(p) 167 | if err != nil { 168 | return nil, fmt.Errorf("error parsing coverage: %v", err) 169 | } 170 | pfss = append(pfss, profs) 171 | } 172 | 173 | sourceFiles, err := toSF(mergeProfs(pfss)) 174 | if err != nil { 175 | return nil, err 176 | } 177 | 178 | return sourceFiles, nil 179 | } 180 | 181 | func findRootPackage(rootDirectory string) string { 182 | modPath := filepath.Join(rootDirectory, "go.mod") 183 | content, err := ioutil.ReadFile(modPath) 184 | if err != nil { 185 | return "" 186 | } 187 | return modfile.ModulePath(content) 188 | } 189 | -------------------------------------------------------------------------------- /gocover_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "golang.org/x/tools/cover" 8 | ) 9 | 10 | func TestMergeProfs(t *testing.T) { 11 | t.Parallel() 12 | 13 | tests := []struct { 14 | in [][]*cover.Profile 15 | want []*cover.Profile 16 | }{ 17 | // empty 18 | {in: nil, want: nil}, 19 | // The number of profiles is 1 20 | {in: [][]*cover.Profile{{{FileName: "name1"}}}, want: []*cover.Profile{{FileName: "name1"}}}, 21 | // merge profile blocks 22 | { 23 | in: [][]*cover.Profile{ 24 | {}, // skip first empty profiles. 25 | { 26 | { 27 | FileName: "name1", 28 | Blocks: []cover.ProfileBlock{ 29 | {StartLine: 1, StartCol: 1, Count: 1}, 30 | }, 31 | }, 32 | { 33 | FileName: "name2", 34 | Blocks: []cover.ProfileBlock{ 35 | {StartLine: 1, StartCol: 1, Count: 0}, 36 | }, 37 | }, 38 | }, 39 | {}, // skip first empty profiles. 40 | { 41 | { 42 | FileName: "name1", 43 | Blocks: []cover.ProfileBlock{ 44 | {StartLine: 1, StartCol: 1, Count: 1}, 45 | }, 46 | }, 47 | { 48 | FileName: "name2", 49 | Blocks: []cover.ProfileBlock{ 50 | {StartLine: 1, StartCol: 1, Count: 1}, 51 | }, 52 | }, 53 | }, 54 | }, 55 | want: []*cover.Profile{ 56 | { 57 | FileName: "name1", 58 | Blocks: []cover.ProfileBlock{ 59 | {StartLine: 1, StartCol: 1, Count: 2}, 60 | }, 61 | }, 62 | { 63 | FileName: "name2", 64 | Blocks: []cover.ProfileBlock{ 65 | {StartLine: 1, StartCol: 1, Count: 1}, 66 | }, 67 | }, 68 | }, 69 | }, 70 | } 71 | 72 | for _, tt := range tests { 73 | if got := mergeProfs(tt.in); !reflect.DeepEqual(got, tt.want) { 74 | t.Errorf("mergeProfs(%#v) = %#v, want %#v", tt.in, got, tt.want) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /goveralls.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Yasuhiro Matsumoto, Jason McVetta. 2 | // This is Free Software, released under the MIT license. 3 | // See http://mattn.mit-license.org/2013 for details. 4 | 5 | // goveralls is a Go client for Coveralls.io. 6 | package main 7 | 8 | import ( 9 | "bytes" 10 | _ "crypto/sha512" 11 | "crypto/tls" 12 | "encoding/json" 13 | "errors" 14 | "flag" 15 | "fmt" 16 | "go/build" 17 | "io/ioutil" 18 | "log" 19 | "net/http" 20 | "net/url" 21 | "os" 22 | "os/exec" 23 | "path/filepath" 24 | "regexp" 25 | "strconv" 26 | "strings" 27 | "time" 28 | 29 | "golang.org/x/tools/cover" 30 | "golang.org/x/tools/go/buildutil" 31 | ) 32 | 33 | /* 34 | https://coveralls.io/docs/api_reference 35 | */ 36 | 37 | // Flags are extra flags to the tests 38 | type Flags []string 39 | 40 | // String implements flag.Value interface. 41 | func (a *Flags) String() string { 42 | return strings.Join(*a, ",") 43 | } 44 | 45 | // Set implements flag.Value interface. 46 | func (a *Flags) Set(value string) error { 47 | *a = append(*a, value) 48 | return nil 49 | } 50 | 51 | var ( 52 | extraFlags Flags 53 | pkg = flag.String("package", "", "Go package") 54 | verbose = flag.Bool("v", false, "Pass '-v' argument to 'go test' and output to stdout") 55 | race = flag.Bool("race", false, "Pass '-race' argument to 'go test'") 56 | debug = flag.Bool("debug", false, "Enable debug output") 57 | coverprof = flag.String("coverprofile", "", "If supplied, use a go cover profile (comma separated)") 58 | covermode = flag.String("covermode", "count", "sent as covermode argument to go test") 59 | repotoken = flag.String("repotoken", os.Getenv("COVERALLS_TOKEN"), "Repository Token on coveralls") 60 | reponame = flag.String("reponame", "", "Repository name") 61 | repotokenfile = flag.String("repotokenfile", os.Getenv("COVERALLS_TOKEN_FILE"), "Repository Token file on coveralls") 62 | parallel = flag.Bool("parallel", os.Getenv("COVERALLS_PARALLEL") != "", "Submit as parallel") 63 | endpoint = flag.String("endpoint", "https://coveralls.io", "Hostname to submit Coveralls data to") 64 | service = flag.String("service", "", "The CI service or other environment in which the test suite was run. ") 65 | shallow = flag.Bool("shallow", false, "Shallow coveralls internal server errors") 66 | ignore = flag.String("ignore", "", "Comma separated files to ignore") 67 | insecure = flag.Bool("insecure", false, "Set insecure to skip verification of certificates") 68 | uploadSource = flag.Bool("uploadsource", true, "Read local source and upload it to coveralls") 69 | allowGitFetch = flag.Bool("allowgitfetch", true, "Perform a 'git fetch' when the reference is different than HEAD; used for GitHub Actions integration") 70 | show = flag.Bool("show", false, "Show which package is being tested") 71 | customJobID = flag.String("jobid", "", "Custom set job token") 72 | jobNumber = flag.String("jobnumber", "", "Custom set job number") 73 | flagName = flag.String("flagname", os.Getenv("COVERALLS_FLAG_NAME"), "Job flag name, e.g. \"Unit\", \"Functional\", or \"Integration\". Will be shown in the Coveralls UI.") 74 | 75 | parallelFinish = flag.Bool("parallel-finish", false, "finish parallel test") 76 | ) 77 | 78 | func init() { 79 | flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc) 80 | } 81 | 82 | // usage supplants package flag's Usage variable 83 | var usage = func() { 84 | cmd := filepath.Base(os.Args[0]) 85 | s := "Usage: %s [options]\n" 86 | fmt.Fprintf(os.Stderr, s, cmd) 87 | flag.PrintDefaults() 88 | } 89 | 90 | // A SourceFile represents a source code file and its coverage data for a 91 | // single job. 92 | type SourceFile struct { 93 | Name string `json:"name"` // File path of this source file 94 | Source string `json:"source"` // Full source code of this file 95 | Coverage []interface{} `json:"coverage"` // Requires both nulls and integers 96 | } 97 | 98 | // A Job represents the coverage data from a single run of a test suite. 99 | type Job struct { 100 | RepoToken *string `json:"repo_token,omitempty"` 101 | ServiceJobID string `json:"service_job_id"` 102 | ServiceJobNumber string `json:"service_job_number,omitempty"` 103 | ServicePullRequest string `json:"service_pull_request,omitempty"` 104 | ServiceName string `json:"service_name"` 105 | FlagName string `json:"flag_name,omitempty"` 106 | SourceFiles []*SourceFile `json:"source_files"` 107 | Parallel *bool `json:"parallel,omitempty"` 108 | Git *Git `json:"git,omitempty"` 109 | RunAt time.Time `json:"run_at"` 110 | } 111 | 112 | // A Response is returned by the Coveralls.io API. 113 | type Response struct { 114 | Message string `json:"message"` 115 | URL string `json:"url"` 116 | Error bool `json:"error"` 117 | } 118 | 119 | // A WebHookResponse is returned by the Coveralls.io WebHook. 120 | type WebHookResponse struct { 121 | Done bool `json:"done"` 122 | } 123 | 124 | // getPkgs returns packages for measuring coverage. Returned packages doesn't 125 | // contain vendor packages. 126 | func getPkgs(pkg string) ([]string, error) { 127 | argList := []string{"list"} 128 | if pkg == "" { 129 | argList = append(argList, "./...") 130 | } else { 131 | argList = append(argList, strings.Split(pkg, " ")...) 132 | } 133 | out, err := exec.Command("go", argList...).CombinedOutput() 134 | if err != nil { 135 | return nil, err 136 | } 137 | allPkgs := strings.Split(strings.Trim(string(out), "\n"), "\n") 138 | pkgs := make([]string, 0, len(allPkgs)) 139 | for _, p := range allPkgs { 140 | if strings.Contains(p, "/vendor/") { 141 | continue 142 | } 143 | // go modules output 144 | if strings.Contains(p, "go: ") { 145 | continue 146 | } 147 | pkgs = append(pkgs, p) 148 | } 149 | return pkgs, nil 150 | } 151 | 152 | func getCoverage() ([]*SourceFile, error) { 153 | if *coverprof != "" { 154 | return parseCover(*coverprof) 155 | } 156 | 157 | // pkgs is packages to run tests and get coverage. 158 | pkgs, err := getPkgs(*pkg) 159 | if err != nil { 160 | return nil, err 161 | } 162 | coverpkg := fmt.Sprintf("-coverpkg=%s", strings.Join(pkgs, ",")) 163 | var pfss [][]*cover.Profile 164 | for _, line := range pkgs { 165 | f, err := ioutil.TempFile("", "goveralls") 166 | if err != nil { 167 | return nil, err 168 | } 169 | f.Close() 170 | cmd := exec.Command("go") 171 | outBuf := new(bytes.Buffer) 172 | cmd.Stdout = outBuf 173 | cmd.Stderr = outBuf 174 | coverm := *covermode 175 | if *race { 176 | coverm = "atomic" 177 | } 178 | args := []string{"go", "test", "-covermode", coverm, "-coverprofile", f.Name(), coverpkg} 179 | if *verbose { 180 | args = append(args, "-v") 181 | cmd.Stdout = os.Stdout 182 | } 183 | if *race { 184 | args = append(args, "-race") 185 | } 186 | args = append(args, extraFlags...) 187 | args = append(args, line) 188 | cmd.Args = args 189 | 190 | if *show { 191 | fmt.Println("goveralls:", line) 192 | } 193 | err = cmd.Run() 194 | if err != nil { 195 | return nil, fmt.Errorf("%v: %v", err, outBuf.String()) 196 | } 197 | 198 | pfs, err := cover.ParseProfiles(f.Name()) 199 | if err != nil { 200 | return nil, err 201 | } 202 | err = os.Remove(f.Name()) 203 | if err != nil { 204 | return nil, err 205 | } 206 | pfss = append(pfss, pfs) 207 | } 208 | 209 | sourceFiles, err := toSF(mergeProfs(pfss)) 210 | if err != nil { 211 | return nil, err 212 | } 213 | 214 | return sourceFiles, nil 215 | } 216 | 217 | var vscDirs = []string{".git", ".hg", ".bzr", ".svn"} 218 | 219 | func findRepositoryRoot(dir string) (string, bool) { 220 | for _, vcsdir := range vscDirs { 221 | if d, err := os.Stat(filepath.Join(dir, vcsdir)); err == nil && d.IsDir() { 222 | return dir, true 223 | } 224 | } 225 | nextdir := filepath.Dir(dir) 226 | if nextdir == dir { 227 | return "", false 228 | } 229 | return findRepositoryRoot(nextdir) 230 | } 231 | 232 | func getCoverallsSourceFileName(name string) string { 233 | if dir, ok := findRepositoryRoot(name); ok { 234 | name = strings.TrimPrefix(name, dir+string(os.PathSeparator)) 235 | } 236 | return filepath.ToSlash(name) 237 | } 238 | 239 | // processParallelFinish notifies coveralls that all jobs are completed 240 | // ref. https://docs.coveralls.io/parallel-build-webhook 241 | func processParallelFinish(jobID, token string) error { 242 | var name string 243 | if reponame != nil && *reponame != "" { 244 | name = *reponame 245 | } else if s := os.Getenv("GITHUB_REPOSITORY"); s != "" { 246 | name = s 247 | } 248 | 249 | params := make(url.Values) 250 | params.Set("repo_token", token) 251 | if name != "" { 252 | params.Set("repo_name", name) 253 | } 254 | params.Set("payload[build_num]", jobID) 255 | params.Set("payload[status]", "done") 256 | res, err := http.PostForm(*endpoint+"/webhook", params) 257 | if *debug { 258 | if token != "" { 259 | params.Set("repo_token", "*******") 260 | } 261 | log.Printf("Posted webhook data: %q", params.Encode()) 262 | } 263 | 264 | if err != nil { 265 | return err 266 | } 267 | defer res.Body.Close() 268 | bodyBytes, err := ioutil.ReadAll(res.Body) 269 | if err != nil { 270 | return fmt.Errorf("unable to read response body from coveralls: %s", err) 271 | } 272 | 273 | if *shallow { 274 | if res.StatusCode >= http.StatusInternalServerError { 275 | fmt.Println("coveralls server failed internally") 276 | return nil 277 | } 278 | 279 | // XXX: It looks that Coveralls is under maintenance. 280 | // Coveralls serves the maintenance page as a static HTML hosting, 281 | // and the maintenance page doesn't accept POST method. 282 | // See https://github.com/mattn/goveralls/issues/204 283 | if res.StatusCode == http.StatusMethodNotAllowed { 284 | fmt.Println("it looks that Coveralls is under maintenance. visit https://status.coveralls.io/") 285 | return nil 286 | } 287 | } 288 | 289 | if res.StatusCode != http.StatusOK { 290 | return fmt.Errorf("bad response status from coveralls: %d\n%s", res.StatusCode, bodyBytes) 291 | } 292 | 293 | var response WebHookResponse 294 | if err = json.Unmarshal(bodyBytes, &response); err != nil { 295 | return fmt.Errorf("unable to unmarshal response JSON from coveralls: %s\n%s", err, bodyBytes) 296 | } 297 | 298 | if !response.Done { 299 | return fmt.Errorf("jobs are not completed:\n%s", bodyBytes) 300 | } 301 | 302 | return nil 303 | } 304 | 305 | func process() error { 306 | log.SetFlags(log.Ltime | log.Lshortfile) 307 | // 308 | // Parse Flags 309 | // 310 | flag.Usage = usage 311 | flag.Var(&extraFlags, "flags", "extra flags to the tests") 312 | flag.Parse() 313 | if len(flag.Args()) > 0 { 314 | flag.Usage() 315 | os.Exit(2) 316 | } 317 | 318 | // 319 | // Setup PATH environment variable 320 | // 321 | paths := filepath.SplitList(os.Getenv("PATH")) 322 | if goroot := os.Getenv("GOROOT"); goroot != "" { 323 | paths = append(paths, filepath.Join(goroot, "bin")) 324 | } 325 | if gopath := os.Getenv("GOPATH"); gopath != "" { 326 | for _, path := range filepath.SplitList(gopath) { 327 | paths = append(paths, filepath.Join(path, "bin")) 328 | } 329 | } 330 | os.Setenv("PATH", strings.Join(paths, string(filepath.ListSeparator))) 331 | 332 | // 333 | // Handle certificate verification configuration 334 | // 335 | if *insecure { 336 | http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 337 | } 338 | 339 | // 340 | // Initialize Job 341 | // 342 | 343 | // flags are never nil, so no nil check needed 344 | githubEvent := getGithubEvent() 345 | var jobID string 346 | if *customJobID != "" { 347 | jobID = *customJobID 348 | } else if ServiceJobID := os.Getenv("COVERALLS_SERVICE_JOB_ID"); ServiceJobID != "" { 349 | jobID = ServiceJobID 350 | } else if travisjobID := os.Getenv("TRAVIS_JOB_ID"); travisjobID != "" { 351 | jobID = travisjobID 352 | } else if circleCIJobID := os.Getenv("CIRCLE_BUILD_NUM"); circleCIJobID != "" { 353 | jobID = circleCIJobID 354 | } else if appveyorJobID := os.Getenv("APPVEYOR_JOB_ID"); appveyorJobID != "" { 355 | jobID = appveyorJobID 356 | } else if semaphorejobID := os.Getenv("SEMAPHORE_BUILD_NUMBER"); semaphorejobID != "" { 357 | jobID = semaphorejobID 358 | } else if jenkinsjobID := os.Getenv("BUILD_NUMBER"); jenkinsjobID != "" { 359 | jobID = jenkinsjobID 360 | } else if buildID := os.Getenv("BUILDKITE_BUILD_ID"); buildID != "" { 361 | jobID = buildID 362 | } else if droneBuildNumber := os.Getenv("DRONE_BUILD_NUMBER"); droneBuildNumber != "" { 363 | jobID = droneBuildNumber 364 | } else if buildkiteBuildNumber := os.Getenv("BUILDKITE_BUILD_NUMBER"); buildkiteBuildNumber != "" { 365 | jobID = buildkiteBuildNumber 366 | } else if codeshipjobID := os.Getenv("CI_BUILD_ID"); codeshipjobID != "" { 367 | jobID = codeshipjobID 368 | } else if githubRunID := os.Getenv("GITHUB_RUN_ID"); githubRunID != "" { 369 | jobID = githubRunID 370 | } else if gitlabRunID := os.Getenv("CI_PIPELINE_ID"); gitlabRunID != "" { 371 | jobID = gitlabRunID 372 | } 373 | 374 | if *repotoken == "" && *repotokenfile != "" { 375 | tokenBytes, err := ioutil.ReadFile(*repotokenfile) 376 | if err != nil { 377 | return err 378 | } 379 | *repotoken = strings.TrimSpace(string(tokenBytes)) 380 | } 381 | 382 | if *parallelFinish { 383 | return processParallelFinish(jobID, *repotoken) 384 | } 385 | 386 | if *repotoken == "" { 387 | repotoken = nil // remove the entry from json 388 | } 389 | 390 | head := "HEAD" 391 | var pullRequest string 392 | if prNumber := os.Getenv("CIRCLE_PR_NUMBER"); prNumber != "" { 393 | // for Circle CI (pull request from forked repo) 394 | pullRequest = prNumber 395 | } else if prURL := os.Getenv("CIRCLE_PULL_REQUEST"); prURL != "" { 396 | // for Circle CI (all other pull requests) 397 | pullRequest = regexp.MustCompile(`[0-9]+$`).FindString(prURL) 398 | } else if prNumber := os.Getenv("TRAVIS_PULL_REQUEST"); prNumber != "" && prNumber != "false" { 399 | pullRequest = prNumber 400 | } else if prNumber := os.Getenv("APPVEYOR_PULL_REQUEST_NUMBER"); prNumber != "" { 401 | pullRequest = prNumber 402 | } else if prNumber := os.Getenv("PULL_REQUEST_NUMBER"); prNumber != "" { 403 | pullRequest = prNumber 404 | } else if prNumber := os.Getenv("BUILDKITE_PULL_REQUEST"); prNumber != "" { 405 | pullRequest = prNumber 406 | } else if prNumber := os.Getenv("DRONE_PULL_REQUEST"); prNumber != "" { 407 | pullRequest = prNumber 408 | } else if prNumber := os.Getenv("BUILDKITE_PULL_REQUEST"); prNumber != "" { 409 | pullRequest = prNumber 410 | } else if prNumber := os.Getenv("CI_PR_NUMBER"); prNumber != "" { 411 | pullRequest = prNumber 412 | } else if prNumber := os.Getenv("CHANGE_ID"); prNumber != "" { 413 | // for Jenkins multibranch projects 414 | pullRequest = prNumber 415 | } else if prURL := os.Getenv("CHANGE_URL"); prURL != "" { 416 | // for Jenkins multibranch projects 417 | pullRequest = regexp.MustCompile(`[0-9]+$`).FindString(prURL) 418 | } else if prURL := os.Getenv("CI_PULL_REQUEST"); prURL != "" { 419 | // for Circle CI 420 | pullRequest = regexp.MustCompile(`[0-9]+$`).FindString(prURL) 421 | } else if os.Getenv("GITHUB_EVENT_NAME") == "pull_request" { 422 | number := githubEvent["number"].(float64) 423 | pullRequest = strconv.Itoa(int(number)) 424 | 425 | ghPR := githubEvent["pull_request"].(map[string]interface{}) 426 | ghHead := ghPR["head"].(map[string]interface{}) 427 | head = ghHead["sha"].(string) 428 | } else if prNumber := os.Getenv("CI_MERGE_REQUEST_IID"); prNumber != "" { 429 | // pull request id from GitHub when building on GitLab 430 | pullRequest = prNumber 431 | } else if prNumber := os.Getenv("CI_EXTERNAL_PULL_REQUEST_IID"); prNumber != "" { 432 | pullRequest = prNumber 433 | } 434 | 435 | if *service == "" && os.Getenv("TRAVIS_JOB_ID") != "" { 436 | *service = "travis-ci" 437 | } 438 | 439 | sourceFiles, err := getCoverage() 440 | if err != nil { 441 | return err 442 | } 443 | 444 | gitInfo, err := collectGitInfo(head) 445 | if err != nil { 446 | return err 447 | } 448 | 449 | j := Job{ 450 | RunAt: time.Now(), 451 | RepoToken: repotoken, 452 | ServicePullRequest: pullRequest, 453 | Parallel: parallel, 454 | Git: gitInfo, 455 | SourceFiles: sourceFiles, 456 | ServiceName: *service, 457 | FlagName: *flagName, 458 | } 459 | 460 | // Only include a job ID if it's known, otherwise, Coveralls looks 461 | // for the job and can't find it. 462 | if jobID != "" { 463 | j.ServiceJobID = jobID 464 | } 465 | j.ServiceJobNumber = *jobNumber 466 | 467 | // Ignore files 468 | if len(*ignore) > 0 { 469 | patterns := strings.Split(*ignore, ",") 470 | for i, pattern := range patterns { 471 | patterns[i] = strings.TrimSpace(pattern) 472 | } 473 | var files []*SourceFile 474 | Files: 475 | for _, file := range j.SourceFiles { 476 | for _, pattern := range patterns { 477 | match, err := filepath.Match(pattern, file.Name) 478 | if err != nil { 479 | return err 480 | } 481 | if match { 482 | fmt.Printf("ignoring %s\n", file.Name) 483 | continue Files 484 | } 485 | } 486 | files = append(files, file) 487 | } 488 | j.SourceFiles = files 489 | } 490 | 491 | if *debug { 492 | j := j 493 | if j.RepoToken != nil && *j.RepoToken != "" { 494 | s := "*******" 495 | j.RepoToken = &s 496 | } 497 | b, err := json.MarshalIndent(j, "", " ") 498 | if err != nil { 499 | return err 500 | } 501 | log.Printf("Posting data: %s", b) 502 | } 503 | 504 | b, err := json.Marshal(j) 505 | if err != nil { 506 | return err 507 | } 508 | 509 | params := make(url.Values) 510 | params.Set("json", string(b)) 511 | res, err := http.PostForm(*endpoint+"/api/v1/jobs", params) 512 | if err != nil { 513 | return err 514 | } 515 | defer res.Body.Close() 516 | bodyBytes, err := ioutil.ReadAll(res.Body) 517 | if err != nil { 518 | return fmt.Errorf("unable to read response body from coveralls: %s", err) 519 | } 520 | 521 | if *shallow { 522 | if res.StatusCode >= http.StatusInternalServerError { 523 | fmt.Println("coveralls server failed internally") 524 | return nil 525 | } 526 | 527 | // XXX: It looks that Coveralls is under maintenance. 528 | // Coveralls serves the maintenance page as a static HTML hosting, 529 | // and the maintenance page doesn't accept POST method. 530 | // See https://github.com/mattn/goveralls/issues/204 531 | if res.StatusCode == http.StatusMethodNotAllowed { 532 | fmt.Println("it looks that Coveralls is under maintenance. visit https://status.coveralls.io/") 533 | return nil 534 | } 535 | } 536 | 537 | if res.StatusCode != 200 { 538 | return fmt.Errorf("bad response status from coveralls: %d\n%s", res.StatusCode, bodyBytes) 539 | } 540 | var response Response 541 | if err = json.Unmarshal(bodyBytes, &response); err != nil { 542 | return fmt.Errorf("unable to unmarshal response JSON from coveralls: %s\n%s", err, bodyBytes) 543 | } 544 | if response.Error { 545 | return errors.New(response.Message) 546 | } 547 | fmt.Println(response.Message) 548 | fmt.Println(response.URL) 549 | return nil 550 | } 551 | 552 | func getGithubEvent() map[string]interface{} { 553 | jsonFilePath := os.Getenv("GITHUB_EVENT_PATH") 554 | if jsonFilePath == "" { 555 | return nil 556 | } 557 | 558 | jsonFile, err := os.Open(jsonFilePath) 559 | if err != nil { 560 | log.Fatal(err) 561 | } 562 | defer jsonFile.Close() 563 | 564 | jsonByte, _ := ioutil.ReadAll(jsonFile) 565 | 566 | // unmarshal the json into a release event 567 | var event map[string]interface{} 568 | err = json.Unmarshal(jsonByte, &event) 569 | if err != nil { 570 | log.Fatal(err) 571 | } 572 | 573 | return event 574 | } 575 | 576 | func main() { 577 | if err := process(); err != nil { 578 | fmt.Fprintf(os.Stderr, "%s\n", err) 579 | os.Exit(1) 580 | } 581 | } 582 | -------------------------------------------------------------------------------- /goveralls_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/url" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "runtime" 12 | "strings" 13 | "testing" 14 | 15 | "net/http" 16 | "net/http/httptest" 17 | ) 18 | 19 | var goverallsTestBin string 20 | 21 | func TestMain(m *testing.M) { 22 | tmpBin, err := ioutil.TempDir("", "goveralls_") 23 | if err != nil { 24 | fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) 25 | os.Exit(1) 26 | } 27 | 28 | // generate the test binary used by all tests 29 | goverallsTestBin = filepath.Join(tmpBin, "bin", "goveralls") 30 | if runtime.GOOS == "windows" { 31 | goverallsTestBin += ".exe" 32 | } 33 | _, err = exec.Command("go", "build", "-o", goverallsTestBin, ".").CombinedOutput() 34 | if err != nil { 35 | fmt.Fprintf(os.Stderr, "ERROR: %v\n", err) 36 | os.Exit(1) 37 | } 38 | 39 | // run all tests 40 | exitVal := m.Run() 41 | 42 | os.RemoveAll(tmpBin) 43 | 44 | os.Exit(exitVal) 45 | } 46 | 47 | func fakeServer() *httptest.Server { 48 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 49 | w.WriteHeader(http.StatusOK) 50 | fmt.Fprintln(w, `{"error":false,"message":"Fake message","URL":"http://fake.url"}`) 51 | })) 52 | } 53 | 54 | func fakeServerWithPayloadChannel(payload chan Job) *httptest.Server { 55 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 56 | body, err := ioutil.ReadAll(r.Body) 57 | if err != nil { 58 | w.WriteHeader(http.StatusInternalServerError) 59 | return 60 | } 61 | // query params are used for the body payload 62 | vals, err := url.ParseQuery(string(body)) 63 | if err != nil { 64 | w.WriteHeader(http.StatusInternalServerError) 65 | return 66 | } 67 | 68 | var job Job 69 | err = json.Unmarshal([]byte(vals["json"][0]), &job) 70 | if err != nil { 71 | w.WriteHeader(http.StatusInternalServerError) 72 | return 73 | } 74 | payload <- job 75 | 76 | w.WriteHeader(http.StatusOK) 77 | // this is a standard baked response 78 | fmt.Fprintln(w, `{"error":false,"message":"Fake message","URL":"http://fake.url"}`) 79 | })) 80 | } 81 | 82 | func TestCustomJobId(t *testing.T) { 83 | t.Parallel() 84 | 85 | jobBodyChannel := make(chan Job, 16) 86 | fs := fakeServerWithPayloadChannel(jobBodyChannel) 87 | 88 | b, err := testRun("-jobid=123abc", "-package=github.com/mattn/goveralls/tester", "-endpoint", "-v", "-endpoint", fs.URL) 89 | if err != nil { 90 | t.Fatal("Expected exit code 0 got 1", err, string(b)) 91 | } 92 | 93 | jobBody := <-jobBodyChannel 94 | 95 | if jobBody.ServiceJobID != "123abc" { 96 | t.Fatalf("Expected job id of 123abc, but was %s", jobBody.ServiceJobID) 97 | } 98 | } 99 | 100 | func TestInvalidArg(t *testing.T) { 101 | t.Parallel() 102 | 103 | b, err := testRun("pkg") 104 | if err == nil { 105 | t.Fatal("Expected exit code 1 got 0") 106 | } 107 | s := strings.Split(string(b), "\n")[0] 108 | expectedPrefix := "Usage: goveralls" 109 | if !strings.HasPrefix(s, expectedPrefix) { 110 | t.Fatalf("Expected %q, but got %q", expectedPrefix, s) 111 | } 112 | } 113 | 114 | func TestVerboseArg(t *testing.T) { 115 | t.Parallel() 116 | 117 | fs := fakeServer() 118 | 119 | t.Run("with verbose", func(t *testing.T) { 120 | t.Parallel() 121 | 122 | b, err := testRun("-package=github.com/mattn/goveralls/tester", "-v", "-endpoint", "-v", "-endpoint", fs.URL) 123 | if err != nil { 124 | t.Fatal("Expected exit code 0 got 1", err, string(b)) 125 | } 126 | 127 | if !strings.Contains(string(b), "--- PASS") { 128 | t.Error("Expected to have verbose go test output in stdout", string(b)) 129 | } 130 | }) 131 | 132 | t.Run("without verbose", func(t *testing.T) { 133 | t.Parallel() 134 | 135 | b, err := testRun("-package=github.com/mattn/goveralls/tester", "-endpoint", "-v", "-endpoint", fs.URL) 136 | if err != nil { 137 | t.Fatal("Expected exit code 0 got 1", err, string(b)) 138 | } 139 | 140 | if strings.Contains(string(b), "--- PASS") { 141 | t.Error("Expected to haven't verbose go test output in stdout", string(b)) 142 | } 143 | }) 144 | } 145 | 146 | func TestShowArg(t *testing.T) { 147 | t.Parallel() 148 | 149 | fs := fakeServer() 150 | 151 | t.Run("with show", func(t *testing.T) { 152 | t.Parallel() 153 | 154 | b, err := testRun("-package=github.com/mattn/goveralls/tester/...", "-show", "-endpoint", "-show", "-endpoint", fs.URL) 155 | if err != nil { 156 | t.Fatal("Expected exit code 0 got 1", err, string(b)) 157 | } 158 | 159 | expected := `goveralls: github.com/mattn/goveralls/tester 160 | Fake message 161 | http://fake.url 162 | ` 163 | if string(b) != expected { 164 | t.Error("Unexpected output for -show:", string(b)) 165 | } 166 | }) 167 | } 168 | 169 | func TestRaceArg(t *testing.T) { 170 | t.Parallel() 171 | 172 | fs := fakeServer() 173 | 174 | t.Run("it should pass the test", func(t *testing.T) { 175 | t.Parallel() 176 | 177 | b, err := testRun("-package=github.com/mattn/goveralls/tester", "-race", "-endpoint", fs.URL) 178 | if err != nil { 179 | t.Fatal("Expected exit code 0 got 1", err, string(b)) 180 | } 181 | }) 182 | } 183 | 184 | func TestUploadSource(t *testing.T) { 185 | t.Parallel() 186 | 187 | t.Run("with uploadsource", func(t *testing.T) { 188 | t.Parallel() 189 | 190 | jobBodyChannel := make(chan Job, 16) 191 | fs := fakeServerWithPayloadChannel(jobBodyChannel) 192 | 193 | b, err := testRun("-uploadsource=true", "-package=github.com/mattn/goveralls/tester", "-endpoint", "-v", "-endpoint", fs.URL) 194 | if err != nil { 195 | t.Fatal("Expected exit code 0 got 1", err, string(b)) 196 | } 197 | 198 | jobBody := <-jobBodyChannel 199 | 200 | for _, sf := range jobBody.SourceFiles { 201 | if len(sf.Source) == 0 { 202 | t.Fatalf("expected source for %q to not be empty", sf.Name) 203 | } 204 | } 205 | }) 206 | 207 | t.Run("without uploadsource", func(t *testing.T) { 208 | t.Parallel() 209 | 210 | jobBodyChannel := make(chan Job, 16) 211 | fs := fakeServerWithPayloadChannel(jobBodyChannel) 212 | 213 | b, err := testRun("-uploadsource=false", "-package=github.com/mattn/goveralls/tester", "-endpoint", "-v", "-endpoint", fs.URL) 214 | if err != nil { 215 | t.Fatal("Expected exit code 0 got 1", err, string(b)) 216 | } 217 | 218 | jobBody := <-jobBodyChannel 219 | for _, sf := range jobBody.SourceFiles { 220 | if len(sf.Source) != 0 { 221 | t.Fatalf("expected source for %q to be empty", sf.Name) 222 | } 223 | } 224 | }) 225 | } 226 | 227 | func testRun(args ...string) ([]byte, error) { 228 | // always disallow the git fetch automatically used for GitHub Actions 229 | args = append([]string{"-allowgitfetch=false"}, args...) 230 | return exec.Command(goverallsTestBin, args...).CombinedOutput() 231 | } 232 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "schedule": "before 3am on the first day of the month" 6 | } 7 | -------------------------------------------------------------------------------- /tester/cov.json: -------------------------------------------------------------------------------- 1 | {"Packages":[{"Name":"github.com/dustin/goveralls/tester","Functions":[{"Name":"GoverallsTester","File":"/Users/dustin/go/src/github.com/dustin/goveralls/tester/tester.go","Start":34,"End":150,"Statements":[{"Start":67,"End":101,"Reached":1},{"Start":103,"End":138,"Reached":1},{"Start":118,"End":135,"Reached":1},{"Start":140,"End":148,"Reached":1}],"Entered":0,"Left":0}]}]} 2 | -------------------------------------------------------------------------------- /tester/tester.go: -------------------------------------------------------------------------------- 1 | package tester 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | func GoverallsTester() string { 8 | s := os.Getenv("GOVERALLS_TESTER") 9 | if s == "" { 10 | s = "hello world" 11 | } 12 | return s 13 | } 14 | -------------------------------------------------------------------------------- /tester/tester_test.go: -------------------------------------------------------------------------------- 1 | package tester 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSimple(t *testing.T) { 8 | value := GoverallsTester() 9 | expected := "hello world" 10 | if value != expected { 11 | t.Fatalf("Expected %v, but %v:", value, expected) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /usage_ge1.15_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.15 2 | // +build go1.15 3 | 4 | package main 5 | 6 | import ( 7 | "os/exec" 8 | "runtime" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestUsage(t *testing.T) { 14 | t.Parallel() 15 | 16 | cmd := exec.Command(goverallsTestBin, "-h") 17 | b, err := cmd.CombinedOutput() 18 | runtime.Version() 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | s := strings.Split(string(b), "\n")[0] 23 | expectedPrefix := "Usage: goveralls" 24 | if runtime.GOOS == "windows" { 25 | expectedPrefix += ".exe" 26 | } 27 | if !strings.HasPrefix(s, expectedPrefix) { 28 | t.Fatalf("Expected prefix %q, but got %q", expectedPrefix, s) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /usage_lt1.15_test.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.15 2 | // +build !go1.15 3 | 4 | package main 5 | 6 | import ( 7 | "os/exec" 8 | "runtime" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestUsage(t *testing.T) { 14 | t.Parallel() 15 | 16 | cmd := exec.Command(goverallsTestBin, "-h") 17 | b, err := cmd.CombinedOutput() 18 | runtime.Version() 19 | if err == nil { 20 | t.Fatal("Expected exit code 1 got 0") 21 | } 22 | s := strings.Split(string(b), "\n")[0] 23 | expectedPrefix := "Usage: goveralls" 24 | if runtime.GOOS == "windows" { 25 | expectedPrefix += ".exe" 26 | } 27 | if !strings.HasPrefix(s, expectedPrefix) { 28 | t.Fatalf("Expected prefix %q, but got %q", expectedPrefix, s) 29 | } 30 | } 31 | --------------------------------------------------------------------------------