Code coverage for Solidity smart-contracts
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
solidity-coverage/test/units/app.js

385 lines
11 KiB

const assert = require('assert');
const fs = require('fs');
const shell = require('shelljs');
const mock = require('../util/integration.truffle');
const plugin = require('../../dist/truffle.plugin');
const path = require('path')
const util = require('util')
const opts = { compact: false, depth: 5, breakLength: 80 };
// =======
// Helpers
// =======
function pathExists(path) { return shell.test('-e', path); }
function pathToContract(config, file) {
return path.join('contracts', file);
}
function assertLineCoverage(expected=[]){
let summary = JSON.parse(fs.readFileSync('coverage/coverage-summary.json'));
expected.forEach(item => assert(summary[item.file].lines.pct === item.pct))
}
function assertCoverageMissing(expected=[]){
let summary = JSON.parse(fs.readFileSync('coverage/coverage-summary.json'));
expected.forEach(item => assert(summary[item.file] === undefined))
}
function assertCleanInitialState(){
assert(pathExists('./coverage') === false, 'should start without: coverage');
assert(pathExists('./coverage.json') === false, 'should start without: coverage.json');
}
function assertCoverageGenerate(truffleConfig){
const jsonPath = path.join(truffleConfig.working_directory, "coverage.json");
assert(pathExists('./coverage') === true, 'should gen coverage folder');
assert(pathExists(jsonPath) === true, 'should gen coverage.json');
}
function assertCoverageNotGenerated(truffleConfig){
const jsonPath = path.join(truffleConfig.working_directory, "coverage.json");
assert(pathExists('./coverage') !== true, 'should NOT gen coverage folder');
assert(pathExists(jsonPath) !== true, 'should NOT gen coverage.json');
}
function getOutput(truffleConfig){
const jsonPath = path.join(truffleConfig.working_directory, "coverage.json");
return JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
}
// ========
// Tests
// ========
describe('app', function() {
let truffleConfig;
let solcoverConfig;
let collector;
beforeEach(() => {
mock.clean();
solcoverConfig = {};
truffleConfig = mock.getDefaultTruffleConfig();
if (process.env.SILENT)
solcoverConfig.silent = true;
})
afterEach(() => mock.clean());
it('simple contract: should generate coverage, cleanup & exit(0)', async function(){
assertCleanInitialState();
mock.install('Simple', 'simple.js', solcoverConfig);
await plugin(truffleConfig);
assertCoverageGenerate(truffleConfig);
const output = getOutput(truffleConfig);
const path = Object.keys(output)[0];
assert(output[path].fnMap['1'].name === 'test', 'coverage.json missing "test"');
assert(output[path].fnMap['2'].name === 'getX', 'coverage.json missing "getX"');
});
// Truffle test asserts balance is 777 ether
it('config with providerOptions', async function() {
solcoverConfig.providerOptions = { default_balance_ether: 777 }
mock.install('Simple', 'testrpc-options.js', solcoverConfig);
await plugin(truffleConfig);
});
it('large contract with many unbracketed statements (time check)', async function() {
assertCleanInitialState();
truffleConfig.compilers.solc.version = "0.4.24";
mock.install('Oraclize', 'oraclize.js', solcoverConfig, truffleConfig, true);
await plugin(truffleConfig);
});
// This project has three contract suites and uses .deployed() instances which
// depend on truffle's migratons and the inter-test evm_revert / evm_snapshot mechanism.
it('project evm_reverts repeatedly', async function() {
assertCleanInitialState();
mock.installFullProject('multiple-migrations');
await plugin(truffleConfig);
const expected = [
{
file: pathToContract(truffleConfig, 'ContractA.sol'),
pct: 100
},
{
file: pathToContract(truffleConfig, 'ContractB.sol'),
pct: 100,
},
{
file: pathToContract(truffleConfig, 'ContractC.sol'),
pct: 100,
},
];
assertLineCoverage(expected);
});
it('project skips a folder', async function() {
assertCleanInitialState();
mock.installFullProject('skipping');
await plugin(truffleConfig);
const expected = [{
file: pathToContract(truffleConfig, 'ContractA.sol'),
pct: 100
}];
const missing = [{
file: pathToContract(truffleConfig, 'ContractB.sol'),
}];
assertLineCoverage(expected);
assertCoverageMissing(missing);
});
it('project with relative path solidity imports', async function() {
assertCleanInitialState();
mock.installFullProject('import-paths');
await plugin(truffleConfig);
});
it('truffle run coverage --config ../.solcover.js', async function() {
assertCleanInitialState();
solcoverConfig = {
silent: process.env.SILENT ? true : false,
istanbulReporter: ['json-summary', 'text']
};
fs.writeFileSync('.solcover.js', `module.exports=${JSON.stringify(solcoverConfig)}`);
// This relative path has to be ./ prefixed
// (because it's path.joined to truffle's working_directory)
truffleConfig.solcoverjs = './../.solcover.js';
mock.install('Simple', 'simple.js');
await plugin(truffleConfig);
// The relative solcoverjs uses the json-summary reporter which
// this assertion requires
const expected = [{
file: pathToContract(truffleConfig, 'Simple.sol'),
pct: 100
}];
assertLineCoverage(expected);
shell.rm('.solcover.js');
});
it('truffle run coverage --help', async function(){
assertCleanInitialState();
truffleConfig.help = "true";
mock.install('Simple', 'simple.js', solcoverConfig);
await plugin(truffleConfig);
})
it('truffle run coverage --version', async function(){
assertCleanInitialState();
truffleConfig.version = "true";
mock.install('Simple', 'simple.js', solcoverConfig);
await plugin(truffleConfig);
})
it('truffle run coverage --file test/<fileName>', async function() {
assertCleanInitialState();
const testPath = path.join(truffleConfig.working_directory, 'test/specific_a.js');
truffleConfig.file = testPath;
mock.installFullProject('test-files');
await plugin(truffleConfig);
const expected = [
{
file: pathToContract(truffleConfig, 'ContractA.sol'),
pct: 100
},
{
file: pathToContract(truffleConfig, 'ContractB.sol'),
pct: 0,
},
{
file: pathToContract(truffleConfig, 'ContractC.sol'),
pct: 0,
},
];
assertLineCoverage(expected);
});
it('truffle run coverage --file test/<glob*>', async function() {
assertCleanInitialState();
const testPath = path.join(truffleConfig.working_directory, 'test/globby*');
truffleConfig.file = testPath;
mock.installFullProject('test-files');
await plugin(truffleConfig);
const expected = [
{
file: pathToContract(truffleConfig, 'ContractA.sol'),
pct: 0,
},
{
file: pathToContract(truffleConfig, 'ContractB.sol'),
pct: 100,
},
{
file: pathToContract(truffleConfig, 'ContractC.sol'),
pct: 100,
},
];
assertLineCoverage(expected);
});
it('truffle run coverage --file test/gl{o,b}*.js', async function() {
assertCleanInitialState();
const testPath = path.join(truffleConfig.working_directory, 'test/gl{o,b}*.js');
truffleConfig.file = testPath;
mock.installFullProject('test-files');
await plugin(truffleConfig);
const expected = [
{
file: pathToContract(truffleConfig, 'ContractA.sol'),
pct: 0,
},
{
file: pathToContract(truffleConfig, 'ContractB.sol'),
pct: 100,
},
{
file: pathToContract(truffleConfig, 'ContractC.sol'),
pct: 100,
},
];
assertLineCoverage(expected);
});
it('contract only uses ".call"', async function(){
assertCleanInitialState();
mock.install('OnlyCall', 'only-call.js', solcoverConfig);
await plugin(truffleConfig);
assertCoverageGenerate(truffleConfig);
const output = getOutput(truffleConfig);
const path = Object.keys(output)[0];
assert(output[path].fnMap['1'].name === 'addTwo', 'cov should map "addTwo"');
});
it('contract sends / transfers to instrumented fallback', async function(){
assertCleanInitialState();
mock.install('Wallet', 'wallet.js', solcoverConfig);
await plugin(truffleConfig);
assertCoverageGenerate(truffleConfig);
const output = getOutput(truffleConfig);
const path = Object.keys(output)[0];
assert(output[path].fnMap['1'].name === 'transferPayment', 'cov should map "transferPayment"');
});
it('contracts are skipped', async function() {
assertCleanInitialState();
solcoverConfig.skipFiles = ['Owned.sol'];
mock.installDouble(['Proxy', 'Owned'], 'inheritance.js', solcoverConfig);
await plugin(truffleConfig);
assertCoverageGenerate(truffleConfig);
const output = getOutput(truffleConfig);
const firstKey = Object.keys(output)[0];
assert(Object.keys(output).length === 1, 'Wrong # of contracts covered');
assert(firstKey.substr(firstKey.length - 9) === 'Proxy.sol', 'Wrong contract covered');
});
it('contract uses inheritance', async function() {
assertCleanInitialState();
mock.installDouble(['Proxy', 'Owned'], 'inheritance.js', solcoverConfig);
await plugin(truffleConfig);
assertCoverageGenerate(truffleConfig);
const output = getOutput(truffleConfig);
const ownedPath = Object.keys(output)[0];
const proxyPath = Object.keys(output)[1];
assert(output[ownedPath].fnMap['1'].name === 'constructor', '"constructor" not covered');
assert(output[proxyPath].fnMap['1'].name === 'isOwner', '"isOwner" not covered');
});
// Simple.sol with a failing assertion in a truffle test
it('truffle tests failing', async function() {
assertCleanInitialState();
mock.install('Simple', 'truffle-test-fail.js', solcoverConfig);
try {
await plugin(truffleConfig);
assert.fail()
} catch(err){
assert(err.message.includes('failed under coverage'));
}
assertCoverageGenerate(truffleConfig);
const output = getOutput(truffleConfig);
const path = Object.keys(output)[0];
assert(output[path].fnMap['1'].name === 'test', 'cov missing "test"');
assert(output[path].fnMap['2'].name === 'getX', 'cov missing "getX"');
});
// Truffle test asserts deployment cost is greater than 20,000,000 gas
it('deployment cost > block gasLimit', async function() {
mock.install('Expensive', 'block-gas-limit.js', solcoverConfig);
await plugin(truffleConfig);
});
// Truffle test contains syntax error
it('truffle crashes', async function() {
assertCleanInitialState();
mock.install('Simple', 'truffle-crash.js', solcoverConfig);
try {
await plugin(truffleConfig);
assert.fail()
} catch(err){
assert(err.toString().includes('SyntaxError'));
}
});
// Solidity syntax errors
it('compilation failure', async function(){
assertCleanInitialState();
mock.install('SimpleError', 'simple.js', solcoverConfig);
try {
await plugin(truffleConfig);
assert.fail()
} catch(err){
assert(err.toString().includes('Compilation failed'));
}
assertCoverageNotGenerated(truffleConfig);
});
});