This commit is contained in:
Andre Medeiros 2018-08-07 15:26:39 -04:00 committed by Iuri Matias
parent 253d3dd57c
commit 8e396a17d8
13 changed files with 7874 additions and 17 deletions

View File

@ -1,22 +1,37 @@
const fs = require('fs');
const path = require('path');
const ContractSource = require('./contract_source');
class ContractSources {
constructor(files) {
if(!Array.isArray(files))
files = [files];
this.files = {};
files.forEach((file) => {
try {
var content = fs.readFileSync(file).toString()
this.files[file] = new ContractSource(file, content);
} catch(e) {
throw new Error(`Error loading ${file}: ${e.code}`)
}
});
switch(Object.prototype.toString.call(files)) {
case '[object Object]':
Object.keys(files).forEach((file) => {
var basename = path.basename(file);
this.files[basename] = new ContractSource(basename, files[file]);
});
break;
case '[object String]':
// No 'break' statement here on purpose, as it shares
// the logic below.
files = [files];
case '[object Array]':
files.forEach((file) => {
var basename = path.basename(file);
try {
var content = fs.readFileSync(file).toString();
this.files[basename] = new ContractSource(basename, content);
} catch(e) {
throw new Error(`Error loading ${file}: ${e.code}`);
}
});
break;
}
}
toSolcInputs() {
@ -31,8 +46,14 @@ class ContractSources {
parseSolcOutput(output) {
for(var file in output.contracts) {
var contractSource = this.files[file];
contractSource.parseSolcOutput(output.sources[file], output.contracts[file])
var basename = path.basename(file);
var contractSource = this.files[basename];
if(!contractSource){
continue; // TODO CHECK THIS LOGIC
throw new Error(`Can't attribute output to ${file}: file has not been read in.`);
}
contractSource.parseSolcOutput(output.sources[file], output.contracts[file]);
}
}
@ -44,7 +65,33 @@ class ContractSources {
coverageReport[file] = contractSource.generateCodeCoverage(trace);
}
return coverageReport;
if(!this.coverageReport) {
this.coverageReport = coverageReport;
return this.coverageReport;
}
// We already have a previous coverage report, so we're merging results here.
Object.keys(coverageReport).forEach((file) => {
if(!this.coverageReport[file]) {
this.coverageReport[file] = coverageReport[file];
return;
}
// Increment counters for statements, functions and lines
['s', 'f', 'l'].forEach((countType) => {
Object.keys(coverageReport[file][countType]).forEach((id) => {
this.coverageReport[file][countType][id] += coverageReport[file][countType][id];
});
});
// Branch counts are tracked in a different manner so we'll do these now
Object.keys(coverageReport[file].b).forEach((id) => {
this.coverageReport[file].b[id][0] += coverageReport[file].b[id][0];
this.coverageReport[file].b[id][1] += coverageReport[file].b[id][1];
});
});
return this.coverageReport;
}
}

View File

@ -1,9 +1,36 @@
const ContractSources = require('./contract_sources');
class CodeCoverage {
constructor(embark, _options) {
this.events = embark.events;
this.logger = embark.logger;
embark.events.on('contracts:compile:solc', this.compileSolc.bind(this));
embark.events.on('contracts:compiled:solc', this.compiledSolc.bind(this));
embark.events.on('contracts:run:solc', this.runSolc.bind(this));
embark.registerActionForEvent('contracts:deploy:afterAll', this.deployed.bind(this));
embark.events.on('block:header', this.runSolc.bind(this));
}
compileSolc(input) {
var sources = {};
Object.keys(input.sources).forEach((path) => {
sources[path] = input.sources[path].content;
});
this.contractSources = new ContractSources(sources);
}
compiledSolc(output) {
this.contractSources.parseSolcOutput(output);
}
runSolc(receipt) {
console.log('runSolc');
console.dir(receipt);
//this.contractSources.generateCodeCoverage(trace);
}
deployed(cb) {

View File

@ -0,0 +1,30 @@
class SourceMap {
constructor(sourceMapStringOrOffset, length, id) {
if(typeof sourceMapStringOrOffset == 'string') {
var [offset, length, id, ..._rest] = sourceMapStringOrOffset.split(":");
this.offset = parseInt(offset, 10);
this.length = parseInt(length, 10);
if(id) this.id = parseInt(id, 10);
} else {
this.offset = sourceMapStringOrOffset;
this.length = length;
this.id = id;
}
}
subtract(sourceMap) {
var length = sourceMap.offset - this.offset;
return new SourceMap(this.offset, length);
}
toString() {
var parts = [this.offset, this.length];
if(this.id) parts.push(this.id);
return parts.join(':');
}
}
module.exports = SourceMap;

View File

@ -200,6 +200,7 @@ class ContractDeployer {
}
// saving code changes back to the contract object
contract.code = contractCode;
self.events.request('contracts:setBytecode', contract.className, contractCode);
next();
});
},

View File

@ -37,7 +37,7 @@ class Solidity {
self.logger.error(__('Error while loading the content of ') + filename);
return fileCb();
}
input[filename] = {content: fileContent.replace(/\r\n/g, '\n')};
input[filename] = {content: fileContent.replace(/\r\n/g, '\n'), path: file.path};
fileCb();
});
},
@ -70,16 +70,34 @@ class Solidity {
},
outputSelection: {
'*': {
'*': ['abi', 'metadata', 'userdoc', 'devdoc', 'evm.legacyAssembly', 'evm.bytecode', 'evm.deployedBytecode', 'evm.methodIdentifiers', 'evm.gasEstimates']
'': [
'ast',
'legacyAST'
],
'*': [
'abi',
'devdoc',
'evm.bytecode',
'evm.deployedBytecode',
'evm.gasEstimates',
'evm.legacyAssembly',
'evm.methodIdentifiers',
'metadata',
'userdoc'
]
}
}
}
};
self.solcW.compile(jsonObj, function (err, output) {
self.events.emit('contracts:compile:solc', jsonObj);
if(err){
return callback(err);
}
output.errors = []; // TODO REMOVE THIS
if (output.errors) {
for (let i=0; i<output.errors.length; i++) {
if (output.errors[i].type === 'Warning') {
@ -90,6 +108,10 @@ class Solidity {
}
}
}
//self.plugins.emitAndRunActionsForEvent('contracts:compiled:solc', output);
self.events.emit('contracts:compiled:solc', output);
callback(null, output);
});
},

47
package-lock.json generated
View File

@ -1490,6 +1490,12 @@
}
}
},
"assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
"dev": true
},
"assign-symbols": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
@ -2849,6 +2855,20 @@
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000865.tgz",
"integrity": "sha512-vs79o1mOSKRGv/1pSkp4EXgl4ZviWeYReXw60XfacPU64uQWZwJT6vZNmxRF9O+6zu71sJwMxLK5JXxbzuVrLw=="
},
"chai": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz",
"integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=",
"dev": true,
"requires": {
"assertion-error": "^1.0.1",
"check-error": "^1.0.1",
"deep-eql": "^3.0.0",
"get-func-name": "^2.0.0",
"pathval": "^1.0.0",
"type-detect": "^4.0.0"
}
},
"chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
@ -2866,6 +2886,12 @@
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
"integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I="
},
"check-error": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
"dev": true
},
"chokidar": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.3.tgz",
@ -3625,6 +3651,15 @@
}
}
},
"deep-eql": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
"dev": true,
"requires": {
"type-detect": "^4.0.0"
}
},
"deep-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
@ -5095,6 +5130,12 @@
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz",
"integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U="
},
"get-func-name": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
"dev": true
},
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
@ -8835,6 +8876,12 @@
"pify": "^3.0.0"
}
},
"pathval": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
"integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
"dev": true
},
"pbkdf2": {
"version": "3.0.16",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz",

View File

@ -102,6 +102,7 @@
"devDependencies": {
"eslint": "4.13.1",
"mocha-sinon": "^1.1.4",
"sinon": "^4.5.0"
"sinon": "^4.5.0",
"chai": "4.1.2"
}
}

237
test/coverage.js Normal file
View File

@ -0,0 +1,237 @@
const {assert, expect} = require('chai');
const fs = require('fs');
const path = require('path');
const sinon = require('sinon');
const ContractSources = require('../lib/modules/coverage/contract_sources');
const ContractSource = require('../lib/modules/coverage/contract_source');
const SourceMap = require('../lib/modules/coverage/source_map');
function fixturePath(fixture) {
return path.join(__dirname, 'fixtures', fixture);
}
function loadFixture(fixture) {
return fs.readFileSync(fixturePath(fixture)).toString();
}
function dumpToFile(obj, path) {
return fs.writeFileSync(path, JSON.stringify(obj));
}
describe('ContractSources', () => {
describe('constructor', () => {
it('should read files and create instances of ContractSource', (done) => {
const contractPath = fixturePath('cont.sol');
var cs = new ContractSources([contractPath]);
assert.instanceOf(cs.files['cont.sol'], ContractSource);
done();
});
it('should work when a single path is passed', (done) => {
const contractPath = fixturePath('cont.sol');
var cs = new ContractSources(contractPath);
assert.instanceOf(cs.files['cont.sol'], ContractSource);
done();
});
it('should throw an error when the file does not exist', (done) => {
assert.throws(() => {
new ContractSources(['fixtures/404.sol'])
}, 'Error loading fixtures/404.sol: ENOENT');
done();
});
});
describe('#toSolcInputs', () => {
it('should build the hash in the format that solc likes', (done) => {
const contractPath = fixturePath('cont.sol');
var cs = new ContractSources([contractPath]);
assert.deepEqual({
'cont.sol': {content: cs.files['cont.sol'].body}
}, cs.toSolcInputs());
done();
});
});
describe('#parseSolcOutput', () => {
it('should send the output to each of the ContractSource instances', (done) => {
const contractPath = fixturePath('cont.sol');
var cs = new ContractSources([contractPath]);
var parseSolcOutputSpy = sinon.spy(cs.files['cont.sol'], 'parseSolcOutput');
const solcOutput = JSON.parse(loadFixture('solc-output.json'));
cs.parseSolcOutput(solcOutput);
assert(parseSolcOutputSpy.calledOnce);
done();
});
});
});
describe('ContractSource', () => {
const contractSource = `
pragma solidity ^0.4.24;
contract x {
int number;
string name;
constructor(string _name)
public
{
name = _name;
}
function g(int _number)
public
returns (int _multiplication)
{
number = _number;
return _number * 5;
}
function f(int _foo, int _bar)
public
pure
returns (int _addition)
{
return _foo + _bar;
}
function h(int _bar)
public
pure
returns (bool _great)
{
if(_bar > 25) {
return true;
} else {
return false;
}
}
}
`.trim();
const cs = new ContractSource('contract.sol', contractSource);
describe('constructor', () => {
it('should set line offsets and line lengths correctly', (done) => {
// +1 here accounts for a newline
assert.equal("pragma solidity ^0.4.24;".length + 1, cs.lineOffsets[1]);
done();
});
});
describe('#sourceMapToLocations', () => {
it('should return objects that indicate start and end location and columns', (done) => {
// constructor function
var loc = cs.sourceMapToLocations('71:60:0');
assert.deepEqual({line: 7, column: 2}, loc.start);
assert.deepEqual({line: 11, column: 3}, loc.end);
// f function
loc = cs.sourceMapToLocations('257:104:0');
assert.deepEqual({line: 21, column: 2}, loc.start);
assert.deepEqual({line: 27, column: 3}, loc.end);
// g function
loc = cs.sourceMapToLocations('135:118:0');
assert.deepEqual({line: 13, column: 2}, loc.start);
assert.deepEqual({line: 19, column: 3}, loc.end);
done();
});
});
describe('#parseSolcOutput', () => {
it('should parse the bytecode output correctly', (done) => {
var solcOutput = JSON.parse(loadFixture('solc-output.json'));
const contractPath = fixturePath('cont.sol');
var cs = new ContractSources(contractPath);
cs.parseSolcOutput(solcOutput);
var contractSource = cs.files['cont.sol'];
assert.isNotEmpty(contractSource.contractBytecode);
assert.isNotEmpty(contractSource.contractBytecode['x']);
var bytecode = contractSource.contractBytecode['x'];
assert.deepEqual({instruction: 'PUSH1', sourceMap: '26:487:0:-', seen: false}, bytecode[0]);
assert.deepEqual({instruction: 'PUSH1', sourceMap: '', seen: false}, bytecode[2]);
assert.deepEqual({instruction: 'MSTORE', sourceMap: '', seen: false}, bytecode[4]);
assert.deepEqual({instruction: 'PUSH1', sourceMap: '', seen: false}, bytecode[5]);
done();
});
});
describe('#generateCodeCoverage', () => {
it('should return an error when solc output was not parsed', (done) => {
const contractPath = fixturePath('cont.sol');
var cs = new ContractSources(contractPath);
var contractSource = cs.files['cont.sol'];
var trace = JSON.parse(loadFixture('geth-debugtrace-output-g.json'));
assert.throws(() => {
contractSource.generateCodeCoverage(trace);
}, 'Error generating coverage: solc output was not assigned');
done();
});
it('should return a coverage report when solc output was parsed', (done) => {
var solcOutput = JSON.parse(loadFixture('solc-output.json'));
const contractPath = fixturePath('cont.sol');
var cs = new ContractSources(contractPath);
cs.parseSolcOutput(solcOutput);
var trace = JSON.parse(loadFixture('geth-debugtrace-output-h-5.json'));
var coverage = cs.generateCodeCoverage(trace);
dumpToFile(coverage, '/tmp/coverage.json');
done();
});
it('should merge coverages as we add more traces', (done) => {
const contractPath = fixturePath('cont.sol');
var cs = new ContractSources(contractPath);
const solcOutput = JSON.parse(loadFixture('solc-output.json'));
cs.parseSolcOutput(solcOutput);
var trace = JSON.parse(loadFixture('geth-debugtrace-output-h-5.json'));
cs.generateCodeCoverage(trace);
trace = JSON.parse(loadFixture('geth-debugtrace-output-h-50.json'));
var coverage = cs.generateCodeCoverage(trace)['cont.sol'];
// In the fixture, the branch has an ID of 61, and the function has the
// ID of 63
assert.deepEqual([1,1], coverage.b['61']);
assert.equal(2, coverage.f['63']);
done();
});
});
});
describe('SourceMap', () => {
describe('#subtract', () => {
it('should return the correct values', (done) => {
var sm1 = new SourceMap('365:146:0');
var sm2 = new SourceMap('428:83:0');
var result = sm1.subtract(sm2);
assert.equal(365, result.offset);
assert.equal(63, result.length);
done();
});
});
});

40
test/fixtures/cont.sol vendored Normal file
View File

@ -0,0 +1,40 @@
pragma solidity ^0.4.24;
contract x {
int number;
string name;
constructor(string _name)
public
{
name = _name;
}
function g(int _number)
public
returns (int _multiplication)
{
number = _number;
return _number * 5;
}
function f(int _foo, int _bar)
public
pure
returns (int _addition)
{
return _foo + _bar;
}
function h(int _bar)
public
pure
returns (bool _great)
{
if(_bar > 25) {
return true;
} else {
return false;
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

3585
test/fixtures/solc-output.json vendored Normal file

File diff suppressed because one or more lines are too long