From 00ef082b970940c207a82405fa9f333ba1953eb1 Mon Sep 17 00:00:00 2001 From: tsbxmw <1050636648@qq.com> Date: Wed, 17 Apr 2019 16:39:05 +0800 Subject: [PATCH 1/2] add docs to haf, fix some bugs of -not -llog -debug, make haf running faster --- haf/__init__.py | 7 ++ haf/apihelper.py | 71 ++++++++++++- haf/apphelper.py | 52 ++++++++++ haf/asserthelper.py | 10 ++ haf/bench.py | 161 +++++++++++++++++++++++++++--- haf/bus.py | 3 + haf/busclient.py | 59 +++++++++++ haf/case.py | 89 +++++++++++++++++ haf/common/database.py | 78 ++++++++++----- haf/common/httprequest.py | 58 +++++++++-- haf/common/lock.py | 6 ++ haf/common/log.py | 63 ++++++++++-- haf/common/schema.py | 16 +++ haf/common/sigleton.py | 12 ++- haf/config.py | 24 ++++- haf/ext/jinjia2report/report.py | 38 +++++++ haf/globalenv.py | 21 +++- haf/helper.py | 10 ++ haf/hookspecs.py | 4 +- haf/lib.py | 13 +++ haf/loader.py | 170 +++++++++++++++++++++----------- haf/logger.py | 61 ++++++++++-- haf/mark.py | 23 ++++- haf/pluginmanager.py | 7 ++ haf/program.py | 71 ++++++++++--- haf/recorder.py | 64 ++++++++++-- haf/runner.py | 100 ++++++++++++++++++- haf/signal.py | 9 ++ haf/suite.py | 6 ++ haf/utils.py | 2 +- haf/webhelper.py | 3 + 31 files changed, 1155 insertions(+), 156 deletions(-) create mode 100644 haf/signal.py diff --git a/haf/__init__.py b/haf/__init__.py index 70506a0..438ab88 100644 --- a/haf/__init__.py +++ b/haf/__init__.py @@ -1,4 +1,11 @@ import pluggy +''' +file name : __init__ +description : the haf init +others : + define the hookimpl by pluggy +''' + hookimpl = pluggy.HookimplMarker("haf") \ No newline at end of file diff --git a/haf/apihelper.py b/haf/apihelper.py index a48db8a..23520c3 100644 --- a/haf/apihelper.py +++ b/haf/apihelper.py @@ -1,11 +1,19 @@ # encoding='utf-8' - +''' +file name : apihelper +description : the api case helper +others: + include request, response ... +''' import json from haf.common.database import SQLConfig from haf.config import * class Request(object): + ''' + Request object of api request + ''' def __init__(self): self.header = {} self.data = {} @@ -16,6 +24,11 @@ def __init__(self): self.url_part = "" def constructor(self, inputs: dict={}): + ''' + constructor the Request by dict type + :param inputs: + :return: + ''' header = inputs.get("request_header") self.header = json.loads(header) if isinstance(header, str) else header data = inputs.get("request_data") @@ -33,6 +46,10 @@ def constructor(self, inputs: dict={}): self.url = f"{self.protocol}://{self.host_port}{self.url_part}" def deserialize(self): + ''' + return the dict type + :return: + ''' flag = False try: data = json.dumps(self.data, indent=4) @@ -51,17 +68,29 @@ def deserialize(self): class Response(object): + ''' + Response + ''' def __init__(self): self.header = {} self.body = {} self.code = "" def constructor(self, inputs:dict={}): + ''' + + :param inputs: + :return: + ''' self.header = inputs.get("header", {}) self.body = inputs.get("body", {}) self.code = inputs.get("code", {}) def deserialize(self): + ''' + + :return: + ''' flag = False try: body = json.dumps(self.body, indent=4) @@ -81,6 +110,9 @@ def deserialize(self): class Ids(object): + ''' + api ids + ''' def __init__(self): self.id = "" self.subid = "" @@ -88,12 +120,21 @@ def __init__(self): self.api_name = "" def constructor(self, inputs:dict={}): + ''' + + :param inputs: + :return: + ''' self.id = inputs.get("id") self.subid = inputs.get("subid") self.name = inputs.get("name") self.api_name = inputs.get("api_name") def deserialize(self): + ''' + + :return: + ''' return { "id": self.id, "subid": self.subid, @@ -103,6 +144,9 @@ def deserialize(self): class SqlInfo(object): + ''' + sql info of api + ''' def __init__(self): self.scripts = {} self.config = None @@ -111,6 +155,11 @@ def __init__(self): self.check_list = {} def constructor(self, inputs:dict={}): + ''' + + :param inputs: + :return: + ''' sql_response = inputs.get("sql_response") if ";" in sql_response: self.scripts["sql_response"] = sql_response.split(";") @@ -128,9 +177,17 @@ def constructor(self, inputs:dict={}): self.config_id = str(inputs.get("sql_config")) if inputs.get("sql_config") is not None else "" def bind_config(self, config:SQLConfig): + ''' + bind sql config + :param config: + :return: + ''' self.config = config def deserialize(self): + ''' + :return: + ''' return { "scripts": self.scripts, "config": self.config.deserialize() if self.config is not None else None, @@ -140,12 +197,20 @@ def deserialize(self): class Expect(object): + ''' + expect of api + ''' def __init__(self): self.response = Response() self.sql_check_func = "" self.sql_response_result = {} def constructor(self, inputs:dict={}): + ''' + + :param inputs: + :return: + ''' body = inputs.get("expect_response") self.response.body = json.loads(body) if isinstance(body, str) else body sql_check_func = inputs.get("expect_sql") @@ -155,6 +220,10 @@ def constructor(self, inputs:dict={}): self.sql_check_func = sql_check_func.rsplit('.', 2) def deserialize(self): + ''' + + :return: + ''' return { "response": self.response.deserialize(), "sql_check_func": str(self.sql_check_func), diff --git a/haf/apphelper.py b/haf/apphelper.py index 2a89020..36447f1 100644 --- a/haf/apphelper.py +++ b/haf/apphelper.py @@ -1,9 +1,18 @@ # -*- coding: utf-8 -*- +''' +file name : apphelper +description : the app case helper +others: + include BasePage +''' import time, os from haf.config import * class BasePage: + ''' + the base page of app pages, can be implement by cases + ''' DEFAULT_TIMEOUT = 3 def __init__(self, driver): @@ -73,6 +82,9 @@ class By(object): class Stage(object): + ''' + stage of app test + ''' def __init__(self): self.id = 0 self.name = "" @@ -85,6 +97,11 @@ def __init__(self): self.run_count = 0 def constructor(self, input: dict={}): + ''' + + :param input: + :return: + ''' self.id = input.get("id") self.name = input.get("name") self.operation = OPERATION_APP_GROUP[input.get("operation")] @@ -95,6 +112,10 @@ def constructor(self, input: dict={}): self.run_count = input.get("run_count") def deserialize(self): + ''' + + :return: + ''' return { "id": self.id, "name": self.name, @@ -109,6 +130,9 @@ def deserialize(self): class AppIds(object): + ''' + ids of app + ''' def __init__(self): self.id = "" self.subid = "" @@ -116,12 +140,21 @@ def __init__(self): self.app_name = "" def constructor(self, inputs:dict={}): + ''' + + :param inputs: + :return: + ''' self.id = inputs.get("id") self.subid = inputs.get("subid") self.name = inputs.get("name") self.app_name = inputs.get("app_name") def deserialize(self): + ''' + + :return: + ''' return { "id": self.id, "subid": self.subid, @@ -131,6 +164,9 @@ def deserialize(self): class DesiredCaps(object): + ''' + desired caps needed by appium + ''' def __init__(self): self.automationName = "" self.platformName = "" @@ -141,6 +177,11 @@ def __init__(self): self.noReset = True def constructor(self, inputs: dict={}): + ''' + + :param inputs: + :return: + ''' self.automationName = inputs.get("automationName") self.platformName = inputs.get("platformName") self.platformVersion = inputs.get("platformVersion") @@ -150,6 +191,10 @@ def constructor(self, inputs: dict={}): self.noReset = inputs.get("noReset", True) def deserialize(self): + ''' + + :return: + ''' return { "automationName": self.automationName, "platformName": self.platformName, @@ -162,6 +207,13 @@ def deserialize(self): def save_screen_shot(driver, path, name): + ''' + save the screen shot to the path of the name + :param driver: + :param path: + :param name: + :return: + ''' try: path = f"{path}/png" if not os.path.exists(path): diff --git a/haf/asserthelper.py b/haf/asserthelper.py index a3e010a..11e01e9 100644 --- a/haf/asserthelper.py +++ b/haf/asserthelper.py @@ -1,4 +1,11 @@ # encoding='utf-8' +''' +file name : asserthelper +description : rewrite assert +others: + include AssertHelper + now not use this +''' from datetime import datetime from assertpy import assert_that @@ -8,6 +15,9 @@ logger = Log.getLogger(__name__) +#TODO: here need extend + + class AssertHelper(object): @staticmethod def assert_that(real, expect, **kwargs): diff --git a/haf/bench.py b/haf/bench.py index 1590fa3..5e15e79 100644 --- a/haf/bench.py +++ b/haf/bench.py @@ -1,4 +1,10 @@ # encoding='utf-8' +''' +file name : bench +description : the test bench +others: + include All Case bench +''' from haf.common.database import SQLConfig from haf.case import HttpApiCase, AppCase, WebCase @@ -14,6 +20,9 @@ def __init__(self, args): class PyBench(BaseBench): + ''' + py case bench + ''' def __init__(self, args): self.name = None self.args = args @@ -21,30 +30,61 @@ def __init__(self, args): self._init_all() def _init_all(self): + ''' + init all bench + :return: + ''' self.cases = {} self.dbs = {} - def add_case(self, case: WebCase): + def add_case(self, case): + ''' + add case to bench + :param case: + :return: + ''' key = f"{case.ids.id}.{case.ids.subid}.{case.ids.name}" self.cases.update({key: case}) def add_db(self, db: SQLConfig): + ''' + add database config to bench + :param db: + :return: + ''' key_db = str(db.id) self.dbs.update({key_db: db}) - def update_case(self, case: WebCase): + def update_case(self, case): + ''' + update case of the exists case + :param case: + :return: + ''' key = f"{case.ids.id}.{case.ids.subid}.{case.ids.name}" self.cases.update({key: case}) - def get_case(self, key: str): + def get_case(self, key: str) -> 'Case': + ''' + get case by key + :param key: + :return: case + ''' return self.cases.get(key, None) - def get_db(self, key: str): + def get_db(self, key: str) -> 'SQLConfig': + ''' + get db by key + :param key: + :return: database config + ''' return self.dbs.get(key, None) - class HttpApiBench(BaseBench): + ''' + the api bench + ''' def __init__(self, args): self.name = None self.args = args @@ -52,29 +92,61 @@ def __init__(self, args): self._init_all() def _init_all(self): + ''' + init bench + :return: + ''' self.cases = {} self.dbs = {} def add_case(self, case:HttpApiCase): + ''' + add case to bench + :param case: + :return: + ''' key = f"{case.ids.id}.{case.ids.subid}.{case.ids.name}" self.cases.update({key: case}) def add_db(self, db:SQLConfig): + ''' + add db to bench + :param db: + :return: + ''' key_db = str(db.id) self.dbs.update({key_db:db}) def update_case(self, case: HttpApiCase): + ''' + update exists case + :param case: + :return: + ''' key = f"{case.ids.id}.{case.ids.subid}.{case.ids.name}" self.cases.update({key: case}) - def get_case(self, key:str): + def get_case(self, key:str) -> 'Case': + ''' + get case by key + :param key: + :return: case + ''' return self.cases.get(key, None) - def get_db(self, key:str): + def get_db(self, key:str) -> 'SQLConfig': + ''' + get database config by key + :param key: + :return: sqlconfig + ''' return self.dbs.get(key, None) class AppBench(BaseBench): + ''' + app bench + ''' def __init__(self, args): self.name = None self.args = args @@ -82,29 +154,61 @@ def __init__(self, args): self._init_all() def _init_all(self): + ''' + init bench + :return: + ''' self.cases = {} self.dbs = {} def add_case(self, case: AppCase): + ''' + add case to bench + :param case: + :return: + ''' key = f"{case.ids.id}.{case.ids.subid}.{case.ids.name}" self.cases.update({key: case}) def add_db(self, db: SQLConfig): + ''' + add db to bench + :param db: + :return: + ''' key_db = str(db.id) self.dbs.update({key_db:db}) def update_case(self, case: AppCase): + ''' + update exists case + :param case: + :return: + ''' key = f"{case.ids.id}.{case.ids.subid}.{case.ids.name}" self.cases.update({key: case}) - def get_case(self, key: str): + def get_case(self, key:str) -> 'Case': + ''' + get case by key + :param key: + :return: case + ''' return self.cases.get(key, None) - def get_db(self, key: str): + def get_db(self, key:str) -> 'SQLConfig': + ''' + get database config by key + :param key: + :return: sqlconfig + ''' return self.dbs.get(key, None) class WebBench(BaseBench): + ''' + web bench + ''' def __init__(self, args): self.name = None self.args = args @@ -112,23 +216,52 @@ def __init__(self, args): self._init_all() def _init_all(self): + ''' + init bench + :return: + ''' self.cases = {} self.dbs = {} - def add_case(self, case: WebCase): + def add_case(self, case: AppCase): + ''' + add case to bench + :param case: + :return: + ''' key = f"{case.ids.id}.{case.ids.subid}.{case.ids.name}" self.cases.update({key: case}) def add_db(self, db: SQLConfig): + ''' + add db to bench + :param db: + :return: + ''' key_db = str(db.id) - self.dbs.update({key_db: db}) + self.dbs.update({key_db:db}) - def update_case(self, case: WebCase): + def update_case(self, case: AppCase): + ''' + update exists case + :param case: + :return: + ''' key = f"{case.ids.id}.{case.ids.subid}.{case.ids.name}" self.cases.update({key: case}) - def get_case(self, key: str): + def get_case(self, key:str) -> 'Case': + ''' + get case by key + :param key: + :return: case + ''' return self.cases.get(key, None) - def get_db(self, key: str): + def get_db(self, key:str) -> 'SQLConfig': + ''' + get database config by key + :param key: + :return: sqlconfig + ''' return self.dbs.get(key, None) \ No newline at end of file diff --git a/haf/bus.py b/haf/bus.py index b83423d..ebd7773 100644 --- a/haf/bus.py +++ b/haf/bus.py @@ -60,6 +60,8 @@ def start_manager_server(self): case_count = Queue(maxsize=1) # case result summary main queue case_result_main = Queue(maxsize=1) + # logger end queue + logger_end = Queue(maxsize=1) # register the functions to InfoManager InfoManager.register("get_case", callable=lambda: case) @@ -74,6 +76,7 @@ def start_manager_server(self): InfoManager.register("get_publish_loader", callable=lambda: publish_loader) InfoManager.register("get_case_count", callable=lambda: case_count) InfoManager.register("get_case_result_main", callable=lambda: case_result_main) + InfoManager.register("get_logger_end", callable=lambda: logger_end) self.queue_manager = InfoManager(address=(self.domain, self.port), authkey=self.auth_key) self.server = self.queue_manager.get_server() diff --git a/haf/busclient.py b/haf/busclient.py index b027eb6..0107b45 100644 --- a/haf/busclient.py +++ b/haf/busclient.py @@ -3,6 +3,7 @@ ''' # BusClient ''' + import logging from multiprocessing.managers import BaseManager from haf.config import * @@ -13,6 +14,9 @@ class BusClient(metaclass=SingletonType): + ''' + bus client, using to connect to the bus server with host,port,auth_key + ''' def __init__(self, domain:str=None, port:str=None, auth_key:str=None): self.domain = BUS_CLIENT_DOMAIN if domain is None else domain self.port = BUS_PORT if port is None else port @@ -31,45 +35,100 @@ def __init__(self, domain:str=None, port:str=None, auth_key:str=None): InfoManager.register("get_case_back") InfoManager.register("get_case_count") InfoManager.register("get_case_result_main") + InfoManager.register("get_logger_end") # connect to the bus self.info_manager = InfoManager(address=(self.domain, self.port), authkey=self.auth_key) self.info_manager.connect() def get_case(self): + ''' + get case, from loader to runner + :return: + ''' return self.info_manager.get_case() def get_param(self): + ''' + get param, from main to loader + :return: + ''' return self.info_manager.get_param() def get_result(self): + ''' + get result, from runner to recorder + :return: + ''' return self.info_manager.get_result() def get_bench(self): + ''' + no use yet ! + # TODO : remove useless + :return: + ''' return self.info_manager.get_bench() def get_system(self): + ''' + get system signal, from program to program + :return: + ''' return self.info_manager.get_system() def get_log(self): + ''' + get log, from loader, runner, recorder -> logger + :return: + ''' return self.info_manager.get_log() def get_publish_result(self): + ''' + get publish, from recorder to report + :return: + ''' return self.info_manager.get_publish_result() def get_publish_loader(self): + ''' + get publish, from loader to report + :return: + ''' return self.info_manager.get_publish_loader() def get_publish_runner(self): + ''' + get publish, from runner to report + :return: + ''' return self.info_manager.get_publish_runner() def get_case_back(self): + ''' + get case, from runner to loader + :return: + ''' return self.info_manager.get_case_back() def get_case_count(self): + ''' + get case count, from recorder to loader, check the case run over + :return: + ''' return self.info_manager.get_case_count() def get_case_result_main(self): + ''' + get case result main, using to the summary in program.py + :return: + ''' return self.info_manager.get_case_result_main() + + def get_logger_end(self): + ''' + ''' + return self.info_manager.get_logger_end() def __new__(cls, *args, **kwargs): return object.__new__(cls) \ No newline at end of file diff --git a/haf/case.py b/haf/case.py index 85f2572..715064b 100644 --- a/haf/case.py +++ b/haf/case.py @@ -1,4 +1,13 @@ # encoding='utf-8' +''' +file name : case.py +desc : the cases +others : + usage : + py case must implement BaseCase + class TestApi(BaseCase): + +''' import json from haf.apihelper import Request, Response, Ids, Expect, SqlInfo @@ -29,6 +38,9 @@ def __init__(self): class PyCase(BaseCase): + ''' + py cases + ''' def __init__(self, module_name, module_path): super().__init__() self.mark = CASE_MARK_API @@ -44,6 +56,10 @@ def __init__(self, module_name, module_path): self.param = None def _init_all(self): + ''' + init all py case's needed + :return: + ''' self.ids = Ids() self.run = CASE_RUN self.dependent = [] @@ -76,13 +92,26 @@ def constructor(self, *args, **kwargs): self.request = temp if temp else Request() def bind_bench(self, bench_name): + ''' + bind bench with bench name + :param bench_name: + :return: + ''' self.bench_name = bench_name self.generate_log_key() def generate_log_key(self): + ''' + generate log key to self + :return: + ''' self.log_key = self.key = f"{self.bench_name}$%{self.ids.id}.{self.ids.subid}.{self.ids.name}$%" def deserialize(self): + ''' + return the dict type of pycase + :return: + ''' return { "ids": self.ids.deserialize(), "run": self.run, @@ -98,6 +127,9 @@ def deserialize(self): class HttpApiCase(BaseCase): + ''' + http api case + ''' def __init__(self): super().__init__() self.mark = CASE_MARK_API @@ -107,6 +139,10 @@ def __init__(self): self._init_all() def _init_all(self): + ''' + init all api case needed + :return: + ''' self.ids = Ids() self.run = CASE_RUN self.dependent = [] @@ -140,13 +176,26 @@ def constructor(self, *args, **kwargs): self.sqlinfo.constructor(args_init) def bind_bench(self, bench_name): + ''' + bind bench with bench_name + :param bench_name: + :return: + ''' self.bench_name = bench_name self.generate_log_key() def generate_log_key(self): + ''' + generate log key to self + :return: + ''' self.log_key = self.key = f"{self.bench_name}$%{self.ids.id}.{self.ids.subid}.{self.ids.name}$%" def deserialize(self): + ''' + return dict type of api case + :return: + ''' return { "ids": self.ids.deserialize(), "run": self.run, @@ -162,6 +211,9 @@ def deserialize(self): class AppCase(BaseCase): + ''' + app case + ''' def __init__(self): super().__init__() self.mark = CASE_MARK_APP @@ -171,6 +223,10 @@ def __init__(self): self._init_all() def _init_all(self): + ''' + init all app case's needed + :return: + ''' self.ids = AppIds() self.run = CASE_RUN self.dependent = [] @@ -208,13 +264,26 @@ def constructor(self, *args, **kwargs): self.stages[stage.id] = stage def bind_bench(self, bench_name): + ''' + bind bench with bench name + :param bench_name: + :return: + ''' self.bench_name = bench_name self.generate_log_key() def generate_log_key(self): + ''' + generate log key to self + :return: + ''' self.log_key = self.key = f"{self.bench_name}$%{self.ids.id}.{self.ids.subid}.{self.ids.name}$%" def deserialize(self): + ''' + return dict type of self + :return: + ''' return { "ids": self.ids.deserialize(), "run": self.run, @@ -230,6 +299,9 @@ def deserialize(self): class WebCase(BaseCase): + ''' + web case + ''' def __init__(self): super().__init__() self.mark = CASE_MARK_WEB @@ -239,6 +311,10 @@ def __init__(self): self._init_all() def _init_all(self): + ''' + init all web case's needed + :return: + ''' self.ids = WebIds() self.run = CASE_RUN self.dependent = [] @@ -276,13 +352,26 @@ def constructor(self, *args, **kwargs): self.stages[stage.id] = stage def bind_bench(self, bench_name): + ''' + bind bench with bench name + :param bench_name: + :return: + ''' self.bench_name = bench_name self.generate_log_key() def generate_log_key(self): + ''' + generate log key to self + :return: + ''' self.log_key = self.key = f"{self.bench_name}$%{self.ids.id}.{self.ids.subid}.{self.ids.name}$%" def deserialize(self): + ''' + return dict type of self + :return: + ''' return { "ids": self.ids.deserialize(), "run": self.run, diff --git a/haf/common/database.py b/haf/common/database.py index f2b54a4..0c05e38 100644 --- a/haf/common/database.py +++ b/haf/common/database.py @@ -1,5 +1,18 @@ # encoding='utf-8' +''' +file name: database.py +description : database tool for haf, include MySQL,SQLServer,Redis +others : +all these tools need the arg : SQLConfig + usage: + sql_config = SQLConfig() + sql_config.constructor(inputs) + note: + inputs is a dict type, include host, port, username, password, dtabase, protocol, id, sql_name key +''' + + from haf.common.log import Log from contextlib import contextmanager @@ -8,7 +21,7 @@ class SQLConfig(object): ''' - sql config + SQLConfig, the sql tool config ''' def __init__(self): @@ -23,7 +36,9 @@ def __init__(self): def constructor(self, inputs:dict={}): ''' - 构造器 + using dict type to init the SQLConfig + :param inputs + :return None ''' self.host = str(inputs.get("host")) self.port = inputs.get("port") @@ -35,6 +50,10 @@ def constructor(self, inputs:dict={}): self.sqlname = str(inputs.get("sql_name")) def deserialize(self): + ''' + deserialize to an dict type + :return: {} of SQLConfig + ''' return { "host": self.host, "port": self.port, @@ -49,18 +68,19 @@ def deserialize(self): class MysqlTool(object): ''' - Mysql 工具类 + MysqlTool ''' def __init__(self): pass - def connect_execute(self, sqlconfig:SQLConfig, sqlscript:list, **kwargs): + def connect_execute(self, sqlconfig: SQLConfig, sqlscript: list, **kwargs)-> tuple: ''' - 连接到数据库并执行脚本 - :参数: + connect and execute - * sqlconfig : sqlconfig 实例 - * sqlscript : 执行的 sqlscript + :param sqlconfig SQLConfig + :param sqlscript the script of sql, can be string or list + :param kwargs, include key of testcase, commit of sqlscript, run_background + :return return the result of sql execute ''' import pymysql key = kwargs.get("key", "database$%common$%") @@ -81,8 +101,11 @@ def connect_execute(self, sqlconfig:SQLConfig, sqlscript:list, **kwargs): self.connect_msql = pymysql.connect(host=sqlconfig.host, port=sqlconfig.port, user=sqlconfig.username, passwd=sqlconfig.password, db=sqlconfig.database) cursor_m = self.connect_msql.cursor() data = [] + # here if sqlscript is list type, must execute every + # script and append the result to the end_result if isinstance(sqlscript, list): for ss in sqlscript: + # valued sql script must not be None and length > 5 if ss != None and ss != "None" and len(ss) > 5: if not run_background: logger.info(f"{key} start {sqlconfig.host} execute {ss}", __name__) @@ -90,14 +113,15 @@ def connect_execute(self, sqlconfig:SQLConfig, sqlscript:list, **kwargs): data.append(cursor_m.fetchall()) if not run_background: logger.info(f"{key} result {str(data)}", __name__) - if isinstance(ss, tuple): + # if sql script is tuple type, means to be 2 parts: 1 is the script, 2 is the parameter + if isinstance(ss, tuple) and len(ss)>2: if not run_background: logger.info(f"{key} tuple start {sqlconfig.host} execute {ss}", __name__) cursor_m.execute(ss[0], ss[1]) data.append(cursor_m.fetchall()) if not run_background: logger.info(f"{key} result {str(data)}", __name__) - + # if the sqlscript is the string type, can just run it elif isinstance(sqlscript, str): if sqlscript != None and sqlscript != "None" and "None" not in sqlscript and len(sqlscript) > 5: if not run_background: @@ -106,7 +130,8 @@ def connect_execute(self, sqlconfig:SQLConfig, sqlscript:list, **kwargs): data.append(cursor_m.fetchall()) if not run_background: logger.info(f"{key} result {str(data)}", __name__) - + # if the sqlscript is the tuple type and we do not know the length, + # just execute the *sqlscript by cursor.execute() elif isinstance(sqlscript, tuple): if not run_background: logger.info(f"{key} start {sqlconfig.host} execute {sqlscript}", __name__) @@ -114,7 +139,7 @@ def connect_execute(self, sqlconfig:SQLConfig, sqlscript:list, **kwargs): data.append(cursor_m.fetchall()) if not run_background: logger.info(f"{key} result {str(data)}", __name__) - + # some sqlscript need commit to make it work, like update, delete, insert if commit: self.connect_msql.commit() return data @@ -135,19 +160,22 @@ def close(self): class SqlServerTool(object): ''' - SqlServerTool 工具类 + SqlServerTool ''' def __init__(self): pass def connect_execute(self, sqlconfig:SQLConfig, sqlscript:list, **kwargs): ''' - 连接到数据库并执行脚本 - :参数: + connect and execute - * sqlconfig : sqlconfig 实例 - * sqlscript : 执行的 sqlscript + :param sqlconfig SQLConfig + :param sqlscript the script of sql, can be string or list + :param kwargs, include key of testcase, commit of sqlscript, run_background + :return return the result of sql execute ''' + # TODO: + # need extend like MysqlTool import pymssql key = kwargs.get("key", "database$%common$%") @@ -191,7 +219,7 @@ def close(self): class RedisTool(object): ''' - Redis 工具类 + Redis ''' def __init__(self): pass @@ -201,13 +229,15 @@ def __str__(self): def connect_execute(self, sqlconfig: SQLConfig, sqlscript: list, **kwargs): ''' - 连接到数据库并执行脚本 - :参数: - - * testcase : testcase 实例 - * caseparam : 执行的 case 中对应的 脚本名称 + connect and execute + + :param sqlconfig SQLConfig + :param sqlscript the script of sql, can be string or list + :param kwargs, include key of testcase, commit of sqlscript, run_background + :return return the result of sql execute ''' - + # TODO: + # need extend like MysqlTool import redis key = kwargs.get("key", "database$%common$%") commit = kwargs.get("commit", False) diff --git a/haf/common/httprequest.py b/haf/common/httprequest.py index 1789672..3ae911f 100644 --- a/haf/common/httprequest.py +++ b/haf/common/httprequest.py @@ -1,5 +1,14 @@ # encoding='utf-8' +''' +file name : httpreqeust +description : http request all need this +others : + support GET POST PUT DELETE method + support with headers, data +''' + + import json import urllib.request as ur import urllib.parse @@ -12,7 +21,8 @@ class HttpController(object): ''' - Http 请求 管理类 + HttpController + using to the get/post/others ''' def __init__(self): @@ -21,11 +31,10 @@ def __init__(self): @staticmethod def getdata(data): ''' - 将 get 的 data 格式化为 url 参数 形式 + getdata: using to make data to the url type - :参数: - * data : str/bytes/dict get 请求的参数 - :return: data + :param data: the origin data + :return the url type data ''' datastr = "?" if isinstance(data, bytes): @@ -46,15 +55,19 @@ def getdata(data): @staticmethod def get(url, data=None, headers=None, **kwargs): ''' - http get 请求方法 - :参数: - * url : 请求的 url - :return: response.read() 返回的 请求内容 + get of http + + :param url the request url + :param data the origin data + :param headers the get headers + + :return the response of get ''' key = kwargs.get("key") try: url = url + HttpController.getdata(data) logger.info(f'{key} [url] {url}', __name__) + # using requests to Request the url with headers and method get request = ur.Request(url=url, headers=headers, method="GET") if headers is not None: for key in headers.keys(): @@ -78,6 +91,15 @@ def get(url, data=None, headers=None, **kwargs): @staticmethod def post(url, data=None, headers=None, **kwargs): + ''' + post of http + + :param url the request url + :param data the origin data + :param headers the post headers + + :return the response of post + ''' key = kwargs.get("key") try: if "application/json" in headers.values(): @@ -105,6 +127,15 @@ def post(url, data=None, headers=None, **kwargs): return ee def put(self, url, data=None, **kwargs): + ''' + put of http + + :param url the request url + :param data the origin data + :param headers the put headers + + :return the response of put + ''' key = kwargs.get("key") try: data = bytes(data, encoding='utf8') if data is not None else None @@ -119,6 +150,15 @@ def put(self, url, data=None, **kwargs): logger.error(f"{key} {str(ee)}", __name__) def delete(self, url, data=None): + ''' + delete of http + + :param url the request url + :param data the origin data + :param headers the delete headers + + :return the response of delete + ''' data = bytes(data, encoding='utf8') if data is not None else None try: request = ur.Request(url, headers=self.headers, data=data) diff --git a/haf/common/lock.py b/haf/common/lock.py index 5342640..d6d2c66 100644 --- a/haf/common/lock.py +++ b/haf/common/lock.py @@ -1,5 +1,11 @@ # encoding='utf-8' +''' +file name : lock +description : the lock of multi-process with queue +others: + already release this +''' class Lock(object): diff --git a/haf/common/log.py b/haf/common/log.py index 406220e..9055fd3 100644 --- a/haf/common/log.py +++ b/haf/common/log.py @@ -1,11 +1,35 @@ # -*- encoding:utf-8 -*- +''' +file name : log +description : the log of all process need +others: + usage: + logger = Log.getLogger(__name__) + # in multi-process + logger.bind_busclient(bus_client) + logger.bind_process(pid) + # if no output + logger.set_output(False, nout) + # if local logger + logger.set_output(True, nout) + logger.debug(msg, logger_name) + logger.info + logger.warning + logger.error +''' + + import logging, os, time from haf.busclient import BusClient from haf.common.sigleton import SingletonType class BaseLogger(metaclass=SingletonType): + ''' + BaseLogger + SingletonType class + ''' def __init__(self, logger_name): self.logger_name = logger_name @@ -15,13 +39,17 @@ def __init__(self, logger_name): # here using this function to reconnect to the bus : loader, runner, recorder def bind_busclient(self, bus_client: BusClient): self.bus_client = BusClient(bus_client.domain, bus_client.port, bus_client.auth_key) - + self.log = self.bus_client.get_log() + + # bind the pid of the process def bind_process(self, process_id): self.process_id = process_id - def set_output(self, local_logger, nout=True): + # set out put or not, or using local logger + def set_output(self, local_logger, nout=True, debug=False): self.local_logger = local_logger self.nout = nout + self.debug_ = debug def debug(self, msg, logger_name=None): msg = {"process": self.process_id, "logger_name":self.logger_name if not logger_name else logger_name, "level":"debug", "msg": msg} @@ -46,24 +74,36 @@ def critical(self, msg, logger_name=None): def __new__(cls, *args, **kwargs): return object.__new__(cls) - def log_output(self, msg): + def log_output(self, msg, origin_msg): + ''' + print msg with params nout + :param msg: + :return: None + ''' if not self.nout: + if origin_msg.get('level')=='debug' and not self.debug_: + return print(msg) def write_local_logger(self, msg): - msg = f"{self.now} {msg.get('level')} <{msg.get('process')}> [{msg.get('logger_name')}] {msg.get('msg')}" - self.log_output(msg) + ''' + write the log with local writer + :param msg: + :return: + ''' + msg_now = f"{self.now} {msg.get('level')} <{msg.get('process')}> [{msg.get('logger_name')}] {msg.get('msg')}" + self.log_output(msg_now, msg) dir = f"./data/log/" if not os.path.exists(dir): os.makedirs(dir) full_name = f"{dir}/log.log" try: with open(full_name, 'a+') as f: - f.write(f"{msg}\n") + f.write(f"{msg_now}\n") f.close() except Exception as e: with open(full_name, 'a+', encoding='utf-8') as f: - f.write(f"{msg}\n") + f.write(f"{msg_now}\n") f.close() @property @@ -80,12 +120,19 @@ def now(self): return timenow def msg_write(self, msg): + ''' + write to local or not + :param msg: + :return: + ''' if self.bus_client is None: self.bus_client = BusClient() if self.local_logger: self.write_local_logger(msg) else: - self.bus_client.get_log().put(msg) + if msg.get('level')=='debug' and not self.debug_: + return + self.log.put(msg) class Log: diff --git a/haf/common/schema.py b/haf/common/schema.py index 6d56e63..e9d36c8 100644 --- a/haf/common/schema.py +++ b/haf/common/schema.py @@ -1,7 +1,23 @@ +''' +file name : schema +description : the json schema check +others: + usage: + check_config(config) +''' + + from jsonschema import validate from haf.config import config_schema + def check_config(config): + ''' + check the haf run config is right or not with the config_schema in haf.config + + :param config: the input + :return: check result + ''' try: validate(instance=config, schema=config_schema) return True diff --git a/haf/common/sigleton.py b/haf/common/sigleton.py index ef9331b..261db9d 100644 --- a/haf/common/sigleton.py +++ b/haf/common/sigleton.py @@ -1,5 +1,15 @@ # encoding='utf-8' -import functools +''' +file name : sigleton +description : the sigleton defined +others: + usage: + 1, class A(metaclass=SingletonType): + 2, class B(Singleton) + 3, @sigleton + class C(object): + +''' # this is metaclass diff --git a/haf/config.py b/haf/config.py index 3b8a20e..04a0bc2 100644 --- a/haf/config.py +++ b/haf/config.py @@ -1,37 +1,48 @@ # encoding='utf-8' -GITHUB_URL_BASE = 'https://github.com/tsbxmw/haf' +# git hub main page of haf +GITHUB_URL_BASE = 'https://github.com/hautof/haf' + +# bus server default port,domain, client, authkey BUS_PORT = 9000 BUS_DOMAIN = u"0.0.0.0" BUS_CLIENT_DOMAIN = u"127.0.0.1" BUS_AUTH_KEY = bytes("hafbus", encoding='utf-8') +# web server default port WEB_SERVER = False WEB_SERVER_PORT = 8888 +# app driver path APP_DRIVER_PATH = 'http://localhost:4723/wd/hub' +# runner count default = 1 COUNT_RUNNER = 1 +# case types [base, webui, httpapi, py, app] CASE_TYPE_BASE = 0 CASE_TYPE_WEBUI = 1 CASE_TYPE_HTTPAPI = 2 CASE_TYPE_PY = 3 CASE_TYPE_APP = 4 +# message type [case, result, api, other] MESSAGE_TYPE_CASE = 10 MESSAGE_TYPE_RESULT = 11 MESSAGE_TYPE_API_RESULT = 12 MESSAGE_TYPE_OTHER = 13 +# signal [start, stop, case-end, result-end, record-end, bus-end] SIGNAL_START = 20 SIGNAL_STOP = 21 SIGNAL_CASE_END = 22 SIGNAL_RESULT_END = 23 SIGNAL_RECORD_END = 24 SIGNAL_BUS_END = 25 +SIGNAL_LOGGER_END = 26 +# api method [get, post, put, delete] CASE_HTTP_API_METHOD_GET = 30 CASE_HTTP_API_METHOD_POST = 31 CASE_HTTP_API_METHOD_PUT= 32 @@ -44,12 +55,14 @@ "33": "DELETE" } +# case run type [run, not run, skip, run, error] CASE_CAN_RUN_HERE = 40 CASE_CAN_NOT_RUN_HERE = 41 CASE_SKIP = 42 CASE_RUN = 43 CASE_ERROR = 44 +# result [pass, fail, skip, error] RESULT_PASS = 50 RESULT_FAIL = 51 RESULT_SKIP = 52 @@ -62,11 +75,13 @@ "53": "ERROR" } +# case mark, [base, api, app, web] CASE_MARK_BASE = 60 CASE_MARK_API = 61 CASE_MARK_APP = 62 CASE_MARK_WEB = 63 +# operation of app [click, sendkeys, swipe, other, exists] OPERATION_APP_CLICK = 70 OPERATION_APP_SENDKEYS = 71 OPERATION_APP_SWIPE = 72 @@ -89,6 +104,7 @@ OPERATION_APP_EXISTS: "exists" } +# operation of web [click, sendkeys, swipe, other, exists] OPERATION_WEB_CLICK = 80 OPERATION_WEB_SENDKEYS = 81 OPERATION_WEB_SWIPE = 82 @@ -111,14 +127,17 @@ OPERATION_WEB_EXISTS: "exists" } +# log path LOG_PATH_DEFAULT = "./data" +# version define MAIN_VERSION = 2 SUB_VERSION = 8 -FIX_VERSION = 3 +FIX_VERSION = 4 VERSION_TYPE = "haf" PLATFORM_VERSION = f"{VERSION_TYPE}-{MAIN_VERSION}.{SUB_VERSION}.{FIX_VERSION}" +# banner string BANNER_STRS = f""" . ____ /\\\\ | | | | ____ /. _/ @@ -137,6 +156,7 @@ |_| |_| v{MAIN_VERSION}.{SUB_VERSION}.{FIX_VERSION} |_| EXIT """ +# run config schema config_schema = { "type": "object", "properties": { diff --git a/haf/ext/jinjia2report/report.py b/haf/ext/jinjia2report/report.py index 3d9b5af..f190d6a 100644 --- a/haf/ext/jinjia2report/report.py +++ b/haf/ext/jinjia2report/report.py @@ -13,6 +13,9 @@ class Jinja2Report(object): + ''' + generate report by jinja2 + ''' def __init__(self): pass @@ -26,6 +29,18 @@ def get_template_customer(path_: str) -> None: @staticmethod def get_template(key: str) -> None: + ''' + get template by the key + key : template: + None/base/online base.html + online-app, base_app base_app.html + online-web, base_web base_app.html + base_email base_eamil.html + third-pary find the template in the path + + :param key: + :return: + ''' if key == "base" or key == "online": template = "base.html" elif key in ["online-app", "base_app", "online-web", "base_web"]: @@ -51,6 +66,13 @@ def get_template(key: str) -> None: @staticmethod def report(results: EndResult, name="base"): + ''' + generate report + + :param results: + :param name: + :return: generate report by template and results + ''' if results is None: return {"status": "error", "msg": "results is None"} if not name: @@ -61,6 +83,12 @@ def report(results: EndResult, name="base"): @staticmethod def write_report_to_file(info, report_path: str): + ''' + write stream to file + :param info: + :param report_path: + :return: None + ''' try: logger.info(f"generate report to {report_path}", __name__) stream = TemplateStream(info) @@ -71,6 +99,11 @@ def write_report_to_file(info, report_path: str): @staticmethod def report_online(results: EndResult): + ''' + generate online report stream + :param results: + :return: + ''' if results is None: return {"status": "error", "msg": "results is None"} template = Jinja2Report.get_template("online") @@ -78,6 +111,11 @@ def report_online(results: EndResult): @staticmethod def report_online_app(results: EndResult): + ''' + generate online app report stream + :param results: + :return: + ''' if results is None: return {"status": "error", "msg": "results is None"} template = Jinja2Report.get_template("online-app") diff --git a/haf/globalenv.py b/haf/globalenv.py index 17c96ca..988800e 100644 --- a/haf/globalenv.py +++ b/haf/globalenv.py @@ -1,16 +1,35 @@ # -*- coding: utf-8 -*- - +''' +file name : globalenv +desc : using to multi file var shared +''' def _init(): + ''' + global init + :return: + ''' global _global_dict _global_dict = {} def set_global(name, value): + ''' + set global var + :param name: + :param value: + :return: + ''' _global_dict[name] = value def get_global(name, defValue=None): + ''' + get from global + :param name: + :param defValue: + :return: + ''' try: return _global_dict[name] except KeyError: diff --git a/haf/helper.py b/haf/helper.py index 38de1ae..9a6eacb 100644 --- a/haf/helper.py +++ b/haf/helper.py @@ -1,4 +1,9 @@ # encoding='utf-8' +''' +file name : helper +desc : show help informations +''' + import os, sys from haf.config import * @@ -95,6 +100,11 @@ def print_help(): --sql-publish-db SQL_PUBLISH_DB, -sp_db SQL_PUBLISH_DB sql publish db config, format like : host:port@username:password@database) + --no-output NO_OUTPUT, -nout NOUT + no output + --local-logger LOCAL_LOGGER, -llog LLOG + local logger writer + ---filter TEST_FILTER, -t """ print(run_help) diff --git a/haf/hookspecs.py b/haf/hookspecs.py index f1fbc27..6cc981d 100644 --- a/haf/hookspecs.py +++ b/haf/hookspecs.py @@ -1,5 +1,7 @@ import pluggy - +''' +hookspecs +''' hookspec = pluggy.HookspecMarker("haf") diff --git a/haf/lib.py b/haf/lib.py index 54f3243..6ca4be6 100644 --- a/haf/lib.py +++ b/haf/lib.py @@ -1,3 +1,7 @@ +''' +lib +''' + import haf from haf.utils import LoadFromConfig from haf.common.log import Log @@ -7,12 +11,21 @@ @haf.hookimpl def add_option(): + ''' + add option to run program + :return: + ''' args = [] return args @haf.hookimpl def load_from_file(file_name): + ''' + u can rewrite this in plugin to overwrite the hooks + :param file_name: + :return: + ''' if file_name.endswith(".xlsx"): output = LoadFromConfig.load_from_xlsx(file_name) elif file_name.endswith(".json"): diff --git a/haf/loader.py b/haf/loader.py index 18740b8..1818bcf 100644 --- a/haf/loader.py +++ b/haf/loader.py @@ -12,12 +12,16 @@ from haf.utils import Utils from haf.mark import locker, new_locker from haf.pluginmanager import plugin_manager +from haf.signal import Signal from progress.bar import ChargingBar logger = Log.getLogger(__name__) class Loader(Process): + ''' + loader + ''' def __init__(self, bus_client: BusClient=None, lock: m_lock=None, args: list=None): super().__init__() self.bus_client = bus_client @@ -29,29 +33,39 @@ def __init__(self, bus_client: BusClient=None, lock: m_lock=None, args: list=Non self.args = args def run(self): + ''' + loader run + :return: + ''' try: logger.bind_process(self.pid) self.key = f"{self.pid}$%loader$%" logger.bind_busclient(self.bus_client) - logger.set_output(self.args.local_logger, self.args.nout) + logger.set_output(self.args.local_logger, self.args.nout, self.args.debug) logger.info(f"{self.key} start loader", __name__) self.case_queue = self.bus_client.get_case() self.case_back_queue = self.bus_client.get_case_back() self.case_count = self.bus_client.get_case_count() + self.web_queue = self.bus_client.get_publish_loader() + self.params_queue = self.bus_client.get_param() + # check the signal of start while True: - if self.get_parameter() == SIGNAL_START: + signal_params_ = self.get_parameter() + if isinstance(signal_params_, Signal) and signal_params_.signal == SIGNAL_START: logger.info(f"{self.key} -- get start signal from main", __name__) break - time.sleep(0.01) + if self.args.nout: cb = ChargingBar(max=100) complete_case_count = 0 + + # check the params while True: temp = self.get_parameter() - if temp == SIGNAL_STOP : + # check the signal of stop, after stop nothing! + if isinstance(temp, Signal) and temp.signal == SIGNAL_STOP : while True: - if not self.case_count.empty(): complete_case_count = self.case_count.get() else: @@ -72,57 +86,60 @@ def run(self): self.end_handler() return time.sleep(0.01) - if temp is None: + elif temp is None: time.sleep(0.01) continue - - file_name = temp.get("file_name") - inputs = plugin_manager.load_from_file(file_name) - - logger.debug(f"{self.key} -- {inputs}", __name__) - input = inputs.get("config")[0] - bench_name = input.get("name") - module_name = input.get("module_name") - module_path = input.get("module_path") - - bench = HttpApiBench(self.args) - if "dbconfig" in inputs.keys(): - for input in inputs.get("dbconfig"): - db = SQLConfig() - db.constructor(input) - bench.add_db(db) - - for input in inputs.get("testcases"): - if input.get("id") is None or input.get("subid") is None: - continue - if input.get("host_port") is None: - input["host_port"] = inputs.get("config")[0].get("host_port") - if "api_name" in input.keys() or input.get("type")=="api": - case = HttpApiCase() - elif input.get("type") == "app": - case = AppCase() - elif input.get("type") == "web": - case = WebCase() - else: - case = PyCase(module_name, module_path) - try: - case.constructor(input) - case.bind_bench(bench_name) - case.sqlinfo.bind_config(bench.get_db(case.sqlinfo.config_id)) - except Exception as e: - case.bind_bench(bench_name) - case.sqlinfo.bind_config(bench.get_db(case.sqlinfo.config_id)) - case.run = CASE_SKIP - case.error = CASE_ERROR - case.error_msg = str(e) - - self.true_case_count += 1 - self.add_case(case) - self.put_case("case", None, case) - self.put_web_message("web") + else: + file_name = temp.get("file_name") + logger.debug(f"file_name = {file_name}", __name__) + inputs = plugin_manager.load_from_file(file_name) + + logger.debug("inputs", __name__) + logger.debug(f"{self.key} -- {inputs}", __name__) + input = inputs.get("config")[0] + bench_name = input.get("name") + module_name = input.get("module_name") + module_path = input.get("module_path") + + logger.debug("bench", __name__) + bench = HttpApiBench(self.args) + if "dbconfig" in inputs.keys(): + for input in inputs.get("dbconfig"): + db = SQLConfig() + db.constructor(input) + bench.add_db(db) + + for input in inputs.get("testcases"): + logger.debug(input, __name__) + if input.get("id") is None or input.get("subid") is None: + continue + if input.get("host_port") is None: + input["host_port"] = inputs.get("config")[0].get("host_port") + if "api_name" in input.keys() or input.get("type")=="api": + case = HttpApiCase() + elif input.get("type") == "app": + case = AppCase() + elif input.get("type") == "web": + case = WebCase() + else: + case = PyCase(module_name, module_path) + try: + case.constructor(input) + case.bind_bench(bench_name) + case.sqlinfo.bind_config(bench.get_db(case.sqlinfo.config_id)) + except Exception as e: + case.bind_bench(bench_name) + case.sqlinfo.bind_config(bench.get_db(case.sqlinfo.config_id)) + case.run = CASE_SKIP + case.error = CASE_ERROR + case.error_msg = str(e) + + self.true_case_count += 1 + self.add_case(case) + self.put_case("case", None, case) + self.put_web_message("web") self.put_web_message("web") - time.sleep(0.01) except Exception as e: logger.error(f"{self.key} {e}", __name__) logger.error(f"{self.key} {FailLoaderException}", __name__) @@ -132,6 +149,12 @@ def run(self): self.end_handler(e) def check_case_complete(self, key: str, lock=None): + ''' + check case complete count and the all case count + :param key: + :param lock: + :return: + ''' if not self.case_count.empty(): complete_case_count = self.case_count.get() if complete_case_count==self.true_case_count: @@ -139,36 +162,63 @@ def check_case_complete(self, key: str, lock=None): return False def get_parameter(self, param_key=None): - params_queue = self.bus_client.get_param() - if not params_queue.empty(): - params = params_queue.get() + ''' + get the param from queue, which send by main program in program + :param param_key: + :return: + ''' + if not self.params_queue.empty(): + params = self.params_queue.get() if param_key is not None: return params.get(param_key) return params return None def add_case(self, case): + ''' + add case to message of publish web server + :param case: + :return: + ''' self.loader["all"] += 1 if case.error_msg != "": self.loader["error"] += 1 self.loader["error_info"][f"{case.ids.id}.{case.ids.subid}.{case.ids.name}"] = case.error_msg def put_web_message(self, key: str, lock=None): - web_queue = self.bus_client.get_publish_loader() - if web_queue.full(): - web_queue.get() - web_queue.put(self.loader) + ''' + put message to web server queue when web enable + :param key: + :param lock: + :return: + ''' + if self.args.web_server: + if self.web_queue.full(): + self.web_queue.get() + self.web_queue.put(self.loader) def put_case(self, key: str, lock, case): + ''' + put case to case queue, from loadert to runners + :param key: + :param lock: + :param case: + :return: + ''' logger.info(f"{self.key} -- put case {case.bench_name} - {case.ids.id}.{case.ids.subid}.{case.ids.name}", __name__) self.case_queue.put(case) def end_handler(self, error=None): + ''' + when catch the stop signal, end loader, and send the case_end signal + :param error: + :return: + ''' try: if error: logger.error(f"{self.key} end loader with Error - {error}", __name__) else: logger.info(f"{self.key} end loader", __name__) - self.case_queue.put(SIGNAL_CASE_END) + self.case_queue.put(Signal(self.pid, SIGNAL_CASE_END)) except Exception as e: logger.error(f"{self.key} {e}", __name__) diff --git a/haf/logger.py b/haf/logger.py index e519cbe..9ee3f1a 100644 --- a/haf/logger.py +++ b/haf/logger.py @@ -4,11 +4,15 @@ from multiprocessing import Process from haf.busclient import BusClient from haf.config import * +from haf.signal import Signal logger = logging.getLogger(__name__) class Logger(Process): + ''' + Logger + ''' def __init__(self, case_name: str, time_str: str, log_dir: str, bus_client: BusClient, args: tuple): super().__init__() self.daemon = True @@ -20,6 +24,10 @@ def __init__(self, case_name: str, time_str: str, log_dir: str, bus_client: BusC self.time_str = time_str def reconnect_bus(self): + ''' + need reconnect bus by self.bus's infos + :return: + ''' self.bus_client = BusClient(self.bus_client.domain, self.bus_client.port, self.bus_client.auth_key) def run(self): @@ -35,22 +43,45 @@ def run(self): # self.bus_client = BusClient() try: log_queue = self.bus_client.get_log() + logger_end = self.bus_client.get_logger_end() while True: - if log_queue.empty(): + if not logger_end.empty(): + logger_signal = logger_end.get() + # check logger end singal to stop logger, from recorder to logger + if isinstance(logger_signal, Signal) and logger_signal.signal == SIGNAL_LOGGER_END: + while True: + if log_queue.empty(): + break + log = log_queue.get() + self.log_handler(log) + self.end_handler() + break + elif log_queue.empty(): time.sleep(0.001) continue - log = log_queue.get() - self.log_handler(log) + else: + log = log_queue.get() + self.log_handler(log) except KeyboardInterrupt as key_e: print(BANNER_STRS_EXIT) def split_log(self, log): + ''' + split origin log msg + :param log: + :return: + ''' try: return log.rsplit("$%", 2) except Exception as ee: return f"log$%error$%{log}" def log_print(self, log): + ''' + to print the log with format + :param log: + :return: + ''' if self.args.nout: return logger_name = log.get("logger_name") @@ -78,9 +109,14 @@ def log_print(self, log): logger.error(f"<{process}> [{logger_name}]\33[31m | {msg}\33[0m") logger.error(f"<{process}> [{logger_name}]\33[31m <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< \33[0m") elif level=="critical": - logger.critical(f"\33[5m{msg}\33[0m") + logger.critical(f"\33[5m{log}\33[0m") def log_handler(self, log): + ''' + handler log here + :param log: + :return: + ''' self.log_print(log) log = log.get("msg") try: @@ -122,6 +158,13 @@ def error_handler(self, temp1, msg): self.write("error", temp1, msg) def write(self, dir, filename, msg): + ''' + write log to filename's file + :param dir: + :param filename: + :param msg: + :return: + ''' msg = f"{self.now}{msg}\n" dir = f"{self.log_dir}/{self.case_name}/{self.time_str}/{dir}" if not os.path.exists(dir): @@ -150,8 +193,10 @@ def now(self): return timenow def end_handler(self): - result_handler = self.bus_client.get_result() - result_handler.put(SIGNAL_RESULT_END) - case_handler = self.bus_client.get_case() - case_handler.put(SIGNAL_CASE_END) + ''' + when logger end, send signal logger end to main + :return: + ''' + logger_handler = self.bus_client.get_system() + logger_handler.put(Signal(self.pid, SIGNAL_LOGGER_END)) diff --git a/haf/mark.py b/haf/mark.py index 9add646..38bbb27 100644 --- a/haf/mark.py +++ b/haf/mark.py @@ -8,6 +8,9 @@ class TestDecorator: + ''' + test decorate + ''' def __init__(self, name): self.name = name self.mark = "test" @@ -25,6 +28,9 @@ def __call__(self, *args, **kwargs): class SkipDecorator: + ''' + test's skip + ''' def __init__(self, test_item: test=None): self.test_item = test_item self.test_item.run = False @@ -39,6 +45,9 @@ def __call__(self): class ParameterizeDecorator: + ''' + parameter decorate + ''' def __init__(self, params: list=[]): self.params = params self.mark = "test" @@ -55,22 +64,33 @@ def __call__(self, *args, **kwargs): class Locker: + ''' + locker + ''' def __init__(self, bus_client, key, lock: m_lock=None): self.bus_client = bus_client self.key = key self.local_lock = lock def get_lock(self): + ''' + get lock from local_lock + :return: + ''' if self.local_lock: self.local_lock.acquire() return def release_lock(self): + ''' + release lock from local_lock + :return: + ''' if self.local_lock: self.local_lock.release() return - +# the lock decorator def locker(func): @functools.wraps(func) def lock(self, *args, **kwargs): @@ -92,6 +112,7 @@ def lock(self, *args, **kwargs): return lock +# the newlocker: can use with 'with' @contextlib.contextmanager def new_locker(bus_client, key, lock: m_lock=None): locker = Locker(bus_client, key, lock) diff --git a/haf/pluginmanager.py b/haf/pluginmanager.py index 1919bc8..0dba250 100644 --- a/haf/pluginmanager.py +++ b/haf/pluginmanager.py @@ -10,6 +10,13 @@ class PluginManager(object): + ''' + plugin manager + now have : add_option before main() + : load_from_file at loader + : publish_to_sql after test + : start_web_server at program begining + ''' def __init__(self): self.get_plugin_manager() diff --git a/haf/program.py b/haf/program.py index e21c1c6..3e51efb 100644 --- a/haf/program.py +++ b/haf/program.py @@ -23,6 +23,7 @@ from haf.logger import Logger from haf.recorder import Recorder from haf.runner import Runner +from haf.signal import Signal from haf.utils import Utils from haf.pluginmanager import PluginManager, plugin_manager @@ -60,7 +61,6 @@ def _start_loader(self, count: int, bus_client: BusClient): for x in range(count): loader = Loader(bus_client, self.loader_recorder_lock, self.args) loader.start() - time.sleep(0.1) def _start_runner(self, count: int, log_dir: str, bus_client: BusClient): ''' @@ -72,46 +72,73 @@ def _start_runner(self, count: int, log_dir: str, bus_client: BusClient): for x in range(count): runner = Runner(log_dir, bus_client, self.multi_process_locks, self.args) runner.start() - time.sleep(0.5) def _start_recorder(self, bus_client: BusClient, count: int=1, log_dir: str="", time_str: str=""): + ''' + start recorder + :param bus_client: + :param count: + :param log_dir: + :param time_str: + :return: + ''' recorder = Recorder(bus_client, count, self.case_name, time_str, log_dir, self.args.report_template, self.loader_recorder_lock, self.args) recorder.start() - time.sleep(0.1) def _init_logging_module(self, args): + ''' + init logging module + :param args: + :return: + ''' logging.basicConfig(level=logging.INFO if not args.debug else logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s') pass def _init_system_logger(self, log_dir: str, bus_client: BusClient): + ''' + init system logger + :param log_dir: + :param bus_client: + :return: + ''' log = Logger(self.case_name, self.time_str, log_dir, bus_client, self.args) log.start() - time.sleep(0.1) def _init_system_lock(self, args): + ''' + generate some locker + :param args: + :return: + ''' self.loader_recorder_lock = m_lock() self.multi_process_locks = [m_lock() for x in range(4)] if self.bus_client.get_case_count().empty(): self.bus_client.get_case_count().put(0) def _bus_client(self, args): + ''' + init bus client + :param args: + :return: + ''' if isinstance(args.bus_server, list): self.bus_client = BusClient(args.bus_server[1], args.bus_server[2], args.bus_server[0]) elif args.bus_server_port: self.bus_client = BusClient(None, args.bus_server_port, None) else: self.bus_client = BusClient() + self.params_loader = self.bus_client.get_param() def start_main(self): - self.bus_client.get_param().put(SIGNAL_START) + self.params_loader.put(Signal("main", SIGNAL_START)) def stop_main(self): - self.bus_client.get_param().put(SIGNAL_STOP) + self.params_loader.put(Signal("main", SIGNAL_STOP)) def put_loader_msg(self, args): if args.case: for arg in args.case: - self.bus_client.get_param().put({"file_name": arg}) + self.params_loader.put({"file_name": arg}) def _start_web_server(self, args): plugin_manager.start_web_server(args, self.bus_client) @@ -180,24 +207,42 @@ def only_loader(self, args): def wait_end_signal(self, args): try: + time.sleep(2) system_signal = self.bus_client.get_system() while True: if not args.console: if not system_signal.empty(): self.signal = system_signal.get() - if self.signal == SIGNAL_RECORD_END or self.signal == SIGNAL_STOP: - logger.info("main -- stop") - system_signal.put(SIGNAL_RECORD_END) - system_signal.put(SIGNAL_BUS_END) + logger.info(f"program signal {self.signal}") + signal = self.signal.signal if isinstance(self.signal, Signal) else None + # check the end signal from recorder to main + if signal == SIGNAL_RECORD_END or signal == SIGNAL_STOP: + if not args.local_logger: + while True: + if not system_signal.empty(): + signal_logger = system_signal.get() + logger.info(f"program signal {signal_logger}") + signal_logger = signal_logger.signal if isinstance(signal_logger, Signal) else None + # check the logger signal from logger to main + if signal_logger == SIGNAL_LOGGER_END: + logger.info("main -- stop") + system_signal.put(Signal("main", SIGNAL_RECORD_END)) + system_signal.put(Signal("main", SIGNAL_BUS_END)) + break + # if use local logger, just end the main program + else: + logger.info("main -- stop") + system_signal.put(Signal("main", SIGNAL_RECORD_END)) + system_signal.put(Signal("main", SIGNAL_BUS_END)) break time.sleep(0.1) else: cmd = input(f"haf-{PLATFORM_VERSION}# ") if self._run_cmd(cmd): break - time.sleep(1) + time.sleep(0.1) except KeyboardInterrupt as key_inter: - self.bus_client.get_param().put(SIGNAL_STOP) + self.params_loader.put(Signal("main", SIGNAL_STOP)) # TODO: Here need CMDER to support cmd command ... def _run_cmd(self, cmd): diff --git a/haf/recorder.py b/haf/recorder.py index 39a1985..5e56917 100644 --- a/haf/recorder.py +++ b/haf/recorder.py @@ -12,12 +12,16 @@ from haf.mark import locker from haf.pluginmanager import plugin_manager from haf.utils import Utils +from haf.signal import Signal from haf.ext.jinjia2report.report import Jinja2Report logger = Log.getLogger(__name__) class Recorder(Process): + ''' + recorder process + ''' def __init__(self, bus_client: BusClient, runner_count: int=1, case_name:str="", time_str: str="", log_dir="", report_template_path="base", lock: m_lock=None, args=None): super().__init__() self.bus_client = bus_client @@ -57,7 +61,7 @@ def run(self): self.recorder_key = f"{self.pid}$%recorder$%" logger.bind_busclient(self.bus_client) logger.bind_process(self.pid) - logger.set_output(self.args.local_logger, self.args.nout) + logger.set_output(self.args.local_logger, self.args.nout, self.args.debug) logger.info(f"{self.recorder_key} start recorder ", __name__) #self.bus_client = BusClient() self.results_handler = self.bus_client.get_result() @@ -68,6 +72,7 @@ def run(self): if not self.results_handler.empty() : result = self.results_handler.get() logger.debug(f"receive message -- {result}", __name__) + # get result from runner to recorder if isinstance(result, (HttpApiResult, AppResult, WebResult)): if isinstance(result.case, (HttpApiCase, BaseCase, PyCase, WebCase, AppCase)): logger.info(f"{self.recorder_key} {result.case.bench_name}.{result.case.ids.id}.{result.case.ids.subid}.{result.case.ids.name} is {RESULT_GROUP.get(str(result.result), None)}", __name__) @@ -76,17 +81,22 @@ def run(self): logger.info(f"{self.recorder_key} recorder {result.run_error}", __name__) self.complete_case_count += 1 self.result_handler(result) - elif result == SIGNAL_RESULT_END: + # check the runner end or not, from runner to recorder, need check signal count + elif isinstance(result, Signal) and result.signal == SIGNAL_RESULT_END: self.signal_end_count += 1 logger.debug(f"receive message -- {self.signal_end_count}", __name__) if self.runner_count == self.signal_end_count: self.end_handler() break - time.sleep(0.01) + time.sleep(0.001) except Exception: raise FailRecorderException def generate_report(self): + ''' + generate report + :return: + ''' report = Jinja2Report.report(self.results, name=self.report_template_path) Jinja2Report.write_report_to_file(report, self.report_path) report = Jinja2Report.report(self.results, name=self.report_template_path) @@ -94,6 +104,10 @@ def generate_report(self): self.generate_export_report() def generate_export_report(self): + ''' + generate email export report + :return: + ''' if self.args: if hasattr(self.args, "report_export_template") and self.args.report_export_template: report = Jinja2Report.report(self.results, name=self.args.report_export_template) @@ -105,6 +119,10 @@ def generate_export_report(self): Jinja2Report.write_report_to_file(report, f"{self.log_dir}/report-export.html") def end_handler(self): + ''' + when recorder end, publish results + :return: + ''' logger.debug("on record handle end", __name__) self.on_recorder_stop() self.publish_results() @@ -115,8 +133,15 @@ def end_handler(self): self.send_record_end_signal() def send_record_end_signal(self): + ''' + send end signal, from recorder to logger/main + :return: + ''' + logger.info(f"{self.recorder_key} send record end signal to main", __name__) system_handler = self.bus_client.get_system() - system_handler.put(SIGNAL_RECORD_END) + system_handler.put(Signal(self.pid, SIGNAL_RECORD_END)) + logger_end = self.bus_client.get_logger_end() + logger_end.put(Signal(self.pid, SIGNAL_LOGGER_END)) def on_case_pass(self, suite_name): self.results.passed += 1 @@ -143,6 +168,11 @@ def on_case_error(self, suite_name): self.results.summary[suite_name].all += 1 def check_case_result(self, result): + ''' + check case's result is ok or not + :param result: + :return: + ''' suite_name = result.case.bench_name if suite_name not in self.results.summary.keys(): self.results.summary[suite_name] = Summary(suite_name, result.case.request.host_port if isinstance(result, HttpApiResult) else None) @@ -176,28 +206,48 @@ def add_result_to_suite(self, result): self.results.details[result.case.bench_name] = suite def count_case(self, key: str, lock: m_lock=None): + ''' + put case's commplete count to loader, from recorder to loader + :param key: + :param lock: + :return: + ''' logger.debug(f"put case count {self.complete_case_count}", __name__) if not self.case_count.empty(): self.case_count.get() self.case_count.put(self.complete_case_count) def result_handler(self, result): + ''' + result handler, include add to suite,check case result, publish result + :param result: + :return: + ''' self.count_case("case_count", self.lock) self.add_result_to_suite(result) self.check_case_result(result) self.publish_results() def publish_results(self): + ''' + publish results to web server + :return: + ''' logger.debug(f"publish results now...", __name__) - if self.publish_result.full(): - self.publish_result.get() - self.publish_result.put(self.results) + if self.args.web_server: + if self.publish_result.full(): + self.publish_result.get() + self.publish_result.put(self.results) if self.publish_result_main.full(): self.publish_result_main.get() self.publish_result_main.put(self.results) # show end results here before publish to mysql def show_in_cs(self): + ''' + show case in command + :return: + ''' result_summary = "|{:^8}|{:^8}|{:^8}|{:^8}|{:^8}|".format(self.results.passed, self.results.failed, self.results.skip, self.results.error, \ self.results.all) if not self.args.nout: diff --git a/haf/runner.py b/haf/runner.py index e1930c0..c63a10f 100644 --- a/haf/runner.py +++ b/haf/runner.py @@ -18,6 +18,7 @@ from haf.result import HttpApiResult, AppResult, WebResult from haf.suite import HttpApiSuite, AppSuite from haf.utils import Utils, Signal, SignalThread +from haf.signal import Signal as SignalTemp from haf.webhelper import * import traceback import asyncio @@ -53,6 +54,11 @@ def init_runner(self, case: BaseCase): self.bench = self.get_bench(case) def get_bench(self, case: BaseCase): + ''' + init bench, if exist, get bench + :param case: + :return: + ''' bench = self.benchs.get(case.bench_name, None) if bench is None : if isinstance(case, HttpApiCase): @@ -70,23 +76,47 @@ def get_bench(self, case: BaseCase): @locker def put_result(self, key: str, lock: m_lock=None, result: HttpApiResult=HttpApiResult()): + ''' + put result to recorder, from runner to recorder, need lock + :param key: + :param lock: + :param result: + :return: + ''' logger.info(f"{self.key} : runner {self.pid} put result {result.case.ids.id}.{result.case.ids.subid}.{result.case.ids.name}", __name__) self.result_handler_queue.put(result) @locker def put_web_message(self, key: str, lock: m_lock=None): + ''' + put web message to web server + :param key: + :param lock: + :return: + ''' if self.web_queue.full(): self.web_queue.get() self.web_queue.put(self.runner) def put_case_back(self, key:str, case): + ''' + put can not run case to loader to republish, from runner to loader, need lock + :param key: + :param case: + :return: + ''' logger.info(f"{self.runner_key} : runner put case {case.ids.id}.{case.ids.subid}-{case.ids.name} for dependent : {case.dependent}", __name__) with new_locker(self.bus_client, key, self.locks[1]): self.case_back_queue.put(case) import random - time.sleep(random.randint(1,2)) + time.sleep(random.randint(0,1)) def result_handler(self, result): + ''' + result handler + :param result: + :return: + ''' if isinstance(result, HttpApiResult) or isinstance(result, AppResult) or isinstance(result, WebResult): if result.case.run == CASE_SKIP: self.runner["skip"] += 1 @@ -111,6 +141,10 @@ def result_handler(self, result): self.put_web_message("web", self.locks[3]) def signal_service(self): + ''' + signal service to make runner run without enough cases + :return: + ''' self.signal = Signal() self.st = SignalThread(self.signal, 0.1) self.st.start() @@ -120,6 +154,12 @@ def stop_signal(self): self.st._stop() def run_loop(self, cases): + ''' + TODO: need use aiohttp to make it work + run loop, sync + :param cases: + :return: + ''' self.loop = asyncio.get_event_loop() results = self.loop.run_until_complete(self.run_cases(cases)) for result in results: @@ -133,7 +173,7 @@ def run(self): self.loop = None logger.bind_busclient(self.bus_client) logger.bind_process(self.pid) - logger.set_output(self.args.local_logger, self.args.nout) + logger.set_output(self.args.local_logger, self.args.nout, self.args.debug) logger.info(f"{self.runner_key} start runner", __name__) self.web_queue = self.bus_client.get_publish_runner() @@ -150,7 +190,7 @@ def run(self): if not self.case_handler_queue.empty() : with new_locker(self.bus_client, "case_runner", self.locks[0]): case = self.case_handler_queue.get() - if case == SIGNAL_CASE_END: + if isinstance(case, SignalTemp) and case.signal == SIGNAL_CASE_END: case_end = True if isinstance(case, HttpApiCase): cases.append(case) @@ -176,6 +216,11 @@ def run(self): raise FailRunnerException async def run_cases(self, local_cases): + ''' + run cases + :param local_cases: + :return: + ''' done, pending = await asyncio.wait([self.run_case(local_case) for local_case in local_cases]) results = [] for r in done: @@ -183,6 +228,11 @@ async def run_cases(self, local_cases): return results async def run_case(self, local_case): + ''' + run case + :param local_case: + :return: + ''' if isinstance(local_case, HttpApiCase): result = HttpApiResult() elif isinstance(local_case, AppCase): @@ -230,9 +280,13 @@ async def run_case(self, local_case): return result def end_handler(self): + ''' + send result end and case end signal to main/loader, from runner to main + :return: + ''' logger.info(f"{self.runner_key} : end runner", __name__) - self.result_handler_queue.put(SIGNAL_RESULT_END) - self.case_handler_queue.put(SIGNAL_CASE_END) + self.result_handler_queue.put(SignalTemp(self.pid, SIGNAL_RESULT_END)) + self.case_handler_queue.put(SignalTemp(self.pid, SIGNAL_CASE_END)) class BaseRunner(object): @@ -240,6 +294,11 @@ def __init__(self, bench:BaseBench): self.bench = bench def check_case_run_here(self, case): + ''' + check case can run here or not + :param case: + :return: + ''' logger.debug(f"Base Runner check case run here {case.dependent}", __name__) if not case.dependent or len(case.dependent) == 0: return True @@ -256,6 +315,11 @@ def check_case_run_here(self, case): return False def check_case_filter(self, case): + ''' + check case is in the filter + :param case: + :return: + ''' logger.debug(f"case <{case.ids.name}> check in [{self.bench.args.filter_case}]", __name__) filter_cases = self.bench.args.filter_case if filter_cases is None or filter_cases=='None': @@ -275,9 +339,19 @@ def check_case_error(self, case): return case.error == CASE_ERROR def get_dependence_case_from_bench(self, dependent): + ''' + get dependency + :param dependent: + :return: + ''' return self.bench.cases.get(dependent) def reuse_with_dependent(self, new_attr): + ''' + TODO: not complete + :param new_attr: + :return: + ''' begin = "<@begin@>" end = "<@end@>" if isinstance(new_attr, str): @@ -534,6 +608,14 @@ async def run(self, case: AppCase): return result def run_operation(self, page, operation, paths, info): + ''' + run the appium stages + :param page: + :param operation: + :param paths: + :param info: + :return: + ''' if operation== OPERATION_APP_CLICK: page.click(paths) elif operation == OPERATION_APP_SENDKEYS: @@ -677,6 +759,14 @@ async def run(self, case: WebCase): return result def run_operation(self, page, operation, paths, info): + ''' + run the appium stages + :param page: + :param operation: + :param paths: + :param info: + :return: + ''' if operation== OPERATION_WEB_CLICK: page.click(paths) elif operation == OPERATION_WEB_SENDKEYS: diff --git a/haf/signal.py b/haf/signal.py new file mode 100644 index 0000000..e176ca7 --- /dev/null +++ b/haf/signal.py @@ -0,0 +1,9 @@ +''' +file name : signal +''' + + +class Signal(object): + def __init__(self, sender, signal): + self.sender = sender + self.signal = signal \ No newline at end of file diff --git a/haf/suite.py b/haf/suite.py index 1aaa530..46a02df 100644 --- a/haf/suite.py +++ b/haf/suite.py @@ -10,6 +10,9 @@ def __init__(self): class HttpApiSuite(BaseSuite): + ''' + http api suite + ''' def __init__(self): super().__init__() self.name = "" @@ -23,6 +26,9 @@ def add_case(self, case): class AppSuite(BaseSuite): + ''' + app suite + ''' def __init__(self): super().__init__() self.name = "" diff --git a/haf/utils.py b/haf/utils.py index 8ebc1c7..47c34b9 100644 --- a/haf/utils.py +++ b/haf/utils.py @@ -451,7 +451,7 @@ def __init__(self, signal, signal_change_time=0.1): def run(self): while True: - logger.debug(f"signal of {self.signal.signal}", __name__) + # logger.debug(f"signal of {self.signal.signal}", __name__) self.signal.change_status() time.sleep(self.time) diff --git a/haf/webhelper.py b/haf/webhelper.py index d4fdd17..2b2600b 100644 --- a/haf/webhelper.py +++ b/haf/webhelper.py @@ -4,6 +4,9 @@ class WebBasePage: + ''' + web base page + ''' DEFAULT_TIMEOUT = 3 def __init__(self, driver): From cd475a37238ba5525752b1d5322634d20a51338c Mon Sep 17 00:00:00 2001 From: tsbxmw <1050636648@qq.com> Date: Wed, 17 Apr 2019 17:08:05 +0800 Subject: [PATCH 2/2] bug fix: no web-server params should be check first at runner&recorder --- haf/loader.py | 2 +- haf/recorder.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/haf/loader.py b/haf/loader.py index 1818bcf..423ef63 100644 --- a/haf/loader.py +++ b/haf/loader.py @@ -192,7 +192,7 @@ def put_web_message(self, key: str, lock=None): :param lock: :return: ''' - if self.args.web_server: + if hasattr(self.args, "web_server") and self.args.web_server: if self.web_queue.full(): self.web_queue.get() self.web_queue.put(self.loader) diff --git a/haf/recorder.py b/haf/recorder.py index 5e56917..6893504 100644 --- a/haf/recorder.py +++ b/haf/recorder.py @@ -234,7 +234,7 @@ def publish_results(self): :return: ''' logger.debug(f"publish results now...", __name__) - if self.args.web_server: + if hasattr(self.args, "web_server") and self.args.web_server: if self.publish_result.full(): self.publish_result.get() self.publish_result.put(self.results)