Skip to content
This repository has been archived by the owner on Apr 17, 2023. It is now read-only.

Commit

Permalink
Merge pull request #165 from 0mkara/remix-tests
Browse files Browse the repository at this point in the history
Unit testing smart contracts using remix-tests
  • Loading branch information
0mkara authored Aug 29, 2018
2 parents e8e1ceb + 76c9897 commit c3bb972
Show file tree
Hide file tree
Showing 20 changed files with 722 additions and 191 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ module.exports = {
atom: true
},
"parserOptions": {
"ecmaVersion": 6,
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
},
"sourceType": "module"
Expand Down
399 changes: 328 additions & 71 deletions build/main.js

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use babel'
import 'idempotent-babel-polyfill'
import { Etheratom } from './lib/ethereum-interface'
import 'idempotent-babel-polyfill';
import { Etheratom } from './lib/ethereum-interface';

module.exports = new Etheratom({
config: atom.config,
workspace: atom.workspace
})
});
4 changes: 2 additions & 2 deletions lib/actions/ContractActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ export const updateInterface = ({ contractName, ContractABI }) => {
export const setInstance = ({ contractName, instance }) => {
return (dispatch) => {
dispatch({ type: SET_INSTANCE, payload: { contractName, instance } });
}
};
};

export const setDeployed = ({ contractName, deployed }) => {
return (dispatch) => {
dispatch({ type: SET_DEPLOYED, payload: { contractName, deployed } });
}
};
};
23 changes: 23 additions & 0 deletions lib/actions/ErrorActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use babel'
// Copyright 2018 Etheratom Authors
// This file is part of Etheratom.

// Etheratom is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Etheratom is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Etheratom. If not, see <http://www.gnu.org/licenses/>.
import { SET_ERRORS } from './types';

export const setErrors = (payload) => {
return (dispatch) => {
dispatch({ type: SET_ERRORS, payload });
};
};
1 change: 1 addition & 0 deletions lib/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export * from './ContractActions';
export * from './AccountActions';
export * from './EventActions';
export * from './NodeActions';
export * from './ErrorActions';
1 change: 1 addition & 0 deletions lib/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const UPDATE_INTERFACE = 'update_interface';
export const SET_INSTANCE = 'set_instance';
export const SET_DEPLOYED = 'set_deployed';
export const SET_GAS_LIMIT = 'set_gas_limit';
export const SET_SOURCES = 'set_sources';

export const SET_COINBASE = 'set_coinbase';
export const SET_PASSWORD = 'set_password';
Expand Down
2 changes: 1 addition & 1 deletion lib/components/ContractCompiled/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
// along with Etheratom. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { connect } from 'react-redux';
import { addInterface } from '../../actions';
import ReactJson from 'react-json-view';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import GasInput from '../GasInput';
import InputsForm from '../InputsForm';
import CreateButton from '../CreateButton';
import PropTypes from 'prop-types';
import { addInterface } from '../../actions';

class ContractCompiled extends React.Component {
constructor(props) {
Expand Down
18 changes: 13 additions & 5 deletions lib/components/Contracts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import { Collapse } from 'react-collapse';
import ContractCompiled from '../ContractCompiled';
import ContractExecution from '../ContractExecution';
import ErrorView from '../ErrorView';
import { addInterface } from '../../actions';
import PropTypes from 'prop-types';

class CollapsedFile extends React.Component {
Expand Down Expand Up @@ -99,13 +98,21 @@ class Contracts extends React.Component {
super(props);
this.helpers = props.helpers;
}
componentDidUpdate(prevProps) {
const { sources } = this.props;
if(sources != prevProps.sources) {
// Start compilation of contracts from here
const workspaceElement = atom.views.getView(atom.workspace);
atom.commands.dispatch(workspaceElement, 'eth-interface:compile');
}
}
render() {
const { compiled, deployed, compiling, interfaces } = this.props;
return (
<Provider store={this.props.store}>
<div id="compiled-code" className="compiled-code">
{
compiled &&
compiled && compiled.contracts &&
Object.keys(compiled.contracts).map((fileName, index) => {
return (
<CollapsedFile
Expand Down Expand Up @@ -147,6 +154,7 @@ CollapsedFile.propTypes = {
};

Contracts.propTypes = {
sources: PropTypes.object,
helpers: PropTypes.any.isRequired,
store: PropTypes.any.isRequired,
compiled: PropTypes.object,
Expand All @@ -156,8 +164,8 @@ Contracts.propTypes = {
};

const mapStateToProps = ({ contract }) => {
const { compiled, deployed, compiling, interfaces } = contract;
return { compiled, deployed, compiling, interfaces };
const { sources, compiled, deployed, compiling, interfaces } = contract;
return { sources, compiled, deployed, compiling, interfaces };
};

export default connect(mapStateToProps, { addInterface })(Contracts);
export default connect(mapStateToProps, { })(Contracts);
4 changes: 4 additions & 0 deletions lib/components/ErrorView/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class ErrorView extends React.Component {
msg.severity === 'error' &&
<span className="icon icon-bug text-error">{msg.formattedMessage || msg.message}</span>
}
{
!msg.severity &&
<span className="icon icon-bug text-error">{msg.message}</span>
}
</li>
);
})
Expand Down
175 changes: 175 additions & 0 deletions lib/components/RemixTests/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
'use babel'
// Copyright 2018 Etheratom Authors
// This file is part of Etheratom.

// Etheratom is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// Etheratom is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Etheratom. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { connect, Provider } from 'react-redux';
import PropTypes from 'prop-types';
import RemixTests from 'remix-tests';
import VirtualList from 'react-tiny-virtual-list';
import ErrorView from '../ErrorView';
import { setErrors } from '../../actions';

Object.defineProperty(String.prototype, 'regexIndexOf', {
value(regex, startpos) {
const indexOf = this.substring(startpos || 0).search(regex);
return (indexOf >= 0) ? (indexOf + (startpos || 0)) : indexOf;
}
});

class RemixTest extends React.Component {
constructor(props) {
super(props);
this.state = {
testResults: [],
running: false
};
this._runRemixTests = this._runRemixTests.bind(this);
this._testCallback = this._testCallback.bind(this);
this._finalCallback = this._finalCallback.bind(this);
this._resultsCallback = this._resultsCallback.bind(this);
}
componentDidUpdate(prevProps) {
const { sources } = this.props;
if(sources != prevProps.sources) {
this._runRemixTests();
}
}
_testCallback(result) {
try {
const { testResults } = this.state;
const t = testResults.slice();
t.push(result);
this.setState({ testResults: t });
} catch (e) {
this.props.setErrors([e]);
console.error(e);
}
}
_resultsCallback(err, result) {
if(err) {
this.props.setErrors([err]);
console.error(err);
}
}
_finalCallback(err, result) {
if(err) {
this.props.setErrors([err]);
console.error(err);
}
this.setState({ testResult: result, running: false });
}
_importFileCb(err, result) {
if(err) {
this.props.setErrors([err]);
console.error(err);
}
}
async _runRemixTests() {
const { sources } = this.props;
this.setState({ testResults: [], running: true });
const promises = [];
for(let filename in sources) {
if(filename.indexOf('_test.sol') > 0) {
continue;
}
sources[filename].content = await this.injectTests(sources[filename]);
promises.push(filename);
}
Promise.all(promises)
.then(testSources => {
RemixTests.runTestSources(sources, this._testCallback, this._resultsCallback, this._finalCallback, this._importFileCb);
})
.catch(e => {
this.props.setErrors([e]);
console.error(e);
});
}
async injectTests(source) {
const s = /^(import)\s['"](remix_tests.sol|tests.sol)['"];/gm;
if(source.content && source.content.regexIndexOf(s) < 0) {
return source.content.replace(/(pragma solidity \^\d+\.\d+\.\d+;)/, '$1\nimport \'remix_tests.sol\';');
}
}
render() {
const { testResults, testResult, running } = this.state;
return (
<Provider store={this.props.store}>
<div id="remix-tests">
<h2 className="block test-header">Naming conventions</h2>
<h3 className="block test-header">File names should end with _test, as in foo_test.sol</h3>
<div className="test-selector">
<button className="btn btn-primary inline-block-tight" onClick={this._runRemixTests}>
Run tests
</button>
{
running &&
<span className='loading loading-spinner-tiny inline-block'></span>
}
{
testResult &&
<div className="test-result">
<span className="text-error">Total failing: {testResult.totalFailing} </span>
<span className="text-success">Total passing: {testResult.totalPassing} </span>
<span className="text-info">Time: {testResult.totalTime}</span>
</div>
}
</div>
<VirtualList
height="50vh"
itemCount={testResults.length}
itemSize={30}
className="test-result-list-container"
overscanCount={10}
renderItem={({ index }) =>
<div key={index} className="test-result-list-item">
{
testResults[index].type === 'contract' &&
<span className="status-renamed icon icon-checklist"></span>
}
{
testResults[index].type === 'testPass' &&
<span className="status-added icon icon-check"></span>
}
{
testResults[index].type === 'testFailure' &&
<span className="status-removed icon icon-x"></span>
}
<span className="padded text-warning">
{testResults[index].value}
</span>
</div>
}
/>
<div id="test-error" className="error-container">
<ErrorView />
</div>
</div>
</Provider>
);
}
}
RemixTest.propTypes = {
helpers: PropTypes.any.isRequired,
sources: PropTypes.object,
compiled: PropTypes.object,
setErrors: PropTypes.func,
store: PropTypes.any.isRequired
};
const mapStateToProps = ({ contract }) => {
const { sources } = contract;
return { sources };
};
export default connect(mapStateToProps, { setErrors })(RemixTest);
2 changes: 1 addition & 1 deletion lib/components/StaticAnalysis/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
// along with Etheratom. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { connect } from 'react-redux';
import { CodeAnalysis } from 'remix-solidity';
import { CodeAnalysis } from 'remix-analyzer';
import CheckboxTree from 'react-checkbox-tree';
import PropTypes from 'prop-types';

Expand Down
11 changes: 9 additions & 2 deletions lib/components/TabView/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import PropTypes from 'prop-types';
import Contracts from '../Contracts';
import TxAnalyzer from '../TxAnalyzer';
import Events from '../Events';
import RemixTest from '../RemixTests';
import NodeControl from '../NodeControl';
import StaticAnalysis from '../StaticAnalysis';

Expand All @@ -37,10 +38,10 @@ class TabView extends React.Component {
this._handleTabSelect = this._handleTabSelect.bind(this);
}
_handleTabSelect(index) {
if(index === 2) {
if(index === 3) {
this.setState({ newTxCounter: 0, txBtnStyle: 'btn' });
}
if(index === 3) {
if(index === 4) {
this.setState({ newEventCounter: 0, eventBtnStyle: 'btn' });
}
}
Expand All @@ -63,6 +64,9 @@ class TabView extends React.Component {
<Tab>
<div className="btn">Contract</div>
</Tab>
<Tab>
<div className="btn">Tests</div>
</Tab>
<Tab>
<div className="btn">Analysis</div>
</Tab>
Expand Down Expand Up @@ -96,6 +100,9 @@ class TabView extends React.Component {
<TabPanel>
<Contracts store={this.props.store} helpers={this.helpers} />
</TabPanel>
<TabPanel>
<RemixTest store={this.props.store} helpers={this.helpers} />
</TabPanel>
<TabPanel>
<StaticAnalysis store={this.props.store} helpers={this.helpers} />
</TabPanel>
Expand Down
Loading

0 comments on commit c3bb972

Please sign in to comment.