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 err_code = 0
try: try:
for path in args.testfile_paths: 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") report_dir = args.report_dir or os.path.join(os.getcwd(), "reports")
gen_html_report( gen_html_report(
summary, testsuite_summary,
report_template=args.report_template, report_template=args.report_template,
report_dir=report_dir, report_dir=report_dir,
report_file=args.report_file 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: except Exception as ex:
logger.error(f"!!!!!!!!!! exception stage: {runner.exception_stage} !!!!!!!!!!\n{str(ex)}") logger.error(f"!!!!!!!!!! exception stage: {runner.exception_stage} !!!!!!!!!!\n{str(ex)}")
err_code = 1 err_code = 1

View File

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

View File

@@ -6,20 +6,21 @@ from jinja2 import Template
from loguru import logger from loguru import logger
from httprunner.exceptions import SummaryEmpty 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 """ render html report with specified report name and template
Args: 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_template (str): specify html report template path, template should be in Jinja2 format.
report_dir (str): specify html report save directory report_dir (str): specify html report save directory
report_file (str): specify html report file path, this has higher priority than specifying report dir. 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: if not testsuite_summary.time or testsuite_summary.stat.testcases["total"] == 0:
logger.error(f"test result summary is empty ! {summary}") logger.error(f"test result testsuite_summary is empty ! {testsuite_summary}")
raise SummaryEmpty raise SummaryEmpty
if not report_template: 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 ...") 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() 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: if report_file:
report_dir = os.path.dirname(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( rendered_content = Template(
template_content, template_content,
extensions=["jinja2.ext.loopcontrols"] extensions=["jinja2.ext.loopcontrols"]
).render(summary) ).render(testsuite_summary.dict())
fp_w.write(rendered_content) fp_w.write(rendered_content)
logger.info(f"Generated Html report: {report_path}") logger.info(f"Generated Html report: {report_path}")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -80,3 +80,74 @@ class ProjectMeta(BaseModel):
class TestsMapping(BaseModel): class TestsMapping(BaseModel):
project_mapping: ProjectMeta # TODO: rename to project_meta project_mapping: ProjectMeta # TODO: rename to project_meta
testcases: List[TestCase] 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]