locust 2.35.1.dev3__tar.gz → 2.35.1.dev11__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/PKG-INFO +1 -1
  2. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/_version.py +2 -2
  3. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/html.py +12 -13
  4. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/main.py +9 -43
  5. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/stats.py +43 -4
  6. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/web.py +23 -21
  7. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/.gitignore +0 -0
  8. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/LICENSE +0 -0
  9. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/README.md +0 -0
  10. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/hatch_build.py +0 -0
  11. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/__init__.py +0 -0
  12. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/__main__.py +0 -0
  13. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/argument_parser.py +0 -0
  14. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/clients.py +0 -0
  15. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/contrib/__init__.py +0 -0
  16. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/contrib/fasthttp.py +0 -0
  17. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/contrib/mongodb.py +0 -0
  18. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/contrib/oai.py +0 -0
  19. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/contrib/postgres.py +0 -0
  20. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/debug.py +0 -0
  21. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/dispatch.py +0 -0
  22. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/env.py +0 -0
  23. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/event.py +0 -0
  24. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/exception.py +0 -0
  25. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/input_events.py +0 -0
  26. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/log.py +0 -0
  27. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/py.typed +0 -0
  28. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/rpc/__init__.py +0 -0
  29. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/rpc/protocol.py +0 -0
  30. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/rpc/zmqrpc.py +0 -0
  31. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/runners.py +0 -0
  32. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/shape.py +0 -0
  33. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/user/__init__.py +0 -0
  34. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/user/inspectuser.py +0 -0
  35. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/user/sequential_taskset.py +0 -0
  36. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/user/task.py +0 -0
  37. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/user/users.py +0 -0
  38. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/user/wait_time.py +0 -0
  39. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/util/__init__.py +0 -0
  40. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/util/cache.py +0 -0
  41. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/util/date.py +0 -0
  42. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/util/deprecation.py +0 -0
  43. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/util/directory.py +0 -0
  44. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/util/exception_handler.py +0 -0
  45. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/util/load_locustfile.py +0 -0
  46. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/util/rounding.py +0 -0
  47. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/util/timespan.py +0 -0
  48. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/util/url.py +0 -0
  49. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/webui/dist/assets/favicon-dark.png +0 -0
  50. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/webui/dist/assets/favicon-light.png +0 -0
  51. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/webui/dist/assets/graphs-dark.png +0 -0
  52. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/webui/dist/assets/graphs-light.png +0 -0
  53. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/webui/dist/assets/index-DQLt1q6M.js +0 -0
  54. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/webui/dist/assets/testruns-dark.png +0 -0
  55. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/webui/dist/assets/testruns-light.png +0 -0
  56. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/webui/dist/auth.html +0 -0
  57. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/webui/dist/index.html +0 -0
  58. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/locust/webui/dist/report.html +0 -0
  59. {locust-2.35.1.dev3 → locust-2.35.1.dev11}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: locust
3
- Version: 2.35.1.dev3
3
+ Version: 2.35.1.dev11
4
4
  Summary: Developer-friendly load testing framework
5
5
  Project-URL: homepage, https://locust.io/
6
6
  Project-URL: repository, https://github.com/locustio/locust
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.35.1.dev3'
21
- __version_tuple__ = version_tuple = (2, 35, 1, 'dev3')
20
+ __version__ = version = '2.35.1.dev11'
21
+ __version_tuple__ = version_tuple = (2, 35, 1, 'dev11')
@@ -5,9 +5,8 @@ from itertools import chain
5
5
  from jinja2 import Environment as JinjaEnvironment
6
6
  from jinja2 import FileSystemLoader
7
7
 
8
- from . import stats as stats_module
8
+ from . import stats
9
9
  from .runners import STATE_STOPPED, STATE_STOPPING, MasterRunner
10
- from .stats import sort_stats, update_stats_history
11
10
  from .user.inspectuser import get_ratio
12
11
  from .util.date import format_duration, format_utc_timestamp
13
12
 
@@ -37,14 +36,14 @@ def get_html_report(
37
36
  show_download_link=True,
38
37
  theme="",
39
38
  ):
40
- stats = environment.runner.stats
39
+ request_stats = environment.runner.stats
41
40
 
42
- start_time = format_utc_timestamp(stats.start_time)
41
+ start_time = format_utc_timestamp(request_stats.start_time)
43
42
 
44
- if end_ts := stats.last_request_timestamp:
43
+ if end_ts := request_stats.last_request_timestamp:
45
44
  end_time = format_utc_timestamp(end_ts)
46
45
  else:
47
- end_ts = stats.start_time
46
+ end_ts = request_stats.start_time
48
47
  end_time = start_time
49
48
 
50
49
  host = None
@@ -55,15 +54,15 @@ def get_html_report(
55
54
  if len(all_hosts) == 1:
56
55
  host = list(all_hosts)[0]
57
56
 
58
- requests_statistics = list(chain(sort_stats(stats.entries), [stats.total]))
59
- failures_statistics = sort_stats(stats.errors)
57
+ requests_statistics = list(chain(stats.sort_stats(request_stats.entries), [request_stats.total]))
58
+ failures_statistics = stats.sort_stats(request_stats.errors)
60
59
  exceptions_statistics = [
61
60
  {**exc, "nodes": ", ".join(exc["nodes"])} for exc in environment.runner.exceptions.values()
62
61
  ]
63
62
 
64
- if stats.history and stats.history[-1]["time"] < end_time:
65
- update_stats_history(environment.runner, end_time)
66
- history = stats.history
63
+ if request_stats.history and request_stats.history[-1]["time"] < end_time:
64
+ stats.update_stats_history(environment.runner, end_time)
65
+ history = request_stats.history
67
66
 
68
67
  is_distributed = isinstance(environment.runner, MasterRunner)
69
68
  user_spawned = (
@@ -98,13 +97,13 @@ def get_html_report(
98
97
  ],
99
98
  "start_time": start_time,
100
99
  "end_time": end_time,
101
- "duration": format_duration(stats.start_time, end_ts),
100
+ "duration": format_duration(request_stats.start_time, end_ts),
102
101
  "host": escape(str(host)),
103
102
  "history": history,
104
103
  "show_download_link": show_download_link,
105
104
  "locustfile": escape(str(environment.locustfile)),
106
105
  "tasks": task_data,
107
- "percentiles_to_chart": stats_module.PERCENTILES_TO_CHART,
106
+ "percentiles_to_chart": stats.PERCENTILES_TO_CHART,
108
107
  "profile": escape(str(environment.profile)) if environment.profile else None,
109
108
  },
110
109
  theme=theme,
@@ -24,16 +24,6 @@ from .env import Environment
24
24
  from .html import get_html_report, process_html_filename
25
25
  from .input_events import input_listener
26
26
  from .log import greenlet_exception_logger, setup_logging
27
- from .stats import (
28
- StatsCSV,
29
- StatsCSVFileWriter,
30
- print_error_report,
31
- print_percentile_stats,
32
- print_stats,
33
- print_stats_json,
34
- stats_history,
35
- stats_printer,
36
- )
37
27
  from .user.inspectuser import print_task_ratio, print_task_ratio_json
38
28
  from .util.load_locustfile import load_locustfile
39
29
 
@@ -141,31 +131,7 @@ def main():
141
131
  available_user_classes[key] = value
142
132
  available_user_tasks[key] = value.tasks or {}
143
133
 
144
- if len(stats.PERCENTILES_TO_CHART) > 6:
145
- logging.error("stats.PERCENTILES_TO_CHART parameter should be a maximum of 6 parameters \n")
146
- sys.exit(1)
147
-
148
- def is_valid_percentile(parameter):
149
- try:
150
- if 0 < float(parameter) < 1:
151
- return True
152
- return False
153
- except ValueError:
154
- return False
155
-
156
- for percentile in stats.PERCENTILES_TO_CHART:
157
- if not is_valid_percentile(percentile):
158
- logging.error(
159
- "stats.PERCENTILES_TO_CHART parameter need to be float and value between. 0 < percentile < 1 Eg 0.95\n"
160
- )
161
- sys.exit(1)
162
-
163
- for percentile in stats.PERCENTILES_TO_STATISTICS:
164
- if not is_valid_percentile(percentile):
165
- logging.error(
166
- "stats.PERCENTILES_TO_STATISTICS parameter need to be float and value between. 0 < percentile < 1 Eg 0.95\n"
167
- )
168
- sys.exit(1)
134
+ stats.validate_stats_configuration()
169
135
 
170
136
  # parse all command line options
171
137
  options = parse_options()
@@ -422,11 +388,11 @@ See https://github.com/locustio/locust/wiki/Installation#increasing-maximum-numb
422
388
  base_csv_dir = options.csv_prefix[: -len(base_csv_file)]
423
389
  if not os.path.exists(base_csv_dir) and len(base_csv_dir) != 0:
424
390
  os.makedirs(base_csv_dir)
425
- stats_csv_writer = StatsCSVFileWriter(
391
+ stats_csv_writer = stats.StatsCSVFileWriter(
426
392
  environment, stats.PERCENTILES_TO_REPORT, options.csv_prefix, options.stats_history_enabled
427
393
  )
428
394
  else:
429
- stats_csv_writer = StatsCSV(environment, stats.PERCENTILES_TO_REPORT)
395
+ stats_csv_writer = stats.StatsCSV(environment, stats.PERCENTILES_TO_REPORT)
430
396
 
431
397
  # start Web UI
432
398
  if not options.headless and not options.worker:
@@ -512,10 +478,10 @@ See https://github.com/locustio/locust/wiki/Installation#increasing-maximum-numb
512
478
  stats_printer_greenlet = None
513
479
  if not options.only_summary and (options.print_stats or (options.headless and not options.worker)):
514
480
  # spawn stats printing greenlet
515
- stats_printer_greenlet = gevent.spawn(stats_printer(runner.stats))
481
+ stats_printer_greenlet = gevent.spawn(stats.stats_printer(runner.stats))
516
482
  stats_printer_greenlet.link_exception(greenlet_exception_handler)
517
483
 
518
- gevent.spawn(stats_history, runner)
484
+ gevent.spawn(stats.stats_history, runner)
519
485
 
520
486
  def start_automatic_run():
521
487
  if options.master:
@@ -633,11 +599,11 @@ See https://github.com/locustio/locust/wiki/Installation#increasing-maximum-numb
633
599
  if runner is not None:
634
600
  runner.quit()
635
601
  if options.json:
636
- print_stats_json(runner.stats)
602
+ stats.print_stats_json(runner.stats)
637
603
  elif not isinstance(runner, locust.runners.WorkerRunner):
638
- print_stats(runner.stats, current=False)
639
- print_percentile_stats(runner.stats)
640
- print_error_report(runner.stats)
604
+ stats.print_stats(runner.stats, current=False)
605
+ stats.print_percentile_stats(runner.stats)
606
+ stats.print_error_report(runner.stats)
641
607
  environment.events.quit.fire(exit_code=code)
642
608
  sys.exit(code)
643
609
 
@@ -6,25 +6,28 @@ import json
6
6
  import logging
7
7
  import os
8
8
  import signal
9
+ import sys
9
10
  import time
10
11
  from abc import abstractmethod
11
12
  from collections import OrderedDict, defaultdict, namedtuple
12
- from collections.abc import Callable, Iterable
13
13
  from copy import copy
14
14
  from html import escape
15
15
  from itertools import chain
16
- from types import FrameType
17
- from typing import TYPE_CHECKING, Any, NoReturn, Protocol, TypedDict, TypeVar, cast
16
+ from typing import TYPE_CHECKING, Protocol, TypedDict, TypeVar, cast
18
17
 
19
18
  import gevent
20
19
 
21
- from .event import Events
22
20
  from .exception import CatchResponseError
23
21
  from .util.date import format_utc_timestamp
24
22
  from .util.rounding import proper_round
25
23
 
26
24
  if TYPE_CHECKING:
25
+ from collections.abc import Callable, Iterable
26
+ from types import FrameType
27
+ from typing import Any, NoReturn
28
+
27
29
  from .env import Environment
30
+ from .event import Events
28
31
  from .runners import Runner
29
32
 
30
33
  console_logger = logging.getLogger("locust.stats_logger")
@@ -1183,3 +1186,39 @@ class StatsCSVFileWriter(StatsCSV):
1183
1186
 
1184
1187
  def stats_history_file_name(self) -> str:
1185
1188
  return self.base_filepath + "_stats_history.csv"
1189
+
1190
+
1191
+ def _is_valid_percentile(parameter) -> bool:
1192
+ """Validate single percentile value from .stats constants."""
1193
+ try:
1194
+ if 0 < float(parameter) < 1:
1195
+ return True
1196
+ return False
1197
+ except ValueError:
1198
+ return False
1199
+
1200
+
1201
+ def validate_stats_configuration() -> None:
1202
+ """
1203
+ This function validates .stats file's constants, that may be patched in user
1204
+ environments.
1205
+
1206
+ No return in normal conditional. Stops locust execution on error.
1207
+ """
1208
+ if len(PERCENTILES_TO_CHART) > 6:
1209
+ sys.stderr.write("stats.PERCENTILES_TO_CHART parameter should be a maximum of 6 parameters \n")
1210
+ sys.exit(1)
1211
+
1212
+ for percentile in PERCENTILES_TO_CHART:
1213
+ if not _is_valid_percentile(percentile):
1214
+ sys.stderr.write(
1215
+ "stats.PERCENTILES_TO_CHART parameter need to be float and value between. 0 < percentile < 1 Eg 0.95\n"
1216
+ )
1217
+ sys.exit(1)
1218
+
1219
+ for percentile in PERCENTILES_TO_STATISTICS:
1220
+ if not _is_valid_percentile(percentile):
1221
+ sys.stderr.write(
1222
+ "stats.PERCENTILES_TO_STATISTICS parameter need to be float and value between. 0 < percentile < 1 Eg 0.95\n"
1223
+ )
1224
+ sys.exit(1)
@@ -32,12 +32,10 @@ from flask_login import LoginManager, login_required
32
32
  from gevent import pywsgi
33
33
 
34
34
  from . import __version__ as version
35
- from . import argument_parser
36
- from . import stats as stats_module
35
+ from . import argument_parser, stats
37
36
  from .html import DEFAULT_BUILD_PATH, get_html_report, render_template_from
38
37
  from .log import get_logs, greenlet_exception_logger
39
38
  from .runners import STATE_MISSING, STATE_RUNNING, MasterRunner
40
- from .stats import StatsCSV, StatsCSVFileWriter, StatsErrorDict, sort_stats
41
39
  from .user.inspectuser import get_ratio
42
40
  from .util.cache import memoize
43
41
  from .util.date import format_safe_timestamp
@@ -126,7 +124,7 @@ class WebUI:
126
124
  web_login: bool = False,
127
125
  tls_cert: str | None = None,
128
126
  tls_key: str | None = None,
129
- stats_csv_writer: StatsCSV | None = None,
127
+ stats_csv_writer: stats.StatsCSV | None = None,
130
128
  delayed_start=False,
131
129
  userclass_picker_is_active=False,
132
130
  build_path: str | None = None,
@@ -145,7 +143,7 @@ class WebUI:
145
143
  allows for adding Flask routes or Blueprints before accepting requests, avoiding errors.
146
144
  """
147
145
  environment.web_ui = self
148
- self.stats_csv_writer = stats_csv_writer or StatsCSV(environment, stats_module.PERCENTILES_TO_REPORT)
146
+ self.stats_csv_writer = stats_csv_writer or stats.StatsCSV(environment, stats.PERCENTILES_TO_REPORT)
149
147
  self.environment = environment
150
148
  self.host = host
151
149
  self.port = port
@@ -404,7 +402,11 @@ class WebUI:
404
402
  @self.auth_required_if_enabled
405
403
  def request_stats_full_history_csv() -> Response:
406
404
  options = self.environment.parsed_options
407
- if options and options.stats_history_enabled and isinstance(self.stats_csv_writer, StatsCSVFileWriter):
405
+ if (
406
+ options
407
+ and options.stats_history_enabled
408
+ and isinstance(self.stats_csv_writer, stats.StatsCSVFileWriter)
409
+ ):
408
410
  return send_file(
409
411
  os.path.abspath(self.stats_csv_writer.stats_history_file_name()),
410
412
  mimetype="text/csv",
@@ -430,12 +432,12 @@ class WebUI:
430
432
  @self.auth_required_if_enabled
431
433
  @memoize(timeout=DEFAULT_CACHE_TIME, dynamic_timeout=True)
432
434
  def request_stats() -> Response:
433
- stats: list[dict[str, Any]] = []
434
- errors: list[StatsErrorDict] = []
435
+ _stats: list[dict[str, Any]] = []
436
+ errors: list[stats.StatsErrorDict] = []
435
437
 
436
438
  if environment.runner is None:
437
439
  report = {
438
- "stats": stats,
440
+ "stats": _stats,
439
441
  "errors": errors,
440
442
  "total_rps": 0.0,
441
443
  "total_fail_per_sec": 0.0,
@@ -451,21 +453,21 @@ class WebUI:
451
453
 
452
454
  return jsonify(report)
453
455
 
454
- for s in chain(sort_stats(environment.runner.stats.entries), [environment.runner.stats.total]):
455
- stats.append(s.to_dict())
456
+ for s in chain(stats.sort_stats(environment.runner.stats.entries), [environment.runner.stats.total]):
457
+ _stats.append(s.to_dict())
456
458
 
457
459
  errors = [e.serialize() for e in environment.runner.errors.values()]
458
460
 
459
461
  # Truncate the total number of stats and errors displayed since a large number of rows will cause the app
460
462
  # to render extremely slowly. Aggregate stats should be preserved.
461
- truncated_stats = stats[:500]
462
- if len(stats) > 500:
463
- truncated_stats += [stats[-1]]
463
+ truncated_stats = _stats[:500]
464
+ if len(_stats) > 500:
465
+ truncated_stats += [_stats[-1]]
464
466
 
465
467
  report = {"stats": truncated_stats, "errors": errors[:500]}
466
- total_stats = stats[-1]
468
+ total_stats = _stats[-1]
467
469
 
468
- if stats:
470
+ if _stats:
469
471
  report["total_rps"] = total_stats["current_rps"]
470
472
  report["total_fail_per_sec"] = total_stats["current_fail_per_sec"]
471
473
  report["fail_ratio"] = environment.runner.stats.total.fail_ratio
@@ -473,7 +475,7 @@ class WebUI:
473
475
  f"response_time_percentile_{percentile}": environment.runner.stats.total.get_current_response_time_percentile(
474
476
  percentile
475
477
  )
476
- for percentile in stats_module.PERCENTILES_TO_CHART
478
+ for percentile in stats.PERCENTILES_TO_CHART
477
479
  }
478
480
 
479
481
  if isinstance(environment.runner, MasterRunner):
@@ -658,7 +660,7 @@ class WebUI:
658
660
  else:
659
661
  worker_count = 0
660
662
 
661
- stats = self.environment.runner.stats
663
+ request_stats = self.environment.runner.stats
662
664
  extra_options = argument_parser.ui_extra_args_dict()
663
665
 
664
666
  available_user_classes = None
@@ -690,7 +692,7 @@ class WebUI:
690
692
  "user_count": self.environment.runner.user_count,
691
693
  "version": version,
692
694
  "host": host if host else "",
693
- "history": stats.history if stats.num_requests > 0 else [],
695
+ "history": request_stats.history if request_stats.num_requests > 0 else [],
694
696
  "override_host_warning": override_host_warning,
695
697
  "num_users": options and options.num_users,
696
698
  "spawn_rate": options and options.spawn_rate,
@@ -710,8 +712,8 @@ class WebUI:
710
712
  "available_shape_classes": available_shape_classes,
711
713
  "available_user_tasks": available_user_tasks,
712
714
  "users": users,
713
- "percentiles_to_chart": stats_module.PERCENTILES_TO_CHART,
714
- "percentiles_to_statistics": stats_module.PERCENTILES_TO_STATISTICS,
715
+ "percentiles_to_chart": stats.PERCENTILES_TO_CHART,
716
+ "percentiles_to_statistics": stats.PERCENTILES_TO_STATISTICS,
715
717
  }
716
718
 
717
719
  self.template_args = {**self.template_args, **new_template_args}
File without changes
File without changes
File without changes