From 5be6c82f457acfa27303529636a9c37281159605 Mon Sep 17 00:00:00 2001 From: Alex Hadley Date: Wed, 13 Dec 2023 17:20:09 -0800 Subject: [PATCH] #203 Add more tests and add pytest-xdist --- paramview/_server.py | 2 +- poetry.lock | 36 +++++++++- pyproject.toml | 2 +- tests/e2e/conftest.py | 33 ++++++++- tests/e2e/helpers.py | 8 ++- tests/e2e/test_page_title.py | 8 +-- tests/e2e/test_parameter_editing.py | 108 +++++++++++++++++++++++++--- 7 files changed, 178 insertions(+), 19 deletions(-) diff --git a/paramview/_server.py b/paramview/_server.py index 296f668..ee314b7 100644 --- a/paramview/_server.py +++ b/paramview/_server.py @@ -33,7 +33,7 @@ def _available_port(host: str, default_port: int) -> int: def start_server( db_path: str, - host: str = "localhost", + host: str = "127.0.0.1", default_port: int = 5050, open_window: bool = True, ) -> None: diff --git a/poetry.lock b/poetry.lock index 73fb09a..e624417 100644 --- a/poetry.lock +++ b/poetry.lock @@ -412,6 +412,20 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "execnet" +version = "2.0.2" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.7" +files = [ + {file = "execnet-2.0.2-py3-none-any.whl", hash = "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41"}, + {file = "execnet-2.0.2.tar.gz", hash = "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + [[package]] name = "flake8" version = "6.1.0" @@ -1085,6 +1099,26 @@ pytest = ">=6.2.4,<8.0.0" pytest-base-url = ">=1.0.0,<3.0.0" python-slugify = ">=6.0.0,<9.0.0" +[[package]] +name = "pytest-xdist" +version = "3.5.0" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-xdist-3.5.0.tar.gz", hash = "sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a"}, + {file = "pytest_xdist-3.5.0-py3-none-any.whl", hash = "sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24"}, +] + +[package.dependencies] +execnet = ">=1.1" +pytest = ">=6.2.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -1559,4 +1593,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "b71dc049e51e6cc0f4492f8e50e3b998c33435d7610928b633515b2a5e170119" +content-hash = "3a7d8997ffb43ffc56395d3b6c39686362938013b1d1f1bd608bc6b2258c2e4a" diff --git a/pyproject.toml b/pyproject.toml index 6118fe2..16526f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ flake8 = "^6.1.0" pylint = "^3.0.2" black = "^23.11.0" pytest = "^7.4.3" +pytest-xdist = "^3.5.0" pytest-playwright = "^0.4.3" freezegun = "1.2.2" requests = "^2.31.0" @@ -40,5 +41,4 @@ strict = true [tool.pytest.ini_options] addopts = ["--import-mode=importlib"] -base_url = "http://localhost:5050" filterwarnings = ["ignore::DeprecationWarning:eventlet.support.greenlets"] diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 58f68a9..117b664 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -7,9 +7,38 @@ from typing import Any from collections.abc import Generator import pytest +from xdist import get_xdist_worker_id # type: ignore +from playwright.sync_api import Page from paramdb import ParamDB from tests.e2e.helpers import setup_db_and_start_server, clear, reset +HOST = "http://127.0.0.1" +STARTING_PORT = 7001 # xdist workers will increment up from this port + + +@pytest.fixture(name="port", scope="session") +def fixture_port(request: pytest.FixtureRequest) -> int: + """Port to run ParamView server on.""" + worker_id = get_xdist_worker_id(request) + if worker_id == "master": + port_offset = 0 + else: + assert worker_id[:2] == "gw" + port_offset = int(worker_id[2:]) + return STARTING_PORT + port_offset + + +@pytest.fixture(name="base_url", scope="session") +def fixture_base_url(port: int) -> str: + """Base URL for ParamView.""" + return f"{HOST}:{port}" + + +@pytest.fixture(name="_visit_page") +def fixture_visit_page(page: Page, base_url: str) -> None: + """Visits the page.""" + page.goto(base_url) + @pytest.fixture(name="db_name", scope="session") def fixture_db_name() -> str: @@ -19,14 +48,14 @@ def fixture_db_name() -> str: @pytest.fixture(name="db_path", scope="session") def fixture_db_path( - tmp_path_factory: pytest.TempPathFactory, base_url: str, db_name: str + tmp_path_factory: pytest.TempPathFactory, port: int, base_url: str, db_name: str ) -> Generator[str, None, None]: """ Path to the ParamDB database. The ParamView server for the database is also started and cleaned up by this fixture. """ db_path = str(tmp_path_factory.mktemp("db") / db_name) - stop_server = setup_db_and_start_server(db_path, base_url) + stop_server = setup_db_and_start_server(db_path, port, base_url) yield db_path stop_server() diff --git a/tests/e2e/helpers.py b/tests/e2e/helpers.py index c28789b..f165bc7 100644 --- a/tests/e2e/helpers.py +++ b/tests/e2e/helpers.py @@ -83,7 +83,9 @@ def reset(db: ParamDB[Any], num_commits: int = 3) -> None: commit(db) -def setup_db_and_start_server(db_path: str, base_url: str) -> Callable[[], None]: +def setup_db_and_start_server( + db_path: str, port: int, base_url: str +) -> Callable[[], None]: """ Set up the database, start the server, wait for the server to be up, and return a function to stop the server. @@ -98,7 +100,9 @@ def setup_db_and_start_server(db_path: str, base_url: str) -> Callable[[], None] reset(ParamDB(db_path)) # pylint: disable=consider-using-with - server_process = subprocess.Popen(["paramview", db_path, "--no-open"]) + server_process = subprocess.Popen( + ["paramview", db_path, "--port", f"{port}", "--no-open"] + ) # Wait for server to be up for _ in range(_SERVER_POLLING_MAX_RETRIES): diff --git a/tests/e2e/test_page_title.py b/tests/e2e/test_page_title.py index 49ebe9c..8867dc0 100644 --- a/tests/e2e/test_page_title.py +++ b/tests/e2e/test_page_title.py @@ -3,13 +3,13 @@ from playwright.sync_api import Page, expect -def test_title_is_db_name(_reset_db: None, page: Page, db_name: str) -> None: +def test_title_is_db_name( + _reset_db: None, _visit_page: None, page: Page, db_name: str +) -> None: """Page title is the database file name.""" - page.goto("/") expect(page).to_have_title(db_name) -def test_title_is_error(_clear_db: None, page: Page) -> None: +def test_title_is_error(_clear_db: None, _visit_page: None, page: Page) -> None: """Page title is "Error" if an error has occurred.""" - page.goto("/") expect(page).to_have_title("Error") diff --git a/tests/e2e/test_parameter_editing.py b/tests/e2e/test_parameter_editing.py index 5bfcae3..8ba8ea7 100644 --- a/tests/e2e/test_parameter_editing.py +++ b/tests/e2e/test_parameter_editing.py @@ -1,14 +1,106 @@ """Tests for parameter editing.""" +from __future__ import annotations +import pytest from playwright.sync_api import Page, expect +from tests.e2e.helpers import get_date -def test_displays_inputs(_reset_single_db: None, page: Page) -> None: - """Displays correct input for each parameter type.""" - page.goto("/") +@pytest.fixture(autouse=True) +def setup(_reset_single_db: None, _visit_page: None, page: Page) -> None: + """Automatically run before each test in this module.""" page.get_by_test_id("edit-button").click() - expect( - page.get_by_test_id("parameter-list-item-int") - .get_by_test_id("leaf-input") - .get_by_role("textbox") - ).to_have_value("123") + + +def test_displays_input_int(page: Page) -> None: + """Displays correct input for int parameters.""" + item = page.get_by_test_id("parameter-list-item-int") + leaf_input = item.get_by_test_id("leaf-input").get_by_role("textbox") + leaf_type_input = item.get_by_test_id("leaf-type-input").get_by_role("combobox") + + expect(leaf_input).to_have_value("123") + expect(item.get_by_test_id("leaf-unit-input")).not_to_be_attached() + expect(leaf_type_input).to_have_text("int/float") + + +def test_displays_input_float(page: Page) -> None: + """Displays correct input for float parameters.""" + item = page.get_by_test_id("parameter-list-item-float") + leaf_input = item.get_by_test_id("leaf-input").get_by_role("textbox") + leaf_type_input = item.get_by_test_id("leaf-type-input").get_by_role("combobox") + + expect(leaf_input).to_have_value("1.2345") + expect(item.get_by_test_id("leaf-unit-input")).not_to_be_attached() + expect(leaf_type_input).to_have_text("int/float") + + +def test_displays_input_bool(page: Page) -> None: + """Displays correct input for bool parameters.""" + item = page.get_by_test_id("parameter-list-item-bool") + leaf_input = item.get_by_test_id("leaf-input").get_by_role("combobox") + leaf_type_input = item.get_by_test_id("leaf-type-input").get_by_role("combobox") + + expect(leaf_input).to_have_text("True") + expect(item.get_by_test_id("leaf-unit-input")).not_to_be_attached() + expect(leaf_type_input).to_have_text("bool") + + +def test_displays_input_str(page: Page) -> None: + """Displays correct input for str parameters.""" + item = page.get_by_test_id("parameter-list-item-str") + leaf_input = item.get_by_test_id("leaf-input").get_by_role("textbox") + leaf_type_input = item.get_by_test_id("leaf-type-input").get_by_role("combobox") + + expect(leaf_input).to_have_value("test") + expect(item.get_by_test_id("leaf-unit-input")).not_to_be_attached() + expect(leaf_type_input).to_have_text("str") + + +def test_displays_input_none(page: Page) -> None: + """Displays correct input for None parameters.""" + item = page.get_by_test_id("parameter-list-item-None") + leaf_input = item.get_by_test_id("leaf-input").get_by_role("textbox") + leaf_type_input = item.get_by_test_id("leaf-type-input").get_by_role("combobox") + + expect(leaf_input).to_have_value("None") + expect(leaf_input).to_be_disabled() + expect(item.get_by_test_id("leaf-unit-input")).not_to_be_attached() + expect(leaf_type_input).to_have_text("None") + + +def test_displays_input_datetime(page: Page) -> None: + """Displays correct input for datetime parameters.""" + datetime_input_value = get_date(1).astimezone().strftime("%Y-%m-%dT%H:%M") + item = page.get_by_test_id("parameter-list-item-datetime") + leaf_input = item.get_by_test_id("leaf-input").locator("input[type=datetime-local]") + leaf_type_input = item.get_by_test_id("leaf-type-input").get_by_role("combobox") + + expect(leaf_input).to_have_value(datetime_input_value) + expect(item.get_by_test_id("leaf-unit-input")).not_to_be_attached() + expect(leaf_type_input).to_have_text("datetime") + + +def test_displays_input_quantity(page: Page) -> None: + """Displays correct input for Quantity parameters.""" + item = page.get_by_test_id("parameter-list-item-Quantity") + leaf_input = item.get_by_test_id("leaf-input").get_by_role("textbox") + leaf_unit_input = item.get_by_test_id("leaf-unit-input").get_by_role("textbox") + leaf_type_input = item.get_by_test_id("leaf-type-input").get_by_role("combobox") + + expect(leaf_input).to_have_value("1.2345") + expect(leaf_unit_input).to_have_value("m") + expect(leaf_type_input).to_have_text("Quantity") + + +def test_can_edit_input_int(page: Page) -> None: + """Input for int parameters can be edited and reset.""" + item = page.get_by_test_id("parameter-list-item-int") + leaf_input = item.get_by_test_id("leaf-input").get_by_role("textbox") + reset_button = item.get_by_test_id("reset-leaf-button") + + expect(leaf_input).to_have_attribute("aria-invalid", "false") + leaf_input.fill("123a") + expect(leaf_input).to_have_attribute("aria-invalid", "true") + reset_button.click() + expect(leaf_input).to_have_value("123") + expect(leaf_input).to_have_attribute("aria-invalid", "false")