From a44bc37c71a0b6aad2cb90ab6bde9e5729998610 Mon Sep 17 00:00:00 2001 From: Vladimir Sapronov Date: Sat, 31 Oct 2020 20:50:50 -0400 Subject: [PATCH 1/4] Added --html option to generate HTML report --- locust/argument_parser.py | 6 ++++ locust/html.py | 68 +++++++++++++++++++++++++++++++++++++++ locust/main.py | 5 +++ locust/web.py | 52 ++---------------------------- 4 files changed, 81 insertions(+), 50 deletions(-) create mode 100644 locust/html.py diff --git a/locust/argument_parser.py b/locust/argument_parser.py index cac5674a2e..d7f617a7e9 100644 --- a/locust/argument_parser.py +++ b/locust/argument_parser.py @@ -350,6 +350,12 @@ def setup_parser_arguments(parser): help="Reset statistics once spawning has been completed. Should be set on both master and workers when running in distributed mode", env_var="LOCUST_RESET_STATS", ) + stats_group.add_argument( + "--html", + dest="html_file", + help="Store HTML report file", + env_var="LOCUST_HTML", + ) log_group = parser.add_argument_group("Logging options") log_group.add_argument( diff --git a/locust/html.py b/locust/html.py new file mode 100644 index 0000000000..a3972c47ba --- /dev/null +++ b/locust/html.py @@ -0,0 +1,68 @@ +from jinja2 import Environment, FileSystemLoader +import os +import pathlib +import datetime +from itertools import chain +from .stats import sort_stats + + +def render_template(file, **kwargs): + templates_path = os.path.join(pathlib.Path(__file__).parent.absolute(), 'templates') + env = Environment(loader=FileSystemLoader(templates_path)) + template = env.get_template(file) + return template.render(**kwargs) + + +def get_html_report(environment): + stats = environment.runner.stats + + start_ts = stats.start_time + start_time = datetime.datetime.fromtimestamp(start_ts) + start_time = start_time.strftime("%Y-%m-%d %H:%M:%S") + + end_ts = stats.last_request_timestamp + end_time = datetime.datetime.fromtimestamp(end_ts) + end_time = end_time.strftime("%Y-%m-%d %H:%M:%S") + + host = None + if environment.host: + host = environment.host + elif environment.runner.user_classes: + all_hosts = set([l.host for l in environment.runner.user_classes]) + if len(all_hosts) == 1: + host = list(all_hosts)[0] + + requests_statistics = list(chain(sort_stats(stats.entries), [stats.total])) + failures_statistics = sort_stats(stats.errors) + exceptions_statistics = [] + for exc in environment.runner.exceptions.values(): + exc["nodes"] = ", ".join(exc["nodes"]) + exceptions_statistics.append(exc) + + history = stats.history + + static_js = "" + js_files = ["jquery-1.11.3.min.js", "echarts.common.min.js", "vintage.js", "chart.js"] + for js_file in js_files: + path = os.path.join(os.path.dirname(__file__), "static", js_file) + with open(path, encoding="utf8") as f: + content = f.read() + static_js += "// " + js_file + "\n" + static_js += content + static_js += "\n\n\n" + + res = render_template( + "report.html", + int=int, + round=round, + requests_statistics=requests_statistics, + failures_statistics=failures_statistics, + exceptions_statistics=exceptions_statistics, + start_time=start_time, + end_time=end_time, + host=host, + history=history, + static_js=static_js, + ) + + return res diff --git a/locust/main.py b/locust/main.py index d7f2ce214c..e8d1e7db12 100644 --- a/locust/main.py +++ b/locust/main.py @@ -24,6 +24,7 @@ from .exception import AuthCredentialsError from .shape import LoadTestShape from .input_events import input_listener +from .html import get_html_report version = locust.__version__ @@ -423,6 +424,10 @@ def sig_term_handler(): try: logger.info("Starting Locust %s" % version) main_greenlet.join() + if options.html_file: + html_report = get_html_report(environment) + with open(options.html_file, 'w+') as file: + file.write(html_report) shutdown() except KeyboardInterrupt: shutdown() diff --git a/locust/web.py b/locust/web.py index 78b596b3c2..4ee53f5426 100644 --- a/locust/web.py +++ b/locust/web.py @@ -25,6 +25,7 @@ from .util.cache import memoize from .util.rounding import proper_round from .util.timespan import parse_timespan +from .html import get_html_report logger = logging.getLogger(__name__) @@ -168,56 +169,7 @@ def reset_stats(): @app.route("/stats/report") @self.auth_required_if_enabled def stats_report(): - stats = self.environment.runner.stats - - start_ts = stats.start_time - start_time = datetime.datetime.fromtimestamp(start_ts) - start_time = start_time.strftime("%Y-%m-%d %H:%M:%S") - - end_ts = stats.last_request_timestamp - end_time = datetime.datetime.fromtimestamp(end_ts) - end_time = end_time.strftime("%Y-%m-%d %H:%M:%S") - - host = None - if environment.host: - host = environment.host - elif environment.runner.user_classes: - all_hosts = set([l.host for l in environment.runner.user_classes]) - if len(all_hosts) == 1: - host = list(all_hosts)[0] - - requests_statistics = list(chain(sort_stats(stats.entries), [stats.total])) - failures_statistics = sort_stats(stats.errors) - exceptions_statistics = [] - for exc in environment.runner.exceptions.values(): - exc["nodes"] = ", ".join(exc["nodes"]) - exceptions_statistics.append(exc) - - history = stats.history - - static_js = "" - js_files = ["jquery-1.11.3.min.js", "echarts.common.min.js", "vintage.js", "chart.js"] - for js_file in js_files: - path = os.path.join(os.path.dirname(__file__), "static", js_file) - with open(path, encoding="utf8") as f: - content = f.read() - static_js += "// " + js_file + "\n" - static_js += content - static_js += "\n\n\n" - - res = render_template( - "report.html", - int=int, - round=round, - requests_statistics=requests_statistics, - failures_statistics=failures_statistics, - exceptions_statistics=exceptions_statistics, - start_time=start_time, - end_time=end_time, - host=host, - history=history, - static_js=static_js, - ) + res = get_html_report(self.environment) if request.args.get("download"): res = app.make_response(res) res.headers["Content-Disposition"] = "attachment;filename=report_%s.html" % time() From 4b8542b3604f88d04c997bdb30b0b7e5699425c1 Mon Sep 17 00:00:00 2001 From: Vladimir Sapronov Date: Sat, 31 Oct 2020 23:07:42 -0400 Subject: [PATCH 2/4] Fixed formatting --- locust/html.py | 2 +- locust/main.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/locust/html.py b/locust/html.py index a3972c47ba..2700ffd441 100644 --- a/locust/html.py +++ b/locust/html.py @@ -7,7 +7,7 @@ def render_template(file, **kwargs): - templates_path = os.path.join(pathlib.Path(__file__).parent.absolute(), 'templates') + templates_path = os.path.join(pathlib.Path(__file__).parent.absolute(), "templates") env = Environment(loader=FileSystemLoader(templates_path)) template = env.get_template(file) return template.render(**kwargs) diff --git a/locust/main.py b/locust/main.py index e8d1e7db12..77ec5e3856 100644 --- a/locust/main.py +++ b/locust/main.py @@ -426,7 +426,7 @@ def sig_term_handler(): main_greenlet.join() if options.html_file: html_report = get_html_report(environment) - with open(options.html_file, 'w+') as file: + with open(options.html_file, "w+") as file: file.write(html_report) shutdown() except KeyboardInterrupt: From 3caf79bb1e5410e36710a060cb0b94eed86b2ca0 Mon Sep 17 00:00:00 2001 From: Robert Loomans Date: Tue, 24 Nov 2020 13:02:27 +1000 Subject: [PATCH 3/4] add a test for the --html option --- locust/test/test_main.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/locust/test/test_main.py b/locust/test/test_main.py index 3bcf89d106..3e5499d462 100644 --- a/locust/test/test_main.py +++ b/locust/test/test_main.py @@ -368,3 +368,33 @@ def t(self): self.assertIn("1 Users have been stopped", output) self.assertIn("10 Users have been stopped", output) self.assertIn("Test task is running", output) + + def test_html_report_option(self): + with mock_locustfile() as mocked: + with temporary_file("", suffix=".html") as html_report_file_path: + try: + output = ( + subprocess.check_output( + ["locust", "-f", mocked.file_path, "--host", "https://test.com/", "--run-time", "5s", "--headless", "--html", html_report_file_path], + stderr=subprocess.STDOUT, + timeout=10, + ) + .decode("utf-8") + .strip() + ) + except subprocess.CalledProcessError as e: + raise AssertionError( + "Running locust command failed. Output was:\n\n%s" % e.stdout.decode("utf-8") + ) from e + + with open(html_report_file_path, encoding="utf-8") as f: + html_report_content = f.read() + + # make sure title appears in the report + self.assertIn("Test Report", html_report_content) + + # make sure host appears in the report + self.assertIn("https://test.com/", html_report_content) + + # make sure the charts container appears in the report + self.assertIn("charts-container", html_report_content) From f9854ddd89012f1eb189798934d5c51f18dde96f Mon Sep 17 00:00:00 2001 From: Robert Loomans Date: Tue, 24 Nov 2020 13:18:41 +1000 Subject: [PATCH 4/4] fix formatting --- locust/test/test_main.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/locust/test/test_main.py b/locust/test/test_main.py index 3e5499d462..4f5d3d495f 100644 --- a/locust/test/test_main.py +++ b/locust/test/test_main.py @@ -375,7 +375,18 @@ def test_html_report_option(self): try: output = ( subprocess.check_output( - ["locust", "-f", mocked.file_path, "--host", "https://test.com/", "--run-time", "5s", "--headless", "--html", html_report_file_path], + [ + "locust", + "-f", + mocked.file_path, + "--host", + "https://test.com/", + "--run-time", + "5s", + "--headless", + "--html", + html_report_file_path, + ], stderr=subprocess.STDOUT, timeout=10, )