parent
b1fa110d89
commit
f1de0f42df
@ -1,61 +0,0 @@ |
||||
const Web3 = require('web3'); |
||||
const shell = require('shelljs'); |
||||
const SolidityCoder = require('web3/lib/solidity/coder.js'); |
||||
const getInstrumentedVersion = require('./instrumentSolidity.js'); |
||||
const fs = require('fs'); |
||||
const path = require('path'); |
||||
const CoverageMap = require('./coverageMap.js'); |
||||
const mkdirp = require('mkdirp'); |
||||
const childprocess = require('child_process'); |
||||
|
||||
const coverage = new CoverageMap(); |
||||
const web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')); |
||||
|
||||
// Patch our local testrpc if necessary
|
||||
if (!shell.test('-e', './node_modules/ethereumjs-vm/lib/opFns.js.orig')) { |
||||
console.log('patch local testrpc...'); |
||||
shell.exec('patch -b ./node_modules/ethereumjs-vm/lib/opFns.js ./hookIntoEvents.patch'); |
||||
} |
||||
// Run the modified testrpc with large block limit
|
||||
const testrpcProcess = childprocess.exec('./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit 0xfffffffffffff --gasPrice 0x1'); |
||||
|
||||
if (shell.test('-d', '../originalContracts')) { |
||||
console.log('There is already an "originalContracts" directory in your truffle directory.\n' + |
||||
'This is probably due to a previous solcover failure.\n' + |
||||
'Please make sure the ./contracts/ directory contains your contracts (perhaps by copying them from originalContracts), ' + |
||||
'and then delete the originalContracts directory.'); |
||||
process.exit(1); |
||||
} |
||||
|
||||
shell.mv('./../contracts/', './../originalContracts/'); |
||||
shell.mkdir('./../contracts/'); |
||||
// For each contract in originalContracts, get the instrumented version
|
||||
shell.ls('./../originalContracts/**/*.sol').forEach(file => { |
||||
if (file !== 'originalContracts/Migrations.sol') { |
||||
const canonicalContractPath = path.resolve(file); |
||||
|
||||
console.log('instrumenting ', canonicalContractPath); |
||||
const contract = fs.readFileSync(canonicalContractPath).toString(); |
||||
const instrumentedContractInfo = getInstrumentedVersion(contract, canonicalContractPath); |
||||
mkdirp.sync(path.dirname(canonicalContractPath.replace('originalContracts', 'contracts'))); |
||||
fs.writeFileSync(canonicalContractPath.replace('originalContracts', 'contracts'), instrumentedContractInfo.contract); |
||||
coverage.addContract(instrumentedContractInfo, canonicalContractPath); |
||||
} |
||||
}); |
||||
shell.cp('./../originalContracts/Migrations.sol', './../contracts/Migrations.sol'); |
||||
|
||||
shell.rm('./allFiredEvents'); // Delete previous results
|
||||
shell.exec('truffle test --network test'); |
||||
|
||||
const events = fs.readFileSync('./allFiredEvents').toString().split('\n'); |
||||
events.pop(); |
||||
// The pop here isn't a bug - there is an empty line at the end of this file, so we
|
||||
// don't want to include it as an event.
|
||||
coverage.generate(events, './../originalContracts/'); |
||||
|
||||
fs.writeFileSync('./coverage.json', JSON.stringify(coverage.coverage)); |
||||
|
||||
shell.exec('./node_modules/istanbul/lib/cli.js report lcov'); |
||||
testrpcProcess.kill(); |
||||
shell.rm('-rf', './../contracts'); |
||||
shell.mv('./../originalContracts', './../contracts'); |
@ -0,0 +1,168 @@ |
||||
|
||||
|
||||
const assert = require('assert'); |
||||
const shell = require('shelljs'); |
||||
const fs = require('fs'); |
||||
const childprocess = require('child_process'); |
||||
const mock = require('./util/mockTruffle.js'); |
||||
|
||||
let launchTestRpc = false; |
||||
|
||||
// shell.test alias for legibility
|
||||
function pathExists(path) { return shell.test('-e', path); } |
||||
|
||||
// tests run out of memory in CI without this
|
||||
function collectGarbage() { |
||||
if (global.gc) { global.gc(); } |
||||
} |
||||
|
||||
describe('run', () => { |
||||
let port = 8555; |
||||
let testrpcProcess = null; |
||||
let script = `node ./exec.js --dir "./mock" --port ${port} --silent`; //
|
||||
|
||||
before(() => { |
||||
mock.protectCoverage(); |
||||
}); |
||||
|
||||
beforeEach(() => { |
||||
// CI (Ubuntu) doesn't seem to be freeing server resources until the parent process of these
|
||||
// tests exits so there are errors launching testrpc repeatedly on the same port. Tests #2 through
|
||||
// #last will use this instance of testrpc (port 8557). Test #1 uses the instance launched by
|
||||
// the run script (which also installs the patch). This allows us to end run CI container issues
|
||||
// AND verify that the script actually works.
|
||||
if (launchTestRpc) { |
||||
port = 8557; |
||||
script = `node ./exec.js --dir "./mock" --port ${port} --norpc --silent`; //
|
||||
const command = `./node_modules/ethereumjs-testrpc/bin/testrpc --gasLimit 0xfffffffffff --port ${port}`; |
||||
testrpcProcess = childprocess.exec(command); |
||||
launchTestRpc = false; |
||||
} |
||||
}); |
||||
|
||||
after(() => { |
||||
mock.restoreCoverage(); |
||||
testrpcProcess.kill(); |
||||
}); |
||||
|
||||
afterEach(() => { |
||||
mock.remove(); |
||||
}); |
||||
|
||||
// This pre-test flushes the suite. There's some kind of sequencing issue here in development
|
||||
// - the first test always fails unless there is a fresh testrpc install.
|
||||
// Should not be a problem on CI but results in false positives @home.
|
||||
it('flush tests', () => { |
||||
mock.install('Simple.sol', 'simple.js'); |
||||
shell.exec(script); |
||||
collectGarbage(); |
||||
}); |
||||
|
||||
// This test should be positioned first in the suite because of the way testrpc is
|
||||
// launched for these tests.
|
||||
it('simple contract: should generate coverage, cleanup & exit(0)', () => { |
||||
// Directory should be clean
|
||||
assert(pathExists('./coverage') === false, 'should start without: coverage'); |
||||
assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); |
||||
|
||||
// Run script (exits 0);
|
||||
mock.install('Simple.sol', 'simple.js'); |
||||
shell.exec(script); |
||||
assert(shell.error() === null, 'script should not error'); |
||||
|
||||
// Directory should have coverage report
|
||||
assert(pathExists('./coverage') === true, 'script should gen coverage folder'); |
||||
assert(pathExists('./coverage.json') === true, 'script should gen coverage.json'); |
||||
|
||||
// Coverage should be real.
|
||||
// This test is tightly bound to the function names in Simple.sol
|
||||
const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8')); |
||||
const path = Object.keys(produced)[0]; |
||||
assert(produced[path].fnMap['1'].name === 'test', 'coverage.json should map "test"'); |
||||
assert(produced[path].fnMap['2'].name === 'getX', 'coverage.json should map "getX"'); |
||||
collectGarbage(); |
||||
launchTestRpc = true; // Toggle flag to launch a single testrpc instance for rest of tests.
|
||||
}); |
||||
|
||||
it('contract only uses .call: should generate coverage, cleanup & exit(0)', () => { |
||||
// Run against contract that only uses method.call.
|
||||
assert(pathExists('./coverage') === false, 'should start without: coverage'); |
||||
assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); |
||||
mock.install('OnlyCall.sol', 'only-call.js'); |
||||
|
||||
shell.exec(script); |
||||
assert(shell.error() === null, 'script should not error'); |
||||
|
||||
assert(pathExists('./coverage') === true, 'script should gen coverage folder'); |
||||
assert(pathExists('./coverage.json') === true, 'script should gen coverage.json'); |
||||
|
||||
const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8')); |
||||
const path = Object.keys(produced)[0]; |
||||
assert(produced[path].fnMap['1'].name === 'getFive', 'coverage.json should map "getFive"'); |
||||
collectGarbage(); |
||||
}); |
||||
|
||||
it('truffle tests failing: should generate coverage, cleanup & exit(0)', () => { |
||||
assert(pathExists('./coverage') === false, 'should start without: coverage'); |
||||
assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); |
||||
|
||||
// Run with Simple.sol and a failing assertion in a truffle test
|
||||
mock.install('Simple.sol', 'truffle-test-fail.js'); |
||||
shell.exec(script); |
||||
assert(shell.error() === null, 'script should not error'); |
||||
assert(pathExists('./coverage') === true, 'script should gen coverage folder'); |
||||
assert(pathExists('./coverage.json') === true, 'script should gen coverage.json'); |
||||
|
||||
const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8')); |
||||
const path = Object.keys(produced)[0]; |
||||
assert(produced[path].fnMap['1'].name === 'test', 'coverage.json should map "test"'); |
||||
assert(produced[path].fnMap['2'].name === 'getX', 'coverage.json should map "getX"'); |
||||
collectGarbage(); |
||||
}); |
||||
|
||||
it('deployment cost > block gasLimit: should generate coverage, cleanup & exit(0)', () => { |
||||
// Just making sure Expensive.sol compiles and deploys here.
|
||||
mock.install('Expensive.sol', 'block-gas-limit.js'); |
||||
shell.exec(script); |
||||
assert(shell.error() === null, 'script should not error'); |
||||
collectGarbage(); |
||||
}); |
||||
|
||||
it('truffle crashes: should generate NO coverage, cleanup and exit(1)', () => { |
||||
assert(pathExists('./coverage') === false, 'should start without: coverage'); |
||||
assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); |
||||
|
||||
// Run with Simple.sol and a syntax error in the truffle test
|
||||
mock.install('Simple.sol', 'truffle-crash.js'); |
||||
shell.exec(script); |
||||
assert(shell.error() !== null, 'script should error'); |
||||
assert(pathExists('./coverage') !== true, 'script should NOT gen coverage folder'); |
||||
assert(pathExists('./coverage.json') !== true, 'script should NOT gen coverage.json'); |
||||
collectGarbage(); |
||||
}); |
||||
|
||||
it('instrumentation errors: should generate NO coverage, cleanup and exit(1)', () => { |
||||
assert(pathExists('./coverage') === false, 'should start without: coverage'); |
||||
assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); |
||||
|
||||
// Run with SimpleError.sol (has syntax error) and working truffle test
|
||||
mock.install('SimpleError.sol', 'simple.js'); |
||||
shell.exec(script); |
||||
assert(shell.error() !== null, 'script should error'); |
||||
assert(pathExists('./coverage') !== true, 'script should NOT gen coverage folder'); |
||||
assert(pathExists('./coverage.json') !== true, 'script should NOT gen coverage.json'); |
||||
collectGarbage(); |
||||
}); |
||||
|
||||
it('no events log produced: should generate NO coverage, cleanup and exit(1)', () => { |
||||
// Run contract and test that pass but fire no events
|
||||
assert(pathExists('./coverage') === false, 'should start without: coverage'); |
||||
assert(pathExists('./coverage.json') === false, 'should start without: coverage.json'); |
||||
mock.install('Empty.sol', 'empty.js'); |
||||
shell.exec(script); |
||||
assert(shell.error() !== null, 'script should error'); |
||||
assert(pathExists('./coverage') !== true, 'script should NOT gen coverage folder'); |
||||
assert(pathExists('./coverage.json') !== true, 'script should NOT gen coverage.json'); |
||||
collectGarbage(); |
||||
}); |
||||
}); |
@ -0,0 +1,6 @@ |
||||
|
||||
const Expensive = artifacts.require('./Expensive.sol'); |
||||
|
||||
contract('Expensive', accounts => { |
||||
it('should deploy', () => Expensive.deployed()); |
||||
}); |
@ -0,0 +1,6 @@ |
||||
|
||||
const Empty = artifacts.require('./Empty.sol'); |
||||
|
||||
contract('Empty', accounts => { |
||||
it('should deploy', () => Empty.deployed()); |
||||
}); |
@ -0,0 +1,10 @@ |
||||
|
||||
const OnlyCall = artifacts.require('./OnlyCall.sol'); |
||||
|
||||
contract('OnlyCall', accounts => { |
||||
it('should return 5', () =>
|
||||
OnlyCall.deployed() |
||||
.then(instance => instance.getFive.call()) |
||||
.then(val => assert.equal(val, 5)) |
||||
); |
||||
}); |
@ -0,0 +1,14 @@ |
||||
|
||||
const Simple = artifacts.require('./Simple.sol'); |
||||
|
||||
contract('Simple', accounts => { |
||||
it('should set x to 5', () => { |
||||
let simple; |
||||
return Simple.deployed().then(instance => { |
||||
simple = instance; |
||||
return simple.test(5); |
||||
}) |
||||
.then(val => simple.getX.call()) |
||||
.then(val => assert.equal(val.toNumber(), 5)); |
||||
}); |
||||
}); |
@ -0,0 +1,16 @@ |
||||
|
||||
|
||||
const Simple = artifacts.require('./Simple.sol'); |
||||
|
||||
// This test is constructed correctly but the SimpleError.sol has a syntax error
|
||||
contract('SimpleError', accounts => { |
||||
it('should set x to 5', () => { |
||||
let simple; |
||||
return Simple.deployed().then(instance => { |
||||
simple = instance; |
||||
return simple.test(5); |
||||
}) |
||||
.then(val => simple.getX.call()) |
||||
.then(val => assert.equal(val, 5)); |
||||
}); |
||||
}); |
@ -0,0 +1,10 @@ |
||||
'use strict' |
||||
|
||||
var Simple = artifacts.require('./Simple.sol'); |
||||
|
||||
// This test should break truffle because it has a syntax error.
|
||||
contract('Simple', function(accounts){ |
||||
it('should crash', function(){ |
||||
return Simple.deployed().then.why. |
||||
}) |
||||
}) |
@ -0,0 +1,15 @@ |
||||
|
||||
|
||||
const Simple = artifacts.require('./Simple.sol'); |
||||
|
||||
contract('Simple', accounts => { |
||||
it('should set x to 5', () => { |
||||
let simple; |
||||
return Simple.deployed().then(instance => { |
||||
simple = instance; |
||||
return simple.test(5); |
||||
}) |
||||
.then(val => simple.getX.call()) |
||||
.then(val => assert.equal(val.toNumber(), 4)); // <-- Wrong result: test fails
|
||||
}); |
||||
}); |
@ -0,0 +1,4 @@ |
||||
pragma solidity ^0.4.3; |
||||
|
||||
contract Empty { |
||||
} |
@ -0,0 +1,15 @@ |
||||
// Cost to deploy Expensive: 0x4e042f |
||||
// Block gas limit is: 0x47e7c4 |
||||
// Should throw out of gas on unmodified truffle |
||||
// Should pass solcover truffle |
||||
pragma solidity ^0.4.2; |
||||
|
||||
contract Expensive { |
||||
mapping (uint => address) map; |
||||
|
||||
function Expensive(){ |
||||
for(uint i = 0; i < 250; i++ ){ |
||||
map[i] = address(this); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,20 @@ |
||||
pragma solidity ^0.4.4; |
||||
contract Migrations { |
||||
address public owner; |
||||
uint public last_completed_migration; |
||||
|
||||
modifier restricted() {if (msg.sender == owner) _;} |
||||
|
||||
function Migrations() { |
||||
owner = msg.sender; |
||||
} |
||||
|
||||
function setCompleted(uint completed) restricted { |
||||
last_completed_migration = completed; |
||||
} |
||||
|
||||
function upgrade(address new_address) restricted { |
||||
Migrations upgraded = Migrations(new_address); |
||||
upgraded.setCompleted(last_completed_migration); |
||||
} |
||||
} |
@ -0,0 +1,11 @@ |
||||
/** |
||||
* This contract contains a single function that is accessed using method.call |
||||
* With an unpatched testrpc it should not generate any events. |
||||
*/ |
||||
pragma solidity ^0.4.3; |
||||
|
||||
contract OnlyCall { |
||||
function getFive() returns (uint){ |
||||
return 5; |
||||
} |
||||
} |
@ -0,0 +1,13 @@ |
||||
pragma solidity ^0.4.3; |
||||
|
||||
contract Simple { |
||||
uint x = 0; |
||||
|
||||
function test(uint val) { |
||||
x = x + val; |
||||
} |
||||
|
||||
function getX() returns (uint){ |
||||
return x; |
||||
} |
||||
} |
@ -0,0 +1,14 @@ |
||||
// This contract should throw a parse error in instrumentSolidity.js |
||||
pragma solidity ^0.4.3; |
||||
|
||||
contract SimpleError { |
||||
uint x = 0; |
||||
|
||||
function test(uint val) { |
||||
x = x + val // <-- no semi-colon |
||||
} |
||||
|
||||
function getX() returns (uint){ |
||||
return x; |
||||
} |
||||
} |
@ -0,0 +1,93 @@ |
||||
|
||||
/* |
||||
This file contains utilities for generating a mock truffle project to test solcover's |
||||
run script against. |
||||
*/ |
||||
const assert = require('assert'); |
||||
const fs = require('fs'); |
||||
const shell = require('shelljs'); |
||||
|
||||
/** |
||||
* Moves existing coverage reports into a safe place while testing run script which |
||||
* would overwrite them. Silences shell complaints about non-existent files. |
||||
*/ |
||||
module.exports.protectCoverage = function () { |
||||
shell.config.silent = true; |
||||
shell.rm('-Rf', './safe'); |
||||
shell.mkdir('./safe'); |
||||
shell.mv('./coverage', './safe/coverage'); |
||||
shell.mv('./coverage.json', './safe/coverage.json'); |
||||
shell.config.silent = false; |
||||
}; |
||||
|
||||
/** |
||||
* Restores pre-existing coverage reports after testing run script. |
||||
* Silences shell complaints about non-existent files. |
||||
*/ |
||||
module.exports.restoreCoverage = function () { |
||||
shell.config.silent = true; |
||||
shell.mv('./safe/coverage', './coverage'); |
||||
shell.mv('./safe/coverage.json', './coverage.json'); |
||||
shell.rm('-Rf', './safe'); |
||||
shell.config.silent = false; |
||||
}; |
||||
|
||||
/** |
||||
* Installs mock truffle project at ./mock with a single contract |
||||
* and test specified by the params. |
||||
* @param {String} contract <contractName.sol> located in /test/sources/run/ |
||||
* @param {[type]} test <testName.js> located in /test/run/ |
||||
*/ |
||||
module.exports.install = function (contract, test) { |
||||
shell.mkdir('./mock'); |
||||
shell.mkdir('./mock/contracts'); |
||||
shell.mkdir('./mock/migrations'); |
||||
shell.mkdir('./mock/test'); |
||||
|
||||
// Mock contracts
|
||||
shell.cp(`./test/sources/run/${contract}`, `./mock/contracts/${contract}`); |
||||
shell.cp('./test/sources/run/Migrations.sol', './mock/contracts/Migrations.sol'); |
||||
|
||||
// Mock migrations
|
||||
const initialMigration = ` |
||||
let Migrations = artifacts.require('Migrations.sol'); |
||||
module.exports = function(deployer) { |
||||
deployer.deploy(Migrations); |
||||
};`;
|
||||
|
||||
const contractLocation = './' + contract; |
||||
const deployContracts = ` |
||||
var contract = artifacts.require('${contractLocation}'); |
||||
module.exports = function(deployer) { |
||||
deployer.deploy(contract); |
||||
};`;
|
||||
|
||||
fs.writeFileSync('./mock/migrations/1_initial_migration.js', initialMigration); |
||||
fs.writeFileSync('./mock/migrations/2_deploy_contracts.js', deployContracts); |
||||
|
||||
// Mock test
|
||||
shell.cp(`./test/run/${test}`, `./mock/test/${test}`); |
||||
|
||||
// Mock truffle.js
|
||||
const trufflejs = `module.exports = {
|
||||
networks: { |
||||
development: { |
||||
host: "localhost",
|
||||
port: 8545, |
||||
network_id: "*" |
||||
}}};` |
||||
; |
||||
|
||||
fs.writeFileSync('./mock/truffle.js', trufflejs); |
||||
}; |
||||
|
||||
/** |
||||
* Removes mock truffle project and coverage reports generated by runCovered tests |
||||
*/ |
||||
module.exports.remove = function () { |
||||
shell.config.silent = true; |
||||
shell.rm('-Rf', 'mock'); |
||||
shell.rm('-Rf', 'coverage'); |
||||
shell.rm('coverage.json'); |
||||
shell.config.silent = false; |
||||
}; |
Loading…
Reference in new issue