feat: run coverage for bytecode and deployedBytecode

This commit is contained in:
Anthony Laibe 2018-11-14 13:48:58 +00:00
parent 3406ae833c
commit f84d7f1d21
2 changed files with 77 additions and 47 deletions

View File

@ -2,17 +2,15 @@ const SourceMap = require('./source_map');
class ContractSource { class ContractSource {
constructor(file, path, body) { constructor(file, path, body) {
let self = this;
this.file = file; this.file = file;
this.path = path; this.path = path;
this.body = body; this.body = body;
this.lineLengths = body.split("\n").map((line) => { return line.length; }); this.lineLengths = body.split("\n").map(line => line.length);
this.lineCount = this.lineLengths.length; this.lineCount = this.lineLengths.length;
this.lineOffsets = this.lineLengths.reduce((sum, _elt, i) => { this.lineOffsets = this.lineLengths.reduce((sum, _elt, i) => {
sum[i] = (i === 0) ? 0 : self.lineLengths[i-1] + sum[i-1] + 1; sum[i] = (i === 0) ? 0 : this.lineLengths[i-1] + sum[i-1] + 1;
return sum; return sum;
}, []); }, []);
@ -57,43 +55,20 @@ class ContractSource {
this.id = source.id; this.id = source.id;
this.ast = source.ast; this.ast = source.ast;
this.contractBytecode = {}; this.contractBytecode = {};
this.contractDeployedBytecode = {};
for(var contractName in contracts) { for(var contractName in contracts) {
this.contractBytecode[contractName] = {}; this.contractBytecode[contractName] = {};
this.contractDeployedBytecode[contractName] = {};
var contract = contracts[contractName]; var contract = contracts[contractName];
var bytecodeMapping = this.contractBytecode[contractName]; var opcodes = contract.evm.bytecode.opcodes.trim().split(' ');
var opcodes = contract.evm.deployedBytecode.opcodes.trim().split(' '); var deployedOpcodes = contract.evm.deployedBytecode.opcodes.trim().split(' ');
var sourceMaps = contract.evm.deployedBytecode.sourceMap.split(';'); var sourceMaps = contract.evm.bytecode.sourceMap.split(';');
var deployedSourceMaps = contract.evm.deployedBytecode.sourceMap.split(';');
var bytecodeIdx = 0; this._buildContractBytecode(contractName, this.contractBytecode, opcodes, sourceMaps);
var pc = 0; this._buildContractBytecode(contractName, this.contractDeployedBytecode, deployedOpcodes, deployedSourceMaps);
var instructions = 0;
var previousSourceMap = null;
do {
let sourceMap;
if(previousSourceMap === null) {
sourceMap = new SourceMap(sourceMaps[instructions]);
} else {
sourceMap = previousSourceMap.createRelativeTo(sourceMaps[instructions]);
}
var instruction = opcodes[bytecodeIdx];
var length = this._instructionLength(instruction);
bytecodeMapping[pc] = {
instruction: instruction,
sourceMap: sourceMap,
jump: sourceMap.jump,
seen: false
};
pc += length;
instructions++;
bytecodeIdx += (length > 1) ? 2 : 1;
previousSourceMap = sourceMap;
} while(bytecodeIdx < opcodes.length);
} }
} }
@ -102,7 +77,7 @@ class ContractSource {
Object.values(this.contractBytecode).every((contractBytecode) => { return (Object.values(contractBytecode).length <= 1); }); Object.values(this.contractBytecode).every((contractBytecode) => { return (Object.values(contractBytecode).length <= 1); });
} }
/*eslint complexity: ["error", 44]*/ /*eslint complexity: ["error", 42]*/
generateCodeCoverage(trace) { generateCodeCoverage(trace) {
if(!this.ast || !this.contractBytecode) throw new Error('Error generating coverage: solc output was not assigned'); if(!this.ast || !this.contractBytecode) throw new Error('Error generating coverage: solc output was not assigned');
@ -128,7 +103,6 @@ class ContractSource {
let children = []; let children = [];
let markLocations = []; let markLocations = [];
let location; let location;
switch(node.nodeType) { switch(node.nodeType) {
case 'Assignment': case 'Assignment':
case 'EventDefinition': case 'EventDefinition':
@ -280,7 +254,6 @@ class ContractSource {
} }
default: default:
//console.log(`Don't know how to handle node type ${node.nodeType}`);
break; break;
} }
@ -294,13 +267,21 @@ class ContractSource {
} while(nodesRequiringVisiting.length > 0); } while(nodesRequiringVisiting.length > 0);
this._generateCodeCoverageForBytecode(trace, coverage, sourceMapToNodeType, this.contractBytecode);
this._generateCodeCoverageForBytecode(trace, coverage, sourceMapToNodeType, this.contractDeployedBytecode);
return coverage;
}
_generateCodeCoverageForBytecode(trace, coverage, sourceMapToNodeType, contractBytecode) {
var contractMatches = true; var contractMatches = true;
for(var contractName in this.contractBytecode) { for(var contractName in contractBytecode) {
var bytecode = this.contractBytecode[contractName]; var bytecode = contractBytecode[contractName];
// Try to match the contract to the bytecode. If it doesn't, // Try to match the contract to the bytecode. If it doesn't,
// then we bail. // then we bail.
contractMatches = trace.structLogs.every((step) => { return bytecode[step.pc]; });
contractMatches = trace.structLogs.every((step) => bytecode[step.pc]);
if(!contractMatches) continue; if(!contractMatches) continue;
trace.structLogs.forEach((step) => { trace.structLogs.forEach((step) => {
@ -336,8 +317,38 @@ class ContractSource {
}); });
}); });
} }
}
return coverage; _buildContractBytecode(contractName, contractBytecode, opcodes, sourceMaps) {
var bytecodeMapping = contractBytecode[contractName];
var bytecodeIdx = 0;
var pc = 0;
var instructions = 0;
var previousSourceMap = null;
do {
let sourceMap;
const sourceMapArgs = sourceMaps[instructions];
if(previousSourceMap === null) {
sourceMap = new SourceMap(sourceMapArgs);
} else {
sourceMap = previousSourceMap.createRelativeTo(sourceMapArgs);
}
var instruction = opcodes[bytecodeIdx];
var length = this._instructionLength(instruction);
bytecodeMapping[pc] = {
instruction: instruction,
sourceMap: sourceMap,
jump: sourceMap.jump,
seen: false
};
pc += length;
instructions++;
bytecodeIdx += (length > 1) ? 2 : 1;
previousSourceMap = sourceMap;
} while(bytecodeIdx < opcodes.length);
} }
_instructionLength(instruction) { _instructionLength(instruction) {

View File

@ -16,10 +16,6 @@ function loadFixture(fixture) {
return fs.readFileSync(fixturePath(fixture)).toString(); return fs.readFileSync(fixturePath(fixture)).toString();
} }
function dumpToFile(obj, path) {
return fs.writeFileSync(path, JSON.stringify(obj));
}
describe('ContractSources', () => { describe('ContractSources', () => {
describe('constructor', () => { describe('constructor', () => {
it('should read files and create instances of ContractSource', (done) => { it('should read files and create instances of ContractSource', (done) => {
@ -148,6 +144,29 @@ contract x {
}); });
}); });
describe('#parseSolcOutput', () => {
it('should parse the deployed 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.contractDeployedBytecode);
assert.isNotEmpty(contractSource.contractDeployedBytecode['x']);
var bytecode = contractSource.contractDeployedBytecode['x'];
assert.deepEqual({instruction: 'PUSH1', sourceMap: {offset: 26, length: 487, id: 0, jump: '-'}, jump: '-', seen: false}, bytecode[0]);
assert.deepEqual({instruction: 'PUSH1', sourceMap: SourceMap.empty(), seen: false, jump: undefined}, bytecode[2]);
assert.deepEqual({instruction: 'MSTORE', sourceMap: SourceMap.empty(), seen: false, jump: undefined}, bytecode[4]);
assert.deepEqual({instruction: 'PUSH1', sourceMap: SourceMap.empty(), seen: false, jump: undefined}, bytecode[5]);
done();
});
});
describe('#parseSolcOutput', () => { describe('#parseSolcOutput', () => {
it('should parse the bytecode output correctly', (done) => { it('should parse the bytecode output correctly', (done) => {
var solcOutput = JSON.parse(loadFixture('solc-output.json')); var solcOutput = JSON.parse(loadFixture('solc-output.json'));
@ -165,7 +184,7 @@ contract x {
assert.deepEqual({instruction: 'PUSH1', sourceMap: {offset: 26, length: 487, id: 0, jump: '-'}, jump: '-', seen: false}, bytecode[0]); assert.deepEqual({instruction: 'PUSH1', sourceMap: {offset: 26, length: 487, id: 0, jump: '-'}, jump: '-', seen: false}, bytecode[0]);
assert.deepEqual({instruction: 'PUSH1', sourceMap: SourceMap.empty(), seen: false, jump: undefined}, bytecode[2]); assert.deepEqual({instruction: 'PUSH1', sourceMap: SourceMap.empty(), seen: false, jump: undefined}, bytecode[2]);
assert.deepEqual({instruction: 'MSTORE', sourceMap: SourceMap.empty(), seen: false, jump: undefined}, bytecode[4]); assert.deepEqual({instruction: 'MSTORE', sourceMap: SourceMap.empty(), seen: false, jump: undefined}, bytecode[4]);
assert.deepEqual({instruction: 'PUSH1', sourceMap: SourceMap.empty(), seen: false, jump: undefined}, bytecode[5]); assert.deepEqual({instruction: 'CALLVALUE', sourceMap: {offset: 71, length: 60, jump: undefined}, seen: false, jump: undefined}, bytecode[5]);
done(); done();
}); });