Working truffle plugin draft w/ integration tests

truffle-plugin
cgewecke 5 years ago
parent e52a3ab59f
commit c8de349fec
  1. 1
      .gitignore
  2. 119
      dist/truffle.plugin.js
  3. 233
      lib/app.js
  4. 30
      lib/collector.js
  5. 40
      lib/coverage.js
  6. 2
      lib/instrumenter.js
  7. 9
      package.json
  8. 3
      temp/.solcover.js
  9. 14
      temp/assets/SimpleError.sol
  10. 1
      temp/assets/asset.js
  11. 25
      temp/contracts/Migrations.sol
  12. 0
      temp/contracts/SimpleError.sol
  13. 4
      temp/migrations/1_initial.js
  14. 4
      temp/migrations/2_deploy.js
  15. 10
      temp/test/simple.js
  16. 86
      temp/truffle-config.js
  17. 2
      test/integration/truffle/contracts/Migrations.sol
  18. 4
      test/integration/truffle/migrations/1_initial.js
  19. 5
      test/sources/js/block-gas-limit.js
  20. 7
      test/sources/js/oraclize.js
  21. 17
      test/sources/js/requires-externally.js
  22. 31
      test/sources/js/sign.js
  23. 15
      test/sources/js/simple.js
  24. 11
      test/sources/js/testrpc-options.js
  25. 0
      test/sources/solidity/contracts/app/Oraclize.sol
  26. 6
      test/sources/solidity/contracts/app/SimpleError.sol
  27. 286
      test/units/app.js
  28. 25
      test/units/assert.js
  29. 22
      test/units/function.js
  30. 2
      test/units/if.js
  31. 2
      test/units/loops.js
  32. 4
      test/units/statements.js
  33. 172
      test/util/integration.truffle.js
  34. 121
      test/util/mock.truffle.js
  35. 74
      test/util/util.js
  36. 4746
      yarn.lock

1
.gitignore vendored

@ -5,3 +5,4 @@ node_modules/
.DS_Store
test/artifacts
test/cache
yarn.lock

@ -27,71 +27,128 @@
const App = require('./../lib/app');
const req = require('req-cwd');
module.exports = async (truffleConfig) =>
const path = require('path');
const dir = require('node-dir');
const Web3 = require('web3');
const util = require('util');
const ganache = require('ganache-core-sc');
module.exports = async function(truffleConfig){
let app;
let error;
let testsErrored = false;
try {
// Load truffle lib & coverage config
const truffle = loadTruffleLibrary();
const coverageConfig = req.silent('./.solcover.js') || {};
const coverageConfigPath = path.join(truffleConfig.working_directory, '.solcover.js');
const coverageConfig = req.silent(coverageConfigPath) || {};
coverageConfig.cwd = truffleConfig.working_directory;
coverageConfig.contractsDir = truffleConfig.contracts_directory;
// Start
const app = new App(coverageConfig);
app = new App(coverageConfig);
// Write instrumented sources to temp folder
app.contractsDirectory = coveragePaths.contracts(truffleConfig, app);
app.generateEnvironment(truffleConfig.contracts_directory, app.contractsDirectory);
app.instrument();
// Have truffle use temp folders
truffleConfig.contracts_directory = app.contractsDirectory;
truffleConfig.build_directory = coveragePaths.artifacts.root(truffleConfig, app);
truffleConfig.contracts_build_directory = coveragePaths.artifacts.contracts(truffleConfig, app);
// Ask truffle to use temp folders
truffleConfig.contracts_directory = paths.contracts(app);
truffleConfig.build_directory = paths.build(app);
truffleConfig.contracts_build_directory = paths.artifacts(truffleConfig, app);
// Compile w/out optimization
truffleConfig.compilers.solc.settings.optimization.enabled = false;
// Additional config
truffleConfig.all = true;
truffleConfig.test_files = tests(truffleConfig);
truffleConfig.compilers.solc.settings.optimizer.enabled = false;
// Compile
await truffle.contracts.compile(truffleConfig);
// Launch provider & run tests
truffleConfig.provider = await app.getCoverageProvider(truffle);
// Launch in-process provider
const networkName = 'soliditycoverage';
const provider = await app.provider(ganache);
const accounts = await (new Web3(provider)).eth.getAccounts();
truffleConfig.provider = provider;
truffleConfig.network = networkName;
truffleConfig.network_id = "*";
truffleConfig.networks[networkName] = {
network_id: truffleConfig.network_id,
provider: truffleConfig.provider,
gas: app.gasLimit,
gasPrice: app.gasPrice,
from: accounts[0]
}
// Run tests
try {
await truffle.test.run(truffleConfig)
failures = await truffle.test.run(truffleConfig)
} catch (e) {
error = e;
app.testsErrored = true;
error = e.stack;
}
// Produce report
app.generateCoverage();
// Run Istanbul
await app.report();
} catch(e){
error = e;
}
// Finish
return app.cleanUp(error);
await app.cleanUp();
if (error !== undefined) throw new Error(error)
if (failures > 0) throw new Error(`${failures} test(s) failed under coverage.`)
}
// -------------------------------------- Helpers --------------------------------------------------
function tests(truffle){
const regex = /.*\.(js|ts|es|es6|jsx|sol)$/;
const files = dir.files(truffle.test_directory, { sync: true }) || [];
return files.filter(f => f.match(regex) != null);
}
function loadTruffleLibrary(){
try { return require("truffle") } catch(err) {};
try { return require("./truffle.library")} catch(err) {};
throw new Error(utils.errors.NO_TRUFFLE_LIB)
throw new Error('Missing truffle lib...')
}
const coveragePaths = {
contracts: (t, c) => path.join(path.dirname(t.contracts_directory), c.contractsDirName)),
artifacts: {
root: (t, c) => path.join(path.dirname(t.build_directory), c.artifactsDirName),
contracts: (t, c) => {
const root = path.join(path.dirname(t.build_directory), c.artifactsDirName);
return path.join(root, path.basename(t.contracts_build_directory));
}
/**
* Functions to generate substitute paths for instrumented contracts and artifacts.
* @type {Object}
*/
const paths = {
// "contracts_directory":
contracts: (app) => {
return path.join(
app.coverageDir,
app.contractsDirName
)
},
// "build_directory":
build: (app) => {
return path.join(
app.coverageDir,
app.artifactsDirName
)
},
// "contracts_build_directory":
artifacts: (truffle, app) => {
return path.join(
app.coverageDir,
app.artifactsDirName,
path.basename(truffle.contracts_build_directory)
)
}
}

@ -2,15 +2,14 @@ const shell = require('shelljs');
const fs = require('fs');
const path = require('path');
const istanbul = require('istanbul');
const util = require('util');
const Instrumenter = require('./instrumenter');
const Coverage = require('./coverage');
const DataCollector = require('./collector');
const isWin = /^win/.test(process.platform);
const gasLimitHex = 0xfffffffffff; // High gas block limit / contract deployment limit
const gasPriceHex = 0x01; // Low gas price
/**
* Coverage Runner
*/
@ -18,73 +17,58 @@ class App {
constructor(config) {
this.coverage = new Coverage();
this.instrumenter = new Instrumenter();
this.provider = config.provider;
this.config = config || {};
// Options
this.silence = ''; // Default log level (passed to shell)
this.log = config.logger || console.log; // Configurable logging
this.testsErrored = false;
this.cwd = config.cwd;
this.tempFolderName = '.coverageEnv';
this.contractsDir = config.contractsDir
this.coverageDir = path.join(this.cwd, this.tempFolderName);
this.contractsDirName = 'contracts';
this.artifactsDirName = 'artifacts';
this.client = config.provider;
this.providerOptions = config.providerOptions || {};
// Other
this.testsErrored = false; // Toggle true when tests error
this.skippedFolders = [];
this.skipFiles = config.skipFiles || [];
// Config
this.config = config || {};
this.contractsDir = config.contractsDir || 'contracts';
this.coverageDir = './.coverageEnv'; // Contracts dir of instrumented .sols
this.workingDir = config.dir || '.'; // Relative path to contracts folder
this.skipFiles = config.skipFiles || []; // Files to exclude from instrumentation
this.log = config.logger || console.log;
this.setLoggingLevel(config.silent);
}
// -------------------------------------- Methods ------------------------------------------------
this.gasLimit = 0xfffffffffff;
this.gasLimitString = "0xfffffffffff";
this.gasPrice = 0x01;
}
// -------------------------------------- Methods -----------------------------------------------
/**
* Generates a copy of the target project configured for solidity-coverage and saves to
* the coverage environment folder.
* Setup temp folder, write instrumented contracts to it and register them as coverage targets
*/
generateCoverageEnvironment() {
this.log('Generating coverage environment');
instrument() {
let currentFile;
try {
this.sanityCheckContext();
this.identifySkippedFolders();
shell.mkdir(this.coverageDir);
this.registerSkippedItems();
this.generateEnvelope();
shell.cp('-Rf', this.contractsDir, this.coverageDir)
const target = `${this.coverageDir}/**/*.sol`;
} catch (err) {
const msg = ('There was a problem generating the coverage environment: ');
this.cleanUp(msg + err);
}
}
/**
* For each contract except migrations.sol (or those in skipFiles):
* + Generate file path reference for coverage report
* + Load contract as string
* + Instrument contract
* + Save instrumented contract to a temp folder which will be the new 'contractsDir for tests'
* + Add instrumentation info to the coverage map
*/
instrumentTarget() {
this.skipFiles = this.skipFiles.map(contract => `${this.coverageDir}/${contract}`);
this.skipFiles.push(`${this.coverageDir}/Migrations.sol`);
let currentFile;
try {
shell.ls(`${this.coverageDir}/**/*.sol`).forEach(file => {
shell.ls(target).forEach(file => {
currentFile = file;
if (!this.skipFiles.includes(file) && !this.inSkippedFolder(file)) {
if (!this.shouldSkip(file)) {
this.log('Instrumenting ', file);
// Remember the real path
const contractPath = this.platformNeutralPath(file);
const working = this.workingDir.substring(1);
const canonicalPath = contractPath.split('/coverageEnv').join(working);
const canonicalPath = path.join(
this.cwd,
contractPath.split(`/${this.tempFolderName}`)[1]
);
// Instrument contract, save, add to coverage map
const contract = this.loadContract(contractPath);
@ -100,26 +84,39 @@ class App {
const msg = `There was a problem instrumenting ${currentFile}: `;
this.cleanUp(msg + err);
}
}
/**
* Launch an in-process ethereum provider and hook up the DataCollector to its VM.
* @param {Object} client ethereum client
* @return {Object} provider
*
* TODO: generalize provider options setting for non-ganache clients..
*/
provider(client){
if(!this.client) this.client = client;
this.collector = new DataCollector(this.instrumenter.instrumentationData);
this.collector = new DataCollector(
this.provider,
this.instrumenter.intrumentationData
)
this.providerOptions.gasLimit = this.gasLimitString;
this.providerOptions.allowUnlimitedContractSize = true;
this.providerOptions.logger = { log: this.collector.step.bind(this.collector) };
this.provider = this.client.provider(this.providerOptions);
return this.provider;
}
/**
* Generate coverage / write coverage report / run istanbul
*/
async generateReport() {
async report() {
const collector = new istanbul.Collector();
const reporter = new istanbul.Reporter();
return new Promise((resolve, reject) => {
try {
const contractsPath = `${this.workingDir}/${this.config.contractsDir}`
this.coverage.generate(this.instrumenter.instrumentationData, contractsPath);
const relativeMapping = this.makeKeysRelative(this.coverage.data, this.workingDir);
this.coverage.generate(this.instrumenter.instrumentationData, this.contractsDir);
const relativeMapping = this.makeKeysRelative(this.coverage.data, this.cwd);
this.saveCoverage(relativeMapping);
collector.add(relativeMapping);
@ -129,18 +126,39 @@ class App {
reporter.write(collector, true, () => {
this.log('Istanbul coverage reports generated');
this.cleanUp();
resolve();
});
} catch (err) {
const msg = 'There was a problem generating the coverage map / running Istanbul.\n';
console.log(err.stack);
this.cleanUp(msg + err);
throw new Error(msg + err);
}
});
}
/**
* Removes coverage build artifacts, kills testrpc.
* Exits (1) and prints msg on error, exits (0) otherwise.
* @param {String} err error message
*
* TODO this needs to delegate process exit to the outer tool....
*/
async cleanUp(err) {
const self = this;
this.log('Cleaning up...');
shell.config.silent = true;
shell.rm('-Rf', this.coverageDir);
if (this.provider && this.provider.close){
this.log('Shutting down ganache-core')
return new Promise(res => self.provider.close(res))
}
}
// ------------------------------------------ Utils ----------------------------------------------
// ========
// File I/O
// ========
loadContract(_path){
return fs.readFileSync(_path).toString();
}
@ -149,29 +167,41 @@ class App {
fs.writeFileSync(_path, contract);
}
saveCoverage(coverageObject){
fs.writeFileSync('./coverage.json', JSON.stringify(coverageObject));
saveCoverage(data){
fs.writeFileSync('./coverage.json', JSON.stringify(data));
}
sanityCheckContext(){
if (!shell.test('-e', `${this.workingDir}/contracts`)){
// ======
// Launch
// ======
sanityCheckContext(contractsDir){
if (!shell.test('-e', this.contractsDir)){
this.cleanUp("Couldn't find a 'contracts' folder to instrument.");
}
if (shell.test('-e', `${this.workingDir}/${this.coverageDir}`)){
if (shell.test('-e', path.join(this.cwd, this.coverageDir))){
shell.rm('-Rf', this.coverageDir);
}
}
generateEnvelope(){
shell.mkdir(this.coverageDir);
shell.mkdir(path.join(this.coverageDir, this.artifactsDirName))
shell.cp('-Rf', this.contractsDir, this.coverageDir)
}
// =====
// Paths
// =====
/**
* Relativizes path keys so that istanbul report can be read on Windows
* @param {Object} map coverage map generated by coverageMap
* @param {[type]} root working directory
* @return {[type]} map with relativized keys
* @param {String} wd working directory
* @return {Object} map with relativized keys
*/
makeKeysRelative(map, root) {
makeKeysRelative(map, wd) {
const newCoverage = {};
Object.keys(map).forEach(pathKey => newCoverage[path.relative(root, pathKey)] = map[pathKey]);
Object.keys(map).forEach(pathKey => newCoverage[path.relative(wd, pathKey)] = map[pathKey]);
return newCoverage;
}
@ -186,6 +216,9 @@ class App {
: path.resolve(file);
}
// ========
// Skipping
// ========
/**
* Determines if a file is in a folder marked skippable.
* @param {String} file file path
@ -193,8 +226,9 @@ class App {
*/
inSkippedFolder(file){
let shouldSkip;
const root = `${this.coverageDir}/${this.contractsDirName}`;
this.skippedFolders.forEach(folderToSkip => {
folderToSkip = `${this.coverageDir}/contracts/${folderToSkip}`;
folderToSkip = `${root}/${folderToSkip}`;
if (file.indexOf(folderToSkip) === 0)
shouldSkip = true;
});
@ -202,57 +236,38 @@ class App {
}
/**
* Helper for parsing the skipFiles option, which also accepts folders.
* Parses the skipFiles option (which also accepts folders)
*/
identifySkippedFolders(){
let files = shell.ls('-A', this.workingDir);
this.skipFiles.forEach(item => {
if (path.extname(item) !== '.sol')
this.skippedFolders.push(item);
});
registerSkippedItems(){
const root = `${this.coverageDir}/${this.contractsDirName}`;
this.skippedFolders = this.skipFiles.filter(item => path.extname(item) !== '.sol')
this.skipFiles = this.skipFiles.map(contract => `${root}/${contract}`);
this.skipFiles.push(`${root}/Migrations.sol`);
}
/**
* Allows config to turn logging off (for CI)
* @param {Boolean} isSilent
* Returns true when file should not be instrumented, false otherwise
* @param {String} file path segment
* @return {Boolean}
*/
setLoggingLevel(isSilent) {
if (isSilent) {
this.silence = '> /dev/null 2>&1';
this.log = () => {};
}
shouldSkip(file){
return this.skipFiles.includes(file) || this.inSkippedFolder(file)
}
// =======
// Logging
// =======
/**
* Removes coverage build artifacts, kills testrpc.
* Exits (1) and prints msg on error, exits (0) otherwise.
* @param {String} err error message
* Turn logging off (for CI)
* @param {Boolean} isSilent
*
* TODO: logic to toggle on/off (instead of just off)
*
* TODO this needs to delegate process exit to the outer tool....
*/
cleanUp(err) {
const self = this;
function exit(err){
if (err) {
self.log(`${err}\nExiting without generating coverage...`);
process.exit(1);
} else if (self.testsErrored) {
self.log('Some truffle tests failed while running coverage');
process.exit(1);
} else {
self.log('Done.');
process.exit(0);
}
setLoggingLevel(isSilent) {
if (isSilent) this.log = () => {};
}
self.log('Cleaning up...');
shell.config.silent = true;
shell.rm('-Rf', self.coverageDir);
exit(err);
}
}
module.exports = App;

@ -1,39 +1,37 @@
const web3Utils = require('web3-utils')
class DataCollector {
constructor(config={}, instrumentationData={}){
constructor(instrumentationData={}){
this.instrumentationData = instrumentationData;
this.vm = config.vmResolver
? config.vmResolver(config.provider)
: this._ganacheVMResolver(config.provider);
this._connect();
}
// Subscribes to vm.on('step').
_connect(){
step(info){
const self = this;
this.vm.on("step", function(info){
if (typeof info !== 'object' || !info.opcode ) return;
if (info.opcode.name.includes("PUSH") && info.stack.length > 0){
const idx = info.stack.length - 1;
const hash = web3Utils.toHex(info.stack[idx]).toString();
let hash = web3Utils.toHex(info.stack[idx]).toString();
hash = self._normalizeHash(hash);
if(self.instrumentationData[hash]){
self.instrumentationData[hash].hits++;
}
}
})
}
_ganacheVMResolver(provider){
return provider.engine.manager.state.blockchain.vm;
}
_setInstrumentationData(data){
this.instrumentationData = data;
}
_normalizeHash(hash){
if (hash.length < 66 && hash.length > 52){
hash = hash.slice(2);
while(hash.length < 64) hash = '0' + hash;
hash = '0x' + hash
}
return hash;
}
}
module.exports = DataCollector;

@ -10,27 +10,19 @@ class Coverage {
constructor() {
this.data = {};
this.assertData = {};
this.lineTopics = [];
this.functionTopics = [];
this.branchTopics = [];
this.statementTopics = [];
this.assertPreTopics = [];
this.assertPostTopics = [];
}
/**
* Initializes a coverage map object for contract
* + instrumented per `info`
* + located at `canonicalContractPath`
* @param {Object} info `info = getIntrumentedVersion(contract, fileName, true)`
* @param {String} canonicalContractPath path to contract file
* @return {Object} coverage map with all values set to zero
* Initializes an entry in the coverage map for an instrumented contract. Tracks by
* its canonical contract path, e.g. *not* by its location in the temp folder.
* @param {Object} info 'info = instrumenter.instrument(contract, fileName, true)'
* @param {String} contractPath canonical path to contract file
*/
addContract(info, canonicalContractPath) {
this.data[canonicalContractPath] = {
addContract(info, contractPath) {
this.data[contractPath] = {
l: {},
path: canonicalContractPath,
path: contractPath,
s: {},
b: {},
f: {},
@ -38,29 +30,29 @@ class Coverage {
statementMap: {},
branchMap: {},
};
this.assertData[canonicalContractPath] = { };
this.assertData[contractPath] = { };
info.runnableLines.forEach((item, idx) => {
this.data[canonicalContractPath].l[info.runnableLines[idx]] = 0;
this.data[contractPath].l[info.runnableLines[idx]] = 0;
});
this.data[canonicalContractPath].fnMap = info.fnMap;
this.data[contractPath].fnMap = info.fnMap;
for (let x = 1; x <= Object.keys(info.fnMap).length; x++) {
this.data[canonicalContractPath].f[x] = 0;
this.data[contractPath].f[x] = 0;
}
this.data[canonicalContractPath].branchMap = info.branchMap;
this.data[contractPath].branchMap = info.branchMap;
for (let x = 1; x <= Object.keys(info.branchMap).length; x++) {
this.data[canonicalContractPath].b[x] = [0, 0];
this.assertData[canonicalContractPath][x] = {
this.data[contractPath].b[x] = [0, 0];
this.assertData[contractPath][x] = {
preEvents: 0,
postEvents: 0,
};
}
this.data[canonicalContractPath].statementMap = info.statementMap;
this.data[contractPath].statementMap = info.statementMap;
for (let x = 1; x <= Object.keys(info.statementMap).length; x++) {
this.data[canonicalContractPath].s[x] = 0;
this.data[contractPath].s[x] = 0;
}
}

@ -62,7 +62,7 @@ class Instrumenter {
// First, we run over the original contract to get the source mapping.
let ast = SolidityParser.parse(contract.source, {range: true});
parse[ast.type](contract, ast);
const retValue = JSON.parse(JSON.stringify(contract)); // ?????
const retValue = JSON.parse(JSON.stringify(contract)); // Possibly apotropaic.
// Now, we reset almost everything and use the preprocessor to increase our effectiveness.
this._initializeCoverageFields(contract);

@ -8,7 +8,7 @@
"test": "test"
},
"scripts": {
"test": "mocha test/units --timeout 70000 --no-warnings",
"test": "mocha test/units --timeout 70000 --no-warnings --exit",
"test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- test/units --timeout 70000 --exit"
},
"homepage": "https://github.com/sc-forks/solidity-coverage",
@ -20,21 +20,24 @@
"license": "ISC",
"dependencies": {
"death": "^1.1.0",
"ganache-cli": "^6.5.0",
"ganache-core-sc": "2.7.0-sc.0",
"istanbul": "^0.4.5",
"node-dir": "^0.1.17",
"req-cwd": "^1.0.1",
"sha1": "^1.1.1",
"shelljs": "^0.8.3",
"solidity-parser-antlr": "^0.4.7",
"web3": "1.2.1",
"web3-utils": "^1.0.0"
},
"devDependencies": {
"@nomiclabs/buidler": "^1.0.0-beta.8",
"@nomiclabs/buidler-truffle5": "^1.0.0-beta.8",
"@nomiclabs/buidler-web3": "^1.0.0-beta.8",
"decache": "^4.5.1",
"mocha": "5.2.0",
"solc": "^0.5.10",
"truffle": "^5.0.31",
"web3": "1.0.0-beta.37"
"truffle-config": "^1.1.18"
}
}

@ -0,0 +1,3 @@
module.exports = {
"silent": true
}

@ -0,0 +1,14 @@
// This contract should throw a parse error in instrumentSolidity.js
pragma solidity ^0.5.0;
contract SimpleError {
uint x = 0;
function test(uint val) public {
x = x + val // <-- no semi-colon
}
function getX() public returns (uint){
return x;
}
}

@ -0,0 +1 @@
module.exports = { value: true };

@ -0,0 +1,25 @@
pragma solidity >=0.4.22 <0.6.0;
contract Migrations {
address public owner;
uint public last_completed_migration;
modifier restricted() {
if (msg.sender == owner) { _; }
}
constructor() public {
owner = msg.sender;
}
function setCompleted(uint completed) public restricted {
last_completed_migration = completed;
}
function upgrade(address new_address) public restricted {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
}
}

@ -0,0 +1,4 @@
const Migrations = artifacts.require('./Migrations.sol');
module.exports = async function(deployer) {
await deployer.deploy(Migrations);
};

@ -0,0 +1,4 @@
const A = artifacts.require("SimpleError");
module.exports = function(deployer) { deployer.deploy(A) };

@ -0,0 +1,10 @@
const Simple = artifacts.require('Simple');
contract('Simple', () => {
it('should set x to 5', async function(){
const simple = await Simple.deployed()
await simple.test(5);
const val = await simple.getX.call();
assert.equal(val.toNumber(), 5);
});
});

@ -0,0 +1,86 @@
module.exports = {
"_deepCopy": [
"compilers"
],
"_values": {
"truffle_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/node_modules",
"working_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage",
"networks": {},
"verboseRpc": false,
"gas": null,
"gasPrice": null,
"from": null,
"confirmations": 0,
"timeoutBlocks": 0,
"production": false,
"skipDryRun": false,
"build": null,
"resolver": null,
"artifactor": null,
"ethpm": {
"ipfs_host": "ipfs.infura.io",
"ipfs_protocol": "https",
"registry": "0x8011df4830b4f696cd81393997e5371b93338878",
"install_provider_uri": "https://ropsten.infura.io/truffle"
},
"compilers": {
"solc": {
"settings": {
"optimizer": {
"enabled": false,
"runs": 200
}
}
},
"vyper": {}
},
"logger": {}
},
"truffle_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/node_modules",
"working_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/temp",
"networks": {
"development": {
"host": "localhost",
"port": 8545,
"network_id": "*"
}
},
"verboseRpc": false,
"build": null,
"resolver": null,
"artifactor": null,
"ethpm": {
"ipfs_host": "ipfs.infura.io",
"ipfs_protocol": "https",
"registry": "0x8011df4830b4f696cd81393997e5371b93338878",
"install_provider_uri": "https://ropsten.infura.io/truffle"
},
"logger": {},
"compilers": {
"solc": {
"version": "0.5.3",
"settings": {
"optimizer": {}
}
}
},
"build_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/temp/build",
"contracts_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/temp/contracts",
"contracts_build_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/temp/build/contracts",
"migrations_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/temp/migrations",
"migrations_file_extension_regexp": {},
"test_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/temp/test",
"test_file_extension_regexp": {},
"example_project_directory": "/Users/cgewecke/code/sc-forks/v2/solidity-coverage/node_modules/example",
"network_id": null,
"from": null,
"gas": 6721975,
"gasPrice": 20000000000,
"provider": null,
"confirmations": 0,
"production": false,
"timeoutBlocks": 0,
"mocha": {
"reporter": "dot"
}
}

@ -1,4 +1,4 @@
pragma solidity ^0.5.0;
pragma solidity >=0.4.22 <0.6.0;
contract Migrations {

@ -0,0 +1,4 @@
const Migrations = artifacts.require('./Migrations.sol');
module.exports = async function(deployer) {
await deployer.deploy(Migrations);
};

@ -1,9 +1,10 @@
/* eslint-env node, mocha */
/* global artifacts, contract */
const Expensive = artifacts.require('./Expensive.sol');
contract('Expensive', () => {
it('should deploy', async () => {
const instance = await Expensive.new()
const hash = instance.transactionHash;
const receipt = await web3.eth.getTransactionReceipt(hash);
assert(receipt.gasUsed > 20000000)
});
});

@ -1,10 +1,9 @@
/* eslint-env node, mocha */
/* global artifacts, contract, assert */
const usingOraclize = artifacts.require('usingOraclize');
contract('Nothing', () => {
it('nothing', async () => {
contract('Oraclize', function(accounts){
it('oraclize', async function(){
const ora = await usingOraclize.new();
await ora.test();
});
});

@ -1,17 +0,0 @@
/* eslint-env node, mocha */
/* global artifacts, contract, assert */
const asset = require('../assets/asset.js');
const Simple = artifacts.require('./Simple.sol');
contract('Simple', () => {
it('should be able to require an external asset', () => {
let simple;
return Simple.deployed().then(instance => {
simple = instance;
assert.equal(asset.value, true);
return simple.test(5); // Make sure we generate an event;
});
});
});

@ -1,31 +0,0 @@
/* eslint-env node, mocha */
/* global artifacts, contract, assert */
const ethUtil = require('ethereumjs-util');
const Simple = artifacts.require('./Simple.sol');
contract('Simple', accounts => {
it('should set x to 5', () => {
let simple;
let messageSha3;
return Simple.deployed()
.then(instance => instance.test(5)) // We need this line to generate some coverage
.then(() => {
const message = 'Enclosed is my formal application for permanent residency in New Zealand';
messageSha3 = web3.utils.sha3(message);
const signature = web3.eth.sign(messageSha3, accounts[0]);
return signature;
})
.then((signature) => {
const messageBuffer = new Buffer(messageSha3.replace('0x', ''), 'hex');
const messagePersonalHash = ethUtil.hashPersonalMessage(messageBuffer);
const sigParams = ethUtil.fromRpcSig(signature);
const publicKey = ethUtil.ecrecover(messagePersonalHash, sigParams.v, sigParams.r, sigParams.s);
const senderBuffer = ethUtil.pubToAddress(publicKey);
const sender = ethUtil.bufferToHex(senderBuffer);
assert.equal(sender, accounts[0].toLowerCase());
});
});
});

@ -1,13 +1,10 @@
const Simple = artifacts.require('./Simple.sol');
const Simple = artifacts.require('Simple');
contract('Simple', () => {
it('should set x to 5', () => {
let simple;
return Simple.deployed().then(instance => {
simple = instance;
return simple.test(5);
})
.then(() => simple.getX.call())
.then(val => assert.equal(val.toNumber(), 5));
it('should set x to 5', async function(){
const simple = await Simple.deployed()
await simple.test(5);
const val = await simple.getX.call();
assert.equal(val.toNumber(), 5);
});
});

@ -1,12 +1,11 @@
/* eslint-env node, mocha */
/* global artifacts, contract, assert */
const Simple = artifacts.require('./Simple.sol');
contract('Simple', accounts => {
// Crash truffle if the account loaded in the options string isn't found here.
it('should load with expected account', () => {
assert(accounts[0] === '0xA4860CEDd5143Bd63F347CaB453Bf91425f8404f');
it('should load with ~ expected balance', async function(){
let balance = await web3.eth.getBalance(accounts[0]);
balance = web3.utils.fromWei(balance);
assert(parseInt(balance) >= 776)
});
// Generate some coverage so the script doesn't exit(1) because there are no events

@ -0,0 +1,6 @@
pragma solidity ^0.5.0;
contract Test {
address a;
address a;
}

@ -1,12 +1,14 @@
/* eslint-env node, mocha */
const assert = require('assert');
const shell = require('shelljs');
const fs = require('fs');
const childprocess = require('child_process');
const mock = require('../util/mockTruffle.js');
// shell.test alias for legibility
const shell = require('shelljs');
const mock = require('../util/integration.truffle');
const plugin = require('../../dist/truffle.plugin');
const util = require('util')
const opts = { compact: false, depth: 5, breakLength: 80 };
// =======
// Helpers
// =======
function pathExists(path) { return shell.test('-e', path); }
function assertCleanInitialState(){
@ -15,114 +17,73 @@ function assertCleanInitialState(){
}
function assertCoverageGenerated(){
assert(pathExists('./coverage') === true, 'script should gen coverage folder');
assert(pathExists('./coverage.json') === true, 'script should gen coverage.json');
assert(pathExists('./coverage') === true, 'should gen coverage folder');
assert(pathExists('./coverage.json') === true, 'should gen coverage.json');
}
function assertCoverageNotGenerated(){
assert(pathExists('./coverage') !== true, 'script should NOT gen coverage folder');
assert(pathExists('./coverage.json') !== true, 'script should NOT gen coverage.json');
assert(pathExists('./coverage') !== true, 'should NOT gen coverage folder');
assert(pathExists('./coverage.json') !== true, 'should NOT gen coverage.json');
}
function assertExecutionSucceeds(){
function getOutput(){
return JSON.parse(fs.readFileSync('./coverage.json', 'utf8'));
}
function assertExecutionFails(){
// ========
// Tests
// ========
describe('app', function() {
let truffleConfig;
let solcoverConfig;
}
beforeEach(() => {
mock.clean();
truffleConfig = mock.getDefaultTruffleConfig();
solcoverConfig = {};
if (process.env.SILENT)
solcoverConfig.silent = true;
})
describe.skip('app', function() {
afterEach(() => mock.remove());
//afterEach(() => mock.clean());
it('simple contract: should generate coverage, cleanup & exit(0)', () => {
it('simple contract: should generate coverage, cleanup & exit(0)', async function(){
assertCleanInitialState();
// Run script (exits 0);
mock.install('Simple.sol', 'simple.js', config);
shell.exec(script);
assert(shell.error() === null, 'script should not error');
mock.install('Simple', 'simple.js', solcoverConfig);
await plugin(truffleConfig);
assertCoverageGenerated();
// 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"');
const output = getOutput();
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"');
});
it('config with testrpc options string: should generate coverage, cleanup & exit(0)', () => {
const privateKey = '0x3af46c9ac38ee1f01b05f9915080133f644bf57443f504d339082cb5285ccae4';
const balance = '0xfffffffffffffff';
const testConfig = Object.assign({}, config);
testConfig.testrpcOptions = `--account="${privateKey},${balance}" --port 8777`;
testConfig.dir = './mock';
testConfig.norpc = false;
testConfig.port = 8777;
// Installed test will process.exit(1) and crash truffle if the test isn't
// loaded with the account specified above
mock.install('Simple.sol', 'testrpc-options.js', testConfig);
shell.exec(script);
assert(shell.error() === null, 'script should not error');
// 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('config with test command options string: should run test', () => {
assert(pathExists('./allFiredEvents') === false, 'should start without: events log');
const testConfig = Object.assign({}, config);
testConfig.testCommand = 'mocha --timeout 5000';
testConfig.dir = './mock';
testConfig.norpc = false;
testConfig.port = 8888;
// Installed test will write a fake allFiredEvents to ./ after 4000ms
// allowing test to pass
mock.install('Simple.sol', 'command-options.js', testConfig);
shell.exec(script);
assert(shell.error() === null, 'script should not error');
});
it('Oraclize @ solc v.0.4.24 (large, many unbracketed statements)', () => {
const trufflejs =
`module.exports = {
networks: {
coverage: {
host: "localhost",
network_id: "*",
port: 8555,
gas: 0xfffffffffff,
gasPrice: 0x01
},
},
compilers: {
solc: {
version: "0.4.24",
}
}
};`;
it('large contract with many unbracketed statements (time check)', async function() {
assertCleanInitialState();
// Run script (exits 0);
mock.install('Oraclize.sol', 'oraclize.js', config, trufflejs, null, true);
shell.exec(script);
assert(shell.error() === null, 'script should not error');
truffleConfig.compilers.solc.version = "0.4.24";
mock.install('Oraclize', 'oraclize.js', solcoverConfig, truffleConfig, true);
await plugin(truffleConfig);
});
it('tests use pure and view modifiers, including with libraries', () => {
it.skip('with pure and view modifiers and libraries', () => {
assertCleanInitialState();
// Run script (exits 0);
mock.installLibraryTest(config);
mock.installDouble(config);
shell.exec(script);
assert(shell.error() === null, 'script should not error');
@ -132,139 +93,122 @@ describe.skip('app', function() {
// This test is tightly bound to the function names in TotallyPure.sol
const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8'));
const path = Object.keys(produced)[0];
assert(produced[path].fnMap['1'].name === 'usesThem', 'coverage.json should map "usesThem"');
assert(produced[path].fnMap['2'].name === 'isPure', 'coverage.json should map "getX"');
assert(produced[path].fnMap['1'].name === 'usesThem', 'should map "usesThem"');
assert(produced[path].fnMap['2'].name === 'isPure', 'should map "getX"');
});
it('tests require assets outside of test folder: should generate coverage, cleanup & exit(0)', () => {
it('contract only uses ".call"', async function(){
assertCleanInitialState();
// Run script (exits 0);
mock.install('Simple.sol', 'requires-externally.js', config);
shell.exec(script);
assert(shell.error() === null, 'script should not error');
mock.install('OnlyCall', 'only-call.js', solcoverConfig);
await plugin(truffleConfig);
assertCoverageGenerated();
// 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"');
const output = getOutput();
const path = Object.keys(output)[0];
assert(output[path].fnMap['1'].name === 'addTwo', 'cov should map "addTwo"');
});
it('contract only uses .call: should generate coverage, cleanup & exit(0)', () => {
it('contract sends / transfers to instrumented fallback', async function(){
assertCleanInitialState();
mock.install('OnlyCall.sol', 'only-call.js', config);
shell.exec(script);
assert(shell.error() === null, 'script should not error');
mock.install('Wallet', 'wallet.js', solcoverConfig);
await plugin(truffleConfig);
assertCoverageGenerated();
const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8'));
const path = Object.keys(produced)[0];
assert(produced[path].fnMap['1'].name === 'addTwo', 'coverage.json should map "addTwo"');
const output = getOutput();
const path = Object.keys(output)[0];
assert(output[path].fnMap['1'].name === 'transferPayment', 'cov should map "transferPayment"');
});
it('contract sends / transfers to instrumented fallback: coverage, cleanup & exit(0)', () => {
it('contracts are skipped', async function() {
assertCleanInitialState();
mock.install('Wallet.sol', 'wallet.js', config);
shell.exec(script);
assert(shell.error() === null, 'script should not error');
solcoverConfig.skipFiles = ['Owned.sol'];
assertCoverageGenerated();
mock.installDouble(['Proxy', 'Owned'], 'inheritance.js', solcoverConfig);
await plugin(truffleConfig);
const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8'));
const path = Object.keys(produced)[0];
assert(produced[path].fnMap['1'].name === 'transferPayment', 'should map "transferPayment"');
assertCoverageGenerated();
const output = getOutput();
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: should generate coverage, cleanup & exit(0)', () => {
it('contract uses inheritance', async function() {
assertCleanInitialState();
mock.installInheritanceTest(config);
shell.exec(script);
assert(shell.error() === null, 'script should not error');
mock.installDouble(['Proxy', 'Owned'], 'inheritance.js', solcoverConfig);
await plugin(truffleConfig);
assertCoverageGenerated();
const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8'));
const ownedPath = Object.keys(produced)[0];
const proxyPath = Object.keys(produced)[1];
assert(produced[ownedPath].fnMap['1'].name === 'constructor', 'coverage.json should map "constructor"');
assert(produced[proxyPath].fnMap['1'].name === 'isOwner', 'coverage.json should map "isOwner"');
const output = getOutput();
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');
});
it('contracts are skipped: should generate coverage, cleanup & exit(0)', () => {
// Simple.sol with a failing assertion in a truffle test
it('truffle tests failing', async function() {
assertCleanInitialState();
const testConfig = Object.assign({}, config);
mock.install('Simple', 'truffle-test-fail.js', solcoverConfig);
testConfig.skipFiles = ['Owned.sol'];
mock.installInheritanceTest(testConfig);
shell.exec(script);
assert(shell.error() === null, 'script should not error');
try {
await plugin(truffleConfig);
assert.fail()
} catch(err){
assert(err.message.includes('failed under coverage'));
}
assertCoverageGenerated();
const produced = JSON.parse(fs.readFileSync('./coverage.json', 'utf8'));
const firstKey = Object.keys(produced)[0];
assert(Object.keys(produced).length === 1, 'coverage.json should only contain instrumentation for one contract');
assert(firstKey.substr(firstKey.length - 9) === 'Proxy.sol', 'coverage.json should only contain instrumentation for Proxy.sol');
const output = getOutput();
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"');
});
it('truffle tests failing: should generate coverage, cleanup & exit(1)', () => {
assertCleanInitialState();
// Run with Simple.sol and a failing assertion in a truffle test
mock.install('Simple.sol', 'truffle-test-fail.js', config);
shell.exec(script);
assert(shell.error() !== null, 'script should exit 1');
assertCoverageGenerated();
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"');
// 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);
});
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', config);
shell.exec(script);
assert(shell.error() === null, 'script should not error');
// 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.message.includes('SyntaxError'));
}
});
it('truffle crashes: should generate NO coverage, cleanup and exit(1)', () => {
// Solidity syntax errors
it('compilation failure', async function(){
assertCleanInitialState();
// Run with Simple.sol and a syntax error in the truffle test
mock.install('Simple.sol', 'truffle-crash.js', config);
shell.exec(script);
assert(shell.error() !== null, 'script should error');
assertCoverageNotGenerated();
});
mock.install('SimpleError', 'simple.js', solcoverConfig);
it('instrumentation errors: should generate NO coverage, cleanup and exit(1)', () => {
assertCleanInitialState();
try {
await plugin(truffleConfig);
assert.fail()
} catch(err){
assert(err.message.includes('Compilation failed'));
}
// Run with SimpleError.sol (has syntax error) and working truffle test
mock.install('SimpleError.sol', 'simple.js', config);
shell.exec(script);
assert(shell.error() !== null, 'script should error');
assertCoverageNotGenerated();
});

@ -1,7 +1,7 @@
const assert = require('assert');
const util = require('./../util/util.js');
const ganache = require('ganache-cli');
const ganache = require('ganache-core-sc');
const Coverage = require('./../../lib/coverage');
describe('asserts and requires', () => {
@ -9,7 +9,7 @@ describe('asserts and requires', () => {
let provider;
let collector;
before(async () => ({ provider, collector } = await util.initializeProvider(ganache)));
before(() => ({ provider, collector } = util.initializeProvider(ganache)));
beforeEach(() => coverage = new Coverage());
after((done) => provider.close(done));
@ -33,6 +33,8 @@ describe('asserts and requires', () => {
});
});
// NB: Truffle replays failing txs as .calls to obtain the revert reason from the return
// data. Hence the 2X measurements.
it('should cover assert statements as `if` statements when they fail', async function() {
const contract = await util.bootstrapCoverage('assert/Assert', provider, collector);
coverage.addContract(contract.instrumented, util.filePath);
@ -40,18 +42,17 @@ describe('asserts and requires', () => {
try { await contract.instance.a(false) } catch(err) { /* Invalid opcode */ }
const mapping = coverage.generate(contract.data, util.pathPrefix);
assert.deepEqual(mapping[util.filePath].l, {
5: 1,
5: 2,
});
assert.deepEqual(mapping[util.filePath].b, {
1: [0, 1],
1: [0, 2],
});
assert.deepEqual(mapping[util.filePath].s, {
1: 1,
1: 2,
});
assert.deepEqual(mapping[util.filePath].f, {
1: 1,
1: 2,
});
});
@ -75,6 +76,8 @@ describe('asserts and requires', () => {
});
});
// NB: Truffle replays failing txs as .calls to obtain the revert reason from the return
// data. Hence the 2X measurements.
it('should cover multi-line require stmts as `if` statements when they fail', async function() {
const contract = await util.bootstrapCoverage('assert/RequireMultiline', provider, collector);
coverage.addContract(contract.instrumented, util.filePath);
@ -84,16 +87,16 @@ describe('asserts and requires', () => {
const mapping = coverage.generate(contract.data, util.pathPrefix);
assert.deepEqual(mapping[util.filePath].l, {
5: 1,
5: 2,
});
assert.deepEqual(mapping[util.filePath].b, {
1: [0, 1],
1: [0, 2],
});
assert.deepEqual(mapping[util.filePath].s, {
1: 1,
1: 2,
});
assert.deepEqual(mapping[util.filePath].f, {
1: 1,
1: 2,
});
});
});

@ -1,7 +1,7 @@
const assert = require('assert');
const util = require('./../util/util.js');
const ganache = require('ganache-cli');
const ganache = require('ganache-core-sc');
const Coverage = require('./../../lib/coverage');
describe('function declarations', () => {
@ -112,7 +112,9 @@ describe('function declarations', () => {
// We try and call a contract at an address where it doesn't exist and the VM
// throws, but we can verify line / statement / fn coverage is getting mapped.
it('should cover a constructor call that chains to a method call', async function() {
//
// NB: 2x values are result of Truffle replaying failing txs to get reason string...
it('should cover a constructor --> method call chain', async function() {
const contract = await util.bootstrapCoverage('function/chainable', provider, collector);
coverage.addContract(contract.instrumented, util.filePath);
@ -121,21 +123,23 @@ describe('function declarations', () => {
const mapping = coverage.generate(contract.data, util.pathPrefix);
assert.deepEqual(mapping[util.filePath].l, {
9: 1,
9: 2,
});
assert.deepEqual(mapping[util.filePath].b, {});
assert.deepEqual(mapping[util.filePath].s, {
1: 1,
1: 2,
});
assert.deepEqual(mapping[util.filePath].f, {
1: 0,
2: 1,
2: 2,
});
});
// The vm runs out of gas here - but we can verify line / statement / fn
// coverage is getting mapped.
it('should cover a constructor call that chains to a method call', async function() {
//
// NB: 2x values are result of Truffle replaying failing txs to get reason string...
it('should cover a constructor --> method --> value call chain', async function() {
const contract = await util.bootstrapCoverage('function/chainable-value', provider, collector);
coverage.addContract(contract.instrumented, util.filePath);
@ -144,15 +148,15 @@ describe('function declarations', () => {
const mapping = coverage.generate(contract.data, util.pathPrefix);
assert.deepEqual(mapping[util.filePath].l, {
10: 1,
10: 2,
});
assert.deepEqual(mapping[util.filePath].b, {});
assert.deepEqual(mapping[util.filePath].s, {
1: 1,
1: 2,
});
assert.deepEqual(mapping[util.filePath].f, {
1: 0,
2: 1,
2: 2,
});
});
});

@ -1,7 +1,7 @@
const assert = require('assert');
const util = require('./../util/util.js');
const ganache = require('ganache-cli');
const ganache = require('ganache-core-sc');
const Coverage = require('./../../lib/coverage');
describe('if, else, and else if statements', () => {

@ -1,7 +1,7 @@
const assert = require('assert');
const util = require('./../util/util.js');
const ganache = require('ganache-cli');
const ganache = require('ganache-core-sc');
const Coverage = require('./../../lib/coverage');
describe('for and while statements', () => {

@ -1,7 +1,7 @@
const assert = require('assert');
const util = require('./../util/util.js');
const ganache = require('ganache-cli');
const ganache = require('ganache-core-sc');
const Coverage = require('./../../lib/coverage');
describe('generic statements', () => {
@ -49,7 +49,7 @@ describe('generic statements', () => {
});
it('should NOT pass tests if the contract has a compilation error', () => {
const info = util.instrumentAndCompile('../errors/compilation-error');
const info = util.instrumentAndCompile('app/SimpleError');
try {
util.report(output.errors);
assert.fail('failure'); // We shouldn't hit this.

@ -0,0 +1,172 @@
/*
Utilities for generating a mock truffle project to test plugin.
*/
const path = require('path');
const fs = require('fs');
const shell = require('shelljs');
const TruffleConfig = require('truffle-config');
const decache = require('decache');
const temp = './temp';
const truffleConfigName = 'truffle-config.js';
const configPath = `${temp}/.solcover.js`;
const testPath = './test/sources/js/';
const sourcesPath = './test/sources/solidity/contracts/app/';
const migrationPath = `${temp}/migrations/2_deploy.js`;
const templatePath = './test/integration/truffle/*';
function getDefaultTruffleConfig(){
const logger = process.env.SILENT ? { log: () => {} } : console;
const reporter = process.env.SILENT ? 'dot' : 'spec';
const mockwd = path.join(process.cwd(), temp);
const vals = {
working_directory: mockwd,
build_directory: path.join(mockwd, 'build'),
contracts_directory: path.join(mockwd, 'contracts'),
contracts_build_directory: path.join(mockwd, 'build', 'contracts'),
migrations_directory: path.join(mockwd, 'migrations'),
test_directory: path.join(mockwd, 'test'),
logger: logger,
mocha: { reporter: reporter },
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "*"
}
},
compilers: {
solc: {
version: "0.5.3",
settings: { optimizer: {} }
}
}
}
return (new TruffleConfig()).with(vals);
}
function getSolcoverJS(config){
return `module.exports = ${JSON.stringify(config, null, ' ')}`
}
function getTruffleConfigJS(config){
if (config) return `module.exports = ${JSON.stringify(config, null, ' ')}`
return `module.exports = ${JSON.stringify(getDefaultTruffleConfig(), null, ' ')}`
}
function deploySingle(contractName){
return `
const A = artifacts.require("${contractName}");
module.exports = function(deployer) { deployer.deploy(A) };
`;
}
function deployDouble(contractNames){
return `
var A = artifacts.require("${contractNames[0]}");
var B = artifacts.require("${contractNames[1]}");
module.exports = function(deployer) {
deployer.deploy(A);
deployer.link(A, B);
deployer.deploy(B);
};
`;
}
/**
* Installs mock truffle project at ./temp with a single contract
* and test specified by the params.
* @param {String} contract <contractName.sol> located in /test/sources/cli/
* @param {[type]} test <testName.js> located in /test/cli/
*/
function install(
contract,
test,
config,
_truffleConfig,
noMigrations
) {
const configjs = getSolcoverJS(config);
const trufflejs = getTruffleConfigJS(_truffleConfig);
const migration = deploySingle(contract);
// Scaffold
shell.mkdir(temp);
shell.cp('-Rf', templatePath, temp);
// Contract
shell.cp(`${sourcesPath}${contract}.sol`, `${temp}/contracts/${contract}.sol`);
// Migration
if (!noMigrations) fs.writeFileSync(migrationPath, migration);
// Test
shell.cp(`${testPath}${test}`, `${temp}/test/${test}`);
// Configs
fs.writeFileSync(`${temp}/${truffleConfigName}`, trufflejs);
fs.writeFileSync(configPath, configjs);
decache(`${process.cwd()}/${temp}/.solcover.js`);
decache(`${process.cwd()}/${temp}/${truffleConfigName}`);
};
/**
* Installs mock truffle project with two contracts (for inheritance, libraries, etc)
*
*/
function installDouble(contracts, test, config) {
const configjs = getSolcoverJS(config);
const migration = deployDouble(contracts);
// Scaffold
shell.mkdir(temp);
shell.cp('-Rf', templatePath, temp);
// Contracts
contracts.forEach(item => {
shell.cp(`${sourcesPath}${item}.sol`, `${temp}/contracts/${item}.sol`)
});
// Migration
fs.writeFileSync(migrationPath, migration)
// Test
shell.cp(`${testPath}${test}`, `${temp}/test/${test}`);
// Configs
fs.writeFileSync(`${temp}/${truffleConfigName}`, getTruffleConfigJS());
fs.writeFileSync(configPath, configjs);
decache(`${process.cwd()}/${temp}/.solcover.js`);
decache(`${process.cwd()}/${temp}/${truffleConfigName}`);
};
/**
* Removes mock truffle project and coverage reports generated by test
*/
function clean() {
shell.config.silent = true;
shell.rm('-Rf', 'temp');
shell.rm('-Rf', 'coverage');
shell.rm('coverage.json');
shell.config.silent = false;
};
module.exports = {
getDefaultTruffleConfig: getDefaultTruffleConfig,
install: install,
installDouble: installDouble,
clean: clean
}

@ -1,121 +0,0 @@
/*
Utilities for generating a mock truffle project to test plugin.
*/
const fs = require('fs');
const shell = require('shelljs');
const configPath = './mock/.solcover.js';
const testPath = './test/sources/js/';
const sourcesPath = './test/sources/solidity/contracts/app/';
const migrationPath = './mock/migrations/2_deploy.js';
const defaultTruffleConfig = `
module.exports = {
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "*"
}
},
compilers: {
solc: {
version: "0.5.3",
}
}
};
`
/**
* 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/cli/
* @param {[type]} test <testName.js> located in /test/cli/
*/
function install(
contract,
test,
config,
_truffleConfig,
_trufflejsName,
noMigrations
) {
const configjs = `module.exports = ${JSON.stringify(config)}`;
const migration = `
const A = artifacts.require('${contract}');
module.exports = function(deployer) { deployer.deploy(A) };
`;
// Mock truffle-config.js
const trufflejsName = _trufflejsName || 'truffle-config.js';
const trufflejs = _truffleConfig || defaultTruffleConfig;
// Generate mock
shell.mkdir('./mock');
shell.cp('-Rf', './test/integration/truffle', './mock');
shell.cp(`${sourcesPath}${contract}.sol`, `./mock/contracts/${contract}.sol`);
if (!noMigrations){
fs.writeFileSync(migrationPath, migration);
}
fs.writeFileSync(`./mock/${trufflejsName}`, trufflejs);
fs.writeFileSync(`${configPath}`, configjs);
shell.cp(`${testPath}${test}`, `./mock/test/${test}`);
};
/**
* Installs mock truffle project with two contracts (for inheritance, libraries, etc)
* @param {config} .solcover.js configuration
*/
function installMultiple(contracts, test, config) {
const configjs = `module.exports = ${JSON.stringify(config)}`;
const deployContracts = `
var A = artifacts.require(`${contracts[0]}`);
var B = artifacts.require(`${contracts[1]}`);
module.exports = function(deployer) {
deployer.deploy(A);
deployer.link(A, B);
deployer.deploy(B);
};
`;
shell.mkdir('./mock');
shell.cp('-Rf', './test/integration/truffle', './mock');
contracts.forEach(item => {
shell.cp(`${sourcesPath}${item}.sol`, `./mock/contracts/${item}.sol`)
});
shell.cp(`${testPath}${test}`, `./mock/test/${test}`);
fs.writeFileSync('./mock/truffle-config.js', defaultTruffleJs);
fs.writeFileSync('./.solcover.js', configjs);
fs.writeFileSync(migrationPath, migration)
};
/**
* Removes mock truffle project and coverage reports generated by exec.js
*/
function remove() {
shell.config.silent = true;
shell.rm('./.solcover.js');
shell.rm('-Rf', 'mock');
shell.rm('-Rf', 'coverage');
shell.rm('coverage.json');
shell.config.silent = false;
};
module.exports = {
install: install,
installMultiple: installMultiple,
remove: remove
}

@ -1,3 +1,7 @@
/**
* Setup and reporting helpers for the suites which test instrumentation
* and coverage correctness. (Integration test helpers are elsewhere)
*/
const fs = require('fs');
const path = require('path');
const solc = require('solc');
@ -6,14 +10,15 @@ const TruffleContract = require('truffle-contract');
const Instrumenter = require('./../../lib/instrumenter');
const DataCollector = require('./../../lib/collector')
// ====================
// Path constants
// ====================
const filePath = path.resolve('./test.sol');
const pathPrefix = './';
function getCode(_path) {
const pathToSources = `./../sources/solidity/contracts/${_path}`;
return fs.readFileSync(path.join(__dirname, pathToSources), 'utf8');
};
// ====================
// Contract deployments
// ====================
function getABI(solcOutput, testFile="test.sol", testName="Test"){
return solcOutput.contracts[testFile][testName].abi;
}
@ -30,6 +35,7 @@ async function getDeployedContractInstance(info, provider){
})
contract.setProvider(provider);
contract.autoGas = false;
const accounts = await contract.web3.eth.getAccounts();
contract.defaults({
@ -41,20 +47,30 @@ async function getDeployedContractInstance(info, provider){
return contract.new();
}
// ============
// Compilation
// ============
function getCode(_path) {
const pathToSources = `./../sources/solidity/contracts/${_path}`;
return fs.readFileSync(path.join(__dirname, pathToSources), 'utf8');
};
function compile(source){
const compilerInput = codeToCompilerInput(source);
return JSON.parse(solc.compile(compilerInput));
}
function report(output=[]) {
output.forEach(item => {
if (item.severity === 'error') {
const errors = JSON.stringify(output, null, ' ');
throw new Error(`Instrumentation fault: ${errors}`);
}
function codeToCompilerInput(code) {
return JSON.stringify({
language: 'Solidity',
sources: { 'test.sol': { content: code } },
settings: { outputSelection: {'*': { '*': [ '*' ] }} }
});
}
// ============================
// Instrumentation Correctness
// ============================
function instrumentAndCompile(sourceName) {
const contract = getCode(`${sourceName}.sol`)
const instrumenter = new Instrumenter();
@ -68,14 +84,18 @@ function instrumentAndCompile(sourceName) {
}
}
function codeToCompilerInput(code) {
return JSON.stringify({
language: 'Solidity',
sources: { 'test.sol': { content: code } },
settings: { outputSelection: {'*': { '*': [ '*' ] }} }
function report(output=[]) {
output.forEach(item => {
if (item.severity === 'error') {
const errors = JSON.stringify(output, null, ' ');
throw new Error(`Instrumentation fault: ${errors}`);
}
});
}
// =====================
// Coverage Correctness
// =====================
async function bootstrapCoverage(file, provider, collector){
const info = instrumentAndCompile(file);
info.instance = await getDeployedContractInstance(info, provider);
@ -83,22 +103,18 @@ async function bootstrapCoverage(file, provider, collector){
return info;
}
async function initializeProvider(ganache){
const provider = ganache.provider();
return new Promise(resolve => {
const interval = setInterval(() => {
if (provider.engine.manager.state.blockchain.vm !== undefined){
clearInterval(interval);
// =========
// Provider
// =========
function initializeProvider(ganache){
const collector = new DataCollector();
const options = { logger: { log: collector.step.bind(collector) }};
const provider = ganache.provider(options);
resolve({
return {
provider: provider,
collector: new DataCollector({provider: provider})
});
collector: collector
}
});
})
}
module.exports = {

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save