import pytest import os import json from tests.browser import create_chrome_driver def get_run_id(config, prefix=None): run_prefix = prefix or _get_run_prefix(config) run_ids = getattr(config, "_pxil_run_ids", {}) if run_prefix in run_ids: return run_ids[run_prefix] base_folder = os.path.join("reports", "screenshots") os.makedirs(base_folder, exist_ok=True) existing = [ name for name in os.listdir(base_folder) if name.startswith(f"{run_prefix}_") ] if not existing: next_number = 1 else: numbers = [ int(name.split("_")[-1]) for name in existing if name.split("_")[-1].isdigit() ] next_number = max(numbers, default=0) + 1 run_id = f"{run_prefix}_{next_number:03d}" run_ids[run_prefix] = run_id config._pxil_run_ids = run_ids return run_id def pytest_configure(config): run_id = get_run_id(config) if config.pluginmanager.hasplugin("html"): reports_dir = os.path.abspath(os.path.join("reports", "html")) os.makedirs(reports_dir, exist_ok=True) config.option.htmlpath = os.path.join(reports_dir, f"{run_id}.html") config.option.self_contained_html = True # ----------------------------- # WebDriver Fixture # ----------------------------- @pytest.fixture def driver(): driver = create_chrome_driver() driver.maximize_window() yield driver driver.quit() # ----------------------------- # Generate Incremental Test ID # ----------------------------- @pytest.fixture def test_context(request): test_id = get_run_id(request.config, _setup_item_prefix(request.node)) test_name = request.node.name folder = os.path.abspath( os.path.join("reports", "screenshots", test_id, test_name) ) log_folder = os.path.abspath( os.path.join("reports", "logs", test_id, test_name) ) os.makedirs(folder, exist_ok=True) os.makedirs(log_folder, exist_ok=True) print(f"\nTEST ID: {test_id}") return { "test_id": test_id, "folder": folder, "log_folder": log_folder, "steps": [], "extra_folders": [], } # ----------------------------- # Attach Screenshot Folder Link # ----------------------------- @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call": context = item.funcargs.get("test_context", None) if context: folder = context["folder"] steps = context.get("steps", []) if report.failed: driver = item.funcargs.get("driver", None) failure_index = len(steps) + 1 failure_name = "failure_state" failure_filename = f"{failure_index:02d}_{failure_name}.png" failure_path = os.path.join(folder, failure_filename) if driver: try: driver.save_screenshot(failure_path) steps.append( { "index": failure_index, "name": failure_name, "path": failure_path, "status": "failed", } ) except Exception: pass if item.config.pluginmanager.hasplugin("html"): from pytest_html import extras extras_list = getattr(report, "extras", []) for step in steps: extras_list.append( extras.url( _as_file_url(step["path"]), name=f"Step {step['index']:02d} [{step['status']}] {step['name']}" ) ) if steps: extras_list.append( extras.html(_build_steps_html(steps)) ) extras_list.append( extras.url(_as_file_url(folder), name="Screenshots Folder") ) log_folder = context.get("log_folder") if log_folder: extras_list.append( extras.url(_as_file_url(log_folder), name="Logs Folder") ) for extra_folder in context.get("extra_folders", []): extras_list.append( extras.url( _as_file_url(extra_folder["path"]), name=extra_folder["name"] ) ) report.extras = extras_list def _as_file_url(path): absolute_path = os.path.abspath(path).replace("\\", "/") return f"file:///{absolute_path}" def _build_steps_html(steps): rows = [] for step in steps: step_url = _as_file_url(step["path"]) rows.append( "" f"{step['index']:02d}" f"{step['status'].capitalize()}" f"{step['name']}" f"Open Screenshot" "" ) return ( "
" "

Step Details

" "" "" f"{''.join(rows)}" "
#StatusStepScreenshot
" "
" ) def _setup_item_prefix(item): """Use the collected product and flow, including when -k selects a subset.""" nodeid = item.nodeid.replace("\\", "/") if "tests/setup/" not in nodeid: return None callspec = getattr(item, "callspec", None) product_config = callspec.params.get("config") if callspec else None if not isinstance(product_config, dict): return None product = product_config["product_name"].upper() if "test_combined_setup.py" in nodeid: return f"{product}_SESSION_TOKEN" if "/sessions/" in nodeid: return f"{product}_SESSION" if "/tokens/" in nodeid: return f"{product}_TOKEN" return f"{product}_SETUP" def _get_run_prefix(config): invocation_args = " ".join(str(arg) for arg in config.invocation_params.args).replace("\\", "/").lower() if "tests/setup/sessions" in invocation_args or "test_session.py" in invocation_args: return "PXIL_SESSION" if "tests/setup/tokens" in invocation_args or "test_trading_token.py" in invocation_args: return "PXIL_TOKEN" if "tests/setup" in invocation_args or "test_combined_setup.py" in invocation_args: return "PXIL_SETUP" runtime_path = os.path.abspath(os.path.join("config", "runtime.json")) if os.path.exists(runtime_path): try: with open(runtime_path, "r", encoding="utf-8") as runtime_file: runtime_config = json.load(runtime_file) execution = runtime_config.get("execution", {}) product_family = execution.get("product_family", "regression").upper() flow_scope = execution.get("flow_scope", "run").upper() return f"{product_family}_{flow_scope}" except Exception: pass if "matching" in invocation_args: return "REGRESSION_MATCHING" if "bidding" in invocation_args: return "REGRESSION_BIDDING" if "reports" in invocation_args: return "REGRESSION_REPORTS" if "order_book" in invocation_args: return "REGRESSION_ORDER_BOOK" if "trade_book" in invocation_args: return "REGRESSION_TRADE_BOOK" return "PXIL_RUN"