Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add allure reporting and update logging #291

Merged
merged 2 commits into from
Oct 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# General
.idea
test_results
allure-report
test_env
tests/data
data
Expand Down
56 changes: 17 additions & 39 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
`conftest.py` and `pylenium.json` files should stay at your Workspace Root (aka Project Root).
The `conftest.py` and `pylenium.json` files generated by Pylenium should stay at your Workspace Root (aka Project Root).

conftest.py
Although this file is editable, you should only change its contents if you know what you are doing.
Expand All @@ -25,13 +25,12 @@ def test_go_to_google(py):
import shutil
import sys
from pathlib import Path
from typing import Dict, Optional

import allure
import pytest
import requests
from faker import Faker
from reportportal_client import RPLogger, RPLogHandler
from selenium.common.exceptions import JavascriptException

from pylenium.a11y import PyleniumAxe
from pylenium.config import PyleniumConfig, TestCase
Expand Down Expand Up @@ -151,7 +150,7 @@ def _load_pylenium_json(project_root, request) -> PyleniumConfig:


@pytest.fixture(scope="session")
def _override_pylenium_config_values(_load_pylenium_json, request) -> PyleniumConfig:
def _override_pylenium_config_values(_load_pylenium_json: PyleniumConfig, request) -> PyleniumConfig:
"""Override any PyleniumConfig values after loading the initial pylenium.json config file.

After a pylenium.json config file is loaded and converted to a PyleniumConfig object,
Expand Down Expand Up @@ -201,39 +200,21 @@ def _override_pylenium_config_values(_load_pylenium_json, request) -> PyleniumCo
if cli_extensions:
config.driver.extension_paths = [ext.strip() for ext in cli_extensions.split(",")]

return config


@pytest.fixture(scope="session")
def lambdatest_config() -> Optional[Dict]:
"""Read the LambdatestConfig for the test session.
cli_log_level = request.config.getoption("--pylog_level")
if cli_log_level:
level = cli_log_level.upper()
config.logging.pylog_level = level if level in ["DEBUG", "COMMAND", "INFO", "USER", "WARNING", "ERROR", "CRITICAL"] else "INFO"

I want to dynamically set these values:
* via CLI, but this currently doesn't work with LambdaTest
* via ENV variables, but this requires more setup on my (aka the user's) side
"""
capabilities = None
if os.environ.get("LT_USERNAME") and os.environ.get("LT_ACCESS_KEY"):
capabilities = {
"build": os.environ.get("LT_BUILD_NAME"),
"name": os.environ.get("LT_TESTRUN_NAME"),
"platform": "Linux",
"browserName": "Chrome",
"version": "latest",
}
return capabilities
return config


@pytest.fixture(scope="function")
def py_config(_override_pylenium_config_values, lambdatest_config) -> PyleniumConfig:
def py_config(_override_pylenium_config_values) -> PyleniumConfig:
"""Get a fresh copy of the PyleniumConfig for each test

See _load_pylenium_json and _override_pylenium_config_values for how the initial configuration is read.
"""
config = _override_pylenium_config_values
if lambdatest_config:
config.driver.capabilities = lambdatest_config
return copy.deepcopy(config)
return copy.deepcopy(_override_pylenium_config_values)


@pytest.fixture(scope="function")
Expand Down Expand Up @@ -270,28 +251,25 @@ def test_go_to_google(py):
try:
if request.node.report.failed:
# if the test failed, execute code in this block
if os.environ.get("LT_USERNAME"):
py.execute_script("lambda-status=failed")
if py_config.logging.screenshots_on:
screenshot = py.screenshot(str(test_case.file_path.joinpath("test_failed.png")))
allure.attach(py.webdriver.get_screenshot_as_png(), "test_failed.png", allure.attachment_type.PNG)

with open(screenshot, "rb") as image_file:
rp_logger.info(
rp_logger.debug(
"Test Failed - Attaching Screenshot",
attachment={"name": "test_failed.png", "data": image_file, "mime": "image/png"},
)
elif request.node.report.passed:
# if the test passed, execute code in this block
if os.environ.get("LT_USERNAME"):
try:
py.webdriver.execute_script("lambda-status=passed")
except JavascriptException:
pass # test not executed in LambdaTest provider
pass
else:
# if the test has another result (ie skipped, inconclusive), execute code in this block
pass
except AttributeError:
rp_logger.error("Unable to access request.node.report.failed, unable to take screenshot.")
except TypeError:
rp_logger.info("Report Portal is not connected to this test run.")
rp_logger.debug("Report Portal is not connected to this test run.")
py.quit()


Expand Down Expand Up @@ -322,7 +300,7 @@ def pytest_addoption(parser):
default="",
help="The filepath of the pylenium.json file to use (ie dev-pylenium.json)",
)
parser.addoption("--pylog_level", action="store", default="", help="Set the pylog_level: 'off' | 'info' | 'debug'")
parser.addoption("--pylog_level", action="store", default="INFO", help="Set the logging level: 'DEBUG' | 'COMMAND' | 'INFO' | 'USER' | 'WARNING' | 'ERROR' | 'CRITICAL'")
parser.addoption(
"--options",
action="store",
Expand Down
2 changes: 1 addition & 1 deletion pylenium/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class DriverConfig(BaseModel):


class LoggingConfig(BaseModel):
pylog_level: str = "debug"
pylog_level: str = "INFO"
screenshots_on: bool = True


Expand Down
1 change: 1 addition & 0 deletions pylenium/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ class Pylenium:

def __init__(self, config: PyleniumConfig):
self.config = config
log.setLevel(self.config.logging.pylog_level)
self.fake = Faker()
self.Keys = Keys
self._webdriver = None
Expand Down
48 changes: 48 additions & 0 deletions pylenium/scripts/allure_reporting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
""" Allure reporting integration """

from typing import List
import rich_click as click
from pylenium.scripts.cli_utils import run_process, parse_response


def _install(commands: List[str]):
"""Command to install allure via the CLI"""
click.echo("\n🛑 It's recommended that you run the above command(s) yourself to see all the output 🛑")
answer = click.prompt("\nWould you like to proceed? (y/n)", default="n")
if answer == "y":
response = run_process(commands)
_, err = parse_response(response)
if response.returncode != 0:
click.echo(f"😢 Unable to install allure. {err}")
return
click.echo("✅ allure installed. Try running `pylenium allure check` to verify the installation.")
return

if answer == "n" or answer is not None:
click.echo("❌ Command aborted")
return


def install_for_linux():
"""Install allure on a Debian-based Linux machine."""
click.echo("This command only works for Debian-based Linux and uses sudo:\n")
click.echo(" sudo apt-add-repository ppa:qameta/allure")
click.echo(" sudo apt-get update")
click.echo(" sudo apt-get install allure")
_install([
"sudo", "apt-add-repository", "ppa:qameta/allure",
"sudo", "apt-get", "update",
"sudo", "apt-get", "install", "allure",
])


def install_for_mac():
click.echo("This command uses homebrew to do the installation:\n")
click.echo(" brew install allure")
_install(["brew", "install", "allure"])


def install_for_windows():
click.echo("This command uses scoop to do the installation:\n")
click.echo(" scoop install allure")
_install(["scoop", "install", "allure"])
55 changes: 55 additions & 0 deletions pylenium/scripts/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@
"""

import os
import platform
import shutil

import rich_click as click
from pyfiglet import Figlet
from pylenium.scripts.cli_utils import run_process, parse_response
from pylenium.scripts import report_portal
from pylenium.scripts import allure_reporting as allure_


def _copy(file, to_dir, message) -> str:
Expand Down Expand Up @@ -113,6 +116,58 @@ def joy():
click.echo(custom_fig.renderText("Pyl e n i u m Sparks Joy"))


# ALLURE REPORTING #
####################

@cli.group()
def allure():
"""CLI Commands to work with allure"""
pass


@allure.command()
def check():
"""Check if the allure CLI is installed on the current machine"""
click.echo("\n>>> allure --version")
response = run_process(["allure", "--version"])
out, err = parse_response(response)
if response.returncode != 0:
click.echo("\n[ERROR] allure is not installed or not added to the PATH. Visit https://docs.qameta.io/allure/#_get_started")
click.echo(err)
return
click.echo(f"\n[SUCCESS] allure is installed with version: {out}")


@allure.command()
def install():
"""Attempt to install allure to the current machine"""
click.echo("\nFor more installation options and details, please visit allure's docs: https://docs.qameta.io/allure/#_get_started")
operating_system = platform.system()
if operating_system.upper() == "LINUX":
allure_.install_for_linux()
return

if operating_system.upper() == "DARWIN":
allure_.install_for_mac()
return

if operating_system.upper() == "WINDOWS":
allure_.install_for_windows()
return


@allure.command()
@click.option("-f", "--folder", type=str, show_default=True)
def serve(folder: str):
"""Start the allure server and serve the allure report given its folder path"""
click.echo(f"\n>>> allure serve {folder}")
click.echo("Press <Ctrl+C> to exit")
response = run_process(["allure", "serve", folder])
_, err = parse_response(response)
if response.returncode != 0:
click.echo(f"\n[ERROR] Unable to serve allure report. Check that the folder path is valid. {err}")


# REPORT PORTAL #
#################

Expand Down
8 changes: 7 additions & 1 deletion pylenium/scripts/cli_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import subprocess
from typing import List, Union
from typing import List, Tuple, Union


def run_process(tokenized_command: Union[List[str], str], shell=False) -> subprocess.CompletedProcess:
Expand All @@ -17,3 +17,9 @@ def run_process(tokenized_command: Union[List[str], str], shell=False) -> subpro
"""
response = subprocess.run(args=tokenized_command, stderr=subprocess.PIPE, stdout=subprocess.PIPE, shell=shell)
return response


def parse_response(response: subprocess.CompletedProcess) -> Tuple[str, str]:
output = str(response.stdout, "utf-8")
error = str(response.stderr, "utf-8")
return output, error
30 changes: 25 additions & 5 deletions pylenium/scripts/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
`conftest.py` and `pylenium.json` files should stay at your Workspace Root (aka Project Root)
The `conftest.py` and `pylenium.json` files generated by Pylenium should stay at your Workspace Root (aka Project Root).

conftest.py
Although this file is editable, you should only change its contents if you know what you are doing.
Expand All @@ -21,10 +21,12 @@ def test_go_to_google(py):
import copy
import json
import logging
import os
import shutil
import sys
from pathlib import Path

import allure
import pytest
import requests
from faker import Faker
Expand Down Expand Up @@ -148,7 +150,7 @@ def _load_pylenium_json(project_root, request) -> PyleniumConfig:


@pytest.fixture(scope="session")
def _override_pylenium_config_values(_load_pylenium_json, request) -> PyleniumConfig:
def _override_pylenium_config_values(_load_pylenium_json: PyleniumConfig, request) -> PyleniumConfig:
"""Override any PyleniumConfig values after loading the initial pylenium.json config file.

After a pylenium.json config file is loaded and converted to a PyleniumConfig object,
Expand All @@ -168,6 +170,10 @@ def _override_pylenium_config_values(_load_pylenium_json, request) -> PyleniumCo
if cli_browser:
config.driver.browser = cli_browser

cli_local_path = request.config.getoption("--local_path")
if cli_local_path:
config.driver.local_path = cli_local_path

cli_capabilities = request.config.getoption("--caps")
if cli_capabilities:
# --caps must be in '{"name": "value", "boolean": true}' format
Expand All @@ -194,6 +200,11 @@ def _override_pylenium_config_values(_load_pylenium_json, request) -> PyleniumCo
if cli_extensions:
config.driver.extension_paths = [ext.strip() for ext in cli_extensions.split(",")]

cli_log_level = request.config.getoption("--pylog_level")
if cli_log_level:
level = cli_log_level.upper()
config.logging.pylog_level = level if level in ["DEBUG", "COMMAND", "INFO", "USER", "WARNING", "ERROR", "CRITICAL"] else "INFO"

return config


Expand Down Expand Up @@ -242,15 +253,23 @@ def test_go_to_google(py):
# if the test failed, execute code in this block
if py_config.logging.screenshots_on:
screenshot = py.screenshot(str(test_case.file_path.joinpath("test_failed.png")))
allure.attach(py.webdriver.get_screenshot_as_png(), "test_failed.png", allure.attachment_type.PNG)

with open(screenshot, "rb") as image_file:
rp_logger.info(
rp_logger.debug(
"Test Failed - Attaching Screenshot",
attachment={"name": "test_failed.png", "data": image_file, "mime": "image/png"},
)
elif request.node.report.passed:
# if the test passed, execute code in this block
pass
else:
# if the test has another result (ie skipped, inconclusive), execute code in this block
pass
except AttributeError:
rp_logger.error("Unable to access request.node.report.failed, unable to take screenshot.")
except TypeError:
rp_logger.info("Report Portal is not connected to this test run.")
rp_logger.debug("Report Portal is not connected to this test run.")
py.quit()


Expand All @@ -272,6 +291,7 @@ def pytest_runtest_makereport(item, call):

def pytest_addoption(parser):
parser.addoption("--browser", action="store", default="", help="The lowercase browser name: chrome | firefox")
parser.addoption("--local_path", action="store", default="", help="The filepath to the local driver")
parser.addoption("--remote_url", action="store", default="", help="Grid URL to connect tests to.")
parser.addoption("--screenshots_on", action="store", default="", help="Should screenshots be saved? true | false")
parser.addoption(
Expand All @@ -280,7 +300,7 @@ def pytest_addoption(parser):
default="",
help="The filepath of the pylenium.json file to use (ie dev-pylenium.json)",
)
parser.addoption("--pylog_level", action="store", default="", help="Set the pylog_level: 'off' | 'info' | 'debug'")
parser.addoption("--pylog_level", action="store", default="INFO", help="Set the logging level: 'DEBUG' | 'COMMAND' | 'INFO' | 'USER' | 'WARNING' | 'ERROR' | 'CRITICAL'")
parser.addoption(
"--options",
action="store",
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ python-dotenv = "^0.20.0"
rich-click = "^1.5.1"
webdriver-manager = "^3.8.4"
selenium-wire = "^5.1.0"
allure-pytest = "^2.11.1"

[tool.poetry.dev-dependencies]
black = "21.12b0"
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"axe-selenium-python>=2.1.6",
"selenium-wire>=4.6.3",
"rich-click>=1.5.1",
"allure-pytest>=2.11.1",
],
data_files=[
(
Expand Down
Loading