diff --git a/integration-tests/.pytest.ini b/integration-tests/.pytest.ini index 07fb7b9ee4..6124409099 100644 --- a/integration-tests/.pytest.ini +++ b/integration-tests/.pytest.ini @@ -17,6 +17,11 @@ log_cli_format = %(name)s %(asctime)s [%(levelname)s] %(message)s log_cli_level = INFO markers = clp: mark tests that use the CLP storage engine + clp_json: mark tests that use the clp-json package clp_s: mark tests that use the CLP-S storage engine + clp_text: mark tests that use the clp-text package + compression: mark tests that test compression core: mark tests that test the CLP core binaries - package: mark tests that run when the CLP package is active + package: mark tests that use the CLP package + search: mark tests that test search + startup: mark tests that test startup diff --git a/integration-tests/tests/conftest.py b/integration-tests/tests/conftest.py index c1af599c98..1741453b3a 100644 --- a/integration-tests/tests/conftest.py +++ b/integration-tests/tests/conftest.py @@ -7,7 +7,7 @@ "tests.fixtures.integration_test_logs", "tests.fixtures.path_configs", "tests.fixtures.package_instance", - "tests.fixtures.package_config", + "tests.fixtures.package_test_config", ] diff --git a/integration-tests/tests/fixtures/package_instance.py b/integration-tests/tests/fixtures/package_instance.py index b2ea7ef4b3..c4425cdca5 100644 --- a/integration-tests/tests/fixtures/package_instance.py +++ b/integration-tests/tests/fixtures/package_instance.py @@ -5,8 +5,8 @@ import pytest from tests.utils.config import ( - PackageConfig, PackageInstance, + PackageTestConfig, ) from tests.utils.package_utils import ( start_clp_package, @@ -14,26 +14,29 @@ ) -@pytest.fixture -def fixt_package_instance(fixt_package_config: PackageConfig) -> Iterator[PackageInstance]: +@pytest.fixture(scope="module") +def fixt_package_instance(fixt_package_test_config: PackageTestConfig) -> Iterator[PackageInstance]: """ Starts a CLP package instance for the given configuration and stops it during teardown. - :param fixt_package_config: + This fixture relies on `fixt_package_test_config`, and as such, the scope of this fixture should + never exceed that of `fixt_package_test_config`. + + :param fixt_package_test_config: :return: Iterator that yields the running package instance. """ - mode_name = fixt_package_config.mode_name - try: - start_clp_package(fixt_package_config) - instance = PackageInstance(package_config=fixt_package_config) + start_clp_package(fixt_package_test_config) + instance = PackageInstance(package_test_config=fixt_package_test_config) yield instance except RuntimeError: - base_port = fixt_package_config.base_port + mode_config = fixt_package_test_config.mode_config + mode_name = mode_config.mode_name + base_port = fixt_package_test_config.base_port pytest.fail( f"Failed to start the {mode_name} package. This could mean that one of the ports" f" derived from base_port={base_port} was unavailable. You can specify a new value for" " base_port with the '--base-port' flag." ) finally: - stop_clp_package(fixt_package_config) + stop_clp_package(fixt_package_test_config) diff --git a/integration-tests/tests/fixtures/package_config.py b/integration-tests/tests/fixtures/package_test_config.py similarity index 50% rename from integration-tests/tests/fixtures/package_config.py rename to integration-tests/tests/fixtures/package_test_config.py index 8db533dcf2..e10ae7f3b9 100644 --- a/integration-tests/tests/fixtures/package_config.py +++ b/integration-tests/tests/fixtures/package_test_config.py @@ -4,29 +4,25 @@ import pytest -from tests.utils.clp_mode_utils import ( - get_clp_config_from_mode, - get_required_component_list, -) -from tests.utils.config import PackageConfig, PackagePathConfig +from tests.utils.config import PackageModeConfig, PackagePathConfig, PackageTestConfig from tests.utils.port_utils import assign_ports_from_base -@pytest.fixture -def fixt_package_config( +@pytest.fixture(scope="module") +def fixt_package_test_config( request: pytest.FixtureRequest, fixt_package_path_config: PackagePathConfig, -) -> Iterator[PackageConfig]: +) -> Iterator[PackageTestConfig]: """ - Creates and maintains a PackageConfig object for a specific CLP mode. + Creates and maintains a module-level PackageTestConfig object for a specific CLP mode. For + efficiency, group all tests for a given mode in the same module. - :param request: - :return: An iterator that yields the PackageConfig object for the specified mode. + :param request: Provides `PackageModeConfig` via `request.param`. + :return: An iterator that yields the PackageTestConfig object for the specified mode. :raise ValueError: if the CLP base port's value is invalid. """ - mode_name: str = request.param - - clp_config_obj = get_clp_config_from_mode(mode_name) + mode_config: PackageModeConfig = request.param + clp_config_obj = mode_config.clp_config # Assign ports based on the clp base port CLI option. base_port_string = request.config.getoption("--base-port") @@ -37,18 +33,14 @@ def fixt_package_config( raise ValueError(err_msg) from err assign_ports_from_base(base_port, clp_config_obj) - required_components = get_required_component_list(clp_config_obj) - - # Construct PackageConfig. - package_config = PackageConfig( + # Construct PackageTestConfig. + package_test_config = PackageTestConfig( path_config=fixt_package_path_config, - mode_name=mode_name, - component_list=required_components, - clp_config=clp_config_obj, + mode_config=mode_config, base_port=base_port, ) try: - yield package_config + yield package_test_config finally: - package_config.temp_config_file_path.unlink(missing_ok=True) + package_test_config.temp_config_file_path.unlink(missing_ok=True) diff --git a/integration-tests/tests/package_tests/clp_json/__init__.py b/integration-tests/tests/package_tests/clp_json/__init__.py new file mode 100644 index 0000000000..e84f07103f --- /dev/null +++ b/integration-tests/tests/package_tests/clp_json/__init__.py @@ -0,0 +1 @@ +"""Integration tests for the clp-json package.""" diff --git a/integration-tests/tests/package_tests/clp_json/test_clp_json.py b/integration-tests/tests/package_tests/clp_json/test_clp_json.py new file mode 100644 index 0000000000..d3c48af72f --- /dev/null +++ b/integration-tests/tests/package_tests/clp_json/test_clp_json.py @@ -0,0 +1,88 @@ +"""Tests for the clp-json package.""" + +import logging + +import pytest +from clp_py_utils.clp_config import ( + ClpConfig, + Package, + QueryEngine, + StorageEngine, +) + +from tests.utils.asserting_utils import ( + validate_package_instance, +) +from tests.utils.clp_mode_utils import CLP_API_SERVER_COMPONENT, CLP_BASE_COMPONENTS +from tests.utils.config import PackageInstance, PackageModeConfig + +logger = logging.getLogger(__name__) + + +# Mode description for this module. +CLP_JSON_MODE = PackageModeConfig( + mode_name="clp-json", + clp_config=ClpConfig( + package=Package( + storage_engine=StorageEngine.CLP_S, + query_engine=QueryEngine.CLP_S, + ), + ), + component_list=(*CLP_BASE_COMPONENTS, CLP_API_SERVER_COMPONENT), +) + + +# Pytest markers for this module. +pytestmark = [ + pytest.mark.package, + pytest.mark.clp_json, + pytest.mark.parametrize("fixt_package_test_config", [CLP_JSON_MODE], indirect=True), +] + + +@pytest.mark.startup +def test_clp_json_startup(fixt_package_instance: PackageInstance) -> None: + """ + Validate that the `clp-json` package starts up successfully. + + :param fixt_package_instance: + """ + validate_package_instance(fixt_package_instance) + + log_msg = "test_clp_json_startup was successful." + logger.info(log_msg) + + +@pytest.mark.compression +def test_clp_json_compression(fixt_package_instance: PackageInstance) -> None: + """ + Validate that the `clp-json` package successfully compresses some dataset. + + :param fixt_package_instance: + """ + validate_package_instance(fixt_package_instance) + + # TODO: compress some dataset and check the correctness of compression. + assert True + + log_msg = "test_clp_json_compression was successful." + logger.info(log_msg) + + +@pytest.mark.search +def test_clp_json_search(fixt_package_instance: PackageInstance) -> None: + """ + Validate that the `clp-json` package successfully searches some dataset. + + :param fixt_package_instance: + """ + validate_package_instance(fixt_package_instance) + + # TODO: compress some dataset and check the correctness of compression. + + # TODO: search through that dataset and check the correctness of the search results. + + assert True + + log_msg = "test_clp_json_search was successful." + logger.info(log_msg) diff --git a/integration-tests/tests/package_tests/clp_text/__init__.py b/integration-tests/tests/package_tests/clp_text/__init__.py new file mode 100644 index 0000000000..85f1b137d5 --- /dev/null +++ b/integration-tests/tests/package_tests/clp_text/__init__.py @@ -0,0 +1 @@ +"""Integration tests for the clp-text package.""" diff --git a/integration-tests/tests/package_tests/clp_text/test_clp_text.py b/integration-tests/tests/package_tests/clp_text/test_clp_text.py new file mode 100644 index 0000000000..dbbc862f4c --- /dev/null +++ b/integration-tests/tests/package_tests/clp_text/test_clp_text.py @@ -0,0 +1,51 @@ +"""Tests for the clp-text package.""" + +import logging + +import pytest +from clp_py_utils.clp_config import ( + ClpConfig, + Package, + QueryEngine, + StorageEngine, +) + +from tests.utils.asserting_utils import ( + validate_package_instance, +) +from tests.utils.clp_mode_utils import CLP_BASE_COMPONENTS +from tests.utils.config import PackageInstance, PackageModeConfig + +logger = logging.getLogger(__name__) + + +# Mode description for this module. +CLP_TEXT_MODE = PackageModeConfig( + mode_name="clp-text", + clp_config=ClpConfig( + package=Package( + storage_engine=StorageEngine.CLP, + query_engine=QueryEngine.CLP, + ), + api_server=None, + log_ingestor=None, + ), + component_list=(*CLP_BASE_COMPONENTS,), +) + + +# Pytest markers for this module. +pytestmark = [ + pytest.mark.package, + pytest.mark.clp_text, + pytest.mark.parametrize("fixt_package_test_config", [CLP_TEXT_MODE], indirect=True), +] + + +@pytest.mark.startup +def test_clp_text_startup(fixt_package_instance: PackageInstance) -> None: + """Tests package startup.""" + validate_package_instance(fixt_package_instance) + + log_msg = "test_clp_text_startup was successful." + logger.info(log_msg) diff --git a/integration-tests/tests/test_package_start.py b/integration-tests/tests/test_package_start.py deleted file mode 100644 index 0603cf2cb0..0000000000 --- a/integration-tests/tests/test_package_start.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Integration tests verifying that the CLP package can be started and stopped.""" - -import pytest - -from tests.utils.asserting_utils import ( - validate_package_running, - validate_running_mode_correct, -) -from tests.utils.clp_mode_utils import CLP_MODE_CONFIGS -from tests.utils.config import PackageInstance - -TEST_MODES = CLP_MODE_CONFIGS.keys() - - -@pytest.mark.package -@pytest.mark.parametrize("fixt_package_config", TEST_MODES, indirect=True) -def test_clp_package(fixt_package_instance: PackageInstance) -> None: - """ - Validate that the CLP package starts up successfully for the selected mode(s) of operation. - - :param fixt_package_instance: - """ - # Ensure that all package components are running. - validate_package_running(fixt_package_instance) - - # Ensure that the package is running in the correct mode. - validate_running_mode_correct(fixt_package_instance) diff --git a/integration-tests/tests/utils/asserting_utils.py b/integration-tests/tests/utils/asserting_utils.py index 651f67ea2e..0a9b78e424 100644 --- a/integration-tests/tests/utils/asserting_utils.py +++ b/integration-tests/tests/utils/asserting_utils.py @@ -37,7 +37,22 @@ def run_and_assert(cmd: list[str], **kwargs: Any) -> subprocess.CompletedProcess return proc -def validate_package_running(package_instance: PackageInstance) -> None: +def validate_package_instance(package_instance: PackageInstance) -> None: + """ + Validate that the given package instance is running by performing two checks: validate that the + instance has exactly the set of running components that it should have, and validate that the + instance is running in the correct mode. + + :param package_instance: + """ + # Ensure that all package components are running. + _validate_package_running(package_instance) + + # Ensure that the package is running in the correct mode. + _validate_running_mode_correct(package_instance) + + +def _validate_package_running(package_instance: PackageInstance) -> None: """ Validate that the given package instance is running by checking that the set of services running in the Compose project exactly matches the list of required components. @@ -51,7 +66,7 @@ def validate_package_running(package_instance: PackageInstance) -> None: running_services = set(list_running_services_in_compose_project(project_name)) # Compare with list of required components. - required_components = set(package_instance.package_config.component_list) + required_components = set(package_instance.package_test_config.mode_config.component_list) if required_components == running_services: return @@ -68,7 +83,7 @@ def validate_package_running(package_instance: PackageInstance) -> None: pytest.fail(fail_msg) -def validate_running_mode_correct(package_instance: PackageInstance) -> None: +def _validate_running_mode_correct(package_instance: PackageInstance) -> None: """ Validate that the mode described in the shared config of the instance matches the intended mode defined by the instance configuration. Calls pytest.fail if the shared config fails validation @@ -85,7 +100,7 @@ def validate_running_mode_correct(package_instance: PackageInstance) -> None: except ValidationError as err: pytest.fail(f"Shared config failed validation: {err}") - intended_config = package_instance.package_config.clp_config + intended_config = package_instance.package_test_config.mode_config.clp_config if not compare_mode_signatures(intended_config, running_config): pytest.fail("Mode mismatch: running configuration does not match intended configuration.") diff --git a/integration-tests/tests/utils/clp_mode_utils.py b/integration-tests/tests/utils/clp_mode_utils.py index 07370bc233..5a57d1458f 100644 --- a/integration-tests/tests/utils/clp_mode_utils.py +++ b/integration-tests/tests/utils/clp_mode_utils.py @@ -1,6 +1,5 @@ """Provides utilities related to the user-level configurations of CLP's operating modes.""" -from collections.abc import Callable from typing import Any from clp_py_utils.clp_config import ( @@ -10,36 +9,16 @@ COMPRESSION_WORKER_COMPONENT_NAME, DB_COMPONENT_NAME, GARBAGE_COLLECTOR_COMPONENT_NAME, - Package, QUERY_SCHEDULER_COMPONENT_NAME, QUERY_WORKER_COMPONENT_NAME, - QueryEngine, QUEUE_COMPONENT_NAME, REDIS_COMPONENT_NAME, REDUCER_COMPONENT_NAME, RESULTS_CACHE_COMPONENT_NAME, - StorageEngine, WEBUI_COMPONENT_NAME, ) from pydantic import BaseModel -CLP_MODE_CONFIGS: dict[str, Callable[[], ClpConfig]] = { - "clp-text": lambda: ClpConfig( - package=Package( - storage_engine=StorageEngine.CLP, - query_engine=QueryEngine.CLP, - ), - api_server=None, - log_ingestor=None, - ), - "clp-json": lambda: ClpConfig( - package=Package( - storage_engine=StorageEngine.CLP_S, - query_engine=QueryEngine.CLP_S, - ), - ), -} - # TODO: This will eventually be replaced by a formalized mapping between component and service. def _to_docker_compose_service_name(name: str) -> str: @@ -52,10 +31,11 @@ def _to_docker_compose_service_name(name: str) -> str: return name.replace("_", "-") -# These component lists should be maintained alongside the CLP_MODE_CONFIGS list. +# Names of components that may comprise a given package mode. Test modules use these lists to +# assemble mode-specific component lists (see tests/package_tests/*/test_*.py). # TODO: Modify these component lists when the Presto Docker Compose project is integrated with the # CLP Docker compose project. -CLP_BASE_COMPONENTS = [ +CLP_BASE_COMPONENTS: tuple[str, ...] = ( _to_docker_compose_service_name(DB_COMPONENT_NAME), _to_docker_compose_service_name(QUEUE_COMPONENT_NAME), _to_docker_compose_service_name(REDIS_COMPONENT_NAME), @@ -67,7 +47,7 @@ def _to_docker_compose_service_name(name: str) -> str: _to_docker_compose_service_name(QUERY_SCHEDULER_COMPONENT_NAME), _to_docker_compose_service_name(QUERY_WORKER_COMPONENT_NAME), _to_docker_compose_service_name(GARBAGE_COLLECTOR_COMPONENT_NAME), -] +) CLP_API_SERVER_COMPONENT = _to_docker_compose_service_name(API_SERVER_COMPONENT_NAME) @@ -84,38 +64,6 @@ def compare_mode_signatures(intended_config: ClpConfig, running_config: ClpConfi return _match_objects_by_explicit_fields(intended_config, running_config) -def get_clp_config_from_mode(mode_name: str) -> ClpConfig: - """ - Return a ClpConfig object for the given mode name. - - :param mode_name: - :return: ClpConfig object corresponding to the mode. - :raise ValueError: If the mode is not supported. - """ - if mode_name not in CLP_MODE_CONFIGS: - err_msg = f"Unsupported mode: {mode_name}" - raise ValueError(err_msg) - return CLP_MODE_CONFIGS[mode_name]() - - -def get_required_component_list(config: ClpConfig) -> list[str]: - """ - Constructs the list of components required for the CLP package described in `config` to run - properly. - - This function should be maintained alongside the CLP_MODE_CONFIGS list. - - :param config: - :return: List of components required by the package. - """ - component_list: list[str] = list(CLP_BASE_COMPONENTS) - - if config.api_server is not None: - component_list.append(CLP_API_SERVER_COMPONENT) - - return component_list - - def _match_objects_by_explicit_fields(intended_obj: Any, running_obj: Any) -> bool: """ Compares `intended_obj` and `running_obj` using Pydantic's `model_fields_set` to recursively diff --git a/integration-tests/tests/utils/config.py b/integration-tests/tests/utils/config.py index ee9fc94a5a..76a8e2d43a 100644 --- a/integration-tests/tests/utils/config.py +++ b/integration-tests/tests/utils/config.py @@ -121,38 +121,46 @@ def stop_script_path(self) -> Path: @dataclass(frozen=True) -class PackageConfig: - """Metadata for a specific configuration of the CLP package.""" - - #: Path configuration for this package. - path_config: PackagePathConfig +class PackageModeConfig: + """Mode configuration for the CLP package.""" #: Name of the package operation mode. mode_name: str + #: The Pydantic representation of the package operation mode. + clp_config: ClpConfig + #: The list of CLP components that this package needs. - component_list: list[str] + component_list: tuple[str, ...] - #: The Pydantic representation of a CLP package configuration. - clp_config: ClpConfig - #: The base port from which all ports for the components are derived. +@dataclass(frozen=True) +class PackageTestConfig: + """Metadata for a specific test of the CLP package.""" + + #: Path configuration for this package test. + path_config: PackagePathConfig + + #: Mode configuration for this package test. + mode_config: PackageModeConfig + + #: The base port from which all port assignments are derived. base_port: int def __post_init__(self) -> None: - """Write the temporary config file for this package.""" + """Write the temporary config file for this package test.""" self._write_temp_config_file() @property def temp_config_file_path(self) -> Path: """:return: The absolute path to the temporary configuration file for the package.""" - return self.path_config.temp_config_dir / f"clp-config-{self.mode_name}.yaml" + return self.path_config.temp_config_dir / f"clp-config-{self.mode_config.mode_name}.yaml" def _write_temp_config_file(self) -> None: - """Writes the temporary config file for this package.""" + """Writes the temporary config file for this package test.""" temp_config_file_path = self.temp_config_file_path - payload = self.clp_config.dump_to_primitive_dict() # type: ignore[no-untyped-call] + payload = self.mode_config.clp_config.dump_to_primitive_dict() # type: ignore[no-untyped-call] tmp_path = temp_config_file_path.with_suffix(temp_config_file_path.suffix + ".tmp") with tmp_path.open("w", encoding="utf-8") as f: @@ -165,7 +173,7 @@ class PackageInstance: """Metadata for a running instance of the CLP package.""" #: The configuration for this package instance. - package_config: PackageConfig + package_test_config: PackageTestConfig #: The instance ID of the running package. clp_instance_id: str = field(init=False, repr=True) @@ -176,10 +184,10 @@ class PackageInstance: def __post_init__(self) -> None: """Validates init values and initializes attributes.""" # Validate that the temp config file exists. - validate_file_exists(self.package_config.temp_config_file_path) + validate_file_exists(self.package_test_config.temp_config_file_path) # Set clp_instance_id from instance-id file. - path_config = self.package_config.path_config + path_config = self.package_test_config.path_config clp_instance_id_file_path = path_config.clp_log_dir / "instance-id" validate_file_exists(clp_instance_id_file_path) clp_instance_id = self._get_clp_instance_id(clp_instance_id_file_path) diff --git a/integration-tests/tests/utils/package_utils.py b/integration-tests/tests/utils/package_utils.py index 7d714f707a..31254b8791 100644 --- a/integration-tests/tests/utils/package_utils.py +++ b/integration-tests/tests/utils/package_utils.py @@ -1,21 +1,21 @@ """Provides utility functions related to the CLP package used across `integration-tests`.""" from tests.utils.asserting_utils import run_and_assert -from tests.utils.config import PackageConfig +from tests.utils.config import PackageTestConfig DEFAULT_CMD_TIMEOUT_SECONDS = 120.0 -def start_clp_package(package_config: PackageConfig) -> None: +def start_clp_package(package_test_config: PackageTestConfig) -> None: """ Starts an instance of the CLP package. - :param package_config: + :param package_test_config: :raise: Propagates `run_and_assert`'s errors. """ - path_config = package_config.path_config + path_config = package_test_config.path_config start_script_path = path_config.start_script_path - temp_config_file_path = package_config.temp_config_file_path + temp_config_file_path = package_test_config.temp_config_file_path # fmt: off start_cmd = [ @@ -26,16 +26,16 @@ def start_clp_package(package_config: PackageConfig) -> None: run_and_assert(start_cmd, timeout=DEFAULT_CMD_TIMEOUT_SECONDS) -def stop_clp_package(package_config: PackageConfig) -> None: +def stop_clp_package(package_test_config: PackageTestConfig) -> None: """ Stops the running instance of the CLP package. - :param package_config: + :param package_test_config: :raise: Propagates `run_and_assert`'s errors. """ - path_config = package_config.path_config + path_config = package_test_config.path_config stop_script_path = path_config.stop_script_path - temp_config_file_path = package_config.temp_config_file_path + temp_config_file_path = package_test_config.temp_config_file_path # fmt: off stop_cmd = [