coverage.js
1 /*global describe, it*/ 2 const {assert} = require('chai'); 3 const fs = require('fs'); 4 const path = require('path'); 5 const sinon = require('sinon'); 6 7 const ContractSources = require('../lib/modules/coverage/contract_sources'); 8 const ContractSource = require('../lib/modules/coverage/contract_source'); 9 const SourceMap = require('../lib/modules/coverage/source_map'); 10 11 function fixturePath(fixture) { 12 return path.join(__dirname, 'fixtures', fixture); 13 } 14 15 function loadFixture(fixture) { 16 return fs.readFileSync(fixturePath(fixture)).toString(); 17 } 18 19 function dumpToFile(obj, path) { 20 return fs.writeFileSync(path, JSON.stringify(obj)); 21 } 22 23 describe('ContractSources', () => { 24 describe('constructor', () => { 25 it('should read files and create instances of ContractSource', (done) => { 26 const contractPath = fixturePath('cont.sol'); 27 var cs = new ContractSources([contractPath]); 28 assert.instanceOf(cs.files['cont.sol'], ContractSource); 29 30 done(); 31 }); 32 33 it('should work when a single path is passed', (done) => { 34 const contractPath = fixturePath('cont.sol'); 35 var cs = new ContractSources(contractPath); 36 assert.instanceOf(cs.files['cont.sol'], ContractSource); 37 38 done(); 39 }); 40 41 it('should throw an error when the file does not exist', (done) => { 42 assert.throws(() => { 43 new ContractSources(['fixtures/404.sol']); 44 }, /ENOENT: no such file or directory, open/); 45 46 done(); 47 }); 48 }); 49 50 describe('#toSolcInputs', () => { 51 it('should build the hash in the format that solc likes', (done) => { 52 const contractPath = fixturePath('cont.sol'); 53 var cs = new ContractSources([contractPath]); 54 assert.deepEqual({ 55 'cont.sol': {content: cs.files['cont.sol'].body} 56 }, cs.toSolcInputs()); 57 done(); 58 }); 59 }); 60 61 describe('#parseSolcOutput', () => { 62 it('should send the output to each of the ContractSource instances', (done) => { 63 const contractPath = fixturePath('cont.sol'); 64 var cs = new ContractSources([contractPath]); 65 66 var parseSolcOutputSpy = sinon.spy(cs.files['cont.sol'], 'parseSolcOutput'); 67 const solcOutput = JSON.parse(loadFixture('solc-output.json')); 68 cs.parseSolcOutput(solcOutput); 69 70 assert(parseSolcOutputSpy.calledOnce); 71 done(); 72 }); 73 }); 74 }); 75 76 describe('ContractSource', () => { 77 const contractSource = ` 78 pragma solidity ^0.4.24; 79 80 contract x { 81 int number; 82 string name; 83 84 constructor(string _name) 85 public 86 { 87 name = _name; 88 } 89 90 function g(int _number) 91 public 92 returns (int _multiplication) 93 { 94 number = _number; 95 return _number * 5; 96 } 97 98 function f(int _foo, int _bar) 99 public 100 pure 101 returns (int _addition) 102 { 103 return _foo + _bar; 104 } 105 106 function h(int _bar) 107 public 108 pure 109 returns (bool _great) 110 { 111 if(_bar > 25) { 112 return true; 113 } else { 114 return false; 115 } 116 } 117 } 118 `.trim(); 119 120 const cs = new ContractSource('contract.sol', '/tmp/contract.sol', contractSource); 121 122 describe('constructor', () => { 123 it('should set line offsets and line lengths correctly', (done) => { 124 // +1 here accounts for a newline 125 assert.equal("pragma solidity ^0.4.24;".length + 1, cs.lineOffsets[1]); 126 done(); 127 }); 128 }); 129 130 describe('#sourceMapToLocations', () => { 131 it('should return objects that indicate start and end location and columns', (done) => { 132 // constructor function 133 var loc = cs.sourceMapToLocations('71:60:0'); 134 assert.deepEqual({line: 7, column: 2}, loc.start); 135 assert.deepEqual({line: 11, column: 3}, loc.end); 136 137 // f function 138 loc = cs.sourceMapToLocations('257:104:0'); 139 assert.deepEqual({line: 21, column: 2}, loc.start); 140 assert.deepEqual({line: 27, column: 3}, loc.end); 141 142 // g function 143 loc = cs.sourceMapToLocations('135:118:0'); 144 assert.deepEqual({line: 13, column: 2}, loc.start); 145 assert.deepEqual({line: 19, column: 3}, loc.end); 146 147 done(); 148 }); 149 }); 150 151 describe('#parseSolcOutput', () => { 152 it('should parse the bytecode output correctly', (done) => { 153 var solcOutput = JSON.parse(loadFixture('solc-output.json')); 154 const contractPath = fixturePath('cont.sol'); 155 var cs = new ContractSources(contractPath); 156 cs.parseSolcOutput(solcOutput); 157 158 var contractSource = cs.files['cont.sol']; 159 160 assert.isNotEmpty(contractSource.contractBytecode); 161 assert.isNotEmpty(contractSource.contractBytecode['x']); 162 163 var bytecode = contractSource.contractBytecode['x']; 164 165 assert.deepEqual({instruction: 'PUSH1', sourceMap: {offset: 26, length: 487, id: 0, jump: '-'}, jump: '-', seen: false}, bytecode[0]); 166 assert.deepEqual({instruction: 'PUSH1', sourceMap: SourceMap.empty(), seen: false, jump: undefined}, bytecode[2]); 167 assert.deepEqual({instruction: 'MSTORE', sourceMap: SourceMap.empty(), seen: false, jump: undefined}, bytecode[4]); 168 assert.deepEqual({instruction: 'PUSH1', sourceMap: SourceMap.empty(), seen: false, jump: undefined}, bytecode[5]); 169 170 done(); 171 }); 172 }); 173 174 describe('#generateCodeCoverage', () => { 175 it('should return an error when solc output was not parsed', (done) => { 176 const contractPath = fixturePath('cont.sol'); 177 var cs = new ContractSources(contractPath); 178 var contractSource = cs.files['cont.sol']; 179 var trace = JSON.parse(loadFixture('geth-debugtrace-output-g.json')); 180 181 assert.throws(() => { 182 contractSource.generateCodeCoverage(trace); 183 }, 'Error generating coverage: solc output was not assigned'); 184 185 done(); 186 }); 187 188 it('should return a coverage report when solc output was parsed', (done) => { 189 var solcOutput = JSON.parse(loadFixture('solc-output.json')); 190 const contractPath = fixturePath('cont.sol'); 191 var cs = new ContractSources(contractPath); 192 cs.parseSolcOutput(solcOutput); 193 194 var trace = JSON.parse(loadFixture('geth-debugtrace-output-h-5.json')); 195 var coverage = cs.generateCodeCoverage(trace); 196 assert.exists(coverage); 197 198 done(); 199 }); 200 201 it('should merge coverages as we add more traces', (done) => { 202 const contractPath = fixturePath('cont.sol'); 203 var cs = new ContractSources(contractPath); 204 205 const solcOutput = JSON.parse(loadFixture('solc-output.json')); 206 cs.parseSolcOutput(solcOutput); 207 208 var trace = JSON.parse(loadFixture('geth-debugtrace-output-h-5.json')); 209 cs.generateCodeCoverage(trace); 210 211 trace = JSON.parse(loadFixture('geth-debugtrace-output-h-50.json')); 212 var coverage = cs.generateCodeCoverage(trace)['cont.sol']; 213 214 // In the fixture, the branch has an ID of 61, and the function has the 215 // ID of 63 216 assert.deepEqual([1,0], coverage.b['61']); 217 assert.equal(6, coverage.f['63']); 218 219 done(); 220 }); 221 }); 222 }); 223 224 describe('SourceMap', () => { 225 describe('#subtract', () => { 226 it('should return the correct values', (done) => { 227 var sm1 = new SourceMap('365:146:0'); 228 var sm2 = new SourceMap('428:83:0'); 229 230 var result = sm1.subtract(sm2); 231 232 assert.equal(365, result.offset); 233 assert.equal(63, result.length); 234 235 done(); 236 }); 237 }); 238 239 describe('#createRelativeTo', () => { 240 it('should return an empty source map on an empty string', (done) => { 241 var sm1 = new SourceMap('192:10:0'); 242 var sm2 = sm1.createRelativeTo(''); 243 244 assert.equal('', sm2.toString()); 245 246 done(); 247 }); 248 249 it('should return the correct source map on a relative string', (done) => { 250 var sm1 = new SourceMap('192:10:0'); 251 var sm2 = sm1.createRelativeTo(':14'); 252 253 assert.equal(192, sm2.offset); 254 assert.equal(14, sm2.length); 255 assert.equal(0, sm2.id); 256 257 done(); 258 }); 259 }); 260 });