From e4316f646316eccbf78b86bafd9aadae9fb538a2 Mon Sep 17 00:00:00 2001 From: Carlos Kidman Date: Thu, 20 May 2021 15:39:31 -0600 Subject: [PATCH 1/4] update test_run fixture in conftest.py to be thread safe --- .gitignore | 2 + conftest.py | 86 +++++++++++++++--------------------- pylenium/scripts/conftest.py | 86 +++++++++++++++--------------------- pyproject.toml | 14 +++++- 4 files changed, 84 insertions(+), 104 deletions(-) diff --git a/.gitignore b/.gitignore index c7e54bf..8a7bccd 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,5 @@ dmypy.json Pipfile.lock .pypyrc + +a11y.json diff --git a/conftest.py b/conftest.py index c2c1741..d66f2e8 100644 --- a/conftest.py +++ b/conftest.py @@ -23,6 +23,7 @@ def test_go_to_google(py): import os import shutil import sys +from pathlib import Path import pytest import requests @@ -34,34 +35,21 @@ def test_go_to_google(py): from pylenium.a11y import PyleniumAxe -def make_dir(filepath) -> bool: - """ Make a directory. - - Returns: - True if successful, False if not. - """ - try: - os.mkdir(filepath) - return True - except FileExistsError: - return False - - @pytest.fixture(scope='function') def fake() -> Faker: - """ A basic instance of Faker to make test data.""" + """A basic instance of Faker to make test data.""" return Faker() @pytest.fixture(scope='function') def api(): - """ A basic instance of Requests to make HTTP API calls. """ + """A basic instance of Requests to make HTTP API calls.""" return requests @pytest.fixture(scope="session") def rp_logger(request): - """ Report Portal Logger """ + """Report Portal Logger""" logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) # Create handler for Report Portal if the service has been @@ -83,7 +71,7 @@ def rp_logger(request): @pytest.fixture(scope='session', autouse=True) def project_root() -> str: - """ The Project (or Workspace) root as a filepath. + """The Project (or Workspace) root as a filepath. * This conftest.py file should be in the Project Root if not already. """ @@ -92,7 +80,7 @@ def project_root() -> str: @pytest.fixture(scope='session', autouse=True) def test_run(project_root, request) -> str: - """ Creates the `/test_results` directory to store the results of the Test Run. + """Creates the `/test_results` directory to store the results of the Test Run. Returns: The `/test_results` directory as a filepath (str). @@ -103,20 +91,19 @@ def test_run(project_root, request) -> str: if os.path.exists(test_results_dir): # delete /test_results from previous Test Run shutil.rmtree(test_results_dir, ignore_errors=True) - if not os.path.exists(test_results_dir): - # create /test_results for this Test Run - make_dir(test_results_dir) + + Path(test_results_dir).mkdir(parents=True, exist_ok=True) for test in session.items: # make the test_result directory for each test - make_dir(f'{test_results_dir}/{test.name}') + Path(f'{test_results_dir}/{test.name}').mkdir(parents=True, exist_ok=True) return test_results_dir @pytest.fixture(scope='session') def py_config(project_root, request) -> PyleniumConfig: - """ Initialize a PyleniumConfig for each test + """Initialize a PyleniumConfig for each test 1. This starts by deserializing the user-created pylenium.json from the Project Root. 2. If that file is not found, then proceed with Pylenium Defaults. @@ -174,7 +161,7 @@ def py_config(project_root, request) -> PyleniumConfig: @pytest.fixture(scope='function') def test_case(test_run, py_config, request) -> TestCase: - """ Manages data pertaining to the currently running Test Function or Case. + """Manages data pertaining to the currently running Test Function or Case. * Creates the test-specific logger. @@ -192,7 +179,7 @@ def test_case(test_run, py_config, request) -> TestCase: @pytest.fixture(scope='function') def py(test_case, py_config, request, rp_logger): - """ Initialize a Pylenium driver for each test. + """Initialize a Pylenium driver for each test. Pass in this `py` fixture into the test function. @@ -209,10 +196,10 @@ def test_go_to_google(py): if py_config.logging.screenshots_on: screenshot = py.screenshot(f'{test_case.file_path}/test_failed.png') with open(screenshot, "rb") as image_file: - rp_logger.info("Test Failed - Attaching Screenshot", - attachment={"name": "test_failed.png", - "data": image_file, - "mime": "image/png"}) + rp_logger.info( + "Test Failed - Attaching Screenshot", + attachment={"name": "test_failed.png", "data": image_file, "mime": "image/png"}, + ) except AttributeError: rp_logger.error('Unable to access request.node.report.failed, unable to take screenshot.') except TypeError: @@ -222,13 +209,13 @@ def test_go_to_google(py): @pytest.fixture(scope='function') def axe(py) -> PyleniumAxe: - """ The aXe A11y audit tool as a fixture. """ + """The aXe A11y audit tool as a fixture.""" return PyleniumAxe(py.webdriver) @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): - """ Yield each test's outcome so we can handle it in other fixtures. """ + """Yield each test's outcome so we can handle it in other fixtures.""" outcome = yield report = outcome.get_result() if report.when == 'call': @@ -237,31 +224,28 @@ 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('--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('--pylog_level', action='store', default='', help="Set the pylog_level: 'off' | 'info' | 'debug'") parser.addoption( - '--browser', action='store', default='', help='The lowercase browser name: chrome | firefox' - ) - 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( - '--pylog_level', action='store', default='', help="Set the pylog_level: 'off' | 'info' | 'debug'" - ) - parser.addoption( - '--options', action='store', - default='', help='Comma-separated list of Browser Options. Ex. "headless, incognito"' + '--options', + action='store', + default='', + help='Comma-separated list of Browser Options. Ex. "headless, incognito"', ) parser.addoption( - '--caps', action='store', - default='', help='List of key-value pairs. Ex. \'{"name": "value", "boolean": true}\'' + '--caps', + action='store', + default='', + help='List of key-value pairs. Ex. \'{"name": "value", "boolean": true}\'', ) parser.addoption( - '--page_load_wait_time', action='store', - default='', help='The amount of time to wait for a page load before raising an error. Default is 0.' + '--page_load_wait_time', + action='store', + default='', + help='The amount of time to wait for a page load before raising an error. Default is 0.', ) parser.addoption( - '--extensions', action='store', - default='', help='Comma-separated list of extension paths. Ex. "*.crx, *.crx"' + '--extensions', action='store', default='', help='Comma-separated list of extension paths. Ex. "*.crx, *.crx"' ) diff --git a/pylenium/scripts/conftest.py b/pylenium/scripts/conftest.py index c2c1741..d66f2e8 100644 --- a/pylenium/scripts/conftest.py +++ b/pylenium/scripts/conftest.py @@ -23,6 +23,7 @@ def test_go_to_google(py): import os import shutil import sys +from pathlib import Path import pytest import requests @@ -34,34 +35,21 @@ def test_go_to_google(py): from pylenium.a11y import PyleniumAxe -def make_dir(filepath) -> bool: - """ Make a directory. - - Returns: - True if successful, False if not. - """ - try: - os.mkdir(filepath) - return True - except FileExistsError: - return False - - @pytest.fixture(scope='function') def fake() -> Faker: - """ A basic instance of Faker to make test data.""" + """A basic instance of Faker to make test data.""" return Faker() @pytest.fixture(scope='function') def api(): - """ A basic instance of Requests to make HTTP API calls. """ + """A basic instance of Requests to make HTTP API calls.""" return requests @pytest.fixture(scope="session") def rp_logger(request): - """ Report Portal Logger """ + """Report Portal Logger""" logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) # Create handler for Report Portal if the service has been @@ -83,7 +71,7 @@ def rp_logger(request): @pytest.fixture(scope='session', autouse=True) def project_root() -> str: - """ The Project (or Workspace) root as a filepath. + """The Project (or Workspace) root as a filepath. * This conftest.py file should be in the Project Root if not already. """ @@ -92,7 +80,7 @@ def project_root() -> str: @pytest.fixture(scope='session', autouse=True) def test_run(project_root, request) -> str: - """ Creates the `/test_results` directory to store the results of the Test Run. + """Creates the `/test_results` directory to store the results of the Test Run. Returns: The `/test_results` directory as a filepath (str). @@ -103,20 +91,19 @@ def test_run(project_root, request) -> str: if os.path.exists(test_results_dir): # delete /test_results from previous Test Run shutil.rmtree(test_results_dir, ignore_errors=True) - if not os.path.exists(test_results_dir): - # create /test_results for this Test Run - make_dir(test_results_dir) + + Path(test_results_dir).mkdir(parents=True, exist_ok=True) for test in session.items: # make the test_result directory for each test - make_dir(f'{test_results_dir}/{test.name}') + Path(f'{test_results_dir}/{test.name}').mkdir(parents=True, exist_ok=True) return test_results_dir @pytest.fixture(scope='session') def py_config(project_root, request) -> PyleniumConfig: - """ Initialize a PyleniumConfig for each test + """Initialize a PyleniumConfig for each test 1. This starts by deserializing the user-created pylenium.json from the Project Root. 2. If that file is not found, then proceed with Pylenium Defaults. @@ -174,7 +161,7 @@ def py_config(project_root, request) -> PyleniumConfig: @pytest.fixture(scope='function') def test_case(test_run, py_config, request) -> TestCase: - """ Manages data pertaining to the currently running Test Function or Case. + """Manages data pertaining to the currently running Test Function or Case. * Creates the test-specific logger. @@ -192,7 +179,7 @@ def test_case(test_run, py_config, request) -> TestCase: @pytest.fixture(scope='function') def py(test_case, py_config, request, rp_logger): - """ Initialize a Pylenium driver for each test. + """Initialize a Pylenium driver for each test. Pass in this `py` fixture into the test function. @@ -209,10 +196,10 @@ def test_go_to_google(py): if py_config.logging.screenshots_on: screenshot = py.screenshot(f'{test_case.file_path}/test_failed.png') with open(screenshot, "rb") as image_file: - rp_logger.info("Test Failed - Attaching Screenshot", - attachment={"name": "test_failed.png", - "data": image_file, - "mime": "image/png"}) + rp_logger.info( + "Test Failed - Attaching Screenshot", + attachment={"name": "test_failed.png", "data": image_file, "mime": "image/png"}, + ) except AttributeError: rp_logger.error('Unable to access request.node.report.failed, unable to take screenshot.') except TypeError: @@ -222,13 +209,13 @@ def test_go_to_google(py): @pytest.fixture(scope='function') def axe(py) -> PyleniumAxe: - """ The aXe A11y audit tool as a fixture. """ + """The aXe A11y audit tool as a fixture.""" return PyleniumAxe(py.webdriver) @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): - """ Yield each test's outcome so we can handle it in other fixtures. """ + """Yield each test's outcome so we can handle it in other fixtures.""" outcome = yield report = outcome.get_result() if report.when == 'call': @@ -237,31 +224,28 @@ 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('--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('--pylog_level', action='store', default='', help="Set the pylog_level: 'off' | 'info' | 'debug'") parser.addoption( - '--browser', action='store', default='', help='The lowercase browser name: chrome | firefox' - ) - 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( - '--pylog_level', action='store', default='', help="Set the pylog_level: 'off' | 'info' | 'debug'" - ) - parser.addoption( - '--options', action='store', - default='', help='Comma-separated list of Browser Options. Ex. "headless, incognito"' + '--options', + action='store', + default='', + help='Comma-separated list of Browser Options. Ex. "headless, incognito"', ) parser.addoption( - '--caps', action='store', - default='', help='List of key-value pairs. Ex. \'{"name": "value", "boolean": true}\'' + '--caps', + action='store', + default='', + help='List of key-value pairs. Ex. \'{"name": "value", "boolean": true}\'', ) parser.addoption( - '--page_load_wait_time', action='store', - default='', help='The amount of time to wait for a page load before raising an error. Default is 0.' + '--page_load_wait_time', + action='store', + default='', + help='The amount of time to wait for a page load before raising an error. Default is 0.', ) parser.addoption( - '--extensions', action='store', - default='', help='Comma-separated list of extension paths. Ex. "*.crx, *.crx"' + '--extensions', action='store', default='', help='Comma-separated list of extension paths. Ex. "*.crx, *.crx"' ) diff --git a/pyproject.toml b/pyproject.toml index 68b1567..71d05c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyleniumio" -version = "1.12.2" +version = "1.12.5" description = "The best of Selenium and Cypress in a single Python Package" authors = ["Carlos "] license = "MIT" @@ -22,7 +22,17 @@ selenium = "^3.141.0" [tool.poetry.dev-dependencies] flake8 = "^3.8.4" -autopep8 = "^1.5.4" +black = "^21.5b1" +poethepoet = "^0.10.0" +pytest-cov = "^2.12.0" + +[tool.black] +line-length = 119 +skip-string-normalization = true + +[tool.poe.tasks] +test-unit = { "cmd" = "pytest tests/unit --cov=. --cov-report term-missing -n 4", "help" = "Run Unit Tests and get Code Coverage Report" } +test-ui = { "cmd" = "pytest tests/ui --cov=. --cov-report term-missing -n 2", "help" = "Run UI Tests" } [build-system] requires = ["poetry-core>=1.0.0"] From daae0be47525169f993cf955417d7cf4defca566 Mon Sep 17 00:00:00 2001 From: Carlos Kidman Date: Thu, 20 May 2021 16:01:24 -0600 Subject: [PATCH 2/4] update pipeline to use python 3.9 --- .github/workflows/build-and-test.yml | 4 ++-- tests/ui/test_element.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index cb63017..722275a 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -20,10 +20,10 @@ jobs: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + name: Set up Python 3.9 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies diff --git a/tests/ui/test_element.py b/tests/ui/test_element.py index df65c66..fa7a318 100644 --- a/tests/ui/test_element.py +++ b/tests/ui/test_element.py @@ -1,6 +1,5 @@ import pytest from pylenium.driver import Pylenium -from selenium.common.exceptions import ElementNotInteractableException THE_INTERNET = 'https://the-internet.herokuapp.com' From c50430127b7539ecb583d906e7644dc08c58da3a Mon Sep 17 00:00:00 2001 From: Carlos Kidman Date: Fri, 21 May 2021 10:29:42 -0600 Subject: [PATCH 3/4] The package is now called Faker --- pyproject.toml | 2 +- setup.py | 33 ++++++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 71d05c9..55708de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ license = "MIT" python = "^3.8" webdriver-manager = "^3.3.0" pydantic = "^1.7.3" -Faker = "^5.8.0" requests = "^2.25.1" click = "^7.1.2" pyfiglet = "^0.8.post1" @@ -19,6 +18,7 @@ pytest-xdist = "^2.2.0" pytest-parallel = "^0.1.0" axe-selenium-python = "^2.1.6" selenium = "^3.141.0" +Faker = "^8.2.1" [tool.poetry.dev-dependencies] flake8 = "^3.8.4" diff --git a/setup.py b/setup.py index a182c27..e27747d 100644 --- a/setup.py +++ b/setup.py @@ -15,17 +15,32 @@ long_description=open('README.md').read(), long_description_content_type='text/markdown', install_requires=[ - 'selenium', 'pytest', 'pytest-xdist', 'pytest-parallel', 'pydantic', 'pytest-reportportal', - 'faker', 'requests', 'webdriver-manager', 'click', 'pyfiglet', 'axe-selenium-python' + 'selenium', + 'pytest', + 'pytest-xdist', + 'pytest-parallel', + 'pydantic', + 'pytest-reportportal', + 'Faker', + 'requests', + 'webdriver-manager', + 'click', + 'pyfiglet', + 'axe-selenium-python', + ], + data_files=[ + ( + '', + [ + 'pylenium/scripts/pylenium.json', + 'pylenium/scripts/pytest.ini', + 'pylenium/scripts/drag_and_drop.js', + 'pylenium/scripts/load_jquery.js', + ], + ) ], - data_files=[('', [ - 'pylenium/scripts/pylenium.json', - 'pylenium/scripts/pytest.ini', - 'pylenium/scripts/drag_and_drop.js', - 'pylenium/scripts/load_jquery.js' - ])], entry_points=''' [console_scripts] pylenium=pylenium.scripts.cli:cli - ''' + ''', ) From ebad057ac242d181c4e5087d75ed1ccc9404fdda Mon Sep 17 00:00:00 2001 From: Carlos Kidman Date: Fri, 21 May 2021 10:38:52 -0600 Subject: [PATCH 4/4] Update API test to use more stable API --- tests/ui/test_pydriver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/ui/test_pydriver.py b/tests/ui/test_pydriver.py index 9da60b0..be4903f 100644 --- a/tests/ui/test_pydriver.py +++ b/tests/ui/test_pydriver.py @@ -10,9 +10,9 @@ def test_jit_webdriver(py: Pylenium): def test_py_request(py: Pylenium): - response = py.request.get('https://statsroyale.com/api/cards') + response = py.request.get('https://deckofcardsapi.com/api/deck/new/shuffle/?deck_count=1') assert response.ok - assert response.json()[0]['name'] + assert response.json()['success'] def test_execute_script(py: Pylenium): @@ -35,7 +35,7 @@ def test_cookies(py: Pylenium): 'name': 'foo', 'path': '/', 'secure': True, - 'value': 'bar' + 'value': 'bar', } py.visit('https://google.com')