├── .github ├── dependabot.yml └── workflows │ ├── create_release.yml │ └── test_and_build.yml ├── .gitignore ├── .goreleaser.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── pre-commit └── testdata ├── coverage.out └── coverage_expected.lcov /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | commit-message: 8 | prefix: "[gomod] " 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | commit-message: 14 | prefix: "[gh-actions] " 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/create_release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' 5 | 6 | name: Upload release assets after tagging 7 | jobs: 8 | build: 9 | name: create assets 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Install Go 13 | uses: actions/setup-go@v5 14 | with: 15 | go-version: 1.23.1 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | - name: Run GoReleaser 19 | uses: goreleaser/goreleaser-action@v6 20 | if: startsWith(github.ref, 'refs/tags/') 21 | with: 22 | version: latest 23 | args: release --clean 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/test_and_build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | name: test and build 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | - name: Install Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: 1.23.x 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v6 22 | with: 23 | version: latest 24 | 25 | test: 26 | runs-on: ubuntu-latest 27 | strategy: 28 | matrix: 29 | goversion: ["1.20", "1.21", "1.22", "1.23"] 30 | steps: 31 | - name: Checkout code 32 | uses: actions/checkout@v4 33 | - name: Install Go 34 | uses: actions/setup-go@v5 35 | with: 36 | go-version: ${{ matrix.goversion }} 37 | - name: Run tests 38 | run: | 39 | go version 40 | make test 41 | 42 | inttest: 43 | runs-on: ubuntu-latest 44 | strategy: 45 | matrix: 46 | buildversion: ["1.20", "1.21", "1.22", "1.23"] 47 | testversion: ["1.20", "1.21", "1.22", "1.23"] 48 | steps: 49 | - name: Checkout code 50 | uses: actions/checkout@v4 51 | - name: Install Go to build artifact 52 | uses: actions/setup-go@v5 53 | with: 54 | go-version: ${{ matrix.buildversion }} 55 | - name: Build artifact for inttest 56 | run: | 57 | go version 58 | make build-linux 59 | - name: Install Go for inttest 60 | uses: actions/setup-go@v5 61 | with: 62 | go-version: ${{ matrix.testversion }} 63 | - name: Integration test 64 | run: | 65 | go version 66 | make inttest 67 | 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.exe 3 | dist/ 4 | bin/ 5 | coverage.out 6 | coverage.lcov 7 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | 7 | builds: 8 | - id: other 9 | env: 10 | - CGO_ENABLED=0 11 | dir: . 12 | goos: 13 | - linux 14 | - darwin 15 | - windows 16 | - freebsd 17 | - openbsd 18 | goarch: 19 | - amd64 20 | - arm 21 | - arm64 22 | goarm: 23 | - "6" 24 | - "7" 25 | ignore: 26 | - goos: darwin 27 | goarch: arm 28 | - goos: openbsd 29 | goarch: arm64 30 | - goos: windows 31 | goarch: arm 32 | 33 | # linux/amd64 binary is named according to the pre 1.1.0 version to ensure 34 | # upwards compatibility of the gcov2lcov-action. see also below in the archives 35 | # section 36 | - id: linux_amd64 37 | env: 38 | - CGO_ENABLED=0 39 | dir: . 40 | binary: bin/gcov2lcov-linux-amd64 41 | goos: 42 | - linux 43 | goarch: 44 | - amd64 45 | 46 | # linux/amd64 binary is named according to the pre 1.1.0 version to ensure 47 | # upwards compatibility of the gcov2lcov-action 48 | archives: 49 | - id: linux_amd64 50 | builds: 51 | - linux_amd64 52 | name_template: gcov2lcov-linux-amd64 53 | files: 54 | - README.md 55 | - LICENSE 56 | - CHANGELOG.md 57 | - id: other 58 | builds: 59 | - other 60 | files: 61 | - README.md 62 | - LICENSE 63 | - CHANGELOG.md 64 | format_overrides: 65 | - goos: windows 66 | format: zip 67 | 68 | checksum: 69 | name_template: 'checksums.txt' 70 | snapshot: 71 | version_template: "{{ incpatch .Version }}-next" 72 | changelog: 73 | sort: asc 74 | filters: 75 | exclude: 76 | - '^docs:' 77 | - '^test:' 78 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # changelog for gcov2lcov 2 | 3 | ## 1.1.1 [2024-10-23] 4 | 5 | * provide a release package `gcov2lcov-linux-amd64.tar.gz` to be compatible 6 | with older `gcov2lcov-action` versions 7 | 8 | ## 1.1.0 [2024-10-11] 9 | 10 | * use goreleaser for builds and provide additional versions 11 | * dependency upgrades 12 | 13 | ## 1.0.6 [2023-08-18] 14 | 15 | * performance otimizations (thanks to zzh8829, #16) 16 | * dependency upgrades 17 | 18 | ## 1.0.5 [2021-04-28] 19 | 20 | * new: option `-use-absolute-source-path` - when set absolut path names are 21 | used for the SF value (#10) 22 | * compile with go 1.16 23 | 24 | ## 1.0.2 [2020-04-25] 25 | 26 | * fix calculation of LH and LF values which led to wrong calculation of 27 | test coverage in coveralls 28 | 29 | ## 1.0.1 [2020-04-25] 30 | 31 | * avoid duplicate DA records for same lines (see 32 | https://github.com/jandelgado/gcov2lcov-action/issues/2) 33 | 34 | ## 1.0.0 [2019-10-07] 35 | 36 | * initial release 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Richard S Allinson 4 | Copyright (c) 2019 Jan Delgado 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # makefile for gcov2lcov 2 | .PHONY: phony 3 | 4 | all: build-linux 5 | 6 | phony: 7 | 8 | build-linux: phony 9 | GOOS=linux GOARCH=amd64 go build -o bin/gcov2lcov-linux-amd64 . 10 | 11 | build-windows: phony 12 | GOOS=windows GOARCH=amd64 go build -o bin/gcov2lcov-win-amd64 . 13 | 14 | build-darwin: phony 15 | GOOS=darwin GOARCH=amd64 go build -o bin/gcov2lcov-darwin-amd64 . 16 | 17 | test: phony 18 | go test ./... -coverprofile coverage.out 19 | go tool cover -func coverage.out 20 | 21 | inttest: phony 22 | ./bin/gcov2lcov-linux-amd64 -infile testdata/coverage.out -outfile coverage.lcov 23 | diff -y testdata/coverage_expected.lcov coverage.lcov 24 | 25 | clean: phony 26 | rm -f bin/* 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gcov2lcov 2 | 3 | [![Build Status](https://github.com/jandelgado/gcov2lcov/workflows/run%20tests/badge.svg)](https://github.com/jandelgado/gcov2lcov/actions?workflow=run%20tests) 4 | [![Coverage Status](https://coveralls.io/repos/github/jandelgado/gcov2lcov/badge.svg?branch=master)](https://coveralls.io/github/jandelgado/gcov2lcov?branch=master) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/jandelgado/gcov2lcov)](https://goreportcard.com/report/github.com/jandelgado/gcov2lcov) 6 | 7 | Convert golang test coverage to lcov format (which can for example be uploaded 8 | to coveralls). 9 | 10 | See [gcov2lcov-action](https://github.com/jandelgado/gcov2lcov-action) 11 | for a github action which uses this tool. 12 | 13 | ## Credits 14 | 15 | This tool is based on [covfmt](https://github.com/ricallinson/covfmt) and 16 | uses some parts of [goveralls](https://github.com/mattn/goveralls). 17 | 18 | ## Installation 19 | 20 | ### Binary download 21 | 22 | Download a version for your platform from the [Releases](https://github.com/jandelgado/gcov2lcov/releases) page. 23 | 24 | You may have noticed that the file `gcov2lcov-linux-amd64.tar.gz` does not 25 | follow the naming convention used for other artifacts. This particular file is 26 | provided for backward compatibility with the `gcov2lcov-action` and can be 27 | disregarded for general use. 28 | 29 | ### Compile from source 30 | 31 | ```text 32 | $ go install github.com/jandelgado/gcov2lcov@latest 33 | ``` 34 | 35 | ## Usage 36 | 37 | ```text 38 | Usage of ./gcov2lcov: 39 | -infile string 40 | go coverage file to read, default: 41 | -outfile string 42 | lcov file to write, default: 43 | -use-absolute-source-path 44 | use absolute paths for source file in lcov output, default: false 45 | ``` 46 | 47 | ### Example 48 | 49 | ```sh 50 | $ go test -coverprofile=coverage.out && \ 51 | gcov2lcov -infile=coverage.out -outfile=coverage.lcov 52 | ``` 53 | 54 | ### GOROOT 55 | 56 | It might be necessary to set the `GOROOT` environment variable properly before 57 | calling `gcov2lcov`. If you see `cannot find GOROOT directory` warnings like 58 | e.g. 59 | 60 | ```text 61 | 022/05/23 16:00:58 warn: go/build: importGo github.com/pashagolub/pgxmock/: exit status 2 62 | go: cannot find GOROOT directory: /opt/hostedtoolcache/go/1.13.15/x64 63 | ``` 64 | 65 | Then call `gcov2lcov` with 66 | 67 | ```text 68 | $ GOROOT=$(go env GOROOT) gcov2lcov -infile=coverage.out -outfile=coverage.lcov 69 | ``` 70 | 71 | ## Build and Test 72 | 73 | * `make test` to run tests 74 | * `make build` to build binary in `bin/` directory 75 | 76 | ## Tracefile format reference 77 | 78 | The following desription is taken from the [geninfo 79 | manpage](http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php) of the [lcov 80 | homepage](http://ltp.sourceforge.net/coverage/lcov/): 81 | 82 | ```text 83 | A tracefile is made up of several human-readable lines of text, divided into 84 | sections. If available, a tracefile begins with the testname which is stored in 85 | the following format: 86 | 87 | TN: 88 | 89 | For each source file referenced in the .da file, there is a section containing 90 | filename and coverage data: 91 | 92 | SF: 93 | 94 | Following is a list of line numbers for each function name found in the source file: 95 | 96 | FN:, 97 | 98 | Next, there is a list of execution counts for each instrumented function: 99 | 100 | FNDA:, 101 | 102 | This list is followed by two lines containing the number of functions found and hit: 103 | 104 | FNF: FNH: 105 | 106 | Branch coverage information is stored which one line per branch: 107 | 108 | BRDA:,,, 109 | 110 | Block number and branch number are gcc internal IDs for the branch. Taken is 111 | either '-' if the basic block containing the branch was never executed or a 112 | number indicating how often that branch was taken. 113 | 114 | Branch coverage summaries are stored in two lines: 115 | 116 | BRF: BRH: 117 | 118 | Then there is a list of execution counts for each instrumented line (i.e. a 119 | line which resulted in executable code): 120 | 121 | DA:,[,] 122 | 123 | Note that there may be an optional checksum present for each instrumented line. 124 | The current geninfo implementation uses an MD5 hash as checksumming algorithm. 125 | 126 | At the end of a section, there is a summary about how many lines were found and 127 | how many were actually instrumented: 128 | 129 | LH: LF: 130 | 131 | Each sections ends with: 132 | 133 | end_of_record 134 | 135 | In addition to the main source code file there are sections for all #included 136 | files which also contain executable code. 137 | 138 | Note that the absolute path of a source file is generated by interpreting the 139 | contents of the respective .bb file (see gcov (1) for more information on this 140 | file type). Relative filenames are prefixed with the directory in which the .bb 141 | file is found. 142 | 143 | Note also that symbolic links to the .bb file will be resolved so that the 144 | actual file path is used instead of the path to a link. This approach is 145 | necessary for the mechanism to work with the /proc/gcov files. 146 | 147 | ``` 148 | 149 | ## Author 150 | 151 | Jan Delgado 152 | 153 | ## License 154 | 155 | MIT 156 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jandelgado/gcov2lcov 2 | 3 | go 1.15 4 | 5 | require github.com/stretchr/testify v1.10.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 9 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 10 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 12 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 13 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 14 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 19 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // gcov2lcov - convert golang coverage files to the lcov format. 2 | // 3 | // Copyright (c) 2019 Jan Delgado 4 | // Copyright (c) 2019 Richard S Allinson 5 | // 6 | // Credits: 7 | // This tool is based on covfmt (https://github.com/ricallinson/covfmt) and 8 | // uses some parts of goveralls (https://github.com/mattn/goveralls). 9 | package main 10 | 11 | import ( 12 | "bufio" 13 | "errors" 14 | "flag" 15 | "go/build" 16 | "io" 17 | "log" 18 | "os" 19 | "path/filepath" 20 | "sort" 21 | "strconv" 22 | "strings" 23 | ) 24 | 25 | type block struct { 26 | startLine int 27 | startChar int 28 | endLine int 29 | endChar int 30 | statements int 31 | covered int 32 | } 33 | 34 | var vscDirs = []string{".git", ".hg", ".bzr", ".svn"} 35 | 36 | type cacheEntry struct { 37 | dir string 38 | err error 39 | } 40 | 41 | var pkgCache = map[string]cacheEntry{} 42 | 43 | // given a module+file spec (e.g. github.com/jandelgado/gcov2lcov/main.go), 44 | // strip of the module name and return the file name (e.g. main.go). 45 | func findFile(filePath string) (string, error) { 46 | dir, file := filepath.Split(filePath) 47 | var result cacheEntry 48 | var ok bool 49 | if result, ok = pkgCache[dir]; !ok { 50 | pkg, err := build.Import(dir, ".", build.FindOnly) 51 | if err == nil { 52 | result = cacheEntry{pkg.Dir, nil} 53 | } else { 54 | result = cacheEntry{"", err} 55 | } 56 | pkgCache[dir] = result 57 | } 58 | return filepath.Join(result.dir, file), result.err 59 | } 60 | 61 | // findRepositoryRoot finds the VCS root dir of a given dir 62 | func findRepositoryRoot(dir string) (string, bool) { 63 | for _, vcsdir := range vscDirs { 64 | if d, err := os.Stat(filepath.Join(dir, vcsdir)); err == nil && d.IsDir() { 65 | return dir, true 66 | } 67 | } 68 | nextdir := filepath.Dir(dir) 69 | if nextdir == dir { 70 | return "", false 71 | } 72 | return findRepositoryRoot(nextdir) 73 | } 74 | 75 | func getSourceFileName(name string) string { 76 | return name 77 | } 78 | 79 | func getCoverallsSourceFileName(name string) string { 80 | if dir, ok := findRepositoryRoot(name); ok { 81 | filename := strings.TrimPrefix(name, dir+string(os.PathSeparator)) 82 | return filename 83 | } 84 | return name 85 | } 86 | 87 | func keysOfMap(m map[int]int) []int { 88 | keys := make([]int, len(m)) 89 | i := 0 90 | for k := range m { 91 | keys[i] = k 92 | i++ 93 | } 94 | return keys 95 | } 96 | 97 | func writeLcovRecord(filePath string, blocks []*block, w io.StringWriter) error { 98 | 99 | writer := func(err error, s string) error { 100 | if err != nil { 101 | return err 102 | } 103 | _, err = w.WriteString(s) 104 | return err 105 | } 106 | var err error 107 | err = writer(err, "TN:\nSF:"+filePath+"\n") 108 | 109 | // Loop over functions 110 | // FN: line,name 111 | 112 | // FNF: total functions 113 | // FNH: covered functions 114 | 115 | // Loop over functions 116 | // FNDA: stats,name ? 117 | 118 | total := 0 119 | covered := 0 120 | 121 | // maps line number to sum of covered 122 | coverMap := map[int]int{} 123 | 124 | // Loop over each block and extract the lcov data needed. 125 | for _, b := range blocks { 126 | // For each line in a block we add an lcov entry and count the lines. 127 | for i := b.startLine; i <= b.endLine; i++ { 128 | coverMap[i] += b.covered 129 | } 130 | } 131 | 132 | lines := keysOfMap(coverMap) 133 | sort.Ints(lines) 134 | for _, line := range lines { 135 | err = writer(err, "DA:"+strconv.Itoa(line)+","+strconv.Itoa(coverMap[line])+"\n") 136 | total++ 137 | if coverMap[line] > 0 { 138 | covered++ 139 | } 140 | } 141 | 142 | // LH: 143 | // LF: 144 | err = writer(err, "LF:"+strconv.Itoa(total)+"\n") 145 | err = writer(err, "LH:"+strconv.Itoa(covered)+"\n") 146 | 147 | // Loop over branches 148 | // BRDA: ? 149 | 150 | // BRF: total branches 151 | // BRH: covered branches 152 | 153 | return writer(err, "end_of_record\n") 154 | } 155 | 156 | func writeLcov(blocks map[string][]*block, f io.Writer) error { 157 | w := bufio.NewWriter(f) 158 | for file, fileBlocks := range blocks { 159 | if err := writeLcovRecord(file, fileBlocks, w); err != nil { 160 | return err 161 | } 162 | } 163 | w.Flush() 164 | return nil 165 | } 166 | 167 | // Format being parsed is: 168 | // 169 | // name.go:line.column,line.column numberOfStatements count 170 | // 171 | // e.g. 172 | // 173 | // github.com/jandelgado/golang-ci-template/main.go:6.14,8.2 1 1 174 | func parseCoverageLine(line string) (string, *block, error) { 175 | path := strings.Split(line, ":") 176 | if len(path) != 2 { 177 | return "", nil, errors.New("unexpected format (path sep): " + line) 178 | } 179 | parts := strings.Split(path[1], " ") 180 | if len(parts) != 3 { 181 | return "", nil, errors.New("unexpected format (parts): " + line) 182 | } 183 | sections := strings.Split(parts[0], ",") 184 | if len(sections) != 2 { 185 | return "", nil, errors.New("unexpected format (pos): " + line) 186 | } 187 | start := strings.Split(sections[0], ".") 188 | end := strings.Split(sections[1], ".") 189 | 190 | safeAtoi := func(err error, s string) (int, error) { 191 | if err != nil { 192 | return 0, err 193 | } 194 | return strconv.Atoi(s) 195 | } 196 | b := &block{} 197 | var err error 198 | b.startLine, err = safeAtoi(nil, start[0]) 199 | b.startChar, err = safeAtoi(err, start[1]) 200 | b.endLine, err = safeAtoi(err, end[0]) 201 | b.endChar, err = safeAtoi(err, end[1]) 202 | b.statements, err = safeAtoi(err, parts[1]) 203 | b.covered, err = safeAtoi(err, parts[2]) 204 | 205 | return path[0], b, err 206 | } 207 | 208 | func parseCoverage(coverage io.Reader, pathResolverFunc func(string) string) (map[string][]*block, error) { 209 | scanner := bufio.NewScanner(coverage) 210 | blocks := map[string][]*block{} 211 | for scanner.Scan() { 212 | line := scanner.Text() 213 | if strings.HasPrefix(line, "mode:") { 214 | continue 215 | } 216 | if f, b, err := parseCoverageLine(line); err == nil { 217 | f, err := findFile(f) 218 | if err != nil { 219 | log.Printf("warn: %v", err) 220 | continue 221 | } 222 | 223 | f = pathResolverFunc(f) 224 | 225 | // Make sure the filePath is a key in the map. 226 | if _, found := blocks[f]; !found { 227 | blocks[f] = []*block{} 228 | } 229 | blocks[f] = append(blocks[f], b) 230 | } else { 231 | log.Printf("warn: %v", err) 232 | } 233 | 234 | } 235 | if err := scanner.Err(); err != nil { 236 | return nil, err 237 | } 238 | return blocks, nil 239 | } 240 | 241 | func convertCoverage(in io.Reader, out io.Writer, pathResolverFunc func(string) string) error { 242 | blocks, err := parseCoverage(in, pathResolverFunc) 243 | if err != nil { 244 | return err 245 | } 246 | return writeLcov(blocks, out) 247 | } 248 | 249 | func main() { 250 | os.Exit(gcovmain()) 251 | } 252 | 253 | func gcovmain() int { 254 | infileName := flag.String("infile", "", "go coverage file to read, default: ") 255 | outfileName := flag.String("outfile", "", "lcov file to write, default: ") 256 | useAbsoluteSourcePath := flag.Bool("use-absolute-source-path", false, 257 | "use absolute paths for source file in lcov output, default: false") 258 | flag.Parse() 259 | if len(flag.Args()) > 0 { 260 | flag.Usage() 261 | return 1 262 | } 263 | 264 | infile := os.Stdin 265 | outfile := os.Stdout 266 | var err error 267 | if *infileName != "" { 268 | infile, err = os.Open(*infileName) 269 | if err != nil { 270 | log.Printf("error opening input file: %v\n", err) 271 | return 2 272 | } 273 | defer infile.Close() 274 | } 275 | if *outfileName != "" { 276 | outfile, err = os.Create(*outfileName) 277 | if err != nil { 278 | log.Printf("error opening output file: %v\n", err) 279 | return 3 280 | } 281 | defer outfile.Close() 282 | } 283 | 284 | var pathResolverFunc func(string) string 285 | if *useAbsoluteSourcePath { 286 | pathResolverFunc = getSourceFileName 287 | } else { 288 | pathResolverFunc = getCoverallsSourceFileName 289 | } 290 | 291 | err = convertCoverage(infile, outfile, pathResolverFunc) 292 | if err != nil { 293 | log.Printf("error: convert: %v", err) 294 | return 4 295 | } 296 | return 0 297 | } 298 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | // gcov2lcov - convert golang coverage files to the lcov format. 2 | // (c) 2019 Jan Delgado 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "os" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestKeysOfMapReturnsAllKeysOfMap(t *testing.T) { 15 | m := map[int]int{1: 10, 10: 100} 16 | 17 | keys := keysOfMap(m) 18 | assert.Contains(t, keys, 1) 19 | assert.Contains(t, keys, 10) 20 | assert.Equal(t, 2, len(keys)) 21 | } 22 | 23 | func TestParseCoverageLineFailsOnInvalidLines(t *testing.T) { 24 | _, _, err := parseCoverageLine("main.go") 25 | assert.NotNil(t, err) 26 | 27 | _, _, err = parseCoverageLine("main.go:A B") 28 | assert.NotNil(t, err) 29 | 30 | _, _, err = parseCoverageLine("main.go:A B C") 31 | assert.NotNil(t, err) 32 | 33 | _, _, err = parseCoverageLine("main.go:6.14,8.3 X 1") 34 | assert.NotNil(t, err) 35 | } 36 | 37 | func TestParseCoverageLineOfParsesValidLineCorrectly(t *testing.T) { 38 | line := "github.com/jandelgado/gcov2lcov/main.go:6.14,8.3 2 1" 39 | file, b, err := parseCoverageLine(line) 40 | 41 | assert.Nil(t, err) 42 | assert.Equal(t, "github.com/jandelgado/gcov2lcov/main.go", file) 43 | assert.Equal(t, 6, b.startLine) 44 | assert.Equal(t, 14, b.startChar) 45 | assert.Equal(t, 8, b.endLine) 46 | assert.Equal(t, 3, b.endChar) 47 | assert.Equal(t, 2, b.statements) 48 | assert.Equal(t, 1, b.covered) 49 | } 50 | 51 | func TestParseCoverage(t *testing.T) { 52 | 53 | // note: in this integrative test, the package path must match the actual 54 | // repository name of this project. 55 | cov := `mode: set 56 | github.com/jandelgado/gcov2lcov/main.go:6.14,8.3 2 1` 57 | 58 | reader := strings.NewReader(cov) 59 | res, err := parseCoverage(reader, getCoverallsSourceFileName) 60 | 61 | assert.NoError(t, err) 62 | assert.Equal(t, 1, len(res)) 63 | for k, blks := range res { 64 | assert.Equal(t, 1, len(blks)) 65 | b := blks[0] 66 | assert.Equal(t, "main.go", k) 67 | assert.Equal(t, 6, b.startLine) 68 | assert.Equal(t, 14, b.startChar) 69 | assert.Equal(t, 8, b.endLine) 70 | assert.Equal(t, 3, b.endChar) 71 | assert.Equal(t, 2, b.statements) 72 | assert.Equal(t, 1, b.covered) 73 | } 74 | } 75 | 76 | func TestConvertCoverage(t *testing.T) { 77 | // note: in this integrative test, the package path must match the actual 78 | // repository name of this project. Format: 79 | // name.go:line.column,line.column numberOfStatements count 80 | cov := `mode: set 81 | github.com/jandelgado/gcov2lcov/main.go:6.14,8.3 2 1 82 | github.com/jandelgado/gcov2lcov/main.go:7.14,9.3 2 0 83 | github.com/jandelgado/gcov2lcov/main.go:10.1,11.10 2 2` 84 | 85 | in := strings.NewReader(cov) 86 | out := bytes.NewBufferString("") 87 | err := convertCoverage(in, out, getCoverallsSourceFileName) 88 | 89 | expected := `TN: 90 | SF:main.go 91 | DA:6,1 92 | DA:7,1 93 | DA:8,1 94 | DA:9,0 95 | DA:10,2 96 | DA:11,2 97 | LF:6 98 | LH:5 99 | end_of_record 100 | ` 101 | assert.NoError(t, err) 102 | assert.Equal(t, expected, out.String()) 103 | } 104 | 105 | func TestPathResolverFunc(t *testing.T) { 106 | pwd, err := os.Getwd() 107 | assert.NoError(t, err) 108 | 109 | name := getCoverallsSourceFileName(pwd + "/main.go") 110 | assert.Equal(t, "main.go", name) 111 | 112 | name = getSourceFileName(pwd + "/main.go") 113 | assert.Equal(t, pwd+"/main.go", name) 114 | } 115 | -------------------------------------------------------------------------------- /pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | golangci-lint -E bodyclose,misspell,gocyclo,dupl,gofmt,unconvert,goimports,gocritic,funlen,errcheck,gosimple,govet,ineffassign,staticcheck,typecheck,unused run 3 | -------------------------------------------------------------------------------- /testdata/coverage.out: -------------------------------------------------------------------------------- 1 | mode: set 2 | github.com/jandelgado/gcov2lcov/main.go:45.44,49.38 4 1 3 | github.com/jandelgado/gcov2lcov/main.go:46.44,50.38 4 0 4 | github.com/jandelgado/gcov2lcov/main.go:58.2,58.32 1 1 5 | -------------------------------------------------------------------------------- /testdata/coverage_expected.lcov: -------------------------------------------------------------------------------- 1 | TN: 2 | SF:main.go 3 | DA:45,1 4 | DA:46,1 5 | DA:47,1 6 | DA:48,1 7 | DA:49,1 8 | DA:50,0 9 | DA:58,1 10 | LF:7 11 | LH:6 12 | end_of_record 13 | --------------------------------------------------------------------------------