From 7f693d352159c8dd36c33da4bf5060fdb188d359 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 22 Apr 2020 21:16:58 +0800 Subject: [PATCH] refactor: add typing with pydantic --- httprunner/cli.py | 6 +-- httprunner/client.py | 36 ++++++-------- httprunner/report/html/gen_report.py | 15 +++--- httprunner/report/html/result.py | 16 ++++--- httprunner/report/html/template.html | 2 +- httprunner/report/stringify.py | 37 +++++++-------- httprunner/report/summarize.py | 46 ++++++++---------- httprunner/v3/api.py | 42 ++++++++-------- httprunner/v3/response.py | 2 +- httprunner/v3/runner.py | 12 ++--- httprunner/v3/schema.py | 71 ++++++++++++++++++++++++++++ 11 files changed, 171 insertions(+), 114 deletions(-) diff --git a/httprunner/cli.py b/httprunner/cli.py index eda5ecd4..0c164b66 100644 --- a/httprunner/cli.py +++ b/httprunner/cli.py @@ -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 diff --git a/httprunner/client.py b/httprunner/client.py index 4577a40a..41e12129 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -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 ] diff --git a/httprunner/report/html/gen_report.py b/httprunner/report/html/gen_report.py index c7791183..95772079 100644 --- a/httprunner/report/html/gen_report.py +++ b/httprunner/report/html/gen_report.py @@ -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}") diff --git a/httprunner/report/html/result.py b/httprunner/report/html/result.py index 762d0bb1..e9d88f2f 100644 --- a/httprunner/report/html/result.py +++ b/httprunner/report/html/result.py @@ -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() diff --git a/httprunner/report/html/template.html b/httprunner/report/html/template.html index 8bbfc1bf..a205a73f 100644 --- a/httprunner/report/html/template.html +++ b/httprunner/report/html/template.html @@ -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 %} {{record.status}} {{record.name}} diff --git a/httprunner/report/stringify.py b/httprunner/report/stringify.py index c6b9cf11..d13e2062 100644 --- a/httprunner/report/stringify.py +++ b/httprunner/report/stringify.py @@ -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) diff --git a/httprunner/report/summarize.py b/httprunner/report/summarize.py index 93c7145f..404d8cb4 100644 --- a/httprunner/report/summarize.py +++ b/httprunner/report/summarize.py @@ -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() + ) diff --git a/httprunner/v3/api.py b/httprunner/v3/api.py index fb1708c9..b7a11d63 100644 --- a/httprunner/v3/api.py +++ b/httprunner/v3/api.py @@ -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: diff --git a/httprunner/v3/response.py b/httprunner/v3/response.py index 57f47eae..01af0bd8 100644 --- a/httprunner/v3/response.py +++ b/httprunner/v3/response.py @@ -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: diff --git a/httprunner/v3/runner.py b/httprunner/v3/runner.py index 4de890d8..b426b2ea 100644 --- a/httprunner/v3/runner.py +++ b/httprunner/v3/runner.py @@ -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 diff --git a/httprunner/v3/schema.py b/httprunner/v3/schema.py index 41721a60..de2f144c 100644 --- a/httprunner/v3/schema.py +++ b/httprunner/v3/schema.py @@ -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]