mirror of https://github.com/embarklabs/embark.git
feat: run coverage for bytecode and deployedBytecode
This commit is contained in:
parent
3406ae833c
commit
f84d7f1d21
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue