mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-13 04:10:21 +08:00
refactor: add typing with pydantic
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user