From 6defa1a49b8bdd5874c80d46cf8f9585163fa460 Mon Sep 17 00:00:00 2001 From: brainbot-devops Date: Thu, 22 Aug 2019 11:31:33 +0200 Subject: [PATCH] Fix botched merge. This reverts commit fb9fdd490cd63ebb8daa1c46730680a0ae5f2d29, reversing changes made to 104992261a5fd720c7840e02c714e2c8f9affbf7. --- .circleci/config.yml | 63 ++++++---- .../scripts/skip-on-bad-commit-message.py | 7 ++ .circleci/scripts/validate-commit-message.py | 4 + Dockerfile | 11 +- README.rst | 19 ++- examples/scenario-example-v2.yaml | 4 +- scenario_player/exceptions/cli.py | 8 ++ scenario_player/main.py | 115 +++++++++++------- scenario_player/utils/legacy.py | 21 ++++ scenario_player/utils/logs.py | 70 +++++++++++ tests/unittests/cli/keystore/UTC--1 | 1 + tests/unittests/cli/keystore/password | 1 + tests/unittests/cli/keystore/wrong_password | 1 + .../scenario/join-network-scenario-J1.yaml | 48 ++++++++ tests/unittests/cli/test_cli.py | 88 ++++++++++++++ tests/unittests/utils/test_logs.py | 101 +++++++++++++++ 16 files changed, 476 insertions(+), 86 deletions(-) create mode 100755 .circleci/scripts/skip-on-bad-commit-message.py create mode 100644 scenario_player/exceptions/cli.py create mode 100644 scenario_player/utils/logs.py create mode 100644 tests/unittests/cli/keystore/UTC--1 create mode 100644 tests/unittests/cli/keystore/password create mode 100644 tests/unittests/cli/keystore/wrong_password create mode 100644 tests/unittests/cli/scenario/join-network-scenario-J1.yaml create mode 100644 tests/unittests/cli/test_cli.py create mode 100644 tests/unittests/utils/test_logs.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 25280c9f5..1bacee3e9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -37,7 +37,6 @@ PR_branches_only: &PR_branches_only ignore: - /^\d+\.\d+.*/ - ################################################################################ # # # Custom Executor definitions. # @@ -68,6 +67,17 @@ commands: # CI Setup Commands # # ================= # + setup-job: + description: | + Attach the workspace and load ENV variables. + Additionally skips the job this is used in, if the commit message is invalid + (unless the workflow executing the job is running on "master" or "dev"). + steps: + - attach_workspace: + at: "/home/circleci" + - export_env_vars + - skip_bad_commit_format + export_env_vars: description: Export ENV variables used by our script. steps: @@ -114,12 +124,12 @@ commands: make install-dev - validate_commit_format: + skip_bad_commit_format: description: Validate the first line of the commit message against a REGEX. steps: - run: name: Validate the commit's title description. - command: python3 ${CI_SCRIPTS_DIR}/validate-commit-message.py + command: python3 ${CI_SCRIPTS_DIR}/skip-on-bad-commit-message.py # ================================== # # Linter and test execution commands # @@ -171,14 +181,6 @@ commands: jobs: - validate-commit-message: - executor: default-executor - steps: - - attach_workspace: - at: "/home/circleci" - - export_env_vars - - validate_commit_format - prep-system: executor: default-executor steps: @@ -192,11 +194,22 @@ jobs: # Run all test suites (aka the test harness) of the repository and lint-check # our code base. + test-harness: + executor: default-executor + steps: + - setup-job + - run_test_harness + + style-check-and-lint: + executor: default-executor + steps: + - setup-job + - lint_codebase + lint-and-test: executor: default-executor steps: - - attach_workspace: - at: "/home/circleci" + - setup-job - lint_codebase - run_test_harness @@ -205,9 +218,7 @@ jobs: bump-versions: executor: default-executor steps: - - attach_workspace: - at: "/home/circleci" - - export_env_vars + - setup-job - run: name: Add Github to known hosts. command: | @@ -227,6 +238,8 @@ jobs: executor: default-executor steps: - checkout + - export_env_vars + - skip_bad_commit_format - run: name: Publish to pypi using flit. command: | @@ -258,7 +271,12 @@ workflows: - prep-system: <<: *PR_branches_only - - lint-and-test: + - test-harness: + context: Raiden-SP-Context + requires: + - prep-system + + - style-check-and-lint: context: Raiden-SP-Context requires: - prep-system @@ -266,23 +284,19 @@ workflows: - finalize: requires: - prep-system - - lint-and-test + - style-check-and-lint + - test-harness Merge-Commit-Workflow: jobs: - prep-system: <<: *master_dev_only - - validate-commit-message: - context: Raiden-SP-Context - requires: - - prep-system - # Run linters and test harnesses against PR branches. - lint-and-test: context: Raiden-SP-Context requires: - - validate-commit-message + - prep-system # Bump the minor or patch for `master` on each merge commit. - bump-versions: @@ -302,7 +316,6 @@ workflows: - finalize: requires: - prep-system - - validate-commit-message - lint-and-test - bump-versions - tear-down diff --git a/.circleci/scripts/skip-on-bad-commit-message.py b/.circleci/scripts/skip-on-bad-commit-message.py new file mode 100755 index 000000000..70f7d4e6f --- /dev/null +++ b/.circleci/scripts/skip-on-bad-commit-message.py @@ -0,0 +1,7 @@ +import subprocess +import os +try: + subprocess.run(f"python {os.environ['CI_SCRIPTS_DIR']}/validate-commit-message.py".split(), check=True) +except subprocess.SubprocessError: + # The commit message is bogus, so we skip the current step. + subprocess.run("circleci step halt".split()) \ No newline at end of file diff --git a/.circleci/scripts/validate-commit-message.py b/.circleci/scripts/validate-commit-message.py index a7012bd22..e703a8fa1 100755 --- a/.circleci/scripts/validate-commit-message.py +++ b/.circleci/scripts/validate-commit-message.py @@ -6,6 +6,10 @@ print(f"Validating commit message {COMMIT_MSG!r}") print(f"Parsed commit type: {COMMIT_TYPE}") +if CURRENT_BRANCH not in ("master", "dev"): + # This workflow is executed on a PR - we skip validation for these. + exit(0) + if not COMMIT_TYPE: # The commit message title does not comply with any of our regexes. print("No commit type parsed - the commit message does not comply with the required pattern!") diff --git a/Dockerfile b/Dockerfile index fbfd72dd1..feb90a81e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,22 +4,13 @@ ARG PY_VERSION=3.7 FROM python:$PY_VERSION AS cache -# Clone raiden repo and switch to its `develop` branch -RUN git clone https://github.com/raiden-network/raiden /raiden -RUN git --git-dir /raiden/.git checkout develop - -# Install raiden's development dependencies. -RUN pip install -r /raiden/requirements/requirements-dev.txt - -# Install the raiden package -RUN pip install ./raiden +RUN pip install raiden FROM python:${PY_VERSION} ARG PY_VERSION # Copy raiden repository and site-packages from build cache -COPY --from=cache /raiden /raiden COPY --from=cache /usr/local/lib/python${PY_VERSION}/dist-packages /usr/local/lib/python${PY_VERSION}/dist-packages # Copy SP folder and install. diff --git a/README.rst b/README.rst index 15958900d..5a7d0aa47 100644 --- a/README.rst +++ b/README.rst @@ -1,22 +1,29 @@ .. image:: https://codecov.io/gh/raiden-network/scenario-player/branch/master/graph/badge.svg + :alt: Code Coverage :target: https://codecov.io/gh/raiden-network/scenario-player .. image:: https://circleci.com/gh/raiden-network/scenario-player.svg?style=shield + :alt: CI Status :target: https://circleci.com/gh/raiden-network/scenario-player +.. image:: https://img.shields.io/docker/cloud/build/raidennetwork/scenario-player + :alt: Docker Cloud + :target: https://cloud.docker.com/u/raidennetwork/repository/docker/raidennetwork/scenario-player/general + +.. image:: https://img.shields.io/github/tag-date/raiden-network/scenario-player?label=STABLE + :alt: Releases + :target: https://github.com/raiden-network/scenario-player/releases + .. image:: https://img.shields.io/github/license/raiden-network/scenario-player + :alt: License :target: https>//github.com/raiden-network/scenario-player .. image:: https://img.shields.io/github/issues-raw/raiden-network/scenario-player/bug?color=red&label=Open%20Bugs + :alt: Open Bugs :target: https://github.com/raiden-network/scenario-player/issues?q=is%3Aissue+is%3Aopen+label%3Abug -.. image:: https://img.shields.io/github/issues-raw/raiden-network/scenario-player/Feature request?color=orange&label=Open%20Feature%20Requests - :target: https://github.com/raiden-network/scenario-player/issues?q=is%3Aissue+is%3Aopen+label%3A%22Feature+request%22 - -.. image:: https://img.shields.io/github/tag-date/raiden-network/scenario-player?label=STABLE - -.. image:: https://img.shields.io/github/tag/raiden-network/scenario-player?label=LATEST +###################### Raiden Scenario Player ###################### diff --git a/examples/scenario-example-v2.yaml b/examples/scenario-example-v2.yaml index 3decfe4e3..92326cace 100644 --- a/examples/scenario-example-v2.yaml +++ b/examples/scenario-example-v2.yaml @@ -60,8 +60,10 @@ nodes: ## Options to apply to all nodes ## Option names correspond to Raiden cli options without the leading double dash (`--`) default_options: + ## Sets Raiden config parameters. gas-price: fast - # environment-type: development + #environment-type: development + #flat-fee: 10 #registry-contract-address: "0xbfa863Ac58a3E0A82B58a8e958F2752Bfb573388" #discovery-contract-address: "0xb2646EB8270a4de7451f6F7b259FdE17DBCeedc9" #secret-registry-contract-address: "0xA5c882f09c3DE551d76dcDE08890fAa0dD1F29E3" diff --git a/scenario_player/exceptions/cli.py b/scenario_player/exceptions/cli.py new file mode 100644 index 000000000..e86b034e3 --- /dev/null +++ b/scenario_player/exceptions/cli.py @@ -0,0 +1,8 @@ +from scenario_player.exceptions import ScenarioError + + +class WrongPassword(ScenarioError): + """ + Generic Error that gets raised if eth_keystore raises ValueError("MAC mismatch") + Usually that's caused by an invalid password + """ diff --git a/scenario_player/main.py b/scenario_player/main.py index 9fd50991b..77d0f17d5 100644 --- a/scenario_player/main.py +++ b/scenario_player/main.py @@ -10,7 +10,6 @@ from enum import Enum from itertools import chain from pathlib import Path -from typing import List import click import gevent @@ -25,6 +24,7 @@ from raiden.utils.cli import EnumChoiceType from scenario_player import tasks from scenario_player.exceptions import ScenarioAssertionError, ScenarioError +from scenario_player.exceptions.cli import WrongPassword from scenario_player.exceptions.services import ServiceProcessException from scenario_player.runner import ScenarioRunner from scenario_player.services.common.app import ServiceProcess @@ -36,6 +36,12 @@ post_task_state_to_rc, send_notification_mail, ) +from scenario_player.utils.legacy import MutuallyExclusiveOption +from scenario_player.utils.logs import ( + pack_n_latest_logs_for_scenario_in_dir, + pack_n_latest_node_logs_in_dir, + verify_scenario_log_dir, +) log = structlog.get_logger(__name__) @@ -85,6 +91,22 @@ def load_account_obj(keystore_file, password): return account +def get_password(password, password_file): + if password_file: + password = open(password_file, "r").read().strip() + if password == password_file is None: + password = click.prompt(text="Please enter your password: ", hide_input=True) + return password + + +def get_account(keystore_file, password): + try: + account = load_account_obj(keystore_file, password) + except ValueError: + raise WrongPassword + return account + + @click.group(invoke_without_command=True, context_settings={"max_content_width": 120}) @click.option( "--data-path", @@ -112,7 +134,20 @@ def main(ctx, chains, data_path): @main.command(name="run") @click.argument("scenario-file", type=click.File(), required=False) @click.option("--keystore-file", required=True, type=click.Path(exists=True, dir_okay=False)) -@click.password_option("--password", envvar="ACCOUNT_PASSWORD", required=True) +@click.option( + "--password-file", + type=click.Path(exists=True, dir_okay=False), + cls=MutuallyExclusiveOption, + mutually_exclusive=["password"], + default=None, +) +@click.option( + "--password", + envvar="ACCOUNT_PASSWORD", + cls=MutuallyExclusiveOption, + mutually_exclusive=["password-file"], + default=None, +) @click.option("--auth", default="") @click.option("--mailgun-api-key") @click.option( @@ -129,7 +164,15 @@ def main(ctx, chains, data_path): ) @click.pass_context def run( - ctx, mailgun_api_key, auth, password, keystore_file, scenario_file, notify_tasks, enable_ui + ctx, + mailgun_api_key, + auth, + password, + keystore_file, + scenario_file, + notify_tasks, + enable_ui, + password_file, ): scenario_file = Path(scenario_file.name).absolute() data_path = ctx.obj["data_path"] @@ -138,7 +181,9 @@ def run( log_file_name = construct_log_file_name("run", data_path, scenario_file) configure_logging_for_subcommand(log_file_name) - account = load_account_obj(keystore_file, password) + password = get_password(password, password_file) + + account = get_account(keystore_file, password) notify_tasks_callable = None if notify_tasks is TaskNotifyType.ROCKETCHAT: @@ -235,7 +280,20 @@ def run( @main.command(name="reclaim-eth") @click.option("--keystore-file", required=True, type=click.Path(exists=True, dir_okay=False)) -@click.password_option("--password", envvar="ACCOUNT_PASSWORD", required=True) +@click.option( + "--password-file", + type=click.Path(exists=True, dir_okay=False), + cls=MutuallyExclusiveOption, + mutually_exclusive=["password"], + default=None, +) +@click.option( + "--password", + envvar="ACCOUNT_PASSWORD", + cls=MutuallyExclusiveOption, + mutually_exclusive=["password-file"], + default=None, +) @click.option( "--min-age", default=72, @@ -243,12 +301,13 @@ def run( help="Minimum account non-usage age before reclaiming eth. In hours.", ) @click.pass_context -def reclaim_eth(ctx, min_age, password, keystore_file): +def reclaim_eth(ctx, min_age, password, password_file, keystore_file): from scenario_player.utils import reclaim_eth data_path = ctx.obj["data_path"] chain_rpc_urls = ctx.obj["chain_rpc_urls"] - account = load_account_obj(keystore_file, password) + password = get_password(password, password_file) + account = get_account(keystore_file, password) configure_logging_for_subcommand(construct_log_file_name("reclaim-eth", data_path)) @@ -271,29 +330,22 @@ def reclaim_eth(ctx, min_age, password, keystore_file): "Specifying 0 will pack all available logs for a scenario.", ) @click.option("--post-to-rocket/--no-post-to-rocket", default=True) -@click.argument("scenario-file", type=click.File(), required=True) +@click.argument("scenario-file", type=click.Path(exists=True, dir_okay=False), required=True) @click.pass_context def pack_logs(ctx, scenario_file, post_to_rocket, pack_n_latest, target_dir): data_path: Path = ctx.obj["data_path"].absolute() scenario_file = Path(scenario_file.name).absolute() scenario_name = Path(scenario_file.name).stem + log_file_name = construct_log_file_name("pack-logs", data_path, scenario_file) configure_logging_for_subcommand(log_file_name) - target_dir = Path(target_dir) - target_dir.mkdir(exist_ok=True) - # The logs are located at .raiden/scenario-player/scenarios/ # - make sure the path exists. - scenarios_path = data_path.joinpath("scenarios") - scenario_log_dir = scenarios_path.joinpath(scenario_name) - if not scenario_log_dir.exists(): - print(f"No log directory found for scenario {scenario_name} at {scenario_log_dir}") - return - - # List all folders - folders = [path for path in scenario_log_dir.iterdir() if path.is_dir()] + scenarios_path, scenario_log_dir = verify_scenario_log_dir(scenario_name, data_path) + # List all node folders which fall into the range of pack_n_latest + folders = pack_n_latest_node_logs_in_dir(scenario_log_dir, pack_n_latest) # List all files that match the filters `scenario_name` and the `pack_n_latest` counter. files = pack_n_latest_logs_for_scenario_in_dir(scenario_name, scenario_log_dir, pack_n_latest) @@ -325,31 +377,6 @@ def pack_logs(ctx, scenario_file, post_to_rocket, pack_n_latest, target_dir): post_to_rocket_chat(archive_fpath, **rc_message) -def pack_n_latest_logs_for_scenario_in_dir(scenario_name, scenario_log_dir: Path, n) -> List[Path]: - """ Add the `n` latest log files for ``scenario_name`` in ``scenario_dir`` to a :cls:``set`` - and return it. - """ - scenario_logs = [ - path for path in scenario_log_dir.iterdir() if (path.is_file() and "-run_" in path.name) - ] - history = sorted(scenario_logs, key=lambda x: x.stat().st_mtime, reverse=True) - - # Can't pack more than the number of available logs. - num_of_packable_iterations = n or len(scenario_logs) - - if not history: - raise RuntimeError(f"No Scenario logs found in {scenario_log_dir}") - - if num_of_packable_iterations < n: - # We ran out of scenario logs to add before reaching the requested number of n latest logs. - print( - f"Only packing {num_of_packable_iterations} logs of requested latest {n} " - f"- no more logs found for {scenario_name}!" - ) - - return history[:num_of_packable_iterations] - - def construct_rc_message(base_dir, packed_log, log_fpath) -> str: """Check the result of the log file at the given `log_fpath`.""" result = None diff --git a/scenario_player/utils/legacy.py b/scenario_player/utils/legacy.py index bc6f1cd59..34d85ac92 100644 --- a/scenario_player/utils/legacy.py +++ b/scenario_player/utils/legacy.py @@ -93,6 +93,27 @@ def convert(self, value, param, ctx): # pylint: disable=unused-argument return name, rpc_url +class MutuallyExclusiveOption(click.Option): + def __init__(self, *args, **kwargs): + self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", [])) + help = kwargs.get("help", "") + if self.mutually_exclusive: + ex_str = ", ".join(self.mutually_exclusive) + kwargs["help"] = help + ( + " NOTE: This argument is mutually exclusive with " " arguments: [" + ex_str + "]." + ) + super(MutuallyExclusiveOption, self).__init__(*args, **kwargs) + + def handle_parse_result(self, ctx, opts, args): + if self.mutually_exclusive.intersection(opts) and self.name in opts: + raise click.UsageError( + f"Illegal usage: {self.name} is mutually exclusive with " + f"arguments {', '.join(self.mutually_exclusive)}." + ) + + return super(MutuallyExclusiveOption, self).handle_parse_result(ctx, opts, args) + + class HTTPExecutor(mirakuru.HTTPExecutor): def start(self, stdout=subprocess.PIPE, stderr=subprocess.PIPE): """ Merged copy paste from the inheritance chain with modified stdout/err behaviour """ diff --git a/scenario_player/utils/logs.py b/scenario_player/utils/logs.py new file mode 100644 index 000000000..efcb20f2a --- /dev/null +++ b/scenario_player/utils/logs.py @@ -0,0 +1,70 @@ +from pathlib import Path +from typing import List + + +def pack_n_latest_node_logs_in_dir(scenario_dir: Path, n: int) -> List[Path]: + """Return the node log folder paths for the `n` last runs.""" + if n == 0: + return [] + # Get the number of runs that have been conducted + run_num_file = scenario_dir.joinpath("run_num.txt") + latest_run = 0 + if run_num_file.exists(): + latest_run = int(run_num_file.read_text()) + + # Run count starts at 0 + num_of_runs = latest_run + 1 + + # Avoid negative indices. + earliest_run_to_pack = max(num_of_runs - n, 0) + + folders = [] + for run_num in range(earliest_run_to_pack, num_of_runs): + for path in scenario_dir.iterdir(): + if not path.is_dir() or not path.name.startswith(f"node_{run_num}_"): + continue + folders.append(path) + + return folders + + +def pack_n_latest_logs_for_scenario_in_dir(scenario_name, scenario_log_dir: Path, n) -> List[Path]: + """ List the `n` newest scenario log files in the given `scenario_log_dir`.""" + if n == 0: + return [] + # Get all scenario run logs, sort and reverse them (newest first) + scenario_logs = [ + path for path in scenario_log_dir.iterdir() if (path.is_file() and "-run_" in path.name) + ] + history = sorted(scenario_logs, reverse=True) + + # Can't pack more than the number of available logs. + num_of_packable_iterations = min(n, len(scenario_logs)) + print(scenario_logs) + print(n, len(scenario_logs), num_of_packable_iterations) + + if not history: + raise RuntimeError(f"No Scenario logs found in {scenario_log_dir}") + + if num_of_packable_iterations < n: + # We ran out of scenario logs to add before reaching the requested number of n latest logs. + print( + f"Only packing {num_of_packable_iterations} logs of requested latest {n} " + f"- no more logs found for {scenario_name}!" + ) + + return history[:num_of_packable_iterations] + + +def verify_scenario_log_dir(scenario_name, data_path: Path): + # The logs are located at .raiden/scenario-player/scenarios/ + # - make sure the path exists. + scenarios_dir = data_path.joinpath("scenarios") + scenario_log_dir = scenarios_dir.joinpath(scenario_name) + if not scenario_log_dir.exists(): + raise FileNotFoundError( + f"No log directory found for scenario {scenario_name} at {scenario_log_dir}" + ) + if not scenario_log_dir.is_dir(): + raise NotADirectoryError(f"Scenario Log path {scenario_log_dir} is not a directory!") + return scenarios_dir, scenario_log_dir diff --git a/tests/unittests/cli/keystore/UTC--1 b/tests/unittests/cli/keystore/UTC--1 new file mode 100644 index 000000000..13ce5147a --- /dev/null +++ b/tests/unittests/cli/keystore/UTC--1 @@ -0,0 +1 @@ +{"id":"e66b03f8-4ce7-8f9b-b5d1-36dc9d5154ba","version":3,"crypto":{"cipher":"aes-128-ctr","cipherparams":{"iv":"49b8891b02604fb05884280509b82fbb"},"ciphertext":"4ce3a7059820b516746d7cc5defca17c2a54ea9dbb7e3a5411a01630b5021244","kdf":"pbkdf2","kdfparams":{"c":10240,"dklen":32,"prf":"hmac-sha256","salt":"9ef5e56c835cf59f4c9dbe6bd6a88b11056d6f01c0590c17d1cbb1653f749e7a"},"mac":"861354d5dd3efb2c6470090fb901957c53035175ababc9dc1863f5c07572d33b"},"address":"ec1fdb2d29c5689416b3f1b55a4d879fddf0e6e3","name":"","meta":"{}"} \ No newline at end of file diff --git a/tests/unittests/cli/keystore/password b/tests/unittests/cli/keystore/password new file mode 100644 index 000000000..190a18037 --- /dev/null +++ b/tests/unittests/cli/keystore/password @@ -0,0 +1 @@ +123 diff --git a/tests/unittests/cli/keystore/wrong_password b/tests/unittests/cli/keystore/wrong_password new file mode 100644 index 000000000..9f358a4ad --- /dev/null +++ b/tests/unittests/cli/keystore/wrong_password @@ -0,0 +1 @@ +123456 diff --git a/tests/unittests/cli/scenario/join-network-scenario-J1.yaml b/tests/unittests/cli/scenario/join-network-scenario-J1.yaml new file mode 100644 index 000000000..d622fd41e --- /dev/null +++ b/tests/unittests/cli/scenario/join-network-scenario-J1.yaml @@ -0,0 +1,48 @@ +version: 2 + +settings: + gas_price: "fast" + chain: any + services: + pfs: + url: https://pfs-goerli.services-dev.raiden.network + udc: + enable: true + token: + deposit: true + +token: + +nodes: + mode: managed + count: 4 + + default_options: + gas-price: fast + environment-type: development + routing-mode: pfs + pathfinding-max-paths: 5 + pathfinding-max-fee: 10 + +scenario: + serial: + tasks: + - parallel: + name: "Setting up a network" + tasks: + - open_channel: {from: 0, to: 1, total_deposit: 10, expected_http_status: 201} + - open_channel: {from: 0, to: 2, total_deposit: 10, expected_http_status: 201} + - open_channel: {from: 1, to: 2, total_deposit: 10, expected_http_status: 201} + - serial: + name: "Checking the network" + tasks: + - assert: {from: 0, to: 1, total_deposit: 10, balance: 10, state: "opened"} + - assert: {from: 0, to: 2, total_deposit: 10, balance: 10, state: "opened"} + - assert: {from: 1, to: 2, total_deposit: 10, balance: 10, state: "opened"} + - serial: + name: "Node Nr. 4 joins" + tasks: + - join_network: {from: 3, funds: 100, initial_channel_target: 3, joinable_funds_target: 0.4, expected_http_status: 204} + - assert: {from: 3, to: 0, total_deposit: 20, balance: 20, state: "opened"} + - assert: {from: 3, to: 1, total_deposit: 20, balance: 20, state: "opened"} + - assert: {from: 3, to: 2, total_deposit: 20, balance: 20, state: "opened"} \ No newline at end of file diff --git a/tests/unittests/cli/test_cli.py b/tests/unittests/cli/test_cli.py new file mode 100644 index 000000000..15425da4c --- /dev/null +++ b/tests/unittests/cli/test_cli.py @@ -0,0 +1,88 @@ +from pathlib import Path +from unittest.mock import patch + +import pytest +from click.testing import CliRunner + +from scenario_player import main +from scenario_player.exceptions.cli import WrongPassword + +KEYSTORE_PATH = str(Path(__file__).resolve().parents[0].joinpath("keystore")) +SCENARIO = f"{Path(__file__).parent.joinpath('scenario', 'join-network-scenario-J1.yaml')}" +CLI_ARGS = f"--chain goerli:http://geth.goerli.ethnodes.brainbot.com:8545 run " \ + f"--keystore-file " + KEYSTORE_PATH + "/UTC--1 " \ + f"--no-ui " \ + f"{{pw_option}} " \ + f"{SCENARIO}" + + +@pytest.fixture(scope="module") +def runner(): + return CliRunner() + + +class Sentinel(Exception): + pass + + +class TestPasswordHandling: + # use a fixture instead of patch directly, + # to avoid having to pass an extra argument to all methods. + @pytest.fixture(autouse=True) + def patch_collect_tasks_on_setup(self): + with patch("scenario_player.main.collect_tasks", side_effect=Sentinel): + # Yield instead of return, + # as that allows the patching to be undone after the test is complete. + yield + + def test_password_file_not_existent(self, runner): + """A not existing password file should raise error""" + result = runner.invoke( + main.main, + CLI_ARGS.format(pw_option=f"--password-file /does/not/exist").split(" ") + ) + assert result.exit_code == 2 + assert '"--password-file": File "/does/not/exist" does not exist.' in result.output + + def test_mutually_exclusive(self, runner): + result = runner.invoke( + main.main, + CLI_ARGS.format( + pw_option= + f"--password-file {KEYSTORE_PATH}" + "/password " + "--password 123").split(" ") + ) + assert result.exit_code == 2 + assert 'Error: Illegal usage: password_file is mutually exclusive' in result.output + + @pytest.mark.parametrize( + "password_file, expected_exec", + argvalues=[("/wrong_password", WrongPassword), ("/password", Sentinel)], + ids=["wrong password", "correct password"], + ) + def test_password_file(self, password_file, expected_exec, runner): + result = runner.invoke(main.main, CLI_ARGS.format( + pw_option=f"--password-file {KEYSTORE_PATH + password_file}")) + assert result.exc_info[0] == expected_exec + assert result.exit_code == 1 + + @pytest.mark.parametrize( + "password, expected_exc", + argvalues=[("wrong_password", WrongPassword), ("123", Sentinel)], + ids=["wrong password", "correct password"], + ) + def test_password(self, password, expected_exc, runner): + result = runner.invoke(main.main, + CLI_ARGS.format(pw_option=f"--password {password}").split(" ")) + assert result.exc_info[0] == expected_exc + assert result.exit_code == 1 + + @pytest.mark.parametrize( + "user_input, expected_exc", + argvalues=[("wrongpassword", WrongPassword), ("123", Sentinel)], + ids=["wrong password", "correct password"], + ) + def test_manual_password_validation(self, user_input, expected_exc, runner): + result = runner.invoke(main.main, + CLI_ARGS.format(pw_option=f"--password {user_input}").split(" ")) + assert result.exc_info[0] == expected_exc + assert result.exit_code == 1 diff --git a/tests/unittests/utils/test_logs.py b/tests/unittests/utils/test_logs.py new file mode 100644 index 000000000..19a2a2b43 --- /dev/null +++ b/tests/unittests/utils/test_logs.py @@ -0,0 +1,101 @@ +import pytest + +from scenario_player.utils.logs import ( + pack_n_latest_logs_for_scenario_in_dir, + pack_n_latest_node_logs_in_dir, + verify_scenario_log_dir, +) + + + +@pytest.fixture +def scenario_dir(tmp_path): + scenario_dir = tmp_path.joinpath("test_scenario") + scenario_dir.mkdir() + return scenario_dir + + +@pytest.fixture(autouse=True) +def faked_scenario_dir(scenario_dir): + + # Create 10 fake scenario run logs, and 3 fake node folders per run + for n in range(10): + scenario_dir.joinpath(f"{scenario_dir.stem}-run_2000-08-{n}.log").touch() + scenario_dir.joinpath(f"node_{n}_001").mkdir() + scenario_dir.joinpath(f"node_{n}_002").mkdir() + scenario_dir.joinpath(f"node_{n}_003").mkdir() + + # Create the run_num text file. + run_num_file = scenario_dir.joinpath("run_num.txt") + run_num_file.touch() + run_num_file.write_text("9") + + +class TestPackNLatestNodeLogsInDir: + @pytest.mark.parametrize( + "given, expected", + argvalues=[(0, 0), (100, 30), (5, 15), (10, 30)], + ids=[ + "n=0 returns empty list", + "n>current run number returns all available", + "ncurrent run number returns all available", + "n