Initial draft: 0.7.0

truffle-plugin
cgewecke 5 years ago
parent 15fa08b566
commit fc3115138c
  1. 19
      .circleci/config.yml
  2. 5
      .gitignore
  3. 3
      .npmignore
  4. 18
      buidler.config.js
  5. 354
      lib/app.js
  6. 39
      lib/collector.js
  7. 112
      lib/coverage.js
  8. 145
      lib/coverageMap.js
  9. 249
      lib/injector.js
  10. 65
      lib/instrumentSolidity.js
  11. 336
      lib/instrumenter.js
  12. 110
      lib/parse.js
  13. 53
      lib/preprocessor.js
  14. 251
      lib/registrar.js
  15. 121
      lib/ternary.js
  16. 40
      package.json
  17. 24
      test/cli/command-options.js
  18. 21
      test/cli/events.js
  19. 274
      test/if.js
  20. 0
      test/jsSources/block-gas-limit.js
  21. 0
      test/jsSources/empty.js
  22. 3
      test/jsSources/inheritance.js
  23. 0
      test/jsSources/only-call.js
  24. 0
      test/jsSources/oraclize.js
  25. 0
      test/jsSources/pureview.js
  26. 0
      test/jsSources/requires-externally.js
  27. 0
      test/jsSources/sign.js
  28. 3
      test/jsSources/simple.js
  29. 0
      test/jsSources/sol-parse-fail.js
  30. 0
      test/jsSources/testrpc-options.js
  31. 0
      test/jsSources/totallyPure.js
  32. 0
      test/jsSources/truffle-crash.js
  33. 0
      test/jsSources/truffle-test-fail.js
  34. 9
      test/jsSources/wallet.js
  35. 0
      test/soliditySources/contracts/assembly/if.sol
  36. 0
      test/soliditySources/contracts/assembly/spaces-in-function.sol
  37. 0
      test/soliditySources/contracts/assert/Assert.sol
  38. 9
      test/soliditySources/contracts/assert/RequireMultiline.sol
  39. 0
      test/soliditySources/contracts/cli/CLibrary.sol
  40. 0
      test/soliditySources/contracts/cli/Empty.sol
  41. 0
      test/soliditySources/contracts/cli/Events.sol
  42. 0
      test/soliditySources/contracts/cli/Expensive.sol
  43. 0
      test/soliditySources/contracts/cli/Face.sol
  44. 0
      test/soliditySources/contracts/cli/Migrations.sol
  45. 0
      test/soliditySources/contracts/cli/OnlyCall.sol
  46. 0
      test/soliditySources/contracts/cli/Owned.sol
  47. 0
      test/soliditySources/contracts/cli/Proxy.sol
  48. 0
      test/soliditySources/contracts/cli/PureView.sol
  49. 0
      test/soliditySources/contracts/cli/Simple.sol
  50. 6
      test/soliditySources/contracts/cli/TotallyPure.sol
  51. 0
      test/soliditySources/contracts/cli/Wallet.sol
  52. 0
      test/soliditySources/contracts/comments/postContractComment.sol
  53. 0
      test/soliditySources/contracts/comments/postFunctionDeclarationComment.sol
  54. 0
      test/soliditySources/contracts/comments/postIfStatementComment.sol
  55. 0
      test/soliditySources/contracts/comments/postLineComment.sol
  56. 0
      test/soliditySources/contracts/conditional/declarative-exp-assignment-alternate.sol
  57. 0
      test/soliditySources/contracts/conditional/identifier-assignment-alternate.sol
  58. 2
      test/soliditySources/contracts/conditional/mapping-assignment.sol
  59. 0
      test/soliditySources/contracts/conditional/multiline-alternate.sol
  60. 0
      test/soliditySources/contracts/conditional/multiline-consequent.sol
  61. 0
      test/soliditySources/contracts/conditional/sameline-alternate.sol
  62. 0
      test/soliditySources/contracts/conditional/sameline-consequent.sol
  63. 0
      test/soliditySources/contracts/conditional/variable-decl-assignment-alternate.sol
  64. 0
      test/soliditySources/contracts/expressions/new-expression.sol
  65. 0
      test/soliditySources/contracts/expressions/single-binary-expression.sol
  66. 0
      test/soliditySources/contracts/function/abstract.sol
  67. 0
      test/soliditySources/contracts/function/calldata.sol
  68. 0
      test/soliditySources/contracts/function/chainable-new.sol
  69. 0
      test/soliditySources/contracts/function/chainable-value.sol
  70. 0
      test/soliditySources/contracts/function/chainable.sol
  71. 0
      test/soliditySources/contracts/function/constructor-keyword.sol
  72. 0
      test/soliditySources/contracts/function/empty-body.sol
  73. 0
      test/soliditySources/contracts/function/function-call.sol
  74. 0
      test/soliditySources/contracts/function/function.sol
  75. 0
      test/soliditySources/contracts/function/modifier.sol
  76. 0
      test/soliditySources/contracts/function/multiple.sol
  77. 0
      test/soliditySources/contracts/if/else-if-unbracketed-multi.sol
  78. 0
      test/soliditySources/contracts/if/else-if-without-brackets.sol
  79. 0
      test/soliditySources/contracts/if/else-with-brackets.sol
  80. 0
      test/soliditySources/contracts/if/else-without-brackets.sol
  81. 0
      test/soliditySources/contracts/if/if-else-no-brackets.sol
  82. 0
      test/soliditySources/contracts/if/if-elseif-else.sol
  83. 0
      test/soliditySources/contracts/if/if-no-brackets-multiline.sol
  84. 0
      test/soliditySources/contracts/if/if-no-brackets.sol
  85. 0
      test/soliditySources/contracts/if/if-with-brackets-multiline.sol
  86. 0
      test/soliditySources/contracts/if/if-with-brackets.sol
  87. 0
      test/soliditySources/contracts/if/nested-if-missing-else.sol
  88. 0
      test/soliditySources/contracts/loops/for-no-brackets.sol
  89. 0
      test/soliditySources/contracts/loops/for-with-brackets.sol
  90. 0
      test/soliditySources/contracts/loops/while-no-brackets.sol
  91. 0
      test/soliditySources/contracts/loops/while-with-brackets.sol
  92. 0
      test/soliditySources/contracts/return/return.sol
  93. 0
      test/soliditySources/contracts/statements/emit-coverage.sol
  94. 0
      test/soliditySources/contracts/statements/emit-instrument.sol
  95. 0
      test/soliditySources/contracts/statements/empty-contract-ala-melonport.sol
  96. 0
      test/soliditySources/contracts/statements/empty-contract-body.sol
  97. 0
      test/soliditySources/contracts/statements/fn-argument-multiline.sol
  98. 0
      test/soliditySources/contracts/statements/fn-argument.sol
  99. 2
      test/soliditySources/contracts/statements/fn-struct.sol
  100. 0
      test/soliditySources/contracts/statements/library.sol
  101. Some files were not shown because too many files have changed in this diff Show More

@ -30,10 +30,12 @@ jobs:
name: Run tests
command: |
npm run test-cov
- run:
name: Upload coverage
command: |
bash <(curl -s https://codecov.io/bash)
# TODO: Re-enable
#- run:
# name: Upload coverage
# command: |
# bash <(curl -s https://codecov.io/bash)
# This works but takes a while....
e2e-colony:
@ -56,7 +58,6 @@ jobs:
name: Zeppelin E2E
command: |
./scripts/run-zeppelin.sh
e2e-metacoin:
machine: true
steps:
@ -71,8 +72,9 @@ workflows:
build:
jobs:
- unit-test
- e2e-zeppelin
- e2e-metacoin
# TODO: re-enable
#- e2e-zeppelin
#- e2e-metacoin
nightly:
triggers:
- schedule:
@ -82,5 +84,6 @@ workflows:
only:
- master
jobs:
- e2e-zeppelin
# TODO: re-enable
#- e2e-zeppelin
#- e2e-colony

5
.gitignore vendored

@ -1,8 +1,7 @@
allFiredEvents
scTopics
scDebugLog
coverage.json
coverage/
node_modules/
.changelog
.DS_Store
test/artifacts
test/cache

@ -0,0 +1,3 @@
test/
.circleci/
docs/

@ -0,0 +1,18 @@
// Mute compiler warnings - this will need to be addressed properly in
// the Buidler plugin by overloading TASK_COMPILE_COMPILE.
const originalLog = console.log;
console.warn = () => {};
console.log = val => val === '\n' ? null : originalLog(val);
module.exports = {
solc: {
version: "0.5.8"
},
paths: {
artifacts: "./test/artifacts",
cache: "./test/cache",
test: "./test/units",
sources: "./test/sources/contracts",
}
}

@ -1,14 +1,10 @@
const shell = require('shelljs');
const fs = require('fs');
const path = require('path');
const childprocess = require('child_process');
const readline = require('readline');
const reqCwd = require('req-cwd');
const istanbul = require('istanbul');
const treeKill = require('tree-kill');
const getInstrumentedVersion = require('./instrumentSolidity.js');
const CoverageMap = require('./coverageMap.js');
const preprocessor = require('./preprocessor');
const Instrumenter = require('./instrumenter');
const Coverage = require('./coverage');
const isWin = /^win/.test(process.platform);
@ -20,37 +16,24 @@ const gasPriceHex = 0x01; // Low gas price
*/
class App {
constructor(config) {
this.coverageDir = './coverageEnv'; // Env that instrumented .sols are tested in
this.coverage = new Coverage();
this.instrumenter = new Instrumenter();
this.provider = config.provider;
// Options
this.network = ''; // Default truffle network execution flag
this.silence = ''; // Default log level passed to shell
this.log = console.log;
this.silence = ''; // Default log level (passed to shell)
this.log = config.logger || console.log; // Configurable logging
// Other
this.testrpcProcess = null; // ref to testrpc server we need to close on exit
this.events = null; // ref to string array loaded from 'allFiredEvents'
this.testsErrored = null; // flag set to non-null if truffle tests error
this.coverage = new CoverageMap(); // initialize a coverage map
this.originalArtifacts = []; // Artifacts from original build (we swap these in)
this.testsErrored = false; // Toggle true when tests error
this.skippedFolders = [];
// 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.accounts = config.accounts || 35; // Number of accounts to testrpc launches with
this.skipFiles = config.skipFiles || []; // Which files should be skipped during instrumentation
this.norpc = config.norpc || false; // Launch testrpc-sc internally?
this.port = config.port || 8555; // Port testrpc should listen on
this.buildDirPath = config.buildDirPath || '/build/contracts' // Build directory path for compiled smart contracts
this.copyNodeModules = config.copyNodeModules || false; // Copy node modules into coverageEnv?
this.copyPackages = config.copyPackages || []; // Only copy specific node_modules packages into coverageEnv
this.testrpcOptions = config.testrpcOptions || null; // Options for testrpc-sc
this.testCommand = config.testCommand || null; // Optional test command
this.compileCommand = config.compileCommand || null; // Optional compile command
this.deepSkip = config.deepSkip || null; // Don't post-process skipped files
this.skipFiles = config.skipFiles || []; // Files to exclude from instrumentation
this.setLoggingLevel(config.silent);
}
@ -58,83 +41,18 @@ class App {
/**
* Generates a copy of the target project configured for solidity-coverage and saves to
* the coverage environment folder. Process exits(1) if try fails
* the coverage environment folder.
*/
generateCoverageEnvironment() {
this.log('Generating coverage environment');
try {
this.sanityCheckContext();
this.identifySkippedFolders();
let files = shell.ls('-A', this.workingDir);
const nmIndex = files.indexOf('node_modules');
// Removes node_modules from array (unless requested).
if (!this.copyNodeModules && nmIndex > -1) {
files.splice(nmIndex, 1);
}
// Identify folders to exclude
this.skipFiles.forEach(item => {
if (path.extname(item) !== '.sol')
this.skippedFolders.push(item);
});
files = files.map(file => `${this.workingDir}/${file}`);
shell.mkdir(this.coverageDir);
shell.cp('-R', files, this.coverageDir);
// Add specific node_modules packages.
if (!this.copyNodeModules && this.copyPackages.length) {
shell.mkdir(this.coverageDir + '/node_modules');
this.copyPackages.forEach((nodePackage) => {
shell.mkdir('-p', this.coverageDir + '/node_modules/' + nodePackage);
shell.cp('-rfL', 'node_modules/' + nodePackage + '/.', this.coverageDir + '/node_modules/' + nodePackage);
});
}
// Load config
const coverageNetwork = {
host: 'localhost',
network_id: '*',
port: this.port,
gas: gasLimitHex,
gasPrice: gasPriceHex
};
let truffleConfig = {
networks: {
coverage: coverageNetwork
}
};
let newTrufflePath = `${this.workingDir}/truffle-config.js`;
let oldTrufflePath = `${this.workingDir}/truffle.js`;
if (shell.test('-e', newTrufflePath)) truffleConfig = reqCwd.silent(newTrufflePath);
else if (shell.test('-e', oldTrufflePath)) truffleConfig = reqCwd.silent(oldTrufflePath);
this.network = '--network coverage';
// Coverage network opts specified: use port if declared
if (truffleConfig.networks && truffleConfig.networks.coverage) {
this.port = truffleConfig.networks.coverage.port || this.port;
} else {
// Put the coverage network in the existing config
if (!truffleConfig.networks) truffleConfig.networks = {};
truffleConfig.networks.coverage = coverageNetwork;
const configString = `module.exports = ${JSON.stringify(truffleConfig)}`;
fs.writeFileSync(`${this.coverageDir}/truffle-config.js`, configString);
}
// Compile the contracts before instrumentation and preserve their ABI's.
// We will be stripping out access modifiers like view before we recompile
// post-instrumentation.
if (shell.test('-e', `${this.coverageDir}${this.buildDirPath}`)){
shell.rm('-Rf', `${this.coverageDir}${this.buildDirPath}`)
}
this.runCompileCommand();
this.originalArtifacts = this.loadArtifacts();
shell.rm('-Rf', `${this.coverageDir}${this.buildDirPath}`);
shell.cp('-Rf', this.contractsDir, this.coverageDir)
} catch (err) {
const msg = ('There was a problem generating the coverage environment: ');
@ -147,31 +65,33 @@ class App {
* + Generate file path reference for coverage report
* + Load contract as string
* + Instrument contract
* + Save instrumented contract in the coverage environment folder where covered tests will run
* + 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}/contracts/${contract}`);
this.skipFiles.push(`${this.coverageDir}/contracts/Migrations.sol`);
this.skipFiles = this.skipFiles.map(contract => `${this.coverageDir}/${contract}`);
this.skipFiles.push(`${this.coverageDir}/Migrations.sol`);
const instrumentedFiles = [];
let currentFile;
try {
shell.ls(`${this.coverageDir}/contracts/**/*.sol`).forEach(file => {
shell.ls(`${this.coverageDir}/**/*.sol`).forEach(file => {
currentFile = file;
if (!this.skipFiles.includes(file) && !this.inSkippedFolder(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 contract = fs.readFileSync(contractPath).toString();
const instrumentedContractInfo = getInstrumentedVersion(contract, canonicalPath);
fs.writeFileSync(contractPath, instrumentedContractInfo.contract);
this.coverage.addContract(instrumentedContractInfo, canonicalPath);
instrumentedFiles.push(file);
// Instrument contract, save, add to coverage map
const contract = this.loadContract(contractPath);
const instrumented = this.instrumenter.instrument(contract, canonicalPath);
this.saveContract(contractPath, instrumented.contract);
this.coverage.addContract(instrumented, canonicalPath);
} else {
this.log('Skipping instrumentation of ', file);
}
@ -181,180 +101,32 @@ class App {
this.cleanUp(msg + err);
}
// Strip any view / pure modifiers in other files in case they depend on any instrumented files
shell
.ls(`${this.coverageDir}/**/*.sol`)
.filter(file => !instrumentedFiles.includes(file))
.forEach(file => {
// Skip post-processing of skipped files
if (this.deepSkip && (this.skipFiles.includes(file) || this.inSkippedFolder(file))) return;
const contractPath = this.platformNeutralPath(file);
const contract = fs.readFileSync(contractPath).toString();
const contractProcessed = preprocessor.run(contract);
if (contractProcessed.name && contractProcessed.name === 'SyntaxError' && file.slice(-15) !== 'SimpleError.sol') {
console.log(`Warning: The file at ${file} was identified as a Solidity Contract, ` +
'but did not parse correctly. You may ignore this warning if it is not a Solidity file, ' +
'or your project does not use it');
} else {
fs.writeFileSync(contractPath, contractProcessed);
}
});
// Now that they've been modified, compile all the contracts again
this.runCompileCommand();
// And swap the original abis into the instrumented artifacts so that truffle etc uses 'call'
// on them.
this.modifyArtifacts();
}
/**
* Run modified testrpc with large block limit, on (hopefully) unused port.
* Changes here should be also be added to the before() block of test/run.js).
* @return {Promise} Resolves when testrpc prints 'Listening' to std out / norpc is true.
*/
launchTestrpc() {
return new Promise((resolve, reject) => {
if (!this.norpc) {
const defaultRpcOptions = `--accounts ${this.accounts} --port ${this.port}`;
const options = (this.testrpcOptions || defaultRpcOptions) + ` --gasLimit ${gasLimitHex}`;
// Launch
const execOpts = {maxBuffer: 1024 * 1024 * 100};
this.testrpcProcess = childprocess.exec(`npx testrpc-sc ${options}`, execOpts, (err, stdout, stderr) => {
if (err) {
if (stdout) this.log(`testRpc stdout:\n${stdout}`);
if (stderr) this.log(`testRpc stderr:\n${stderr}`);
this.cleanUp('testRpc errored after launching as a childprocess.');
}
});
// Resolve when testrpc logs that it's listening.
this.testrpcProcess.stdout.on('data', data => {
if (data.includes('Listening')) {
this.log(`Launched testrpc on port ${this.port}`);
return resolve();
}
});
} else {
return resolve();
}
});
}
/**
* Run truffle (or config.testCommand) over instrumented contracts in the
* coverage environment folder. Shell cd command needs to be invoked
* as its own statement for command line options to work, apparently.
* Also reads the 'allFiredEvents' log.
*/
runTestCommand() {
try {
const defaultCommand = `truffle test ${this.network} ${this.silence}`;
const command = this.testCommand || defaultCommand;
this.log(`Running: ${command}\n(this can take a few seconds)...`);
shell.cd(this.coverageDir);
shell.exec(command);
this.testsErrored = shell.error();
shell.cd('./..');
} catch (err) {
const msg =
`
There was an error generating coverage. Possible reasons include:
1. Another application is using port ${this.port}
2. Your test runner (Truffle?) crashed because the tests encountered an error.
`;
this.cleanUp(msg + err);
}
}
/**
* Run truffle compile (or config.compileCommand) over instrumented contracts in the
* coverage environment folder. Shell cd command needs to be invoked
* as its own statement for command line options to work, apparently.
*/
runCompileCommand() {
try {
const defaultCommand = `truffle compile ${this.network} ${this.silence}`;
const command = this.compileCommand || defaultCommand;
this.log(`Running: ${command}\n(this can take a few seconds)...`);
shell.cd(this.coverageDir);
shell.exec(command);
this.testsErrored = shell.error();
shell.cd('./..');
} catch (err) {
const msg =
`
There was an error compiling the contracts.
`;
this.cleanUp(msg + err);
}
}
/**
* Loads artifacts generated by compiling the contracts before we instrument them.
* @return {Array} Array of artifact objects
*/
loadArtifacts() {
const artifacts = [];
shell.ls(`${this.coverageDir}${this.buildDirPath}/*.json`).forEach(file => {
const artifactPath = this.platformNeutralPath(file);
const artifactRaw = fs.readFileSync(artifactPath);
const artifact = JSON.parse(artifactRaw);
artifacts.push(artifact);
})
return artifacts;
}
/**
* Swaps original ABIs into artifacts generated post-instrumentation. We are stripping
* access modifiers like `view` out of the source during that step and need to ensure
* truffle automatically invokes those methods by `.call`, based on the ABI sig.
*/
modifyArtifacts(){
shell.ls(`${this.coverageDir}${this.buildDirPath}/*.json`).forEach((file, index) => {
const artifactPath = this.platformNeutralPath(file);
const artifactRaw = fs.readFileSync(artifactPath);
const artifact = JSON.parse(artifactRaw);
artifact.abi = this.originalArtifacts[index].abi;
fs.writeFileSync(artifactPath, JSON.stringify(artifact));
})
this.collector = new DataCollector(
this.provider,
this.instrumenter.intrumentationData
)
}
/**
* Generate coverage / write coverage report / run istanbul
*/
generateReport() {
async generateReport() {
const collector = new istanbul.Collector();
const reporter = new istanbul.Reporter();
return new Promise((resolve, reject) => {
// Get events fired during instrumented contracts execution.
const stream = fs.createReadStream(`./allFiredEvents`);
stream.on('error', err => this.cleanUp('Event trace could not be read.\n' + err));
const reader = readline.createInterface({
input: stream,
});
this.events = [];
reader
.on('line', line => this.events.push(line))
.on('close', () => {
// Generate Istanbul report
try {
this.coverage.generate(this.events, `${this.workingDir}/contracts`);
const relativeMapping = this.makeKeysRelative(this.coverage.coverage, this.workingDir);
const json = JSON.stringify(relativeMapping);
fs.writeFileSync('./coverage.json', json);
const contractsPath = `${this.workingDir}/${this.config.contractsDir}`
this.coverage.generate(this.instrumenter.instrumentationData, contractsPath);
const relativeMapping = this.makeKeysRelative(this.coverage.data, this.workingDir);
this.saveCoverage(relativeMapping);
collector.add(relativeMapping);
reporter.add('html');
reporter.add('lcov');
reporter.add('text');
reporter.write(collector, true, () => {
this.log('Istanbul coverage reports generated');
this.cleanUp();
@ -366,10 +138,20 @@ class App {
this.cleanUp(msg + err);
}
});
});
}
// ------------------------------------------ Utils ----------------------------------------------
loadContract(_path){
return fs.readFileSync(_path).toString();
}
saveContract(_path, contract){
fs.writeFileSync(_path, contract);
}
saveCoverage(coverageObject){
fs.writeFileSync('./coverage.json', JSON.stringify(coverageObject));
}
sanityCheckContext(){
if (!shell.test('-e', `${this.workingDir}/contracts`)){
@ -379,10 +161,6 @@ class App {
if (shell.test('-e', `${this.workingDir}/${this.coverageDir}`)){
shell.rm('-Rf', this.coverageDir);
}
if (shell.test('-e', `${this.workingDir}/scTopics`)){
shell.rm(`${this.workingDir}/scTopics`);
}
}
/**
@ -393,16 +171,14 @@ class App {
*/
makeKeysRelative(map, root) {
const newCoverage = {};
Object.keys(map).forEach(pathKey => {
newCoverage[path.relative(root, pathKey)] = map[pathKey];
});
Object.keys(map).forEach(pathKey => newCoverage[path.relative(root, pathKey)] = map[pathKey]);
return newCoverage;
}
/**
* Conver absolute paths from Windows, if necessary
* Normalizes windows paths
* @param {String} file path
* @return {[type]} normalized path
* @return {String} normalized path
*/
platformNeutralPath(file) {
return (isWin)
@ -425,6 +201,18 @@ class App {
return shouldSkip;
}
/**
* Helper for parsing 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);
});
}
/**
* Allows config to turn logging off (for CI)
* @param {Boolean} isSilent
@ -436,10 +224,14 @@ class App {
}
}
/**
* 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....
*/
cleanUp(err) {
const self = this;
@ -459,18 +251,8 @@ class App {
self.log('Cleaning up...');
shell.config.silent = true;
shell.rm('-Rf', self.coverageDir);
shell.rm('./allFiredEvents');
shell.rm('./scTopics');
if (self.testrpcProcess) {
treeKill(self.testrpcProcess.pid, function(killError){
self.log(`Shutting down testrpc-sc (pid ${self.testrpcProcess.pid})`)
exit(err)
});
} else {
exit(err);
}
}
}
module.exports = App;

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

@ -0,0 +1,112 @@
/**
* Converts instrumentation data accumulated a the vm steps to an instanbul spec coverage object.
* @type {Coverage}
*/
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
*/
addContract(info, canonicalContractPath) {
this.data[canonicalContractPath] = {
l: {},
path: canonicalContractPath,
s: {},
b: {},
f: {},
fnMap: {},
statementMap: {},
branchMap: {},
};
this.assertData[canonicalContractPath] = { };
info.runnableLines.forEach((item, idx) => {
this.data[canonicalContractPath].l[info.runnableLines[idx]] = 0;
});
this.data[canonicalContractPath].fnMap = info.fnMap;
for (let x = 1; x <= Object.keys(info.fnMap).length; x++) {
this.data[canonicalContractPath].f[x] = 0;
}
this.data[canonicalContractPath].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] = {
preEvents: 0,
postEvents: 0,
};
}
this.data[canonicalContractPath].statementMap = info.statementMap;
for (let x = 1; x <= Object.keys(info.statementMap).length; x++) {
this.data[canonicalContractPath].s[x] = 0;
}
}
/**
* Populates an empty coverage map with values derived from a hash map of
* data collected as the instrumented contracts are tested
* @param {Object} map of collected instrumentation data
* @return {Object} coverage map.
*/
generate(collectedData) {
const hashes = Object.keys(collectedData);
for (let hash of hashes){
const data = collectedData[hash];
const contractPath = collectedData[hash].contractPath;
const id = collectedData[hash].id;
const hits = collectedData[hash].hits;
switch(collectedData[hash].type){
case 'line': this.data[contractPath].l[id] = hits; break;
case 'function': this.data[contractPath].f[id] = hits; break;
case 'statement': this.data[contractPath].s[id] = hits; break;
case 'branch': this.data[contractPath].b[id][data.locationIdx] = hits; break;
case 'assertPre': this.assertData[contractPath][id].preEvents = hits; break;
case 'assertPost': this.assertData[contractPath][id].postEvents = hits; break;
}
}
// Finally, interpret the assert pre/post events
const contractPaths = Object.keys(this.assertData);
for (let contractPath of contractPaths){
const contract = this.data[contractPath];
Object.keys(contract.b).forEach((item, i) => {
const branch = this.assertData[contractPath][i];
// Was it an assert branch?
if (branch && branch.preEvents > 0){
this.data[contractPath].b[i] = [
branch.postEvents,
branch.preEvents - branch.postEvents
]
}
})
}
return Object.assign({}, this.data);
}
};
module.exports = Coverage;

@ -1,145 +0,0 @@
/**
* This file contains methods that produce a coverage map to pass to instanbul
* from data generated by `instrumentSolidity.js`
*/
const { AbiCoder } = require('web3-eth-abi');
const SolidityCoder = AbiCoder();
const path = require('path');
const keccak = require('keccakjs');
const fs = require('fs');
/**
* Converts solcover event data into an object that can be
* be passed to instanbul to produce coverage reports.
* @type {CoverageMap}
*/
module.exports = class CoverageMap {
constructor() {
this.coverage = {};
this.assertCoverage = {};
this.lineTopics = [];
this.functionTopics = [];
this.branchTopics = [];
this.statementTopics = [];
this.assertPreTopics = [];
this.assertPostTopics = [];
}
/**
* Initializes a coverage map object for contract instrumented per `info` and located
* at `canonicalContractPath`
* @param {Object} info `info = getIntrumentedVersion(contract, fileName, true)`
* @param {String} canonicalContractPath target file location
* @return {Object} coverage map with all values set to zero
*/
addContract(info, canonicalContractPath) {
this.coverage[canonicalContractPath] = {
l: {},
path: canonicalContractPath,
s: {},
b: {},
f: {},
fnMap: {},
statementMap: {},
branchMap: {},
};
this.assertCoverage[canonicalContractPath] = { };
info.runnableLines.forEach((item, idx) => {
this.coverage[canonicalContractPath].l[info.runnableLines[idx]] = 0;
});
this.coverage[canonicalContractPath].fnMap = info.fnMap;
for (let x = 1; x <= Object.keys(info.fnMap).length; x++) {
this.coverage[canonicalContractPath].f[x] = 0;
}
this.coverage[canonicalContractPath].branchMap = info.branchMap;
for (let x = 1; x <= Object.keys(info.branchMap).length; x++) {
this.coverage[canonicalContractPath].b[x] = [0, 0];
this.assertCoverage[canonicalContractPath][x] = {
preEvents: 0,
postEvents: 0,
};
}
this.coverage[canonicalContractPath].statementMap = info.statementMap;
for (let x = 1; x <= Object.keys(info.statementMap).length; x++) {
this.coverage[canonicalContractPath].s[x] = 0;
}
const keccakhex = (x => {
const hash = new keccak(256); // eslint-disable-line new-cap
hash.update(x);
return hash.digest('hex');
});
const lineHash = keccakhex('__Coverage' + info.contractName + '(string,uint256)');
const fnHash = keccakhex('__FunctionCoverage' + info.contractName + '(string,uint256)');
const branchHash = keccakhex('__BranchCoverage' + info.contractName + '(string,uint256,uint256)');
const statementHash = keccakhex('__StatementCoverage' + info.contractName + '(string,uint256)');
const assertPreHash = keccakhex('__AssertPreCoverage' + info.contractName + '(string,uint256)');
const assertPostHash = keccakhex('__AssertPostCoverage' + info.contractName + '(string,uint256)');
this.lineTopics.push(lineHash);
this.functionTopics.push(fnHash);
this.branchTopics.push(branchHash);
this.statementTopics.push(statementHash);
this.assertPreTopics.push(assertPreHash);
this.assertPostTopics.push(assertPostHash);
const topics = `${lineHash}\n${fnHash}\n${branchHash}\n${statementHash}\n${assertPreHash}\n${assertPostHash}\n`;
fs.appendFileSync('./scTopics', topics);
}
/**
* Populates an empty coverage map with values derived from an array of events
* fired by instrumented contracts as they are tested
* @param {Array} events
* @param {String} relative path to host contracts eg: './../contracts'
* @return {Object} coverage map.
*/
generate(events, pathPrefix) {
for (let idx = 0; idx < events.length; idx++) {
const event = JSON.parse(events[idx]);
if (event.topics.filter(t => this.lineTopics.indexOf(t) >= 0).length > 0) {
const data = SolidityCoder.decodeParameters(['string', 'uint256'], `0x${event.data}`);
const canonicalContractPath = data[0];
this.coverage[canonicalContractPath].l[parseInt(data[1], 10)] += 1;
} else if (event.topics.filter(t => this.functionTopics.indexOf(t) >= 0).length > 0) {
const data = SolidityCoder.decodeParameters(['string', 'uint256'], `0x${event.data}`);
const canonicalContractPath = data[0];
this.coverage[canonicalContractPath].f[parseInt(data[1], 10)] += 1;
} else if (event.topics.filter(t => this.branchTopics.indexOf(t) >= 0).length > 0) {
const data = SolidityCoder.decodeParameters(['string', 'uint256', 'uint256'], `0x${event.data}`);
const canonicalContractPath = data[0];
this.coverage[canonicalContractPath].b[parseInt(data[1], 10)][parseInt(data[2], 10)] += 1;
} else if (event.topics.filter(t => this.statementTopics.indexOf(t) >= 0).length > 0) {
const data = SolidityCoder.decodeParameters(['string', 'uint256'], `0x${event.data}`);
const canonicalContractPath = data[0];
this.coverage[canonicalContractPath].s[parseInt(data[1], 10)] += 1;
} else if (event.topics.filter(t => this.assertPreTopics.indexOf(t) >= 0).length > 0) {
const data = SolidityCoder.decodeParameters(['string', 'uint256'], `0x${event.data}`);
const canonicalContractPath = data[0];
this.assertCoverage[canonicalContractPath][parseInt(data[1], 10)].preEvents += 1;
} else if (event.topics.filter(t => this.assertPostTopics.indexOf(t) >= 0).length > 0) {
const data = SolidityCoder.decodeParameters(['string', 'uint256'], `0x${event.data}`);
const canonicalContractPath = data[0];
this.assertCoverage[canonicalContractPath][parseInt(data[1], 10)].postEvents += 1;
}
}
// Finally, interpret the assert pre/post events
Object.keys(this.assertCoverage).forEach(contractPath => {
const contract = this.coverage[contractPath];
for (let i = 1; i <= Object.keys(contract.b).length; i++) {
const branch = this.assertCoverage[contractPath][i];
if (branch.preEvents > 0) {
// Then it was an assert branch.
this.coverage[contractPath].b[i] = [branch.postEvents, branch.preEvents - branch.postEvents];
}
}
});
return Object.assign({}, this.coverage);
}
};

@ -1,77 +1,184 @@
const injector = {};
// These functions are used to actually inject the instrumentation events.
injector.callEvent = function injectCallEvent(contract, fileName, injectionPoint) {
const linecount = (contract.instrumented.slice(0, injectionPoint).match(/\n/g) || []).length + 1;
const sha1 = require("sha1");
const web3Utils = require("web3-utils");
class Injector {
constructor(){
this.hashCounter = 0;
this.definitionCounter = 0;
}
/**
* Generates solidity statement to inject for line, stmt, branch, fn 'events'
* @param {String} memoryVariable
* @param {String} hash hash key to an instrumentationData entry (see _getHash)
* @param {String} type instrumentation type, e.g. line, statement
* @return {String} ex: _sc_82e0891[0] = bytes32(0xdc08...08ed1); // function
*/
_getInjectable(memoryVariable, hash, type){
return `${memoryVariable}[0] = bytes32(${hash}); /* ${type} */ \n`;
}
_getHash(fileName) {
this.hashCounter++;
return web3Utils.keccak256(`${fileName}:${this.hashCounter}`);
}
/**
* Generates a solidity statement injection. Declared once per fn.
* Definition is the same for every fn in file.
* @param {String} fileName
* @return {String} ex: bytes32[1] memory _sc_82e0891
*/
_getMemoryVariableDefinition(fileName){
this.definitionCounter++;
return `\nbytes32[1] memory _sc_${sha1(fileName).slice(0,7)};\n`;
}
_getMemoryVariableAssignment(fileName){
return `\n_sc_${sha1(fileName).slice(0,7)}`;
}
injectLine(contract, fileName, injectionPoint, injection, instrumentation){
const type = 'line';
const start = contract.instrumented.slice(0, injectionPoint);
const end = contract.instrumented.slice(injectionPoint);
const newLines = start.match(/\n/g);
const linecount = ( newLines || []).length + 1;
contract.runnableLines.push(linecount);
contract.instrumented = contract.instrumented.slice(0, injectionPoint) +
'emit __Coverage' + contract.contractName + '(\'' + fileName + '\',' + linecount + ');\n' +
contract.instrumented.slice(injectionPoint);
};
injector.callFunctionEvent = function injectCallFunctionEvent(contract, fileName, injectionPoint, injection) {
contract.instrumented = contract.instrumented.slice(0, injectionPoint) +
'emit __FunctionCoverage' + contract.contractName + '(\'' + fileName + '\',' + injection.fnId + ');\n' +
contract.instrumented.slice(injectionPoint);
};
injector.callBranchEvent = function injectCallFunctionEvent(contract, fileName, injectionPoint, injection) {
contract.instrumented = contract.instrumented.slice(0, injectionPoint) +
(injection.openBracket ? '{' : '') +
'emit __BranchCoverage' + contract.contractName + '(\'' + fileName + '\',' + injection.branchId + ',' + injection.locationIdx + ')' +
(injection.comma ? ',' : ';') +
contract.instrumented.slice(injectionPoint);
};
injector.callEmptyBranchEvent = function injectCallEmptyBranchEvent(contract, fileName, injectionPoint, injection) {
contract.instrumented = contract.instrumented.slice(0, injectionPoint) +
'else { emit __BranchCoverage' + contract.contractName + '(\'' + fileName + '\',' + injection.branchId + ',' + injection.locationIdx + ');}\n' +
contract.instrumented.slice(injectionPoint);
};
injector.callAssertPreEvent = function callAssertPreEvent(contract, fileName, injectionPoint, injection) {
contract.instrumented = contract.instrumented.slice(0, injectionPoint) +
'emit __AssertPreCoverage' + contract.contractName + '(\'' + fileName + '\',' + injection.branchId + ');\n' +
contract.instrumented.slice(injectionPoint);
};
injector.callAssertPostEvent = function callAssertPostEvent(contract, fileName, injectionPoint, injection) {
contract.instrumented = contract.instrumented.slice(0, injectionPoint) +
'emit __AssertPostCoverage' + contract.contractName + '(\'' + fileName + '\',' + injection.branchId + ');\n' +
contract.instrumented.slice(injectionPoint);
};
injector.openParen = function injectOpenParen(contract, fileName, injectionPoint, injection) {
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + '(' + contract.instrumented.slice(injectionPoint);
const hash = this._getHash(fileName);
const memoryVariable = this._getMemoryVariableAssignment(fileName);
const injectable = this._getInjectable(memoryVariable, hash , type)
instrumentation[hash] = {
id: linecount,
type: type,
contractPath: fileName,
hits: 0
}
contract.instrumented = `${start}${injectable}${end}`;
}
injectStatement(contract, fileName, injectionPoint, injection, instrumentation) {
const type = 'statement';
const start = contract.instrumented.slice(0, injectionPoint);
const end = contract.instrumented.slice(injectionPoint);
const hash = this._getHash(fileName);
const memoryVariable = this._getMemoryVariableAssignment(fileName);
const injectable = this._getInjectable(memoryVariable, hash, type)
instrumentation[hash] = {
id: injection.statementId,
type: type,
contractPath: fileName,
hits: 0
}
contract.instrumented = `${start}${injectable}${end}`;
};
injector.closeParen = function injectCloseParen(contract, fileName, injectionPoint, injection) {
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + ')' + contract.instrumented.slice(injectionPoint);
injectFunction(contract, fileName, injectionPoint, injection, instrumentation){
const type = 'function';
const start = contract.instrumented.slice(0, injectionPoint);
const end = contract.instrumented.slice(injectionPoint);
const hash = this._getHash(fileName);
const memoryVariableDefinition = this._getMemoryVariableDefinition(fileName);
const memoryVariable = this._getMemoryVariableAssignment(fileName);
const injectable = this._getInjectable(memoryVariable, hash, type);
instrumentation[hash] = {
id: injection.fnId,
type: type,
contractPath: fileName,
hits: 0
}
contract.instrumented = `${start}${memoryVariableDefinition}${injectable}${end}`;
}
injectBranch(contract, fileName, injectionPoint, injection, instrumentation){
const type = 'branch';
const start = contract.instrumented.slice(0, injectionPoint);
const end = contract.instrumented.slice(injectionPoint);
const hash = this._getHash(fileName);
const memoryVariable = this._getMemoryVariableAssignment(fileName);
const injectable = this._getInjectable(memoryVariable, hash, type);
instrumentation[hash] = {
id: injection.branchId,
locationIdx: injection.locationIdx,
type: type,
contractPath: fileName,
hits: 0
}
contract.instrumented = `${start}${injectable}${end}`;
}
injectEmptyBranch(contract, fileName, injectionPoint, injection, instrumentation) {
const type = 'branch';
const start = contract.instrumented.slice(0, injectionPoint);
const end = contract.instrumented.slice(injectionPoint);
const hash = this._getHash(fileName);
const memoryVariable = this._getMemoryVariableAssignment(fileName);
const injectable = this._getInjectable(memoryVariable, hash, type);
instrumentation[hash] = {
id: injection.branchId,
locationIdx: injection.locationIdx,
type: type,
contractPath: fileName,
hits: 0
}
contract.instrumented = `${start}else { ${injectable}}${end}`;
}
injectAssertPre(contract, fileName, injectionPoint, injection, instrumentation) {
const type = 'assertPre';
const start = contract.instrumented.slice(0, injectionPoint);
const end = contract.instrumented.slice(injectionPoint);
const hash = this._getHash(fileName);
const memoryVariable = this._getMemoryVariableAssignment(fileName);
const injectable = this._getInjectable(memoryVariable, hash, type);
instrumentation[hash] = {
id: injection.branchId,
type: type,
contractPath: fileName,
hits: 0
}
contract.instrumented = `${start}${injectable}${end}`;
}
injectAssertPost(contract, fileName, injectionPoint, injection, instrumentation) {
const type = 'assertPost';
const start = contract.instrumented.slice(0, injectionPoint);
const end = contract.instrumented.slice(injectionPoint);
const hash = this._getHash(fileName);
const memoryVariable = this._getMemoryVariableAssignment(fileName);
const injectable = this._getInjectable(memoryVariable, hash, type);
instrumentation[hash] = {
id: injection.branchId,
type: type,
contractPath: fileName,
hits: 0
}
contract.instrumented = `${start}${injectable}${end}`;
}
};
injector.literal = function injectLiteral(contract, fileName, injectionPoint, injection) {
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + injection.string + contract.instrumented.slice(injectionPoint);
};
injector.statement = function injectStatement(contract, fileName, injectionPoint, injection) {
contract.instrumented = contract.instrumented.slice(0, injectionPoint) +
'emit __StatementCoverage' + contract.contractName + '(\'' + fileName + '\',' + injection.statementId + ');\n' +
contract.instrumented.slice(injectionPoint);
};
injector.eventDefinition = function injectEventDefinition(contract, fileName, injectionPoint, injection) {
contract.instrumented = contract.instrumented.slice(0, injectionPoint) +
'event __Coverage' + contract.contractName + '(string fileName, uint256 lineNumber);\n' +
'event __FunctionCoverage' + contract.contractName + '(string fileName, uint256 fnId);\n' +
'event __StatementCoverage' + contract.contractName + '(string fileName, uint256 statementId);\n' +
'event __BranchCoverage' + contract.contractName + '(string fileName, uint256 branchId, uint256 locationIdx);\n' +
'event __AssertPreCoverage' + contract.contractName + '(string fileName, uint256 branchId);\n' +
'event __AssertPostCoverage' + contract.contractName + '(string fileName, uint256 branchId);\n' +
contract.instrumented.slice(injectionPoint);
};
module.exports = injector;
module.exports = Injector;

@ -1,65 +0,0 @@
const SolidityParser = require('solidity-parser-antlr');
const preprocessor = require('./preprocessor');
const injector = require('./injector');
const parse = require('./parse');
const path = require('path');
module.exports = function instrumentSolidity(contractSource, fileName) {
const contract = {};
contract.source = contractSource;
contract.instrumented = contractSource;
contract.runnableLines = [];
contract.fnMap = {};
contract.fnId = 0;
contract.branchMap = {};
contract.branchId = 0;
contract.statementMap = {};
contract.statementId = 0;
contract.injectionPoints = {};
// 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));
// Now, we reset almost everything and use the preprocessor first to increase our effectiveness.
contract.runnableLines = [];
contract.fnMap = {};
contract.fnId = 0;
contract.branchMap = {};
contract.branchId = 0;
contract.statementMap = {};
contract.statementId = 0;
contract.injectionPoints = {};
contract.preprocessed = preprocessor.run(contract.source);
contract.instrumented = contract.preprocessed;
ast = SolidityParser.parse(contract.preprocessed, {range: true});
const contractStatement = ast.children.filter(node => (node.type === 'ContractDefinition' ||
node.type === 'LibraryDefinition' ||
node.type === 'InterfaceDefinition'));
contract.contractName = contractStatement[0].name;
parse[ast.type](contract, ast);
// We have to iterate through these injection points in descending order to not mess up
// the injection process.
const sortedPoints = Object.keys(contract.injectionPoints).sort((a, b) => b - a);
sortedPoints.forEach(injectionPoint => {
// Line instrumentation has to happen first
contract.injectionPoints[injectionPoint].sort((a, b) => {
const eventTypes = ['openParen', 'callBranchEvent', 'callEmptyBranchEvent', 'callEvent'];
return eventTypes.indexOf(b.type) - eventTypes.indexOf(a.type);
});
contract.injectionPoints[injectionPoint].forEach(injection => {
injector[injection.type](contract, fileName, injectionPoint, injection);
});
});
retValue.runnableLines = contract.runnableLines;
retValue.contract = contract.instrumented;
retValue.contractName = contractStatement[0].name;
return retValue;
};

@ -1,254 +1,106 @@
const instrumenter = {};
// These functions work out where in an expression we can inject our
// instrumenation events.
function createOrAppendInjectionPoint(contract, key, value) {
if (contract.injectionPoints[key]) {
contract.injectionPoints[key].push(value);
} else {
contract.injectionPoints[key] = [value];
}
const SolidityParser = require('solidity-parser-antlr');
const path = require('path');
const Injector = require('./injector');
const preprocess = require('./preprocessor');
const parse = require('./parse');
/**
* Top level controller for the instrumentation sequence. Also hosts the instrumentation data map
* which the vm step listener writes its output to. This only needs to be instantiated once
* per coverage run.
*/
class Instrumenter {
constructor(){
this.instrumentationData = {};
this.injector = new Injector();
}
instrumenter.prePosition = function prePosition(expression) {
if (expression.right.type === 'ConditionalExpression' &&
expression.left.type === 'MemberExpression') {
expression.range[0] -= 2;
}
};
instrumenter.instrumentAssignmentExpression = function instrumentAssignmentExpression(contract, expression) {
// This is suspended for 0.5.0 which tries to accomodate the new `emit` keyword.
// Solc is not allowing us to use the construction `emit SomeEvent()` within the parens :/
return;
// --------------------------------------------------------------------------------------------
// The only time we instrument an assignment expression is if there's a conditional expression on
// the right
/*if (expression.right.type === 'ConditionalExpression') {
if (expression.left.type === 'DeclarativeExpression' || expression.left.type === 'Identifier') {
// Then we need to go from bytes32 varname = (conditional expression)
// to bytes32 varname; (,varname) = (conditional expression)
createOrAppendInjectionPoint(contract, expression.left.range[1], {
type: 'literal', string: '; (,' + expression.left.name + ')',
});
instrumenter.instrumentConditionalExpression(contract, expression.right);
} else if (expression.left.type === 'MemberExpression') {
createOrAppendInjectionPoint(contract, expression.left.range[0], {
type: 'literal', string: '(,',
});
createOrAppendInjectionPoint(contract, expression.left.range[1], {
type: 'literal', string: ')',
});
instrumenter.instrumentConditionalExpression(contract, expression.right);
} else {
const err = 'Error instrumenting assignment expression @ solidity-coverage/lib/instrumenter.js';
console.log(err, contract, expression.left);
process.exit();
_isRootNode(node){
return (node.type === 'ContractDefinition' ||
node.type === 'LibraryDefinition' ||
node.type === 'InterfaceDefinition');
}
}*/
};
instrumenter.instrumentConditionalExpression = function instrumentConditionalExpression(contract, expression) {
// ----------------------------------------------------------------------------------------------
// This is suspended for 0.5.0 which tries to accomodate the new `emit` keyword.
// Solc is not allowing us to use the construction `emit SomeEvent()` within the parens :/
// Very sad, this is the coolest thing in here.
return;
// ----------------------------------------------------------------------------------------------
/*contract.branchId += 1;
const startline = (contract.instrumented.slice(0, expression.range[0]).match(/\n/g) || []).length + 1;
const startcol = expression.range[0] - contract.instrumented.slice(0, expression.range[0]).lastIndexOf('\n') - 1;
const consequentStartCol = startcol + (contract, expression.trueBody.range[0] - expression.range[0]);
const consequentEndCol = consequentStartCol + (contract, expression.trueBody.range[1] - expression.trueBody.range[0]);
const alternateStartCol = startcol + (contract, expression.falseBody.range[0] - expression.range[0]);
const alternateEndCol = alternateStartCol + (contract, expression.falseBody.range[1] - expression.falseBody.range[0]);
// NB locations for conditional branches in istanbul are length 1 and associated with the : and ?.
contract.branchMap[contract.branchId] = {
line: startline,
type: 'cond-expr',
locations: [{
start: {
line: startline, column: consequentStartCol,
},
end: {
line: startline, column: consequentEndCol,
},
}, {
start: {
line: startline, column: alternateStartCol,
},
end: {
line: startline, column: alternateEndCol,
},
}],
};
// Right, this could be being used just by itself or as an assignment. In the case of the latter, because
// the comma operator doesn't exist, we're going to have to get funky.
// if we're on a line by ourselves, this is easier
//
// Now if we've got to wrap the expression it's being set equal to, do that...
// Wrap the consequent
createOrAppendInjectionPoint(contract, expression.trueBody.range[0], {
type: 'openParen',
});
createOrAppendInjectionPoint(contract, expression.trueBody.range[0], {
type: 'callBranchEvent', comma: true, branchId: contract.branchId, locationIdx: 0,
});
createOrAppendInjectionPoint(contract, expression.trueBody.range[1], {
type: 'closeParen',
});
// Wrap the alternate
createOrAppendInjectionPoint(contract, expression.falseBody.range[0], {
type: 'openParen',
});
createOrAppendInjectionPoint(contract, expression.falseBody.range[0], {
type: 'callBranchEvent', comma: true, branchId: contract.branchId, locationIdx: 1,
});
createOrAppendInjectionPoint(contract, expression.falseBody.range[1], {
type: 'closeParen',
});*/
};
instrumenter.instrumentStatement = function instrumentStatement(contract, expression) {
contract.statementId += 1;
// We need to work out the lines and columns the expression starts and ends
const startline = (contract.instrumented.slice(0, expression.range[0]).match(/\n/g) || []).length + 1;
const startcol = expression.range[0] - contract.instrumented.slice(0, expression.range[0]).lastIndexOf('\n') - 1;
const expressionContent = contract.instrumented.slice(expression.range[0], expression.range[1] + 1);
const endline = startline + (contract, expressionContent.match('/\n/g') || []).length;
let endcol;
if (expressionContent.lastIndexOf('\n') >= 0) {
endcol = contract.instrumented.slice(expressionContent.lastIndexOf('\n'), expression.range[1]).length;
} else {
endcol = startcol + (contract, expressionContent.length - 1);
}
contract.statementMap[contract.statementId] = {
start: {
line: startline, column: startcol,
},
end: {
line: endline, column: endcol,
},
};
createOrAppendInjectionPoint(contract, expression.range[0], {
type: 'statement', statementId: contract.statementId,
});
};
instrumenter.instrumentLine = function instrumentLine(contract, expression) {
// what's the position of the most recent newline?
const startchar = expression.range[0];
const endchar = expression.range[1] + 1;
const lastNewLine = contract.instrumented.slice(0, startchar).lastIndexOf('\n');
const nextNewLine = startchar + contract.instrumented.slice(startchar).indexOf('\n');
const contractSnipped = contract.instrumented.slice(lastNewLine, nextNewLine);
const restOfLine = contract.instrumented.slice(endchar, nextNewLine);
if (contract.instrumented.slice(lastNewLine, startchar).trim().length === 0 &&
(restOfLine.replace(';', '').trim().length === 0 || restOfLine.replace(';', '').trim().substring(0, 2) === '//')) {
createOrAppendInjectionPoint(contract, lastNewLine + 1, {
type: 'callEvent',
});
} else if (contract.instrumented.slice(lastNewLine, startchar).replace('{', '').trim().length === 0 &&
contract.instrumented.slice(endchar, nextNewLine).replace(/[;}]/g, '').trim().length === 0) {
createOrAppendInjectionPoint(contract, expression.range[0], {
type: 'callEvent',
});
_initializeCoverageFields(contract){
contract.runnableLines = [];
contract.fnMap = {};
contract.fnId = 0;
contract.branchMap = {};
contract.branchId = 0;
contract.statementMap = {};
contract.statementId = 0;
contract.injectionPoints = {};
}
// Is everything before us and after us on this line whitespace?
};
instrumenter.instrumentFunctionDeclaration = function instrumentFunctionDeclaration(contract, expression) {
contract.fnId += 1;
const startline = (contract.instrumented.slice(0, expression.range[0]).match(/\n/g) || []).length + 1;
// We need to work out the lines and columns the function declaration starts and ends
const startcol = expression.range[0] - contract.instrumented.slice(0, expression.range[0]).lastIndexOf('\n') - 1;
const endlineDelta = contract.instrumented.slice(expression.range[0]).indexOf('{');
const functionDefinition = contract.instrumented.slice(expression.range[0], expression.range[0] + endlineDelta);
const endline = startline + (functionDefinition.match(/\n/g) || []).length;
const endcol = functionDefinition.length - functionDefinition.lastIndexOf('\n');
contract.fnMap[contract.fnId] = {
name: expression.isConstructor ? 'constructor' : expression.name,
line: startline,
loc: {
start: {
line: startline, column: startcol,
},
end: {
line: endline, column: endcol,
},
},
};
createOrAppendInjectionPoint(contract, expression.range[0] + endlineDelta + 1, {
type: 'callFunctionEvent', fnId: contract.fnId,
/**
* Per `contractSource`:
* - wraps any unbracketed singleton consequents of if, for, while stmts (preprocessor.js)
* - walks the file's AST, creating an instrumentation map (parse.js, registrar.js)
* - injects `instrumentation` solidity statements into the target solidity source (injector.js)
*
* @param {String} contractSource solidity source code
* @param {String} fileName absolute path to source file
* @return {Object} instrumented `contract` object
* {
* contract: instrumented solidity source code,
* contractName: contract name,
* runnableLines: integer
* }
*
*/
instrument(contractSource, fileName) {
const contract = {};
contract.source = contractSource;
contract.instrumented = contractSource;
this._initializeCoverageFields(contract);
// 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)); // ?????
// Now, we reset almost everything and use the preprocessor to increase our effectiveness.
this._initializeCoverageFields(contract);
contract.instrumented = preprocess(contract.source);
// Walk the AST, recording injection points
ast = SolidityParser.parse(contract.instrumented, {range: true});
contract.contractName = ast.children.filter(node => this._isRootNode(node))[0].name;
parse[ast.type](contract, ast);
// We have to iterate through these points in descending order
const sortedPoints = Object.keys(contract.injectionPoints).sort((a, b) => b - a);
sortedPoints.forEach(injectionPoint => {
// Line instrumentation has to happen first
contract.injectionPoints[injectionPoint].sort((a, b) => {
const injections = ['injectBranch', 'injectEmptyBranch', 'injectLine'];
return injections.indexOf(b.type) - injections.indexOf(a.type);
});
};
instrumenter.addNewBranch = function addNewBranch(contract, expression) {
contract.branchId += 1;
const startline = (contract.instrumented.slice(0, expression.range[0]).match(/\n/g) || []).length + 1;
const startcol = expression.range[0] - contract.instrumented.slice(0, expression.range[0]).lastIndexOf('\n') - 1;
// NB locations for if branches in istanbul are zero length and associated with the start of the if.
contract.branchMap[contract.branchId] = {
line: startline,
type: 'if',
locations: [{
start: {
line: startline, column: startcol,
},
end: {
line: startline, column: startcol,
},
}, {
start: {
line: startline, column: startcol,
},
end: {
line: startline, column: startcol,
},
}],
};
};
instrumenter.instrumentAssertOrRequire = function instrumentAssertOrRequire(contract, expression) {
instrumenter.addNewBranch(contract, expression);
createOrAppendInjectionPoint(contract, expression.range[0], {
type: 'callAssertPreEvent', branchId: contract.branchId,
contract.injectionPoints[injectionPoint].forEach(injection => {
this.injector[injection.type](
contract,
fileName,
injectionPoint,
injection,
this.instrumentationData
);
});
createOrAppendInjectionPoint(contract, expression.range[1] + 2, {
type: 'callAssertPostEvent', branchId: contract.branchId,
});
};
instrumenter.instrumentIfStatement = function instrumentIfStatement(contract, expression) {
instrumenter.addNewBranch(contract, expression);
if (expression.trueBody.type === 'Block') {
createOrAppendInjectionPoint(contract, expression.trueBody.range[0] + 1, {
type: 'callBranchEvent', branchId: contract.branchId, locationIdx: 0,
});
retValue.runnableLines = contract.runnableLines;
retValue.contract = contract.instrumented;
retValue.contractName = contract.contractName;
return retValue;
}
if (expression.falseBody && expression.falseBody.type === 'IfStatement') {
// Do nothing - we must be pre-preprocessor, so don't bother instrumenting -
// when we're actually instrumenting, this will never happen (we've wrapped it in
// a block statement)
} else if (expression.falseBody && expression.falseBody.type === 'Block') {
createOrAppendInjectionPoint(contract, expression.falseBody.range[0] + 1, {
type: 'callBranchEvent', branchId: contract.branchId, locationIdx: 1,
});
} else {
createOrAppendInjectionPoint(contract, expression.trueBody.range[1] + 1, {
type: 'callEmptyBranchEvent', branchId: contract.branchId, locationIdx: 1,
});
}
};
module.exports = instrumenter;
module.exports = Instrumenter;

@ -1,39 +1,37 @@
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */
/**
* Methods in this file walk the AST and call the instrumenter
* functions where appropriate, which determine where to inject events.
* (Listed in alphabetical order)
*/
const Registrar = require('./registrar');
const register = new Registrar();
const parse = {};
const instrumenter = require('./instrumenter');
parse.AssignmentExpression = function parseAssignmentExpression(contract, expression) {
instrumenter.prePosition(expression);
instrumenter.instrumentStatement(contract, expression);
instrumenter.instrumentAssignmentExpression(contract, expression);
parse.AssignmentExpression = function(contract, expression) {
register.prePosition(expression);
register.statement(contract, expression);
};
parse.Block = function parseBlock(contract, expression) {
parse.Block = function(contract, expression) {
for (let x = 0; x < expression.statements.length; x++) {
instrumenter.instrumentLine(contract, expression.statements[x]);
register.line(contract, expression.statements[x]);
parse[expression.statements[x].type] &&
parse[expression.statements[x].type](contract, expression.statements[x]);
}
};
parse.BinaryOperation = function parseBinaryOperation(contract, expression) {
instrumenter.instrumentStatement(contract, expression);
parse.BinaryOperation = function(contract, expression) {
register.statement(contract, expression);
}
parse.FunctionCall = function parseCallExpression(contract, expression) {
// In any given chain of call expressions, only the last one will fail this check. This makes sure
// we don't instrument a chain of expressions multiple times.
parse.FunctionCall = function(contract, expression) {
// In any given chain of call expressions, only the last one will fail this check.
// This makes sure we don't instrument a chain of expressions multiple times.
if (expression.expression.type !== 'FunctionCall') {
instrumenter.instrumentStatement(contract, expression);
register.statement(contract, expression);
if (expression.expression.name === 'assert' || expression.expression.name === 'require') {
instrumenter.instrumentAssertOrRequire(contract, expression);
register.assertOrRequire(contract, expression);
}
parse[expression.expression.type] &&
parse[expression.expression.type](contract, expression.expression);
@ -43,28 +41,16 @@ parse.FunctionCall = function parseCallExpression(contract, expression) {
}
};
parse.ConditionalExpression = function parseConditionalExpression(contract, expression) {
instrumenter.instrumentStatement(contract, expression);
instrumenter.instrumentConditionalExpression(contract, expression);
parse.ConditionalExpression = function(contract, expression) {
register.statement(contract, expression);
register.conditionalExpression(contract, expression);
};
parse.ContractDefinition = function ParseContractStatement(contract, expression) {
parse.ContractDefinition = function(contract, expression) {
parse.ContractOrLibraryStatement(contract, expression);
};
parse.ContractOrLibraryStatement = function parseContractOrLibraryStatement(contract, expression) {
// From the start of this contract statement, find the first '{', and inject there.
const injectionPoint = expression.range[0] + contract.instrumented.slice(expression.range[0]).indexOf('{') + 1;
if (contract.injectionPoints[injectionPoint]) {
contract.injectionPoints[expression.range[0] + contract.instrumented.slice(expression.range[0]).indexOf('{') + 1].push({
type: 'eventDefinition',
});
} else {
contract.injectionPoints[expression.range[0] + contract.instrumented.slice(expression.range[0]).indexOf('{') + 1] = [{
type: 'eventDefinition',
}];
}
parse.ContractOrLibraryStatement = function(contract, expression) {
if (expression.subNodes) {
expression.subNodes.forEach(construct => {
parse[construct.type] &&
@ -73,33 +59,33 @@ parse.ContractOrLibraryStatement = function parseContractOrLibraryStatement(cont
}
};
parse.EmitStatement = function parseExpressionStatement(contract, expression){
instrumenter.instrumentStatement(contract, expression);
parse.EmitStatement = function(contract, expression){
register.statement(contract, expression);
};
parse.ExpressionStatement = function parseExpressionStatement(contract, content) {
parse.ExpressionStatement = function(contract, content) {
parse[content.expression.type] &&
parse[content.expression.type](contract, content.expression);
};
parse.ForStatement = function parseForStatement(contract, expression) {
instrumenter.instrumentStatement(contract, expression);
parse.ForStatement = function(contract, expression) {
register.statement(contract, expression);
parse[expression.body.type] &&
parse[expression.body.type](contract, expression.body);
};
parse.FunctionDefinition = function parseFunctionDefinition(contract, expression) {
parse.FunctionDefinition = function(contract, expression) {
parse.Modifiers(contract, expression.modifiers);
if (expression.body) {
instrumenter.instrumentFunctionDeclaration(contract, expression);
register.functionDeclaration(contract, expression);
parse[expression.body.type] &&
parse[expression.body.type](contract, expression.body);
}
};
parse.IfStatement = function parseIfStatement(contract, expression) {
instrumenter.instrumentStatement(contract, expression);
instrumenter.instrumentIfStatement(contract, expression);
parse.IfStatement = function(contract, expression) {
register.statement(contract, expression);
register.ifStatement(contract, expression);
parse[expression.trueBody.type] &&
parse[expression.trueBody.type](contract, expression.trueBody);
@ -109,20 +95,20 @@ parse.IfStatement = function parseIfStatement(contract, expression) {
}
};
parse.InterfaceStatement = function parseInterfaceStatement(contract, expression) {
parse.InterfaceStatement = function(contract, expression) {
parse.ContractOrLibraryStatement(contract, expression);
};
parse.LibraryStatement = function parseLibraryStatement(contract, expression) {
parse.LibraryStatement = function(contract, expression) {
parse.ContractOrLibraryStatement(contract, expression);
};
parse.MemberExpression = function parseMemberExpression(contract, expression) {
parse.MemberExpression = function(contract, expression) {
parse[expression.object.type] &&
parse[expression.object.type](contract, expression.object);
};
parse.Modifiers = function parseModifier(contract, modifiers) {
parse.Modifiers = function(contract, modifiers) {
if (modifiers) {
modifiers.forEach(modifier => {
parse[modifier.type] && parse[modifier.type](contract, modifier);
@ -130,52 +116,50 @@ parse.Modifiers = function parseModifier(contract, modifiers) {
}
};
parse.ModifierDefinition = function parseModifierDefinition(contract, expression) {
instrumenter.instrumentFunctionDeclaration(contract, expression);
parse.ModifierDefinition = function(contract, expression) {
register.functionDeclaration(contract, expression);
parse[expression.body.type] &&
parse[expression.body.type](contract, expression.body);
};
parse.NewExpression = function parseNewExpression(contract, expression) {
parse.NewExpression = function(contract, expression) {
parse[expression.typeName.type] &&
parse[expression.typeName.type](contract, expression.typeName);
};
parse.SourceUnit = function parseSourceUnit(contract, expression) {
parse.SourceUnit = function(contract, expression) {
expression.children.forEach(construct => {
parse[construct.type] &&
parse[construct.type](contract, construct);
});
};
parse.ReturnStatement = function parseReturnStatement(contract, expression) {
instrumenter.instrumentStatement(contract, expression);
parse.ReturnStatement = function(contract, expression) {
register.statement(contract, expression);
};
parse.UnaryExpression = function parseUnaryExpression(contract, expression) {
parse.UnaryExpression = function(contract, expression) {
parse[expression.argument.type] &&
parse[expression.argument.type](contract, expression.argument);
};
parse.UsingStatement = function parseUsingStatement(contract, expression) {
parse.UsingStatement = function (contract, expression) {
parse[expression.for.type] &&
parse[expression.for.type](contract, expression.for);
};
parse.VariableDeclarationStatement = function parseVariableDeclarationStatement(contract, expression) {
instrumenter.instrumentStatement(contract, expression);
// parse[expression.declarations[0].id.type] &&
// parse[expression.declarations[0].id.type](contract, expression.declarations[0].id);
parse.VariableDeclarationStatement = function (contract, expression) {
register.statement(contract, expression);
};
parse.VariableDeclarationTuple = function parseVariableDeclarationTuple(contract, expression) {
instrumenter.instrumentStatement(contract, expression);
parse.VariableDeclarationTuple = function (contract, expression) {
register.statement(contract, expression);
parse[expression.declarations[0].id.type] &&
parse[expression.declarations[0].id.type](contract, expression.declarations[0].id);
};
parse.WhileStatement = function parseWhileStatement(contract, expression) {
instrumenter.instrumentStatement(contract, expression);
parse.WhileStatement = function (contract, expression) {
register.statement(contract, expression);
parse[expression.body.type] &&
parse[expression.body.type](contract, expression.body);
};

@ -1,4 +1,3 @@
const SolExplore = require('sol-explore');
const SolidityParser = require('solidity-parser-antlr');
const crRegex = /[\r\n ]+$/g;
@ -6,52 +5,31 @@ const OPEN = '{';
const CLOSE = '}';
/**
* Splices enclosing brackets into `contract` around `expression`;
* Inserts an open or close brace e.g. `{` or `}` at specified position in solidity source
*
* @param {String} contract solidity source
* @param {Object} node AST node to bracket
* @param {Object} item AST node to bracket
* @param {Number} offset tracks the number of previously inserted braces
* @return {String} contract
*/
function insertBrace(contract, item, offset) {
return contract.slice(0,item.pos + offset) + item.type + contract.slice(item.pos + offset)
}
/** Remove 'pure' and 'view' from the function declaration.
* @param {String} contract solidity source
* @param {Object} function AST node
* @return {String} contract with the modifiers removed from the given function.
*/
function removePureView(contract, node){
let fDefStart = node.range[0];
if (node.body){
fDefEnd = node.body.range[0];
} else if (node.returnParameters) {
fDefEnd = node.returnParameters.range[0];
} else {
fDefEnd = node.range[1];
}
let fDef = contract.slice(fDefStart, fDefEnd + 1);
fDef = fDef.replace(/\bview\b/i, ' ');
fDef = fDef.replace(/\bpure\b/i, ' ');
return contract.slice(0, fDefStart) + fDef + contract.slice(fDefEnd + 1);
}
/**
* Locates unbracketed singleton statements attached to if, else, for and while statements
* and brackets them. Instrumenter needs to inject events at these locations and having
* them pre-bracketed simplifies the process. Each time a modification is made the contract
* is passed back to the parser and re-walked because all the starts and ends get shifted.
*
* Also removes pure and view modifiers.
* them pre-bracketed simplifies the process.
*
* @param {String} contract solidity code
* @return {String} contract
* @param {String} contract solidity source code
* @return {String} modified solidity source code
*/
module.exports.run = function r(contract) {
function preprocess(contract) {
try {
const ast = SolidityParser.parse(contract, { range: true });
insertions = [];
viewPureToRemove = [];
SolidityParser.visit(ast, {
IfStatement: function(node) {
if (node.trueBody.type !== 'Block') {
@ -74,23 +52,18 @@ module.exports.run = function r(contract) {
insertions.push({type: OPEN, pos: node.body.range[0]});
insertions.push({type: CLOSE, pos: node.body.range[1] + 1});
}
},
FunctionDefinition: function(node){
if (node.stateMutability === 'view' || node.stateMutability === 'pure'){
viewPureToRemove.push(node);
}
}
})
// Firstly, remove pures and views. Note that we replace 'pure' and 'view' with spaces, so
// character counts remain the same, so we can do this in any order
viewPureToRemove.forEach(node => contract = removePureView(contract, node));
// Sort the insertion points.
insertions.sort((a,b) => a.pos - b.pos);
insertions.forEach((item, idx) => contract = insertBrace(contract, item, idx));
} catch (err) {
contract = err;
keepRunning = false;
}
return contract;
};
module.exports = preprocess;

@ -0,0 +1,251 @@
/**
* Registers injections points (e.g source location, type) and their associated data with
* a contract / instrumentation target. Run during the `parse` step. This data is
* consumed by the Injector as it modifies the source code in instrumentation's final step.
*/
class Registrar {
constructor(){}
/**
* Adds injection point to injection points map
* @param {Object} contract instrumentation target
* @param {String} key injection point `type`
* @param {Number} value injection point `id`
*/
_createInjectionPoint(contract, key, value) {
(contract.injectionPoints[key])
? contract.injectionPoints[key].push(value)
: contract.injectionPoints[key] = [value];
}
/**
* TODO - idk what this is anymore
* @param {Object} expression AST node
*/
prePosition(expression) {
if (
expression.right.type === 'ConditionalExpression' &&
expression.left.type === 'MemberExpression'
) {
expression.range[0] -= 2;
}
}
/**
* Registers injections for statement measurements
* @param {Object} contract instrumentation target
* @param {Object} expression AST node
*/
statement(contract, expression) {
const startContract = contract.instrumented.slice(0, expression.range[0]);
const startline = ( startContract.match(/\n/g) || [] ).length + 1;
const startcol = expression.range[0] - startContract.lastIndexOf('\n') - 1;
const expressionContent = contract.instrumented.slice(
expression.range[0],
expression.range[1] + 1
);
const endline = startline + (contract, expressionContent.match('/\n/g') || []).length;
let endcol;
if (expressionContent.lastIndexOf('\n') >= 0) {
endcol = contract.instrumented.slice(
expressionContent.lastIndexOf('\n'),
expression.range[1]
).length;
} else endcol = startcol + (contract, expressionContent.length - 1);
contract.statementId += 1;
contract.statementMap[contract.statementId] = {
start: { line: startline, column: startcol },
end: { line: endline, column: endcol },
};
this._createInjectionPoint(contract, expression.range[0], {
type: 'injectStatement', statementId: contract.statementId,
});
};
/**
* Registers injections for line measurements
* @param {Object} contract instrumentation target
* @param {Object} expression AST node
*/
line(contract, expression) {
const startchar = expression.range[0];
const endchar = expression.range[1] + 1;
const lastNewLine = contract.instrumented.slice(0, startchar).lastIndexOf('\n');
const nextNewLine = startchar + contract.instrumented.slice(startchar).indexOf('\n');
const contractSnipped = contract.instrumented.slice(lastNewLine, nextNewLine);
const restOfLine = contract.instrumented.slice(endchar, nextNewLine);
if (
contract.instrumented.slice(lastNewLine, startchar).trim().length === 0 &&
(
restOfLine.replace(';', '').trim().length === 0 ||
restOfLine.replace(';', '').trim().substring(0, 2) === '//'
)
)
{
this._createInjectionPoint(contract, lastNewLine + 1, { type: 'injectLine' });
} else if (
contract.instrumented.slice(lastNewLine, startchar).replace('{', '').trim().length === 0 &&
contract.instrumented.slice(endchar, nextNewLine).replace(/[;}]/g, '').trim().length === 0)
{
this._createInjectionPoint(contract, expression.range[0], { type: 'injectLine' });
}
};
/**
* Registers injections for function measurements
* @param {Object} contract instrumentation target
* @param {Object} expression AST node
*/
functionDeclaration(contract, expression) {
const startContract = contract.instrumented.slice(0, expression.range[0]);
const startline = ( startContract.match(/\n/g) || [] ).length + 1;
const startcol = expression.range[0] - startContract.lastIndexOf('\n') - 1;
const endlineDelta = contract.instrumented.slice(expression.range[0]).indexOf('{');
const functionDefinition = contract.instrumented.slice(
expression.range[0],
expression.range[0] + endlineDelta
);
const endline = startline + (functionDefinition.match(/\n/g) || []).length;
const endcol = functionDefinition.length - functionDefinition.lastIndexOf('\n');
contract.fnId += 1;
contract.fnMap[contract.fnId] = {
name: expression.isConstructor ? 'constructor' : expression.name,
line: startline,
loc: {
start: { line: startline, column: startcol },
end: { line: endline, column: endcol },
},
};
this._createInjectionPoint(
contract,
expression.range[0] + endlineDelta + 1,
{
type: 'injectFunction',
fnId: contract.fnId,
}
);
};
/**
* Registers injections for branch measurements. This generic is consumed by
* the `assert/require` and `if` registration methods.
* @param {Object} contract instrumentation target
* @param {Object} expression AST node
*/
addNewBranch(contract, expression) {
const startContract = contract.instrumented.slice(0, expression.range[0]);
const startline = ( startContract.match(/\n/g) || [] ).length + 1;
const startcol = expression.range[0] - startContract.lastIndexOf('\n') - 1;
contract.branchId += 1;
// NB locations for if branches in istanbul are zero
// length and associated with the start of the if.
contract.branchMap[contract.branchId] = {
line: startline,
type: 'if',
locations: [{
start: {
line: startline, column: startcol,
},
end: {
line: startline, column: startcol,
},
}, {
start: {
line: startline, column: startcol,
},
end: {
line: startline, column: startcol,
},
}],
};
};
/**
* Registers injections for assert/require statement measurements (branches)
* @param {Object} contract instrumentation target
* @param {Object} expression AST node
*/
assertOrRequire(contract, expression) {
this.addNewBranch(contract, expression);
this._createInjectionPoint(
contract,
expression.range[0],
{
type: 'injectAssertPre',
branchId: contract.branchId,
}
);
this._createInjectionPoint(
contract,
expression.range[1] + 2,
{
type: 'injectAssertPost',
branchId: contract.branchId,
}
);
};
/**
* Registers injections for if statement measurements (branches)
* @param {Object} contract instrumentation target
* @param {Object} expression AST node
*/
ifStatement(contract, expression) {
this.addNewBranch(contract, expression);
if (expression.trueBody.type === 'Block') {
this._createInjectionPoint(
contract,
expression.trueBody.range[0] + 1,
{
type: 'injectBranch',
branchId: contract.branchId,
locationIdx: 0,
}
);
}
if (expression.falseBody && expression.falseBody.type === 'IfStatement') {
// Do nothing - we must be pre-preprocessing
} else if (expression.falseBody && expression.falseBody.type === 'Block') {
this._createInjectionPoint(
contract,
expression.falseBody.range[0] + 1,
{
type: 'injectBranch',
branchId: contract.branchId,
locationIdx: 1,
}
);
} else {
this._createInjectionPoint(
contract,
expression.trueBody.range[1] + 1,
{
type: 'injectEmptyBranch',
branchId: contract.branchId,
locationIdx: 1,
}
);
}
}
}
module.exports = Registrar;

@ -0,0 +1,121 @@
/**
* This is logic to instrument ternary conditional assignment statements. Preserving
* here for the time being, because instrumentation of these became impossible in
* solc >= 0.5.0
*/
function instrumentAssignmentExpression(contract, expression) {
// This is suspended for 0.5.0 which tries to accomodate the new `emit` keyword.
// Solc is not allowing us to use the construction `emit SomeEvent()` within the parens :/
return;
// --------------------------------------------------------------------------------------------
// The only time we instrument an assignment expression is if there's a conditional expression on
// the right
/*if (expression.right.type === 'ConditionalExpression') {
if (expression.left.type === 'DeclarativeExpression' || expression.left.type === 'Identifier') {
// Then we need to go from bytes32 varname = (conditional expression)
// to bytes32 varname; (,varname) = (conditional expression)
createOrAppendInjectionPoint(contract, expression.left.range[1], {
type: 'literal', string: '; (,' + expression.left.name + ')',
});
instrumenter.instrumentConditionalExpression(contract, expression.right);
} else if (expression.left.type === 'MemberExpression') {
createOrAppendInjectionPoint(contract, expression.left.range[0], {
type: 'literal', string: '(,',
});
createOrAppendInjectionPoint(contract, expression.left.range[1], {
type: 'literal', string: ')',
});
instrumenter.instrumentConditionalExpression(contract, expression.right);
} else {
const err = 'Error instrumenting assignment expression @ solidity-coverage/lib/instrumenter.js';
console.log(err, contract, expression.left);
process.exit();
}
}*/
};
function instrumentConditionalExpression(contract, expression) {
// ----------------------------------------------------------------------------------------------
// This is suspended for 0.5.0 which tries to accomodate the new `emit` keyword.
// Solc is not allowing us to use the construction `emit SomeEvent()` within the parens :/
// Very sad, this is the coolest thing in here.
return;
// ----------------------------------------------------------------------------------------------
/*contract.branchId += 1;
const startline = (contract.instrumented.slice(0, expression.range[0]).match(/\n/g) || []).length + 1;
const startcol = expression.range[0] - contract.instrumented.slice(0, expression.range[0]).lastIndexOf('\n') - 1;
const consequentStartCol = startcol + (contract, expression.trueBody.range[0] - expression.range[0]);
const consequentEndCol = consequentStartCol + (contract, expression.trueBody.range[1] - expression.trueBody.range[0]);
const alternateStartCol = startcol + (contract, expression.falseBody.range[0] - expression.range[0]);
const alternateEndCol = alternateStartCol + (contract, expression.falseBody.range[1] - expression.falseBody.range[0]);
// NB locations for conditional branches in istanbul are length 1 and associated with the : and ?.
contract.branchMap[contract.branchId] = {
line: startline,
type: 'cond-expr',
locations: [{
start: {
line: startline, column: consequentStartCol,
},
end: {
line: startline, column: consequentEndCol,
},
}, {
start: {
line: startline, column: alternateStartCol,
},
end: {
line: startline, column: alternateEndCol,
},
}],
};
// Right, this could be being used just by itself or as an assignment. In the case of the latter, because
// the comma operator doesn't exist, we're going to have to get funky.
// if we're on a line by ourselves, this is easier
//
// Now if we've got to wrap the expression it's being set equal to, do that...
// Wrap the consequent
createOrAppendInjectionPoint(contract, expression.trueBody.range[0], {
type: 'openParen',
});
createOrAppendInjectionPoint(contract, expression.trueBody.range[0], {
type: 'callBranchEvent', comma: true, branchId: contract.branchId, locationIdx: 0,
});
createOrAppendInjectionPoint(contract, expression.trueBody.range[1], {
type: 'closeParen',
});
// Wrap the alternate
createOrAppendInjectionPoint(contract, expression.falseBody.range[0], {
type: 'openParen',
});
createOrAppendInjectionPoint(contract, expression.falseBody.range[0], {
type: 'callBranchEvent', comma: true, branchId: contract.branchId, locationIdx: 1,
});
createOrAppendInjectionPoint(contract, expression.falseBody.range[1], {
type: 'closeParen',
});*/
};
// Paren / Literal injectors
/*
injector.openParen = function injectOpenParen(contract, fileName, injectionPoint, injection) {
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + '(' + contract.instrumented.slice(injectionPoint);
};
injector.closeParen = function injectCloseParen(contract, fileName, injectionPoint, injection) {
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + ')' + contract.instrumented.slice(injectionPoint);
};
injector.literal = function injectLiteral(contract, fileName, injectionPoint, injection) {
contract.instrumented = contract.instrumented.slice(0, injectionPoint) + injection.string + contract.instrumented.slice(injectionPoint);
};
*/

@ -1,16 +1,15 @@
{
"name": "solidity-coverage",
"version": "0.6.7",
"version": "0.7.0-beta.0",
"description": "",
"bin": {
"solidity-coverage": "./bin/exec.js"
},
"main": "index.js",
"buidler": "dist/buidler.coverage.js",
"directories": {
"test": "test"
},
"scripts": {
"test": "mocha --timeout 60000",
"test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --timeout 60000 --exit"
"test": "mocha test/units --timeout 70000 --no-warnings",
"test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --timeout 70000 --exit"
},
"homepage": "https://github.com/sc-forks/solidity-coverage",
"repository": {
@ -21,27 +20,22 @@
"license": "ISC",
"dependencies": {
"death": "^1.1.0",
"ethereumjs-testrpc-sc": "6.5.1-sc.1",
"ganache-cli": "^6.5.0",
"ganache-core": "^2.6.0",
"istanbul": "^0.4.5",
"keccakjs": "^0.2.1",
"req-cwd": "^1.0.1",
"sha1": "^1.1.1",
"shelljs": "^0.8.3",
"sol-explore": "^1.6.2",
"solidity-parser-antlr": "0.4.7",
"tree-kill": "^1.2.0",
"web3": "1.2.1",
"web3-eth-abi": "1.0.0-beta.55"
"solidity-parser-antlr": "0.4.5",
"web3-utils": "^1.0.0"
},
"devDependencies": {
"crypto-js": "^3.1.9-1",
"ethereumjs-account": "~2.0.4",
"ethereumjs-tx": "^1.2.2",
"ethereumjs-util": "^5.0.1",
"ethereumjs-vm": "https://github.com/sc-forks/ethereumjs-vm-sc.git#336d8841ab2c37da079d290ea5c5af6b34f20495",
"merkle-patricia-tree": "~2.1.2",
"mocha": "^4.1.0",
"request": "^2.88.0",
"solc": "^0.5.3",
"truffle": "^5.0.30"
"@nomiclabs/buidler": "^1.0.0-beta.8",
"@nomiclabs/buidler-truffle5": "^1.0.0-beta.8",
"@nomiclabs/buidler-web3": "^1.0.0-beta.8",
"mocha": "5.2.0",
"solc": "^0.5.10",
"truffle": "^5.0.26",
"web3": "1.0.0-beta.37"
}
}

@ -1,24 +0,0 @@
/* eslint-env node, mocha */
const assert = require('assert');
const fs = require('fs');
// Fake event for Simple.sol
const fakeEvent = {"address":"6d6cf716c2a7672047e15a255d4c9624db60f215","topics":["34b35f4b1a8c3eb2caa69f05fb5aadc827cedd2d8eb3bb3623b6c4bba3baec17"],"data":"00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000003a2f55736572732f757365722f53697465732f73632d666f726b732f6d657461636f696e2f636f6e7472616374732f4d657461436f696e2e736f6c000000000000"}
/* {
address: '7c548f8a5ba3a37774440587743bb50f58c7e91c',
topics: ['1accf53d733f86cbefdf38d52682bc905cf6715eb3d860be0b5b052e58b0741d'],
data: '0',
};*/
// Tests whether or not the testCommand option is invoked by exec.js
// Mocha's default timeout is 2000 - here we fake the creation of
// allFiredEvents at 4000.
describe('Test uses mocha', () => {
it('should run "mocha --timeout 5000" successfully', done => {
setTimeout(() => {
fs.writeFileSync('./../allFiredEvents', JSON.stringify(fakeEvent) + '\n');
done();
}, 4000);
});
});

@ -1,21 +0,0 @@
/* eslint-env node, mocha */
/* global artifacts, contract, assert */
const Events = artifacts.require('./Events.sol');
contract('Events', accounts => {
it('logs events correctly', done => {
const loggedEvents = [];
Events.deployed().then(instance => {
const allEvents = instance.allEvents();
allEvents.on("data", event => { loggedEvents.push(event); });
instance.test(5).then(() => {
const bad = loggedEvents.filter(e => e.event !== 'LogEventOne' && e.event !== 'LogEventTwo');
assert(bad.length === 0, 'Did not filter events correctly');
done();
});
});
});
});

@ -1,274 +0,0 @@
/* eslint-env node, mocha */
const solc = require('solc');
const path = require('path');
const getInstrumentedVersion = require('./../lib/instrumentSolidity.js');
const util = require('./util/util.js');
const CoverageMap = require('./../lib/coverageMap');
const vm = require('./util/vm');
const assert = require('assert');
describe('if, else, and else if statements', () => {
const filePath = path.resolve('./test.sol');
const pathPrefix = './';
it('should compile after instrumenting multiple if-elses', () => {
const contract = util.getCode('if/else-if-unbracketed-multi.sol');
const info = getInstrumentedVersion(contract, filePath);
const output = JSON.parse(solc.compile(util.codeToCompilerInput(info.contract)));
util.report(output.errors);
});
it('should compile after instrumenting unbracketed if-elses', () => {
const contract = util.getCode('if/if-else-no-brackets.sol');
const info = getInstrumentedVersion(contract, filePath);
const output = JSON.parse(solc.compile(util.codeToCompilerInput(info.contract)));
util.report(output.errors);
});
it('should cover an if statement with a bracketed consequent', done => {
const contract = util.getCode('if/if-with-brackets.sol');
const info = getInstrumentedVersion(contract, filePath);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
// Runs: a(1) => if (x == 1) { x = 3; }
vm.execute(info.contract, 'a', [1]).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {
5: 1,
});
assert.deepEqual(mapping[filePath].b, {
1: [1, 0],
});
assert.deepEqual(mapping[filePath].s, {
1: 1, 2: 1,
});
assert.deepEqual(mapping[filePath].f, {
1: 1,
});
done();
}).catch(done);
});
// Runs: a(1) => if (x == 1) x = 2;
it('should cover an unbracketed if consequent (single line)', done => {
const contract = util.getCode('if/if-no-brackets.sol');
const info = getInstrumentedVersion(contract, filePath);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
// Same results as previous test
vm.execute(info.contract, 'a', [1]).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {
5: 1,
});
assert.deepEqual(mapping[filePath].b, {
1: [1, 0],
});
assert.deepEqual(mapping[filePath].s, {
1: 1, 2: 1,
});
assert.deepEqual(mapping[filePath].f, {
1: 1,
});
done();
}).catch(done);
});
it('should cover an if statement with multiline bracketed consequent', done => {
const contract = util.getCode('if/if-with-brackets-multiline.sol');
const info = getInstrumentedVersion(contract, filePath);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
// Runs: a(1) => if (x == 1){\n x = 3; }
vm.execute(info.contract, 'a', [1]).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {
5: 1, 6: 1,
});
assert.deepEqual(mapping[filePath].b, {
1: [1, 0],
});
assert.deepEqual(mapping[filePath].s, {
1: 1, 2: 1,
});
assert.deepEqual(mapping[filePath].f, {
1: 1,
});
done();
}).catch(done);
});
// Runs: a(1) => if (x == 1)\n x = 3;
it('should cover an unbracketed if consequent (multi-line)', done => {
const contract = util.getCode('if/if-no-brackets-multiline.sol');
const info = getInstrumentedVersion(contract, filePath);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
// Same results as previous test
vm.execute(info.contract, 'a', [1]).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {
5: 1, 6: 1,
});
assert.deepEqual(mapping[filePath].b, {
1: [1, 0],
});
assert.deepEqual(mapping[filePath].s, {
1: 1, 2: 1,
});
assert.deepEqual(mapping[filePath].f, {
1: 1,
});
done();
}).catch(done);
});
it('should cover a simple if statement with a failing condition', done => {
const contract = util.getCode('if/if-with-brackets.sol');
const info = getInstrumentedVersion(contract, filePath);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
// Runs: a(2) => if (x == 1) { x = 3; }
vm.execute(info.contract, 'a', [2]).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {
5: 1,
});
assert.deepEqual(mapping[filePath].b, {
1: [0, 1],
});
assert.deepEqual(mapping[filePath].s, {
1: 1, 2: 0,
});
assert.deepEqual(mapping[filePath].f, {
1: 1,
});
done();
}).catch(done);
});
// Runs: a(2) => if (x == 1){\n throw;\n }else{\n x = 5; \n}
it('should cover an if statement with a bracketed alternate', done => {
const contract = util.getCode('if/else-with-brackets.sol');
const info = getInstrumentedVersion(contract, filePath);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
vm.execute(info.contract, 'a', [2]).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {
5: 1, 6: 0, 8: 1,
});
assert.deepEqual(mapping[filePath].b, {
1: [0, 1],
});
assert.deepEqual(mapping[filePath].s, {
1: 1, 2: 0, 3: 1,
});
assert.deepEqual(mapping[filePath].f, {
1: 1,
});
done();
}).catch(done);
});
it('should cover an if statement with an unbracketed alternate', done => {
const contract = util.getCode('if/else-without-brackets.sol');
const info = getInstrumentedVersion(contract, filePath);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
vm.execute(info.contract, 'a', [2]).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {
5: 1, 6: 0, 8: 1,
});
assert.deepEqual(mapping[filePath].b, {
1: [0, 1],
});
assert.deepEqual(mapping[filePath].s, {
1: 1, 2: 0, 3: 1,
});
assert.deepEqual(mapping[filePath].f, {
1: 1,
});
done();
}).catch(done);
});
it('should cover an else if statement with an unbracketed alternate', done => {
const contract = util.getCode('if/else-if-without-brackets.sol');
const info = getInstrumentedVersion(contract, filePath);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
vm.execute(info.contract, 'a', [2]).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {
5: 1, 6: 0, 8: 0,
});
assert.deepEqual(mapping[filePath].b, {
1: [0, 1], 2: [0, 1]
});
assert.deepEqual(mapping[filePath].s, {
1: 1, 2: 0, 3: 1, 4: 0
});
assert.deepEqual(mapping[filePath].f, {
1: 1,
});
done();
}).catch(done);
});
it('should cover nested if statements with missing else statements', done => {
const contract = util.getCode('if/nested-if-missing-else.sol');
const info = getInstrumentedVersion(contract, filePath);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
vm.execute(info.contract, 'a', [2, 3, 3]).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {
5: 1, 7: 1,
});
assert.deepEqual(mapping[filePath].b, {
1: [0, 1], 2: [1, 0], 3: [1, 0],
});
assert.deepEqual(mapping[filePath].s, {
1: 1, 2: 1, 3: 1,
});
assert.deepEqual(mapping[filePath].f, {
1: 1,
});
done();
}).catch(done);
});
it('should cover if-elseif-else statements that are at the same depth as each other', done => {
const contract = util.getCode('if/if-elseif-else.sol');
const info = getInstrumentedVersion(contract, filePath);
const coverage = new CoverageMap();
coverage.addContract(info, filePath);
vm.execute(info.contract, 'a', [2, 3, 3]).then(events => {
const mapping = coverage.generate(events, pathPrefix);
assert.deepEqual(mapping[filePath].l, {
5: 1, 6: 0, 8: 1, 10: 0, 13: 1, 14: 0, 16: 1, 18: 0,
});
assert.deepEqual(mapping[filePath].b, {
1: [0, 1], 2: [1, 0], 3: [0, 1], 4: [1, 0]
});
assert.deepEqual(mapping[filePath].s, {
1: 1, 2: 0, 3: 1, 4: 1, 5: 0, 6: 1, 7: 0, 8: 1, 9: 1, 10: 0,
});
assert.deepEqual(mapping[filePath].f, {
1: 1,
});
done();
}).catch(done);
});
});

@ -1,6 +1,3 @@
/* eslint-env node, mocha */
/* global artifacts, contract, assert */
const Owned = artifacts.require('./Owned.sol');
const Proxy = artifacts.require('./Proxy.sol');

@ -1,6 +1,3 @@
/* eslint-env node, mocha */
/* global artifacts, contract, assert */
const Simple = artifacts.require('./Simple.sol');
contract('Simple', () => {

@ -1,6 +1,3 @@
/* eslint-env node, mocha */
/* global artifacts, contract, assert, web3 */
const Wallet = artifacts.require('./Wallet.sol');
contract('Wallet', accounts => {
@ -11,15 +8,15 @@ contract('Wallet', accounts => {
await walletA.sendTransaction({
value: web3.utils.toBN(500), from: accounts[0],
});
console.log('transaction done')
await walletA.sendPayment(50, walletB.address, {
from: accounts[0],
});
console.log('transaction done')
await walletA.transferPayment(50, walletB.address, {
from: accounts[0],
});
console.log('transaction done')
const balance = await walletB.getBalance();
assert.equal(balance.toNumber(), 100);
});

@ -0,0 +1,9 @@
pragma solidity ^0.5.0;
contract Test {
function a(bool _a, bool _b, bool _c) public {
require(_a &&
_b &&
_c);
}
}

@ -1,8 +1,8 @@
pragma solidity ^0.5.0;
import "./../assets/Face.sol";
import "./../assets/PureView.sol";
import "./../assets/CLibrary.sol";
import "./../../externalSources/Face.sol";
import "./../../externalSources/PureView.sol";
import "./../../externalSources/CLibrary.sol";
contract TotallyPure is PureView, Face {
uint onehundred = 99;

@ -8,7 +8,7 @@ contract Test {
Vote vote;
function a() public {
var isYay = false;
bool isYay = false;
vote.voted[msg.sender] = isYay ? 1 : 2;
}
}

@ -1,3 +1,5 @@
pragma solidity ^0.5.0;
contract Test {
struct Fn {
function(bytes32) internal view returns(bool) startConditions;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save