From 66a08016f9f009ad351ce37c06c712dbab601df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mari=C3=A1n=20Kr=C4=8Dm=C3=A1rik?= Date: Thu, 28 May 2026 09:13:47 +0200 Subject: [PATCH] feat: add MTV operator upgrade/update migration testing (#487) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add MTV operator upgrade/update migration testing Add upgrade test infrastructure: - test_upgrade_migration.py: incremental test class that creates migration resources, upgrades MTV operator, verifies post-upgrade state, and runs cold migration - utilities/upgrade.py: subprocess-based upgrade runner with fd 3 support for mtv-autodeploy logging, plus pod readiness and plan verification - Add @pytest.mark.upgrade to warm migration for post-upgrade warm test - Remove tier0 from cold migration (covered by upgrade test) - Add upgrade_script_path, mtv_upgrade_to_version, mtv_upgrade_to_source, mtv_upgrade_image_index config parameters - Add 'upgrade' marker to pytest.ini - Add MtvUpgradeError and ForkliftPodsNotRunningError exceptions * fix: complete Generator type hint and remove unused fixture parameter * fix: validate upgrade script path exists before execution * Address PR #487 review comments for upgrade tests Remove redundant code and simplify upgrade test implementation: - Remove redundant wait_for_forklift_pods_ready call from post-upgrade test (upgrade script already handles this) - Remove test_verify_pre_upgrade_version test method (redundant with existing session-level validation) - Replace verify_plan_ready_after_upgrade() with standard plan.wait_for_condition() pattern - Remove _duplicate_stdout_to_fd3() fd3 hack, simplify subprocess call - Remove unused functions and their imports from utilities/upgrade.py * fix: add timeout guard to MTV upgrade subprocess Bound the upgrade script execution with a 1-hour timeout to prevent indefinite hangs. Maps TimeoutExpired to MtvUpgradeError. * refactor: clone upgrade repo at test time via fixture Replace CI-provided local script path with a session-scoped fixture that shallow-clones the upgrade repo to tmp_path. Config now takes repo URL, ref, and relative script path instead of an absolute path. * refactor: use gitpython for upgrade repo clone Replace subprocess git clone with Repo.clone_from() and simplify clone directory setup by removing unnecessary subdirectory. * refactor: move upgrade resource creation to fixtures and remove config placeholders Move StorageMap, NetworkMap, and Plan creation from test methods into class-scoped fixtures in conftest.py. The empty-string config placeholders (upgrade_repo_url, upgrade_repo_ref, etc.) were runtime-only --tc parameters, not test data — remove them and use .get() with validation in the fixture instead. * refactor: rename upgrade fixtures to pre_upgrade_* for clarity * add credential redaction, git runtime dep, and upgrade docs Redact sensitive fields (password, token, thumbprint) in upgrade log output. Add git package to Dockerfile runtime stage for upgrade test branch operations. Document upgrade test usage in README. --- README.md | 44 ++++++ exceptions/exceptions.py | 4 + pyproject.toml | 1 + pytest.ini | 1 + tests/tests_config/config.py | 27 +++- tests/upgrade/__init__.py | 0 tests/upgrade/conftest.py | 185 ++++++++++++++++++++++++ tests/upgrade/test_upgrade_migration.py | 129 +++++++++++++++++ tests/warm/test_mtv_warm_migration.py | 1 + utilities/upgrade.py | 75 ++++++++++ uv.lock | 35 +++++ 11 files changed, 501 insertions(+), 1 deletion(-) create mode 100644 tests/upgrade/__init__.py create mode 100644 tests/upgrade/conftest.py create mode 100644 tests/upgrade/test_upgrade_migration.py create mode 100644 utilities/upgrade.py diff --git a/README.md b/README.md index 14c7ba7a..426119e2 100644 --- a/README.md +++ b/README.md @@ -257,6 +257,7 @@ The Quick Start runs **tier0** tests (smoke tests). You can run other test categ | `copyoffload` | Fast migrations via shared storage | Testing storage arrays | | `copyoffload_sanity` | Copy-offload sanity subset (see below) | Quick copy-offload validation | | `warm` | Warm migrations (VMs stay running) | Specific scenario testing | +| `upgrade` | Migration across MTV operator upgrades | Validating upgrade compatibility | ### Copy-Offload Sanity Tests @@ -629,6 +630,49 @@ Configure pod affinity/anti-affinity and node affinity rules. --- +## Upgrade Tests + +Upgrade tests validate that migration resources created on a pre-upgrade MTV version survive the +operator upgrade and that migrations execute successfully afterward. The test flow is: + +1. Create migration resources (StorageMap, NetworkMap, Plan) on the current MTV version +2. Upgrade the MTV operator to the target version using an external upgrade script +3. Verify the upgraded operator version and that existing Plan CRs remain in Ready state +4. Execute the migration on the upgraded operator +5. Validate the migrated VMs + +### Required `--tc` Parameters + +Upgrade tests require additional `--tc` parameters beyond the standard cluster and provider options: + +| Parameter | Required | Description | +| --------- | -------- | ----------- | +| `upgrade_repo_url` | Yes | URL of the Git repository containing the upgrade script | +| `upgrade_repo_ref` | Yes | Git ref (branch or tag) to checkout | +| `upgrade_script_path` | Yes | Relative path to the upgrade script within the repository | +| `mtv_upgrade_to_version` | Yes | Target MTV version to upgrade to (e.g., `2.7.0`) | +| `mtv_upgrade_to_source` | Yes | MTV source identifier (e.g., `brew`, `released`) | +| `mtv_upgrade_image_index` | No | Image index override (pass empty string `""` to skip) | + +### Example + +```bash +uv run pytest -m upgrade -v \ + --tc=cluster_host:https://api.your-cluster.com:6443 \ + --tc=cluster_username:kubeadmin \ + --tc=cluster_password:${CLUSTER_PASSWORD} \ + --tc=source_provider:vsphere-8.0.1 \ + --tc=storage_class:YOUR-STORAGE-CLASS \ + --tc=upgrade_repo_url:https://github.com/org/mtv-autodeploy.git \ + --tc=upgrade_repo_ref:main \ + --tc=upgrade_script_path:scripts/upgrade.sh \ + --tc=mtv_upgrade_to_version:2.7.0 \ + --tc=mtv_upgrade_to_source:released \ + --tc=mtv_upgrade_image_index:"" +``` + +--- + ## Useful Test Options ### Debug and Troubleshooting Flags diff --git a/exceptions/exceptions.py b/exceptions/exceptions.py index 64b0f533..763ae32d 100644 --- a/exceptions/exceptions.py +++ b/exceptions/exceptions.py @@ -125,3 +125,7 @@ class InvalidVMNameError(Exception): class GuestCommandError(Exception): """Raised when a command executed via VMware Guest Operations exits with non-zero code.""" + + +class MtvUpgradeError(Exception): + """Raised when MTV operator upgrade fails.""" diff --git a/pyproject.toml b/pyproject.toml index aeaebe0c..f63895ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,4 +64,5 @@ dependencies = [ "filelock>=3.16.0", "python-dotenv>=1.2.1", "requests>=2.32.5", + "gitpython>=3.1.50", ] diff --git a/pytest.ini b/pytest.ini index 123969eb..b4f7c814 100644 --- a/pytest.ini +++ b/pytest.ini @@ -28,5 +28,6 @@ markers = rhv: RHV provider-specific tests openstack: OpenStack provider-specific tests openshift: OpenShift provider-specific tests + upgrade: MTV operator upgrade tests junit_logging = all diff --git a/tests/tests_config/config.py b/tests/tests_config/config.py index cb19cd5d..2692429e 100644 --- a/tests/tests_config/config.py +++ b/tests/tests_config/config.py @@ -13,7 +13,6 @@ snapshots_interval: int = 2 mins_before_cutover: int = 5 plan_wait_timeout: int = 3600 - tests_params: dict = { "test_sanity_warm_mtv_migration": { "virtual_machines": [ @@ -606,6 +605,32 @@ "post_hook": {"expected_result": "fail"}, "expected_migration_result": "fail", }, + "test_shared_disk_rhel_migration": { + "virtual_machines": [ + { + "name": "mtv-feature-shared-rhel1", + "source_vm_power": "off", + "guest_agent": True, + "migrate_shared_disks": True, + }, + { + "name": "mtv-feature-shared-rhel2", + "source_vm_power": "off", + "guest_agent": True, + "migrate_shared_disks": False, + }, + ], + "warm_migration": False, + "migrate_shared_disks": True, + "shared_disk_device": "/dev/vdc", + "target_power_state": "on", + }, + "test_upgrade_cold_migration": { + "virtual_machines": [ + {"name": "mtv-tests-rhel8", "guest_agent": True}, + ], + "warm_migration": False, + }, } for _dir in dir(): diff --git a/tests/upgrade/__init__.py b/tests/upgrade/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/upgrade/conftest.py b/tests/upgrade/conftest.py new file mode 100644 index 00000000..41f4a6af --- /dev/null +++ b/tests/upgrade/conftest.py @@ -0,0 +1,185 @@ +from __future__ import annotations + +import stat +from typing import TYPE_CHECKING, Any + +import pytest +from git import Repo +from ocp_resources.network_map import NetworkMap +from ocp_resources.plan import Plan +from ocp_resources.storage_map import StorageMap +from pytest_testconfig import config as py_config +from simple_logger.logger import get_logger + +from utilities.mtv_migration import ( + create_plan_resource, + get_network_migration_map, + get_storage_migration_map, +) +from utilities.utils import populate_vm_ids + +if TYPE_CHECKING: + from kubernetes.dynamic import DynamicClient + + from libs.base_provider import BaseProvider + from libs.forklift_inventory import ForkliftInventory + from libs.providers.openshift import OCPProvider + +LOGGER = get_logger(name=__name__) + + +@pytest.fixture(scope="session") +def upgrade_script_path(tmp_path_factory: pytest.TempPathFactory) -> str: + """Clone the mtv-autodeploy repo and return the absolute path to the upgrade script. + + Args: + tmp_path_factory (pytest.TempPathFactory): Pytest factory for session-scoped temporary directories. + + Returns: + str: Absolute path to the executable upgrade script. + + Raises: + ValueError: If required config values are missing or the script is not found after cloning. + git.exc.GitCommandError: If the git clone fails. + """ + repo_url: str = py_config.get("upgrade_repo_url", "") + repo_ref: str = py_config.get("upgrade_repo_ref", "") + script_relative_path: str = py_config.get("upgrade_script_path", "") + + if not repo_url: + raise ValueError("upgrade_repo_url must be provided via --tc=upgrade_repo_url:") + if not repo_ref: + raise ValueError("upgrade_repo_ref must be provided via --tc=upgrade_repo_ref:") + if not script_relative_path: + raise ValueError("upgrade_script_path must be provided via --tc=upgrade_script_path:") + + clone_dir = tmp_path_factory.mktemp("mtv-autodeploy") + + LOGGER.info(f"Cloning {repo_url} (ref={repo_ref}) into {clone_dir}") + Repo.clone_from(url=repo_url, to_path=str(clone_dir), branch=repo_ref, depth=1) + + script_path = clone_dir / script_relative_path + if not script_path.is_file(): + raise ValueError(f"Upgrade script not found at '{script_path}' after cloning {repo_url}") + + current_mode = script_path.stat().st_mode + script_path.chmod(current_mode | stat.S_IXUSR) + + return str(script_path) + + +@pytest.fixture(scope="class") +def pre_upgrade_storage_map( + prepared_plan: dict[str, Any], + fixture_store: dict[str, Any], + ocp_admin_client: DynamicClient, + source_provider: BaseProvider, + destination_provider: BaseProvider, + source_provider_inventory: ForkliftInventory, + target_namespace: str, +) -> StorageMap: + """Create StorageMap resource for upgrade migration tests. + + Args: + prepared_plan (dict[str, Any]): Test plan configuration with VM details. + fixture_store (dict[str, Any]): Resource tracking dictionary. + ocp_admin_client (DynamicClient): OpenShift admin client. + source_provider (BaseProvider): Source provider connection. + destination_provider (BaseProvider): Destination provider. + source_provider_inventory (ForkliftInventory): Provider inventory. + target_namespace (str): Target namespace. + + Returns: + StorageMap: The created StorageMap resource. + """ + vms: list[str] = [vm["name"] for vm in prepared_plan["virtual_machines"]] + return get_storage_migration_map( + fixture_store=fixture_store, + source_provider=source_provider, + destination_provider=destination_provider, + source_provider_inventory=source_provider_inventory, + ocp_admin_client=ocp_admin_client, + target_namespace=target_namespace, + vms=vms, + ) + + +@pytest.fixture(scope="class") +def pre_upgrade_network_map( + prepared_plan: dict[str, Any], + fixture_store: dict[str, Any], + ocp_admin_client: DynamicClient, + source_provider: BaseProvider, + destination_provider: BaseProvider, + source_provider_inventory: ForkliftInventory, + target_namespace: str, + multus_network_name: dict[str, str], +) -> NetworkMap: + """Create NetworkMap resource for upgrade migration tests. + + Args: + prepared_plan (dict[str, Any]): Test plan configuration with VM details. + fixture_store (dict[str, Any]): Resource tracking dictionary. + ocp_admin_client (DynamicClient): OpenShift admin client. + source_provider (BaseProvider): Source provider connection. + destination_provider (BaseProvider): Destination provider. + source_provider_inventory (ForkliftInventory): Provider inventory. + target_namespace (str): Target namespace. + multus_network_name (dict[str, str]): Multus network name mapping. + + Returns: + NetworkMap: The created NetworkMap resource. + """ + vms: list[str] = [vm["name"] for vm in prepared_plan["virtual_machines"]] + return get_network_migration_map( + fixture_store=fixture_store, + source_provider=source_provider, + destination_provider=destination_provider, + source_provider_inventory=source_provider_inventory, + ocp_admin_client=ocp_admin_client, + target_namespace=target_namespace, + multus_network_name=multus_network_name, + vms=vms, + ) + + +@pytest.fixture(scope="class") +def pre_upgrade_plan_resource( + prepared_plan: dict[str, Any], + fixture_store: dict[str, Any], + ocp_admin_client: DynamicClient, + source_provider: BaseProvider, + destination_provider: OCPProvider, + source_provider_inventory: ForkliftInventory, + target_namespace: str, + pre_upgrade_storage_map: StorageMap, + pre_upgrade_network_map: NetworkMap, +) -> Plan: + """Create MTV Plan CR resource for upgrade migration tests. + + Args: + prepared_plan (dict[str, Any]): Test plan configuration with VM details. + fixture_store (dict[str, Any]): Resource tracking dictionary. + ocp_admin_client (DynamicClient): OpenShift admin client. + source_provider (BaseProvider): Source provider connection. + destination_provider (OCPProvider): Destination provider. + source_provider_inventory (ForkliftInventory): Provider inventory. + target_namespace (str): Target namespace. + pre_upgrade_storage_map (StorageMap): Storage map for the migration. + pre_upgrade_network_map (NetworkMap): Network map for the migration. + + Returns: + Plan: The created Plan CR resource. + """ + populate_vm_ids(prepared_plan, source_provider_inventory) + return create_plan_resource( + ocp_admin_client=ocp_admin_client, + fixture_store=fixture_store, + source_provider=source_provider, + destination_provider=destination_provider, + storage_map=pre_upgrade_storage_map, + network_map=pre_upgrade_network_map, + virtual_machines_list=prepared_plan["virtual_machines"], + target_namespace=target_namespace, + warm_migration=prepared_plan.get("warm_migration", False), + ) diff --git a/tests/upgrade/test_upgrade_migration.py b/tests/upgrade/test_upgrade_migration.py new file mode 100644 index 00000000..4448694a --- /dev/null +++ b/tests/upgrade/test_upgrade_migration.py @@ -0,0 +1,129 @@ +""" +Upgrade migration tests for MTV. + +This module implements tests that validate migration behavior across MTV +operator upgrades. The test flow creates migration resources (StorageMap, +NetworkMap, Plan) on the pre-upgrade version, upgrades the MTV operator, +verifies post-upgrade state, and then executes the migration. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import pytest +from ocp_resources.network_map import NetworkMap +from ocp_resources.plan import Plan +from ocp_resources.storage_map import StorageMap +from packaging.version import InvalidVersion, Version +from pytest_testconfig import config as py_config +from simple_logger.logger import get_logger + +from utilities.mtv_migration import execute_migration +from utilities.post_migration import check_vms +from utilities.upgrade import run_mtv_upgrade +from utilities.utils import get_mtv_version + +if TYPE_CHECKING: + from kubernetes.dynamic import DynamicClient + + from libs.base_provider import BaseProvider + from libs.forklift_inventory import ForkliftInventory + from utilities.ssh_utils import SSHConnectionManager + +LOGGER = get_logger(name=__name__) + + +@pytest.mark.upgrade +@pytest.mark.vsphere +@pytest.mark.incremental +@pytest.mark.parametrize( + "class_plan_config", + [ + pytest.param( + py_config["tests_params"]["test_upgrade_cold_migration"], + ) + ], + indirect=True, + ids=["upgrade-cold"], +) +@pytest.mark.usefixtures("cleanup_migrated_vms") +class TestUpgradeColdMigration: + """Cold migration with MTV operator upgrade between plan creation and execution.""" + + def test_upgrade_mtv(self, upgrade_script_path: str, pre_upgrade_plan_resource: Plan) -> None: + """Upgrade the MTV operator to the target version.""" + run_mtv_upgrade( + script_path=upgrade_script_path, + mtv_version=py_config["mtv_upgrade_to_version"], + mtv_source=py_config["mtv_upgrade_to_source"], + image_index=py_config["mtv_upgrade_image_index"], + ) + + def test_verify_post_upgrade( + self, + ocp_admin_client: DynamicClient, + pre_upgrade_plan_resource: Plan, + ) -> None: + """Verify MTV version, pod readiness, and Plan CR state after upgrade.""" + version = get_mtv_version(client=ocp_admin_client) + LOGGER.info(f"Post-upgrade MTV version: {version}") + + expected_version: str = py_config["mtv_upgrade_to_version"] + try: + actual = Version(version) + expected = Version(expected_version) + except InvalidVersion as exc: + raise AssertionError( + f"Could not parse MTV version strings: actual='{version}', expected='{expected_version}'" + ) from exc + assert (actual.major, actual.minor) == (expected.major, expected.minor), ( + f"Expected MTV major.minor version '{expected.major}.{expected.minor}', got '{version}'" + ) + + pre_upgrade_plan_resource.wait_for_condition( + condition=Plan.Condition.READY, + status=Plan.Condition.Status.TRUE, + timeout=300, + ) + LOGGER.info(f"Plan '{pre_upgrade_plan_resource.name}' is in Ready state after upgrade") + + def test_migrate_vms( + self, + fixture_store: dict[str, Any], + ocp_admin_client: DynamicClient, + target_namespace: str, + pre_upgrade_plan_resource: Plan, + ) -> None: + """Execute migration on the upgraded MTV operator.""" + execute_migration( + ocp_admin_client=ocp_admin_client, + fixture_store=fixture_store, + plan=pre_upgrade_plan_resource, + target_namespace=target_namespace, + ) + + def test_check_vms( + self, + prepared_plan: dict[str, Any], + source_provider: BaseProvider, + destination_provider: BaseProvider, + source_provider_data: dict[str, Any], + source_vms_namespace: str, + source_provider_inventory: ForkliftInventory, + vm_ssh_connections: SSHConnectionManager | None, + pre_upgrade_network_map: NetworkMap, + pre_upgrade_storage_map: StorageMap, + ) -> None: + """Validate migrated VMs after upgrade.""" + check_vms( + plan=prepared_plan, + source_provider=source_provider, + destination_provider=destination_provider, + network_map_resource=pre_upgrade_network_map, + storage_map_resource=pre_upgrade_storage_map, + source_provider_data=source_provider_data, + source_vms_namespace=source_vms_namespace, + source_provider_inventory=source_provider_inventory, + vm_ssh_connections=vm_ssh_connections, + ) diff --git a/tests/warm/test_mtv_warm_migration.py b/tests/warm/test_mtv_warm_migration.py index b8d72fca..657f202f 100644 --- a/tests/warm/test_mtv_warm_migration.py +++ b/tests/warm/test_mtv_warm_migration.py @@ -19,6 +19,7 @@ @pytest.mark.rhv @pytest.mark.tier0 @pytest.mark.warm +@pytest.mark.upgrade @pytest.mark.incremental @pytest.mark.parametrize( "class_plan_config", diff --git a/utilities/upgrade.py b/utilities/upgrade.py new file mode 100644 index 00000000..092e0223 --- /dev/null +++ b/utilities/upgrade.py @@ -0,0 +1,75 @@ +""" +Upgrade utilities for MTV operator upgrade tests. + +This module provides functions to run the MTV upgrade process. +""" + +from __future__ import annotations + +import os +import subprocess + +from pytest_testconfig import config as py_config +from simple_logger.logger import get_logger + +from exceptions.exceptions import MtvUpgradeError + +LOGGER = get_logger(name=__name__) + + +def run_mtv_upgrade( + script_path: str, + mtv_version: str, + mtv_source: str, + image_index: str = "", +) -> None: + """Run the MTV operator upgrade using the specified upgrade script. + + Args: + script_path (str): Full path to the upgrade script. + mtv_version (str): Target MTV version to upgrade to. + mtv_source (str): MTV source identifier (e.g., "brew", "released"). + image_index (str): Optional image index override for the upgrade. + + Raises: + MtvUpgradeError: If the upgrade script exits with a non-zero status or times out. + """ + env = os.environ.copy() + env.update({ + "MTV_VERSION": mtv_version, + "MTV_SOURCE": mtv_source.upper(), + "IMAGE_INDEX": image_index, + "CLUSTER_USERNAME": py_config["cluster_username"], + "CLUSTER_PASSWORD": py_config["cluster_password"], + "CLUSTER_API_URL": py_config["cluster_host"], + }) + + LOGGER.info(f"Running MTV upgrade: {script_path} (version={mtv_version}, source={mtv_source}, index={image_index})") + + try: + result = subprocess.run( + [script_path], + env=env, + cwd=os.path.dirname(script_path), + check=True, + timeout=3600, + capture_output=True, + ) + except subprocess.TimeoutExpired as exc: + stdout = (exc.stdout.decode() if exc.stdout else "").replace(env["CLUSTER_PASSWORD"], "***") + stderr = (exc.stderr.decode() if exc.stderr else "").replace(env["CLUSTER_PASSWORD"], "***") + raise MtvUpgradeError( + f"MTV upgrade script timed out after {exc.timeout} seconds\nstdout: {stdout}\nstderr: {stderr}" + ) from exc + except subprocess.CalledProcessError as exc: + stdout = (exc.stdout.decode() if exc.stdout else "").replace(env["CLUSTER_PASSWORD"], "***") + stderr = (exc.stderr.decode() if exc.stderr else "").replace(env["CLUSTER_PASSWORD"], "***") + raise MtvUpgradeError( + f"MTV upgrade script failed with exit code {exc.returncode}\nstdout: {stdout}\nstderr: {stderr}" + ) from exc + + LOGGER.info(f"MTV upgrade stdout:\n{result.stdout.decode().replace(env['CLUSTER_PASSWORD'], '***')}") + if result.stderr: + LOGGER.warning(f"MTV upgrade stderr:\n{result.stderr.decode().replace(env['CLUSTER_PASSWORD'], '***')}") + + LOGGER.info(f"MTV upgrade to version {mtv_version} completed successfully") diff --git a/uv.lock b/uv.lock index 12b611a8..5abf60fb 100644 --- a/uv.lock +++ b/uv.lock @@ -635,6 +635,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, +] + [[package]] name = "gprof2dot" version = "2025.4.14" @@ -1154,6 +1178,7 @@ source = { virtual = "." } dependencies = [ { name = "cloudpickle" }, { name = "filelock" }, + { name = "gitpython" }, { name = "humanfriendly" }, { name = "jc" }, { name = "openshift-python-utilities" }, @@ -1196,6 +1221,7 @@ dev = [ requires-dist = [ { name = "cloudpickle", specifier = ">=3.1.1" }, { name = "filelock", specifier = ">=3.16.0" }, + { name = "gitpython", specifier = ">=3.1.50" }, { name = "humanfriendly", specifier = ">=10.0" }, { name = "jc", specifier = ">=1.25.0" }, { name = "openshift-python-utilities", specifier = ">=6.0.7" }, @@ -2404,6 +2430,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "smmap" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, +] + [[package]] name = "soupsieve" version = "2.8.4"