/ test / coverage.js
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  });