From 1b6079cf536efae24ea924a9d4f6ccf4c021ceb1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:56:39 +0000 Subject: [PATCH 1/8] chore(deps): bump python-dotenv in the uv group across 1 directory Bumps the uv group with 1 update in the / directory: [python-dotenv](https://github.com/theskumar/python-dotenv). Updates `python-dotenv` from 1.2.1 to 1.2.2 - [Release notes](https://github.com/theskumar/python-dotenv/releases) - [Changelog](https://github.com/theskumar/python-dotenv/blob/main/CHANGELOG.md) - [Commits](https://github.com/theskumar/python-dotenv/compare/v1.2.1...v1.2.2) --- updated-dependencies: - dependency-name: python-dotenv dependency-version: 1.2.2 dependency-type: direct:production dependency-group: uv ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- requirements_dev.txt | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index c6a5a3f..5b1e2e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,7 +34,7 @@ Pygments==2.20.0 pyproject_hooks==1.2.0 PySocks==1.7.1 pytest==9.0.3 -python-dotenv==1.2.1 +python-dotenv==1.2.2 PyYAML==6.0.3 requests==2.33.0 roman-numerals==4.1.0 diff --git a/requirements_dev.txt b/requirements_dev.txt index 8f2c0e3..fb4f3b1 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,6 +1,6 @@ behave==1.2.6 deepmerge==2.0 -python-dotenv==1.2.1 +python-dotenv==1.2.2 PyYAML==6.0.3 pytest==9.0.3 selenium==4.40.0 diff --git a/uv.lock b/uv.lock index 7057edc..d1c67a7 100644 --- a/uv.lock +++ b/uv.lock @@ -56,7 +56,7 @@ wheels = [ [[package]] name = "argobeast" -version = "2.1.4" +version = "2.1.4.post1" source = { editable = "." } dependencies = [ { name = "behave" }, From aa9ce5d8f9bd3db13cda050e175218b8bf75faca Mon Sep 17 00:00:00 2001 From: "paul.s" Date: Fri, 24 Apr 2026 16:34:27 +0100 Subject: [PATCH 2/8] chore: created the base code for building the lab --- argobeast_lab/argobeast.dockercompose.yml | 22 ++++++++ argobeast_lab/argobeast.dockerfile | 8 +++ pyproject.toml | 2 +- src/argo_beast/cli/build_lab.py | 22 ++++++++ src/argo_beast/cli/main.py | 62 ++++++++++++++++------- src/argo_beast/cli/templates.py | 36 +++++++++++++ uv.lock | 2 +- 7 files changed, 134 insertions(+), 20 deletions(-) create mode 100644 argobeast_lab/argobeast.dockercompose.yml create mode 100644 argobeast_lab/argobeast.dockerfile create mode 100644 src/argo_beast/cli/build_lab.py diff --git a/argobeast_lab/argobeast.dockercompose.yml b/argobeast_lab/argobeast.dockercompose.yml new file mode 100644 index 0000000..44e979d --- /dev/null +++ b/argobeast_lab/argobeast.dockercompose.yml @@ -0,0 +1,22 @@ + +services: + selenium-grid: + image: selenium/standalone-chrome:latest + ports: + - "4444:4444" + - "7900:7900" # NoVNC - lets users WATCH the tests in a browser + shm_size: 2gb + + argobeast-runner: + build: . + volumes: + - .:/app + working_dir: /app + environment: + - SE_REMOTE_URL=http://selenium-grid:4444/wd/hub + - ARGO_ENV=container + - IS_IN_LAB=True + depends_on: + - selenium-grid + tty: true + stdin_open: true diff --git a/argobeast_lab/argobeast.dockerfile b/argobeast_lab/argobeast.dockerfile new file mode 100644 index 0000000..d47dc7c --- /dev/null +++ b/argobeast_lab/argobeast.dockerfile @@ -0,0 +1,8 @@ + +FROM python:3.11-slim +WORKDIR /app +COPY requirements.txt* . +RUN pip install --no-cache-dir argobeast && if [ -f requirements.txt ]; then pip install --no-cache-dir -r requirements.txt; fi +RUN useradd -m argouser +USER argouser +ENV PS1="[argobeast lab]: " IS_IN_LAB=True diff --git a/pyproject.toml b/pyproject.toml index d062fff..3e9d06a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "argobeast" -version = "2.1.4.post1" +version = "2.1.5" description = "A Python-based test automation framework for Behave with a clean Page Object architecture and CLI scaffolding." readme = "README.md" requires-python = ">=3.10" diff --git a/src/argo_beast/cli/build_lab.py b/src/argo_beast/cli/build_lab.py new file mode 100644 index 0000000..c16e635 --- /dev/null +++ b/src/argo_beast/cli/build_lab.py @@ -0,0 +1,22 @@ +import os +from .helpers import ensure_dir, ok, warn +from .templates import DOCKERFILE_TEMPLATE, DOCKER_COMPOSE_TEMPLATE + + +def build_lab(): + ensure_dir("argobeast_lab") + if os.path.exists("argobeast_lab/argobeast.dockerfile"): + warn("The ArgoBEAST lab already exists") + else: + ok("Adding furniture and equipment...") + with open("argobeast_lab/argobeast.dockerfile", "w", encoding="utf-8") as f: + f.write(DOCKERFILE_TEMPLATE) + if os.path.exists("argobeast_lab/argobeast.dockercompose.yml"): + warn("The ArgoBEAST lab already exists") + else: + warn("Somebody dropped a test tube and it broke!") + ok("replacing equipment...") + with open("argobeast_lab/argobeast.dockercompose.yml", "w", encoding="utf-8") as f: + f.write(DOCKER_COMPOSE_TEMPLATE) + ok("cleaning up the mess...") + ok("The ArgoBEAST lab is ready to use! run `argobeast open lab` to get started") diff --git a/src/argo_beast/cli/main.py b/src/argo_beast/cli/main.py index 0754272..e94ea70 100644 --- a/src/argo_beast/cli/main.py +++ b/src/argo_beast/cli/main.py @@ -3,6 +3,7 @@ from .create import create, create_all, init from .helpers import warn, info, ARGO_BEAST from .generate_feature_docs import generate_rst_documentation +from .build_lab import build_lab def fix_windows_encoding(): @@ -16,6 +17,43 @@ def fix_windows_encoding(): sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8") +def _less_than_three_args(args): + if args[0] == "init": + print(ARGO_BEAST) + init() + elif args[0] == "help": + print( + "Usage: argobeast \n\n" + "Commands:\n" + " create - Create a new page, actions, feature, or steps file.\n" + " init - Initialize a new ArgoBEAST project in the current directory.\n" + " hello - Display a welcome message and introduction to ArgoBEAST.\n" + " generate-docs - Generate documentation for all features in the project.\n\n" + " build lab - Build the ArgoBEAST lab Docker image.\n\n" + "Types (for create command):\n" + " page - Create a new Page Object class.\n" + " actions - Create a new Actions class.\n" + " feature - Create a new feature file with scenarios.\n" + " steps - Create a new steps definition file.\n" + " all - Create all of the above with the given name." + ) + elif args[0] == "hello": + print(ARGO_BEAST) + print( + " ### Welcome to ArgoBEAST! ###" + "\n___________________________________________________________________" + "\nA Python-based test automation framework for web applications, " + "\nbuilt on Behave, Selenium, and a clean Page Object Model. " + "\nGet started by running 'argobeast init' to set up your first project." + ) + elif args[0] == "generate-docs": + generate_rst_documentation(args) + else: + info( + "Usage: argobeast create \n To initiate a new project try argobeast init" + ) + + def main(): """ Main CLI entry point @@ -29,25 +67,13 @@ def main(): "Usage: argobeast create \n To initiate a new project try argobeast init" ) return + + if args[0] == "build" and len(args) > 1 and args[1] == "lab": + build_lab() + return + if len(args) < 3: - if args[0] == "init": - print(ARGO_BEAST) - init() - elif args[0] == "hello": - print(ARGO_BEAST) - print( - " ### Welcome to ArgoBEAST! ###" - "\n___________________________________________________________________" - "\nA Python-based test automation framework for web applications, " - "\nbuilt on Behave, Selenium, and a clean Page Object Model. " - "\nGet started by running 'argobeast init' to set up your first project." - ) - elif args[0] == "generate-docs": - generate_rst_documentation(args) - else: - info( - "Usage: argobeast create \n To initiate a new project try argobeast init" - ) + _less_than_three_args(args) return if len(args) > 3: diff --git a/src/argo_beast/cli/templates.py b/src/argo_beast/cli/templates.py index bd09252..3de55bd 100644 --- a/src/argo_beast/cli/templates.py +++ b/src/argo_beast/cli/templates.py @@ -137,3 +137,39 @@ def step_assert_result(context): When I click the "Continue as Guest" link Then I should be on the home page """ + +DOCKERFILE_TEMPLATE = """ +FROM python:3.11-slim +WORKDIR /app +COPY requirements.txt* . +RUN pip install --no-cache-dir argobeast \ + && if [ -f requirements.txt ]; then pip install --no-cache-dir -r requirements.txt; fi +RUN useradd -m argouser +USER argouser +ENV PS1="[argobeast lab]: " \ + IS_IN_LAB=True +""" + +DOCKER_COMPOSE_TEMPLATE = """ +services: + selenium-grid: + image: selenium/standalone-chrome:latest + ports: + - "4444:4444" + - "7900:7900" # NoVNC - lets users WATCH the tests in a browser + shm_size: 2gb + + argobeast-runner: + build: . + volumes: + - .:/app + working_dir: /app + environment: + - SE_REMOTE_URL=http://selenium-grid:4444/wd/hub + - ARGO_ENV=container + - IS_IN_LAB=True + depends_on: + - selenium-grid + tty: true + stdin_open: true +""" \ No newline at end of file diff --git a/uv.lock b/uv.lock index d1c67a7..a9a3b0e 100644 --- a/uv.lock +++ b/uv.lock @@ -56,7 +56,7 @@ wheels = [ [[package]] name = "argobeast" -version = "2.1.4.post1" +version = "2.1.5" source = { editable = "." } dependencies = [ { name = "behave" }, From 17aee5b4e0521b9b7ba09bcb6cbc15f5f397e3a7 Mon Sep 17 00:00:00 2001 From: "paul.s" Date: Mon, 27 Apr 2026 11:55:30 +0100 Subject: [PATCH 3/8] chore: added all beast lab commands --- argobeast_lab/argobeast.dockercompose.yml | 22 --- argobeast_lab/argobeast.dockerfile | 8 -- src/argo_beast/cli/beast_lab.py | 160 ++++++++++++++++++++++ src/argo_beast/cli/build_lab.py | 22 --- src/argo_beast/cli/helpers.py | 8 +- src/argo_beast/cli/main.py | 18 ++- src/argo_beast/cli/templates.py | 11 +- 7 files changed, 187 insertions(+), 62 deletions(-) delete mode 100644 argobeast_lab/argobeast.dockercompose.yml delete mode 100644 argobeast_lab/argobeast.dockerfile create mode 100644 src/argo_beast/cli/beast_lab.py delete mode 100644 src/argo_beast/cli/build_lab.py diff --git a/argobeast_lab/argobeast.dockercompose.yml b/argobeast_lab/argobeast.dockercompose.yml deleted file mode 100644 index 44e979d..0000000 --- a/argobeast_lab/argobeast.dockercompose.yml +++ /dev/null @@ -1,22 +0,0 @@ - -services: - selenium-grid: - image: selenium/standalone-chrome:latest - ports: - - "4444:4444" - - "7900:7900" # NoVNC - lets users WATCH the tests in a browser - shm_size: 2gb - - argobeast-runner: - build: . - volumes: - - .:/app - working_dir: /app - environment: - - SE_REMOTE_URL=http://selenium-grid:4444/wd/hub - - ARGO_ENV=container - - IS_IN_LAB=True - depends_on: - - selenium-grid - tty: true - stdin_open: true diff --git a/argobeast_lab/argobeast.dockerfile b/argobeast_lab/argobeast.dockerfile deleted file mode 100644 index d47dc7c..0000000 --- a/argobeast_lab/argobeast.dockerfile +++ /dev/null @@ -1,8 +0,0 @@ - -FROM python:3.11-slim -WORKDIR /app -COPY requirements.txt* . -RUN pip install --no-cache-dir argobeast && if [ -f requirements.txt ]; then pip install --no-cache-dir -r requirements.txt; fi -RUN useradd -m argouser -USER argouser -ENV PS1="[argobeast lab]: " IS_IN_LAB=True diff --git a/src/argo_beast/cli/beast_lab.py b/src/argo_beast/cli/beast_lab.py new file mode 100644 index 0000000..6633ef3 --- /dev/null +++ b/src/argo_beast/cli/beast_lab.py @@ -0,0 +1,160 @@ +import os +import subprocess +from .helpers import ensure_dir, ok, warn, error +from .templates import DOCKERFILE_TEMPLATE, DOCKER_COMPOSE_TEMPLATE + + +def _update_driver_config(): + config_path = "config/driver.yml" + remote_url = "http://selenium-grid:4444/wd/hub" + + if not os.path.exists(config_path): + warn("No driver.yml found, skipping lab configuration.") + ok( + "if you are using a custom configuration, you can add the following to your driver.yml to connect to the lab Grid:\n" + ) + ok(f'remote_url: "{remote_url}"\n') + return False + + with open(config_path, "r") as f: + content = f.readlines() + + # Check if we've already added the lab config + if any("remote_url:" in line for line in content): + if any(line.strip().startswith("#") and remote_url in line for line in content): + ok( + "The lab configuration is already present but commented out in driver.yml." + ) + ok("Uncomment the remote_url line to connect to the lab Grid.") + elif any(f'remote_url: "{remote_url}"' in line for line in content): + ok("The lab configuration looks ok in driver.yml.") + return True + else: + warn( + "remote_url configuration already exists in driver.yml but it does not match the lab Grid URL.\n" + "To use the lab, please update the remote_url in your driver.yml to point to {}. \n" + "or remove the existing remote_url configuration to allow argobeast to add the correct one automatically.".format( + remote_url + ) + ) + return False + + ok("Wiring up the driver.yml to the lab Grid...") + + # We append it to the end or modify the specific key + # Adding it as a commented-out toggle is often the friendliest way + with open(config_path, "a") as f: + f.write("\n# Added by argobeast build lab\n") + f.write(f'remote_url: "{remote_url}"\n') + return True + + +def build_lab(): + + if os.environ.get("IS_IN_LAB"): + warn("You are already in the lab! No need to build it again.!") + return + + ensure_dir("argobeast_lab") + if os.path.exists("argobeast_lab/argobeast.dockerfile"): + warn("The ArgoBEAST lab already exists") + else: + ok("Adding furniture and equipment...") + with open("argobeast_lab/argobeast.dockerfile", "w", encoding="utf-8") as f: + f.write(DOCKERFILE_TEMPLATE) + if os.path.exists("argobeast_lab/argobeast.dockercompose.yml"): + warn("The ArgoBEAST lab already exists") + else: + warn("Somebody dropped a test tube and it broke!") + ok("replacing equipment...") + with open( + "argobeast_lab/argobeast.dockercompose.yml", "w", encoding="utf-8" + ) as f: + f.write(DOCKER_COMPOSE_TEMPLATE) + ok("cleaning up the mess...") + ok("The ArgoBEAST lab is ready to use! run `argobeast open lab` to get started") + + +def open_lab(): + + if os.environ.get("IS_IN_LAB"): + warn("You are already in the lab!") + return + if not os.path.exists("argobeast_lab/argobeast.dockerfile") or not os.path.exists( + "argobeast_lab/argobeast.dockercompose.yml" + ): + warn( + "The lab is either not built or there are missing files. " + "Please run `argobeast build lab` first." + ) + return + + if not _update_driver_config(): + return + + ok("Setting things up and opening the lab door...") + cmd = [ + "docker", + "compose", + "-f", + "argobeast_lab/argobeast.dockercompose.yml", + "up", + "-d", + ] + cmd_enter = ["docker", "exec", "-it", "argobeast-runner", "/bin/bash"] + try: + subprocess.run(cmd, capture_output=True, text=True, check=True) + ok("The door to the lab swings open...") + try: + subprocess.run(cmd_enter, check=True) + except subprocess.CalledProcessError as e: + error("Error entering the lab: " + str(e.stderr)) + warn( + "The door is open but something went wrong when you tried to enter. " + "Please ensure the container is running and try again." + ) + except subprocess.CalledProcessError as e: + error_msg = e.stderr.lower() + + if "permission denied" in error_msg: + warn("[SYSTEM] Permission Denied: Cannot connect to the Docker daemon.") + ok( + "To fix this, please run the following commands and restart your terminal:" + ) + print("\n sudo usermod -aG docker $USER") + print(" newgrp docker\n") + else: + error("Error details: " + str(e.stderr)) + warn( + "The door is jammed shut, please ensure you have Docker installed and running and then try again." + ) + warn( + "If you would prefer to use a different container or VM solution, " + "you can use the provided Dockerfile to build your own image and set up the lab environment manually." + ) + return + + +def close_lab(): + if os.environ.get("IS_IN_LAB"): + ok( + "You must leave the lab before you can close it. " + "Type `exit` to leave and then run this command again." + ) + return + cmd = [ + "docker", + "compose", + "-f", + "argobeast_lab/argobeast.dockercompose.yml", + "down", + ] + try: + ok("Shutting down the lab and cleaning up...") + subprocess.run(cmd, capture_output=True, text=True, check=True) + ok("The lab is now closed. See you next time!") + except subprocess.CalledProcessError as e: + error("Error closing the lab: " + str(e.stderr)) + warn( + "Something went wrong while trying to close the lab. Please ensure Docker is running and try again." + ) diff --git a/src/argo_beast/cli/build_lab.py b/src/argo_beast/cli/build_lab.py deleted file mode 100644 index c16e635..0000000 --- a/src/argo_beast/cli/build_lab.py +++ /dev/null @@ -1,22 +0,0 @@ -import os -from .helpers import ensure_dir, ok, warn -from .templates import DOCKERFILE_TEMPLATE, DOCKER_COMPOSE_TEMPLATE - - -def build_lab(): - ensure_dir("argobeast_lab") - if os.path.exists("argobeast_lab/argobeast.dockerfile"): - warn("The ArgoBEAST lab already exists") - else: - ok("Adding furniture and equipment...") - with open("argobeast_lab/argobeast.dockerfile", "w", encoding="utf-8") as f: - f.write(DOCKERFILE_TEMPLATE) - if os.path.exists("argobeast_lab/argobeast.dockercompose.yml"): - warn("The ArgoBEAST lab already exists") - else: - warn("Somebody dropped a test tube and it broke!") - ok("replacing equipment...") - with open("argobeast_lab/argobeast.dockercompose.yml", "w", encoding="utf-8") as f: - f.write(DOCKER_COMPOSE_TEMPLATE) - ok("cleaning up the mess...") - ok("The ArgoBEAST lab is ready to use! run `argobeast open lab` to get started") diff --git a/src/argo_beast/cli/helpers.py b/src/argo_beast/cli/helpers.py index 5052218..5383228 100644 --- a/src/argo_beast/cli/helpers.py +++ b/src/argo_beast/cli/helpers.py @@ -29,14 +29,14 @@ def ok(log: str): print(string) -def warn(log: str): +def warn(log: str, *kwargs): string = f"{YELLOW}[WARN]{RESET} {log}" - print(string) + print(string, *kwargs) -def info(log: str): +def info(log: str, *kwargs): string = f"{PURPLE}[INFO]{RESET} {log}" - print(string) + print(string, kwargs) def error(log: str): diff --git a/src/argo_beast/cli/main.py b/src/argo_beast/cli/main.py index e94ea70..1f04371 100644 --- a/src/argo_beast/cli/main.py +++ b/src/argo_beast/cli/main.py @@ -3,7 +3,7 @@ from .create import create, create_all, init from .helpers import warn, info, ARGO_BEAST from .generate_feature_docs import generate_rst_documentation -from .build_lab import build_lab +from .beast_lab import build_lab, open_lab, close_lab def fix_windows_encoding(): @@ -68,8 +68,20 @@ def main(): ) return - if args[0] == "build" and len(args) > 1 and args[1] == "lab": - build_lab() + if len(args) > 1 and args[1] == "lab": + if args[0] == "build": + build_lab() + elif args[0] == "open": + open_lab() + elif args[0] in ["close","stop"]: + close_lab() + else: + info( + "Usage: argobeast lab\n" + " build lab - Build the ArgoBEAST lab Docker image.\n" + " open lab - Open the ArgoBEAST lab environment (requires Docker).\n" + " close lab - Close the ArgoBEAST lab environment." + ) return if len(args) < 3: diff --git a/src/argo_beast/cli/templates.py b/src/argo_beast/cli/templates.py index 3de55bd..de3fba8 100644 --- a/src/argo_beast/cli/templates.py +++ b/src/argo_beast/cli/templates.py @@ -145,6 +145,7 @@ def step_assert_result(context): RUN pip install --no-cache-dir argobeast \ && if [ -f requirements.txt ]; then pip install --no-cache-dir -r requirements.txt; fi RUN useradd -m argouser +RUN echo "export PS1='[argobeast lab]: \\w \\$ '" >> /home/argouser/.bashrc USER argouser ENV PS1="[argobeast lab]: " \ IS_IN_LAB=True @@ -159,8 +160,12 @@ def step_assert_result(context): - "7900:7900" # NoVNC - lets users WATCH the tests in a browser shm_size: 2gb - argobeast-runner: - build: . + argobeast_runner: + image: argobeast_runner + container_name: argobeast-runner + build: + context: . + dockerfile: argobeast.dockerfile volumes: - .:/app working_dir: /app @@ -172,4 +177,4 @@ def step_assert_result(context): - selenium-grid tty: true stdin_open: true -""" \ No newline at end of file +""" From 0df5f4c050e48641613dc5a46506cc29618021bc Mon Sep 17 00:00:00 2001 From: "paul.s" Date: Mon, 27 Apr 2026 12:13:18 +0100 Subject: [PATCH 4/8] chore: added pytests to beast lab --- src/argo_beast/cli/beast_lab.py | 16 ++--- tests/unit/test_beast_lab.py | 104 ++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 tests/unit/test_beast_lab.py diff --git a/src/argo_beast/cli/beast_lab.py b/src/argo_beast/cli/beast_lab.py index 6633ef3..8e31021 100644 --- a/src/argo_beast/cli/beast_lab.py +++ b/src/argo_beast/cli/beast_lab.py @@ -11,12 +11,13 @@ def _update_driver_config(): if not os.path.exists(config_path): warn("No driver.yml found, skipping lab configuration.") ok( - "if you are using a custom configuration, you can add the following to your driver.yml to connect to the lab Grid:\n" + "if you are using a custom configuration, you can add the following to your driver.yml \n" + "to connect to the lab Grid:\n" ) ok(f'remote_url: "{remote_url}"\n') return False - with open(config_path, "r") as f: + with open(config_path, "r", encoding="utf-8") as f: content = f.readlines() # Check if we've already added the lab config @@ -32,10 +33,9 @@ def _update_driver_config(): else: warn( "remote_url configuration already exists in driver.yml but it does not match the lab Grid URL.\n" - "To use the lab, please update the remote_url in your driver.yml to point to {}. \n" - "or remove the existing remote_url configuration to allow argobeast to add the correct one automatically.".format( - remote_url - ) + f"To use the lab, please update the remote_url in your driver.yml to point to {remote_url}. \n" + "or remove the existing remote_url configuration to allow argobeast to add the correct " + "one automatically." ) return False @@ -43,7 +43,7 @@ def _update_driver_config(): # We append it to the end or modify the specific key # Adding it as a commented-out toggle is often the friendliest way - with open(config_path, "a") as f: + with open(config_path, "a", encoding="utf-8") as f: f.write("\n# Added by argobeast build lab\n") f.write(f'remote_url: "{remote_url}"\n') return True @@ -91,7 +91,7 @@ def open_lab(): if not _update_driver_config(): return - + ok("Setting things up and opening the lab door...") cmd = [ "docker", diff --git a/tests/unit/test_beast_lab.py b/tests/unit/test_beast_lab.py new file mode 100644 index 0000000..55d1d3b --- /dev/null +++ b/tests/unit/test_beast_lab.py @@ -0,0 +1,104 @@ +from unittest.mock import patch, mock_open, MagicMock +import subprocess +import os +from argo_beast.cli.beast_lab import ( + build_lab, + open_lab, + close_lab, + _update_driver_config, +) +from argo_beast.cli.templates import DOCKERFILE_TEMPLATE + + +## 1. Test Building the Lab +@patch("os.path.exists") +@patch("argo_beast.cli.beast_lab.ensure_dir") +@patch("builtins.open", new_callable=mock_open) +def test_build_lab_creates_files(mock_file, mock_ensure_dir, mock_exists): + # Setup: Files don't exist yet + mock_exists.return_value = False + + build_lab() + + # Verify directory was checked/created + mock_ensure_dir.assert_called_with("argobeast_lab") + + # Verify Dockerfile and Compose were written + # 2 files + potentially directory creation check + assert mock_file.call_count >= 2 + mock_file().write.assert_any_call(DOCKERFILE_TEMPLATE) + + +## 2. Test Opening the Lab (Success Path) +@patch("os.path.exists") +@patch("subprocess.run") +@patch("argo_beast.cli.beast_lab._update_driver_config") +def test_open_lab_success(mock_update_cfg, mock_run, mock_exists): + mock_exists.return_value = True + mock_update_cfg.return_value = True + + # Simulate Docker Compose Up + mock_run.return_value = MagicMock(returncode=0) + + open_lab() + + # Should run 'docker compose up' AND 'docker exec' + assert mock_run.call_count == 2 + assert mock_run.call_args_list[0][0][0][1] == "compose" + assert mock_run.call_args_list[1][0][0][1] == "exec" + + +## 3. Test Safety Guard (Already in Lab) +def test_open_lab_aborts_if_already_inside(monkeypatch): + monkeypatch.setenv("IS_IN_LAB", "True") + + with patch("argo_beast.cli.beast_lab.warn") as mock_warn: + open_lab() + mock_warn.assert_called_with("You are already in the lab!") + + +## 4. Test Docker Permission Denied Handling +@patch("os.path.exists") +@patch("subprocess.run") +@patch("argo_beast.cli.beast_lab._update_driver_config") +def test_open_lab_permission_denied(mock_update_cfg, mock_run, mock_exists): + mock_exists.return_value = True + mock_update_cfg.return_value = True + + # Simulate the Permission Denied error from Docker + error = subprocess.CalledProcessError(1, cmd="docker compose") + error.stderr = "permission denied while trying to connect to the Docker daemon" + mock_run.side_effect = error + + with patch("argo_beast.cli.beast_lab.warn") as mock_warn: + open_lab() + # Verify the custom permission instructions were triggered + mock_warn.assert_any_call( + "[SYSTEM] Permission Denied: Cannot connect to the Docker daemon." + ) + + +## 5. Test Config Wiring (driver.yml) +@patch("os.path.exists") +@patch("builtins.open", new_callable=mock_open, read_data="browser: chrome\n") +def test_update_driver_config_appends_url(mock_file, mock_exists): + mock_exists.return_value = True + + result = _update_driver_config() + + assert result is True + # Verify it opened in append mode + mock_file.assert_called_with("config/driver.yml", "a", encoding="utf-8") + mock_file().write.assert_any_call("\n# Added by argobeast build lab\n") + mock_file().write.assert_any_call('remote_url: "http://selenium-grid:4444/wd/hub"\n') + + +## 6. Test Closing the Lab +@patch("os.path.exists") +@patch("subprocess.run") +def test_close_lab(mock_run, mock_exists): + # Ensure it doesn't think it's inside the lab + with patch.dict(os.environ, {}, clear=True): + close_lab() + # Check if 'docker compose down' was called + assert mock_run.call_args[0][0][4] == "down" From 95e7e3bacbec167f60ae6cf60d6c1ade1e866415 Mon Sep 17 00:00:00 2001 From: "paul.s" Date: Mon, 27 Apr 2026 12:26:28 +0100 Subject: [PATCH 5/8] chore: add beast lab documentation --- docs/source/beast_lab.rst | 105 ++++++++++++++++++++++++++++++++ docs/source/index.rst | 1 + pyproject.toml | 2 +- src/argo_beast/cli/beast_lab.py | 8 +-- uv.lock | 2 +- 5 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 docs/source/beast_lab.rst diff --git a/docs/source/beast_lab.rst b/docs/source/beast_lab.rst new file mode 100644 index 0000000..1e23dde --- /dev/null +++ b/docs/source/beast_lab.rst @@ -0,0 +1,105 @@ + +======================== +The ArgoBEAST Lab (BETA) +======================== + +The **ArgoBEAST Lab** is a standardised, containerised testing environment. It provides a consistent execution space that includes a **Selenium Grid** and a dedicated **ArgoBEAST Runner**. + +This is the recommended way to run tests in environments like **WSL2**, **GitPod**, or **CI/CD pipelines** where local browser management is often problematic. + +Commands Overview +================= + ++---------------------------+---------------+------------------------------------------------------------+ +| Command | Action | Description | ++===========================+===============+============================================================+ +| ``argobeast build lab`` | **Construct** | Generates the Docker infrastructure and local directory. | ++---------------------------+---------------+------------------------------------------------------------+ +| ``argobeast open lab`` | **Enter** | Boots the Selenium Grid, updates config, and enters shell. | ++---------------------------+---------------+------------------------------------------------------------+ +| ``argobeast close lab`` | **Sanitise** | Shuts down the environment and frees up system resources. | ++---------------------------+---------------+------------------------------------------------------------+ + +1. argobeast build lab +====================== + +This command prepares the physical "equipment" for your testing environment. It creates a dedicated ``argobeast_lab/`` directory in your project root. + +* **Infrastructure**: Generates a custom ``argobeast.dockerfile`` and ``argobeast.dockercompose.yml``. +* **Isolation**: Uses a **Debian-slim** base to ensure compatibility across WSL, Linux, and Mac. +* **Persistence**: The Lab is built with an ``argouser`` to prevent file permission issues on your host machine. + +.. note:: + Run this once per project, or whenever you need to reset your Lab equipment. + +2. argobeast open lab +===================== + +The primary gateway to your execution environment. When you run this command, ArgoBEAST performs a "System Pre-flight Check": + +#. **Integrity Check**: Ensures Lab files exist. +#. **Config Sync**: Automatically scans your ``config/driver.yml``. If ``remote_url`` is missing or commented out, the Lab will wire it up for you. +#. **Engine Start**: Spins up a **Standalone Chrome** container and the **ArgoBEAST Runner**. +#. **Infiltration**: Executes an interactive session, placing you at the prompt: ``[argobeast lab]: /app #`` + +**Inside the Lab:** + +Once the doors swing open, you are in a pure Python environment. Simply run: + +.. code-block:: bash + + behave + +to execute your tests. The Lab's internal network allows seamless communication with the Selenium Grid, so your tests will run as if they were on a local machine. + +You can run any argobeast command from within the lab, but remember that the Lab is designed to be a self-contained environment. If you need to make changes to your host machine's configuration or files, exit the Lab first with: + +.. code-block:: bash + + exit + +3. argobeast close lab +====================== + +When testing is complete, use this command to "turn off the lights." It performs a ``docker compose down``, ensuring no orphaned containers are eating your RAM. + +.. warning:: + For safety, you cannot close the Lab from *inside* the Lab. Type ``exit`` to return to your host machine first. + +Technical Specifications +======================== + +The Selenium Grid (VNC) +----------------------- + +The Lab includes a built-in "Observation Window." While the Lab is open, you can watch your tests execute in real-time: + +* **URL**: ``http://localhost:7900`` +* **Features**: Live browser interaction and debugging via NoVNC. + +Environment Variables +--------------------- + +The Lab injects the following into your session: + +* ``IS_IN_LAB=True``: Used by the framework to prevent recursive loops. +* ``ARGO_ENV=container``: Can be used in your code to toggle specific behaviors. +* ``SE_REMOTE_URL``: Pre-configured to point to the internal Grid service. + +Troubleshooting +=============== + +"The Lab is Locked" (Permission Denied) +--------------------------------------- + +If you encounter a Docker socket error on Linux/WSL, your user needs permission to handle the equipment. Run: + +.. code-block:: bash + + sudo usermod -aG docker $USER + newgrp docker + +"Missing Requirements" +---------------------- + +The Lab attempts to install your host's ``requirements.txt`` upon build. If you add new dependencies to your project, you must run ``argobeast build lab`` again to "restock" the Lab's libraries. diff --git a/docs/source/index.rst b/docs/source/index.rst index 5d7661b..6fb80d1 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -14,6 +14,7 @@ Welcome to ArgoBEAST's documentation! ArgoBEAST is a powerful Python-based test :caption: Contents: getting_started + beast_lab pages actions steps diff --git a/pyproject.toml b/pyproject.toml index 3e9d06a..b6a754c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "argobeast" -version = "2.1.5" +version = "2.2.4b1" description = "A Python-based test automation framework for Behave with a clean Page Object architecture and CLI scaffolding." readme = "README.md" requires-python = ">=3.10" diff --git a/src/argo_beast/cli/beast_lab.py b/src/argo_beast/cli/beast_lab.py index 8e31021..ce6f2f0 100644 --- a/src/argo_beast/cli/beast_lab.py +++ b/src/argo_beast/cli/beast_lab.py @@ -10,12 +10,12 @@ def _update_driver_config(): if not os.path.exists(config_path): warn("No driver.yml found, skipping lab configuration.") - ok( + warn( "if you are using a custom configuration, you can add the following to your driver.yml \n" "to connect to the lab Grid:\n" ) - ok(f'remote_url: "{remote_url}"\n') - return False + warn(f'remote_url: "{remote_url}"\n') + return True with open(config_path, "r", encoding="utf-8") as f: content = f.readlines() @@ -37,7 +37,7 @@ def _update_driver_config(): "or remove the existing remote_url configuration to allow argobeast to add the correct " "one automatically." ) - return False + return False ok("Wiring up the driver.yml to the lab Grid...") diff --git a/uv.lock b/uv.lock index a9a3b0e..0672efe 100644 --- a/uv.lock +++ b/uv.lock @@ -56,7 +56,7 @@ wheels = [ [[package]] name = "argobeast" -version = "2.1.5" +version = "2.2.4b1" source = { editable = "." } dependencies = [ { name = "behave" }, From 5c0679d842a160ae6c859c5a8e772509566df226 Mon Sep 17 00:00:00 2001 From: "paul.s" Date: Mon, 27 Apr 2026 12:34:29 +0100 Subject: [PATCH 6/8] chore: updated helpers to act like print() --- src/argo_beast/cli/helpers.py | 45 +++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/argo_beast/cli/helpers.py b/src/argo_beast/cli/helpers.py index 5383228..aff52dd 100644 --- a/src/argo_beast/cli/helpers.py +++ b/src/argo_beast/cli/helpers.py @@ -1,4 +1,5 @@ from pathlib import Path +import string GREEN = "\033[92m" YELLOW = "\033[93m" @@ -24,24 +25,44 @@ def ensure_dir(directory): Path(directory).mkdir(parents=True, exist_ok=True) -def ok(log: str): - string = f"{GREEN}[OK]{RESET} {log}" - print(string) +def ok(*args, **kwargs): + prefix = f"{GREEN}[OK]{RESET}" + if args: + args = list(args) + args[0] = f"{prefix} {args[0]}" + else: + args = [prefix] + print(*args, **kwargs) -def warn(log: str, *kwargs): - string = f"{YELLOW}[WARN]{RESET} {log}" - print(string, *kwargs) +def warn(*args, **kwargs): + prefix = f"{YELLOW}[WARN]{RESET}" + if args: + args = list(args) + args[0] = f"{prefix} {args[0]}" + else: + args = [prefix] + print(*args, **kwargs) -def info(log: str, *kwargs): - string = f"{PURPLE}[INFO]{RESET} {log}" - print(string, kwargs) +def info(*args, **kwargs): + prefix = f"{PURPLE}[INFO]{RESET}" + if args: + args = list(args) + args[0] = f"{prefix} {args[0]}" + else: + args = [prefix] + print(*args, **kwargs) -def error(log: str): - string = f"{RED}[ERROR]{RESET} {log}" - print(string) +def error(*args, **kwargs): + prefix = f"{RED}[ERROR]{RESET}" + if args: + args = list(args) + args[0] = f"{prefix} {args[0]}" + else: + args = [prefix] + print(*args, **kwargs) def get_class_name(name): From ff7a10dee01325221a77df18ae02d5bf513f2f01 Mon Sep 17 00:00:00 2001 From: "paul.s" Date: Mon, 27 Apr 2026 12:36:36 +0100 Subject: [PATCH 7/8] bug: unused import in helpers --- src/argo_beast/cli/helpers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/argo_beast/cli/helpers.py b/src/argo_beast/cli/helpers.py index aff52dd..8f509ab 100644 --- a/src/argo_beast/cli/helpers.py +++ b/src/argo_beast/cli/helpers.py @@ -1,5 +1,4 @@ from pathlib import Path -import string GREEN = "\033[92m" YELLOW = "\033[93m" From 894f8450e1543fa5b9a60a27f89b897417784eb8 Mon Sep 17 00:00:00 2001 From: "paul.s" Date: Mon, 27 Apr 2026 14:14:09 +0100 Subject: [PATCH 8/8] fix: refactored helpers to remove duplication --- src/argo_beast/cli/helpers.py | 40 +++++++++------------------------ src/argo_beast/cli/templates.py | 2 +- 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/src/argo_beast/cli/helpers.py b/src/argo_beast/cli/helpers.py index 8f509ab..cbc089e 100644 --- a/src/argo_beast/cli/helpers.py +++ b/src/argo_beast/cli/helpers.py @@ -24,44 +24,26 @@ def ensure_dir(directory): Path(directory).mkdir(parents=True, exist_ok=True) -def ok(*args, **kwargs): - prefix = f"{GREEN}[OK]{RESET}" +def _beast_print(prefix, color, *args, **kwargs): + prefix_str = f"{color}[{prefix}]{RESET}" if args: args = list(args) - args[0] = f"{prefix} {args[0]}" + args[0] = f"{prefix_str} {args[0]}" else: - args = [prefix] + args = [prefix_str] print(*args, **kwargs) +def error(*args, **kwargs): + _beast_print("ERROR", RED, *args, **kwargs) -def warn(*args, **kwargs): - prefix = f"{YELLOW}[WARN]{RESET}" - if args: - args = list(args) - args[0] = f"{prefix} {args[0]}" - else: - args = [prefix] - print(*args, **kwargs) +def ok(*args, **kwargs): + _beast_print("OK", GREEN, *args, **kwargs) +def warn(*args, **kwargs): + _beast_print("WARN", YELLOW, *args, **kwargs) def info(*args, **kwargs): - prefix = f"{PURPLE}[INFO]{RESET}" - if args: - args = list(args) - args[0] = f"{prefix} {args[0]}" - else: - args = [prefix] - print(*args, **kwargs) - - -def error(*args, **kwargs): - prefix = f"{RED}[ERROR]{RESET}" - if args: - args = list(args) - args[0] = f"{prefix} {args[0]}" - else: - args = [prefix] - print(*args, **kwargs) + _beast_print("INFO", PURPLE, *args, **kwargs) def get_class_name(name): diff --git a/src/argo_beast/cli/templates.py b/src/argo_beast/cli/templates.py index de3fba8..21f70fc 100644 --- a/src/argo_beast/cli/templates.py +++ b/src/argo_beast/cli/templates.py @@ -50,7 +50,7 @@ def step_example_action(context,name): # Example: actions.login("user", "pass") pass -@then("I should see an example result") +@then("I should see an expected result") def step_assert_result(context): actions = context.app.get_actions({ClassName}Actions) # Example: assert actions.page.is_visible(actions.page.SUCCESS_MESSAGE)