refactor: add typing with pydantic

This commit is contained in:
debugtalk
2020-04-22 21:16:58 +08:00
parent 1d63acfc49
commit 7f693d3521
11 changed files with 171 additions and 114 deletions

View File

@@ -73,15 +73,15 @@ def main_run(args):
err_code = 0
try:
for path in args.testfile_paths:
summary = runner.run_path(path, dot_env_path=args.dot_env_path)
testsuite_summary = runner.run_path(path, dot_env_path=args.dot_env_path)
report_dir = args.report_dir or os.path.join(os.getcwd(), "reports")
gen_html_report(
summary,
testsuite_summary,
report_template=args.report_template,
report_dir=report_dir,
report_file=args.report_file
)
err_code |= (0 if summary and summary["success"] else 1)
err_code |= (0 if testsuite_summary and testsuite_summary.success else 1)
except Exception as ex:
logger.error(f"!!!!!!!!!! exception stage: {runner.exception_stage} !!!!!!!!!!\n{str(ex)}")
err_code = 1

View File

@@ -11,6 +11,7 @@ from requests.exceptions import (InvalidSchema, InvalidURL, MissingSchema,
from httprunner import response
from httprunner.utils import lower_dict_keys, omit_long_data
from httprunner.v3.schema import MetaData, RequestStat
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
@@ -107,9 +108,8 @@ class HttpSession(requests.Session):
def init_meta_data(self):
""" initialize meta_data, it will store detail data of request and response
"""
self.meta_data = {
"name": "",
"data": [
self.meta_data = MetaData(
data=[
{
"request": {
"url": "N/A",
@@ -124,19 +124,15 @@ class HttpSession(requests.Session):
}
}
],
"stat": {
"content_size": "N/A",
"response_time_ms": "N/A",
"elapsed_ms": "N/A",
}
}
stat=RequestStat()
)
def update_last_req_resp_record(self, resp_obj):
"""
update request and response info from Response() object.
"""
self.meta_data["data"].pop()
self.meta_data["data"].append(get_req_resp_record(resp_obj))
self.meta_data.data.pop()
self.meta_data.data.append(get_req_resp_record(resp_obj))
def request(self, method, url, name=None, **kwargs):
"""
@@ -180,13 +176,13 @@ class HttpSession(requests.Session):
self.init_meta_data()
# record test name
self.meta_data["name"] = name
self.meta_data.name = name
# record original request info
self.meta_data["data"][0]["request"]["method"] = method
self.meta_data["data"][0]["request"]["url"] = url
self.meta_data.data[0]["request"]["method"] = method
self.meta_data.data[0]["request"]["url"] = url
kwargs.setdefault("timeout", 120)
self.meta_data["data"][0]["request"].update(kwargs)
self.meta_data.data[0]["request"].update(kwargs)
start_timestamp = time.time()
response = self._send_request_safe_mode(method, url, **kwargs)
@@ -200,15 +196,13 @@ class HttpSession(requests.Session):
content_size = len(response.content or "")
# record the consumed time
self.meta_data["stat"] = {
"response_time_ms": response_time_ms,
"elapsed_ms": response.elapsed.microseconds / 1000.0,
"content_size": content_size
}
self.meta_data.stat.response_time_ms = response_time_ms
self.meta_data.stat.elapsed_ms = response.elapsed.microseconds / 1000.0
self.meta_data.stat.content_size = content_size
# record request and response histories, include 30X redirection
response_list = response.history + [response]
self.meta_data["data"] = [
self.meta_data.data = [
get_req_resp_record(resp_obj)
for resp_obj in response_list
]

View File

@@ -6,20 +6,21 @@ from jinja2 import Template
from loguru import logger
from httprunner.exceptions import SummaryEmpty
from httprunner.v3.schema import TestSuiteSummary
def gen_html_report(summary, report_template=None, report_dir=None, report_file=None):
def gen_html_report(testsuite_summary: TestSuiteSummary, report_template=None, report_dir=None, report_file=None):
""" render html report with specified report name and template
Args:
summary (dict): test result summary data
testsuite_summary (dict): testsuite result summary data
report_template (str): specify html report template path, template should be in Jinja2 format.
report_dir (str): specify html report save directory
report_file (str): specify html report file path, this has higher priority than specifying report dir.
"""
if not summary["time"] or summary["stat"]["testcases"]["total"] == 0:
logger.error(f"test result summary is empty ! {summary}")
if not testsuite_summary.time or testsuite_summary.stat.testcases["total"] == 0:
logger.error(f"test result testsuite_summary is empty ! {testsuite_summary}")
raise SummaryEmpty
if not report_template:
@@ -33,9 +34,9 @@ def gen_html_report(summary, report_template=None, report_dir=None, report_file=
logger.info("Start to render Html report ...")
start_at_timestamp = summary["time"]["start_at"]
start_at_timestamp = testsuite_summary.time.start_at
utc_time_iso_8601_str = datetime.utcfromtimestamp(start_at_timestamp).isoformat()
summary["time"]["start_datetime"] = utc_time_iso_8601_str
testsuite_summary.time.start_datetime = utc_time_iso_8601_str
if report_file:
report_dir = os.path.dirname(report_file)
@@ -55,7 +56,7 @@ def gen_html_report(summary, report_template=None, report_dir=None, report_file=
rendered_content = Template(
template_content,
extensions=["jinja2.ext.loopcontrols"]
).render(summary)
).render(testsuite_summary.dict())
fp_w.write(rendered_content)
logger.info(f"Generated Html report: {report_path}")

View File

@@ -3,6 +3,8 @@ import unittest
from loguru import logger
from httprunner.v3.schema import Record
class HtmlTestResult(unittest.TextTestResult):
""" A html result class that can generate formatted html results.
@@ -13,13 +15,13 @@ class HtmlTestResult(unittest.TextTestResult):
self.records = []
def _record_test(self, test, status, attachment=''):
data = {
'name': test.shortDescription(),
'status': status,
'attachment': attachment,
"meta_datas": test.meta_datas
}
self.records.append(data)
record = Record(
name=test.shortDescription(),
status=status,
attachment=attachment,
meta_datas=test.meta_datas
)
self.records.append(record)
def startTestRun(self):
self.start_at = time.time()

View File

@@ -201,7 +201,7 @@
{% for record in test_suite_summary.records %}
{% set record_index = "{}_{}".format(suite_index, loop.index) %}
{% set record_meta_datas = record.meta_datas_expanded %}
{% set record_meta_datas = record.meta_datas %}
<tr id="record_{{record_index}}">
<th class="{{record.status}}" style="width:5em;">{{record.status}}</th>
<td colspan="2">{{record.name}}</td>

View File

@@ -1,10 +1,13 @@
import json
from base64 import b64encode
from collections import Iterable
from typing import List
from jinja2 import escape
from requests.cookies import RequestsCookieJar
from httprunner.v3.schema import TestSuiteSummary, MetaData
def dumps_json(value):
""" dumps json value to indented string
@@ -164,20 +167,20 @@ def __expand_meta_datas(meta_datas, meta_datas_expanded):
[dict1, dict2, dict3]
"""
if isinstance(meta_datas, dict):
if isinstance(meta_datas, MetaData):
meta_datas_expanded.append(meta_datas)
elif isinstance(meta_datas, list):
for meta_data in meta_datas:
__expand_meta_datas(meta_data, meta_datas_expanded)
def __get_total_response_time(meta_datas_expanded):
def __get_total_response_time(meta_datas: List[MetaData]):
""" caculate total response time of all meta_datas
"""
try:
response_time = 0
for meta_data in meta_datas_expanded:
response_time += meta_data["stat"]["response_time_ms"]
for meta_data in meta_datas:
response_time += meta_data.stat.response_time_ms
return "{:.2f}".format(response_time)
@@ -186,30 +189,24 @@ def __get_total_response_time(meta_datas_expanded):
return "N/A"
def __stringify_meta_datas(meta_datas):
def __stringify_meta_datas(meta_datas: List[MetaData]):
if isinstance(meta_datas, list):
for _meta_data in meta_datas:
__stringify_meta_datas(_meta_data)
elif isinstance(meta_datas, dict):
data_list = meta_datas["data"]
for meta_data in meta_datas:
data_list = meta_data.data
for data in data_list:
__stringify_request(data["request"])
__stringify_response(data["response"])
def stringify_summary(summary):
def stringify_summary(testsuite_summary: TestSuiteSummary):
""" stringify summary, in order to dump json file and generate html report.
"""
for index, suite_summary in enumerate(summary["details"]):
for index, testcase_summary in enumerate(testsuite_summary.details):
if not suite_summary.get("name"):
suite_summary["name"] = f"testcase {index}"
if not testcase_summary.name:
testcase_summary.name = f"testcase {index}"
for record in suite_summary.get("records"):
meta_datas = record['meta_datas']
for record in testcase_summary.records:
meta_datas = record.meta_datas
__stringify_meta_datas(meta_datas)
meta_datas_expanded = []
__expand_meta_datas(meta_datas, meta_datas_expanded)
record["meta_datas_expanded"] = meta_datas_expanded
record["response_time"] = __get_total_response_time(meta_datas_expanded)
record.response_time = __get_total_response_time(meta_datas)

View File

@@ -1,6 +1,8 @@
import platform
from httprunner import __version__
from httprunner.report.html.result import HtmlTestResult
from httprunner.v3.schema import TestCaseSummary, TestCaseStat, TestCaseTime, TestCaseInOut
def get_platform():
@@ -38,7 +40,7 @@ def aggregate_stat(origin_stat, new_stat):
origin_stat[key] += new_stat[key]
def get_summary(result):
def get_summary(result: HtmlTestResult) -> TestCaseSummary:
""" get summary from test result
Args:
@@ -55,28 +57,20 @@ def get_summary(result):
}
"""
summary = {
"success": result.wasSuccessful(),
"stat": {
'total': result.testsRun,
'failures': len(result.failures),
'errors': len(result.errors),
'skipped': len(result.skipped),
'expectedFailures': len(result.expectedFailures),
'unexpectedSuccesses': len(result.unexpectedSuccesses)
}
}
summary["stat"]["successes"] = summary["stat"]["total"] \
- summary["stat"]["failures"] \
- summary["stat"]["errors"] \
- summary["stat"]["skipped"] \
- summary["stat"]["expectedFailures"] \
- summary["stat"]["unexpectedSuccesses"]
summary["time"] = {
'start_at': result.start_at,
'duration': result.duration
}
summary["records"] = result.records
return summary
return TestCaseSummary(
success=result.wasSuccessful(),
stat=TestCaseStat(
total=result.testsRun,
failures=len(result.failures),
errors=len(result.errors),
skipped=len(result.skipped),
expectedFailures=len(result.expectedFailures),
unexpectedSuccesses=len(result.unexpectedSuccesses)
),
time=TestCaseTime(
start_at=result.start_at,
duration=result.duration
),
records=result.records,
in_out=TestCaseInOut()
)

View File

@@ -7,7 +7,7 @@ from loguru import logger
from httprunner import report, loader, utils, exceptions, __version__
from httprunner.v3.runner import TestCaseRunner
from httprunner.v3.schema import TestsMapping
from httprunner.v3.schema import TestsMapping, TestCaseSummary, TestSuiteSummary
class HttpRunner(object):
@@ -91,10 +91,10 @@ class HttpRunner(object):
return prepared_testcases
def _run_suite(self, prepared_testcases: List[unittest.TestSuite]) -> List[Dict]:
def _run_suite(self, prepared_testcases: List[unittest.TestSuite]) -> List[TestCaseSummary]:
""" run prepared testcases
"""
tests_results: List[Dict] = []
tests_results: List[TestCaseSummary] = []
for index, testcase in enumerate(prepared_testcases):
log_handler = None
@@ -108,18 +108,16 @@ class HttpRunner(object):
result = self.unittest_runner.run(testcase)
testcase_summary = report.get_summary(result)
testcase_summary["name"] = testcase.config.name
testcase_summary["in_out"] = {
"in": testcase.config.variables,
"out": testcase.config.export
}
testcase_summary.name = testcase.config.name
testcase_summary.in_out.vars = testcase.config.variables
testcase_summary.in_out.out = testcase.config.export
if self.save_tests and log_handler:
logger.remove(log_handler)
logs_file_abs_path = utils.prepare_log_file_abs_path(
self.test_path, f"testcase_{index+1}.log"
)
testcase_summary["log"] = logs_file_abs_path
testcase_summary.log = logs_file_abs_path
if result.wasSuccessful():
tests_results.append(testcase_summary)
@@ -128,14 +126,14 @@ class HttpRunner(object):
return tests_results
def _aggregate(self, tests_results: List[Dict]):
def _aggregate(self, tests_results: List[TestCaseSummary]) -> TestSuiteSummary:
""" aggregate multiple testcase results
Args:
tests_results (list): list of testcase summary
"""
summary = {
testsuite_summary = {
"success": True,
"stat": {
"testcases": {
@@ -151,21 +149,21 @@ class HttpRunner(object):
}
for testcase_summary in tests_results:
if testcase_summary["success"]:
summary["stat"]["testcases"]["success"] += 1
if testcase_summary.success:
testsuite_summary["stat"]["testcases"]["success"] += 1
else:
summary["stat"]["testcases"]["fail"] += 1
testsuite_summary["stat"]["testcases"]["fail"] += 1
summary["success"] &= testcase_summary["success"]
testsuite_summary["success"] &= testcase_summary.success
report.aggregate_stat(summary["stat"]["teststeps"], testcase_summary["stat"])
report.aggregate_stat(summary["time"], testcase_summary["time"])
report.aggregate_stat(testsuite_summary["stat"]["teststeps"], testcase_summary.stat.dict())
report.aggregate_stat(testsuite_summary["time"], testcase_summary.time.dict())
summary["details"].append(testcase_summary)
testsuite_summary["details"].append(testcase_summary)
return summary
return TestSuiteSummary.parse_obj(testsuite_summary)
def run_tests(self, tests_mapping):
def run_tests(self, tests_mapping) -> TestSuiteSummary:
""" run testcase/testsuite data
"""
tests = TestsMapping.parse_obj(tests_mapping)
@@ -195,7 +193,7 @@ class HttpRunner(object):
if self.save_tests:
utils.dump_json_file(
self._summary,
self._summary.dict(),
utils.prepare_log_file_abs_path(self.test_path, "summary.json")
)
# save variables and export data
@@ -207,7 +205,7 @@ class HttpRunner(object):
return self._summary
def run_path(self, path, dot_env_path=None, mapping=None):
def run_path(self, path, dot_env_path=None, mapping=None) -> TestSuiteSummary:
""" run testcase/testsuite file or folder.
Args:

View File

@@ -25,7 +25,7 @@ class ResponseObject(object):
"headers": resp_obj.headers,
"body": resp_obj.json()
}
self.validation_results = {}
self.validation_results: Dict = {}
def __getattr__(self, key):
try:

View File

@@ -1,4 +1,4 @@
from typing import List
from typing import List, Dict
from loguru import logger
@@ -7,7 +7,7 @@ from httprunner.client import HttpSession
from httprunner.exceptions import ValidationFailure
from httprunner.v3.parser import build_url, parse_data, parse_variables_mapping
from httprunner.v3.response import ResponseObject
from httprunner.v3.schema import TestsConfig, TestStep, VariablesMapping, TestCase
from httprunner.v3.schema import TestsConfig, TestStep, VariablesMapping, TestCase, MetaData
class TestCaseRunner(object):
@@ -15,8 +15,8 @@ class TestCaseRunner(object):
config: TestsConfig = {}
teststeps: List[TestStep] = []
session: HttpSession = None
meta_datas: List = []
validation_results: List = []
meta_datas: List[MetaData] = []
validation_results: Dict = {}
def init(self, testcase: TestCase) -> "TestCaseRunner":
self.config = testcase.config
@@ -92,8 +92,8 @@ class TestCaseRunner(object):
finally:
self.validation_results = resp_obj.validation_results
# save request & response meta data
self.session.meta_data["validators"] = self.validation_results
self.session.meta_data["name"] = step.name
self.session.meta_data.validators = self.validation_results
self.session.meta_data.name = step.name
self.meta_datas.append(self.session.meta_data)
return extract_mapping

View File

@@ -80,3 +80,74 @@ class ProjectMeta(BaseModel):
class TestsMapping(BaseModel):
project_mapping: ProjectMeta # TODO: rename to project_meta
testcases: List[TestCase]
class Stat(BaseModel):
testcases: Dict
teststeps: Dict
class TestCaseTime(BaseModel):
start_at: float
duration: float
start_datetime: Text = ""
class TestCaseStat(BaseModel):
total: int = 0
successes: int = 0
failures: int = 0
errors: int = 0
skipped: int = 0
expectedFailures: int = 0
unexpectedSuccesses: int = 0
class TestCaseInOut(BaseModel):
vars: VariablesMapping = {}
out: Export = []
class RequestStat(BaseModel):
content_size: Text = "N/A"
response_time_ms: Text = "N/A"
elapsed_ms: Text = "N/A"
class MetaData(BaseModel):
name: Text = ""
data: List[Dict]
stat: RequestStat
validators: Dict = {}
class Record(BaseModel):
name: Text = ""
status: Text = ""
attachment: Text = ""
meta_datas: List[MetaData] = []
response_time: Text = "N/A"
class TestCaseSummary(BaseModel):
name: Text = ""
success: bool
stat: TestCaseStat
time: TestCaseTime
records: List = [Record]
in_out: TestCaseInOut = {}
log: Text = ""
class PlatformInfo(BaseModel):
httprunner_version: Text
python_version: Text
platform: Text
class TestSuiteSummary(BaseModel):
success: bool
stat: Stat
time: TestCaseTime
platform: PlatformInfo
details: List[TestCaseSummary]