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)}"