Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: locustio/locust
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 2.32.10
Choose a base ref
...
head repository: locustio/locust
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 2.33.0
Choose a head ref

Commits on Feb 11, 2025

  1. Bump vitest from 2.1.6 to 2.1.9 in /locust/webui

    Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 2.1.6 to 2.1.9.
    - [Release notes](https://github.com/vitest-dev/vitest/releases)
    - [Commits](https://github.com/vitest-dev/vitest/commits/v2.1.9/packages/vitest)
    
    ---
    updated-dependencies:
    - dependency-name: vitest
      dependency-type: direct:development
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    dependabot[bot] authored Feb 11, 2025
    Copy the full SHA
    c3c6cb5 View commit details

Commits on Feb 18, 2025

  1. Accept brotli and zstd compression encoding

    kamilbednarz committed Feb 18, 2025
    Copy the full SHA
    7cd7de6 View commit details
  2. Update autogenerated changelog

    cyberw committed Feb 18, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    fab2227 View commit details
  3. Enable HTML Report Filename Parsing

    Add HTML filename parsing
    Add usage details to help
    Add tests
    ktchani committed Feb 18, 2025
    Copy the full SHA
    3f16f93 View commit details

Commits on Feb 19, 2025

  1. Improve error message on missing user_count or spawn_rate in /swarm p…

    …ayload. Adresses #3051
    cyberw committed Feb 19, 2025
    Copy the full SHA
    68780b5 View commit details

Commits on Feb 20, 2025

  1. Merge pull request #3044 from locustio/dependabot/npm_and_yarn/locust…

    …/webui/vitest-2.1.9
    
    Bump vitest from 2.1.6 to 2.1.9 in /locust/webui
    cyberw authored Feb 20, 2025
    Copy the full SHA
    cdfec9b View commit details
  2. remove uv lock file

    mquinnfd committed Feb 20, 2025
    Copy the full SHA
    a16775b View commit details

Commits on Feb 21, 2025

  1. Merge pull request #3055 from mquinnfd/remove-uv-lock-file-from-builds

    Remove uv lock file from build artifacts
    cyberw authored Feb 21, 2025
    Copy the full SHA
    bb24746 View commit details
  2. Merge pull request #3048 from kamilbednarz/accept-other-compressions

    FastHttpUser: Accept brotli and zstd compression encoding
    cyberw authored Feb 21, 2025
    Copy the full SHA
    b3493c0 View commit details
  3. Merge pull request #3052 from locustio/improve-error-message-for-miss…

    …ing-/swarm-payload
    
    Improve error message on missing user_count or spawn_rate in swarm payload
    cyberw authored Feb 21, 2025
    Copy the full SHA
    97529ef View commit details
  4. Merge pull request #3049 from ktchani/master

    Enable HTML Report Filename Parsing
    cyberw authored Feb 21, 2025
    Copy the full SHA
    e49b8fb View commit details
  5. Update vite to 6.0.11

    cyberw committed Feb 21, 2025
    Copy the full SHA
    7e88189 View commit details
  6. Merge pull request #3056 from locustio/update-vite

    Update vite to 6.0.11
    cyberw authored Feb 21, 2025
    Copy the full SHA
    3bb8ff0 View commit details
  7. Update changelog in preparation of 2.32.11

    cyberw committed Feb 21, 2025
    Copy the full SHA
    c6eaf25 View commit details
  8. Use enter to open UI in default browser

    cyberw committed Feb 21, 2025
    Copy the full SHA
    5984304 View commit details
  9. Use enter to open webui: Support mac/unix style newlines & fix test c…

    …ase.
    cyberw committed Feb 21, 2025
    Copy the full SHA
    a8c7b90 View commit details
  10. Merge pull request #3057 from locustio/Use-enter-to-automatically-ope…

    …n-web-ui-in-default-browser
    
    Use enter to automatically open web UI in default browser
    cyberw authored Feb 21, 2025
    Copy the full SHA
    77831cd View commit details
  11. Update changelog (going to bump minor revision in next release)

    cyberw committed Feb 21, 2025
    Copy the full SHA
    d6812b2 View commit details

Commits on Feb 22, 2025

  1. dos: correct venv activation path in docs

    Fix incorrect path in virtual environment activation command (.venv/bin.activate -> .venv/bin/activate)
    n0h0 committed Feb 22, 2025
    Copy the full SHA
    018cbb5 View commit details
  2. docs: update python-requests documentation links

    Replace outdated python-requests URLs (python-requests.org) with current documentation URL (requests.readthedocs.io) in various documentation files and source code.
    n0h0 committed Feb 22, 2025
    Copy the full SHA
    6335881 View commit details
  3. Merge pull request #3058 from n0h0/fix-venv-bin-activate

    dos: correct venv activation path in docs
    cyberw authored Feb 22, 2025
    Copy the full SHA
    d01765d View commit details
  4. Merge pull request #3059 from n0h0/fix-python-requests-link

    docs: update python-requests documentation links
    cyberw authored Feb 22, 2025
    Copy the full SHA
    c5af270 View commit details
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
# Detailed changelog
The most important changes can also be found in [the documentation](https://docs.locust.io/en/latest/changelog.html).

## [2.32.10](https://github.com/locustio/locust/tree/2.32.10) (2025-02-18)

[Full Changelog](https://github.com/locustio/locust/compare/2.32.9...2.32.10)

**Closed issues:**

- Switch from Poetry to uv [\#3033](https://github.com/locustio/locust/issues/3033)

**Merged pull requests:**

- Add uv lock file to builds [\#3047](https://github.com/locustio/locust/pull/3047) ([mquinnfd](https://github.com/mquinnfd))
- Use uv/hatch instead of Poetry [\#3039](https://github.com/locustio/locust/pull/3039) ([mquinnfd](https://github.com/mquinnfd))

## [2.32.9](https://github.com/locustio/locust/tree/2.32.9) (2025-02-10)

[Full Changelog](https://github.com/locustio/locust/compare/2.32.8...2.32.9)
6 changes: 6 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -4,6 +4,12 @@ Changelog Highlights

For full details of changes, please see https://github.com/locustio/locust/releases or https://github.com/locustio/locust/blob/master/CHANGELOG.md

2.33.0
======
* Press enter to automatically open web UI in browser https://github.com/locustio/locust/pull/3057
* Enable HTML Report Filename Parsing https://github.com/locustio/locust/pull/3049
* Various minor fixes and dependency updates

2.32.10
=======
* Use uv/hatch instead of Poetry https://github.com/locustio/locust/pull/3039
2 changes: 1 addition & 1 deletion docs/developing-locust.rst
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ Fork Locust on `GitHub <https://github.com/locustio/locust/>`_ and then
# [optional] create a virtual environment and activate it
$ uv venv
$ . .venv/bin.activate
$ . .venv/bin/activate
# perform an editable install of the "locust" package along with the dev and test packages:
$ uv sync
2 changes: 1 addition & 1 deletion docs/increase-performance.rst
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
Increase performance with a faster HTTP client
==============================================================

Locust's default HTTP client uses `python-requests <http://www.python-requests.org/>`_.
Locust's default HTTP client uses `python-requests <https://requests.readthedocs.io/>`_.
It provides a nice API that many python developers are familiar with, and is very well-maintained. But if you're planning to run tests with very high throughput and have limited hardware for running Locust, it is sometimes not efficient enough.

Because of this, Locust also comes with :py:class:`FastHttpUser <locust.contrib.fasthttp.FastHttpUser>` which
4 changes: 2 additions & 2 deletions docs/running-in-debugger.rst
Original file line number Diff line number Diff line change
@@ -57,7 +57,7 @@ Print HTTP communication

Sometimes it can be hard to understand why an HTTP request fails in Locust when it works from a regular browser/other application. Here's how to examine the communication in detail:

For ``HttpUser`` (`python-requests <https://python-requests.org>`_):
For ``HttpUser`` (`python-requests <https://requests.readthedocs.io/>`_):

.. code-block:: python
@@ -91,7 +91,7 @@ Example output (for FastHttpUser):
REQUEST: http://example.com/
GET / HTTP/1.1
user-agent: python/gevent-http-client-1.5.3
accept-encoding: gzip, deflate
accept-encoding: gzip, deflate, br, zstd
host: example.com
RESPONSE: HTTP/1.1 200
5 changes: 4 additions & 1 deletion locust/argument_parser.py
Original file line number Diff line number Diff line change
@@ -208,6 +208,9 @@ def get_empty_argument_parser(add_help=True, default_config_files=DEFAULT_CONFIG
locust --headless -u 100 -t 20m --processes 4 MyHttpUser AnotherUser
locust --headless -u 100 -r 10 -t 50 --print-stats --html "test_report_{u}_{r}_{t}.html" -H https://www.example.com
(The above run would generate an html file with the name "test_report_100_10_50.html")
See documentation for more details, including how to set options using a file or environment variables: https://docs.locust.io/en/stable/configuration.html""",
)
parser.add_argument(
@@ -754,7 +757,7 @@ def setup_parser_arguments(parser):
"--html",
metavar="<filename>",
dest="html_file",
help="Store HTML report to file path specified",
help="Store HTML report to file path specified. Able to parse certain tags - {u}, {r}, {t} and convert them to number of users, spawn rate and run time respectively.",
env_var="LOCUST_HTML",
)
stats_group.add_argument(
2 changes: 1 addition & 1 deletion locust/clients.py
Original file line number Diff line number Diff line change
@@ -68,7 +68,7 @@ class HttpSession(requests.Session):
to be able to log in and out of websites). Each request is logged so that locust can display
statistics.
This is a slightly extended version of `python-request <http://python-requests.org>`_'s
This is a slightly extended version of `python-request <https://requests.readthedocs.io/>`_'s
:py:class:`requests.Session` class and mostly this class works exactly the same. However
the methods for making requests (get, post, delete, put, head, options, patch, request)
can now take a *url* argument that's only the path part of the URL, in which case the host
2 changes: 1 addition & 1 deletion locust/contrib/fasthttp.py
Original file line number Diff line number Diff line change
@@ -229,7 +229,7 @@ def request(
elif self.auth_header:
headers["Authorization"] = self.auth_header
if "Accept-Encoding" not in headers and "accept-encoding" not in headers:
headers["Accept-Encoding"] = "gzip, deflate"
headers["Accept-Encoding"] = "gzip, deflate, br, zstd"

if not data and json is not None:
data = unshadowed_json.dumps(json)
19 changes: 19 additions & 0 deletions locust/html.py
Original file line number Diff line number Diff line change
@@ -15,6 +15,25 @@
DEFAULT_BUILD_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "webui", "dist")


def process_html_filename(options) -> None:
num_users = options.num_users
spawn_rate = options.spawn_rate
run_time = options.run_time

option_mapping = {
"{u}": num_users,
"{r}": spawn_rate,
"{t}": run_time,
}

html_filename = options.html_file

for option_term, option_value in option_mapping.items():
html_filename = html_filename.replace(option_term, str(int(option_value)))

options.html_file = html_filename


def render_template_from(file, build_path=DEFAULT_BUILD_PATH, **kwargs):
env = JinjaEnvironment(loader=FileSystemLoader(build_path))
template = env.get_template(file)
20 changes: 10 additions & 10 deletions locust/main.py
Original file line number Diff line number Diff line change
@@ -14,13 +14,14 @@
import sys
import time
import traceback
import webbrowser

import gevent

from . import log, stats
from .argument_parser import parse_locustfile_option, parse_options
from .env import Environment
from .html import get_html_report
from .html import get_html_report, process_html_filename
from .input_events import input_listener
from .log import greenlet_exception_logger, setup_logging
from .stats import (
@@ -444,16 +445,12 @@ def kill_workers(children):
else:
web_host = options.web_host
if web_host:
logger.info(f"Starting web interface at {protocol}://{web_host}:{options.web_port}{options.web_base_path}")
if options.web_host_display_name:
logger.info(f"Starting web interface at {options.web_host_display_name}")
url = f"{protocol}://{web_host}:{options.web_port}{options.web_base_path}"
elif options.web_host_display_name:
url = f"{options.web_host_display_name}"
else:
if os.name == "nt":
logger.info(
f"Starting web interface at {protocol}://localhost:{options.web_port}{options.web_base_path} (accepting connections from all network interfaces)"
)
else:
logger.info(f"Starting web interface at {protocol}://0.0.0.0:{options.web_port}{options.web_base_path}")
url = f"{protocol}://{'localhost' if os.name == 'nt' else '0.0.0.0'}:{options.web_port}{options.web_base_path}"
logger.info(f"Starting web interface at {url}, press enter to open your default browser.")

web_ui = environment.create_web_ui(
host=web_host,
@@ -597,6 +594,8 @@ def start_automatic_run():
"S": lambda: runner.start(max(0, runner.user_count - 10), 100)
if runner.state != "spawning"
else logging.warning("Spawning users, can't stop right now"),
"\r": lambda: webbrowser.open_new_tab(url),
"\n": lambda: webbrowser.open_new_tab(url),
},
)
)
@@ -651,6 +650,7 @@ def sig_term_handler():

def save_html_report():
html_report = get_html_report(environment, show_download_link=False)
process_html_filename(options)
logger.info("writing html report to file: %s", options.html_file)
with open(options.html_file, "w", encoding="utf-8") as file:
file.write(html_report)
42 changes: 42 additions & 0 deletions locust/test/test_html_filename.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from locust.html import process_html_filename

import unittest
from unittest.mock import MagicMock


class TestProcessHtmlFilename(unittest.TestCase):
def test_process_html_filename(self):
mock_options = MagicMock()
mock_options.num_users = 100
mock_options.spawn_rate = 10
mock_options.run_time = 60
mock_options.html_file = "report_u{u}_r{r}_t{t}.html"

process_html_filename(mock_options)

expected_filename = "report_u100_r10_t60.html"
self.assertEqual(mock_options.html_file, expected_filename)

def test_process_html_filename_partial_replacement(self):
mock_options = MagicMock()
mock_options.num_users = 50
mock_options.spawn_rate = 5
mock_options.run_time = 30
mock_options.html_file = "loadtest_{u}_{r}.html"

process_html_filename(mock_options)

expected_filename = "loadtest_50_5.html"
self.assertEqual(mock_options.html_file, expected_filename)

def test_process_html_filename_no_replacement(self):
mock_options = MagicMock()
mock_options.num_users = 50
mock_options.spawn_rate = 5
mock_options.run_time = 30
mock_options.html_file = "static_report.html"

process_html_filename(mock_options)

expected_filename = "static_report.html"
self.assertEqual(mock_options.html_file, expected_filename)
145 changes: 106 additions & 39 deletions locust/test/test_main.py
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
import socket
import subprocess
import sys
import tempfile
import textwrap
import unittest
from subprocess import PIPE, STDOUT
@@ -110,7 +111,7 @@ def my_task(self):
gevent.sleep(1)

requests.post(
"http://127.0.0.1:%i/swarm" % port,
f"http://127.0.0.1:{port}/swarm",
data={"user_count": 1, "spawn_rate": 1, "host": "https://localhost", "custom_string_arg": "web_form_value"},
)
gevent.sleep(1)
@@ -859,7 +860,7 @@ def test_web_options(self):
stderr=PIPE,
)
gevent.sleep(1)
self.assertEqual(200, requests.get("http://127.0.0.1:%i/" % port, timeout=3).status_code)
self.assertEqual(200, requests.get(f"http://127.0.0.1:{port}/", timeout=3).status_code)
proc.terminate()

@unittest.skipIf(os.name == "nt", reason="termios doesnt exist on windows, and thus we cannot import pty")
@@ -933,6 +934,57 @@ def t(self):
self.assertIn("Shutting down (exit code 0)", output)
self.assertEqual(0, proc.returncode)

@unittest.skipIf(os.name == "nt", reason="termios doesnt exist on windows, and thus we cannot import pty")
def test_autospawn_browser(self):
import pty

LOCUSTFILE_CONTENT = textwrap.dedent(
"""
from pytest import MonkeyPatch
import sys
import webbrowser
monkeypatch = MonkeyPatch()
def open_new_tab(url):
print("browser opened with url", url)
sys.exit(0)
monkeypatch.setattr(webbrowser, "open_new_tab", open_new_tab)
print("patched")
from locust import User, TaskSet, task, between
class UserSubclass(User):
@task
def t(self):
print("Test task is running")
"""
)
with mock_locustfile(content=LOCUSTFILE_CONTENT) as mocked:
stdin_m, stdin_s = pty.openpty()
stdin = os.fdopen(stdin_m, "wb", 0)

proc = subprocess.Popen(
[
"locust",
"-f",
mocked.file_path,
],
stdin=stdin_s,
stdout=PIPE,
text=True,
)
gevent.sleep(1)
stdin.write(b"\n")
try:
output, _ = proc.communicate(timeout=1)
except Exception:
proc.kill()
output, _ = proc.communicate()

self.assertIn("browser opened", output)

def test_spawning_with_fixed(self):
LOCUSTFILE_CONTENT = textwrap.dedent(
"""
@@ -1150,44 +1202,59 @@ def t(self):
self.assertEqual(0, proc.returncode)

def test_html_report_option(self):
html_template = "some_name_{u}_{r}_{t}.html"
expected_filename = "some_name_11_5_2.html"

with mock_locustfile() as mocked:
with temporary_file("", suffix=".html") as html_report_file_path:
try:
subprocess.check_output(
[
"locust",
"-f",
mocked.file_path,
"--host",
"https://test.com/",
"--run-time",
"2s",
"--headless",
"--exit-code-on-error",
"0",
"--html",
html_report_file_path,
],
stderr=subprocess.STDOUT,
timeout=10,
text=True,
).strip()
except subprocess.CalledProcessError as e:
raise AssertionError(f"Running locust command failed. Output was:\n\n{e.stdout}") 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
_, locustfile = os.path.split(mocked.file_path)
self.assertIn(locustfile, html_report_content)

# make sure host appears in the report
self.assertIn("https://test.com/", html_report_content)
self.assertIn('"show_download_link": false', html_report_content)
self.assertRegex(html_report_content, r'"start_time": "\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z"')
self.assertRegex(html_report_content, r'"end_time": "\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z"')
self.assertRegex(html_report_content, r'"duration": "\d* seconds?"')
# Get system temp directory
temp_dir = tempfile.gettempdir()

# Define the input filename as well as the resulting filename within the temp directory
html_report_file_path = os.path.join(temp_dir, html_template)
output_html_report_file_path = os.path.join(temp_dir, expected_filename)

try:
output = subprocess.check_output(
[
"locust",
"-f",
mocked.file_path,
"--host",
"https://test.com/",
"--run-time",
"2s",
"--headless",
"--exit-code-on-error",
"0",
"-u",
"11",
"-r",
"5",
"--html",
html_report_file_path,
],
stderr=subprocess.STDOUT,
timeout=10,
text=True,
).strip()

except subprocess.CalledProcessError as e:
raise AssertionError(f"Running locust command failed. Output was:\n\n{e.stdout}") from e
with open(output_html_report_file_path, encoding="utf-8") as f:
html_report_content = f.read()

# make sure correct name is generated based on filename arguments
self.assertIn(expected_filename, output)

_, locustfile = os.path.split(mocked.file_path)
self.assertIn(locustfile, html_report_content)

# make sure host appears in the report
self.assertIn("https://test.com/", html_report_content)
self.assertIn('"show_download_link": false', html_report_content)
self.assertRegex(html_report_content, r'"start_time": "\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z"')
self.assertRegex(html_report_content, r'"end_time": "\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z"')
self.assertRegex(html_report_content, r'"duration": "\d* seconds?"')

def test_run_with_userclass_picker(self):
with temporary_file(content=MOCK_LOCUSTFILE_CONTENT_A) as file1:
6 changes: 6 additions & 0 deletions locust/web.py
Original file line number Diff line number Diff line change
@@ -253,6 +253,8 @@ def swarm() -> Response:

parsed_options_dict = vars(environment.parsed_options) if environment.parsed_options else {}
run_time = None
user_count = None
spawn_rate = None
for key, value in request.form.items():
if key == "user_count": # if we just renamed this field to "users" we wouldn't need this
user_count = int(value)
@@ -303,6 +305,10 @@ def swarm() -> Response:
self._swarm_greenlet = None

if environment.runner is not None:
if user_count is None or spawn_rate is None:
err_msg = "Missing user_count or spawn_rate from /swarm request"
logger.error(err_msg)
return jsonify({"success": False, "message": err_msg, "host": environment.host})
self._swarm_greenlet = gevent.spawn(environment.runner.start, user_count, spawn_rate)
self._swarm_greenlet.link_exception(greenlet_exception_handler)
response_data = {
4 changes: 2 additions & 2 deletions locust/webui/package.json
Original file line number Diff line number Diff line change
@@ -56,7 +56,7 @@
"react-markdown": "^9.0.0",
"react-redux": "^9.1.2",
"rimraf": "^6.0.1",
"vite": "^6.0.1",
"vite": "^6.0.11",
"vite-plugin-checker": "^0.8.0",
"vite-plugin-dts": "^4.3.0",
"vite-plugin-singlefile": "^2.0.3",
@@ -80,7 +80,7 @@
"msw": "^2.6.6",
"prettier": "^3.0.3",
"typescript": "^5.7.2",
"vitest": "^2.1.6",
"vitest": "^2.1.9",
"vitest-webgl-canvas-mock": "^1.1.0"
},
"engines": {
991 changes: 633 additions & 358 deletions locust/webui/yarn.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -110,12 +110,12 @@ source = "vcs"
version-file = "locust/_version.py"

[tool.hatch.build.targets.sdist]
include = ["locust", "uv.lock"]
include = ["locust"]
exclude = ["locust/webui/*", "locust/test", "locust/build"]
artifacts = ["locust/webui/dist"]

[tool.hatch.build.targets.wheel]
include = ["locust", "uv.lock"]
include = ["locust"]
artifacts = ["locust/webui/dist"]

[tool.hatch.version.raw-options]