From 5d2e3c4079116a7cab5e5fa73399168e5de7b660 Mon Sep 17 00:00:00 2001 From: kazk Date: Tue, 22 Aug 2017 00:36:59 -0700 Subject: [PATCH] Add Solidity runner Truffle v3.4.8 (core: 3.4.8) Solidity v0.4.15 (solc-js) --- .eslintignore | 1 + .travis.yml | 1 + Makefile | 2 +- docker/solidity.docker | 47 ++ documentation/environments/solidity.md | 11 + examples/solidity.yml | 1 + .../solidity/truffle/contracts/Migrations.sol | 23 + .../solidity/truffle/contracts/setup.sol | 7 + .../solidity/truffle/contracts/solution.sol | 36 ++ .../truffle/migrations/1_initial_migration.js | 5 + .../truffle/migrations/2_deploy_contracts.js | 8 + frameworks/solidity/truffle/package.json | 13 + frameworks/solidity/truffle/start-testrpc.sh | 5 + frameworks/solidity/truffle/test/fixture.js | 75 +++ frameworks/solidity/truffle/truffle.js | 12 + lib/config.js | 1 + lib/runners/solidity.js | 120 ++++ lib/services.js | 20 +- test/runners/solidity_spec.js | 536 ++++++++++++++++++ 19 files changed, 922 insertions(+), 2 deletions(-) create mode 100644 docker/solidity.docker create mode 100644 documentation/environments/solidity.md create mode 100644 examples/solidity.yml create mode 100644 frameworks/solidity/truffle/contracts/Migrations.sol create mode 100644 frameworks/solidity/truffle/contracts/setup.sol create mode 100644 frameworks/solidity/truffle/contracts/solution.sol create mode 100644 frameworks/solidity/truffle/migrations/1_initial_migration.js create mode 100644 frameworks/solidity/truffle/migrations/2_deploy_contracts.js create mode 100644 frameworks/solidity/truffle/package.json create mode 100644 frameworks/solidity/truffle/start-testrpc.sh create mode 100644 frameworks/solidity/truffle/test/fixture.js create mode 100644 frameworks/solidity/truffle/truffle.js create mode 100644 lib/runners/solidity.js create mode 100644 test/runners/solidity_spec.js diff --git a/.eslintignore b/.eslintignore index 9d6ced52..a386eb9d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ frameworks/java +frameworks/solidity/truffle diff --git a/.travis.yml b/.travis.yml index c083dc70..a83d7d28 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,6 +42,7 @@ env: - TEST_IMG=elixir - TEST_IMG=powershell - TEST_IMG=gradle + - TEST_IMG=solidity script: - eslint '**/*.js' diff --git a/Makefile b/Makefile index 1e16602b..f54334ca 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ HOSTNAME=codewars -CONTAINERS=node dotnet jvm java python ruby alt rust julia systems dart crystal ocaml swift haskell objc go lua esolangs chapel nim r erlang elixir powershell gradle +CONTAINERS=node dotnet jvm java python ruby alt rust julia systems dart crystal ocaml swift haskell objc go lua esolangs chapel nim r erlang elixir powershell gradle solidity ALL_CONTAINERS=${CONTAINERS} base diff --git a/docker/solidity.docker b/docker/solidity.docker new file mode 100644 index 00000000..b1c5a00b --- /dev/null +++ b/docker/solidity.docker @@ -0,0 +1,47 @@ +FROM node:8.4.0-alpine +RUN apk add --no-cache bash git coreutils findutils + +RUN adduser -D codewarrior +RUN ln -s /home/codewarrior /workspace + +# ethereumjs-testrpc >= 3.0.2, requires Node >= 6.9.1 +ENV NPM_CONFIG_LOGLEVEL=warn \ + NODE_PATH=/usr/local/lib/node_modules +RUN npm -g install \ + truffle@3.4.8 \ + ethereumjs-testrpc@4.0.1 \ + \ + # for testing solidity-runner + mocha@3.5.0 \ + chai@4.1.1 \ + && rm -rf /tmp/npm-* + +WORKDIR /runner +COPY package.json package.json +RUN npm install --production && rm -rf /tmp/npm-* + +COPY frameworks/solidity/truffle /workspace/solidity +RUN chown -R codewarrior:codewarrior /workspace/solidity +COPY frameworks/javascript/mocha-reporter.js /runner/frameworks/solidity/ + +COPY *.js ./ +COPY lib/*.js lib/ +COPY lib/*.sh lib/ +COPY lib/utils lib/utils +COPY lib/runners/solidity.js lib/runners/ +COPY test/runner.js test/ +COPY test/runners/solidity_spec.js test/runners/ + +USER codewarrior +ENV USER=codewarrior HOME=/home/codewarrior + +RUN cd /workspace/solidity \ + && npm install \ + && rm -rf /tmp/npm-* \ +#&& npm test \ + && rm -f ./contracts/setup.sol ./contracts/solution.sol \ + && rm -rf /tmp/test-* \ + && truffle version +RUN mocha test/runners/solidity_spec.js + +ENTRYPOINT ["node"] diff --git a/documentation/environments/solidity.md b/documentation/environments/solidity.md new file mode 100644 index 00000000..7c2ea77c --- /dev/null +++ b/documentation/environments/solidity.md @@ -0,0 +1,11 @@ +## Language + +Solidity v0.4.13 (Truffle v3.4.8) + +## Test Framework + +[Mocha](http://truffleframework.com/docs/getting_started/javascript-tests) + +## Packages + +- web3 diff --git a/examples/solidity.yml b/examples/solidity.yml new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/examples/solidity.yml @@ -0,0 +1 @@ +# TODO diff --git a/frameworks/solidity/truffle/contracts/Migrations.sol b/frameworks/solidity/truffle/contracts/Migrations.sol new file mode 100644 index 00000000..c564f090 --- /dev/null +++ b/frameworks/solidity/truffle/contracts/Migrations.sol @@ -0,0 +1,23 @@ +pragma solidity ^0.4.13; + +contract Migrations { + address public owner; + uint public last_completed_migration; + + modifier restricted() { + if (msg.sender == owner) _; + } + + function Migrations() { + owner = msg.sender; + } + + function setCompleted(uint completed) restricted { + last_completed_migration = completed; + } + + function upgrade(address new_address) restricted { + Migrations upgraded = Migrations(new_address); + upgraded.setCompleted(last_completed_migration); + } +} diff --git a/frameworks/solidity/truffle/contracts/setup.sol b/frameworks/solidity/truffle/contracts/setup.sol new file mode 100644 index 00000000..ef45bee2 --- /dev/null +++ b/frameworks/solidity/truffle/contracts/setup.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.4.13; + +library ConvertLib { + function convert(uint amount, uint conversionRate) returns (uint convertedAmount) { + return amount * conversionRate; + } +} diff --git a/frameworks/solidity/truffle/contracts/solution.sol b/frameworks/solidity/truffle/contracts/solution.sol new file mode 100644 index 00000000..9f8bae4a --- /dev/null +++ b/frameworks/solidity/truffle/contracts/solution.sol @@ -0,0 +1,36 @@ +pragma solidity ^0.4.13; + +import "./setup.sol"; + +contract MetaCoin { + mapping (address => uint) balances; + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + uint public endTime; // for time-travel demo + function MetaCoin() { + balances[tx.origin] = 10000; + endTime = now + 1 days; + } + + function sendCoin(address receiver, uint amount) returns(bool sufficient) { + if (balances[msg.sender] < amount) return false; + balances[msg.sender] -= amount; + balances[receiver] += amount; + Transfer(msg.sender, receiver, amount); + return true; + } + + function getBalanceInEth(address addr) returns(uint) { + return ConvertLib.convert(getBalance(addr), 2); + } + + function getBalance(address addr) returns(uint) { + return balances[addr]; + } + + // for time travel demo, only valid for a day + function isValid() returns(bool) { + return now <= endTime; + } +} diff --git a/frameworks/solidity/truffle/migrations/1_initial_migration.js b/frameworks/solidity/truffle/migrations/1_initial_migration.js new file mode 100644 index 00000000..6e0a9f15 --- /dev/null +++ b/frameworks/solidity/truffle/migrations/1_initial_migration.js @@ -0,0 +1,5 @@ +var Migrations = artifacts.require("Migrations"); + +module.exports = function(deployer) { + deployer.deploy(Migrations); +}; diff --git a/frameworks/solidity/truffle/migrations/2_deploy_contracts.js b/frameworks/solidity/truffle/migrations/2_deploy_contracts.js new file mode 100644 index 00000000..d0fa7286 --- /dev/null +++ b/frameworks/solidity/truffle/migrations/2_deploy_contracts.js @@ -0,0 +1,8 @@ +var ConvertLib = artifacts.require("ConvertLib"); +var MetaCoin = artifacts.require("MetaCoin"); + +module.exports = function(deployer) { + deployer.deploy(ConvertLib); + deployer.link(ConvertLib, [MetaCoin]); + deployer.deploy(MetaCoin); +}; diff --git a/frameworks/solidity/truffle/package.json b/frameworks/solidity/truffle/package.json new file mode 100644 index 00000000..d1566748 --- /dev/null +++ b/frameworks/solidity/truffle/package.json @@ -0,0 +1,13 @@ +{ + "name": "codewars-truffle", + "private": true, + "version": "1.0.0", + "description": "", + "license": "BSD-3-Clause", + "scripts": { + "test": "bash ./start-testrpc.sh && truffle test" + }, + "dependencies": { + "web3": "0.20.x" + } +} diff --git a/frameworks/solidity/truffle/start-testrpc.sh b/frameworks/solidity/truffle/start-testrpc.sh new file mode 100644 index 00000000..d0caebd4 --- /dev/null +++ b/frameworks/solidity/truffle/start-testrpc.sh @@ -0,0 +1,5 @@ +#!/bin/bash +if nc -z 127.0.0.1 8545; then # TestRPC already running + pkill -f "node /usr/local/bin/testrpc" +fi +testrpc & diff --git a/frameworks/solidity/truffle/test/fixture.js b/frameworks/solidity/truffle/test/fixture.js new file mode 100644 index 00000000..a6fed0b8 --- /dev/null +++ b/frameworks/solidity/truffle/test/fixture.js @@ -0,0 +1,75 @@ +// Example tests produced by `truffle init`, changed to use async/await and added time-travel demo +const MetaCoin = artifacts.require("MetaCoin"); + +const Web3 = require('web3'); +const web3 = new Web3(); +web3.setProvider(new Web3.providers.HttpProvider("http://localhost:8545")); + +contract('MetaCoin', function(accounts) { + it("should put 10000 MetaCoin in the first account", async function() { + const m = await MetaCoin.deployed(); + const balance = await m.getBalance.call(accounts[0]); + assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account"); + }); + + it("should call a function that depends on a linked library", async function() { + const m = await MetaCoin.deployed(); + const coinBalance = await m.getBalance.call(accounts[0]); + const coinEthBalance = await m.getBalanceInEth.call(accounts[0]); + assert.equal(coinEthBalance.toNumber(), 2*coinBalance.toNumber(), "Library function returned unexpected function, linkage may be broken"); + }); + + it("should send coin correctly", async function() { + const amount = 10; + const m = await MetaCoin.deployed(); + // Get initial balances of first and second account. + const account1 = accounts[0], account2 = accounts[1]; + const account1Start = (await m.getBalance.call(account1)).toNumber(); + const account2Start = (await m.getBalance.call(account2)).toNumber(); + + await m.sendCoin(account2, amount, {from: account1}); + + const account1End = (await m.getBalance.call(account1)).toNumber(); + const account2End = (await m.getBalance.call(account2)).toNumber(); + + assert.equal(account1End, account1Start - amount, "Amount wasn't correctly taken from the sender"); + assert.equal(account2End, account2Start + amount, "Amount wasn't correctly sent to the receiver"); + }); + + it("should be valid if the time is within a day from creation", async function() { + const m = await MetaCoin.new(); + await increaseTime(10); // 10 seconds + assert.equal(await m.isValid.call(), true, "status wasn't valid after 10 seconds"); + }); + + it("should be invalid after a day", async function() { + const m = await MetaCoin.new(); + await increaseTime(2 * 86400); // 2 days later + assert.equal(await m.isValid.call(), false, "status was valid even after a day"); + }); +}); + +function increaseTime(seconds) { + return new Promise((resolve, reject) => { + web3.currentProvider.sendAsync({ + jsonrpc: "2.0", + method: "evm_increaseTime", + params: [seconds], + id: new Date().getTime() + }, (err, result) => { + if (err) return reject(err); + // resolve(result); + // HACK workarounds https://github.com/ethereumjs/testrpc/issues/336 + mineBlock().then(_ => resolve(result)).catch(reject); + }); + }); +} + +function mineBlock() { + return new Promise((resolve, reject) => { + web3.currentProvider.sendAsync({ + jsonrpc: "2.0", + method: "evm_mine" + }, (err, result) => err ? reject(err) : resolve(result)); + }); +} diff --git a/frameworks/solidity/truffle/truffle.js b/frameworks/solidity/truffle/truffle.js new file mode 100644 index 00000000..0882db55 --- /dev/null +++ b/frameworks/solidity/truffle/truffle.js @@ -0,0 +1,12 @@ +module.exports = { + networks: { + development: { + host: "localhost", + port: 8545, + network_id: "*" // Match any network id + } + }, + mocha: { + reporter: '/runner/frameworks/solidity/mocha-reporter.js', + }, +}; diff --git a/lib/config.js b/lib/config.js index 1f5f5bb5..b37abde6 100644 --- a/lib/config.js +++ b/lib/config.js @@ -19,6 +19,7 @@ module.exports = { kotlin: 23000, groovy: 23000, scala: 27000, + solidity: 20000, }, moduleRegExs: { haskell: /module\s+([A-Z]([a-z|A-Z|0-9]|\.[A-Z])*)\W/, diff --git a/lib/runners/solidity.js b/lib/runners/solidity.js new file mode 100644 index 00000000..86e82713 --- /dev/null +++ b/lib/runners/solidity.js @@ -0,0 +1,120 @@ +"use strict"; + +const fs = require('fs-extra'); + +module.exports = { + modifyOpts(opts) { + opts.services = ['testrpc']; + }, + + // solutionOnly(opts, runCode) { + // }, + + testIntegration(opts, runCode, fail) { + fs.outputFileSync('/workspace/solidity/contracts/solution.sol', opts.solution); + fs.outputFileSync('/workspace/solidity/test/fixture.js', opts.fixture); + + // collect declared libraries and contracts to generate `2_deploy_contracts.js`. + // TODO link library to library? + const libs = [], contracts = []; + if (opts.setup) { + fs.outputFileSync('/workspace/solidity/contracts/setup.sol', opts.setup); + collectLibraries(opts.setup, libs); + collectContracts(opts.setup, contracts); + } + collectLibraries(opts.solution, libs); + collectContracts(opts.solution, contracts); + + const deploy = []; + for (const x of libs) deploy.push(`var ${x} = artifacts.require('${x}');`); + for (const x of contracts) deploy.push(`var ${x} = artifacts.require('${x}');`); + deploy.push('module.exports = function(deployer) {'); + for (const x of libs) { + deploy.push(` deployer.deploy(${x});`); + // for each library, link to all contracts. + // if a contract doesn't rely on the library, it will be ignored + if (contracts.length > 0) deploy.push(` deployer.link(${x}, [${contracts.join(',')}]);`); + } + // switch to using `deployer.autolink();` available on Truffle Beta when it's stable. + // deploy.push(' deployer.autolink();'); + for (const x of contracts) deploy.push(` deployer.deploy(${x});`); + deploy.push('};'); + fs.outputFileSync('/workspace/solidity/migrations/2_deploy_contracts.js', deploy.join('\n')); + + runCode({ + name: 'truffle', + args: ['test'], + options: { + cwd: '/workspace/solidity', + }, + }); + }, + + sanitizeStdOut(opts, stdout) { + return stdout.replace(/^Compiling \.\/.*\n/gm, ''); + }, +}; + +function collectContracts(code, out) { + const m = code.match(/^\s*contract\s+\S+/gm); + if (m !== null) out.push.apply(out, m.map(c => c.replace(/^\s*contract\s+/, ''))); +} +function collectLibraries(code, out) { + const m = code.match(/^\s*library\s+\S+/gm); + if (m !== null) out.push.apply(out, m.map(c => c.replace(/^\s*library\s+/, ''))); +} + +/* +// used to parse and format outputs of `--verbose-rpc`, formatRPC(stdout) in sanitizeStdOut +function formatRPC(out) { + const reqs = new Map(); + return out.replace(/\n? {3}> \{[\s\S]*?\n {3}> \}(?=\n)/g, m => req(m.replace(/^ {3}> /gm, ''))) + .replace(/\n? {3}> \[[\s\S]*?\n {3}> \](?=\n)/g, m => req(m.replace(/^ {3}> /gm, ''))) + .replace(/\n? < {3}\{[\s\S]*?\n < {3}\}(?=\n)/g, m => res(m.replace(/^ < {3}/gm, ''))) + .replace(/\n? < {3}\[[\s\S]*?\n < {3}\](?=\n)/g, m => res(m.replace(/^ < {3}/gm, ''))); + + function req(s) { + try { + const o = JSON.parse(s); + const rs = Array.isArray(o) ? o : [o]; + for (const r of rs) reqs.set(r.id, r); + return ''; + } + catch (_) { + return s; + } + } + + function res(s) { + const out = []; + try { + const o = JSON.parse(s); + const rs = Array.isArray(o) ? o : [o]; + for (const r of rs) { + if (reqs.has(r.id)) { + const req = reqs.get(r.id); + const method = req.method; + const params = req.params; + const result = r.result; + if (method === 'net_version') { + out.push(`\n${result}`); + } else if (method === 'eth_accounts') { + out.push(`\n${JSON.stringify(r.result.map((r, i) => ({'#': i, account: r})))}`); + } else { + out.push(`\n[${JSON.stringify({method, params})}]`); + if (typeof result == 'object') { + out.push(`\n[${JSON.stringify(result)}]`); + } else { + out.push(`\n[${JSON.stringify({result})}]`); + } + } + } + } + return out.join(''); + } + catch (_) { + return s; + } + } +} +*/ diff --git a/lib/services.js b/lib/services.js index 94d8ebc7..0a5f6f62 100644 --- a/lib/services.js +++ b/lib/services.js @@ -30,7 +30,25 @@ const startService = { }, rabbitmq: function(opts) { // TODO - } + }, + testrpc: function(opts) { + opts.publish('status', 'Starting TestRPC'); + return new Promise(function(resolve, reject) { + const s = spawn('testrpc', [], {detached: true}); + s.unref(); // don't wait for child to exit + opts.onCompleted.push(() => s.kill()); + s.stderr.on('data', data => process.stderr.write(data.toString())); + s.stdout.on('data', data => { + if (data.toString().includes('Listening on localhost:8545')) { + s.stdout.removeAllListeners(); + resolve(); + } + }); + s.on('exit', resolve); + s.on('error', reject); + setTimeout(resolve, 2000); + }); + }, }; function spawnAndWait(opts, cmd, args, text, delay) { diff --git a/test/runners/solidity_spec.js b/test/runners/solidity_spec.js new file mode 100644 index 00000000..74dfef0f --- /dev/null +++ b/test/runners/solidity_spec.js @@ -0,0 +1,536 @@ +"use strict"; + +const exec = require('child_process').exec; + +const expect = require('chai').expect; + +const runner = require('../runner'); + +describe('truffle test', function() { + afterEach(function cleanup(done) { + exec('rm -rf /workspace/solidity/contracts/solution.sol /workspace/solidity/contracts/setup.sol /tmp/test-*', err => err ? done(err) : done()); + }); + + it('should handle basic assertion', function(done) { + this.timeout(0); + runner.run({ + language: 'solidity', + solution: ` + pragma solidity ^0.4.13; + + contract MetaCoin { + mapping (address => uint) balances; + function MetaCoin() { + balances[tx.origin] = 10000; + } + function getBalance(address addr) returns(uint) { + return balances[addr]; + } + } + `, + fixture: ` + const MetaCoin = artifacts.require("MetaCoin"); + contract('MetaCoin', function(accounts) { + it("should put 10000 MetaCoin in the first account", async function() { + const m = await MetaCoin.deployed(); + const balance = await m.getBalance.call(accounts[0]); + assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account"); + }); + }); + `, + }, function(buffer) { + expect(buffer.stdout).to.contain('\n'); + expect(buffer.stdout).not.to.contain('\n'); + expect(buffer.stderr).to.be.empty; + done(); + }); + }); + + it('should handle basic assertion failure', function(done) { + this.timeout(0); + runner.run({ + language: 'solidity', + solution: ` + pragma solidity ^0.4.13; + + contract MetaCoin { + mapping (address => uint) balances; + function MetaCoin() { + balances[tx.origin] = 10001; + } + function getBalance(address addr) returns(uint) { + return balances[addr]; + } + } + `, + fixture: ` + const MetaCoin = artifacts.require("MetaCoin"); + contract('MetaCoin', function(accounts) { + it("should put 10000 MetaCoin in the first account", async function() { + const m = await MetaCoin.deployed(); + const balance = await m.getBalance.call(accounts[0]); + assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account"); + }); + }); + `, + }, function(buffer) { + expect(buffer.stdout).to.contain('\n'); + done(); + }); + }); + + it('should handle basic assertion failure from error', function(done) { + this.timeout(0); + runner.run({ + language: 'solidity', + solution: ` + pragma solidity ^0.4.13; + + contract MetaCoin { + mapping (address => uint) balances; + function MetaCoin() { + balances[tx.origin] = 10001; + } + function getBalance(address addr) returns(uint) { + return balances[addr]; + } + } + `, + fixture: ` + const MetaCoin = artifacts.require("MetaCoin"); + contract('MetaCoin', function(accounts) { + it("should put 10000 MetaCoin in the first account", async function() { + const m = await MetaCoin.deployed(); + const balance = await m.getBalance.call(accounts[0]); + await m.foo.call(); + assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account"); + }); + }); + `, + }, function(buffer) { + expect(buffer.stdout).to.contain('\n'); + done(); + }); + }); + + it('should output correct format', function(done) { + this.timeout(0); + runner.run({ + language: 'solidity', + solution: ` + pragma solidity ^0.4.13; + + contract MetaCoin { + mapping (address => uint) balances; + function MetaCoin() { + balances[tx.origin] = 10000; + } + function getBalance(address addr) returns(uint) { + return balances[addr]; + } + } + `, + fixture: ` + const MetaCoin = artifacts.require("MetaCoin"); + contract('MetaCoin', function(accounts) { + it("should put 10000 MetaCoin in the first account", async function() { + const m = await MetaCoin.deployed(); + const balance = await m.getBalance.call(accounts[0]); + assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account"); + }); + }); + `, + }, function(buffer) { + const expected = [ + '', + ' ', + '', + ].join('').replace(/\s/g, ''); + expect(buffer.stdout.match(/<(?:DESCRIBE|IT|PASSED|FAILED|COMPLETEDIN)::>/g).join('')).to.equal(expected); + expect(buffer.stderr).to.be.empty; + done(); + }); + }); + + it('should output nested describes', function(done) { + this.timeout(0); + runner.run({ + language: 'solidity', + solution: ` + pragma solidity ^0.4.13; + + contract MetaCoin { + mapping (address => uint) balances; + function MetaCoin() { + balances[tx.origin] = 10000; + } + function getBalance(address addr) returns(uint) { + return balances[addr]; + } + } + `, + fixture: ` + const MetaCoin = artifacts.require("MetaCoin"); + describe("contracts", function() { + contract('MetaCoin', function(accounts) { + it("should put 10000 MetaCoin in the first account", async function() { + const m = await MetaCoin.deployed(); + const balance = await m.getBalance.call(accounts[0]); + assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account"); + }); + }); + }); + `, + }, function(buffer) { + const expected = [ + '', + ' ', + ' ', + ' ', + '', + ].join('').replace(/\s/g, ''); + expect(buffer.stdout.match(/<(?:DESCRIBE|IT|PASSED|FAILED|COMPLETEDIN)::>/g).join('')).to.equal(expected); + expect(buffer.stderr).to.be.empty; + done(); + }); + }); + + + it('should support opts.setup', function(done) { + this.timeout(0); + runner.run({ + language: 'solidity', + setup: ` + pragma solidity ^0.4.13; + + library ConvertLib { + function convert(uint amount, uint conversionRate) returns (uint convertedAmount) { + return amount * conversionRate; + } + } + `, + solution: ` + pragma solidity ^0.4.13; + + import "./setup.sol"; + + contract MetaCoin { + mapping (address => uint) balances; + + function MetaCoin() { + balances[tx.origin] = 10000; + } + function getBalanceInEth(address addr) returns(uint) { + return ConvertLib.convert(getBalance(addr), 2); + } + function getBalance(address addr) returns(uint) { + return balances[addr]; + } + } + `, + fixture: ` + const MetaCoin = artifacts.require("MetaCoin"); + contract('MetaCoin', function(accounts) { + it("should call a function that depends on a linked library", async function() { + const m = await MetaCoin.deployed(); + const coinBalance = await m.getBalance.call(accounts[0]); + const coinEthBalance = await m.getBalanceInEth.call(accounts[0]); + assert.equal(coinEthBalance.toNumber(), 2*coinBalance.toNumber(), "Library function returned unexpected function, linkage may be broken"); + }); + }); + `, + }, function(buffer) { + expect(buffer.stdout).to.contain('\n'); + expect(buffer.stdout).not.to.contain('\n'); + done(); + }); + }); + + it('should handle time traveling', function(done) { + this.timeout(0); + runner.run({ + language: 'solidity', + solution: ` + pragma solidity ^0.4.13; + + contract MetaCoin { + uint public endTime; // for time-travel demo + + function MetaCoin() { + endTime = now + 1 days; + } + function isValid() returns(bool) { // for time travel demo, only valid for a day + return now <= endTime; + } + } + `, + fixture: ` + const MetaCoin = artifacts.require("MetaCoin"); + + const Web3 = require('web3'); + const web3 = new Web3(); + web3.setProvider(new Web3.providers.HttpProvider("http://localhost:8545")); + + contract('MetaCoin', function(accounts) { + it("should be valid if the time is within a day from creation", async function() { + const m = await MetaCoin.new(); + await increaseTime(10); // 10 seconds + assert.equal(await m.isValid.call(), true, "status wasn't valid after 10 seconds"); + }); + + it("should be invalid after a day", async function() { + const m = await MetaCoin.new(); + await increaseTime(2 * 86400); // 2 days later + assert.equal(await m.isValid.call(), false, "status was valid even after a day"); + }); + }); + + function increaseTime(seconds) { + return new Promise((resolve, reject) => { + web3.currentProvider.sendAsync({ + jsonrpc: "2.0", + method: "evm_increaseTime", + params: [seconds], + id: new Date().getTime() + }, (err, result) => { + if (err) return reject(err); + // resolve(result); + // HACK workarounds https://github.com/ethereumjs/testrpc/issues/336 + mineBlock().then(_ => resolve(result)).catch(reject); + }); + }); + } + + function mineBlock() { + return new Promise((resolve, reject) => { + web3.currentProvider.sendAsync({ + jsonrpc: "2.0", + method: "evm_mine" + }, (err, result) => err ? reject(err) : resolve(result)); + }); + } + `, + }, function(buffer) { + expect(buffer.stdout).to.contain('\n'); + expect(buffer.stdout).not.to.contain('\n'); + done(); + }); + }); + + it('tests can access and output some information', function(done) { + this.timeout(0); + runner.run({ + language: 'solidity', + solution: ` + pragma solidity ^0.4.13; + + contract MetaCoin { + mapping (address => uint) balances; + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + + uint public endTime; // for time-travel demo + + function MetaCoin() { + balances[tx.origin] = 10000; + endTime = now + 1 days; + } + function sendCoin(address receiver, uint amount) returns(bool sufficient) { + if (balances[msg.sender] < amount) return false; + balances[msg.sender] -= amount; + balances[receiver] += amount; + Transfer(msg.sender, receiver, amount); + return true; + } + function getBalance(address addr) returns(uint) { + return balances[addr]; + } + function isValid() returns(bool) { // for time travel demo, only valid for a day + return now <= endTime; + } + } + `, + fixture: ` + const MetaCoin = artifacts.require("MetaCoin"); + + const Web3 = require('web3'); + const web3 = new Web3(); + web3.setProvider(new Web3.providers.HttpProvider("http://localhost:8545")); + + contract('MetaCoin', function(accounts) { + before(async () => { + console.log('' + JSON.stringify(accounts)); + }); + + it("should put 10000 MetaCoin in the first account", async function() { + const m = await MetaCoin.deployed(); + const balance = await m.getBalance.call(accounts[0]); + console.log('' + JSON.stringify(web3.eth.getBlock(web3.eth.blockNumber))); + console.log('' + web3.eth.gasPrice.toString(10)); + assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account"); + }); + + it("should send coin correctly", async function() { + const amount = 10; + const m = await MetaCoin.deployed(); + // Get initial balances of first and second account. + const account1 = accounts[0], account2 = accounts[1]; + const account1Start = (await m.getBalance.call(account1)).toNumber(); + const account2Start = (await m.getBalance.call(account2)).toNumber(); + + const result = await m.sendCoin(account2, amount, {from: account1}); + const receipt = result.receipt; + console.log('' + JSON.stringify({ + transactionHash: receipt.transactionHash, + blockNumber: receipt.blockNumber, + gasUsed: receipt.gasUsed, + cumulativeGasUsed: receipt.cumulativeGasUsed, + })); + for (const log of result.logs) { + console.log('' + JSON.stringify(log)); + } + + const account1End = (await m.getBalance.call(account1)).toNumber(); + const account2End = (await m.getBalance.call(account2)).toNumber(); + + assert.equal(account1End, account1Start - amount, "Amount wasn't correctly taken from the sender"); + assert.equal(account2End, account2Start + amount, "Amount wasn't correctly sent to the receiver"); + }); + }); + `, + }, function(buffer) { + expect(buffer.stdout).to.contain(''); + expect(buffer.stdout).to.contain(''); + expect(buffer.stdout).to.contain(''); + expect(buffer.stdout).to.contain(''); + expect(buffer.stdout).not.to.contain('\n'); + expect(buffer.stderr).to.be.empty; + done(); + }); + }); + + it('should support multiple libraries in same file', function(done) { + this.timeout(0); + runner.run({ + language: 'solidity', + solution: ` + pragma solidity ^0.4.13; + + library Foo { + function fun(uint x, uint y) returns(uint) { + return x + y; + } + } + + library Bar { + function fun(uint x, uint y) returns(uint) { + return x - y; + } + } + + contract Buz { + function fun(uint x, uint y) returns(uint) { + return Bar.fun(Foo.fun(x, y), y); + } + } + `, + fixture: ` + const Buz = artifacts.require("Buz"); + contract('Buz', function(accounts) { + it("should return x: (x + y) - y", async function() { + const m = await Buz.deployed(); + const v = await m.fun.call(1, 1); + assert.equal(v.valueOf(), 1); + }); + }); + `, + }, function(buffer) { + expect(buffer.stdout).to.contain('\n'); + expect(buffer.stdout).not.to.contain('\n'); + expect(buffer.stderr).to.be.empty; + done(); + }); + }); + + it('should support multiple libraries setup', function(done) { + this.timeout(0); + runner.run({ + language: 'solidity', + setup: ` + pragma solidity ^0.4.13; + + library Foo { + function fun(uint x, uint y) returns(uint) { + return x + y; + } + } + + library Bar { + function fun(uint x, uint y) returns(uint) { + return x - y; + } + } + `, + solution: ` + pragma solidity ^0.4.13; + + import "./setup.sol"; + + contract Buz { + function fun(uint x, uint y) returns(uint) { + return Bar.fun(Foo.fun(x, y), y); + } + } + `, + fixture: ` + const Buz = artifacts.require("Buz"); + contract('Buz', function(accounts) { + it("should return x: (x + y) - y", async function() { + const m = await Buz.deployed(); + const v = await m.fun.call(1, 1); + assert.equal(v.valueOf(), 1); + }); + }); + `, + }, function(buffer) { + expect(buffer.stdout).to.contain('\n'); + expect(buffer.stdout).not.to.contain('\n'); + expect(buffer.stderr).to.be.empty; + done(); + }); + }); +});