Skip to content
Merged
105 changes: 105 additions & 0 deletions docs/source/beast_lab.rst
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "argobeast"
version = "2.1.4.post1"
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"
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
160 changes: 160 additions & 0 deletions src/argo_beast/cli/beast_lab.py
Original file line number Diff line number Diff line change
@@ -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.")
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"
)
warn(f'remote_url: "{remote_url}"\n')
return True

with open(config_path, "r", encoding="utf-8") 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"
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

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", encoding="utf-8") 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."
)
38 changes: 20 additions & 18 deletions src/argo_beast/cli/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,26 @@ 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 warn(log: str):
string = f"{YELLOW}[WARN]{RESET} {log}"
print(string)


def info(log: str):
string = f"{PURPLE}[INFO]{RESET} {log}"
print(string)


def error(log: str):
string = f"{RED}[ERROR]{RESET} {log}"
print(string)
def _beast_print(prefix, color, *args, **kwargs):
prefix_str = f"{color}[{prefix}]{RESET}"
if args:
args = list(args)
args[0] = f"{prefix_str} {args[0]}"
else:
args = [prefix_str]
print(*args, **kwargs)

def error(*args, **kwargs):
_beast_print("ERROR", RED, *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):
_beast_print("INFO", PURPLE, *args, **kwargs)


def get_class_name(name):
Expand Down
Loading
Loading