diff --git a/tests/README.md b/tests/README.md index 48a6c41a7..0bf097254 100644 --- a/tests/README.md +++ b/tests/README.md @@ -50,6 +50,20 @@ To run the same tests on a release build, replace `debug` with `release`. # Functional tests [TODO] +Functional tests are located under the `tests/functional` directory. + +First, run a regtest daemon in the offline mode and with a fixed difficulty: +``` +monerod --regtest --offline --fixed-difficulty 1 +``` +Alternatively, you can run multiple daemons and let them connect with each other by using `--add-exclusive-node`. In this case, make sure that the same fixed difficulty is given to all the daemons. + +Next, restore a mainnet wallet with the following seed and restore height 0 (the file path doesn't matter): +``` +velvet lymph giddy number token physics poetry unquoted nibs useful sabotage limits benches lifestyle eden nitrogen anvil fewest avoid batch vials washing fences goat unquoted +``` + +Open the wallet file with `monero-wallet-rpc` with RPC port 18083. Finally, start tests by invoking ./blockchain.py or ./speed.py # Fuzz tests diff --git a/tests/functional_tests/blockchain.py b/tests/functional_tests/blockchain.py new file mode 100755 index 000000000..983658a7c --- /dev/null +++ b/tests/functional_tests/blockchain.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2018 The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Test blockchain RPC calls + +Test the following RPCs: + - get_info + - generateblocks + - [TODO: many tests still need to be written] + +""" + +from test_framework.daemon import Daemon +from test_framework.wallet import Wallet + +class BlockchainTest(): + def run_test(self): + self._test_get_info() + self._test_hardfork_info() + self._test_generateblocks(5) + + def _test_get_info(self): + print('Test get_info') + + daemon = Daemon() + res = daemon.get_info() + + # difficulty should be set to 1 for this test + assert 'difficulty' in res.keys() + assert res['difficulty'] == 1; + + # nettype should not be TESTNET + assert 'testnet' in res.keys() + assert res['testnet'] == False; + + # nettype should not be STAGENET + assert 'stagenet' in res.keys() + assert res['stagenet'] == False; + + # nettype should be FAKECHAIN + assert 'nettype' in res.keys() + assert res['nettype'] == "fakechain"; + + # free_space should be > 0 + assert 'free_space' in res.keys() + assert res['free_space'] > 0 + + # height should be greater or equal to 1 + assert 'height' in res.keys() + assert res['height'] >= 1 + + + def _test_hardfork_info(self): + print('Test hard_fork_info') + + daemon = Daemon() + res = daemon.hard_fork_info() + + # hard_fork version should be set at height 1 + assert 'earliest_height' in res.keys() + assert res['earliest_height'] == 1; + + + def _test_generateblocks(self, blocks): + print("Test generating", blocks, 'blocks') + + daemon = Daemon() + res = daemon.get_info() + height = res['height'] + res = daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', blocks) + + assert res['height'] == height + blocks - 1 + + +if __name__ == '__main__': + BlockchainTest().run_test() diff --git a/tests/functional_tests/speed.py b/tests/functional_tests/speed.py new file mode 100755 index 000000000..3d2af9a10 --- /dev/null +++ b/tests/functional_tests/speed.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2018 The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Test speed of various procedures + +Test the following RPCs: + - generateblocks + - transfer + - [TODO: many tests still need to be written] + +""" + + +import time +from time import sleep +from decimal import Decimal + +from test_framework.daemon import Daemon +from test_framework.wallet import Wallet + + +class SpeedTest(): + def set_test_params(self): + self.num_nodes = 1 + + def run_test(self): + daemon = Daemon() + wallet = Wallet() + + destinations = wallet.make_uniform_destinations('44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A',1,3) + + self._test_speed_generateblocks(daemon=daemon, blocks=70) + for i in range(1, 10): + while wallet.get_balance()['unlocked_balance'] == 0: + print('Waiting for wallet to refresh...') + sleep(1) + self._test_speed_transfer_split(wallet=wallet) + self._test_speed_generateblocks(daemon=daemon, blocks=10) + + def _test_speed_generateblocks(self, daemon, blocks): + print('Test speed of block generation') + start = time.time() + + res = daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', blocks) + # wallet seed: velvet lymph giddy number token physics poetry unquoted nibs useful sabotage limits benches lifestyle eden nitrogen anvil fewest avoid batch vials washing fences goat unquoted + + print('generating ', blocks, 'blocks took: ', time.time() - start, 'seconds') + + def _test_speed_transfer_split(self, wallet): + print('Test speed of transfer') + start = time.time() + + destinations = wallet.make_uniform_destinations('44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A',1) + res = wallet.transfer_split(destinations) + + print('generating tx took: ', time.time() - start, 'seconds') + + +if __name__ == '__main__': + SpeedTest().run_test() diff --git a/tests/functional_tests/test_framework/__init__.py b/tests/functional_tests/test_framework/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/functional_tests/test_framework/daemon.py b/tests/functional_tests/test_framework/daemon.py new file mode 100644 index 000000000..f3490b232 --- /dev/null +++ b/tests/functional_tests/test_framework/daemon.py @@ -0,0 +1,105 @@ +# Copyright (c) 2018 The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Daemon class to make rpc calls and store state.""" + +from .rpc import JSONRPC + +class Daemon(object): + + def __init__(self, protocol='http', host='127.0.0.1', port=18081, path='/json_rpc'): + self.rpc = JSONRPC('{protocol}://{host}:{port}{path}'.format(protocol=protocol, host=host, port=port, path=path)) + + def getblocktemplate(self, address): + getblocktemplate = { + 'method': 'getblocktemplate', + 'params': { + 'wallet_address': address, + 'reserve_size' : 1 + }, + 'jsonrpc': '2.0', + 'id': '0' + } + return self.rpc.send_request(getblocktemplate) + + def submitblock(self, block): + submitblock = { + 'method': 'submitblock', + 'params': [ block ], + 'jsonrpc': '2.0', + 'id': '0' + } + return self.rpc.send_request(submitblock) + + def getblock(self, height=0): + getblock = { + 'method': 'getblock', + 'params': { + 'height': height + }, + 'jsonrpc': '2.0', + 'id': '0' + } + return self.rpc.send_request(getblock) + + def get_connections(self): + get_connections = { + 'method': 'get_connections', + 'jsonrpc': '2.0', + 'id': '0' + } + return self.rpc.send_request(get_connections) + + def get_info(self): + get_info = { + 'method': 'get_info', + 'jsonrpc': '2.0', + 'id': '0' + } + return self.rpc.send_request(get_info) + + def hard_fork_info(self): + hard_fork_info = { + 'method': 'hard_fork_info', + 'jsonrpc': '2.0', + 'id': '0' + } + return self.rpc.send_request(hard_fork_info) + + def generateblocks(self, address, blocks=1): + generateblocks = { + 'method': 'generateblocks', + 'params': { + 'amount_of_blocks' : blocks, + 'reserve_size' : 20, + 'wallet_address': address + }, + 'jsonrpc': '2.0', + 'id': '0' + } + return self.rpc.send_request(generateblocks) diff --git a/tests/functional_tests/test_framework/rpc.py b/tests/functional_tests/test_framework/rpc.py new file mode 100644 index 000000000..b21df7b93 --- /dev/null +++ b/tests/functional_tests/test_framework/rpc.py @@ -0,0 +1,49 @@ +# Copyright (c) 2018 The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import requests +import json + +class JSONRPC(object): + def __init__(self, url): + self.url = url + + def send_request(self, inputs): + res = requests.post( + self.url, + data=json.dumps(inputs), + headers={'content-type': 'application/json'}) + res = res.json() + + assert 'error' not in res, res + + return res['result'] + + + + diff --git a/tests/functional_tests/test_framework/wallet.py b/tests/functional_tests/test_framework/wallet.py new file mode 100644 index 000000000..357eab5b2 --- /dev/null +++ b/tests/functional_tests/test_framework/wallet.py @@ -0,0 +1,120 @@ +# Copyright (c) 2018 The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Daemon class to make rpc calls and store state.""" + +from .rpc import JSONRPC + +class Wallet(object): + + def __init__(self, protocol='http', host='127.0.0.1', port=18083, path='/json_rpc'): + self.rpc = JSONRPC('{protocol}://{host}:{port}{path}'.format(protocol=protocol, host=host, port=port, path=path)) + + def make_uniform_destinations(self, address, transfer_amount, transfer_number_of_destinations=1): + destinations = [] + for i in range(transfer_number_of_destinations): + destinations.append({"amount":transfer_amount,"address":address}) + return destinations + + def make_destinations(self, addresses, transfer_amounts): + destinations = [] + for i in range(len(addresses)): + destinations.append({'amount':transfer_amounts[i],'address':addresses[i]}) + return destinations + + def transfer(self, destinations, ringsize=7, payment_id=''): + transfer = { + 'method': 'transfer', + 'params': { + 'destinations': destinations, + 'mixin' : ringsize - 1, + 'get_tx_key' : True + }, + 'jsonrpc': '2.0', + 'id': '0' + } + if(len(payment_id) > 0): + transfer['params'].update({'payment_id' : payment_id}) + return self.rpc.send_request(transfer) + + def transfer_split(self, destinations, ringsize=7, payment_id=''): + print(destinations) + transfer = { + "method": "transfer_split", + "params": { + "destinations": destinations, + "mixin" : ringsize - 1, + "get_tx_key" : True, + "new_algorithm" : True + }, + "jsonrpc": "2.0", + "id": "0" + } + if(len(payment_id) > 0): + transfer['params'].update({'payment_id' : payment_id}) + return self.rpc.send_request(transfer) + + def create_wallet(self, index=''): + create_wallet = { + 'method': 'create_wallet', + 'params': { + 'filename': 'testWallet' + index, + 'password' : '', + 'language' : 'English' + }, + 'jsonrpc': '2.0', + 'id': '0' + } + return self.rpc.send_request(create_wallet) + + def get_balance(self): + get_balance = { + 'method': 'get_balance', + 'jsonrpc': '2.0', + 'id': '0' + } + return self.rpc.send_request(get_balance) + + def sweep_dust(self): + sweep_dust = { + 'method': 'sweep_dust', + 'jsonrpc': '2.0', + 'id': '0' + } + return self.rpc.send_request(sweep_dust) + + def sweep_all(self, address): + sweep_all = { + 'method': 'sweep_all', + 'params' : { + 'address' : '' + }, + 'jsonrpc': '2.0', + 'id': '0' + } + return self.rpc.send_request(sweep_all)