JoinColony / solcover

Code coverage for solidity
MIT License
64 stars 8 forks source link

Model tests after Istanbul #13

Closed area closed 7 years ago

area commented 7 years ago

Currently, the tests just check that after instrumentation, the contract still compiles. Of course we actually want to check that it compiles correctly, and returns the right instrumentation information. @cgewecke identified istanbul's tests as the sort of thing to aspire to, which makes sense given the use of istanbul in this project!

The below snipped is a (very rough and ready) reimplementation of the first test in our test suite (NB with the throw removed from the test contract, which stops any events from firing). It takes the contract, instruments it, compiles it, deploys it to a chain in memory, calls a function, and then compares the results to what we'd expect our instrumentation to conclude.

Obviously, this all needs to be put into a wrapper so that the rests of the tests are DRY, and similarly should use the same code that runCoveredTests.js uses, rather than just copying it into the helper.

I also don't like the use of async here, but that's a discussion for another issue, and is a consequence of me basing this off of a tutorial in `ethereumjs-vm, which I hadn't used before.

  it('should compile after instrumenting else statements with brackets',function(done){
    var contract = util.getCode('if/else-with-brackets.sol');
    var instrumentedContractInfo = getInstrumentedVersion(contract, "test.sol", true);

    var coverage = {};
    var canonicalContractPath = path.resolve('./../originalContracts/' + "test.sol");
    coverage[canonicalContractPath] = { "l": {}, "path": canonicalContractPath, "s": {}, "b": {}, "f": {}, "fnMap": {}, "statementMap": {}, "branchMap": {} };
    for (idx in instrumentedContractInfo.runnableLines) {
        coverage[canonicalContractPath]["l"][instrumentedContractInfo.runnableLines[idx]] = 0;
    }
    coverage[canonicalContractPath].fnMap = instrumentedContractInfo.fnMap;
    for (x=1; x<=Object.keys(instrumentedContractInfo.fnMap).length; x++ ){
        coverage[canonicalContractPath]["f"][x] = 0;
    }
    coverage[canonicalContractPath].branchMap = instrumentedContractInfo.branchMap;
    for (x=1; x<=Object.keys(instrumentedContractInfo.branchMap).length; x++ ){
        coverage[canonicalContractPath]["b"][x] = [0,0];
    }
    coverage[canonicalContractPath].statementMap= instrumentedContractInfo.statementMap;
    for (x=1; x<=Object.keys(instrumentedContractInfo.statementMap).length; x++ ){
        coverage[canonicalContractPath]["s"][x] = 0;
    }

    var output = solc.compile(instrumentedContractInfo.contract, 1);
    var code = output.contracts.Test.bytecode;

    var VM = require('ethereumjs-vm')
    var Account = require('ethereumjs-account')
    var utils = require('ethereumjs-util');
    var Transaction = require('ethereumjs-tx');
    var Trie = require('merkle-patricia-tree');

    var stateTrie = new Trie();
    var vm = new VM({state: stateTrie});
    //code needs to be a buffer
    code = new Buffer(code, 'hex')

    //we will use this later
    var storageRoot;

    //Don't use this address for anything, obviously!
    var secretKey = 'e81cb653c260ee12c72ec8750e6bfd8a4dc2c3d7e3ede68dd2f150d0a67263d8';
    var address = new Buffer('7caf6f9bc8b3ba5c7824f934c826bd6dc38c8467', 'hex');

    function setup(cb) {
      //create a new account
      var account = new Account();

      //give the account some wei.
      //This needs to be a `Buffer` or a string. all strings need to be in hex.
      account.balance = 'f00000000000000000';

      //store in the trie
      stateTrie.put(address, account.serialize(), cb);
    }

    var rawTx = {
      gasPrice: '1',
      gasLimit: 'ffffff',
      data: code
    };

    var createdAddress;

    function runTx(raw, cb) {
      //create a new transaction out of the json

      var tx = new Transaction(raw);

      tx.sign(new Buffer(secretKey, 'hex'));

      //run the tx
      vm.runTx({tx:tx}, function(err, results) {
        if (err){
          return cb(err)
        }
        createdAddress = results.createdAddress;
        cb();
      });
    }

    var rawTx2 = {
      gasPrice: '1',
      gasLimit: 'ffffff',
      data: utils.bufferToHex(utils.sha3('a(uint256)')).substr(0,10) + "0".repeat(63) + "1" ,
      //Note even though the contract is a(uint), uint is just an alias for uint256 so we have to use
      //that when working out the hash.
      nonce: 0x1
    };

    var seenEvents = "";

    function runTest(raw, cb){
      raw.to = utils.bufferToHex(createdAddress);
      var tx = new Transaction(raw);
      tx.sign(new Buffer(secretKey, 'hex'));
      vm.runTx({tx:tx}, function(err, results) {
        if (err){
          return cb(err)
        }
        results.vm.runState.logs.map(function(log){
          var toWrite = {};
          toWrite.address= log[0].toString('hex');
          toWrite.topics = log[1].map(function(x){return x.toString('hex')})
          toWrite.data = log[2].toString('hex');
          seenEvents += JSON.stringify(toWrite) + '\n'
        })

        cb();
      });
    }

    function checkTest(cb){
      var events = seenEvents.split('\n').slice(0,-1);
      events.map(function(event){
        event = JSON.parse(event);

        if (event.topics.indexOf("b8995a65f405d9756b41a334f38d8ff0c93c4934e170d3c1429c3e7ca101014d") >= 0) {
            var data = SolidityCoder.decodeParams(["string", "uint256"], event.data.replace("0x", ""));
            var canonicalContractPath = path.resolve('./../originalContracts/' + path.basename(data[0]));
            coverage[canonicalContractPath]["l"][data[1].toNumber()] += 1;
        }else if(event.topics.indexOf("d4ce765fd23c5cc3660249353d61ecd18ca60549dd62cb9ca350a4244de7b87f")>=0){
            var data = SolidityCoder.decodeParams(["string", "uint256"], event.data.replace("0x", ""));
            var canonicalContractPath = path.resolve('./../originalContracts/' + path.basename(data[0]));
            coverage[canonicalContractPath]["f"][data[1].toNumber()] += 1;
        }else if(event.topics.indexOf("d4cf56ed5ba572684f02f889f12ac42d9583c8e3097802060e949bfbb3c1bff5")>=0){
            var data = SolidityCoder.decodeParams(["string", "uint256", "uint256"], event.data.replace("0x", ""));
            var canonicalContractPath = path.resolve('./../originalContracts/' + path.basename(data[0]));
            coverage[canonicalContractPath]["b"][data[1].toNumber()][data[2].toNumber()] += 1;
        }else if(event.topics.indexOf("b51abbff580b3a34bbc725f2dc6f736e9d4b45a41293fd0084ad865a31fde0c8")>=0){
            var data = SolidityCoder.decodeParams(["string","uint256"], event.data.replace("0x", ""));
            var canonicalContractPath = path.resolve('./../originalContracts/' + path.basename(data[0]));
            coverage[canonicalContractPath]["s"][data[1].toNumber()]+= 1;
        }
      })
      assert.deepEqual(coverage[canonicalContractPath].l, { '5': 1, '6': 1, '8': 0 })
      assert.deepEqual(coverage[canonicalContractPath].b, { '1': [ 1, 0 ] })
      assert.deepEqual(coverage[canonicalContractPath].s, { '1': 1, '2': 1, '3': 0 })
      assert.deepEqual(coverage[canonicalContractPath].f, { '1': 1 })

      cb();
    }

    async.series([
        setup,
        async.apply(runTx, rawTx), //Deploy the contract
        async.apply(runTest, rawTx2), //Call the contract
        checkTest
    ], done);

  })
cgewecke commented 7 years ago

Fantastic! Wow. 👍 👍 👍 :boom: