diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 8c5c9c504e..76f215a4d6 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -7,6 +7,7 @@ on: - main jobs: Check: + timeout-minutes: 40 continue-on-error: ${{ matrix.optional || false }} runs-on: ${{ matrix.os }} name: >- @@ -18,8 +19,16 @@ jobs: fail-fast: false matrix: os: [ubuntu-20.04, windows-2019] - python-version: ['3.9', '3.10'] + python-version: ['3.9', '3.10', '3.11', '3.12-dev'] nox-session: [test, example] + + exclude: + # Azure installation (Win, 3.12) currently hangs due to excessive pip backtracking + # Disable unit tests and retry in 2Q23 + - python-version: '3.12-dev' + os: windows-2019 + nox-session: test + include: - os: ubuntu-latest diff --git a/docs/installation_linux.rst b/docs/installation_linux.rst index cd1b34cb85..3d60372825 100644 --- a/docs/installation_linux.rst +++ b/docs/installation_linux.rst @@ -4,7 +4,7 @@ Install LISA on Linux Minimum System Requirements --------------------------- -1. Your favorite Linux distribution supporting Python 3.8 - 3.10 +1. Your favorite Linux distribution supporting Python 3.8 or above 2. Dual core processor 3. 4 GB system memory @@ -15,8 +15,8 @@ The following commands assume Ubuntu is being used. Install Python on Linux ----------------------- -LISA has been tested to work with `Python 3.8 - 3.10 64-bit `__. -Python 3.10 is recommended. Support for 3.11+ is under development. +LISA has been tested to work with `Python >=3.8 64-bit `__. +Python 3.11 is recommended. If you find that LISA is not compatible with a supported version, `please file an issue `__. @@ -29,14 +29,12 @@ To check which version of Python is used on your system, run the following: If you need to install a different Python package, there are likely packaged versions for your distro. -Here is an example to install Python 3.10 on Ubuntu 20.04 +Here is an example to install Python 3.11 on Ubuntu 22.04 .. code:: bash sudo apt update - sudo apt install software-properties-common -y - sudo add-apt-repository ppa:deadsnakes/ppa -y - sudo apt install python3.10 python3.10-dev -y + sudo apt install python3.11 python3.11-dev -y Install system dependencies diff --git a/docs/installation_windows.rst b/docs/installation_windows.rst index 5aa98049c8..f8cd549020 100644 --- a/docs/installation_windows.rst +++ b/docs/installation_windows.rst @@ -26,7 +26,7 @@ The full installer allows greater customization and doesn't have the security re of the Microsoft Store packages, so may be preferred in some situations. Navigate to `Python releases for Windows `__. -Download and install *Windows installer (64-bit)* for Python 3.8 - 3.10 64-bit. +Download and install *Windows installer (64-bit)* for Python 3.8 64-bit or above. More information on the full installer, including installation without a GUI, can be found `here `_. diff --git a/lisa/environment.py b/lisa/environment.py index bf6fb0415d..daf215471d 100644 --- a/lisa/environment.py +++ b/lisa/environment.py @@ -79,7 +79,7 @@ def _get_environment_id() -> int: class EnvironmentMessage(MessageBase): type: str = "Environment" name: str = "" - runbook: schema.Environment = schema.Environment() + runbook: schema.Environment = field(default_factory=schema.Environment) status: EnvironmentStatus = EnvironmentStatus.New diff --git a/lisa/features/nvme.py b/lisa/features/nvme.py index 3ad1ce4742..adab37854f 100644 --- a/lisa/features/nvme.py +++ b/lisa/features/nvme.py @@ -3,6 +3,7 @@ import re from dataclasses import dataclass, field +from functools import partial from typing import Any, List, Type from dataclasses_json import dataclass_json @@ -93,7 +94,7 @@ def _get_device_from_ls(self, force_run: bool = False) -> None: class NvmeSettings(FeatureSettings): type: str = "Nvme" disk_count: search_space.CountSpace = field( - default=search_space.IntRange(min=0), + default_factory=partial(search_space.IntRange, min=0), metadata=field_metadata(decoder=search_space.decode_count_space), ) diff --git a/lisa/features/security_profile.py b/lisa/features/security_profile.py index 08c88fe559..2ef5e9f720 100644 --- a/lisa/features/security_profile.py +++ b/lisa/features/security_profile.py @@ -66,10 +66,12 @@ def __hash__(self) -> int: def _get_key(self) -> str: return f"{self.type}/{self.security_profile}" - def _call_requirement_method(self, method_name: str, capability: Any) -> Any: + def _call_requirement_method( + self, method: search_space.RequirementMethod, capability: Any + ) -> Any: value = SecurityProfileSettings() value.security_profile = getattr( - search_space, f"{method_name}_setspace_by_priority" + search_space, f"{method.value}_setspace_by_priority" )( self.security_profile, capability.security_profile, diff --git a/lisa/schema.py b/lisa/schema.py index debae5b293..22dd7e935e 100644 --- a/lisa/schema.py +++ b/lisa/schema.py @@ -379,13 +379,15 @@ def check(self, capability: Any) -> search_space.ResultReason: def _get_key(self) -> str: return self.type - def _call_requirement_method(self, method_name: str, capability: Any) -> Any: + def _call_requirement_method( + self, method: search_space.RequirementMethod, capability: Any + ) -> Any: assert isinstance(capability, FeatureSettings), f"actual: {type(capability)}" # default FeatureSetting is a place holder, nothing to do. value = FeatureSettings.create(self.type) # try best to intersect the extended schemas - if method_name == search_space.RequirementMethod.intersect: + if method == search_space.RequirementMethod.intersect: if self.extended_schemas and capability and capability.extended_schemas: value.extended_schemas = deep_update_dict( self.extended_schemas, @@ -533,20 +535,22 @@ def _get_key(self) -> str: f"{self.data_disk_iops}/{self.data_disk_size}" ) - def _call_requirement_method(self, method_name: str, capability: Any) -> Any: + def _call_requirement_method( + self, method: search_space.RequirementMethod, capability: Any + ) -> Any: assert isinstance(capability, DiskOptionSettings), f"actual: {type(capability)}" - parent_value = super()._call_requirement_method(method_name, capability) + parent_value = super()._call_requirement_method(method, capability) # convert parent type to child type value = DiskOptionSettings() value.extended_schemas = parent_value.extended_schemas search_space_countspace_method = getattr( - search_space, f"{method_name}_countspace" + search_space, f"{method.value}_countspace" ) if self.disk_type or capability.disk_type: value.disk_type = getattr( - search_space, f"{method_name}_setspace_by_priority" + search_space, f"{method.value}_setspace_by_priority" )(self.disk_type, capability.disk_type, disk_type_priority) if self.data_disk_count or capability.data_disk_count: value.data_disk_count = search_space_countspace_method( @@ -670,28 +674,30 @@ def check(self, capability: Any) -> search_space.ResultReason: return result - def _call_requirement_method(self, method_name: str, capability: Any) -> Any: + def _call_requirement_method( + self, method: search_space.RequirementMethod, capability: Any + ) -> Any: assert isinstance( capability, NetworkInterfaceOptionSettings ), f"actual: {type(capability)}" - parent_value = super()._call_requirement_method(method_name, capability) + parent_value = super()._call_requirement_method(method, capability) # convert parent type to child type value = NetworkInterfaceOptionSettings() value.extended_schemas = parent_value.extended_schemas - value.max_nic_count = getattr(search_space, f"{method_name}_countspace")( + value.max_nic_count = getattr(search_space, f"{method.value}_countspace")( self.max_nic_count, capability.max_nic_count ) if self.nic_count or capability.nic_count: - value.nic_count = getattr(search_space, f"{method_name}_countspace")( + value.nic_count = getattr(search_space, f"{method.value}_countspace")( self.nic_count, capability.nic_count ) else: raise LisaException("nic_count cannot be zero") - value.data_path = getattr(search_space, f"{method_name}_setspace_by_priority")( + value.data_path = getattr(search_space, f"{method.value}_setspace_by_priority")( self.data_path, capability.data_path, _network_data_path_priority ) return value @@ -723,21 +729,21 @@ class NodeSpace(search_space.RequirementMixin, TypedSchema, ExtendableSchemaMixi name: str = "" is_default: bool = field(default=False) node_count: search_space.CountSpace = field( - default=search_space.IntRange(min=1), + default_factory=partial(search_space.IntRange, min=1), metadata=field_metadata(decoder=search_space.decode_count_space), ) core_count: search_space.CountSpace = field( - default=search_space.IntRange(min=1), + default_factory=partial(search_space.IntRange, min=1), metadata=field_metadata(decoder=search_space.decode_count_space), ) memory_mb: search_space.CountSpace = field( - default=search_space.IntRange(min=512), + default_factory=partial(search_space.IntRange, min=512), metadata=field_metadata(decoder=search_space.decode_count_space), ) disk: Optional[DiskOptionSettings] = None network_interface: Optional[NetworkInterfaceOptionSettings] = None gpu_count: search_space.CountSpace = field( - default=search_space.IntRange(min=0), + default_factory=partial(search_space.IntRange, min=0), metadata=field_metadata(decoder=search_space.decode_count_space), ) # all features on requirement should be included. @@ -926,7 +932,9 @@ def has_feature(self, find_type: str) -> bool: return any(feature for feature in self.features if feature.type == find_type) - def _call_requirement_method(self, method_name: str, capability: Any) -> Any: + def _call_requirement_method( + self, method: search_space.RequirementMethod, capability: Any + ) -> Any: assert isinstance(capability, NodeSpace), f"actual: {type(capability)}" # copy to duplicate extended schema @@ -938,32 +946,32 @@ def _call_requirement_method(self, method_name: str, capability: Any) -> Any: # capability can have more node value.node_count = capability.node_count else: - value.node_count = getattr(search_space, f"{method_name}_countspace")( + value.node_count = getattr(search_space, f"{method.value}_countspace")( self.node_count, capability.node_count ) else: raise LisaException("node_count cannot be zero") if self.core_count or capability.core_count: - value.core_count = getattr(search_space, f"{method_name}_countspace")( + value.core_count = getattr(search_space, f"{method.value}_countspace")( self.core_count, capability.core_count ) else: raise LisaException("core_count cannot be zero") if self.memory_mb or capability.memory_mb: - value.memory_mb = getattr(search_space, f"{method_name}_countspace")( + value.memory_mb = getattr(search_space, f"{method.value}_countspace")( self.memory_mb, capability.memory_mb ) else: raise LisaException("memory_mb cannot be zero") if self.disk or capability.disk: - value.disk = getattr(search_space, method_name)(self.disk, capability.disk) + value.disk = getattr(search_space, method.value)(self.disk, capability.disk) if self.network_interface or capability.network_interface: - value.network_interface = getattr(search_space, method_name)( + value.network_interface = getattr(search_space, method.value)( self.network_interface, capability.network_interface ) if self.gpu_count or capability.gpu_count: - value.gpu_count = getattr(search_space, f"{method_name}_countspace")( + value.gpu_count = getattr(search_space, f"{method.value}_countspace")( self.gpu_count, capability.gpu_count ) else: @@ -971,7 +979,7 @@ def _call_requirement_method(self, method_name: str, capability: Any) -> Any: if ( capability.features - and method_name == search_space.RequirementMethod.generate_min_capability + and method == search_space.RequirementMethod.generate_min_capability ): # The requirement features are ignored, if cap doesn't have it. value.features = search_space.SetSpace[FeatureSettings](is_allow_set=True) @@ -983,11 +991,11 @@ def _call_requirement_method(self, method_name: str, capability: Any) -> Any: self._find_feature_by_type(capability_feature.type, self.features) or capability_feature ) - current_feature = getattr(requirement_feature, method_name)( + current_feature = getattr(requirement_feature, method.value)( capability_feature ) value.features.add(current_feature) - elif method_name == search_space.RequirementMethod.intersect and ( + elif method == search_space.RequirementMethod.intersect and ( capability.features or self.features ): # This is a hack to work with lisa_runner. The capability features @@ -997,7 +1005,7 @@ def _call_requirement_method(self, method_name: str, capability: Any) -> Any: if ( capability.excluded_features - and method_name == search_space.RequirementMethod.generate_min_capability + and method == search_space.RequirementMethod.generate_min_capability ): # TODO: the min value for excluded feature is not clear. It may need # to be improved with real scenarios. @@ -1014,11 +1022,11 @@ def _call_requirement_method(self, method_name: str, capability: Any) -> Any: ) or capability_feature ) - current_feature = getattr(requirement_feature, method_name)( + current_feature = getattr(requirement_feature, method.value)( capability_feature ) value.excluded_features.add(current_feature) - elif method_name == search_space.RequirementMethod.intersect and ( + elif method == search_space.RequirementMethod.intersect and ( capability.excluded_features or self.excluded_features ): # This is a hack to work with lisa_runner. The capability features diff --git a/lisa/search_space.py b/lisa/search_space.py index 183bbdc4c8..523652a6a5 100644 --- a/lisa/search_space.py +++ b/lisa/search_space.py @@ -14,7 +14,7 @@ T = TypeVar("T") -class RequirementMethod(str, Enum): +class RequirementMethod(Enum): generate_min_capability: str = "generate_min_capability" intersect: str = "intersect" @@ -66,18 +66,20 @@ def intersect(self, capability: Any) -> Any: self._validate_result(capability) return self._intersect(capability) - def _call_requirement_method(self, method_name: str, capability: Any) -> Any: - raise NotImplementedError(method_name) + def _call_requirement_method( + self, method: RequirementMethod, capability: Any + ) -> Any: + raise NotImplementedError(method) def _generate_min_capability(self, capability: Any) -> Any: return self._call_requirement_method( - method_name=RequirementMethod.generate_min_capability, + method=RequirementMethod.generate_min_capability, capability=capability, ) def _intersect(self, capability: Any) -> Any: return self._call_requirement_method( - method_name=RequirementMethod.intersect, capability=capability + method=RequirementMethod.intersect, capability=capability ) def _validate_result(self, capability: Any) -> None: @@ -619,14 +621,14 @@ def check( def _call_requirement_method( - method: str, + method: RequirementMethod, requirement: Union[T_SEARCH_SPACE, List[T_SEARCH_SPACE], None], capability: Union[T_SEARCH_SPACE, List[T_SEARCH_SPACE], None], ) -> Any: check_result = check(requirement, capability) if not check_result.result: raise NotMeetRequirementException( - f"cannot call {method}, capability doesn't support requirement" + f"cannot call {method.value}, capability doesn't support requirement" ) result: Optional[T_SEARCH_SPACE] = None @@ -641,7 +643,7 @@ def _call_requirement_method( for req_item in requirement: temp_result = req_item.check(capability) if temp_result.result: - temp_min = getattr(req_item, method)(capability) + temp_min = getattr(req_item, method.value)(capability) if result is None: result = temp_min else: @@ -649,7 +651,7 @@ def _call_requirement_method( # It can be improved by implement __eq__, __lt__ functions. result = min(result, temp_min) elif requirement is not None: - result = getattr(requirement, method)(capability) + result = getattr(requirement, method.value)(capability) return result diff --git a/lisa/sut_orchestrator/aws/features.py b/lisa/sut_orchestrator/aws/features.py index 5921f8e63d..d3ff717306 100644 --- a/lisa/sut_orchestrator/aws/features.py +++ b/lisa/sut_orchestrator/aws/features.py @@ -397,7 +397,9 @@ def check(self, capability: Any) -> search_space.ResultReason: return result - def _call_requirement_method(self, method_name: str, capability: Any) -> Any: + def _call_requirement_method( + self, method: RequirementMethod, capability: Any + ) -> Any: assert isinstance( capability, AwsDiskOptionSettings ), f"actual: {type(capability)}" @@ -407,7 +409,7 @@ def _call_requirement_method(self, method_name: str, capability: Any) -> Any: ), "capability should have at least one disk type, but it's None" value = AwsDiskOptionSettings() super_value = schema.DiskOptionSettings._call_requirement_method( - self, method_name, capability + self, method, capability ) set_filtered_fields(super_value, value, ["data_disk_count"]) @@ -425,13 +427,13 @@ def _call_requirement_method(self, method_name: str, capability: Any) -> Any: f"unknown disk type on capability, type: {cap_disk_type}" ) - value.disk_type = getattr(search_space, f"{method_name}_setspace_by_priority")( + value.disk_type = getattr(search_space, f"{method.value}_setspace_by_priority")( self.disk_type, capability.disk_type, schema.disk_type_priority ) # below values affect data disk only. if self.data_disk_count is not None or capability.data_disk_count is not None: - value.data_disk_count = getattr(search_space, f"{method_name}_countspace")( + value.data_disk_count = getattr(search_space, f"{method.value}_countspace")( self.data_disk_count, capability.data_disk_count ) @@ -440,7 +442,7 @@ def _call_requirement_method(self, method_name: str, capability: Any) -> Any: or capability.max_data_disk_count is not None ): value.max_data_disk_count = getattr( - search_space, f"{method_name}_countspace" + search_space, f"{method.value}_countspace" )(self.max_data_disk_count, capability.max_data_disk_count) # The Ephemeral doesn't support data disk, but it needs a value. And it @@ -448,7 +450,7 @@ def _call_requirement_method(self, method_name: str, capability: Any) -> Any: value.data_disk_iops = 0 value.data_disk_size = 0 - if method_name == RequirementMethod.generate_min_capability: + if method == RequirementMethod.generate_min_capability: assert isinstance( value.disk_type, schema.DiskType ), f"actual: {type(value.disk_type)}" diff --git a/lisa/sut_orchestrator/azure/features.py b/lisa/sut_orchestrator/azure/features.py index 5ede37351f..25032fc464 100644 --- a/lisa/sut_orchestrator/azure/features.py +++ b/lisa/sut_orchestrator/azure/features.py @@ -917,7 +917,9 @@ def check(self, capability: Any) -> search_space.ResultReason: return result - def _call_requirement_method(self, method_name: str, capability: Any) -> Any: + def _call_requirement_method( + self, method: RequirementMethod, capability: Any + ) -> Any: assert isinstance( capability, AzureDiskOptionSettings ), f"actual: {type(capability)}" @@ -927,7 +929,7 @@ def _call_requirement_method(self, method_name: str, capability: Any) -> Any: ), "capability should have at least one disk type, but it's None" value = AzureDiskOptionSettings() super_value = schema.DiskOptionSettings._call_requirement_method( - self, method_name, capability + self, method, capability ) set_filtered_fields(super_value, value, ["data_disk_count"]) @@ -945,13 +947,13 @@ def _call_requirement_method(self, method_name: str, capability: Any) -> Any: f"unknown disk type on capability, type: {cap_disk_type}" ) - value.disk_type = getattr(search_space, f"{method_name}_setspace_by_priority")( + value.disk_type = getattr(search_space, f"{method.value}_setspace_by_priority")( self.disk_type, capability.disk_type, schema.disk_type_priority ) # below values affect data disk only. if self.data_disk_count is not None or capability.data_disk_count is not None: - value.data_disk_count = getattr(search_space, f"{method_name}_countspace")( + value.data_disk_count = getattr(search_space, f"{method.value}_countspace")( self.data_disk_count, capability.data_disk_count ) @@ -960,7 +962,7 @@ def _call_requirement_method(self, method_name: str, capability: Any) -> Any: or capability.max_data_disk_count is not None ): value.max_data_disk_count = getattr( - search_space, f"{method_name}_countspace" + search_space, f"{method.value}_countspace" )(self.max_data_disk_count, capability.max_data_disk_count) # The Ephemeral doesn't support data disk, but it needs a value. And it @@ -968,7 +970,7 @@ def _call_requirement_method(self, method_name: str, capability: Any) -> Any: value.data_disk_iops = 0 value.data_disk_size = 0 - if method_name == RequirementMethod.generate_min_capability: + if method == RequirementMethod.generate_min_capability: assert isinstance( value.disk_type, schema.DiskType ), f"actual: {type(value.disk_type)}" @@ -1027,7 +1029,7 @@ def _call_requirement_method(self, method_name: str, capability: Any) -> Any: value.data_disk_size = self._get_disk_size_from_iops( value.data_disk_iops, disk_type_iops ) - elif method_name == RequirementMethod.intersect: + elif method == RequirementMethod.intersect: value.data_disk_iops = search_space.intersect_countspace( self.data_disk_iops, capability.data_disk_iops ) @@ -1973,14 +1975,16 @@ def check(self, capability: Any) -> search_space.ResultReason: return result - def _call_requirement_method(self, method_name: str, capability: Any) -> Any: + def _call_requirement_method( + self, method: RequirementMethod, capability: Any + ) -> Any: assert isinstance( capability, VhdGenerationSettings ), f"actual: {type(capability)}" value = VhdGenerationSettings() if self.gen or capability.gen: - value.gen = getattr(search_space, f"{method_name}_setspace_by_priority")( + value.gen = getattr(search_space, f"{method.value}_setspace_by_priority")( self.gen, capability.gen, [1, 2] ) return value @@ -2063,7 +2067,9 @@ def check(self, capability: Any) -> search_space.ResultReason: return result - def _call_requirement_method(self, method_name: str, capability: Any) -> Any: + def _call_requirement_method( + self, method: RequirementMethod, capability: Any + ) -> Any: assert isinstance( capability, ArchitectureSettings ), f"actual: {type(capability)}" diff --git a/pyproject.toml b/pyproject.toml index d4590103f4..ca6a8e109e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ dynamic = ["version"] license = {text = "MIT"} readme = "README.rst" -requires-python = ">=3.8, <3.11" +requires-python = ">=3.8" [project.optional-dependencies] @@ -51,8 +51,8 @@ azure = [ "azure-storage-file-share ~= 12.4.0", "cachetools ~= 5.2.0", "requests", - "Pillow ~= 9.0.0", - "PyGObject ~= 3.38.0; platform_system == 'Linux'", + "Pillow ~= 9.4.0", + "PyGObject ~= 3.42.0; platform_system == 'Linux'", ] black = [ diff --git a/selftests/test_search_space.py b/selftests/test_search_space.py index 8050edb579..e125a0e5a9 100644 --- a/selftests/test_search_space.py +++ b/selftests/test_search_space.py @@ -3,7 +3,8 @@ import logging import unittest -from dataclasses import dataclass +from dataclasses import dataclass, field +from functools import partial from typing import Any, List, Optional, TypeVar from lisa.search_space import ( @@ -30,7 +31,7 @@ class MockSchema: @dataclass class MockItem(RequirementMixin): - number: CountSpace = IntRange(min=1, max=5) + number: CountSpace = field(default_factory=partial(IntRange, min=1, max=5)) def check(self, capability: Any) -> ResultReason: assert isinstance(capability, MockItem), f"actual: {type(capability)}"