├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cibuild.sh ├── coverage.nim ├── coverage.nimble ├── coverageTemplate.html ├── nimcoverage.nim └── tests ├── nakefile.nim └── test.nim /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | before_install: 5 | - docker pull yglukhov/nim-base 6 | script: 7 | - docker run -v "$(pwd):/project" -w /project yglukhov/nim-base run sh ./cibuild.sh 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yuriy Glukhov 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coverage [](https://travis-ci.org/yglukhov/coverage) [](https://coveralls.io/github/yglukhov/coverage?branch=master) 2 | Code coverage library for Nim. Inspired by [Andreas Rumpf talk at OSCON](https://github.com/Araq/oscon2015). 3 | 4 | ## Usage 5 | ```nim 6 | import coverage 7 | import tables 8 | 9 | proc myProcToCover(x: int) {.cov.} = # Add cov pragma to proc definition to enable code coverage. 10 | if x == 0: 11 | echo "x is 0" 12 | else: 13 | echo "x is ", x 14 | 15 | # Run your program or unittest 16 | myProcToCover(1) 17 | 18 | # At the end of the program, display coverage results: 19 | echo "BY FILE: " 20 | for fname, perc in coveragePercentageByFile(): 21 | echo fname, " ", perc 22 | # Outputs: BY FILE: {test.nim: 0.5} 23 | 24 | echo "TOTAL: ", totalCoverage() 25 | # Outputs: TOTAL: 0.5 26 | 27 | # Finer grained information may be accessed with coverageInfoByFile proc. 28 | ``` 29 | 30 | ### Adding coverage to a unittest file 31 | ```nim 32 | import coverage, tables, unittest 33 | 34 | # Import your code and run the tests as usual 35 | # suite "test": ... 36 | 37 | echo "Coverage by file: " 38 | for fname, num in coveragePercentageByFile().pairs(): 39 | echo fname, " ", num 40 | 41 | echo "Total coverage: ", totalCoverage() 42 | ``` 43 | 44 | Add "import coverage" and the top of your sources and add "{.cov.}" to every proc. 45 | 46 | 47 | ### Generating a report 48 | ```bash 49 | export NIM_COVERAGE_DIR=coverage_results 50 | mkdir -p "$NIM_COVERAGE_DIR" 51 | 52 | nim c -r your_tests.nim 53 | nimcoverage genreport 54 | ``` 55 | 56 | ### Notes 57 | - Code coverage is disabled if ```release``` is defined. Define ```enableCodeCoverage``` option to keep it enabled in release mode. 58 | 59 | # Travic CI + Coveralls integration 60 | If you're using [Travis CI](https://travis-ci.org) and [Coveralls](https://coveralls.io), you can upload coverage results right at the end of your program: 61 | ```nim 62 | sendCoverageResultsToCoveralls() 63 | ``` 64 | -------------------------------------------------------------------------------- /cibuild.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | nimble install -y 3 | cd tests 4 | nake 5 | cd - 6 | -------------------------------------------------------------------------------- /coverage.nim: -------------------------------------------------------------------------------- 1 | import macros, tables, strutils, os, sequtils, algorithm 2 | 3 | proc fileName(n: NimNode): string = 4 | let ln = n.lineInfo 5 | let i = ln.rfind('(') 6 | result = ln.substr(0, i - 1) 7 | 8 | proc lineNumber(n: NimNode): int = 9 | let ln = n.lineInfo 10 | let i = ln.rfind('(') 11 | let j = ln.rfind(',') 12 | result = parseInt(ln.substr(i + 1, j - 1)) 13 | 14 | type CovData* = tuple[lineNo: int, passes: int] 15 | type CovChunk* = seq[CovData] 16 | var coverageResults = initTable[string, seq[ptr CovChunk]]() 17 | 18 | proc registerCovChunk(fileName: string, chunk: var CovChunk) = 19 | if coverageResults.getOrDefault(fileName).len == 0: 20 | coverageResults[fileName] = @[addr chunk] 21 | else: 22 | coverageResults[fileName].add(addr chunk) 23 | 24 | proc transform(n, track, list: NimNode): NimNode {.compileTime.} = 25 | result = copyNimNode(n) 26 | for c in n.children: 27 | result.add c.transform(track, list) 28 | 29 | if n.kind in {nnkElifBranch, nnkOfBranch, nnkExceptBranch, nnkElse}: 30 | let lineno = result[^1].lineNumber 31 | 32 | template trackStmt(track, i) = 33 | inc track[i].passes 34 | 35 | result[^1] = newStmtList(getAst trackStmt(track, list.len), result[^1]) 36 | template tup(lineno) = 37 | (lineno, 0) 38 | list.add(getAst tup(lineno)) 39 | 40 | macro cov*(body: untyped): untyped = 41 | when defined(release) and not defined(enableCodeCoverage): 42 | result = body 43 | else: 44 | let file = body.fileName 45 | var trackSym = genSym(nskVar, "track") 46 | var trackList = newNimNode(nnkBracket) 47 | var listVar = newStmtList( 48 | newNimNode(nnkVarSection).add( 49 | newNimNode(nnkIdentDefs).add(trackSym, newNimNode(nnkBracketExpr).add(newIdentNode("seq"), newIdentNode("CovData")), prefix(trackList, "@"))), 50 | newCall(bindSym "registerCovChunk", newStrLitNode(file), trackSym) 51 | ) 52 | 53 | result = transform(body, trackSym, trackList) 54 | result = newStmtList(listVar, result) 55 | 56 | template derefChunk(dest: var CovChunk, src: ptr CovChunk) = 57 | shallowCopy(dest, src[]) 58 | 59 | proc coveredLinesInFile*(fileName: string): seq[CovData] = 60 | result = newSeq[CovData]() 61 | var tmp : seq[ptr CovChunk] 62 | shallowCopy(tmp, coverageResults[fileName]) 63 | for i in 0 ..< tmp.len: 64 | var covChunk : CovChunk 65 | derefChunk(covChunk, tmp[i]) 66 | result = result.concat(covChunk) 67 | result.sort(proc (a, b: CovData): int = cmp(a.lineNo, b.lineNo)) 68 | 69 | var newRes = newSeq[CovData](result.len) 70 | # Deduplicate lines 71 | var j = 0 72 | var lastLine = 0 73 | for i in 0 ..< result.len: 74 | if result[i].lineNo == lastLine: 75 | if result[i].passes == 0: 76 | newRes[j - 1].passes = 0 77 | else: 78 | lastLine = result[i].lineNo 79 | newRes[j] = result[i] 80 | inc j 81 | newRes.setLen(j) 82 | shallowCopy(result, newRes) 83 | 84 | proc coverageInfoByFile*(): Table[string, tuple[linesTracked, linesCovered: int]] = 85 | result = initTable[string, tuple[linesTracked, linesCovered: int]]() 86 | for k, v in coverageResults: 87 | var linesTracked = 0 88 | var linesCovered = 0 89 | for i in 0 ..< v.len: 90 | var covChunk : CovChunk 91 | derefChunk(covChunk, v[i]) 92 | for data in covChunk: 93 | inc linesTracked 94 | if data.passes != 0: inc linesCovered 95 | result[k] = (linesTracked, linesCovered) 96 | 97 | proc coveragePercentageByFile*(): Table[string, float] = 98 | result = initTable[string, float]() 99 | for k, v in coverageInfoByFile(): 100 | result[k] = v.linesCovered.float / v.linesTracked.float 101 | 102 | proc totalCoverage*(): float = 103 | var linesTracked = 0 104 | var linesCovered = 0 105 | for k, v in coverageResults: 106 | for i in 0 ..< v.len: 107 | var covChunk : CovChunk 108 | derefChunk(covChunk, v[i]) 109 | for data in covChunk: 110 | inc linesTracked 111 | if data.passes != 0: inc linesCovered 112 | result = linesCovered.float / linesTracked.float 113 | 114 | when not defined(js) and not defined(emscripten): 115 | import os, osproc 116 | import json 117 | import httpclient 118 | import md5 119 | 120 | proc initCoverageDir*(path: string = ".") = 121 | putEnv("NIM_COVERAGE_DIR", path) 122 | 123 | proc saveCovResults() {.noconv.} = 124 | let jCov = newJObject() 125 | for k, v in coverageResults: 126 | let jChunks = newJArray() 127 | for chunk in v: 128 | let jChunk = newJArray() 129 | for ln in chunk[]: 130 | let jLn = newJArray() 131 | jLn.add(newJInt(ln.lineNo)) 132 | jLn.add(newJInt(ln.passes)) 133 | jChunk.add(jLn) 134 | jChunks.add(jChunk) 135 | jCov[k] = jChunks 136 | var i = 0 137 | while true: 138 | let covFile = getEnv("NIM_COVERAGE_DIR") / "cov" & $i & ".json" 139 | if not fileExists(covFile): 140 | writeFile(covFile, $jCov) 141 | break 142 | inc i 143 | 144 | proc getFileSourceCode(p: string): string = 145 | readFile(p) 146 | 147 | proc expandCovSeqIfNeeded(s: var seq[int], toLen: int) = 148 | if s.len <= toLen: 149 | let oldLen = s.len 150 | s.setLen(toLen + 1) 151 | for i in oldLen .. toLen: 152 | s[i] = -1 153 | 154 | proc createCoverageReport*() = 155 | let covDir = getEnv("NIM_COVERAGE_DIR") 156 | var i = 0 157 | 158 | var covData = initTable[string, seq[int]]() 159 | 160 | while true: 161 | let covFile = getEnv("NIM_COVERAGE_DIR") / "cov" & $i & ".json" 162 | if not fileExists(covFile): 163 | break 164 | inc i 165 | let jf = parseJson(readFile(covFile)) 166 | for fileName, chunks in jf: 167 | if fileName notin covData: 168 | covData[fileName] = @[] 169 | 170 | for chunk in chunks: 171 | for ln in chunk: 172 | let lineNo = int(ln[0].num) 173 | let passes = int(ln[1].num) 174 | expandCovSeqIfNeeded(covData[fileName], lineNo) 175 | if covData[fileName][lineNo] == -1: 176 | covData[fileName][lineNo] = 0 177 | covData[fileName][lineNo] += passes 178 | removeFile(covFile) 179 | 180 | let jCovData = newJObject() 181 | for k, v in covData: 182 | let arr = newJArray() 183 | for i in v: arr.add(%i) 184 | let d = newJObject() 185 | d["l"] = arr 186 | d["s"] = % getFileSourceCode(k) 187 | jCovData[k] = d 188 | 189 | const htmlTemplate = staticRead("coverageTemplate.html") 190 | writeFile(getEnv("NIM_COVERAGE_DIR") / "cov.html", htmlTemplate.replace("$COV_DATA", $jCovData)) 191 | 192 | if getEnv("NIM_COVERAGE_DIR").len > 0: 193 | addQuitProc(saveCovResults) 194 | 195 | proc md5OfFile(path: string): string = $getMD5(readFile(path)) 196 | 197 | proc sendCoverageResultsToCoveralls*() = 198 | var request = newJObject() 199 | if existsEnv("TRAVIS_JOB_ID"): 200 | request["service_name"] = newJString("travis-ci") 201 | request["service_job_id"] = newJString(getEnv("TRAVIS_JOB_ID")) 202 | 203 | # Assume we're in git repo. Paths to sources should be relative to 204 | # repo root 205 | let gitRes = execCmdEx("git rev-parse --show-toplevel") 206 | if gitRes.exitCode != 0: 207 | raise newException(Exception, "GIT Error") 208 | 209 | let curDir = getCurrentDir() 210 | 211 | # TODO: The following is too naive! 212 | let relativePath = curDir.substr(gitRes.output.len) 213 | 214 | var files = newJArray() 215 | for k, v in coverageResults: 216 | let lines = coveredLinesInFile(k) 217 | var jLines = newJArray() 218 | var curLine = 1 219 | for data in lines: 220 | while data.lineNo > curLine: 221 | jLines.add(newJNull()) 222 | inc curLine 223 | jLines.add(newJInt(data.passes)) 224 | inc curLine 225 | var jFile = newJObject() 226 | jFile["name"] = newJString(relativePath / k) 227 | jFile["coverage"] = jLines 228 | jFile["source_digest"] = newJString(md5OfFile(k)) 229 | #jFile["source"] = newJString(readFile(k)) 230 | files.add(jFile) 231 | request["source_files"] = files 232 | var data = newMultipartData() 233 | echo "COVERALLS REQUEST: ", $request 234 | data["json_file"] = ("file.json", "application/json", $request) 235 | echo "COVERALLS RESPONSE: ", newHttpClient().postContent("https://coveralls.io/api/v1/jobs", multipart=data) 236 | -------------------------------------------------------------------------------- /coverage.nimble: -------------------------------------------------------------------------------- 1 | version = "0.1.0" 2 | author = "Yuriy Glukhov" 3 | description = "Code coverage library for Nim" 4 | license = "MIT" 5 | bin = @["nimcoverage"] 6 | 7 | installFiles = @["coverageTemplate.html", "coverage.nim"] 8 | 9 | # Deps 10 | requires "nim >= 0.10.0" 11 | requires "nake" 12 | -------------------------------------------------------------------------------- /coverageTemplate.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 6 | 20 | 21 | 22 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /nimcoverage.nim: -------------------------------------------------------------------------------- 1 | # This is the main module for command-line cnimcoverage tool. This module 2 | # should not be imported. 3 | 4 | import coverage 5 | import os 6 | 7 | if getEnv("NIM_COVERAGE_DIR").len == 0: 8 | echo "NIM_COVERAGE_DIR environment variable not set" 9 | quit 1 10 | 11 | createCoverageReport() 12 | -------------------------------------------------------------------------------- /tests/nakefile.nim: -------------------------------------------------------------------------------- 1 | import nake 2 | import coverage 3 | 4 | task defaultTask, "Build and run": 5 | initCoverageDir() 6 | for nimFile in walkFiles "*.nim": 7 | if nimFile != "nakefile.nim": 8 | echo "Running: ", nimFile 9 | direShell nimExe, "c", "--run", "-d:ssl", nimFile 10 | direShell nimExe, "js", "--run", "-d:nodejs", nimFile 11 | createCoverageReport() 12 | -------------------------------------------------------------------------------- /tests/test.nim: -------------------------------------------------------------------------------- 1 | import "../coverage" 2 | 3 | proc test1(x: int) {.cov.} = 4 | if x == 0: 5 | echo "x is 0" 6 | else: 7 | echo "x is ", x 8 | 9 | test1(0) 10 | 11 | doAssert(totalCoverage() == 0.5) 12 | 13 | test1(1) 14 | 15 | doAssert(totalCoverage() == 1.0) 16 | 17 | proc toTest(x, y: int) {.cov.} = 18 | if x == 8: 19 | if y == 8: 20 | discard "This line should be covered" 21 | else: 22 | discard "This line should not be covered" 23 | else: 24 | discard "This line should be covered" 25 | 26 | if y == 5: 27 | discard "This line should be covered" 28 | 29 | if y == 6: 30 | discard "This line should not be covered" 31 | else: 32 | discard "This line should be covered" 33 | 34 | toTest(8, 8) 35 | toTest(5, 5) 36 | 37 | when defined(js): 38 | import tables 39 | 40 | # Get current working directory from nodejs 41 | proc cwd(): string {.importc: "process.cwd", nodecl.} 42 | 43 | # The string returned by cwd is dirty, clean it 44 | proc convert(s: string): string = 45 | for c in s: 46 | result &= $ord(c) 47 | 48 | echo coverageInfoByFile() 49 | echo coveragePercentageByFile() 50 | echo coveredLinesInFile(convert(cwd()) & "/test.nim") 51 | else: 52 | sendCoverageResultsToCoveralls() 53 | --------------------------------------------------------------------------------