зеркало из https://github.com/microsoft/lisa.git
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:
Родитель
b4452ac471
Коммит
8b1226ea1c
|
@ -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
|
||||
|
|
22
README.md
22
README.md
|
@ -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 Poetry’s 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")
|
|
@ -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
|
||||
|
|
14
lisa/main.py
14
lisa/main.py
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче