diff --git a/.gitignore b/.gitignore index 1f970fef..a9a9c042 100644 --- a/.gitignore +++ b/.gitignore @@ -109,6 +109,7 @@ venv/ ENV/ env.bak/ venv.bak/ +.vscode/ # Spyder project settings .spyderproject @@ -128,4 +129,4 @@ dmypy.json # Pyre type checker .pyre/ -example/test/build \ No newline at end of file +example/test/build diff --git a/.pylintrc b/.pylintrc index bfe8d2b1..a44c6018 100644 --- a/.pylintrc +++ b/.pylintrc @@ -395,11 +395,7 @@ logging-modules=logging # Only show warnings with the listed confidence levels. Leave empty to show # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, # UNDEFINED. -confidence=HIGH, - CONTROL_FLOW, - INFERENCE, - INFERENCE_FAILURE, - UNDEFINED +confidence= # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 00000000..57068bc0 --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,2 @@ +temp/ +test_app/ \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000..a8b7df78 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,52 @@ +# Pytest Validation for DUNE + +# WARNING + +These tests are destructive. Do NOT run them when there is important data in your container! + +## Getting Started + +Make sure you have a recent version of `pytest` installed. Development was initially done with version `7.1`. + +Pytest can be installed with [pip](https://pypi.org/project/pytest/) +or - very likely - is part of your Linux distribution. Additionally, +the source is available [here](https://github.com/pytest-dev/pytest). + +Following the [instructions](../README.md#getting-started) for +building and installing DUNE. + +## Running the Tests + +From the root directory, execute all the tests as follows: +``` +$ pytest ./tests/ +``` + +If you need to run a single test, it can be done similar to the example below: +``` +$ pytest ./tests/test_version.py +``` + +More information regarding running and developing tests with pytest +can be found [here](https://docs.pytest.org/en/stable/). + +## What to do when you find a defect? + +Check DUNE's github site to see if the issue already exists. Make sure +that your issue isn't already fixed in the main DUNE branch by +rerunning the tests there if you haven't already. If the problem is +repeatable in the main branch and no issue already exists, add a new +issue including what platform (e.g. Ubuntu 20.04 x86, OSX arm64, etc), +complier, python version, pytest version, and steps we might need to +repeat the defect. + +## Adding new tests + +Make sure any new tests follow the same basic format as exist +here. You can use [test_version.py](test_version.py) as a sample. + +Note that you will NEED to run pylint against the tests. This can be +done from the root directory like this: +``` +$ pylint ./tests/.py +``` diff --git a/tests/common.py b/tests/common.py new file mode 100644 index 00000000..88b52eaa --- /dev/null +++ b/tests/common.py @@ -0,0 +1,15 @@ + + +import os + +# Find path for tests: +TEST_PATH = os.path.dirname(os.path.abspath(__file__)) + +# Set path for executable: +DUNE_EXE = os.path.split(TEST_PATH)[0] + "/dune" +print("Executable path: ", DUNE_EXE) + +# Default addresses +DEFAULT_HTTP_ADDR="127.0.0.1:8888" +DEFAULT_P2P_ADDR="0.0.0.0:9876" +DEFAULT_SHIP_ADDR="127.0.0.1:8080" diff --git a/tests/config.ini b/tests/config.ini new file mode 100644 index 00000000..c2d24189 --- /dev/null +++ b/tests/config.ini @@ -0,0 +1,24 @@ +wasm-runtime = eos-vm +abi-serializer-max-time-ms = 15 +chain-state-db-size-mb = 65536 +# chain-threads = 2 +contracts-console = true +http-server-address = 127.0.0.1:9991 +p2p-listen-endpoint = 0.0.0.0:9992 +state-history-endpoint = 127.0.0.1:9993 +verbose-http-errors = true +# http-threads = 2 +agent-name = "DUNE Test Node" +net-threads = 2 +max-transaction-time = 100 +producer-name = eosio +enable-stale-production = true +# producer-threads = 2 +# trace-history = false +# chain-state-history = false +resource-monitor-not-shutdown-on-threshold-exceeded=true + +plugin = eosio::chain_api_plugin +plugin = eosio::http_plugin +plugin = eosio::producer_plugin +plugin = eosio::producer_api_plugin diff --git a/tests/container.py b/tests/container.py new file mode 100644 index 00000000..4b15b735 --- /dev/null +++ b/tests/container.py @@ -0,0 +1,141 @@ +import os +import platform +import subprocess + + +class container: + _container_name = "" + _image_name = "" + + def __init__(self, container_name='dune_container', image_name='dune:latest'): + self._container_name = container_name + self._image_name = image_name + self._debug = True + + @staticmethod + def abs_host_path(directory): + abs_path = os.path.abspath(directory) + if platform.system() == 'Windows': + # remove the drive letter prefix and replace the separators + abs_path = abs_path[3:].replace('\\', '/') + else: + abs_path = abs_path[1:] + + return '/host/' + abs_path + + def get_container(self): + return self._container_name + + def get_image(self): + return self._image_name + + def execute_docker_cmd(self, cmd): + with subprocess.Popen(['docker'] + cmd, + stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: + stdout, stderr = proc.communicate() + if self._debug: + print('docker '+' '.join(cmd)) + print(stdout.decode('UTF-8')) + print(stderr.decode('UTF-8')) + return [stdout.decode('UTF-8'), stderr.decode('UTF-8'), proc.poll()] + + def file_exists(self, file_name): + return self.execute_cmd(['test', '-f', file_name])[2] == 0 + + def dir_exists(self, directory): + return self.execute_cmd(['test', '-d', directory])[2] == 0 + + def tar_dir(self, file_name, directory): + return self.execute_cmd(['tar', 'cvzf', file_name + '.tgz', directory]) + + def untar(self, directory): + return self.execute_cmd(['tar', 'xvzf', directory]) + + def cp_to_host(self, container_file, host_file): + return self.execute_docker_cmd( + ['cp', self._container_name + ":" + container_file, host_file]) + + def cp_from_host(self, host_file, container_file): + return self.execute_docker_cmd( + ['cp', host_file, self._container_name + ":" + container_file]) + + def rm_file(self, file_name): + self.execute_cmd(['rm', '-rf', file_name]) + + def find_pid(self, process_name): + stdout, _, _ = self.execute_cmd(['ps', 'ax']) + for line in stdout.splitlines(True): + if "PID TTY" in line: + continue + if process_name in line: + return line.split()[0] + return -1 + + def get_container_name(self): + return self._container_name + + def commit(self): + self.execute_docker_cmd(['commit', 'dune', 'dune']) + + def start(self): + print("Starting docker container [" + self._container_name + "]") + self.execute_docker_cmd(['container', 'start', self._container_name]) + + def stop(self): + print("Stopping docker container [" + self._container_name + "]") + self.execute_docker_cmd(['container', 'stop', self._container_name]) + + def destroy(self): + print("Destroying docker container [" + self._container_name + "]") + self.execute_docker_cmd(['container', 'stop', self._container_name]) + self.execute_docker_cmd(['container', 'rm', self._container_name]) + + def execute_cmd_at(self, directory, cmd): + with subprocess.Popen(['docker', 'container', 'exec', '-w', directory, + self._container_name] + cmd) as proc: + proc.communicate() + + def execute_cmd(self, cmd): + return self.execute_docker_cmd( + ['container', 'exec', self._container_name] + cmd) + + def execute_interactive_cmd(self, cmd): + with subprocess.Popen(['docker', 'container', + 'exec', '-i', self._container_name] + cmd) as proc: + proc.communicate() + + def execute_cmd2(self, cmd): + with subprocess.Popen(['docker', 'container', + 'exec', self._container_name] + cmd) as proc: + proc.communicate() + + def execute_bg_cmd(self, cmd): + return self.execute_cmd(cmd + ['&']) + + # possible values for the status: created, restarting, running, removing, paused, exited, dead + def check_status(self, status): + stdout, _, _ = self.execute_docker_cmd(['ps', '--filter', + 'status=' + status]) + for line in stdout.splitlines(True): + if "CONTAINER ID" in line: + continue + if self._container_name in line: + return True + return False + + # check if the container is still exists and was not deleted + def exists(self): + stdout, _, _ = self.execute_docker_cmd(['ps', '--filter', + 'name=' + self._container_name]) + for line in stdout.splitlines(True): + if "CONTAINER ID" in line: + continue + if self._container_name in line: + return True + return False + + # create a new container + def create(self): + print("Creating docker container [" + self._container_name + "]") + self.execute_docker_cmd(["run", "--name=" + self._container_name, + self._image_name, "exit"]) diff --git a/tests/show_untested_options.sh b/tests/show_untested_options.sh new file mode 100755 index 00000000..6aa60fef --- /dev/null +++ b/tests/show_untested_options.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +# This file MUST remain co-located with the files to work. + + +# Find paths to the tests and the dune executable. +SCRIPT=`readlink -f "$0"` +TEST_DIR=`dirname "$SCRIPT"` +DUNE_DIR=`dirname "$TEST_DIR"` +DUNE="$DUNE_DIR/dune" + +# Get a list of the options. +options=`$DUNE --help | grep -o "^ --[a-z\-]*"` + +# Get a list of the test files. +files=`find "$TEST_DIR" | grep "[.]py\$"` + +# Return value. Initially set to zero/success. +rv=0 + +# Search for each option. +for opt in $options; do + if ! grep --quiet \"$opt\" $files ; then + # Report missing options and set the return value to non-zeor/failure. + echo "Missing option: $opt" + rv=1 + fi +done + +# Report success/fail. +exit $rv diff --git a/tests/test_all_options_tested.py b/tests/test_all_options_tested.py new file mode 100644 index 00000000..d0c47f43 --- /dev/null +++ b/tests/test_all_options_tested.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +"""Test DUNE Version_ + +This script tests that the compiled binary produce expected output +in response to `--version` option. +""" + + + +import subprocess + +from common import TEST_PATH + + +def test_all_options_tested(): + """Test that all the options from the output of `--help` are in the various test files.""" + + script=TEST_PATH+"/show_untested_options.sh" + + subprocess.run(script, check=True) diff --git a/tests/test_boostrap.py b/tests/test_boostrap.py new file mode 100644 index 00000000..e51bc796 --- /dev/null +++ b/tests/test_boostrap.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +"""Test DUNE bootstrap + +These options are tested: + --create-key + --import-dev-key + --bootstrap-system-full +""" + +import subprocess + +from common import DUNE_EXE + +# Globals +NODE_NAME = "my_node" +ACCT_NAME = "myaccount" + + + +def test_booststrap(): + + # Remove any existing containers. + subprocess.run([DUNE_EXE,"--destroy-container"], check=True) + + # Start the new node. + subprocess.run([DUNE_EXE,"--start",NODE_NAME], check=True) + + # Create an account. + subprocess.run([DUNE_EXE,"--create-account",ACCT_NAME], check=True) + + # Create a key. Get it to a var as well. + public_key = None + private_key = None + stdout_result = subprocess.run([DUNE_EXE,"--create-key"], check=True, stdout=subprocess.PIPE) + result_list = stdout_result.stdout.decode().split("\n") + for entry in result_list: + # ignore empty entries. + if len(entry) == 0: + continue + items = entry.split(': ') + if len(items) == 2: + if items[0] == "Private key": + private_key = items[1] + elif items[0] == "Public key": + public_key = items[1] + assert private_key is not None + + # Import the key. + subprocess.run([DUNE_EXE,"--import-dev-key",private_key], check=True) + + # Bootstrap the system. + subprocess.run([DUNE_EXE, "--bootstrap-system-full"], check=True) + assert False # Asserting because --bootstrap-system-full doesn't currently behave correctly. diff --git a/tests/test_container.py b/tests/test_container.py new file mode 100644 index 00000000..367a3c28 --- /dev/null +++ b/tests/test_container.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +"""Test DUNE container controls + +This script test works with the Docker container. +""" +import subprocess + +from common import DUNE_EXE +from container import container + + +def test_container_actions(): + """ Test the start, stop, and destroy action for containers. """ + + cntr = container('dune_container', 'dune:latest') + + # Remove any container that already exists. + if cntr.exists(): + subprocess.run([DUNE_EXE, "--destroy-container"], check=True) + + # Create/start the container. + subprocess.run([DUNE_EXE, "--start-container"], check=True) + assert cntr.check_status("running") is True + + # Stop the container. + subprocess.run([DUNE_EXE, "--stop-container"], check=True) + assert cntr.check_status("exited") is True + + # Restart the container. + subprocess.run([DUNE_EXE, "--start-container"], check=True) + assert cntr.check_status("running") is True + + # Destroy the container. + subprocess.run([DUNE_EXE, "--destroy-container"], check=True) + assert cntr.exists() is False diff --git a/tests/test_debug.py b/tests/test_debug.py new file mode 100644 index 00000000..330dc037 --- /dev/null +++ b/tests/test_debug.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + +"""Test DUNE Version + +This script tests that the compiled binary produces output in response +to `--debug` option. + +""" + + + +import subprocess + +from common import DUNE_EXE + + +def test_version_debug(): + """Test that the output of `--version --debug` is as expected.""" + + # Call DUNE, we only care that `--debug` is available, not that it + # does anything. For now. + subprocess.run([DUNE_EXE,"--version","--debug"], check=True) diff --git a/tests/test_deploy.py b/tests/test_deploy.py new file mode 100644 index 00000000..66d1b8f8 --- /dev/null +++ b/tests/test_deploy.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 + +"""Test DUNE Functions. + +This script tests work with the smart contract related keys: + --deploy + --send-action +""" + +import os +import shutil +import subprocess + +from common import DUNE_EXE,TEST_PATH + +# Globals +NODE_NAME = "my_node" +ACCT_NAME = "myaccount" + +PROJECT_NAME = "test_app" +TEST_APP_DIR = TEST_PATH + "/" + PROJECT_NAME +TEST_APP_BLD_DIR = TEST_APP_DIR + "/build/" + PROJECT_NAME +TEST_APP_WASM = TEST_APP_BLD_DIR + "/" + PROJECT_NAME + ".wasm" # TEST_APP_BLD_DIR + "/test_app.wasm" + + +def test_deploy(): + """Test `--deploy` key.""" + + # Remove any existing containers and old build directories. + subprocess.run([DUNE_EXE,"--destroy-container"], check=True) + if os.path.exists(TEST_APP_DIR): + print("Removing TEST_APP_DIR: ", TEST_APP_DIR) + shutil.rmtree(TEST_APP_DIR) + + # Create a new node and an account. + subprocess.run([DUNE_EXE, "--start", NODE_NAME], check=True) + subprocess.run([DUNE_EXE, "--create-account", ACCT_NAME], check=True) + + # Create and build a test app. + subprocess.run([DUNE_EXE, "--create-cmake-app", PROJECT_NAME, TEST_PATH], check=True) + subprocess.run([DUNE_EXE, "--cmake-build", TEST_APP_DIR], check=True) + assert os.path.isfile(TEST_APP_WASM) is True + + subprocess.run([DUNE_EXE, "--deploy", TEST_APP_BLD_DIR, ACCT_NAME], check=True) + + # Send the action and search for a response in the result. + # ./dune --debug --send-action myaccount hi ["test"] eosio@active + results = subprocess.run([DUNE_EXE, "--send-action", ACCT_NAME, "hi", '["test"]', "eosio@active"], check=True, stdout=subprocess.PIPE) + assert b'>> Name : test' in results.stdout + + # Clean up after tests. + shutil.rmtree(TEST_APP_DIR) diff --git a/tests/test_help.py b/tests/test_help.py new file mode 100644 index 00000000..d7e5b414 --- /dev/null +++ b/tests/test_help.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +"""Test DUNE Help + +This script tests that the compiled binary produce expected output +in response to `-h`, `--help`, and `` options. +""" + + + +import subprocess + +from common import DUNE_EXE + + +def test_invalid_option(): + """Test that the output of `dune ` is as expected.""" + + # List of expected values. + expect_list = \ + [ + b'usage: dune', + b'dune: error: unrecognized arguments: --some-invalid-option' + ] + + # Call the tool, check for failed return code + # pylint: disable=subprocess-run-check + completed_process = subprocess.run([DUNE_EXE,"--some-invalid-option"], stderr=subprocess.PIPE) + assert completed_process.returncode != 0 + + # Test for expected values in the captured output. + for expect in expect_list: + assert expect in completed_process.stderr + + +def test_help(): + """Test that the output of `dune -h` and `dune --help` is as expected.""" + + # List of expected values. + expect_list = \ + [ + b'usage: dune', + b'DUNE: Docker Utilities for Node Execution', + b'optional arguments:', + b'-h, --help', + b'--monitor', + b'--start', + b'--stop', + b'--remove', + b'--list', + b'--version', + ] + + + # Call DUNE. + completed_process_h = subprocess.run([DUNE_EXE,"-h"], check=True, stdout=subprocess.PIPE) + + # Call DUNE. + completed_process_help = subprocess.run([DUNE_EXE,"--help"], check=True, stdout=subprocess.PIPE) + + # Test that the output of all the above executions is the same + assert completed_process_h.stdout == completed_process_help.stdout + + # Test for expected values in the captured output. + # We need only test ONE output because we ensure the output is the same above. + for expect in expect_list: + assert expect in completed_process_h.stdout diff --git a/tests/test_keys.py b/tests/test_keys.py new file mode 100644 index 00000000..bb9b6dc8 --- /dev/null +++ b/tests/test_keys.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +"""Test DUNE Version + +This script tests work with the crypto keys: +--create-key +--import-dev-key + +""" +import subprocess + +from common import DUNE_EXE +from container import container + + +def test_create_and_import_keys(): + """Test `--create-key` and `--import-dev-key` key.""" + + # Ensure a container exists. + cntr = container('dune_container', 'dune:latest') + if not cntr.exists(): + cntr.create() + + # Create a key. Get it to a var as well. + public_key = None + private_key = None + stdout_result = subprocess.run([DUNE_EXE,"--create-key"], check=True, stdout=subprocess.PIPE) + result_list = stdout_result.stdout.decode().split("\n") + for entry in result_list: + # ignore empty entries. + if len(entry) == 0: + continue + items = entry.split(': ') + if len(items) == 2: + if items[0] == "Private key": + private_key = items[1] + elif items[0] == "Public key": + public_key = items[1] + assert public_key is not None + assert private_key is not None + + # Import the key. + subprocess.run([DUNE_EXE,"--import-dev-key",private_key], check=True) diff --git a/tests/test_list_features.py b/tests/test_list_features.py new file mode 100644 index 00000000..e23aee3f --- /dev/null +++ b/tests/test_list_features.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +"""Test DUNE List Features + +This script tests that the compiled binary produce expected output in +response to the `--list-features` option. +""" + + + +import subprocess + +from common import DUNE_EXE + + +def test_list_features(): + """Test that the output of `dune --list-features` is as expected.""" + + # List of expected output lines from `dune --list-features`. + expect_list = \ + [ + b'KV_DATABASE', + b'ACTION_RETURN_VALUE', + b'BLOCKCHAIN_PARAMETERS', + b'GET_SENDER', + b'FORWARD_SETCODE', + b'ONLY_BILL_FIRST_AUTHORIZER', + b'RESTRICT_ACTION_TO_SELF', + b'DISALLOW_EMPTY_PRODUCER_SCHEDULE', + b'FIX_LINKAUTH_RESTRICTION', + b'REPLACE_DEFERRED', + b'NO_DUPLICATE_DEFERRED_ID', + b'ONLY_LINK_TO_EXISTING_PERMISSION', + b'RAM_RESTRICTIONS', + b'WEBAUTHN_KEY', + b'WTMSIG_BLOCK_SIGNATURES', + ] + + # Convert the list to a useful comparison value. + expect = b'' + for temp in expect_list: + expect = expect + temp + b'\n' + + # Call the tool, check return code, check expected value. + completed_process = subprocess.run([DUNE_EXE,"--list-features"], + check=True, stdout=subprocess.PIPE) + assert completed_process.stdout == expect diff --git a/tests/test_monitor.py b/tests/test_monitor.py new file mode 100644 index 00000000..8e9153db --- /dev/null +++ b/tests/test_monitor.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +"""Test DUNE Version + +This script tests --monitor key + +""" +import subprocess + +from common import DUNE_EXE +from container import container + + +def test_monitor(): + """Test `--monitor` key.""" + + + # Remove any container that already exists. + cntr = container('dune_container', 'dune:latest') + if cntr.exists(): + subprocess.run([DUNE_EXE, "--destroy-container"], check=True) + + # This will start a container; however, there will be NO active node, so it will fail. + results = subprocess.run([DUNE_EXE, "--monitor"], capture_output=True, check=False) + assert results.returncode != 0 + assert cntr.check_status("running") is True + + # Start a node. + subprocess.run([DUNE_EXE,"--start", "my_node"], check=True) + + # Now try to monitor again. + results = subprocess.run([DUNE_EXE, "--monitor"], capture_output=True, check=False) + assert b'server_version' in results.stdout diff --git a/tests/test_nodes.py b/tests/test_nodes.py new file mode 100644 index 00000000..759882d5 --- /dev/null +++ b/tests/test_nodes.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 + +"""Test various DUNE commands. + +This script tests that the compiled binary produce expected output for these commands: + --start + --config + --stop + --remove + --simple-list + --list + --get-active + --set-active + --export-node + --import-node +""" + + +import os # mkdir +import shutil # rmtree +import subprocess + +# pylint: disable=wildcard-import +from common import * # local defines +from container import container + + +# Globals +NODE_ALPHA = "ALPHA_NODE" +NODE_BRAVO = "BRAVO_NODE" +NODE_CHARLIE = "CHARLIE_NODE" + +CONFIG_PATH = TEST_PATH +CONFIG_FILE = TEST_PATH + "/config.ini" + +EXPORT_DIR = TEST_PATH + "/temp" + +ALT_HTTP_ADDR="127.0.0.1:9991" +ALT_P2P_ADDR="0.0.0.0:9992" +ALT_SHIP_ADDR="127.0.0.1:9993" + + +def remove_all(): + """ Remove any existing nodes. """ + + # Call dune, check the return is True. + completed_process = subprocess.run([DUNE_EXE,"--simple-list"], + check=True, stdout=subprocess.PIPE) + + # Convert the captured stdin to a list. + result_list = completed_process.stdout.decode().split("\n") + + # Remove the header. + result_list.pop(0) + + # Remove all the entries in the list. + for entry in result_list: + # ignore empty entries. + if len(entry) == 0: + continue + # Remove the entry + name = entry.split('|')[0] + print("Removing: ", name) + subprocess.run([DUNE_EXE,"--remove",name], check=True) + + +def validate_node_state( node_name, active_state, running_state ): + """Validate the result of a call to `dune --simple-list` contains the + node in a given state. + + :param node_name: The node to search for. + :param active_state: True/False + :param running_state: True/False + """ + + # Validate the entry + assert active_state in (True, False) + assert running_state in (True, False) + + expect = node_name + "|" + if active_state: + expect += "Y|" + else: + expect += "N|" + if running_state: + expect += "Y|" + else: + expect += "N|" + expect += DEFAULT_HTTP_ADDR + "|" + DEFAULT_P2P_ADDR + "|" + DEFAULT_SHIP_ADDR + + # Call dune, check the return is True. + completed_process = subprocess.run([DUNE_EXE,"--simple-list"], + check=True, stdout=subprocess.PIPE) + + # Convert the captured stdin to a list for comparison with expected output. + result_list = completed_process.stdout.decode().split("\n") + + assert expect in result_list + + +# pylint: disable=too-many-branches +def validate_node_list( node_list ): + """Validate the result of a call to `dune --simple-list` contains all + the nodes and states in node_list. + + :param node_list: A list of lists with the form: + [node name, active (t/F), running (t/F), http_addr, p2p_addr, ship_addr]. + where node name is required, but other values have reasonable defaults. + """ + + # Test algorith: + # Build the list of expected results. + # Get the actual results. + # For each entry in the actual results, + # Test the entry is in expected and remove it. + # Test that all entries are removed from expected results. + + # Create a list of expected strings. + expect_list = ["Node|Active|Running|HTTP|P2P|SHiP"] + for entry in node_list: + + # Valid the array count in the entry. + is_valid = True + if not len(entry) in (1,2,3,4,5,6): + print("len() should be a value between 1 and 6 but is: ", len(entry), " value: ", entry) + is_valid = False + + # Determine if this entry should be active. + active = False + if len(entry) > 1: + active = entry[1] + if not active in (True,False): + print("Invalid value for Active. Expect True/False, received: ", active) + is_valid = False + + # Determine if this entry should be running. + running = False + if len(entry) > 2: + running = entry[2] + if not running in (True,False): + print("Invalid value for Running. Expect True/False, received: ", running) + is_valid = False + + # Determine the expected ip addrs. + http_addr=DEFAULT_HTTP_ADDR + if len(entry) > 3: + http_addr = entry[3] + p2p_addr=DEFAULT_P2P_ADDR + if len(entry) > 4: + p2p_addr = entry[4] + ship_addr=DEFAULT_SHIP_ADDR + if len(entry) > 5: + ship_addr = entry[5] + + assert is_valid + + # Make an expected string for this entry and append it the expected results list + temp = entry[0] + "|" + if entry[1]: + temp += "Y|" + else: + temp += "N|" + if entry[2]: + temp += "Y|" + else: + temp += "N|" + temp += http_addr + "|" + p2p_addr + "|" + ship_addr + expect_list.append(temp) + + + # Call dune, check the return is True. + completed_process = subprocess.run([DUNE_EXE,"--simple-list"], + check=True, stdout=subprocess.PIPE) + + # Convert the captured stdin to a list for comparison with expected output. + result_list = completed_process.stdout.decode().split("\n") + + # Iterate over the elements in the results list + for entry in result_list: + # Ignore empty lines. + if entry == "": + continue + # Test a value exists in expect_list, then remove it. + assert entry in expect_list + expect_list.remove(entry) + + # Test that there are no entries remaining in expect_list. + assert not expect_list + + +def expect_empty_verbose_list(): + """Test that the output of list options are empty.""" + + # List of expected output lines from `dune --list`. + empty_verbose_list = \ + "Node Name | Active? | Running? | HTTP | P2P | SHiP \n" + \ + "---------------------------------------------------------------------------------\n" + + # Call the tool, check expected value. + completed_process = subprocess.run([DUNE_EXE,"--list"], check=True, stdout=subprocess.PIPE) + assert completed_process.stdout.decode() == empty_verbose_list + + +# pylint: disable=too-many-statements +def test_nodes(): + """Run the tests.""" + + # Remove any container that already exists and create a fresh one. + cntr = container('dune_container', 'dune:latest') + if cntr.exists(): + subprocess.run([DUNE_EXE, "--destroy-container"], check=True) + subprocess.run([DUNE_EXE, "--start-container"], check=True) + + # Ensure there are no existing nodes. + # Tests `--simple-list` and `--list` + remove_all() + validate_node_list([]) + expect_empty_verbose_list() + + # Create a node and test its state. + # Tests `--start` when the node needs to be created. + subprocess.run([DUNE_EXE,"--start", NODE_ALPHA], check=True) + validate_node_state(NODE_ALPHA, True, True) + # Stop the node and test its state. + # Tests `--stop` + subprocess.run([DUNE_EXE,"--stop", NODE_ALPHA], check=True) + validate_node_state(NODE_ALPHA, True, False) + # Restart the node and test its state. + # Tests `--start` when the node already exists. + subprocess.run([DUNE_EXE,"--start", NODE_ALPHA], check=True) + validate_node_state(NODE_ALPHA, True, True) + + # Create a 2nd node and test the state of both nodes. + # Tests the behavior of `--start` on an already active, running node. + subprocess.run([DUNE_EXE,"--start", NODE_BRAVO], check=True) + validate_node_state(NODE_BRAVO, True, True) + validate_node_list([[NODE_ALPHA, False, False],[NODE_BRAVO, True, True]]) + + # Test --get-active shows NODE_BRAVO + # Tests `--get-active`. + assert subprocess.run([DUNE_EXE,"--get-active"], check=True, stdout=subprocess.PIPE).stdout.decode() == (NODE_BRAVO + "\n") + + # Test --set-active works to switch to NODE_ALPHA and --get active returns the correct value. + # Tests `--set-active` switch active node while run state is left unchanged. + subprocess.run([DUNE_EXE,"--set-active", NODE_ALPHA], check=True) + validate_node_list([[NODE_ALPHA, True, False],[NODE_BRAVO, False, True]]) # Note this is TF,FT + assert subprocess.run([DUNE_EXE,"--get-active"], check=True, stdout=subprocess.PIPE).stdout.decode() == (NODE_ALPHA + "\n") + + # Remove NODE_ALPHA, ensure it is no longer in the list. + # Tests `--remove`. + subprocess.run([DUNE_EXE,"--remove", NODE_ALPHA], check=True) + validate_node_list([[NODE_BRAVO, False, True]]) # Note the state of NODE_BRAVO is FT + + # Remove anything to get to a clean slate. + remove_all() + + # Test `--start` where start includes a config path. + subprocess.run([DUNE_EXE,"--start", NODE_ALPHA, "--config", CONFIG_PATH], check=True) + validate_node_list([[NODE_ALPHA, True, True, ALT_HTTP_ADDR, ALT_P2P_ADDR, ALT_SHIP_ADDR]]) + + # Test `--start` where start includes a config file. + subprocess.run([DUNE_EXE,"--start", NODE_BRAVO, "--config", CONFIG_FILE], check=True) + validate_node_list([[NODE_ALPHA, False, False, ALT_HTTP_ADDR, ALT_P2P_ADDR, ALT_SHIP_ADDR], + [NODE_BRAVO, True, True, ALT_HTTP_ADDR, ALT_P2P_ADDR, ALT_SHIP_ADDR]]) + + # Test `--start` with invalid config file path. + # pylint: disable=subprocess-run-check + completed_process = subprocess.run([DUNE_EXE,"--start", NODE_ALPHA, "--config", "unknown_config"], check=False) + assert completed_process.returncode != 0 + + # Test `--config` alone. + # pylint: disable=subprocess-run-check + completed_process = subprocess.run([DUNE_EXE,"--config", "unknown_config"], check=False) + assert completed_process.returncode != 0 + + # + # Testing the import and export of nodes may not be sophisticated + # enough. + # + + # remove any existing directory and ensure a fresh one is created. + if os.path.exists(EXPORT_DIR): + print("Removing EXPORT_DIR: ", EXPORT_DIR) + shutil.rmtree(EXPORT_DIR) + os.mkdir(EXPORT_DIR) + + + # Just add an additional node for export. + subprocess.run([DUNE_EXE,"--start", NODE_CHARLIE], check=True) + + + # things we need to test: + # export to TEST_PATH, TEST_PATH/my_file.tgz, TEST_PATH/does_not_exist_yet/my_file.tgz + + # Test --export-node using standard filename. + subprocess.run([DUNE_EXE,"--export-node", NODE_ALPHA, EXPORT_DIR], check=True) + assert os.path.exists(EXPORT_DIR + "/" + NODE_ALPHA + ".tgz") + + # Test --export-node using provided filename. + subprocess.run([DUNE_EXE,"--export-node", NODE_BRAVO, EXPORT_DIR + "/bravo_export.tgz"], check=True) + assert os.path.exists(EXPORT_DIR + "/bravo_export.tgz") + + # Test --export-node using non-existing path. + subprocess.run([DUNE_EXE,"--export-node", NODE_CHARLIE, EXPORT_DIR + "/new_path/charlie_export.tgz"], check=True) + assert os.path.exists(EXPORT_DIR + "/new_path/charlie_export.tgz") + + + # Clean up before import. + remove_all() + + # Test --import-node + # Import each node from the export tests and + subprocess.run([DUNE_EXE,"--import-node", EXPORT_DIR + "/ALPHA_NODE.tgz", NODE_ALPHA], check=True) + validate_node_list([[NODE_ALPHA, True, True, ALT_HTTP_ADDR, ALT_P2P_ADDR, ALT_SHIP_ADDR]]) + + subprocess.run([DUNE_EXE,"--import-node", EXPORT_DIR + "/bravo_export.tgz", NODE_BRAVO], check=True) + validate_node_list([[NODE_ALPHA, False, False, ALT_HTTP_ADDR, ALT_P2P_ADDR, ALT_SHIP_ADDR], + [NODE_BRAVO, True, True, ALT_HTTP_ADDR, ALT_P2P_ADDR, ALT_SHIP_ADDR]]) + + subprocess.run([DUNE_EXE,"--import-node", EXPORT_DIR + "/new_path/charlie_export.tgz", NODE_CHARLIE], check=True) + validate_node_list([[NODE_ALPHA, False, False, ALT_HTTP_ADDR, ALT_P2P_ADDR, ALT_SHIP_ADDR], + [NODE_BRAVO, False, True, ALT_HTTP_ADDR, ALT_P2P_ADDR, ALT_SHIP_ADDR], + [NODE_CHARLIE, True, True, DEFAULT_HTTP_ADDR, DEFAULT_P2P_ADDR, DEFAULT_SHIP_ADDR]]) + + # Finally, clean everything up before final return. + remove_all() diff --git a/tests/test_project.py b/tests/test_project.py new file mode 100644 index 00000000..3e6dc5fa --- /dev/null +++ b/tests/test_project.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 + +"""Test DUNE Version + +This script tests work with the a smart contract project keys: + --create-cmake-app + --create-bare-app + --cmake-build + --ctest + --gdb +""" + +import glob +import os +import shutil +import subprocess + +from common import DUNE_EXE,TEST_PATH +from container import container + + +PROJECT_NAME = "test_app" +TEST_APP_DIR = TEST_PATH + "/" + PROJECT_NAME +TEST_APP_BLD_DIR = TEST_APP_DIR + "/build/" + PROJECT_NAME +TEST_APP_WASM = TEST_APP_BLD_DIR + "/" + PROJECT_NAME + ".wasm" # TEST_APP_BLD_DIR + "/test_app.wasm" + + +def remove_existing(): + """ Remove an existing `./test_app` dir. """ + + cntr = container('dune_container', 'dune:latest') + cntr.stop() + + if os.path.exists(TEST_APP_DIR): + print("Removing TEST_APP_DIR: ", TEST_APP_DIR) + shutil.rmtree(TEST_APP_DIR) + + +def test_create_cmake_app(): + """Test `--create-cmake-app` key.""" + + remove_existing() + + # Expected files. + filelist = [TEST_APP_DIR + '/', + TEST_APP_DIR + '/src', + TEST_APP_DIR + '/src/' + PROJECT_NAME + '.cpp', + TEST_APP_DIR + '/src/CMakeLists.txt', + TEST_APP_DIR + '/include', + TEST_APP_DIR + '/include/' + PROJECT_NAME + '.hpp', + TEST_APP_DIR + '/ricardian', + TEST_APP_DIR + '/ricardian/' + PROJECT_NAME + '.contracts.md', + TEST_APP_DIR + '/build', + TEST_APP_DIR + '/CMakeLists.txt', + TEST_APP_DIR + '/README.txt'] + + # Create the test app. + completed_process = subprocess.run([DUNE_EXE, "--create-cmake-app", PROJECT_NAME, TEST_PATH], check=True) + assert completed_process.returncode == 0 + assert os.path.isdir(TEST_APP_DIR) is True + + # Get a list of the files created. + lst = glob.glob(TEST_APP_DIR + "/**", recursive=True) + + # Sort the lists and compare. + filelist.sort() + lst.sort() + assert filelist == lst + + # Cleanup + shutil.rmtree(TEST_APP_DIR) + + +def test_create_bare_app(): + """Test `--create-bare-app` key.""" + + remove_existing() + + # Expected file list. + filelist = [TEST_APP_DIR + '/', + TEST_APP_DIR + '/' + PROJECT_NAME + '.hpp', + TEST_APP_DIR + '/' + PROJECT_NAME + '.cpp', + TEST_APP_DIR + '/' + PROJECT_NAME + '.contracts.md', + TEST_APP_DIR + '/README.txt'] + + + subprocess.run([DUNE_EXE, "--create-bare-app", PROJECT_NAME, TEST_PATH], check=True) + assert os.path.isdir(TEST_APP_DIR) is True + + # Actual file list. + lst = glob.glob(TEST_APP_DIR + "/**", recursive=True) + + # Sort and compare expected and actual. + filelist.sort() + lst.sort() + assert filelist == lst + + # Cleanup + shutil.rmtree(TEST_APP_DIR) + + + +def test_cmake_and_ctest(): + """Test `--cmake` and `--ctest` key.""" + + remove_existing() + + # Create the cmake app, test it exists. + subprocess.run([DUNE_EXE, "--create-cmake-app", PROJECT_NAME, TEST_PATH], check=True) + assert os.path.isdir(TEST_APP_DIR) is True + + # Build the app, test that the expected output file is created. + subprocess.run([DUNE_EXE, "--cmake-build", TEST_APP_DIR], check=True) + assert os.path.isfile(TEST_APP_WASM) is True + + # Test that CTest files are run. + # @TODO - This should be updated to create and test some PASSING tests. + # @TODO - This should be updated to create and test some FAILING tests. + with subprocess.Popen( + [DUNE_EXE, "--debug", "--ctest", TEST_APP_DIR, "--", "--no-tests=ignore"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8") as proc: + _, stderr = proc.communicate() + + assert "No tests were found!!!" in stderr + + + shutil.rmtree(TEST_APP_DIR) + + +def test_gdb(): + """Test `--gdb` key.""" + + # Simply ensure gdb is run. + proc = subprocess.run([DUNE_EXE, "--gdb", "/bin/sh"], capture_output=True, encoding="utf-8", check=True) + assert "GNU gdb" in proc.stdout diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 00000000..ec0ac035 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +"""Test DUNE Version_ + +This script tests that the compiled binary produce expected output +in response to `--version` option. +""" + + + +import subprocess + +from common import DUNE_EXE + + +def test_version(): + """Test that the output of `--version` is as expected.""" + + # List of expected values. + expect_list = \ + [ + b'DUNE v1.', + ] + + # Call DUNE. + completed_process = subprocess.run([DUNE_EXE,"--version"], check=True, stdout=subprocess.PIPE) + + # Test for expected values in the captured output. + for expect in expect_list: + assert expect in completed_process.stdout diff --git a/tests/test_wallet.py b/tests/test_wallet.py new file mode 100644 index 00000000..57529ff3 --- /dev/null +++ b/tests/test_wallet.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +"""Test DUNE Version + +This script tests export and import of the wallet +""" + +import os +import shutil +import subprocess + +from container import container +from common import DUNE_EXE + + +def tar_dir(file_name, directory): + subprocess.run(['tar', 'cvzf', file_name + '.tgz', directory], check=True) + + +def untar(file_name): + subprocess.run(['tar', 'xvzf', file_name], check=True) + + +def test_export(): + """Test `--export-wallet` key.""" + + subprocess.run([DUNE_EXE, "--export-wallet"], check=True) + + assert os.path.exists("wallet.tgz") is True + + +def test_import(): + """Test `--import-wallet` key.""" + + cntr = container('dune_container', 'dune:latest') + + cntr.rm_file("/app/wallet.tgz") + + assert os.path.exists("wallet.tgz") is True + + untar("wallet.tgz") + + # Platform dependent locale encoding is acceptable here. + # pylint: disable=unspecified-encoding + with open("_wallet/eosio-wallet/import_test_file", "w") as flag_file: + flag_file.write("this is a flag file for testing purposes") + + tar_dir("wallet", "_wallet") + + # Use wallet.tgz created by successfully finished test of export + subprocess.run([DUNE_EXE, "--debug", "--import-wallet", "./wallet.tgz"], check=True) + + os.remove("wallet.tgz") + shutil.rmtree("_wallet") + + assert cntr.file_exists("/root/eosio-wallet/import_test_file") is True + + cntr.rm_file("/root/eosio-wallet/import_test_file") diff --git a/tests/untested.py b/tests/untested.py new file mode 100644 index 00000000..36a878c9 --- /dev/null +++ b/tests/untested.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +""" +This file exists solely to allow for `show_untested_options.sh` to ignore certain keys. + +These keys + "--set-core-contract" + "--set-bios-contract" + "--set-token-contract" + +are not tested because: + + Alex: I already do deployment of a contract and calling of + it's action during test of --deploy key. Does it mean that all keys above don't need to be + tested because actually they do deployment of contracts? + + Bucky: Yes, I think its fine to not worry about testing those specifically as they are just + deploying contracts. When we get to creating the EOS specific plugin system we will have will + need to validate the bootstrapping, but it will be more than deploying at that point. + +"""