2017-10-09 22:44:55 -04:00
|
|
|
# Copyright 2015-2017 Espressif Systems (Shanghai) PTE LTD
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http:#www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
|
|
|
|
""" Interface for test cases. """
|
|
|
|
import os
|
|
|
|
import time
|
|
|
|
import traceback
|
|
|
|
import functools
|
2019-03-07 07:17:43 -05:00
|
|
|
import socket
|
|
|
|
from datetime import datetime
|
2017-10-09 22:44:55 -04:00
|
|
|
|
2018-07-13 04:47:42 -04:00
|
|
|
import junit_xml
|
2017-10-09 22:44:55 -04:00
|
|
|
|
2019-12-27 04:58:36 -05:00
|
|
|
from . import Env
|
|
|
|
from . import DUT
|
|
|
|
from . import App
|
|
|
|
from . import Utility
|
2017-10-09 22:44:55 -04:00
|
|
|
|
|
|
|
|
|
|
|
class DefaultEnvConfig(object):
|
|
|
|
"""
|
|
|
|
default test configs. There're 3 places to set configs, priority is (high -> low):
|
|
|
|
|
|
|
|
1. overwrite set by caller of test method
|
|
|
|
2. values set by test_method decorator
|
|
|
|
3. default env config get from this class
|
|
|
|
"""
|
|
|
|
DEFAULT_CONFIG = {
|
|
|
|
"app": App.BaseApp,
|
|
|
|
"dut": DUT.BaseDUT,
|
|
|
|
"env_tag": "default",
|
|
|
|
"env_config_file": None,
|
|
|
|
"test_suite_name": None,
|
|
|
|
}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def set_default_config(cls, **kwargs):
|
|
|
|
"""
|
|
|
|
:param kwargs: configs need to be updated
|
|
|
|
:return: None
|
|
|
|
"""
|
|
|
|
cls.DEFAULT_CONFIG.update(kwargs)
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_default_config(cls):
|
|
|
|
"""
|
|
|
|
:return: current default config
|
|
|
|
"""
|
|
|
|
return cls.DEFAULT_CONFIG.copy()
|
|
|
|
|
|
|
|
|
|
|
|
set_default_config = DefaultEnvConfig.set_default_config
|
|
|
|
get_default_config = DefaultEnvConfig.get_default_config
|
|
|
|
|
2018-07-13 04:47:42 -04:00
|
|
|
MANDATORY_INFO = {
|
|
|
|
"execution_time": 1,
|
|
|
|
"env_tag": "default",
|
|
|
|
"category": "function",
|
|
|
|
"ignore": False,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class JunitReport(object):
|
|
|
|
# wrapper for junit test report
|
2019-03-07 07:17:43 -05:00
|
|
|
# TODO: JunitReport methods are not thread safe (although not likely to be used this way).
|
2018-07-13 04:47:42 -04:00
|
|
|
|
|
|
|
JUNIT_FILE_NAME = "XUNIT_RESULT.xml"
|
|
|
|
JUNIT_DEFAULT_TEST_SUITE = "test-suite"
|
2019-03-07 07:17:43 -05:00
|
|
|
JUNIT_TEST_SUITE = junit_xml.TestSuite(JUNIT_DEFAULT_TEST_SUITE,
|
|
|
|
hostname=socket.gethostname(),
|
|
|
|
timestamp=datetime.utcnow().isoformat())
|
2018-07-13 04:47:42 -04:00
|
|
|
JUNIT_CURRENT_TEST_CASE = None
|
|
|
|
_TEST_CASE_CREATED_TS = 0
|
2017-10-09 22:44:55 -04:00
|
|
|
|
|
|
|
@classmethod
|
2018-07-13 04:47:42 -04:00
|
|
|
def output_report(cls, junit_file_path):
|
|
|
|
""" Output current test result to file. """
|
|
|
|
with open(os.path.join(junit_file_path, cls.JUNIT_FILE_NAME), "w") as f:
|
|
|
|
cls.JUNIT_TEST_SUITE.to_file(f, [cls.JUNIT_TEST_SUITE], prettyprint=False)
|
2017-10-09 22:44:55 -04:00
|
|
|
|
|
|
|
@classmethod
|
2018-07-13 04:47:42 -04:00
|
|
|
def get_current_test_case(cls):
|
2017-10-09 22:44:55 -04:00
|
|
|
"""
|
2018-07-13 04:47:42 -04:00
|
|
|
By default, the test framework will handle junit test report automatically.
|
|
|
|
While some test case might want to update some info to test report.
|
|
|
|
They can use this method to get current test case created by test framework.
|
|
|
|
|
|
|
|
:return: current junit test case instance created by ``JunitTestReport.create_test_case``
|
2017-10-09 22:44:55 -04:00
|
|
|
"""
|
2018-07-13 04:47:42 -04:00
|
|
|
return cls.JUNIT_CURRENT_TEST_CASE
|
2017-10-09 22:44:55 -04:00
|
|
|
|
|
|
|
@classmethod
|
2018-07-13 04:47:42 -04:00
|
|
|
def test_case_finish(cls, test_case):
|
2017-10-09 22:44:55 -04:00
|
|
|
"""
|
2018-07-13 04:47:42 -04:00
|
|
|
Append the test case to test suite so it can be output to file.
|
|
|
|
Execution time will be automatically updated (compared to ``create_test_case``).
|
2017-10-09 22:44:55 -04:00
|
|
|
"""
|
2018-07-13 04:47:42 -04:00
|
|
|
test_case.elapsed_sec = time.time() - cls._TEST_CASE_CREATED_TS
|
|
|
|
cls.JUNIT_TEST_SUITE.test_cases.append(test_case)
|
2017-10-09 22:44:55 -04:00
|
|
|
|
2018-07-13 04:47:42 -04:00
|
|
|
@classmethod
|
|
|
|
def create_test_case(cls, name):
|
|
|
|
"""
|
|
|
|
Extend ``junit_xml.TestCase`` with:
|
2017-10-09 22:44:55 -04:00
|
|
|
|
2018-07-13 04:47:42 -04:00
|
|
|
1. save create test case so it can be get by ``get_current_test_case``
|
|
|
|
2. log create timestamp, so ``elapsed_sec`` can be auto updated in ``test_case_finish``.
|
2017-10-09 22:44:55 -04:00
|
|
|
|
2018-07-13 04:47:42 -04:00
|
|
|
:param name: test case name
|
|
|
|
:return: instance of ``junit_xml.TestCase``
|
|
|
|
"""
|
|
|
|
# set stdout to empty string, so we can always append string to stdout.
|
|
|
|
# It won't affect output logic. If stdout is empty, it won't be put to report.
|
|
|
|
test_case = junit_xml.TestCase(name, stdout="")
|
|
|
|
cls.JUNIT_CURRENT_TEST_CASE = test_case
|
|
|
|
cls._TEST_CASE_CREATED_TS = time.time()
|
|
|
|
return test_case
|
2017-10-09 22:44:55 -04:00
|
|
|
|
2019-03-07 07:17:43 -05:00
|
|
|
@classmethod
|
|
|
|
def update_performance(cls, performance_items):
|
|
|
|
"""
|
|
|
|
Update performance results to ``stdout`` of current test case.
|
|
|
|
|
|
|
|
:param performance_items: a list of performance items. each performance item is a key-value pair.
|
|
|
|
"""
|
|
|
|
assert cls.JUNIT_CURRENT_TEST_CASE
|
|
|
|
|
|
|
|
for item in performance_items:
|
|
|
|
cls.JUNIT_CURRENT_TEST_CASE.stdout += "[{}]: {}\n".format(item[0], item[1])
|
|
|
|
|
2017-10-09 22:44:55 -04:00
|
|
|
|
|
|
|
def test_method(**kwargs):
|
|
|
|
"""
|
|
|
|
decorator for test case function.
|
|
|
|
The following keyword arguments are pre-defined.
|
|
|
|
Any other keyword arguments will be regarded as filter for the test case,
|
|
|
|
able to access them by ``case_info`` attribute of test method.
|
|
|
|
|
|
|
|
:keyword app: class for test app. see :doc:`App <App>` for details
|
|
|
|
:keyword dut: class for current dut. see :doc:`DUT <DUT>` for details
|
|
|
|
:keyword env_tag: name for test environment, used to select configs from config file
|
|
|
|
:keyword env_config_file: test env config file. usually will not set this keyword when define case
|
|
|
|
:keyword test_suite_name: test suite name, used for generating log folder name and adding xunit format test result.
|
|
|
|
usually will not set this keyword when define case
|
2018-07-13 04:47:42 -04:00
|
|
|
:keyword junit_report_by_case: By default the test fw will handle junit report generation.
|
|
|
|
In some cases, one test function might test many test cases.
|
|
|
|
If this flag is set, test case can update junit report by its own.
|
2017-10-09 22:44:55 -04:00
|
|
|
"""
|
2020-04-03 04:16:40 -04:00
|
|
|
|
2017-10-09 22:44:55 -04:00
|
|
|
def test(test_func):
|
|
|
|
|
|
|
|
case_info = MANDATORY_INFO.copy()
|
2018-07-03 10:00:09 -04:00
|
|
|
case_info["name"] = case_info["ID"] = test_func.__name__
|
2018-07-13 04:47:42 -04:00
|
|
|
case_info["junit_report_by_case"] = False
|
2017-10-09 22:44:55 -04:00
|
|
|
case_info.update(kwargs)
|
|
|
|
|
|
|
|
@functools.wraps(test_func)
|
|
|
|
def handle_test(extra_data=None, **overwrite):
|
|
|
|
"""
|
|
|
|
create env, run test and record test results
|
|
|
|
|
|
|
|
:param extra_data: extra data that runner or main passed to test case
|
|
|
|
:param overwrite: args that runner or main want to overwrite
|
|
|
|
:return: None
|
|
|
|
"""
|
2018-01-16 09:16:03 -05:00
|
|
|
# create env instance
|
|
|
|
env_config = DefaultEnvConfig.get_default_config()
|
|
|
|
for key in kwargs:
|
|
|
|
if key in env_config:
|
|
|
|
env_config[key] = kwargs[key]
|
|
|
|
|
2017-10-09 22:44:55 -04:00
|
|
|
env_config.update(overwrite)
|
2020-04-03 04:16:40 -04:00
|
|
|
|
|
|
|
# FIXME: CI need more variable here. add `if CI_TARGET: ...` later with CI.
|
|
|
|
target = env_config['target'] if 'target' in env_config else kwargs['target']
|
|
|
|
dut_dict = kwargs['dut_dict']
|
|
|
|
if isinstance(target, list):
|
|
|
|
target = target[0]
|
|
|
|
elif isinstance(target, str):
|
|
|
|
target = target
|
|
|
|
else:
|
|
|
|
raise TypeError('keyword targets can only be list or str')
|
|
|
|
if target not in dut_dict:
|
|
|
|
raise Exception('target can only be {%s}' % ', '.join(dut_dict.keys()))
|
|
|
|
|
|
|
|
dut = dut_dict[target]
|
|
|
|
try:
|
|
|
|
# try to config the default behavior of erase nvs
|
|
|
|
dut.ERASE_NVS = kwargs['erase_nvs']
|
|
|
|
except AttributeError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
env_config['dut'] = dut
|
2017-10-09 22:44:55 -04:00
|
|
|
env_inst = Env.Env(**env_config)
|
2018-07-13 04:47:42 -04:00
|
|
|
|
2017-10-09 22:44:55 -04:00
|
|
|
# prepare for xunit test results
|
2018-07-13 04:47:42 -04:00
|
|
|
junit_file_path = env_inst.app_cls.get_log_folder(env_config["test_suite_name"])
|
2019-04-01 03:16:47 -04:00
|
|
|
junit_test_case = JunitReport.create_test_case(case_info["ID"])
|
2018-06-15 05:33:39 -04:00
|
|
|
result = False
|
2018-07-13 04:47:42 -04:00
|
|
|
|
2017-10-09 22:44:55 -04:00
|
|
|
try:
|
2017-11-07 23:27:57 -05:00
|
|
|
Utility.console_log("starting running test: " + test_func.__name__, color="green")
|
2017-10-09 22:44:55 -04:00
|
|
|
# execute test function
|
|
|
|
test_func(env_inst, extra_data)
|
|
|
|
# if finish without exception, test result is True
|
|
|
|
result = True
|
|
|
|
except Exception as e:
|
|
|
|
# handle all the exceptions here
|
|
|
|
traceback.print_exc()
|
|
|
|
# log failure
|
2018-07-13 04:47:42 -04:00
|
|
|
junit_test_case.add_failure_info(str(e) + ":\r\n" + traceback.format_exc())
|
2017-10-09 22:44:55 -04:00
|
|
|
finally:
|
2019-03-16 08:07:52 -04:00
|
|
|
# do close all DUTs, if result is False then print DUT debug info
|
|
|
|
close_errors = env_inst.close(dut_debug=(not result))
|
|
|
|
# We have a hook in DUT close, allow DUT to raise error to fail test case.
|
|
|
|
# For example, we don't allow DUT exception (reset) during test execution.
|
|
|
|
# We don't want to implement in exception detection in test function logic,
|
|
|
|
# as we need to add it to every test case.
|
|
|
|
# We can implement it in DUT receive thread,
|
|
|
|
# and raise exception in DUT close to fail test case if reset detected.
|
|
|
|
if close_errors:
|
|
|
|
for error in close_errors:
|
|
|
|
junit_test_case.add_failure_info(str(error))
|
|
|
|
result = False
|
2018-07-13 04:47:42 -04:00
|
|
|
if not case_info["junit_report_by_case"]:
|
|
|
|
JunitReport.test_case_finish(junit_test_case)
|
|
|
|
|
2017-10-09 22:44:55 -04:00
|
|
|
# end case and output result
|
2018-07-13 04:47:42 -04:00
|
|
|
JunitReport.output_report(junit_file_path)
|
2017-10-09 22:44:55 -04:00
|
|
|
|
|
|
|
if result:
|
2017-11-07 23:27:57 -05:00
|
|
|
Utility.console_log("Test Succeed: " + test_func.__name__, color="green")
|
2017-10-09 22:44:55 -04:00
|
|
|
else:
|
2017-11-07 23:27:57 -05:00
|
|
|
Utility.console_log(("Test Fail: " + test_func.__name__), color="red")
|
2017-10-09 22:44:55 -04:00
|
|
|
return result
|
|
|
|
|
|
|
|
handle_test.case_info = case_info
|
|
|
|
handle_test.test_method = True
|
|
|
|
return handle_test
|
2020-04-03 04:16:40 -04:00
|
|
|
|
2017-10-09 22:44:55 -04:00
|
|
|
return test
|