├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── keymaps └── atom-lcov.json ├── lib ├── atom-lcov.js └── parseLcov.js ├── menus └── atom-lcov.json ├── package.json ├── spec └── parseLcov-spec.js └── styles └── atom-lcov.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | email: 3 | on_success: never 4 | on_failure: change 5 | 6 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh' 7 | 8 | git: 9 | depth: 10 10 | 11 | sudo: false 12 | 13 | os: 14 | - linux 15 | - osx 16 | 17 | env: 18 | global: 19 | - APM_TEST_PACKAGES="" 20 | 21 | matrix: 22 | - ATOM_CHANNEL=stable 23 | - ATOM_CHANNEL=beta 24 | 25 | addons: 26 | apt: 27 | packages: 28 | - build-essential 29 | - git 30 | - libgnome-keyring-dev 31 | - fakeroot 32 | 33 | branches: 34 | only: 35 | - master -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.0 - First Release 2 | * Every feature added 3 | * Every bug fixed 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atom-lcov [![Build Status](https://travis-ci.org/taskworld/atom-lcov.svg?branch=master)](https://travis-ci.org/taskworld/atom-lcov) 2 | 3 | This Atom package shows your code’s test coverage in the gutter. 4 | It guides you towards more code coverage! 5 | Inspired by [Wallaby.js](https://wallabyjs.com/). 6 | 7 | ![Screenshot](http://i.imgur.com/0s9XvbR.png) 8 | 9 | Tested with: 10 | 11 | - [Mocha](https://mochajs.org/) + [nyc](https://github.com/bcoe/nyc) 12 | - [Karma](https://karma-runner.github.io/0.13/index.html) + [babel-plugin-\_\_coverage\_\_](https://github.com/dtinth/babel-plugin-__coverage__) 13 | 14 | 15 | ## Usage 16 | 17 | ### 1. Write your code and your tests 18 | 19 | ![Write your code and your tests](http://i.imgur.com/Wa8SsJY.png) 20 | 21 | ### 2. Set up your test tool to output lcov data 22 | 23 | In this example, I use [nyc](https://github.com/bcoe/nyc) to capture the code coverage while running [Mocha](https://mochajs.org/) test. 24 | 25 | ![Set up your test tool to output lcov data](http://i.imgur.com/8v3TmsY.png) 26 | 27 | ### 3. Run your test once 28 | 29 | Make sure the coverage data file is generated. It’s usually named `lcov.info`. 30 | 31 | ![Run your test](http://i.imgur.com/VXJUsYC.png) 32 | 33 | ### 4. Right click on your lcov file and select “watch for coverage” 34 | 35 | ![Watch for coverage](http://i.imgur.com/p0fhJi1.png) 36 | 37 | ### 5. Atom will begin watching the lcov file and show it in the gutter 38 | 39 | ![Gutter](http://i.imgur.com/n7kny1x.png) 40 | 41 | ### 6. Run your tests in watch mode 42 | 43 | Here I use [onchange](https://www.npmjs.com/package/onchange) to monitor the `.js` files and re-run the tests. 44 | 45 | ![Watch mode](http://i.imgur.com/3IM1Kh5.png) 46 | 47 | ### 7. Add more tests and see the gutter turn green!!!! 48 | 49 | ![It turns green](http://i.imgur.com/aMBSxHf.png) 50 | -------------------------------------------------------------------------------- /keymaps/atom-lcov.json: -------------------------------------------------------------------------------- 1 | { 2 | "atom-workspace": { 3 | "ctrl-alt-o": "atom-lcov:toggle" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/atom-lcov.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | /* global atom */ 3 | import $ from 'jquery' 4 | import fs from 'fs' 5 | import Rx from 'rx' 6 | import { CompositeDisposable } from 'atom' 7 | import parseLcov from './parseLcov' 8 | 9 | function lcovData川FromLcovFile川 (lcovFile川) { 10 | return (lcovFile川 11 | .flatMapLatest((path) => Rx.Observable.create((observer) => { 12 | refresh() 13 | fs.watchFile(path, { interval: 1024 }, refresh) 14 | return close 15 | function refresh () { 16 | fs.readFile(path, 'utf8', (err, data) => { 17 | if (err) { 18 | observer.onNext('') 19 | throw err 20 | } else { 21 | observer.onNext(data) 22 | return data 23 | } 24 | }) 25 | } 26 | function close () { 27 | fs.unwatchFile(path, refresh) 28 | } 29 | })) 30 | ) 31 | } 32 | 33 | function getClass (counts) { 34 | const l = counts.length 35 | let covered = 0 36 | for (let i = 0; i < l; i++) { 37 | if (counts[i] > 0) covered++ 38 | } 39 | if (covered === l) return 'lcov-covered' 40 | if (covered) return 'lcov-partially-covered' 41 | return 'lcov-missed' 42 | } 43 | 44 | function MarkerManager (textEditor) { 45 | const markers = { } 46 | function handleFileCoverage (fileCoverage) { 47 | for (const key of Object.keys(markers)) { 48 | markers[key].destroy() 49 | delete markers[key] 50 | } 51 | for (const lineNumber of Object.keys(fileCoverage)) { 52 | markers[lineNumber] = textEditor.markBufferPosition([ lineNumber - 1, 0 ]) 53 | textEditor.decorateMarker(markers[lineNumber], { 54 | type: 'line-number', 55 | class: getClass(fileCoverage[lineNumber]) 56 | }) 57 | } 58 | } 59 | return { handleFileCoverage } 60 | } 61 | 62 | export default { 63 | activate (state) { 64 | this.lcovFile川 = new Rx.Subject() 65 | 66 | // An observable representing the latest coverage. 67 | const coverage川 = lcovData川FromLcovFile川(this.lcovFile川).map(parseLcov).shareReplay(1) 68 | 69 | // Events subscribed to in atom's system can be easily cleaned up with a CompositeDisposable 70 | this.subscriptions = new CompositeDisposable() 71 | 72 | // Register command that toggles this view 73 | this.subscriptions.add(atom.commands.add('atom-workspace', { 74 | 'atom-lcov:select': (e) => this.selectFile(e) 75 | })) 76 | 77 | // Work with each text editor 78 | this.subscriptions.add(atom.workspace.observeTextEditors(textEditor => { 79 | const markerManager = new MarkerManager(textEditor) 80 | const subscription = coverage川.subscribe(coverage => { 81 | const fileCoverage = coverage[textEditor.getPath()] 82 | if (fileCoverage) markerManager.handleFileCoverage(fileCoverage) 83 | }) 84 | this.subscriptions.add(subscription) 85 | this.subscriptions.add(textEditor.onDidDestroy(() => { 86 | subscription.dispose() 87 | })) 88 | })) 89 | }, 90 | 91 | selectFile (e) { 92 | const path = $(e.target).closest('.file').find('.name').attr('data-path') 93 | this.lcovFile川.onNext(path) 94 | }, 95 | 96 | deactivate () { 97 | this.subscriptions.dispose() 98 | }, 99 | 100 | serialize () { 101 | return { } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/parseLcov.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | function parseLcov (lcov) { 4 | const lines = lcov.split('\n') 5 | const data = { } 6 | let _current 7 | function record (line, count) { 8 | if (line && !isNaN(count)) { 9 | const lineArray = _current[+line] || (_current[+line] = [ ]) 10 | lineArray.push(+count) 11 | } 12 | } 13 | for (const lineUntrimmed of lines) { 14 | const line = lineUntrimmed.trim() 15 | if (line.substr(0, 3) === 'SF:') { 16 | data[line.substr(3)] = _current = { } 17 | } else if (_current && line.substr(0, 3) === 'DA:') { 18 | const fields = line.substr(3).split(',') 19 | record(+fields[0], +fields[1]) 20 | } else if (_current && line.substr(0, 5) === 'BRDA:') { 21 | const fields = line.substr(5).split(',') 22 | record(+fields[0], +(fields[3] === '-' ? 0 : fields[3])) 23 | } 24 | } 25 | return data 26 | } 27 | 28 | export default parseLcov 29 | -------------------------------------------------------------------------------- /menus/atom-lcov.json: -------------------------------------------------------------------------------- 1 | { 2 | "context-menu": { 3 | ".tree-view .file": [ 4 | { 5 | "label": "Watch for coverage", 6 | "command": "atom-lcov:select" 7 | } 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-lcov", 3 | "main": "./lib/atom-lcov", 4 | "version": "2.0.0", 5 | "description": "See your code’s coverage in the gutter, with live reloading.", 6 | "keywords": [], 7 | "activationCommands": { 8 | "atom-workspace": "atom-lcov:select" 9 | }, 10 | "repository": "https://github.com/taskworld/atom-lcov", 11 | "license": "MIT", 12 | "engines": { 13 | "atom": ">=1.0.0 <2.0.0" 14 | }, 15 | "dependencies": { 16 | "rx": "^4.1.0", 17 | "jquery": "^2" 18 | }, 19 | "devDependencies": { 20 | "standard": "^7.1.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /spec/parseLcov-spec.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | /* global describe, it, expect */ 3 | import parseLcov from '../lib/parseLcov' 4 | 5 | describe('parseLcov', () => { 6 | it('parses the lcov data', () => { 7 | const data = parseLcov(text(` 8 | SF:/a.js 9 | DA:1,0 10 | DA:2,1 11 | DA:3,0 12 | BRDA:1,1.1,1,1 13 | BRDA:1,1.1,1,0 14 | SF:/b.js 15 | DA:1,0 16 | DA:2,2 17 | DA:3,2 18 | BRDA:2,1.1,1,1 19 | BRDA:3,1.1,1,0 20 | `)) 21 | expect(data['/a.js']).toEqual({ 22 | 1: [ 0, 1, 0 ], 23 | 2: [ 1 ], 24 | 3: [ 0 ] 25 | }) 26 | expect(data['/b.js']).toEqual({ 27 | 1: [ 0 ], 28 | 2: [ 2, 1 ], 29 | 3: [ 2, 0 ] 30 | }) 31 | }) 32 | }) 33 | 34 | function text (input) { 35 | return input.split(/\n/).map(line => line.trim()).filter(line => line).join('\n') 36 | } 37 | -------------------------------------------------------------------------------- /styles/atom-lcov.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | .lcov-gutter(@color) { 8 | &::after { 9 | display: block; 10 | content: ''; 11 | position: absolute; 12 | top: 50%; 13 | right: 0; 14 | width: 8px; 15 | height: 8px; 16 | margin-top: -4px; 17 | background: @color; 18 | border-radius: 4px; 19 | } 20 | } 21 | 22 | html /deep/ .lcov-covered { 23 | .lcov-gutter(@background-color-success); 24 | } 25 | 26 | html /deep/ .lcov-partially-covered { 27 | .lcov-gutter(@background-color-warning); 28 | } 29 | 30 | html /deep/ .lcov-missed { 31 | .lcov-gutter(@background-color-error); 32 | } 33 | --------------------------------------------------------------------------------