diff --git a/locust/argument_parser.py b/locust/argument_parser.py index e63de78a8f..1d20f9a450 100644 --- a/locust/argument_parser.py +++ b/locust/argument_parser.py @@ -838,6 +838,11 @@ def setup_parser_arguments(parser): dest="equal_weights", help="Use equally distributed task weights, overriding the weights specified in the locustfile.", ) + other_group.add_argument( + "--profile", + type=str, + help="Set a profile to group the testruns together", + ) user_classes_group = parser.add_argument_group("User classes") user_classes_group.add_argument( diff --git a/locust/env.py b/locust/env.py index 04c6bf5ae4..dfabd7524a 100644 --- a/locust/env.py +++ b/locust/env.py @@ -39,6 +39,7 @@ def __init__( available_shape_classes: dict[str, LoadTestShape] | None = None, available_user_tasks: dict[str, list[TaskSet | Callable]] | None = None, dispatcher_class: type[UsersDispatcher] = UsersDispatcher, + profile: str | None = None, ): self.runner: Runner | None = None """Reference to the :class:`Runner ` instance""" @@ -76,6 +77,8 @@ def __init__( """Base URL of the target system""" self.reset_stats = reset_stats """Determines if stats should be reset once all simulated users have been spawned""" + self.profile = profile + """Profile name for the test run""" if stop_timeout is not None: self.stop_timeout = stop_timeout elif parsed_options: diff --git a/locust/html.py b/locust/html.py index 8dafd1a3e5..9029e2dc3e 100644 --- a/locust/html.py +++ b/locust/html.py @@ -105,6 +105,7 @@ def get_html_report( "locustfile": escape(str(environment.locustfile)), "tasks": task_data, "percentiles_to_chart": stats_module.PERCENTILES_TO_CHART, + "profile": escape(str(environment.profile)) if environment.profile else None, }, theme=theme, ) diff --git a/locust/main.py b/locust/main.py index b3b7b7cec4..3071a686a8 100644 --- a/locust/main.py +++ b/locust/main.py @@ -89,6 +89,7 @@ def create_environment( available_user_classes=available_user_classes, available_shape_classes=available_shape_classes, available_user_tasks=available_user_tasks, + profile=options.profile, ) diff --git a/locust/test/test_load_locustfile.py b/locust/test/test_load_locustfile.py index cb0ace4f05..cee00f1791 100644 --- a/locust/test/test_load_locustfile.py +++ b/locust/test/test_load_locustfile.py @@ -225,3 +225,26 @@ def test_locustfile_from_url(self): f"{os.getcwd()}/examples/basic.py", ) ) + + def test_profile_flag(self): + options = parse_options() + self.assertEqual(None, options.profile) + options = parse_options(args=["--profile", "test-profile"]) + self.assertEqual("test-profile", options.profile) + with temporary_file("profile=test-profile-from-file", suffix=".conf") as conf_file_path: + options = parse_options( + args=[ + "--config", + conf_file_path, + ] + ) + self.assertEqual("test-profile-from-file", options.profile) + options = parse_options( + args=[ + "--config", + conf_file_path, + "--profile", + "test-profile-from-arg", + ] + ) + self.assertEqual("test-profile-from-arg", options.profile) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index b9770d572c..5cd913bb8c 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -116,6 +116,7 @@ def __init__(self): self.heartbeat_interval = 1 self.stop_timeout = 0.0 self.connection_broken = False + self.profile = None def reset_stats(self): pass diff --git a/locust/webui/src/pages/HtmlReport.tsx b/locust/webui/src/pages/HtmlReport.tsx index 05520bee05..bd2897420c 100644 --- a/locust/webui/src/pages/HtmlReport.tsx +++ b/locust/webui/src/pages/HtmlReport.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { Box, Typography, Container, Link } from '@mui/material'; import CssBaseline from '@mui/material/CssBaseline'; import { ThemeProvider } from '@mui/material/styles'; @@ -51,6 +52,12 @@ export default function HtmlReport({ responseTimeStatistics, tasks, }: IReport) { + useEffect(() => { + document.title = window.templateArgs.profile + ? `Locust - ${window.templateArgs.profile}` + : 'Locust'; + }, []); + return ( @@ -73,6 +80,12 @@ export default function HtmlReport({ )} + {window.templateArgs.profile && ( + + Profile: + {window.templateArgs.profile} + + )} During: diff --git a/locust/webui/src/pages/tests/HtmlReport.test.tsx b/locust/webui/src/pages/tests/HtmlReport.test.tsx index 5bc7553896..a103d996c8 100644 --- a/locust/webui/src/pages/tests/HtmlReport.test.tsx +++ b/locust/webui/src/pages/tests/HtmlReport.test.tsx @@ -1,11 +1,19 @@ -import { describe, expect, test } from 'vitest'; +import { describe, expect, test, beforeEach } from 'vitest'; import HtmlReport from 'pages/HtmlReport'; import { swarmReportMock } from 'test/mocks/swarmState.mock'; import { renderWithProvider } from 'test/testUtils'; +import { IReportTemplateArgs } from 'types/swarm.types'; import { formatLocaleString } from 'utils/date'; + describe('HtmlReport', () => { + beforeEach(() => { + // Reset window.templateArgs before each test + window.templateArgs = {} as IReportTemplateArgs; + document.title = ''; + }); + test('renders a report', () => { const { getByRole, getByText } = renderWithProvider(); @@ -19,6 +27,21 @@ describe('HtmlReport', () => { expect(getByText(swarmReportMock.host)).toBeTruthy(); }); + test('profile is not rendered when it is not present', () => { + const { queryByText } = renderWithProvider(); + + expect(queryByText('Profile:')).toBeNull(); + }); + + test('profile is rendered when it is present', () => { + window.templateArgs.profile = 'test-profile'; + const { getByText } = renderWithProvider(); + + expect(getByText('Profile:')).toBeTruthy(); + expect(getByText('test-profile')).toBeTruthy(); + expect(document.title).toBe('Locust - test-profile'); + }); + test('formats the start and end time as expected', () => { const { getByText } = renderWithProvider(); diff --git a/locust/webui/src/types/swarm.types.ts b/locust/webui/src/types/swarm.types.ts index 7c83fd8f0b..c5af27fcaf 100644 --- a/locust/webui/src/types/swarm.types.ts +++ b/locust/webui/src/types/swarm.types.ts @@ -64,6 +64,7 @@ export interface ISwarmState { users: { [key: string]: ISwarmUser }; version: string; workerCount: number; + profile?: string; } export interface IReport { @@ -86,6 +87,7 @@ export interface IReportTemplateArgs extends Omit { isReport?: boolean; percentilesToChart: number[]; percentilesToStatistics: number[]; + profile?: string; } export interface ISwarmFormInput