change to multiple envs, and strict typing

1. fixed most format and typing
2. update yaml schema as feedback
3. fix some file names
4. Add environments factory to manage multiple environments
. move environment request logic to runner, but will be flow part later.
. other minor improvements.
This commit is contained in:
Chi Song 2020-08-05 17:54:27 +08:00
Родитель b4452ac471
Коммит 8b1226ea1c
34 изменённых файлов: 590 добавлений и 405 удалений

6
.gitignore поставляемый
Просмотреть файл

@ -6,3 +6,9 @@
# python cache
__pycache__
# you can have your own command alias on Windows
lisa.cmd
# it's done by
lisa.egg-info

Просмотреть файл

@ -15,7 +15,7 @@ then install Poetry:
On Linux (or WSL):
```bash
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3
PATH=$PATH:$HOME/.poetry/bin
```
@ -23,18 +23,19 @@ On Windows (in PowerShell):
```powershell
(Invoke-WebRequest -Uri https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py -UseBasicParsing).Content | python
# the path can be added to system, so it applies to every terminal.
$env:PATH += ";$env:USERPROFILE\.poetry\bin"
```
Then use Poetry to install our Python package dependencies:
```
```bash
poetry install
```
Now run LISAv3 using Poetrys environment:
```
```bash
poetry run lisa/main.py
```
@ -48,14 +49,19 @@ Make sure below settings are in root level of `.vscode/settings.json`
```json
{
"python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true,
"python.linting.enabled": true,
"python.analysis.typeCheckingMode": "strict",
"python.formatting.provider": "black",
"python.linting.mypyEnabled": true,
"python.linting.enabled": true,
"python.linting.flake8Args": [
"--max-line-length",
"88"
]
],
"python.linting.flake8CategorySeverity.W": "Error",
"python.linting.flake8Enabled": true,
"python.linting.mypyCategorySeverity.note": "Error",
"python.linting.mypyEnabled": true,
"python.linting.pylintEnabled": false,
"python.linting.pylintUseMinimalCheckers": false,
"editor.formatOnSave": true,
}
```

Просмотреть файл

@ -14,14 +14,14 @@ parent:
# add extended classes can be put in folders and include here.
# it doesn't matter how those files are organized, lisa loads by their inherits relationship.
# if there is any conflict on type name, there should be an error message.
extensions:
extension:
paths:
- "./myextends"
# it uses to support variables in other fields.
# duplicate items will be overwritten one by one.
# if a variable is not defined here, LISA can fail earlier to ask check it.
# file path is relative to LISA command starts.
variables:
variable:
- file: secrets.yaml
# If it's secret, it will be removed from log and other output information.
# secret files also need to be removed after test is done.
@ -51,51 +51,55 @@ artifact:
# it's spec of environment and node.
# for ready type, it's optional.
environment:
topology: subnet
# template and nodes conflicts, they should have only one.
# it uses to prevent duplicate content for big amount nodes.
template:
- vcpu: 4
nodeCount: 2
# optional, if there is only one artifact.
artifact: default
# sometimes, it may need special configuration for a platform
# we should avoid this kind of setting, it limits configuration to other platforms.
azure:
vmSize: testSize
flag: "test=true"
nodes:
- type: spec
azure:
vmSize: testSize
- type: spec
memoryGB: 8
gpuCount: 1
artifact: dsvm
# local and remote are node type, and don't need platform to handle
- type: local
# If test suite doesn't specify where to run a case,
# it should be run on default node.
isDefault: true
# it's optional
- type: remote
name: secondary
address: 10.0.0.2
port: 22
publicAddress: 23.36.36.36
publicPort: 1112
username: $$linuxTestUsername$$
password: $$linuxTestPassword$$
privateKeyFile: $$sshPrivateKey$$
# the environment spec may not be fully supported by each platform.
# If so, there is a warning message.
# Environment spec can be forced to apply, as error is loud.
warnAsError: true
maxConcurrency: 1
environments:
- name: default
topology: subnet
# template and nodes conflicts, they should have only one.
# it uses to prevent duplicate content for big amount nodes.
template:
- vcpu: 4
nodeCount: 2
# optional, if there is only one artifact.
artifact: default
# sometimes, it may need special configuration for a platform
# we should avoid this kind of setting, it limits configuration to other platforms.
azure:
vmSize: testSize
flag: "test=true"
nodes:
- type: spec
azure:
vmSize: testSize
- type: spec
memoryGB: 8
gpuCount: 1
artifact: dsvm
# local and remote are node type, and don't need platform to handle
- type: local
# If test suite doesn't specify where to run a case,
# it should be run on default node.
isDefault: true
# it's optional
- type: remote
name: secondary
address: 10.0.0.2
port: 22
publicAddress: 23.36.36.36
publicPort: 1112
username: $$linuxTestUsername$$
password: $$linuxTestPassword$$
privateKeyFile: $$sshPrivateKey$$
# the environment spec may not be fully supported by each platform.
# If so, there is a warning message.
# Environment spec can be forced to apply, as error is loud.
warnAsError: true
# it sends test progress and results to any place wanted.
notifier:
- type: junit
- type: html
- type: database
enableTelemetry: true
connectionstring: $$ResultDatabaseString$$
# it's examples for all platforms, but currently should leave single one only.
platform:
@ -110,7 +114,7 @@ platform:
- type: ready
# rules apply ordered on previous selection.
# The order of test cases running is not guaranteed, until it set dependencies.
tests:
testcase:
- criteria:
# all rules in same criteria are AND condition.
# we may support richer conditions later.
@ -124,24 +128,25 @@ tests:
tag: vf
# force to select. If any reason test cases are dropped, fail the run.
forceInclude: true
# if it's false, the test cases are disable in current run.
# it uses to control test cases dynamic form command line.
enable: true
name: network
# TODO: if failed
stopRunAfterFailed: true
# it means there are dependencies among test cases.
- dependedOn: network
# we will support some basic functionality of other test runner,
# for example, environment information, once it's ready.
# any other parameters should be handled by runner itself.
type: LISAv2
extraParameters: -TestNames "VERIFY-DEPLOYMENT-PROVISION" -ResourceCleanup Keep
- name: settings
criteria:
name: this group shows different settings
# run this group of test cases several times
# default is 1
iteration: 5
needNewEnvironment: true
# Once it's set, failed test result will be rewrite to success
# it uses to work around some cases temporarily, don't overuse it.
# default is false
ignoreFailure: true
environment: default
- name: excluded cases
criteria:
name: "excluded test cases"

Просмотреть файл

@ -2,19 +2,16 @@
name: local try
# share configurations for similar runs.
# it's examples for all platforms, but currently should leave single one only.
extensions:
extension:
paths:
- "examples/testsuites"
environment:
nodes:
- type: local
# If test suite doesn't specify where to run a case,
# it should be run on default node.
isDefault: true
# it's optional
environments:
- nodes:
- type: local
platform:
- type: ready
tests:
testcase:
- criteria:
# compatible with current test cases.
area: demo

Просмотреть файл

@ -5,16 +5,13 @@ notifier:
- type: junit
- type: html
environment:
nodes:
- type: local
# If test suite doesn't specify where to run a case,
# it should be run on default node.
isDefault: true
# it's optional
environments:
- nodes:
- type: local
# it's examples for all platforms, but currently should leave single one only.
platform:
- type: ready
tests:
testcase:
- criteria:
# all rules in same criteria are AND condition.
# we may support richer conditions later.
@ -29,13 +26,6 @@ tests:
# force to select. If any reason test cases are dropped, fail the run.
forceInclude: true
name: network
# it means there are dependencies among test cases.
- dependedOn: network
# we will support some basic functionality of other test runner,
# for example, environment information, once it's ready.
# any other parameters should be handled by runner itself.
type: LISAv2
extraParameters: -TestNames "VERIFY-DEPLOYMENT-PROVISION" -ResourceCleanup Keep
- name: settings
criteria:
name: this group shows different settings

Просмотреть файл

@ -1,5 +1,6 @@
from lisa.core.testsuite import TestSuite
from lisa import SuiteMetadata, CaseMetadata, log
from lisa import SuiteMetadata, CaseMetadata
from lisa.common.logger import log
from lisa.core.testSuite import TestSuite
@SuiteMetadata(area="demo", category="simple", tags=["demo"])
@ -12,11 +13,11 @@ class SimpleTestSuite(TestSuite):
def bye(self):
log.info("bye!")
def setup(self):
def caseSetup(self):
log.info("setup my test suite")
log.info("see my code at %s", __file__)
def cleanup(self):
def caseCleanup(self):
log.info("clean up my test suite")
def beforeCase(self):

Просмотреть файл

@ -1,24 +1,13 @@
from .util import constants
from .common.logger import log
from .core.action import Action, ActionStatus
from __future__ import annotations
from .core.decorator.caseMetadata import CaseMetadata
from .core.decorator.suiteMetadata import SuiteMetadata
from .core.testrunner import TestRunner
from .core.testsuite import TestSuite
from .core.environment import Environment
from .core.node import Node
from .core.platform import Platform
from .core.testSuite import TestSuite
__all__ = [
"Action",
"ActionStatus",
"Environment",
"Node",
"TestRunner",
"SuiteMetadata",
"CaseMetadata",
"TestSuite",
"Platform",
"log",
"constants",
]

Просмотреть файл

@ -1,62 +0,0 @@
import asyncio
from lisa.core.platform_factory import platform_factory
from lisa.core.runtimeObject import RuntimeObject
from lisa.core.environment import Environment
import os
from lisa.core.package import import_module
from lisa import log
from lisa.test_runner.lisarunner import LISARunner
from lisa.parameter_parser.parser import parse
def _load_extends(extends_config):
if extends_config is not None:
paths = extends_config.get("paths")
if paths is not None:
for path in paths:
import_module(path)
def _initialize(args):
# make sure extensions in lisa is loaded
base_module_path = os.path.dirname(__file__)
import_module(base_module_path, logDetails=False)
# merge all parameters
config = parse(args)
runtime_object = RuntimeObject(config)
# load external extensions
extends_config = config.getExtensions()
_load_extends(extends_config)
# initialize environment
environment_config = config.getEnvironment()
environment = Environment.loadEnvironment(environment_config)
runtime_object.environment = environment
# initialize platform
platform_config = config.getPlatform()
runtime_object.platform = platform_factory.initializePlatform(platform_config)
runtime_object.validate()
return runtime_object
def run(args):
runtime_object = _initialize(args)
runner = LISARunner()
awaitable = runner.start()
asyncio.run(awaitable)
# check configs
def check(args):
_initialize(args)
def list_start(args):
runtime_object = _initialize(args)
log.info("list information here")

91
lisa/commands.py Normal file
Просмотреть файл

@ -0,0 +1,91 @@
import asyncio
import os
from argparse import Namespace
from typing import Dict, List, Optional, cast
from lisa.common.logger import log
from lisa.core.environment_factory import environment_factory
from lisa.core.package import import_module
from lisa.core.platform_factory import platform_factory
from lisa.core.runtimeObject import RuntimeObject
from lisa.core.test_factory import test_factory
from lisa.parameter_parser.parser import parse
from lisa.test_runner.lisarunner import LISARunner
from lisa.util import constants
def _load_extends(extends_config: Dict[str, object]):
if extends_config is not None:
paths = cast(List[str], extends_config.get("paths"))
if paths is not None:
for path in paths:
import_module(path)
def _initialize(args: Namespace):
# make sure extension in lisa is loaded
base_module_path = os.path.dirname(__file__)
import_module(base_module_path, logDetails=False)
# merge all parameters
config = parse(args)
runtime_object = RuntimeObject(config)
# load external extension
extends_config = config.getExtension()
_load_extends(extends_config)
# initialize environment
environments_config = config.getEnvironment()
environment_factory.loadEnvironments(environments_config)
runtime_object.environment_factory = environment_factory
# initialize platform
platform_config = config.getPlatform()
runtime_object.platform = platform_factory.initializePlatform(platform_config)
runtime_object.validate()
return runtime_object
def run(args):
runtime_object = _initialize(args)
platform = runtime_object.platform
environment_factory = runtime_object.environment_factory
runner = LISARunner()
runner.config(constants.CONFIG_ENVIRONMENT_FACTORY, environment_factory)
runner.config(constants.CONFIG_PLATFORM, platform)
awaitable = runner.start()
asyncio.run(awaitable)
# check configs
def check(args: Namespace):
_initialize(args)
def list_start(args: Namespace):
_initialize(args)
listAll = cast(Optional[bool], args.listAll)
if args.type == "case":
if listAll is True:
for metadata in test_factory.cases.values():
log.info(
"case: %s, suite: %s, area: %s, "
+ "category: %s, tags: %s, priority: %s",
metadata.name,
metadata.suite.name,
metadata.suite.area,
metadata.suite.category,
",".join(metadata.suite.tags),
metadata.priority,
)
else:
log.error("TODO: cannot list selected cases yet.")
else:
raise Exception("unknown list type '%s'" % args.type)
log.info("list information here")

Просмотреть файл

@ -1,4 +0,0 @@
from .logger import log
import lisa.common.env as env
__all__ = ["log", "env"]

Просмотреть файл

@ -1,18 +1,19 @@
import logging
import os
import time
from lisa.common import env
# to prevent circular import, hard code it here.
env_result_path = "LISA_RESULT_PATH"
def init_log():
format = "%(asctime)s.%(msecs)03d[%(levelname)-.1s]%(name)s %(message)s"
logging.basicConfig(
level=logging.DEBUG,
level=logging.INFO,
format=format,
datefmt="%m%d %H:%M:%S",
handlers=[
logging.FileHandler(
"%s/lisa-host.log" % env.get_env(env.RESULT_PATH)
),
logging.FileHandler("%s/lisa-host.log" % os.getenv(env_result_path)),
logging.StreamHandler(),
],
)

Просмотреть файл

@ -1,6 +1,8 @@
from abc import ABC, abstractmethod
from enum import Enum
from lisa.common import log
from typing import Dict
from lisa.common.logger import log
class ActionStatus(Enum):
@ -34,26 +36,26 @@ class Action(ABC):
return name
@abstractmethod
def getTypeName(self):
def getTypeName(self) -> str:
raise NotImplementedError()
@abstractmethod
async def start(self):
async def start(self) -> None:
self.isStarted = True
self.setStatus(ActionStatus.RUNNING)
@abstractmethod
async def stop(self):
async def stop(self) -> None:
self.validateStarted()
@abstractmethod
async def cleanup(self):
async def cleanup(self) -> None:
self.validateStarted()
def getStatus(self):
def getStatus(self) -> ActionStatus:
return self.__status
def setStatus(self, status: ActionStatus):
def setStatus(self, status: ActionStatus) -> None:
if self.__status != status:
log.info(
"%s status changed from %s to %s"
@ -68,8 +70,6 @@ class Action(ABC):
def getPrerequisites(self):
return None
def validateParameters(self, parameters):
# TODO to validate action specified configs
def validateConfig(self, config: Dict[str, object]) -> None:
pass
def getPostValidation(self, parameters):
return None

Просмотреть файл

@ -1,19 +1,21 @@
from timeit import default_timer as timer
from lisa import log
from lisa.core.testfactory import testFactory
from typing import Callable, Optional
from lisa.common.logger import log
from lisa.core.test_factory import test_factory
class CaseMetadata(object):
def __init__(self, priority):
def __init__(self, priority: Optional[int]):
self.priority = priority
def __call__(self, func):
testFactory.addTestMethod(func, self.priority)
def __call__(self, func: Callable[..., None]) -> Callable[..., None]:
test_factory.addTestMethod(func, self.priority)
def wrapper(*args):
def wrapper(*args: object):
log.info("case '%s' started", func.__name__)
start = timer()
func(args)
func(*args)
end = timer()
log.info("case '%s' ended with %f", func.__name__, end - start)

Просмотреть файл

@ -1,20 +1,31 @@
from lisa.core.testfactory import testFactory
from typing import List
from __future__ import annotations
from typing import TYPE_CHECKING, Callable, List, Optional, Type
from lisa.core.test_factory import test_factory
from lisa.core.testSuite import TestSuite
if TYPE_CHECKING:
from lisa.core.environment import Environment
class SuiteMetadata:
def __init__(self, area: str, category: str, tags: List[str], name=None):
def __init__(
self, area: str, category: str, tags: List[str], name: Optional[str] = None
):
self.area = area
self.category = category
self.tags = tags
self.name = name
def __call__(self, test_class):
testFactory.addTestClass(
def __call__(self, test_class: Type[TestSuite]) -> Callable[..., object]:
test_factory.addTestClass(
test_class, self.area, self.category, self.tags, self.name
)
def wrapper(test_class, *args):
return test_class(args)
def wrapper(
test_class: Type[TestSuite], environment: Environment, cases: List[str]
) -> TestSuite:
return test_class(environment, cases)
return wrapper

Просмотреть файл

@ -1,52 +1,65 @@
from __future__ import annotations
import copy
from lisa import log
from typing import TYPE_CHECKING, Dict, List, Optional, cast
from lisa.common.logger import log
from lisa.core.nodeFactory import NodeFactory
from lisa.util import constants
from .node import Node
from lisa import constants
if TYPE_CHECKING:
from lisa.core.platform import Platform
class Environment(object):
CONFIG_NODES = "nodes"
CONFIG_TEMPLATE = "template"
CONFIG_TEMPLATE_NODE_COUNT = "nodeCount"
def __init__(self):
self.nodes: list[Node] = []
self.name: Optional[str] = None
self.platform = None
self.spec = None
self.isReady = False
self.spec: Optional[Dict[str, object]] = None
@staticmethod
def loadEnvironment(config):
def loadEnvironment(config: Dict[str, object]):
environment = Environment()
spec = copy.deepcopy(config)
environment.name = cast(Optional[str], spec.get(constants.NAME))
has_default_node = False
nodes_spec = []
node_config = spec.get(Environment.CONFIG_NODES)
if node_config is not None:
for node_config in config.get(Environment.CONFIG_NODES):
nodes_config = cast(
List[Dict[str, object]], spec.get(constants.ENVIRONMENTS_NODES)
)
if nodes_config is not None:
for node_config in nodes_config:
node = NodeFactory.createNodeFromConfig(node_config)
if node is not None:
environment.nodes.append(node)
else:
nodes_spec.append(node_config)
is_default = cast(Optional[bool], node_config.get(constants.IS_DEFAULT))
has_default_node = environment._validateSingleDefault(
has_default_node, node_config.get(constants.IS_DEFAULT)
has_default_node, is_default
)
# validate template and node not appear together
nodes_template = spec.get(Environment.CONFIG_TEMPLATE)
nodes_template = spec.get(constants.ENVIRONMENTS_TEMPLATE)
if nodes_template is not None:
nodes_template = cast(List[Dict[str, object]], nodes_template)
for item in nodes_template:
node_count = item.get(Environment.CONFIG_TEMPLATE_NODE_COUNT)
node_count = cast(
Optional[int], item.get(constants.ENVIRONMENTS_TEMPLATE_NODE_COUNT)
)
if node_count is None:
node_count = 1
else:
del item[Environment.CONFIG_TEMPLATE_NODE_COUNT]
del item[constants.ENVIRONMENTS_TEMPLATE_NODE_COUNT]
is_default = item.get(constants.IS_DEFAULT)
is_default = cast(Optional[bool], item.get(constants.IS_DEFAULT))
has_default_node = environment._validateSingleDefault(
has_default_node, is_default
)
@ -56,12 +69,12 @@ class Environment(object):
if is_default is True and index > 0:
del copied_item[constants.IS_DEFAULT]
nodes_spec.append(copied_item)
del spec[Environment.CONFIG_TEMPLATE]
del spec[constants.ENVIRONMENTS_TEMPLATE]
if len(nodes_spec) == 0 and len(environment.nodes) == 0:
raise Exception("not found any node in environment")
spec[Environment.CONFIG_NODES] = nodes_spec
spec[constants.ENVIRONMENTS_NODES] = nodes_spec
environment.spec = spec
log.debug("environment spec is %s", environment.spec)
@ -79,7 +92,7 @@ class Environment(object):
default = self.nodes[0]
return default
def getNodeByName(self, name: str, throwError=True):
def getNodeByName(self, name: str, throwError: bool = True):
found = None
if self.nodes is not None:
for node in self.nodes:
@ -93,7 +106,7 @@ class Environment(object):
raise Exception("nodes shouldn't be None when call getNode")
return found
def getNodeByIndex(self, index: int, throwError=True):
def getNodeByIndex(self, index: int, throwError: bool = True):
found = None
if self.nodes is not None:
if len(self.nodes) > index:
@ -102,13 +115,12 @@ class Environment(object):
raise Exception("nodes shouldn't be None when call getNode")
return found
def setPlatform(self, platform):
def setPlatform(self, platform: Platform):
self.platform = platform
def setNodes(self, nodes):
self.nodes = nodes
def _validateSingleDefault(self, has_default, is_default):
def _validateSingleDefault(
self, has_default: bool, is_default: Optional[bool]
) -> bool:
if is_default is True:
if has_default is True:
raise Exception("only one node can set isDefault to True")

Просмотреть файл

@ -0,0 +1,44 @@
from typing import Dict, List, Optional, cast
from lisa.util import constants
from .environment import Environment
default_no_name = "_no_name_default"
class EnvironmentsFactory:
def __init__(self):
self.environments: Dict[str, Environment] = dict()
self.maxConcurrency = 1
def loadEnvironments(self, config: Dict[str, object]):
maxConcurrency = config.get(constants.ENVIRONMENT_MAX_CONCURRENDCY)
if maxConcurrency is not None:
self.maxConcurrency = maxConcurrency
environments_config = cast(
List[Dict[str, object]], config.get(constants.ENVIRONMENTS)
)
without_name: bool = False
for environment_config in environments_config:
environment = Environment.loadEnvironment(environment_config)
if environment.name is None:
if without_name is True:
raise Exception("at least two environments has no name")
environment.name = default_no_name
without_name = True
self.environments[environment.name] = environment
def getEnvironment(self, name: Optional[str] = None) -> Environment:
if name is None:
key = default_no_name
else:
key = name.lower()
environmet = self.environments.get(key)
if environmet is None:
raise Exception("not found environment '%s'", name)
return environmet
environment_factory = EnvironmentsFactory()

Просмотреть файл

@ -1,4 +1,5 @@
from abc import ABC
from lisa import Node
@ -12,9 +13,9 @@ class ExecutableBase(ABC):
self.node = node
def getCommand(self) -> str:
pass
return ""
def run(self, extraParameters: str) -> str:
def run(self, extraParameters: str) -> None:
pass
def canInstall(self):

Просмотреть файл

@ -1,22 +1,32 @@
from typing import Dict, Optional
from lisa.core.sshConnection import SshConnection
from lisa.util import constants
class Node:
def __init__(self, isRemote=True, spec=None, isDefault=False):
self.isDefault: bool = isDefault
self.isRemote: bool = isRemote
def __init__(
self,
isRemote: bool = True,
spec: Optional[Dict[str, object]] = None,
isDefault: bool = False,
):
self.name: Optional[str] = None
self.isDefault = isDefault
self.isRemote = isRemote
self.spec = spec
self.connection = None
self.publicSshSession = None
self.publicSshSession: Optional[SshConnection] = None
@staticmethod
def createNode(
spec=None, node_type=constants.ENVIRONMENT_NODES_REMOTE, isDefault=False
spec: Optional[Dict[str, object]] = None,
node_type: str = constants.ENVIRONMENTS_NODES_REMOTE,
isDefault: bool = False,
):
if node_type == constants.ENVIRONMENT_NODES_REMOTE:
if node_type == constants.ENVIRONMENTS_NODES_REMOTE:
isRemote = True
elif node_type == constants.ENVIRONMENT_NODES_LOCAL:
elif node_type == constants.ENVIRONMENTS_NODES_LOCAL:
isRemote = False
else:
raise Exception("unsupported node_type '%s'", node_type)
@ -38,5 +48,4 @@ class Node:
)
def connect(self):
if self.sshSession is None:
pass
pass

Просмотреть файл

@ -1,17 +1,21 @@
from lisa import log, constants
from typing import Dict, Optional
from lisa.common.logger import log
from lisa.util import constants
from .node import Node
class NodeFactory:
@staticmethod
def createNodeFromConfig(config):
def createNodeFromConfig(config: Dict[str, object]) -> Optional[Node]:
node_type = config.get(constants.TYPE)
node = None
if node_type is None:
raise Exception("type of node shouldn't be None")
if node_type in [
constants.ENVIRONMENT_NODES_LOCAL,
constants.ENVIRONMENT_NODES_REMOTE,
constants.ENVIRONMENTS_NODES_LOCAL,
constants.ENVIRONMENTS_NODES_REMOTE,
]:
is_default = NodeFactory._isDefault(config)
node = Node.createNode(node_type=node_type, isDefault=is_default)
@ -20,18 +24,15 @@ class NodeFactory:
return node
@staticmethod
def createNodeFromSpec(spec, node_type=constants.ENVIRONMENT_NODES_REMOTE):
if node_type == Node.TYPE_REMOTE:
isRemote = True
elif node_type == Node.TYPE_LOCAL:
isRemote = False
else:
raise Exception("unsupported node_type '%s'", node_type)
node = Node.createNode(spec=spec, node_type=node_type, isRemote=isRemote)
def createNodeFromSpec(
spec: Dict[str, object], node_type: str = constants.ENVIRONMENTS_NODES_REMOTE
) -> Node:
is_default = NodeFactory._isDefault(spec)
node = Node.createNode(spec=spec, node_type=node_type, isDefault=is_default)
return node
@staticmethod
def _isDefault(config) -> bool:
def _isDefault(config: Dict[str, object]) -> bool:
default = config.get(constants.IS_DEFAULT)
if default is not None and default is True:
default = True

Просмотреть файл

@ -3,10 +3,10 @@ import os
import sys
from glob import glob
from lisa import log
from lisa.common.logger import log
def import_module(path, logDetails=True):
def import_module(path: str, logDetails: bool = True):
path = os.path.realpath(path)
if not os.path.exists(path):
@ -18,7 +18,7 @@ def import_module(path, logDetails=True):
package_dir = os.path.dirname(path)
sys.path.append(package_dir)
if logDetails:
log.info("loading extensions from %s", path)
log.info("loading extension from %s", path)
for file in glob(os.path.join(path, "**", "*.py"), recursive=True):
file_name = os.path.basename(file)

Просмотреть файл

@ -1,20 +1,35 @@
from lisa.core.environment import Environment
from abc import ABC, abstractclassmethod
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .environment import Environment
class Platform(ABC):
@abstractclassmethod
@classmethod
@abstractmethod
def platformType(cls) -> str:
pass
raise NotImplementedError()
@abstractclassmethod
@abstractmethod
def config(self, key: str, value: object):
pass
@abstractclassmethod
def requestEnvironment(self, environmentSpec):
pass
@abstractmethod
def requestEnvironmentInternal(self, environment: Environment) -> Environment:
raise NotImplementedError
@abstractclassmethod
def deleteEnvironment(self, environment: Environment):
pass
@abstractmethod
def deleteEnvironmentInternal(self, environment: Environment) -> None:
raise NotImplementedError()
def requestEnvironment(self, environment: Environment) -> Environment:
environment = self.requestEnvironmentInternal(environment)
environment.isReady = True
return environment
def deleteEnvironment(self, environment: Environment) -> None:
self.deleteEnvironmentInternal(environment)
environment.isReady = False

Просмотреть файл

@ -1,27 +1,29 @@
from lisa.util import constants
from typing import Dict, List, Optional, Type, cast
from lisa.common.logger import log
from lisa.util import constants
from .platform import Platform
from typing import Dict
class PlatformFactory:
def __init__(self):
self.platforms: Dict[str, Platform] = dict()
def registerPlatform(self, platform):
platform_type = platform.platformType(platform).lower()
def registerPlatform(self, platform: Type[Platform]):
platform_type = platform.platformType().lower()
if self.platforms.get(platform_type) is None:
self.platforms[platform_type] = platform()
else:
raise Exception("platform '%s' exists, cannot be registered again")
def initializePlatform(self, config):
def initializePlatform(self, config: List[Dict[str, object]]):
# we may extend it later to support multiple platforms
platform_count = len(config)
if platform_count != 1:
raise Exception("There must be 1 and only 1 platform")
platform_type = config[0].get("type")
platform_type = cast(Optional[str], config[0].get("type"))
if platform_type is None:
raise Exception("type of platfrom shouldn't be None")

Просмотреть файл

@ -1,31 +1,38 @@
from lisa.parameter_parser.config import Config
from typing import Optional, cast
from lisa.common.logger import log
from lisa import Platform, Environment
from lisa.core.environment import Environment
from lisa.core.platform import Platform
from lisa.parameter_parser.config import Config
from lisa.sut_orchestrator.ready import ReadyPlatform
from lisa import constants
from lisa.util import constants
class RuntimeObject:
def __init__(self, config: Config):
# global config
self.config: Config = config
self.environment: Environment = None
self.platform: Platform = None
self.environment: Optional[Environment] = None
self.platform: Optional[Platform] = None
# do some cross object validation
def validate(self):
environment_config = self.config.getEnvironment()
warn_as_error = None
warn_as_error: Optional[bool] = None
if environment_config is not None:
warn_as_error = environment_config.get(constants.WARN_AS_ERROR)
if self.environment.spec is not None and isinstance(
self.platform, ReadyPlatform
warn_as_error = cast(
Optional[bool], environment_config.get(constants.WARN_AS_ERROR)
)
if (
self.environment is not None
and self.environment.spec is not None
and isinstance(self.platform, ReadyPlatform)
):
self._validateMessage(
warn_as_error, "the ready platform cannot process environment spec"
)
def _validateMessage(self, warn_as_error: bool, message, *args):
def _validateMessage(self, warn_as_error: Optional[bool], message: str, *args: str):
if warn_as_error:
raise Exception(message % args)
else:

Просмотреть файл

@ -1,24 +1,35 @@
from typing import Dict
from lisa import log
from typing import Callable, Dict, List, Optional, Type
from lisa.common.logger import log
from lisa.core.testSuite import TestSuite
class TestCaseMetadata:
def __init__(self, method, priority, name=None):
if name is not None:
def __init__(
self, method: Callable[[], None], priority: Optional[int] = 2, name: str = "",
):
if name is not None and name != "":
self.name = name
else:
self.name = method.__name__
self.key = self.name.lower()
self.key: str = self.name.lower()
self.full_name = method.__qualname__.lower()
self.method = method
self.priority = priority
self.suite = None
self.suite: Optional[TestSuiteMetadata] = None
class TestSuiteMetadata:
def __init__(self, test_class, area, category, tags, name=None):
def __init__(
self,
test_class: Type[TestSuite],
area: Optional[str],
category: Optional[str],
tags: List[str],
name: str = "",
):
self.test_class = test_class
if name is not None:
if name is not None and name != "":
self.name = name
else:
self.name = test_class.__name__
@ -26,7 +37,7 @@ class TestSuiteMetadata:
self.area = area
self.category = category
self.tags = tags
self.cases = dict()
self.cases: Dict[str, TestCaseMetadata] = dict()
def addCase(self, test_case: TestCaseMetadata):
if self.cases.get(test_case.key) is None:
@ -42,7 +53,14 @@ class TestFactory:
self.suites: Dict[str, TestSuiteMetadata] = dict()
self.cases: Dict[str, TestCaseMetadata] = dict()
def addTestClass(self, test_class, area, category, tags, name):
def addTestClass(
self,
test_class: Type[TestSuite],
area: Optional[str],
category: Optional[str],
tags: List[str],
name: Optional[str],
):
if name is not None:
name = name
else:
@ -65,7 +83,7 @@ class TestFactory:
", ".join([key for key in test_suite.cases]),
)
def addTestMethod(self, test_method, priority):
def addTestMethod(self, test_method: Callable[[], None], priority: Optional[int]):
test_case = TestCaseMetadata(test_method, priority)
full_name = test_case.full_name
@ -81,9 +99,7 @@ class TestFactory:
class_name = full_name.split(".")[0]
test_suite = self.suites.get(class_name)
if test_suite is not None:
log.debug(
"add case '%s' to suite '%s'", test_case.name, test_suite.name
)
log.debug("add case '%s' to suite '%s'", test_case.name, test_suite.name)
self._addCaseToSuite(test_suite, test_case)
def _addCaseToSuite(
@ -93,4 +109,4 @@ class TestFactory:
test_case.suite = test_suite
testFactory = TestFactory()
test_factory = TestFactory()

Просмотреть файл

@ -1,4 +1,4 @@
from lisa import Action
from lisa.core.action import Action
class TestRunner(Action):

Просмотреть файл

@ -1,9 +1,13 @@
from lisa.common.logger import log
from lisa.core.action import ActionStatus
from typing import List
from lisa import Action
from .environment import Environment
from __future__ import annotations
from abc import ABC
from typing import TYPE_CHECKING, List
from lisa.common.logger import log
from lisa.core.action import Action, ActionStatus
if TYPE_CHECKING:
from .environment import Environment
class TestSuite(Action, ABC):
@ -16,10 +20,10 @@ class TestSuite(Action, ABC):
self.cases = cases
self.shouldStop = False
def setup(self):
def suiteSetup(self):
pass
def cleanup(self):
def suiteCleanup(self):
pass
def beforeCase(self):
@ -32,18 +36,21 @@ class TestSuite(Action, ABC):
return "TestSuite"
async def start(self):
self.setup()
self.suiteSetup()
for test_case in self.cases:
self.beforeCase()
test_method = getattr(self, test_case)
test_method(self)
test_method()
self.afterCase()
if self.shouldStop:
log.info("received stop message, stop run")
self.setStatus(ActionStatus.STOPPED)
break
self.cleanup()
self.suiteCleanup()
async def stop(self):
self.setStatus(ActionStatus.STOPPING)
self.shouldStop = True
async def cleanup(self):
pass

Просмотреть файл

@ -1,24 +1,24 @@
from lisa.parameter_parser.argparser import parse_args
import os
import sys
from datetime import datetime
from logging import DEBUG, INFO
from lisa.common import env
from lisa.common.logger import init_log, log
from retry import retry
from lisa.common import env
from lisa.parameter_parser.argparser import parse_args
from .common.logger import init_log, log
@retry(FileExistsError, tries=10, delay=0)
def create_result_path():
def create_result_path() -> str:
path_template = "runtime/results/{0}/{0}-{1}"
date = datetime.utcnow().strftime("%Y%m%d")
time = datetime.utcnow().strftime("%H%M%S-%f")[:-3]
current_path = path_template.format(date, time)
if os.path.exists(current_path):
raise FileExistsError(
"%s exists, and not found an unique path." % current_path
)
raise FileExistsError("%s exists, and not found an unique path." % current_path)
return current_path

Просмотреть файл

@ -1,5 +1,6 @@
from argparse import ArgumentParser
from lisa import command_entries
from lisa import commands
def support_config_file(parser: ArgumentParser, required=True):
@ -41,21 +42,23 @@ def parse_args():
subparsers = parser.add_subparsers(dest="cmd", required=False)
run_parser = subparsers.add_parser("run")
run_parser.set_defaults(func=command_entries.run)
run_parser.set_defaults(func=commands.run)
support_config_file(run_parser)
support_variable(run_parser)
list_parser = subparsers.add_parser("list")
list_parser.set_defaults(func=command_entries.list_start)
list_parser.set_defaults(func=commands.list_start)
list_parser.add_argument("--type", "-t", dest="type", choices=["case"])
list_parser.add_argument("--all", "-a", dest="listAll", action="store_true")
support_config_file(list_parser)
support_variable(list_parser)
check_parser = subparsers.add_parser("check")
check_parser.set_defaults(func=command_entries.check)
check_parser.set_defaults(func=commands.check)
support_config_file(check_parser)
support_variable(check_parser)
parser.set_defaults(func=command_entries.run)
parser.set_defaults(func=commands.run)
for sub_parser in subparsers.choices.values():
support_debug(sub_parser)

Просмотреть файл

@ -1,22 +1,27 @@
from lisa import constants
from typing import Dict, Optional, cast
from lisa.util import constants
class Config(dict):
def __init__(self, config):
self.config = config
def __init__(self, config: Dict[str, object]):
self.config: Dict[str, object] = config
def validate(self):
# TODO implement config validation
pass
def getExtensions(self):
return self.config.get(constants.EXTENSIONS)
def getExtension(self) -> Optional[Dict[str, object]]:
return self._getAndCast(constants.EXTENSION)
def getEnvironment(self):
return self.config.get(constants.ENVIRONMENT)
def getEnvironment(self) -> Optional[Dict[str, object]]:
return self._getAndCast(constants.ENVIRONMENT)
def getPlatform(self):
return self.config.get(constants.PLATFORM)
def getPlatform(self) -> Optional[Dict[str, object]]:
return self._getAndCast(constants.PLATFORM)
def getTests(self):
return self.config.get(constants.TESTS)
def getTestCase(self) -> Optional[Dict[str, object]]:
return self._getAndCast(constants.TESTCASE)
def _getAndCast(self, name: str) -> Optional[Dict[str, object]]:
return cast(Optional[Dict[str, object]], self.config.get(name))

Просмотреть файл

@ -1,7 +1,9 @@
from lisa.parameter_parser.config import Config
import os
import yaml
from lisa import log
from lisa.common.logger import log
from lisa.parameter_parser.config import Config
def parse(args):

Просмотреть файл

@ -1,17 +1,19 @@
from lisa import Platform, Environment
from lisa.core.environment import Environment
from lisa.core.platform import Platform
class ReadyPlatform(Platform):
@classmethod
def platformType(cls) -> str:
return "ready"
def config(self, key: str, value: object):
def config(self, key: str, value: object) -> None:
# ready platform has no config
pass
def requestEnvironment(self, environment: Environment):
def requestEnvironmentInternal(self, environment: Environment) -> Environment:
return environment
def deleteEnvironment(self, environment: Environment):
def deleteEnvironmentInternal(self, environment: Environment) -> None:
# ready platform doesn't support delete environment
pass

Просмотреть файл

@ -1,44 +1,58 @@
from lisa.core.environment import Environment
from lisa.core.testsuite import TestSuite
from lisa.core.testfactory import testFactory
from lisa import ActionStatus, TestRunner
from typing import cast
from lisa.common.logger import log
from lisa.core.action import ActionStatus
from lisa.core.environment_factory import EnvironmentsFactory
from lisa.core.platform import Platform
from lisa.core.test_factory import test_factory
from lisa.core.testRunner import TestRunner
from lisa.core.testSuite import TestSuite
from lisa.util import constants
class LISARunner(TestRunner):
def __init__(self):
super().__init__()
self.process = None
self.exitCode = None
def getTypeName(self):
return "LISAv2"
def config(self, key: str, value: object):
if key == constants.CONFIG_ENVIRONMENT_FACTORY:
self.environmentsFactory: EnvironmentsFactory = cast(
EnvironmentsFactory, value
)
elif key == constants.CONFIG_PLATFORM:
self.platform: Platform = cast(Platform, value)
async def start(self):
await super().start()
self.setStatus(ActionStatus.RUNNING)
suites = testFactory.suites
environment = Environment()
suites = test_factory.suites
# request environment
log.info("platform %s environment requesting", self.platform.platformType())
environment = self.environmentsFactory.getEnvironment()
log.info("platform %s environment requested", self.platform.platformType())
for suite in suites.values():
test_object: TestSuite = suite.test_class(environment, suite.cases)
test_object: TestSuite = suite.test_class(
environment, list(suite.cases.keys())
)
await test_object.start()
# delete enviroment after run
log.info("platform %s environment deleting", self.platform.platformType())
self.platform.deleteEnvironment(environment)
log.info("platform %s environment deleted", self.platform.platformType())
self.setStatus(ActionStatus.SUCCESS)
async def stop(self):
super().stop()
self.process.stop()
def cleanup(self):
async def cleanup(self):
super().cleanup()
self.process.cleanup()
def getStatus(self):
if self.process is not None:
running = self.process.isRunning()
if not running:
self.exitCode = self.process.getExitCode()
if self.exitCode == 0:
self.setStatus(ActionStatus.SUCCESS)
else:
self.setStatus(ActionStatus.FAILED)
return super().getStatus()
pass

Просмотреть файл

@ -1,5 +1,7 @@
# config types
CONFIG_CONFIG = "config"
CONFIG_PLATFORM = "platform"
CONFIG_ENVIRONMENT_FACTORY = "environmentFactory"
# common
NAME = "name"
@ -21,10 +23,10 @@ OPERATION_REMOVE = "remove"
OPERATION_ADD = "add"
OPERATION_OVERWRITE = "overwrite"
EXTENSIONS = "extensions"
EXTENSION = "extension"
VARIABLES = "variables"
VARIABLES_ISSECRET = "isSecret"
VARIABLE = "variable"
VARIABLE_ISSECRET = "isSecret"
ARTIFACT = "artifact"
ARTIFACT_TYPE_VHD = "vhd"
@ -32,20 +34,22 @@ ARTIFACT_LOCATIONS = "locations"
ARTIFACT_LOCATIONS_TYPE_HTTP = "http"
ENVIRONMENT = "environment"
ENVIRONMENT_TOPOLOGY = "topology"
ENVIRONMENT_TEMPLATE = "template"
ENVIRONMENT_TEMPLATE_NODECOUNT = "nodeCount"
ENVIRONMENT_NODES = "nodes"
ENVIRONMENT_NODES_SPEC = "spec"
ENVIRONMENT_NODES_REMOTE = "remote"
ENVIRONMENT_NODES_LOCAL = "local"
ENVIRONMENT_NODES_REMOTE_ADDRESS = "address"
ENVIRONMENT_NODES_REMOTE_PORT = "port"
ENVIRONMENT_NODES_REMOTE_PUBLIC_ADDRESS = "publicAddress"
ENVIRONMENT_NODES_REMOTE_PUBLIC_PORT = "publicPort"
ENVIRONMENT_NODES_REMOTE_USERNAME = "username"
ENVIRONMENT_NODES_REMOTE_PASSWORD = "password"
ENVIRONMENT_NODES_REMOTE_PRIVATEKEYFILE = "privateKeyFile"
ENVIRONMENT_MAX_CONCURRENDCY = "maxConcurrency"
ENVIRONMENTS = "environments"
ENVIRONMENTS_TOPOLOGY = "topology"
ENVIRONMENTS_TEMPLATE = "template"
ENVIRONMENTS_TEMPLATE_NODE_COUNT = "nodeCount"
ENVIRONMENTS_NODES = "nodes"
ENVIRONMENTS_NODES_SPEC = "spec"
ENVIRONMENTS_NODES_REMOTE = "remote"
ENVIRONMENTS_NODES_LOCAL = "local"
ENVIRONMENTS_NODES_REMOTE_ADDRESS = "address"
ENVIRONMENTS_NODES_REMOTE_PORT = "port"
ENVIRONMENTS_NODES_REMOTE_PUBLIC_ADDRESS = "publicAddress"
ENVIRONMENTS_NODES_REMOTE_PUBLIC_PORT = "publicPort"
ENVIRONMENTS_NODES_REMOTE_USERNAME = "username"
ENVIRONMENTS_NODES_REMOTE_PASSWORD = "password"
ENVIRONMENTS_NODES_REMOTE_PRIVATEKEYFILE = "privateKeyFile"
NOTIFIER = "notifier"
@ -53,14 +57,14 @@ PLATFORM = "platform"
PLATFORM_AZURE = "azure"
PLATFORM_READY = "ready"
TESTS = "tests"
TESTS_CRITERIA = "criteria"
TESTS_CRITERIA_AREA = "area"
TESTS_CRITERIA_CATEGORY = "category"
TESTS_CRITERIA_PRIORITY = "priority"
TESTS_CRITERIA_TAG = "tag"
TESTS_FORCEINCLUDE = "forceInclude"
TESTS_ITERATION = "iteration"
TESTS_IGNOREFAILURE = "ignoreFailure"
TESTS_INCLUDE = "include"
TESTS_RETRY = "retry"
TESTCASE = "testcase"
TESTCASE_CRITERIA = "criteria"
TESTCASE_CRITERIA_AREA = "area"
TESTCASE_CRITERIA_CATEGORY = "category"
TESTCASE_CRITERIA_PRIORITY = "priority"
TESTCASE_CRITERIA_TAG = "tag"
TESTCASE_FORCEINCLUDE = "forceInclude"
TESTCASE_ITERATION = "iteration"
TESTCASE_IGNOREFAILURE = "ignoreFailure"
TESTCASE_INCLUDE = "include"
TESTCASE_RETRY = "retry"

Просмотреть файл

@ -1,14 +1,17 @@
import os
import subprocess
import logging
import os
import shlex
import subprocess
from threading import Thread
import psutil
from typing import Dict, Optional, cast
from lisa.common import log
from psutil import Process as psutilProcess
from lisa.common.logger import log
class LogPipe(Thread):
def __init__(self, level):
def __init__(self, level: int):
"""Setup the object with a logger and a loglevel
and start the thread
"""
@ -40,56 +43,61 @@ class LogPipe(Thread):
class Process:
def __init__(self):
self.process = None
self.exitCode = None
self.running = None
self.log_pipe = None
self.process: Optional[subprocess.Popen] = None
self.exitCode: Optional[int] = None
self.running: Optional[bool] = None
self.log_pipe: Optional[LogPipe] = None
def start(self, command: str, cwd: str = None, new_envs: dict = None):
def start(
self,
command: str,
cwd: Optional[str] = None,
new_envs: Optional[Dict[str, str]] = None,
):
"""
command include all parameters also.
"""
environ = os.environ.copy()
dictEnv = cast(Dict[str, str], dict(os.environ.copy()))
if new_envs is not None:
for key, value in new_envs:
environ[key] = value
self.stdout_pipe = LogPipe(logging.INFO)
self.stderr_pipe = LogPipe(logging.ERROR)
for key, value in new_envs.items():
dictEnv[key] = value
self.stdout_pipe = cast(int, LogPipe(logging.INFO))
self.stderr_pipe = cast(int, LogPipe(logging.ERROR))
args = shlex.split(command)
self.process = subprocess.Popen(
command,
args,
shell=True,
stdout=self.stdout_pipe,
stderr=self.stderr_pipe,
cwd=cwd,
env=dict(environ),
env=cast(Optional[Dict[str, str]], dictEnv),
)
self.running = True
log.debug("process %s started", self.process.pid)
if self.process is not None:
log.debug("process %s started", self.process.pid)
def stop(self):
if self.process is not None:
for child in psutil.Process(self.process.pid).children(True):
for child in psutilProcess(self.process.pid).children(True):
child.terminate()
self.process.terminate()
log.debug("process %s stopped", self.process.pid)
def cleanup(self):
if self.self.stdout_pipe is not None:
self.self.stdout_pipe.close()
if self.self.stderr_pipe is not None:
self.self.stderr_pipe.close()
if self.stdout_pipe is not None:
self.stdout_pipe.close()
if self.stderr_pipe is not None:
self.stderr_pipe.close()
def isRunning(self):
self.exitCode = self.getExitCode()
if self.exitCode is not None:
if self.running:
log.debug(
"process %s exited: %s", self.process.pid, self.exitCode
)
if self.exitCode is not None and self.process is not None:
if self.running is True:
log.debug("process %s exited: %s", self.process.pid, self.exitCode)
self.running = False
return self.running
def getExitCode(self):
def getExitCode(self) -> Optional[int]:
if self.process is not None:
self.exitCode = self.process.poll()
return self.exitCode