diff --git a/.gitignore b/.gitignore index 2432dd5adc..cfb9aff9af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +__pycache__/ .clang-format .clang-tidy .lint-venv/ diff --git a/docs/src/dev-docs/building-package.md b/docs/src/dev-docs/building-package.md index b899dbd706..c84aeaf6f2 100644 --- a/docs/src/dev-docs/building-package.md +++ b/docs/src/dev-docs/building-package.md @@ -15,6 +15,7 @@ prebuilt version instead, check out the [releases](https://github.com/y-scope/cl * python3-venv (for the version of Python installed) * [Task] 3.44.0 * We pin the version to 3.44.0 due to [y-scope/clp-ffi-js#110]. +* [uv] >= 0.8 ## Setup @@ -80,4 +81,5 @@ task docker-images:package This will create a Docker image named `clp-package:dev`. [Task]: https://taskfile.dev/ +[uv]: https://docs.astral.sh/uv/ [y-scope/clp-ffi-js#110]: https://github.com/y-scope/clp-ffi-js/issues/110 diff --git a/docs/src/dev-docs/components-core/index.md b/docs/src/dev-docs/components-core/index.md index a210243223..b86e4fa44b 100644 --- a/docs/src/dev-docs/components-core/index.md +++ b/docs/src/dev-docs/components-core/index.md @@ -14,6 +14,7 @@ CLP core is the low-level component that performs compression, decompression, an * We constrain the version to < 4.0.0 due to [y-scope/clp#795]. * [Task] 3.44.0 * We pin the version to 3.44.0 due to [y-scope/clp-ffi-js#110]. +* [uv] >= 0.8 To build, we require some source dependencies, packages from package managers, and libraries built from source. @@ -113,6 +114,12 @@ the relevant paths on your machine. make -j ``` +## Test + +```shell +task tests:integration:core +``` + :::{toctree} :hidden: @@ -125,6 +132,7 @@ regex-utils [CMake]: https://cmake.org/ [feature-req]: https://github.com/y-scope/clp/issues/new?assignees=&labels=enhancement&template=feature-request.yml [Task]: https://taskfile.dev/ +[uv]: https://docs.astral.sh/uv/ [y-scope/clp-ffi-js#110]: https://github.com/y-scope/clp-ffi-js/issues/110 [y-scope/clp#795]: https://github.com/y-scope/clp/issues/795 [yscope-dev-utils]: https://github.com/y-scope/yscope-dev-utils diff --git a/docs/src/dev-docs/index.md b/docs/src/dev-docs/index.md index 5372121755..a4ef9e60fe 100644 --- a/docs/src/dev-docs/index.md +++ b/docs/src/dev-docs/index.md @@ -15,6 +15,13 @@ Building Docs about building CLP. ::: +:::{grid-item-card} +:link: testing/index +Testing +^^^ +Docs about testing CLP. +::: + :::{grid-item-card} :link: contributing-getting-started Contributing @@ -50,6 +57,15 @@ Any design docs describing parts of this project. building-package ::: +:::{toctree} +:caption: Testing +:hidden: + +testing/index +testing/unit-tests +testing/integration-tests +::: + :::{toctree} :caption: Contributing :hidden: diff --git a/docs/src/dev-docs/testing/index.md b/docs/src/dev-docs/testing/index.md new file mode 100644 index 0000000000..0fd9e03f82 --- /dev/null +++ b/docs/src/dev-docs/testing/index.md @@ -0,0 +1,19 @@ +# Overview + +::::{grid} 1 1 1 1 +:gutter: 2 + +:::{grid-item-card} +:link: unit-tests +Unit tests +^^^ +Docs about running unit tests for each component. +::: + +:::{grid-item-card} +:link: integration-tests +Integration tests +^^^ +Docs about running CLP's integration tests. +::: +:::: diff --git a/docs/src/dev-docs/testing/integration-tests.md b/docs/src/dev-docs/testing/integration-tests.md new file mode 100644 index 0000000000..145aa41bbe --- /dev/null +++ b/docs/src/dev-docs/testing/integration-tests.md @@ -0,0 +1,48 @@ +# Integration tests + +The `integration-tests` directory contains a Python project that provides end-to-end tests for +CLP via the `pytest` framework. + +## Running tests + +To run all integration tests: + +```shell +task tests:integration +``` + +To test the core CLP binaries: + +```shell +task tests:integration:core +``` + +### Using `pytest` markers + +To run more specific sets of tests, you can use `pytest` directly with `pytest` markers. + +:::{note} +Before running tests using `pytest`, ensure that the CLP package and/or core binaries have been +built. +::: + +Ensure all commands below are run from inside the `integration-tests` directory. + +To list all available markers: + +```shell +uv run python -m pytest --markers +``` + +To run tests related to a specific marker (e.g., `clp_s`): + +```shell +uv run python -m pytest -m clp_s +``` + +### Specifying custom CLP binary paths + +You can override the default binary paths by setting the following environment variables: + +* **`CLP_CORE_BINS_DIR`**: Directory containing the CLP core binaries to test. +* **`CLP_PACKAGE_DIR`**: Directory of the CLP package to test. diff --git a/docs/src/dev-docs/testing/unit-tests.md b/docs/src/dev-docs/testing/unit-tests.md new file mode 100644 index 0000000000..8a2bb742e1 --- /dev/null +++ b/docs/src/dev-docs/testing/unit-tests.md @@ -0,0 +1,5 @@ +# Unit tests + +:::{warning} +🚧 This section is under construction. +::: diff --git a/integration-tests/.pytest.ini b/integration-tests/.pytest.ini new file mode 100644 index 0000000000..6235100f27 --- /dev/null +++ b/integration-tests/.pytest.ini @@ -0,0 +1,20 @@ +[pytest] +addopts = + --capture=no + --code-highlight=yes + --color=yes + --strict-config + --strict-markers + --verbose +env = + D:CLP_BUILD_DIR=../build + D:CLP_CORE_BINS_DIR=../build/core + D:CLP_PACKAGE_DIR=../build/clp-package +log_cli = True +log_cli_date_format = %Y-%m-%d %H:%M:%S,%f +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_s: mark tests that use the CLP-S storage engine + core: mark tests that test the CLP core binaries diff --git a/integration-tests/.python-version b/integration-tests/.python-version new file mode 100644 index 0000000000..bd28b9c5c2 --- /dev/null +++ b/integration-tests/.python-version @@ -0,0 +1 @@ +3.9 diff --git a/integration-tests/README.md b/integration-tests/README.md new file mode 100644 index 0000000000..479704d8f5 --- /dev/null +++ b/integration-tests/README.md @@ -0,0 +1,7 @@ +# CLP integration tests + +This Python project provides end-to-end tests for CLP via the `pytest` framework. + +For more information, see the [integration test docs][integration-test-docs]. + +[integration-test-docs]: https://docs.yscope.com/clp/main/dev-docs/testing/integration-tests diff --git a/integration-tests/pyproject.toml b/integration-tests/pyproject.toml new file mode 100644 index 0000000000..06b5752775 --- /dev/null +++ b/integration-tests/pyproject.toml @@ -0,0 +1,71 @@ +[project] +name = "integration-tests" +version = "0.1.0" +description = "Integration tests for CLP." +readme = "README.md" +authors = [ + { name = "YScope Inc.", email = "dev@yscope.com" } +] +requires-python = ">=3.9" + +[project.scripts] +integration-tests = "integration_tests:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[dependency-groups] +dev = [ + "mypy>=1.16.0", + "ruff>=0.11.12", + "pytest>=8.4.1", + "pytest-env>=1.1.5", +] + +[tool.mypy] +strict = true + +# Additional output +pretty = true +show_error_code_links = true +show_error_context = true +show_error_end = true + +[tool.ruff] +line-length = 100 + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "ANN401", # Allow using `Any` type for function signatures + "COM812", # Redundant and conflicts with ruff format + "D203", # No blank line before docstrings (D211) + "D205", # Breaks if summary is larger than one line due to wrapping or if no summary exists + "D212", # Enforce docstring summary line on the next line after quotes (D213) + "D400", # First line of docstrings may not end in period + "D401", # Docstrings should be written in present tense (not imperative) + "D415", # First line of docstrings may not end in a period, question mark, or exclamation point + "FBT", # Allow bool positional parameters since other value positions are allowed + "FIX002", # Allow todo statements + "PERF401", # Allow for loops when creating lists + "PERF403", # Allow for loops when creating dicts + "S311", # Allow usage of `random` package + "S603", # Automatically trust inputs of subprocess execution + "SIM102", # Allow collapsible if statements for readability + "SIM300", # Skip Yoda-condition format fixes + "TD002", # Author unnecessary for todo statement + "TD003", # Issue link unnecessary for todo statement + "UP015", # Explicit open modes are helpful +] +isort.order-by-type = false + +[tool.ruff.lint.per-file-ignores] +"tests/**" = [ + "S101", # Allow usage of pytest `assert` + "TC003", # Ignore performance overhead of imports only used for type checking +] + +[tool.ruff.format] +docstring-code-format = true +docstring-code-line-length = 100 diff --git a/integration-tests/src/integration_tests/__init__.py b/integration-tests/src/integration_tests/__init__.py new file mode 100644 index 0000000000..dc20358fd2 --- /dev/null +++ b/integration-tests/src/integration_tests/__init__.py @@ -0,0 +1 @@ +"""Integration test package for CLP.""" diff --git a/integration-tests/tests/__init__.py b/integration-tests/tests/__init__.py new file mode 100644 index 0000000000..5d36df00ef --- /dev/null +++ b/integration-tests/tests/__init__.py @@ -0,0 +1 @@ +"""Top-level package for CLP integration tests.""" diff --git a/integration-tests/tests/conftest.py b/integration-tests/tests/conftest.py new file mode 100644 index 0000000000..2cb709a249 --- /dev/null +++ b/integration-tests/tests/conftest.py @@ -0,0 +1,6 @@ +"""Make the fixtures defined in `tests/fixtures/` globally available without imports.""" + +pytest_plugins = [ + "tests.fixtures.integration_test_config", + "tests.fixtures.integration_test_logs", +] diff --git a/integration-tests/tests/fixtures/__init__.py b/integration-tests/tests/fixtures/__init__.py new file mode 100644 index 0000000000..a74b1a0c32 --- /dev/null +++ b/integration-tests/tests/fixtures/__init__.py @@ -0,0 +1 @@ +"""Fixtures for CLP integration tests.""" diff --git a/integration-tests/tests/fixtures/integration_test_config.py b/integration-tests/tests/fixtures/integration_test_config.py new file mode 100644 index 0000000000..f097021b3d --- /dev/null +++ b/integration-tests/tests/fixtures/integration_test_config.py @@ -0,0 +1,29 @@ +"""Define the integration test configuration fixture.""" + +from pathlib import Path + +import pytest + +from tests.utils.config import ( + CoreConfig, + IntegrationTestConfig, + PackageConfig, +) +from tests.utils.utils import get_env_var + + +@pytest.fixture(scope="session") +def integration_test_config() -> IntegrationTestConfig: + """Fixture that provides an IntegrationTestConfig shared across tests.""" + core_config = CoreConfig( + clp_core_bins_dir=Path(get_env_var("CLP_CORE_BINS_DIR")).expanduser().resolve() + ) + package_config = PackageConfig( + clp_package_dir=Path(get_env_var("CLP_PACKAGE_DIR")).expanduser().resolve() + ) + test_root_dir = Path(get_env_var("CLP_BUILD_DIR")).expanduser().resolve() / "integration-tests" + return IntegrationTestConfig( + core_config=core_config, + package_config=package_config, + test_root_dir=test_root_dir, + ) diff --git a/integration-tests/tests/fixtures/integration_test_logs.py b/integration-tests/tests/fixtures/integration_test_logs.py new file mode 100644 index 0000000000..21c288e4c5 --- /dev/null +++ b/integration-tests/tests/fixtures/integration_test_logs.py @@ -0,0 +1,96 @@ +"""Define test logs fixtures.""" + +import logging +import shutil +import subprocess + +import pytest + +from tests.utils.config import ( + IntegrationTestConfig, + IntegrationTestLogs, +) +from tests.utils.utils import unlink + +logger = logging.getLogger(__name__) + + +@pytest.fixture(scope="session") +def hive_24hr( + request: pytest.FixtureRequest, + integration_test_config: IntegrationTestConfig, +) -> IntegrationTestLogs: + """Fixture that provides `hive_24hr` test logs shared across tests.""" + return _download_and_extract_dataset( + request=request, + integration_test_config=integration_test_config, + name="hive-24hr", + tarball_url="https://zenodo.org/records/7094921/files/hive-24hr.tar.gz?download=1", + ) + + +@pytest.fixture(scope="session") +def postgresql( + request: pytest.FixtureRequest, + integration_test_config: IntegrationTestConfig, +) -> IntegrationTestLogs: + """Fixture that provides `postgresql` test logs shared across tests.""" + return _download_and_extract_dataset( + request=request, + integration_test_config=integration_test_config, + name="postgresql", + tarball_url="https://zenodo.org/records/10516402/files/postgresql.tar.gz?download=1", + ) + + +def _download_and_extract_dataset( + request: pytest.FixtureRequest, + integration_test_config: IntegrationTestConfig, + name: str, + tarball_url: str, +) -> IntegrationTestLogs: + integration_test_logs = IntegrationTestLogs( + name=name, + tarball_url=tarball_url, + integration_test_config=integration_test_config, + ) + if request.config.cache.get(name, False): + logger.info("Test logs `%s` are up-to-date. Skipping download.", name) + return integration_test_logs + + curl_bin = shutil.which("curl") + if curl_bin is None: + err_msg = "curl executable not found" + raise RuntimeError(err_msg) + + try: + # fmt: off + curl_cmds = [ + curl_bin, + "--fail", + "--location", + "--output", str(integration_test_logs.tarball_path), + "--show-error", + tarball_url, + ] + # fmt: on + subprocess.run(curl_cmds, check=True) + + unlink(integration_test_logs.extraction_dir) + shutil.unpack_archive( + integration_test_logs.tarball_path, integration_test_logs.extraction_dir + ) + except Exception as e: + err_msg = f"Failed to download and extract dataset `{name}`." + raise RuntimeError(err_msg) from e + + # Allow the extracted content to be deletable or overwritable + chmod_bin = shutil.which("chmod") + if chmod_bin is None: + err_msg = "chmod executable not found" + raise RuntimeError(err_msg) + subprocess.run([chmod_bin, "-R", "gu+w", integration_test_logs.extraction_dir], check=True) + + logger.info("Downloaded and extracted uncompressed logs for dataset `%s`.", name) + request.config.cache.set(name, True) + return integration_test_logs diff --git a/integration-tests/tests/test_identity_transformation.py b/integration-tests/tests/test_identity_transformation.py new file mode 100644 index 0000000000..953ffd913f --- /dev/null +++ b/integration-tests/tests/test_identity_transformation.py @@ -0,0 +1,146 @@ +""" +Integration tests verifying that CLP core compression binaries perform lossless round-trip +compression and decompression. +""" + +import pytest + +from tests.utils.asserting_utils import run_and_assert +from tests.utils.config import ( + CompressionTestConfig, + IntegrationTestConfig, + IntegrationTestLogs, +) +from tests.utils.utils import ( + is_dir_tree_content_equal, + is_json_file_structurally_equal, +) + +pytestmark = pytest.mark.core + +text_datasets = pytest.mark.parametrize( + "test_logs_fixture", + [ + "hive_24hr", + ], +) + +json_datasets = pytest.mark.parametrize( + "test_logs_fixture", + [ + "postgresql", + ], +) + + +@pytest.mark.clp +@text_datasets +def test_clp_identity_transform( + request: pytest.FixtureRequest, + integration_test_config: IntegrationTestConfig, + test_logs_fixture: str, +) -> None: + """ + Validate that compression and decompression by the core binary `clp` run successfully and are + lossless. + + :param request: + :param integration_test_config: + :param test_logs_fixture: + """ + integration_test_logs: IntegrationTestLogs = request.getfixturevalue(test_logs_fixture) + test_paths = CompressionTestConfig( + test_name=f"clp-{integration_test_logs.name}", + logs_source_dir=integration_test_logs.extraction_dir, + integration_test_config=integration_test_config, + ) + test_paths.clear_test_outputs() + + bin_path = str(integration_test_config.core_config.clp_binary_path) + src_path = str(test_paths.logs_source_dir) + compression_path = str(test_paths.compression_dir) + decompression_path = str(test_paths.decompression_dir) + # fmt: off + compression_cmd = [ + bin_path, + "c", + "--progress", + "--remove-path-prefix", src_path, + compression_path, + src_path, + ] + # fmt: on + run_and_assert(compression_cmd) + + decompression_cmd = [bin_path, "x", compression_path, decompression_path] + run_and_assert(decompression_cmd) + + input_path = test_paths.logs_source_dir + output_path = test_paths.decompression_dir + assert is_dir_tree_content_equal( + input_path, + output_path, + ), f"Mismatch between clp input {input_path} and output {output_path}." + + test_paths.clear_test_outputs() + + +@pytest.mark.clp_s +@json_datasets +def test_clp_s_identity_transform( + request: pytest.FixtureRequest, + integration_test_config: IntegrationTestConfig, + test_logs_fixture: str, +) -> None: + """ + Validate that compression and decompression by the core binary `clp-s` run successfully and are + lossless. + + :param request: + :param integration_test_config: + :param test_logs_fixture: + """ + integration_test_logs: IntegrationTestLogs = request.getfixturevalue(test_logs_fixture) + test_logs_name = integration_test_logs.name + + test_paths = CompressionTestConfig( + test_name=f"clp-s-{test_logs_name}", + logs_source_dir=integration_test_logs.extraction_dir, + integration_test_config=integration_test_config, + ) + _clp_s_compress_and_decompress(integration_test_config, test_paths) + + # Recompress the decompressed output that's consolidated into a single json file, and decompress + # it again to verify consistency. The compression input of the second iteration points to the + # decompression output of the first. + # TODO: Remove this check once we can directly compare decompressed logs (which would preserve + # the directory structure and row/key order) with the original downloaded logs. + # See also: https://docs.yscope.com/clp/main/user-guide/core-clp-s.html#current-limitations + consolidated_json_test_paths = CompressionTestConfig( + test_name=f"clp-s-{test_logs_name}-consolidated-json", + logs_source_dir=test_paths.decompression_dir, + integration_test_config=integration_test_config, + ) + _clp_s_compress_and_decompress(integration_test_config, consolidated_json_test_paths) + + _consolidated_json_file_name = "original" + input_path = consolidated_json_test_paths.logs_source_dir / _consolidated_json_file_name + output_path = consolidated_json_test_paths.decompression_dir / _consolidated_json_file_name + assert is_json_file_structurally_equal(input_path, output_path), ( + f"Mismatch between clp-s input {input_path} and output {output_path}." + ) + + test_paths.clear_test_outputs() + consolidated_json_test_paths.clear_test_outputs() + + +def _clp_s_compress_and_decompress( + integration_test_config: IntegrationTestConfig, test_paths: CompressionTestConfig +) -> None: + test_paths.clear_test_outputs() + bin_path = str(integration_test_config.core_config.clp_s_binary_path) + src_path = str(test_paths.logs_source_dir) + compression_path = str(test_paths.compression_dir) + decompression_path = str(test_paths.decompression_dir) + run_and_assert([bin_path, "c", compression_path, src_path]) + run_and_assert([bin_path, "x", compression_path, decompression_path]) diff --git a/integration-tests/tests/utils/__init__.py b/integration-tests/tests/utils/__init__.py new file mode 100644 index 0000000000..15f5b9e1db --- /dev/null +++ b/integration-tests/tests/utils/__init__.py @@ -0,0 +1 @@ +"""Utility functions and configuration classes for CLP integration tests.""" diff --git a/integration-tests/tests/utils/asserting_utils.py b/integration-tests/tests/utils/asserting_utils.py new file mode 100644 index 0000000000..fe39ec4576 --- /dev/null +++ b/integration-tests/tests/utils/asserting_utils.py @@ -0,0 +1,22 @@ +"""Utilities that raise pytest assertions on failure.""" + +import subprocess +from typing import Any + +import pytest + + +def run_and_assert(cmd: list[str], **kwargs: Any) -> subprocess.CompletedProcess[Any]: + """ + Runs a command with subprocess and asserts that it succeeds with pytest. + + :param cmd: Command and arguments to execute. + :param kwargs: Additional keyword arguments passed through to the subprocess. + :return: The completed process object, for inspection or further handling. + :raise: pytest.fail if the command exits with a non-zero return code. + """ + try: + proc = subprocess.run(cmd, check=True, **kwargs) + except subprocess.CalledProcessError as e: + pytest.fail(f"Command failed: {' '.join(cmd)}: {e}") + return proc diff --git a/integration-tests/tests/utils/config.py b/integration-tests/tests/utils/config.py new file mode 100644 index 0000000000..b087a106f2 --- /dev/null +++ b/integration-tests/tests/utils/config.py @@ -0,0 +1,158 @@ +"""Define all python classes used in `integration-tests`.""" + +from __future__ import annotations + +from dataclasses import dataclass, field, InitVar +from pathlib import Path + +from tests.utils.utils import ( + unlink, + validate_dir_exists, +) + + +@dataclass(frozen=True) +class CoreConfig: + """The configuration for the clp core binaries subject to testing.""" + + #: + clp_core_bins_dir: Path + + def __post_init__(self) -> None: + """ + Validates that the CLP core binaries directory exists and contains all required + executables. + """ + clp_core_bins_dir = self.clp_core_bins_dir + validate_dir_exists(clp_core_bins_dir) + + # Check for required CLP core binaries + required_binaries = ["clg", "clo", "clp", "clp-s", "indexer", "reducer-server"] + missing_binaries = [b for b in required_binaries if not (clp_core_bins_dir / b).is_file()] + if len(missing_binaries) > 0: + err_msg = ( + f"CLP core binaries at {clp_core_bins_dir} are incomplete." + f" Missing binaries: {', '.join(missing_binaries)}" + ) + raise ValueError(err_msg) + + @property + def clp_binary_path(self) -> Path: + """:return: The absolute path to the core binary `clp`.""" + return self.clp_core_bins_dir / "clp" + + @property + def clp_s_binary_path(self) -> Path: + """:return: The absolute path to the core binary `clp-s`.""" + return self.clp_core_bins_dir / "clp-s" + + +@dataclass(frozen=True) +class PackageConfig: + """The configuration for the clp package subject to testing.""" + + #: + clp_package_dir: Path + + def __post_init__(self) -> None: + """Validates that the CLP package directory exists and contains all required directories.""" + clp_package_dir = self.clp_package_dir + validate_dir_exists(clp_package_dir) + + # Check for required package script directories + required_dirs = ["bin", "etc", "lib", "sbin"] + missing_dirs = [d for d in required_dirs if not (clp_package_dir / d).is_dir()] + if len(missing_dirs) > 0: + err_msg = ( + f"CLP package at {clp_package_dir} is incomplete." + f" Missing directories: {', '.join(missing_dirs)}" + ) + raise ValueError(err_msg) + + +@dataclass(frozen=True) +class IntegrationTestConfig: + """The general configuration for integration tests.""" + + #: + core_config: CoreConfig + #: + package_config: PackageConfig + #: Root directory for integration tests output. + test_root_dir: Path + logs_download_dir_init: InitVar[Path | None] = None + #: Directory to store the downloaded logs. + logs_download_dir: Path = field(init=False, repr=True) + + def __post_init__(self, logs_download_dir_init: Path | None) -> None: + """Initialize and create required directories for integration tests.""" + if logs_download_dir_init is not None: + object.__setattr__(self, "logs_download_dir", logs_download_dir_init) + else: + object.__setattr__(self, "logs_download_dir", self.test_root_dir / "downloads") + + self.test_root_dir.mkdir(parents=True, exist_ok=True) + self.logs_download_dir.mkdir(parents=True, exist_ok=True) + + +@dataclass(frozen=True) +class IntegrationTestLogs: + """Metadata for the downloaded logs used for integration tests.""" + + #: + name: str + #: + tarball_url: str + integration_test_config: InitVar[IntegrationTestConfig] + #: + tarball_path: Path = field(init=False, repr=True) + #: + extraction_dir: Path = field(init=False, repr=True) + + def __post_init__(self, integration_test_config: IntegrationTestConfig) -> None: + """Initialize and set tarball and extraction paths for integration test logs.""" + name = self.name.strip() + if 0 == len(name): + err_msg = "`name` cannot be empty." + raise ValueError(err_msg) + logs_download_dir = integration_test_config.logs_download_dir + validate_dir_exists(logs_download_dir) + + object.__setattr__(self, "name", name) + object.__setattr__(self, "tarball_path", logs_download_dir / f"{name}.tar.gz") + object.__setattr__(self, "extraction_dir", logs_download_dir / name) + + +@dataclass(frozen=True) +class CompressionTestConfig: + """Compression test configuration providing per-test metadata for artifacts and directories.""" + + #: + test_name: str + #: Directory containing the original (uncompressed) log files used by this test. + logs_source_dir: Path + integration_test_config: InitVar[IntegrationTestConfig] + #: Path to store compressed archives generated by the test. + compression_dir: Path = field(init=False, repr=True) + #: Path to store decompressed logs generated by the test. + decompression_dir: Path = field(init=False, repr=True) + + def __post_init__(self, integration_test_config: IntegrationTestConfig) -> None: + """Initialize and set required directory paths for compression tests.""" + test_name = self.test_name.strip() + if 0 == len(test_name): + err_msg = "`test_name` cannot be empty." + raise ValueError(err_msg) + test_root_dir = integration_test_config.test_root_dir + validate_dir_exists(test_root_dir) + + object.__setattr__(self, "test_name", test_name) + object.__setattr__(self, "compression_dir", test_root_dir / f"{test_name}-archives") + object.__setattr__( + self, "decompression_dir", test_root_dir / f"{test_name}-decompressed-logs" + ) + + def clear_test_outputs(self) -> None: + """Remove any existing output directories created by this compression test.""" + unlink(self.compression_dir) + unlink(self.decompression_dir) diff --git a/integration-tests/tests/utils/utils.py b/integration-tests/tests/utils/utils.py new file mode 100644 index 0000000000..1dca8ba162 --- /dev/null +++ b/integration-tests/tests/utils/utils.py @@ -0,0 +1,119 @@ +"""Provide general utility functions used across `integration-tests`.""" + +import os +import shutil +import subprocess +from pathlib import Path +from tempfile import NamedTemporaryFile +from typing import IO + + +def get_env_var(var_name: str) -> str: + """ + :param var_name: + :return: The string value of the specified environment variable. + :raise: ValueError if the environment variable is not set + """ + value = os.environ.get(var_name) + if value is None: + err_msg = f"Environment variable {var_name} is not set." + raise ValueError(err_msg) + return value + + +def is_dir_tree_content_equal(path1: Path, path2: Path) -> bool: + """ + :param path1: + :param path2: + :return: Whether two files/directories hold the exactly same content. + :raise: RuntimeError if the diff command fails due to execution errors. + """ + cmd = ["diff", "--brief", "--recursive", str(path1), str(path2)] + proc = subprocess.run(cmd, check=False, capture_output=True) + if proc.returncode == 0: + return True + if proc.returncode == 1: + return False + err_msg = f"Command failed {' '.join(cmd)}: {proc.stderr.decode()}" + raise RuntimeError(err_msg) + + +def is_json_file_structurally_equal(json_fp1: Path, json_fp2: Path) -> bool: + """ + :param json_fp1: + :param json_fp2: + :return: Whether two JSON files are structurally equal after sorting has been applied. + """ + with ( + _sort_json_keys_and_rows(json_fp1) as temp_file_1, + _sort_json_keys_and_rows(json_fp2) as temp_file_2, + ): + return is_dir_tree_content_equal(Path(temp_file_1.name), Path(temp_file_2.name)) + + +def unlink(rm_path: Path, force: bool = True) -> None: + """ + Remove a file or directory at `path`. + + :param rm_path: + :param force: Whether to force remove with sudo priviledges in case the normal operation fails. + Defaults to True. + """ + try: + shutil.rmtree(rm_path) + except FileNotFoundError: + pass + except PermissionError: + if not force: + raise + + sudo_rm_cmds = ["sudo", "rm", "-rf", str(rm_path)] + try: + subprocess.run(sudo_rm_cmds, check=True) + except subprocess.CalledProcessError as e: + err_msg = f"Failed to remove {rm_path} due to lack of superuser privileges (sudo)." + raise OSError(err_msg) from e + + +def validate_dir_exists(dir_path: Path) -> None: + """ + :param dir_path: + :raise: ValueError if the path does not exist or is not a directory. + """ + if not dir_path.is_dir(): + err_msg = f"Path does not exist or is not a directory: {dir_path}" + raise ValueError(err_msg) + + +def _sort_json_keys_and_rows(json_fp: Path) -> IO[str]: + """ + Normalize a JSON file to a stable, deterministically ordered form for comparison. + + :param json_fp: + :return: A named temporary file (delete on close) that contains the sorted JSON content. + :raise: RuntimeError if either jq or sort is missing or fails due to execution errors. + """ + jq_bin = shutil.which("jq") + sort_bin = shutil.which("sort") + if jq_bin is None or sort_bin is None: + err_msg = "jq/sort executable not found" + raise RuntimeError(err_msg) + + sorted_fp = NamedTemporaryFile(mode="w+", delete=True) # noqa: SIM115 + jq_proc = subprocess.Popen( + [jq_bin, "--sort-keys", "--compact-output", ".", str(json_fp)], + stdout=subprocess.PIPE, + ) + try: + subprocess.run([sort_bin], stdin=jq_proc.stdout, stdout=sorted_fp, check=True) + finally: + if jq_proc.stdout is not None: + jq_proc.stdout.close() + jq_rc = jq_proc.wait() + if jq_rc != 0: + err_msg = f"jq failed with exit code {jq_rc} for {json_fp}" + raise RuntimeError(err_msg) + + sorted_fp.flush() + sorted_fp.seek(0) + return sorted_fp diff --git a/integration-tests/uv.lock b/integration-tests/uv.lock new file mode 100644 index 0000000000..67070d7ec5 --- /dev/null +++ b/integration-tests/uv.lock @@ -0,0 +1,261 @@ +version = 1 +revision = 2 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "integration-tests" +version = "0.1.0" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-env" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.16.0" }, + { name = "pytest", specifier = ">=8.4.1" }, + { name = "pytest-env", specifier = ">=1.1.5" }, + { name = "ruff", specifier = ">=0.11.12" }, +] + +[[package]] +name = "mypy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/a9/3d7aa83955617cdf02f94e50aab5c830d205cfa4320cf124ff64acce3a8e/mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972", size = 11003299, upload-time = "2025-07-31T07:54:06.425Z" }, + { url = "https://files.pythonhosted.org/packages/83/e8/72e62ff837dd5caaac2b4a5c07ce769c8e808a00a65e5d8f94ea9c6f20ab/mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7", size = 10125451, upload-time = "2025-07-31T07:53:52.974Z" }, + { url = "https://files.pythonhosted.org/packages/7d/10/f3f3543f6448db11881776f26a0ed079865926b0c841818ee22de2c6bbab/mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df", size = 11916211, upload-time = "2025-07-31T07:53:18.879Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/63e83ed551282d67bb3f7fea2cd5561b08d2bb6eb287c096539feb5ddbc5/mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390", size = 12652687, upload-time = "2025-07-31T07:53:30.544Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/68f2eeef11facf597143e85b694a161868b3b006a5fbad50e09ea117ef24/mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94", size = 12896322, upload-time = "2025-07-31T07:53:50.74Z" }, + { url = "https://files.pythonhosted.org/packages/a3/87/8e3e9c2c8bd0d7e071a89c71be28ad088aaecbadf0454f46a540bda7bca6/mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b", size = 9507962, upload-time = "2025-07-31T07:53:08.431Z" }, + { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, + { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, + { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, + { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, + { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, + { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, + { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, + { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, + { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, + { url = "https://files.pythonhosted.org/packages/38/56/79c2fac86da57c7d8c48622a05873eaab40b905096c33597462713f5af90/mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733", size = 11040037, upload-time = "2025-07-31T07:54:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c3/adabe6ff53638e3cad19e3547268482408323b1e68bf082c9119000cd049/mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd", size = 10131550, upload-time = "2025-07-31T07:53:41.307Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c5/2e234c22c3bdeb23a7817af57a58865a39753bde52c74e2c661ee0cfc640/mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0", size = 11872963, upload-time = "2025-07-31T07:53:16.878Z" }, + { url = "https://files.pythonhosted.org/packages/ab/26/c13c130f35ca8caa5f2ceab68a247775648fdcd6c9a18f158825f2bc2410/mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a", size = 12710189, upload-time = "2025-07-31T07:54:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/c7d79d09f6de8383fe800521d066d877e54d30b4fb94281c262be2df84ef/mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91", size = 12900322, upload-time = "2025-07-31T07:53:10.551Z" }, + { url = "https://files.pythonhosted.org/packages/b8/98/3d5a48978b4f708c55ae832619addc66d677f6dc59f3ebad71bae8285ca6/mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed", size = 9751879, upload-time = "2025-07-31T07:52:56.683Z" }, + { url = "https://files.pythonhosted.org/packages/29/cb/673e3d34e5d8de60b3a61f44f80150a738bff568cd6b7efb55742a605e98/mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9", size = 10992466, upload-time = "2025-07-31T07:53:57.574Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d0/fe1895836eea3a33ab801561987a10569df92f2d3d4715abf2cfeaa29cb2/mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99", size = 10117638, upload-time = "2025-07-31T07:53:34.256Z" }, + { url = "https://files.pythonhosted.org/packages/97/f3/514aa5532303aafb95b9ca400a31054a2bd9489de166558c2baaeea9c522/mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8", size = 11915673, upload-time = "2025-07-31T07:52:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c3/c0805f0edec96fe8e2c048b03769a6291523d509be8ee7f56ae922fa3882/mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8", size = 12649022, upload-time = "2025-07-31T07:53:45.92Z" }, + { url = "https://files.pythonhosted.org/packages/45/3e/d646b5a298ada21a8512fa7e5531f664535a495efa672601702398cea2b4/mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259", size = 12895536, upload-time = "2025-07-31T07:53:06.17Z" }, + { url = "https://files.pythonhosted.org/packages/14/55/e13d0dcd276975927d1f4e9e2ec4fd409e199f01bdc671717e673cc63a22/mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d", size = 9512564, upload-time = "2025-07-31T07:53:12.346Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "pytest-env" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/31/27f28431a16b83cab7a636dce59cf397517807d247caa38ee67d65e71ef8/pytest_env-1.1.5.tar.gz", hash = "sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf", size = 8911, upload-time = "2024-09-17T22:39:18.566Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30", size = 6141, upload-time = "2024-09-17T22:39:16.942Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/45/2e403fa7007816b5fbb324cb4f8ed3c7402a927a0a0cb2b6279879a8bfdc/ruff-0.12.9.tar.gz", hash = "sha256:fbd94b2e3c623f659962934e52c2bea6fc6da11f667a427a368adaf3af2c866a", size = 5254702, upload-time = "2025-08-14T16:08:55.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/20/53bf098537adb7b6a97d98fcdebf6e916fcd11b2e21d15f8c171507909cc/ruff-0.12.9-py3-none-linux_armv6l.whl", hash = "sha256:fcebc6c79fcae3f220d05585229463621f5dbf24d79fdc4936d9302e177cfa3e", size = 11759705, upload-time = "2025-08-14T16:08:12.968Z" }, + { url = "https://files.pythonhosted.org/packages/20/4d/c764ee423002aac1ec66b9d541285dd29d2c0640a8086c87de59ebbe80d5/ruff-0.12.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aed9d15f8c5755c0e74467731a007fcad41f19bcce41cd75f768bbd687f8535f", size = 12527042, upload-time = "2025-08-14T16:08:16.54Z" }, + { url = "https://files.pythonhosted.org/packages/8b/45/cfcdf6d3eb5fc78a5b419e7e616d6ccba0013dc5b180522920af2897e1be/ruff-0.12.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b15ea354c6ff0d7423814ba6d44be2807644d0c05e9ed60caca87e963e93f70", size = 11724457, upload-time = "2025-08-14T16:08:18.686Z" }, + { url = "https://files.pythonhosted.org/packages/72/e6/44615c754b55662200c48bebb02196dbb14111b6e266ab071b7e7297b4ec/ruff-0.12.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d596c2d0393c2502eaabfef723bd74ca35348a8dac4267d18a94910087807c53", size = 11949446, upload-time = "2025-08-14T16:08:21.059Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d1/9b7d46625d617c7df520d40d5ac6cdcdf20cbccb88fad4b5ecd476a6bb8d/ruff-0.12.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b15599931a1a7a03c388b9c5df1bfa62be7ede6eb7ef753b272381f39c3d0ff", size = 11566350, upload-time = "2025-08-14T16:08:23.433Z" }, + { url = "https://files.pythonhosted.org/packages/59/20/b73132f66f2856bc29d2d263c6ca457f8476b0bbbe064dac3ac3337a270f/ruff-0.12.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d02faa2977fb6f3f32ddb7828e212b7dd499c59eb896ae6c03ea5c303575756", size = 13270430, upload-time = "2025-08-14T16:08:25.837Z" }, + { url = "https://files.pythonhosted.org/packages/a2/21/eaf3806f0a3d4c6be0a69d435646fba775b65f3f2097d54898b0fd4bb12e/ruff-0.12.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:17d5b6b0b3a25259b69ebcba87908496e6830e03acfb929ef9fd4c58675fa2ea", size = 14264717, upload-time = "2025-08-14T16:08:27.907Z" }, + { url = "https://files.pythonhosted.org/packages/d2/82/1d0c53bd37dcb582b2c521d352fbf4876b1e28bc0d8894344198f6c9950d/ruff-0.12.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72db7521860e246adbb43f6ef464dd2a532ef2ef1f5dd0d470455b8d9f1773e0", size = 13684331, upload-time = "2025-08-14T16:08:30.352Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2f/1c5cf6d8f656306d42a686f1e207f71d7cebdcbe7b2aa18e4e8a0cb74da3/ruff-0.12.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a03242c1522b4e0885af63320ad754d53983c9599157ee33e77d748363c561ce", size = 12739151, upload-time = "2025-08-14T16:08:32.55Z" }, + { url = "https://files.pythonhosted.org/packages/47/09/25033198bff89b24d734e6479e39b1968e4c992e82262d61cdccaf11afb9/ruff-0.12.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc83e4e9751e6c13b5046d7162f205d0a7bac5840183c5beebf824b08a27340", size = 12954992, upload-time = "2025-08-14T16:08:34.816Z" }, + { url = "https://files.pythonhosted.org/packages/52/8e/d0dbf2f9dca66c2d7131feefc386523404014968cd6d22f057763935ab32/ruff-0.12.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:881465ed56ba4dd26a691954650de6ad389a2d1fdb130fe51ff18a25639fe4bb", size = 12899569, upload-time = "2025-08-14T16:08:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b614d7c08515b1428ed4d3f1d4e3d687deffb2479703b90237682586fa66/ruff-0.12.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43f07a3ccfc62cdb4d3a3348bf0588358a66da756aa113e071b8ca8c3b9826af", size = 11751983, upload-time = "2025-08-14T16:08:39.314Z" }, + { url = "https://files.pythonhosted.org/packages/58/d6/383e9f818a2441b1a0ed898d7875f11273f10882f997388b2b51cb2ae8b5/ruff-0.12.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07adb221c54b6bba24387911e5734357f042e5669fa5718920ee728aba3cbadc", size = 11538635, upload-time = "2025-08-14T16:08:41.297Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/56f869d314edaa9fc1f491706d1d8a47747b9d714130368fbd69ce9024e9/ruff-0.12.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f5cd34fabfdea3933ab85d72359f118035882a01bff15bd1d2b15261d85d5f66", size = 12534346, upload-time = "2025-08-14T16:08:43.39Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4b/d8b95c6795a6c93b439bc913ee7a94fda42bb30a79285d47b80074003ee7/ruff-0.12.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6be1d2ca0686c54564da8e7ee9e25f93bdd6868263805f8c0b8fc6a449db6d7", size = 13017021, upload-time = "2025-08-14T16:08:45.889Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c1/5f9a839a697ce1acd7af44836f7c2181cdae5accd17a5cb85fcbd694075e/ruff-0.12.9-py3-none-win32.whl", hash = "sha256:cc7a37bd2509974379d0115cc5608a1a4a6c4bff1b452ea69db83c8855d53f93", size = 11734785, upload-time = "2025-08-14T16:08:48.062Z" }, + { url = "https://files.pythonhosted.org/packages/fa/66/cdddc2d1d9a9f677520b7cfc490d234336f523d4b429c1298de359a3be08/ruff-0.12.9-py3-none-win_amd64.whl", hash = "sha256:6fb15b1977309741d7d098c8a3cb7a30bc112760a00fb6efb7abc85f00ba5908", size = 12840654, upload-time = "2025-08-14T16:08:50.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fd/669816bc6b5b93b9586f3c1d87cd6bc05028470b3ecfebb5938252c47a35/ruff-0.12.9-py3-none-win_arm64.whl", hash = "sha256:63c8c819739d86b96d500cce885956a1a48ab056bbcbc61b747ad494b2485089", size = 11949623, upload-time = "2025-08-14T16:08:52.233Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] diff --git a/taskfile.yaml b/taskfile.yaml index 874af69cf0..c01dda1d7b 100644 --- a/taskfile.yaml +++ b/taskfile.yaml @@ -8,6 +8,7 @@ includes: docker-images: "taskfiles/docker-images.yaml" docs: "taskfiles/docs.yaml" lint: "taskfiles/lint.yaml" + tests: "taskfiles/tests/main.yaml" utils: "tools/yscope-dev-utils/exports/taskfiles/utils/utils.yaml" vars: @@ -63,6 +64,7 @@ tasks: vars: COMPONENT: "job-orchestration" - task: "clean-webui" + - task: "tests:integration:cache-clear" clean-core: cmds: @@ -605,6 +607,7 @@ tasks: vars: CHECKSUM_FILE: "{{.CHECKSUM_FILE}}" INCLUDE_PATTERNS: ["{{.OUTPUT_DIR}}"] + clean-python-component: internal: true label: "clean-{{.COMPONENT}}" diff --git a/taskfiles/lint.yaml b/taskfiles/lint.yaml index 758f1b15ee..c1abcb5626 100644 --- a/taskfiles/lint.yaml +++ b/taskfiles/lint.yaml @@ -78,14 +78,16 @@ tasks: - task: "py" vars: BLACK_FLAGS: "--check --diff" - RUFF_FLAGS: "" + RUFF_CHECK_FLAGS: "" + RUFF_FORMAT_FLAGS: "--diff" fix-py: cmds: - task: "py" vars: BLACK_FLAGS: "" - RUFF_FLAGS: "--fix" + RUFF_CHECK_FLAGS: "--fix" + RUFF_FORMAT_FLAGS: "" yaml: aliases: @@ -757,8 +759,11 @@ tasks: py: internal: true + vars: + UV_PYTHON_PROJECTS: + - "{{.G_INTEGRATION_TESTS_DIR}}" requires: - vars: ["BLACK_FLAGS", "RUFF_FLAGS"] + vars: ["BLACK_FLAGS", "RUFF_CHECK_FLAGS", "RUFF_FORMAT_FLAGS"] deps: ["venv"] cmds: - for: @@ -773,7 +778,13 @@ tasks: . "{{.G_LINT_VENV_DIR}}/bin/activate" cd "{{.ITEM}}" black --color --line-length 100 {{.BLACK_FLAGS}} . - ruff check {{.RUFF_FLAGS}} . + ruff check {{.RUFF_CHECK_FLAGS}} . + - for: + var: "UV_PYTHON_PROJECTS" + cmd: |- + uv run --directory "{{.ITEM}}" ruff format {{.RUFF_FORMAT_FLAGS}} . + uv run --directory "{{.ITEM}}" ruff check {{.RUFF_CHECK_FLAGS}} . + uv run --directory "{{.ITEM}}" mypy . venv: internal: true diff --git a/taskfiles/tests/integration.yaml b/taskfiles/tests/integration.yaml new file mode 100644 index 0000000000..823c6853b0 --- /dev/null +++ b/taskfiles/tests/integration.yaml @@ -0,0 +1,27 @@ +version: "3" + +vars: + G_INTEGRATION_TESTS_DIR: "{{.ROOT_DIR}}/integration-tests" + +tasks: + default: + deps: + - task: "core" + + cache-clear: + dir: "{{.G_INTEGRATION_TESTS_DIR}}" + cmds: + - >- + uv run python -m pytest --cache-clear --collect-only --override-ini addopts="" --quiet + > /dev/null + + core: + deps: + # TODO: https://github.com/y-scope/clp/issues/1281 + - task: "::package" + dir: "{{.G_INTEGRATION_TESTS_DIR}}" + env: + CLP_BUILD_DIR: "{{.G_BUILD_DIR}}" + CLP_CORE_BINS_DIR: "{{.G_CORE_COMPONENT_BUILD_DIR}}" + CLP_PACKAGE_DIR: "{{.G_PACKAGE_BUILD_DIR}}" + cmd: "uv run python -m pytest -m core" diff --git a/taskfiles/tests/main.yaml b/taskfiles/tests/main.yaml new file mode 100644 index 0000000000..9f561314ea --- /dev/null +++ b/taskfiles/tests/main.yaml @@ -0,0 +1,4 @@ +version: "3" + +includes: + integration: "integration.yaml"