diff --git a/docs/config_reference.rst b/docs/config_reference.rst index 299a1be9d..a085e893f 100644 --- a/docs/config_reference.rst +++ b/docs/config_reference.rst @@ -10,21 +10,6 @@ Android ------------------------ *\- (no description).* -Appium -====== - -``appium_cmd`` (string) ------------------------ -*\- (no description). Default:* ``appium`` - -``port`` (string) ------------------ -*\- (no description). Default:* ``4723`` - -``user`` (string) ------------------ -*\- (no description). Default:* ``""`` - Autostop ======== @@ -47,53 +32,66 @@ Autostop ------------------------ *\- path to file to store autostop report. Default:* ``autostop_report.txt`` -BatteryHistorian -================ - -``device_id`` (string) ----------------------- -*\- (no description). Default:* ``None`` - -:nullable: - True - Bfg === ``address`` (string) -------------------- -*\- (no description).* +*\- Address of target. Format: [host]:port, [ipv4]:port, [ipv6]:port. Port is optional. Tank checks each test if port is available.* + +:examples: + ``127.0.0.1:8080`` + + ``www.w3c.org`` ``ammo_limit`` (integer) ------------------------ -*\- (no description). Default:* ``-1`` +*\- Upper limit for the total number of requests. Default:* ``-1`` ``ammo_type`` (string) ---------------------- -*\- (no description). Default:* ``caseline`` +*\- Ammo format. Default:* ``caseline`` ``ammofile`` (string) --------------------- -*\- (no description). Default:* ``""`` +*\- Path to ammo file. Default:* ``""`` -``autocases`` (string) ----------------------- -*\- (no description). Default:* ``0`` +:tutorial_link: + http://yandextank.readthedocs.io/en/latest/core_and_modules.html#bfg + +``autocases`` (integer or string) +--------------------------------- +*\- Use to automatically tag requests. Requests might be grouped by tag for later analysis. Default:* ``0`` + +:one of: + :````: use N first uri parts to tag request, slashes are replaced with underscores + :``uniq``: tag each request with unique uid + :``uri``: tag each request with its uri path, slashes are replaced with underscores + +:examples: + ``2`` + /example/search/hello/help/us?param1=50 -> _example_search + ``3`` + /example/search/hello/help/us?param1=50 -> _example_search_hello + ``uniq`` + /example/search/hello/help/us?param1=50 -> c98b0520bb6a451c8bc924ed1fd72553 + ``uri`` + /example/search/hello/help/us?param1=50 -> _example_search_hello_help_us ``cache_dir`` (string) ---------------------- -*\- (no description). Default:* ``None`` +*\- stpd\-file cache directory. If not specified, defaults to base artifacts directory. Default:* ``None`` :nullable: True ``cached_stpd`` (boolean) ------------------------- -*\- (no description). Default:* ``False`` +*\- Use cached stpd file. Default:* ``False`` ``chosen_cases`` (string) ------------------------- -*\- (no description). Default:* ``""`` +*\- Use only selected cases. Default:* ``""`` ``enum_ammo`` (boolean) ----------------------- @@ -105,80 +103,120 @@ Bfg ``force_stepping`` (integer) ---------------------------- -*\- (no description). Default:* ``0`` +*\- Ignore cached stpd files, force stepping. Default:* ``0`` ``green_threads_per_instance`` (integer) ---------------------------------------- -*\- (no description). Default:* ``1000`` +*\- Number of green threads every worker process will execute. For "green" worker type only. Default:* ``1000`` + +:tutorial_link: + http://yandextank.readthedocs.io/en/latest/core_and_modules.html#bfg ``gun_config`` (dict) --------------------- -*\- (no description).* +*\- Options for your load scripts.* :``base_address`` (string): - *\- (no description).* + *\- base target address.* :``class_name`` (string): - *\- (no description). Default:* ``LoadTest`` + *\- class that contains load scripts. Default:* ``LoadTest`` :``init_param`` (string): - *\- (no description). Default:* ``""`` + *\- parameter that's passed to "setup" method. Default:* ``""`` :``module_name`` (string): - *\- (no description).* + *\- name of module that contains load scripts.* :``module_path`` (string): - *\- (no description). Default:* ``""`` + *\- directory of python module that contains load scripts. Default:* ``""`` :allow_unknown: True +:tutorial_link: + http://yandextank.readthedocs.io/en/latest/core_and_modules.html#bfg ``gun_type`` (string) --------------------- -*\- (no description).* **Required.** +*\- Type of gun BFG should use.* **Required.** + +:tutorial_link: + http://yandextank.readthedocs.io/en/latest/core_and_modules.html#bfg-options :one of: [``custom``, ``http``, ``scenario``, ``ultimate``] ``header_http`` (string) ------------------------ -*\- (no description). Default:* ``1.0`` +*\- HTTP version. Default:* ``1.0`` -``headers`` (string) --------------------- -*\- (no description). Default:* ``""`` +:one of: + :``1.0``: http 1.0 + :``1.1``: http 1.1 + +``headers`` (list of string) +---------------------------- +*\- HTTP headers. Default:* ``[]`` + +:[list_element] (string): + *\- Format: "Header: Value".* + + :examples: + ``accept: text/html`` ``instances`` (integer) ----------------------- -*\- (no description). Default:* ``1000`` +*\- number of processes (simultaneously working clients). Default:* ``1000`` ``load_profile`` (dict) ----------------------- -*\- (no description).* **Required.** +*\- Configure your load setting the number of RPS or instances (clients) as a function of time, or using a prearranged schedule.* **Required.** :``load_type`` (string): - *\- (no description).* **Required.** + *\- Choose control parameter.* **Required.** - :regex: - ^rps|instances|stpd_file$ + :one of: + :``instances``: control the number of instances + :``rps``: control the rps rate + :``stpd_file``: use prearranged schedule file :``schedule`` (string): - *\- (no description).* **Required.** + *\- load schedule or path to stpd file.* **Required.** + + :examples: + ``const(200,90s)`` + constant load of 200 instances/rps during 90s + ``line(100,200,10m)`` + linear growth from 100 to 200 instances/rps during 10 minutes + ``test_dir/test_backend.stpd`` + path to ready schedule file + +:tutorial_link: + http://yandextank.readthedocs.io/en/latest/tutorial.html#tutorials ``loop`` (integer) ------------------ -*\- (no description). Default:* ``-1`` +*\- Loop over ammo file for the given amount of times. Default:* ``-1`` ``pip`` (string) ---------------- -*\- (no description). Default:* ``""`` +*\- pip modules to install before the test. Use multiline to install multiple modules. Default:* ``""`` -``uris`` (string) ------------------ -*\- (no description). Default:* ``""`` +``uris`` (list of string) +------------------------- +*\- URI list. Default:* ``[]`` + +:[list_element] (string): + *\- URI path string.* + + :examples: + ``["/example/search", "/example/search/hello", "/example/search/hello/help"]`` ``use_caching`` (boolean) ------------------------- -*\- (no description). Default:* ``True`` +*\- Enable stpd\-file caching. Default:* ``True`` ``worker_type`` (string) ------------------------ *\- (no description). Default:* ``""`` +:tutorial_link: + http://yandextank.readthedocs.io/en/latest/core_and_modules.html#bfg-worker-type + Console ======= @@ -225,47 +263,47 @@ DataUploader ``api_address`` (string) ------------------------ -*\- (no description). Default:* ``https://overload.yandex.net/`` +*\- api base address. Default:* ``https://overload.yandex.net/`` ``api_attempts`` (integer) -------------------------- -*\- (no description). Default:* ``60`` +*\- number of retries in case of api fault. Default:* ``60`` ``api_timeout`` (integer) ------------------------- -*\- (no description). Default:* ``10`` +*\- delay between retries in case of api fault. Default:* ``10`` ``chunk_size`` (integer) ------------------------ -*\- (no description). Default:* ``500000`` +*\- max amount of data to be sent in single requests. Default:* ``500000`` ``component`` (string) ---------------------- -*\- (no description). Default:* ``""`` +*\- component of your software. Default:* ``""`` ``connection_timeout`` (integer) -------------------------------- -*\- (no description). Default:* ``30`` +*\- tcp connection timeout. Default:* ``30`` ``ignore_target_lock`` (boolean) -------------------------------- -*\- (no description). Default:* ``False`` +*\- start test even if target is locked. Default:* ``False`` ``job_dsc`` (string) -------------------- -*\- (no description). Default:* ``""`` +*\- job description. Default:* ``""`` ``job_name`` (string) --------------------- -*\- (no description). Default:* ``none`` +*\- job name. Default:* ``none`` ``jobno_file`` (string) ----------------------- -*\- (no description). Default:* ``jobno_file.txt`` +*\- file to save job number to. Default:* ``jobno_file.txt`` ``jobno`` (string) ------------------ -*\- (no description).* +*\- number of an existing job. Use to upload data to an existing job. Requres upload token.* :dependencies: upload_token @@ -283,70 +321,70 @@ DataUploader ``log_data_requests`` (boolean) ------------------------------- -*\- (no description). Default:* ``False`` +*\- log POSTs of test data for debugging. Tank should be launched in debug mode (\-\-debug). Default:* ``False`` ``log_monitoring_requests`` (boolean) ------------------------------------- -*\- (no description). Default:* ``False`` +*\- log POSTs of monitoring data for debugging. Tank should be launched in debug mode (\-\-debug). Default:* ``False`` ``log_other_requests`` (boolean) -------------------------------- -*\- (no description). Default:* ``False`` +*\- log other api requests for debugging. Tank should be launched in debug mode (\-\-debug). Default:* ``False`` ``log_status_requests`` (boolean) --------------------------------- -*\- (no description). Default:* ``False`` +*\- log status api requests for debugging. Tank should be launched in debug mode (\-\-debug). Default:* ``False`` ``maintenance_attempts`` (integer) ---------------------------------- -*\- (no description). Default:* ``10`` +*\- number of retries in case of api maintanance downtime. Default:* ``10`` ``maintenance_timeout`` (integer) --------------------------------- -*\- (no description). Default:* ``60`` +*\- delay between retries in case of api maintanance downtime. Default:* ``60`` ``meta`` (dict) --------------- -*\- (no description).* +*\- additional meta information.* ``network_attempts`` (integer) ------------------------------ -*\- (no description). Default:* ``60`` +*\- number of retries in case of network fault. Default:* ``60`` ``network_timeout`` (integer) ----------------------------- -*\- (no description). Default:* ``10`` +*\- delay between retries in case of network fault. Default:* ``10`` -``notify`` (string) -------------------- -*\- (no description). Default:* ``""`` +``notify`` (list of string) +--------------------------- +*\- users to notify. Default:* ``[]`` ``operator`` (string) --------------------- -*\- (no description). Default:* ``None`` +*\- user who started the test. Default:* ``None`` :nullable: True ``regress`` (boolean) --------------------- -*\- (no description). Default:* ``False`` +*\- mark test as regression. Default:* ``False`` ``send_status_period`` (integer) -------------------------------- -*\- (no description). Default:* ``10`` +*\- delay between status notifications. Default:* ``10`` ``strict_lock`` (boolean) ------------------------- -*\- (no description). Default:* ``False`` +*\- set true to abort the test if the the target's lock check is failed. Default:* ``False`` ``target_lock_duration`` (string) --------------------------------- -*\- (no description). Default:* ``30m`` +*\- how long should the target be locked. In most cases this should be long enough for the test to run. Target will be unlocked automatically right after the test is finished. Default:* ``30m`` ``task`` (string) ----------------- -*\- (no description). Default:* ``""`` +*\- task title. Default:* ``""`` ``threads_timeout`` (integer) ----------------------------- @@ -354,11 +392,11 @@ DataUploader ``token_file`` (string) ----------------------- -*\- (no description).* +*\- API token.* ``upload_token`` (string) ------------------------- -*\- (no description). Default:* ``None`` +*\- Job's token. Use to upload data to an existing job. Requres jobno. Default:* ``None`` :dependencies: jobno @@ -367,11 +405,11 @@ DataUploader ``ver`` (string) ---------------- -*\- (no description). Default:* ``""`` +*\- version of the software tested. Default:* ``""`` ``writer_endpoint`` (string) ---------------------------- -*\- (no description). Default:* ``""`` +*\- writer api endpoint. Default:* ``""`` Influx ====== @@ -417,18 +455,18 @@ JMeter ``args`` (string) ----------------- -*\- (no description). Default:* ``""`` +*\- additional commandline arguments for JMeter. Default:* ``""`` ``buffer_size`` (integer) ------------------------- -*\- (no description). Default:* ``None`` +*\- jmeter buffer size. Default:* ``None`` :nullable: True ``buffered_seconds`` (integer) ------------------------------ -*\- (no description). Default:* ``3`` +*\- Aggregator delay \- to be sure that everything were read from jmeter results file. Default:* ``3`` ``exclude_markers`` (list of string) ------------------------------------ @@ -442,46 +480,46 @@ JMeter ``ext_log`` (string) -------------------- -*\- (no description). Default:* ``none`` +*\- additional log, jmeter xml format. Saved in test dir as jmeter_ext_XXXX.jtl. Default:* ``none`` :one of: [``none``, ``errors``, ``all``] ``extended_log`` (string) ------------------------- -*\- (no description). Default:* ``none`` +*\- additional log, jmeter xml format. Saved in test dir as jmeter_ext_XXXX.jtl. Default:* ``none`` :one of: [``none``, ``errors``, ``all``] ``jmeter_path`` (string) ------------------------ -*\- (no description). Default:* ``jmeter`` +*\- Path to JMeter. Default:* ``jmeter`` ``jmeter_ver`` (float) ---------------------- -*\- (no description). Default:* ``3.0`` +*\- Which JMeter version tank should expect. Affects the way connection time is logged. Default:* ``3.0`` ``jmx`` (string) ---------------- -*\- (no description).* +*\- Testplan for execution.* ``shutdown_timeout`` (integer) ------------------------------ -*\- (no description). Default:* ``10`` +*\- timeout for automatic test shutdown. Default:* ``10`` ``variables`` (dict) -------------------- -*\- (no description). Default:* ``{}`` +*\- variables for jmx testplan. Default:* ``{}`` JsonReport ========== ``monitoring_log`` (string) --------------------------- -*\- (no description). Default:* ``monitoring.log`` +*\- file name for monitoring log. Default:* ``monitoring.log`` ``test_data_log`` (string) -------------------------- -*\- (no description). Default:* ``test_data.log`` +*\- file name for test data log. Default:* ``test_data.log`` Pandora ======= @@ -513,9 +551,6 @@ Phantom ------------------------------------ *\- Libs for Phantom, to be added to phantom config file in section "module_setup". Default:* ``[]`` -:[list_element] (string): - *\- (no description).* - ``address`` (string) -------------------- *\- Address of target. Format: [host]:port, [ipv4]:port, [ipv6]:port. Port is optional. Tank checks each test if port is available.* **Required.** @@ -672,8 +707,9 @@ Phantom linear growth from 100 to 200 instances/rps during 10 minutes ``test_dir/test_backend.stpd`` path to ready schedule file - :tutorial_link: - http://yandextank.readthedocs.io/en/latest/tutorial.html#tutorials + +:tutorial_link: + http://yandextank.readthedocs.io/en/latest/tutorial.html#tutorials ``loop`` (integer) ------------------ @@ -804,73 +840,80 @@ ShellExec ``catch_out`` (boolean) ----------------------- -*\- (no description). Default:* ``False`` +*\- show commands stdout. Default:* ``False`` ``end`` (string) ---------------- -*\- (no description). Default:* ``""`` +*\- shell command to execute after test end. Default:* ``""`` ``poll`` (string) ----------------- -*\- (no description). Default:* ``""`` +*\- shell command to execute every second while test is running. Default:* ``""`` ``post_process`` (string) ------------------------- -*\- (no description). Default:* ``""`` +*\- shell command to execute on post process stage. Default:* ``""`` ``prepare`` (string) -------------------- -*\- (no description). Default:* ``""`` +*\- shell command to execute on prepare stage. Default:* ``""`` ``start`` (string) ------------------ -*\- (no description). Default:* ``""`` +*\- shell command to execute on start. Default:* ``""`` ShootExec ========= ``cmd`` (string) ---------------- -*\- (no description).* **Required.** +*\- command that produces test results and stats in Phantom format.* **Required.** ``output_path`` (string) ------------------------ -*\- (no description).* **Required.** +*\- path to test results.* **Required.** ``stats_path`` (string) ----------------------- -*\- (no description). Default:* ``""`` +*\- path to tests stats. Default:* ``None`` + +:nullable: + True Telegraf ======== ``config_contents`` (string) ---------------------------- -*\- (no description).* +*\- used to repeat tests from Overload, not for manual editing.* ``config`` (string) ------------------- -*\- (no description). Default:* ``auto`` +*\- Path to monitoring config file. Default:* ``auto`` + +:one of: + :````: path to telegraf configuration file + :``auto``: collect default metrics from default_target host + :``none``: disable monitoring ``default_target`` (string) --------------------------- -*\- (no description). Default:* ``localhost`` +*\- host to collect default metrics from (if "config: auto" specified). Default:* ``localhost`` ``disguise_hostnames`` (boolean) -------------------------------- -*\- (no description). Default:* ``True`` +*\- Disguise real host names \- use this if you upload results to Overload and dont want others to see your hostnames. Default:* ``True`` ``kill_old`` (boolean) ---------------------- -*\- (no description). Default:* ``False`` +*\- kill old hanging agents on target(s). Default:* ``False`` ``ssh_timeout`` (string) ------------------------ -*\- (no description). Default:* ``5s`` - -TipsAndTricks -============= +*\- timeout of ssh connection to target(s). Default:* ``5s`` -``disable`` (boolean) ---------------------- -*\- (no description). Default:* ``False`` \ No newline at end of file +:examples: + ``10s`` + 10 seconds + ``2m`` + 2 minutes \ No newline at end of file diff --git a/setup.py b/setup.py index a6bced7a6..be671b73b 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,8 @@ setup( name='yandextank', - version='1.9.9', + version='1.9.10', + description='a performance measurement tool', longer_description=''' Yandex.Tank is a performance measurement and load testing automatization tool. diff --git a/yandextank/aggregator/tank_aggregator.py b/yandextank/aggregator/tank_aggregator.py index 93b71b543..fbb42e555 100644 --- a/yandextank/aggregator/tank_aggregator.py +++ b/yandextank/aggregator/tank_aggregator.py @@ -7,7 +7,7 @@ from .aggregator import Aggregator, DataPoller from .chopper import TimeChopper -from yandextank.common.interfaces import AggregateResultListener +from yandextank.common.interfaces import AggregateResultListener, StatsReader from yandextank.common.util import Drain, Chopper logger = logging.getLogger(__name__) @@ -59,11 +59,14 @@ def __init__(self, generator): self.drain = None self.stats_drain = None + @staticmethod + def load_config(): + return json.loads(resource_string(__name__, 'config/phout.json').decode('utf8')) + def start_test(self): self.reader = self.generator.get_reader() self.stats_reader = self.generator.get_stats_reader() - aggregator_config = json.loads( - resource_string(__name__, 'config/phout.json').decode('utf8')) + aggregator_config = self.load_config() verbose_histogram = True if verbose_histogram: logger.info("using verbose histogram") @@ -84,14 +87,14 @@ def start_test(self): else: logger.warning("Generator not found. Generator must provide a reader and a stats_reader interface") - def _collect_data(self): + def _collect_data(self, end=False): """ Collect data, cache it and send to listeners """ data = get_from_queue(self.results) stats = get_from_queue(self.stats) - logger.debug("Data timestamps:\n%s" % [d.get('ts') for d in data]) - logger.debug("Stats timestamps:\n%s" % [d.get('ts') for d in stats]) + logger.debug("Data timestamps: %s" % [d.get('ts') for d in data]) + logger.debug("Stats timestamps: %s" % [d.get('ts') for d in stats]) for item in data: ts = item['ts'] if ts in self.stat_cache: @@ -110,6 +113,9 @@ def _collect_data(self): self.__notify_listeners(data_item, stat_item) else: self.stat_cache[ts] = item + if end and len(self.data_cache) > 0: + for ts, data_item in sorted(self.data_cache.items(), key=lambda i: i[0]): + self.__notify_listeners(data_item, StatsReader.stats_item(ts, 0, 0)) def is_test_finished(self): self._collect_data() @@ -125,7 +131,7 @@ def end_test(self, retcode): self.drain.join() if self.stats_drain: self.stats_drain.join() - self._collect_data() + self._collect_data(end=True) return retcode def add_result_listener(self, listener): diff --git a/yandextank/aggregator/tests/phout2927 b/yandextank/aggregator/tests/phout2927 new file mode 100644 index 000000000..ed93bb29d --- /dev/null +++ b/yandextank/aggregator/tests/phout2927 @@ -0,0 +1,3 @@ +1502376593.698 "Technology 797 208 12 521 56 670 31 315 0 404 +1502376594.699 "/v1/tech/ru-RU/latest/maps/jsapi", 750 206 11 452 81 602 24 315 0 404 +1502376597.698 #3 669 146 9 410 104 581 18 315 0 404 \ No newline at end of file diff --git a/yandextank/aggregator/tests/phout2927res.jsonl b/yandextank/aggregator/tests/phout2927res.jsonl new file mode 100644 index 000000000..b5447943d --- /dev/null +++ b/yandextank/aggregator/tests/phout2927res.jsonl @@ -0,0 +1,5 @@ +[ + {"tagged": {"\"Technology": {"size_in": {"max": 315, "total": 315, "len": 1, "min": 315}, "latency": {"max": 521, "total": 521, "len": 1, "min": 521}, "interval_real": {"q": {"q": [50, 75, 80, 85, 90, 95, 98, 99, 100], "value": [797.0, 797.0, 797.0, 797.0, 797.0, 797.0, 797.0, 797.0, 797.0]}, "min": 797, "max": 797, "len": 1, "hist": {"data": [1], "bins": [800.0]}, "total": 797}, "interval_event": {"max": 670, "total": 670, "len": 1, "min": 670}, "receive_time": {"max": 56, "total": 56, "len": 1, "min": 56}, "connect_time": {"max": 208, "total": 208, "len": 1, "min": 208}, "proto_code": {"count": {"404": 1}}, "size_out": {"max": 31, "total": 31, "len": 1, "min": 31}, "send_time": {"max": 12, "total": 12, "len": 1, "min": 12}, "net_code": {"count": {"0": 1}}}}, "overall": {"size_in": {"max": 315, "total": 315, "len": 1, "min": 315}, "latency": {"max": 521, "total": 521, "len": 1, "min": 521}, "interval_real": {"q": {"q": [50, 75, 80, 85, 90, 95, 98, 99, 100], "value": [797.0, 797.0, 797.0, 797.0, 797.0, 797.0, 797.0, 797.0, 797.0]}, "min": 797, "max": 797, "len": 1, "hist": {"data": [1], "bins": [800.0]}, "total": 797}, "interval_event": {"max": 670, "total": 670, "len": 1, "min": 670}, "receive_time": {"max": 56, "total": 56, "len": 1, "min": 56}, "connect_time": {"max": 208, "total": 208, "len": 1, "min": 208}, "proto_code": {"count": {"404": 1}}, "size_out": {"max": 31, "total": 31, "len": 1, "min": 31}, "send_time": {"max": 12, "total": 12, "len": 1, "min": 12}, "net_code": {"count": {"0": 1}}}, "ts": 1502376593}, + {"tagged": {"\"/v1/tech/ru-RU/latest/maps/jsapi\",": {"size_in": {"max": 315, "total": 315, "len": 1, "min": 315}, "latency": {"max": 452, "total": 452, "len": 1, "min": 452}, "interval_real": {"q": {"q": [50, 75, 80, 85, 90, 95, 98, 99, 100], "value": [750.0, 750.0, 750.0, 750.0, 750.0, 750.0, 750.0, 750.0, 750.0]}, "min": 750, "max": 750, "len": 1, "hist": {"data": [1], "bins": [760.0]}, "total": 750}, "interval_event": {"max": 602, "total": 602, "len": 1, "min": 602}, "receive_time": {"max": 81, "total": 81, "len": 1, "min": 81}, "connect_time": {"max": 206, "total": 206, "len": 1, "min": 206}, "proto_code": {"count": {"404": 1}}, "size_out": {"max": 24, "total": 24, "len": 1, "min": 24}, "send_time": {"max": 11, "total": 11, "len": 1, "min": 11}, "net_code": {"count": {"0": 1}}}}, "overall": {"size_in": {"max": 315, "total": 315, "len": 1, "min": 315}, "latency": {"max": 452, "total": 452, "len": 1, "min": 452}, "interval_real": {"q": {"q": [50, 75, 80, 85, 90, 95, 98, 99, 100], "value": [750.0, 750.0, 750.0, 750.0, 750.0, 750.0, 750.0, 750.0, 750.0]}, "min": 750, "max": 750, "len": 1, "hist": {"data": [1], "bins": [760.0]}, "total": 750}, "interval_event": {"max": 602, "total": 602, "len": 1, "min": 602}, "receive_time": {"max": 81, "total": 81, "len": 1, "min": 81}, "connect_time": {"max": 206, "total": 206, "len": 1, "min": 206}, "proto_code": {"count": {"404": 1}}, "size_out": {"max": 24, "total": 24, "len": 1, "min": 24}, "send_time": {"max": 11, "total": 11, "len": 1, "min": 11}, "net_code": {"count": {"0": 1}}}, "ts": 1502376594}, + {"tagged": {"": {"size_in": {"max": 315, "total": 315, "len": 1, "min": 315}, "latency": {"max": 410, "total": 410, "len": 1, "min": 410}, "interval_real": {"q": {"q": [50, 75, 80, 85, 90, 95, 98, 99, 100], "value": [669.0, 669.0, 669.0, 669.0, 669.0, 669.0, 669.0, 669.0, 669.0]}, "min": 669, "max": 669, "len": 1, "hist": {"data": [1], "bins": [670.0]}, "total": 669}, "interval_event": {"max": 581, "total": 581, "len": 1, "min": 581}, "receive_time": {"max": 104, "total": 104, "len": 1, "min": 104}, "connect_time": {"max": 146, "total": 146, "len": 1, "min": 146}, "proto_code": {"count": {"404": 1}}, "size_out": {"max": 18, "total": 18, "len": 1, "min": 18}, "send_time": {"max": 9, "total": 9, "len": 1, "min": 9}, "net_code": {"count": {"0": 1}}}}, "overall": {"size_in": {"max": 315, "total": 315, "len": 1, "min": 315}, "latency": {"max": 410, "total": 410, "len": 1, "min": 410}, "interval_real": {"q": {"q": [50, 75, 80, 85, 90, 95, 98, 99, 100], "value": [669.0, 669.0, 669.0, 669.0, 669.0, 669.0, 669.0, 669.0, 669.0]}, "min": 669, "max": 669, "len": 1, "hist": {"data": [1], "bins": [670.0]}, "total": 669}, "interval_event": {"max": 581, "total": 581, "len": 1, "min": 581}, "receive_time": {"max": 104, "total": 104, "len": 1, "min": 104}, "connect_time": {"max": 146, "total": 146, "len": 1, "min": 146}, "proto_code": {"count": {"404": 1}}, "size_out": {"max": 18, "total": 18, "len": 1, "min": 18}, "send_time": {"max": 9, "total": 9, "len": 1, "min": 9}, "net_code": {"count": {"0": 1}}}, "ts": 1502376597} + ] \ No newline at end of file diff --git a/yandextank/aggregator/tests/test_pipeline.py b/yandextank/aggregator/tests/test_pipeline.py index 2cb52dea3..841d2b8cb 100644 --- a/yandextank/aggregator/tests/test_pipeline.py +++ b/yandextank/aggregator/tests/test_pipeline.py @@ -1,16 +1,19 @@ import json import numpy as np -from pkg_resources import resource_filename +import pytest from queue import Queue + +from yandextank.aggregator import TankAggregator from yandextank.aggregator.aggregator import Aggregator, DataPoller from yandextank.aggregator.chopper import TimeChopper from conftest import MAX_TS, random_split from yandextank.common.util import Drain -with open(resource_filename("yandextank.aggregator", 'config/phout.json')) as f: - AGGR_CONFIG = json.load(f) +from yandextank.plugins.Phantom.reader import string_to_df + +AGGR_CONFIG = TankAggregator.load_config() class TestPipeline(object): @@ -49,3 +52,20 @@ def producer(): drain = Drain(pipeline, results_queue) drain.run() assert results_queue.qsize() == MAX_TS + + @pytest.mark.parametrize('phout, results', [ + ('yandextank/aggregator/tests/phout2927', 'yandextank/aggregator/tests/phout2927res.jsonl') + ]) + def test_invalid_ammo(self, phout, results): + with open(phout) as fp: + reader = [string_to_df(line) for line in fp.readlines()] + pipeline = Aggregator( + TimeChopper( + DataPoller(source=reader, poll_period=0), + cache_size=3), + AGGR_CONFIG, + True) + with open(results) as fp: + results_parsed = json.load(fp) + for item, result in zip(pipeline, results_parsed): + assert item == result diff --git a/yandextank/common/interfaces.py b/yandextank/common/interfaces.py index 71c25f776..a8a92088e 100644 --- a/yandextank/common/interfaces.py +++ b/yandextank/common/interfaces.py @@ -221,3 +221,15 @@ def get_stats_reader(self): def end_test(self, retcode): pass + + +class StatsReader(object): + @staticmethod + def stats_item(ts, instances, rps): + return { + 'ts': ts, + 'metrics': { + 'instances': instances, + 'reqps': rps + } + } diff --git a/yandextank/config_converter/converter.py b/yandextank/config_converter/converter.py index 21bedd433..552aab997 100644 --- a/yandextank/config_converter/converter.py +++ b/yandextank/config_converter/converter.py @@ -36,7 +36,7 @@ def parse_package_name(package_path): 'BatteryHistorian': 'battery_historian', 'Bfg': 'bfg|ultimate_gun|http_gun|custom_gun|scenario_gun', 'Phantom': 'phantom(-.*)?', - 'DataUploader': 'meta|overload', + 'DataUploader': 'meta|overload|uploader|datauploader', 'Telegraf': 'telegraf|monitoring', 'JMeter': 'jmeter', 'ResourceCheck': 'rcheck', @@ -181,6 +181,7 @@ class Option(object): 'Bfg': { 'rps_schedule': convert_rps_schedule, 'instances_schedule': convert_instances_schedule, + 'headers': lambda key, value: {key: re.compile('\[(.*?)\]').findall(value)} }, 'JMeter': { 'exclude_markers': lambda key, value: {key: value.strip().split(' ')} diff --git a/yandextank/config_converter/tests/test_config3.yaml b/yandextank/config_converter/tests/test_config3.yaml index 30f21af09..9599bbbda 100644 --- a/yandextank/config_converter/tests/test_config3.yaml +++ b/yandextank/config_converter/tests/test_config3.yaml @@ -8,7 +8,8 @@ uploader: job_name: light ignore_target_lock: true regress: true - notify: fomars + notify: + - fomars meta: launched_from: centurion #production log diff --git a/yandextank/config_converter/tests/test_config5.1.yaml b/yandextank/config_converter/tests/test_config5.1.yaml index 346119a17..ed615925a 100644 --- a/yandextank/config_converter/tests/test_config5.1.yaml +++ b/yandextank/config_converter/tests/test_config5.1.yaml @@ -11,9 +11,11 @@ bfg: schedule: const(10, 60s) instances: 1 header_http: '1.1' - uris: / + uris: ['/'] loop: 1000 - headers: "[Host: nodejs.load.yandex.net]\n[Connection: close]" + headers: + - 'Host: nodejs.load.yandex.net' + - 'Connection: close' gun_type: ultimate gun_config: module_path: '.' diff --git a/yandextank/config_converter/tests/test_config5.yaml b/yandextank/config_converter/tests/test_config5.yaml index 346119a17..ed615925a 100644 --- a/yandextank/config_converter/tests/test_config5.yaml +++ b/yandextank/config_converter/tests/test_config5.yaml @@ -11,9 +11,11 @@ bfg: schedule: const(10, 60s) instances: 1 header_http: '1.1' - uris: / + uris: ['/'] loop: 1000 - headers: "[Host: nodejs.load.yandex.net]\n[Connection: close]" + headers: + - 'Host: nodejs.load.yandex.net' + - 'Connection: close' gun_type: ultimate gun_config: module_path: '.' diff --git a/yandextank/core/config/00-base.ini b/yandextank/core/config/00-base.ini index e47a4c6cb..f78c6bde5 100644 --- a/yandextank/core/config/00-base.ini +++ b/yandextank/core/config/00-base.ini @@ -7,7 +7,6 @@ plugin_aggregate=yandextank.plugins.Aggregator plugin_autostop=yandextank.plugins.Autostop plugin_telegraf=yandextank.plugins.Telegraf plugin_console=yandextank.plugins.Console -plugin_tips=yandextank.plugins.TipsAndTricks plugin_rcassert=yandextank.plugins.RCAssert plugin_jsonreport=yandextank.plugins.JsonReport artifacts_base_dir=logs diff --git a/yandextank/core/config/00-base.yaml b/yandextank/core/config/00-base.yaml index 577f40121..8e63d2d50 100644 --- a/yandextank/core/config/00-base.yaml +++ b/yandextank/core/config/00-base.yaml @@ -28,9 +28,6 @@ shellexec: telegraf: enabled: true package: yandextank.plugins.Telegraf -tips: - enabled: true - package: yandextank.plugins.TipsAndTricks rcassert: enabled: true package: yandextank.plugins.RCAssert diff --git a/yandextank/core/tankcore.py b/yandextank/core/tankcore.py index 0cab6e20b..d6ff0e933 100644 --- a/yandextank/core/tankcore.py +++ b/yandextank/core/tankcore.py @@ -309,7 +309,7 @@ def wait_for_finish(self): end_time = time.time() diff = end_time - begin_time logger.debug("Polling took %s", diff) - logger.debug("Tank status:\n%s", json.dumps(self.status, indent=2)) + logger.debug("Tank status: %s", json.dumps(self.status)) # screen refresh every 0.5 s if diff < 0.5: time.sleep(0.5 - diff) diff --git a/yandextank/core/tests/test_tankcore.py b/yandextank/core/tests/test_tankcore.py index 9e544b2c7..c766ba398 100644 --- a/yandextank/core/tests/test_tankcore.py +++ b/yandextank/core/tests/test_tankcore.py @@ -100,11 +100,11 @@ def setup_module(module): (CFG1, {'plugin_telegraf', 'plugin_phantom', 'plugin_lunapark', 'plugin_rcheck', 'plugin_shellexec', 'plugin_autostop', - 'plugin_console', 'plugin_tips', 'plugin_rcassert', 'plugin_json_report', + 'plugin_console', 'plugin_rcassert', 'plugin_json_report', }), (CFG2, {'plugin_phantom', 'plugin_lunapark', 'plugin_rcheck', - 'plugin_autostop', 'plugin_console', 'plugin_tips', + 'plugin_autostop', 'plugin_console', 'plugin_rcassert', 'plugin_json_report', } ) diff --git a/yandextank/plugins/Appium/__init__.py b/yandextank/plugins/Appium/__init__.py deleted file mode 100644 index 727937bd0..000000000 --- a/yandextank/plugins/Appium/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from plugin import Plugin # noqa:F401 diff --git a/yandextank/plugins/Appium/config/schema.yaml b/yandextank/plugins/Appium/config/schema.yaml deleted file mode 100644 index 99e1954b9..000000000 --- a/yandextank/plugins/Appium/config/schema.yaml +++ /dev/null @@ -1,9 +0,0 @@ -appium_cmd: - type: string - default: appium -user: - type: string - default: '' -port: - type: string - default: '4723' \ No newline at end of file diff --git a/yandextank/plugins/Appium/plugin.py b/yandextank/plugins/Appium/plugin.py deleted file mode 100644 index 60f3674c3..000000000 --- a/yandextank/plugins/Appium/plugin.py +++ /dev/null @@ -1,75 +0,0 @@ -import logging -import subprocess -import time - -from ...common.interfaces import AbstractPlugin - -logger = logging.getLogger(__name__) - - -class Plugin(AbstractPlugin): - ''' Start appium before the test, stop after it ended ''' - - SECTION = "appium" - - def __init__(self, core, cfg, cfg_updater): - super(Plugin, self).__init__(core, cfg, cfg_updater) - self.appium_cmd = None - self.appium_log = None - self.appium_port = None - self.process_stdout = None - - @staticmethod - def get_key(): - return __file__ - - def get_available_options(self): - opts = ["appium_cmd"] - return opts - - def configure(self): - # plugin part - self.appium_cmd = self.get_option("appium_cmd", "appium") - self.appium_user = self.get_option("user", "") - self.appium_port = self.get_option("port", "4723") - self.appium_log = self.core.mkstemp(".log", "appium_") - self.core.add_artifact_file(self.appium_log) - - def prepare_test(self): - args = [self.appium_cmd, '-p', self.appium_port, '-g', self.appium_log] - if self.appium_user: - args = ["su", "-c", " ".join(args), self.appium_user] - logger.info("Starting appium server: %s", args) - self.process_start_time = time.time() - process_stdout_file = self.core.mkstemp(".log", "appium_stdout_") - self.core.add_artifact_file(process_stdout_file) - self.process_stdout = open(process_stdout_file, 'w') - self.process = subprocess.Popen( - args, - stderr=self.process_stdout, - stdout=self.process_stdout, - close_fds=True) - logger.info("Waiting 5 seconds for Appium to start...") - time.sleep(5) - - def is_test_finished(self): - retcode = self.process.poll() - if retcode is not None: - logger.warning("Appium exited: %s", retcode) - return abs(retcode) - else: - return -1 - - def end_test(self, retcode): - if self.process and self.process.poll() is None: - logger.info( - "Terminating appium process with PID %s", self.process.pid) - self.process.terminate() - if self.process_stdout: - self.process_stdout.close() - else: - logger.warn("Appium finished unexpectedly") - return retcode - - def get_info(self): - return None diff --git a/yandextank/plugins/BatteryHistorian/__init__.py b/yandextank/plugins/BatteryHistorian/__init__.py deleted file mode 100644 index 1decf5270..000000000 --- a/yandextank/plugins/BatteryHistorian/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from plugin import * # noqa:F401,F403 diff --git a/yandextank/plugins/BatteryHistorian/config/schema.yaml b/yandextank/plugins/BatteryHistorian/config/schema.yaml deleted file mode 100644 index 7914c021e..000000000 --- a/yandextank/plugins/BatteryHistorian/config/schema.yaml +++ /dev/null @@ -1,4 +0,0 @@ -device_id: - type: string - nullable: true - default: null \ No newline at end of file diff --git a/yandextank/plugins/BatteryHistorian/plugin.py b/yandextank/plugins/BatteryHistorian/plugin.py deleted file mode 100644 index 2b533319a..000000000 --- a/yandextank/plugins/BatteryHistorian/plugin.py +++ /dev/null @@ -1,79 +0,0 @@ -''' Module that collects android device battery usage ''' - -import logging -import subprocess - -from ...common.interfaces import AbstractPlugin - -logger = logging.getLogger(__name__) - - -class Plugin(AbstractPlugin): - """ Plugin that collects android device battery usage """ - SECTION = "battery_historian" - - @staticmethod - def get_key(): - return __file__ - - def __init__(self, core, cfg, cfg_updater): - AbstractPlugin.__init__(self, core, cfg, cfg_updater) - self.logfile = None - self.default_target = None - self.device_id = None - self.cmds = { - "enable_full_log": - "adb %s shell dumpsys batterystats --enable full-wake-history", - "disable_full_log": - "adb %s shell dumpsys batterystats --disable full-wake-history", - "reset": "adb %s shell dumpsys batterystats --reset", - "dump": "adb %s shell dumpsys batterystats" - } - - def get_available_options(self): - return ["device_id"] - - def configure(self): - self.device_id = self.get_option("device_id", None).strip() - if self.device_id: - modify = '-s {device_id}'.format(device_id=self.device_id) - for key, value in self.cmds.iteritems(): - self.cmds[key] = value % modify - self.logfile = self.core.mkstemp(".log", "battery_historian_") - self.core.add_artifact_file(self.logfile) - - def prepare_test(self): - if self.device_id: - try: - out = subprocess.check_output( - self.cmds['enable_full_log'], shell=True) - logger.debug('Enabling full-log: %s', out) - out = subprocess.check_output(self.cmds['reset'], shell=True) - logger.debug('Reseting battery stats: %s', out) - except subprocess.CalledProcessError: - logger.error( - 'Error trying to prepare battery historian plugin', - exc_info=True) - - def end_test(self, retcode): - if self.device_id: - try: - logger.debug('dumping battery stats') - dump = subprocess.Popen( - self.cmds['dump'], stdout=subprocess.PIPE, - shell=True).communicate()[0] - out = subprocess.check_output( - self.cmds['disable_full_log'], shell=True) - logger.debug('Disabling fulllog: %s', out) - out = subprocess.check_output(self.cmds['reset'], shell=True) - logger.debug('Battery stats reset: %s', out) - except subprocess.CalledProcessError: - logger.error( - 'Error trying to collect battery historian plugin data', - exc_info=True) - with open(self.logfile, 'w') as f: - f.write(dump) - return retcode - - def is_test_finished(self): - return -1 diff --git a/yandextank/plugins/Bfg/config/schema.yaml b/yandextank/plugins/Bfg/config/schema.yaml index 8c258a9ea..2a0fc50f2 100644 --- a/yandextank/plugins/Bfg/config/schema.yaml +++ b/yandextank/plugins/Bfg/config/schema.yaml @@ -1,27 +1,49 @@ address: type: string + description: 'Address of target. Format: [host]:port, [ipv4]:port, [ipv6]:port. Port is optional. Tank checks each test if port is available' + examples: {'127.0.0.1:8080': '', 'www.w3c.org': ''} ammofile: type: string default: '' + description: Path to ammo file + tutorial_link: http://yandextank.readthedocs.io/en/latest/core_and_modules.html#bfg ammo_limit: default: -1 type: integer + description: Upper limit for the total number of requests ammo_type: type: string default: caseline + description: Ammo format autocases: - type: string - default: '0' + description: Use to automatically tag requests. Requests might be grouped by tag for later analysis. + anyof: + - type: integer + - type: string + allowed: [uri, uniq] + default: 0 + values_description: + uri: tag each request with its uri path, slashes are replaced with underscores + uniq: tag each request with unique uid + : use N first uri parts to tag request, slashes are replaced with underscores + examples: + 2: '/example/search/hello/help/us?param1=50 -> _example_search' + 3: '/example/search/hello/help/us?param1=50 -> _example_search_hello' + uri: '/example/search/hello/help/us?param1=50 -> _example_search_hello_help_us' + uniq: '/example/search/hello/help/us?param1=50 -> c98b0520bb6a451c8bc924ed1fd72553' cached_stpd: type: boolean default: false + description: Use cached stpd file cache_dir: type: string nullable: true default: null + description: stpd-file cache directory. If not specified, defaults to base artifacts directory chosen_cases: type: string default: '' + description: Use only selected cases. enum_ammo: type: boolean default: false @@ -31,62 +53,107 @@ file_cache: force_stepping: type: integer default: 0 + description: Ignore cached stpd files, force stepping green_threads_per_instance: type: integer default: 1000 + description: Number of green threads every worker process will execute. For "green" worker type only. + tutorial_link: http://yandextank.readthedocs.io/en/latest/core_and_modules.html#bfg gun_config: type: dict + tutorial_link: http://yandextank.readthedocs.io/en/latest/core_and_modules.html#bfg + description: Options for your load scripts schema: base_address: type: string + description: base target address class_name: type: string default: LoadTest + description: class that contains load scripts module_path: type: string default: '' + description: directory of python module that contains load scripts module_name: type: string + description: name of module that contains load scripts init_param: type: string default: '' + description: parameter that's passed to "setup" method allow_unknown: true gun_type: type: string - allowed: ['custom', 'http', 'scenario', 'ultimate'] + allowed: [custom, http, scenario, ultimate] required: true + description: Type of gun BFG should use + tutorial_link: http://yandextank.readthedocs.io/en/latest/core_and_modules.html#bfg-options headers: - type: string - default: '' + type: list + default: [] + description: HTTP headers + schema: + description: 'Format: "Header: Value"' + type: string + examples: {'accept: text/html': ''} header_http: type: string default: '1.0' + description: HTTP version + allowed: ['1.0', '1.1'] + values_description: + '1.0': http 1.0 + '1.1': http 1.1 instances: type: integer default: 1000 + description: number of processes (simultaneously working clients) load_profile: type: dict required: true + description: Configure your load setting the number of RPS or instances (clients) as a function of time, or using a prearranged schedule + tutorial_link: http://yandextank.readthedocs.io/en/latest/tutorial.html#tutorials schema: load_type: type: string - regex: '^rps|instances|stpd_file$' + allowed: [rps, instances, stpd_file] + values_description: + rps: control the rps rate + instances: control the number of instances + stpd_file: use prearranged schedule file required: true + description: Choose control parameter schedule: type: string required: true + description: load schedule or path to stpd file + examples: + line(100,200,10m): linear growth from 100 to 200 instances/rps during 10 minutes + const(200,90s): constant load of 200 instances/rps during 90s + test_dir/test_backend.stpd: path to ready schedule file loop: type: integer default: -1 + description: Loop over ammo file for the given amount of times pip: type: string default: '' + description: pip modules to install before the test. Use multiline to install multiple modules. uris: - type: string - default: '' + type: list + default: [] + description: URI list + schema: + type: string + description: URI path string + examples: + '["/example/search", "/example/search/hello", "/example/search/hello/help"]': '' use_caching: type: boolean default: true + description: Enable stpd-file caching worker_type: type: string - default: '' \ No newline at end of file + default: '' + tutorial_link: http://yandextank.readthedocs.io/en/latest/core_and_modules.html#bfg-worker-type \ No newline at end of file diff --git a/yandextank/plugins/Console/screen.py b/yandextank/plugins/Console/screen.py index 28188843a..ccbf4d99a 100644 --- a/yandextank/plugins/Console/screen.py +++ b/yandextank/plugins/Console/screen.py @@ -212,15 +212,15 @@ def __init__(self, window): self.ticks = '_▁▂▃▄▅▆▇'.decode('utf-8') def recalc_active(self, ts): + if not self.active_seconds: + self.active_seconds.append(ts) + self.data[ts] = {} if ts not in self.active_seconds: - if self.active_seconds: + if ts > max(self.active_seconds): for i in range(max(self.active_seconds) + 1, ts + 1): self.active_seconds.append(i) self.active_seconds.sort() self.data[i] = {} - else: - self.active_seconds.append(ts) - self.data[ts] = {} while len(self.active_seconds) > self.window: self.active_seconds.pop(0) for sec in self.data.keys(): @@ -241,6 +241,9 @@ def get_key_data(self, key): def add(self, ts, key, value, color=''): if ts not in self.data: self.recalc_active(ts) + if ts < min(self.active_seconds): + self.log.warning('Sparkline got outdated second %s, oldest in list %s', ts, min(self.active_seconds)) + return value = max(value, 0) self.data[ts][key] = (color, value) diff --git a/yandextank/plugins/DataUploader/cli.py b/yandextank/plugins/DataUploader/cli.py index 0092acf96..cef7635d0 100644 --- a/yandextank/plugins/DataUploader/cli.py +++ b/yandextank/plugins/DataUploader/cli.py @@ -115,8 +115,12 @@ def get_plugin_dir(shooting_dir): def make_symlink(shooting_dir, name): plugin_dir = get_plugin_dir(shooting_dir) link_name = os.path.join(plugin_dir, str(name)) - os.symlink(os.path.relpath(shooting_dir, plugin_dir), link_name) - logger.info('Symlink created: {}'.format(os.path.abspath(link_name))) + try: + os.symlink(os.path.relpath(shooting_dir, plugin_dir), link_name) + except OSError: + logger.warning('Unable to create symlink for artifact: %s', link_name) + else: + logger.info('Symlink created: {}'.format(os.path.abspath(link_name))) class ConfigError(Exception): diff --git a/yandextank/plugins/DataUploader/client.py b/yandextank/plugins/DataUploader/client.py index 0806db96e..a4abf80f9 100644 --- a/yandextank/plugins/DataUploader/client.py +++ b/yandextank/plugins/DataUploader/client.py @@ -149,8 +149,7 @@ def format_request_info(self, request, request_id): 'headers': str(self.filter_headers(request.headers)), 'body': request.body.replace('\n', '\\n') if isinstance(request.body, str) else request.body } - return """ - Request: {}""".format(json.dumps(request_info)) + return """Request: {}""".format(json.dumps(request_info)) def format_response_info(self, resp, request_id): response_info = { @@ -161,8 +160,7 @@ def format_response_info(self, resp, request_id): 'headers': str(self.filter_headers(resp.headers)), 'content': resp.content.replace('\n', '\\n') if isinstance(resp.content, str) else resp.content } - return """ - Response: {}""".format(json.dumps(response_info)) + return """Response: {}""".format(json.dumps(response_info)) def __make_api_request( self, diff --git a/yandextank/plugins/DataUploader/config/schema.yaml b/yandextank/plugins/DataUploader/config/schema.yaml index e4fe4ba86..5d7528b03 100644 --- a/yandextank/plugins/DataUploader/config/schema.yaml +++ b/yandextank/plugins/DataUploader/config/schema.yaml @@ -1,36 +1,48 @@ meta: type: dict + description: additional meta information api_address: + description: api base address default: https://overload.yandex.net/ type: string api_attempts: + description: number of retries in case of api fault default: 60 type: integer api_timeout: + description: delay between retries in case of api fault default: 10 type: integer chunk_size: + description: max amount of data to be sent in single requests default: 500000 type: integer component: + description: component of your software type: string default: "" connection_timeout: + description: tcp connection timeout default: 30 type: integer ignore_target_lock: + description: start test even if target is locked. default: false type: boolean job_dsc: + description: job description type: string default: "" job_name: + description: job name default: none type: string jobno: + description: number of an existing job. Use to upload data to an existing job. Requres upload token. type: string dependencies: upload_token jobno_file: + description: file to save job number to type: string default: jobno_file.txt lock_targets: @@ -45,64 +57,85 @@ lock_targets: allowed: [auto] tutorial_link: http://yandextank.readthedocs.io log_data_requests: + description: log POSTs of test data for debugging. Tank should be launched in debug mode (--debug) default: false type: boolean log_monitoring_requests: + description: log POSTs of monitoring data for debugging. Tank should be launched in debug mode (--debug) default: false type: boolean log_other_requests: + description: log other api requests for debugging. Tank should be launched in debug mode (--debug) default: false type: boolean log_status_requests: + description: log status api requests for debugging. Tank should be launched in debug mode (--debug) default: false type: boolean maintenance_attempts: + description: number of retries in case of api maintanance downtime default: 10 type: integer maintenance_timeout: + description: delay between retries in case of api maintanance downtime default: 60 type: integer network_attempts: + description: number of retries in case of network fault default: 60 type: integer network_timeout: + description: delay between retries in case of network fault default: 10 type: integer notify: - type: string - default: "" + description: users to notify + type: list + default: [] + schema: + type: string operator: + description: user who started the test type: string nullable: true default: null regress: + description: mark test as regression default: false type: boolean send_status_period: + description: delay between status notifications default: 10 type: integer strict_lock: + description: set true to abort the test if the the target's lock check is failed default: false type: boolean target_lock_duration: + description: how long should the target be locked. In most cases this should be long enough for the test to run. Target will be unlocked automatically right after the test is finished. default: 30m type: string task: + description: task title type: string default: "" threads_timeout: default: 60 type: integer token_file: + description: API token type: string upload_token: + description: Job's token. Use to upload data to an existing job. Requres jobno. type: string nullable: true default: null dependencies: jobno ver: + description: version of the software tested type: string default: "" writer_endpoint: + description: writer api endpoint type: string default: "" diff --git a/yandextank/plugins/DataUploader/plugin.py b/yandextank/plugins/DataUploader/plugin.py index b6f2fe246..ebbd81c03 100644 --- a/yandextank/plugins/DataUploader/plugin.py +++ b/yandextank/plugins/DataUploader/plugin.py @@ -494,13 +494,17 @@ def make_symlink(self, name): PLUGIN_DIR = os.path.join(self.core.artifacts_base_dir, 'lunapark') if not os.path.exists(PLUGIN_DIR): os.makedirs(PLUGIN_DIR) - os.symlink( - os.path.relpath( - self.core.artifacts_dir, - PLUGIN_DIR), - os.path.join( - PLUGIN_DIR, - str(name))) + try: + os.symlink( + os.path.relpath( + self.core.artifacts_dir, + PLUGIN_DIR), + os.path.join( + PLUGIN_DIR, + str(name))) + # this exception catch for filesystems w/o symlinks + except OSError: + logger.warning('Unable to create symlink for artifact: %s', name) def _get_user_agent(self): plugin_agent = 'Uploader/{}'.format(self.VERSION) @@ -576,7 +580,7 @@ def __get_lp_job(self): name=self.get_option('job_name', 'none').decode('utf8'), description=self.get_option('job_dsc').decode('utf8'), tank=self.core.job.tank, - notify_list=self.get_option("notify", '').split(' '), + notify_list=self.get_option("notify"), load_scheme=loadscheme, version=self.get_option('ver'), log_data_requests=self.get_option('log_data_requests'), diff --git a/yandextank/plugins/Influx/plugin.py b/yandextank/plugins/Influx/plugin.py index 61f012f97..178179332 100644 --- a/yandextank/plugins/Influx/plugin.py +++ b/yandextank/plugins/Influx/plugin.py @@ -5,11 +5,9 @@ import logging import sys import datetime -from uuid import uuid4 +from uuid import uuid4 from builtins import str - - from influxdb import InfluxDBClient from ...common.interfaces import AbstractPlugin, \ diff --git a/yandextank/plugins/JMeter/config/schema.yaml b/yandextank/plugins/JMeter/config/schema.yaml index 066b430a8..8212b5f9f 100644 --- a/yandextank/plugins/JMeter/config/schema.yaml +++ b/yandextank/plugins/JMeter/config/schema.yaml @@ -1,11 +1,14 @@ args: + description: additional commandline arguments for JMeter. type: string default: '' buffer_size: + description: jmeter buffer size type: integer nullable: true default: null buffered_seconds: + description: Aggregator delay - to be sure that everything were read from jmeter results file. type: integer default: 3 exclude_markers: @@ -15,6 +18,7 @@ exclude_markers: empty: false default: [] ext_log: + description: additional log, jmeter xml format. Saved in test dir as jmeter_ext_XXXX.jtl type: string allowed: - none @@ -22,6 +26,7 @@ ext_log: - all default: none extended_log: + description: additional log, jmeter xml format. Saved in test dir as jmeter_ext_XXXX.jtl type: string allowed: - none @@ -29,16 +34,21 @@ extended_log: - all default: none jmeter_path: + description: Path to JMeter type: string default: jmeter jmeter_ver: + description: Which JMeter version tank should expect. Affects the way connection time is logged. type: float default: 3.0 jmx: + description: Testplan for execution. type: string shutdown_timeout: + description: timeout for automatic test shutdown type: integer default: 10 variables: + description: variables for jmx testplan type: dict default: {} diff --git a/yandextank/plugins/JsonReport/config/schema.yaml b/yandextank/plugins/JsonReport/config/schema.yaml index 12e019ab1..ba3fe27b0 100644 --- a/yandextank/plugins/JsonReport/config/schema.yaml +++ b/yandextank/plugins/JsonReport/config/schema.yaml @@ -1,6 +1,8 @@ monitoring_log: + description: file name for monitoring log type: string default: monitoring.log test_data_log: + description: file name for test data log type: string default: test_data.log \ No newline at end of file diff --git a/yandextank/plugins/Maven/__init__.py b/yandextank/plugins/Maven/__init__.py deleted file mode 100644 index 727937bd0..000000000 --- a/yandextank/plugins/Maven/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from plugin import Plugin # noqa:F401 diff --git a/yandextank/plugins/Maven/console.py b/yandextank/plugins/Maven/console.py deleted file mode 100644 index 1785e2dcc..000000000 --- a/yandextank/plugins/Maven/console.py +++ /dev/null @@ -1,43 +0,0 @@ -from __future__ import division, absolute_import - -import datetime -import time - -from builtins import super -from ...common.interfaces import AbstractInfoWidget - -from ..Console import screen as ConsoleScreen - - -class MavenInfoWidget(AbstractInfoWidget): - ''' Right panel widget ''' - - def __init__(self, owner): - # FIXME python version 2.7 does not support this syntax. super() should - # have arguments in Python 2 - super().__init__() - self.krutilka = ConsoleScreen.krutilka() - self.owner = owner - - def get_index(self): - return 0 - - def on_aggregated_data(self, data, stats): - pass - - def render(self, screen): - text = " Maven Test %s" % next(self.krutilka) - space = screen.right_panel_width - len(text) - 1 - left_spaces = space // 2 - right_spaces = space // 2 - - dur_seconds = int(time.time()) - int(self.owner.process_start_time) - duration = str(datetime.timedelta(seconds=dur_seconds)) - - template = screen.markup.BG_BROWN + '~' * left_spaces + \ - text + ' ' + '~' * right_spaces + screen.markup.RESET + "\n" - template += "Command Line: %s\n" - template += " Duration: %s" - data = (self.owner.maven_cmd, duration) - - return template % data diff --git a/yandextank/plugins/Maven/plugin.py b/yandextank/plugins/Maven/plugin.py deleted file mode 100644 index 3033fd0c3..000000000 --- a/yandextank/plugins/Maven/plugin.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -Run maven test as load test -""" -from __future__ import division, absolute_import - -import logging -import subprocess -import time - -from builtins import super -from ...common.resource import manager as resource_manager -from ...common.interfaces import AbstractPlugin, GeneratorPlugin - -from .console import MavenInfoWidget -from .reader import MavenReader, MavenStatsReader -from ..Console import Plugin as ConsolePlugin - -logger = logging.getLogger(__name__) - - -class Plugin(AbstractPlugin, GeneratorPlugin): - - SECTION = "maven" - - def __init__(self, core, cfg, cfg_updater): - # FIXME python version 2.7 does not support this syntax. super() should - # have arguments in Python 2 - super(Plugin, self).__init__(core, cfg, cfg_updater) - self.maven_cmd = "mvn" - self.process = None - self.process_stderr = None - self.process_start_time = None - - @staticmethod - def get_key(): - return __file__ - - def get_available_options(self): - opts = ["pom", "testcase", "mvn_args"] - return opts - - def configure(self): - # plugin part - self.pom = resource_manager.resource_filename( - self.get_option("pom", "pom.xml")) - self.testcase = self.get_option("testcase", "") - self.maven_args = self.get_option("mvn_args", '').split() - - def prepare_test(self): - aggregator = self.core.job.aggregator_plugin - - if aggregator: - aggregator.reader = MavenReader() - aggregator.stats_reader = MavenStatsReader() - - try: - console = self.core.get_plugin_of_type(ConsolePlugin) - except KeyError as ex: - logger.debug("Console not found: %s", ex) - console = None - - if console: - widget = MavenInfoWidget(self) - console.add_info_widget(widget) - if aggregator: - aggregator.add_result_listener(widget) - - def start_test(self): - args = [self.maven_cmd, "test", "-Dtest=%s" % self.testcase - ] + self.maven_args + ["-f", self.pom] - logger.info("Starting: %s", args) - self.process_start_time = time.time() - process_stderr_file = self.core.mkstemp(".log", "maven_") - self.core.add_artifact_file(process_stderr_file) - self.process_stderr = open(process_stderr_file, 'w') - self.process = subprocess.Popen( - args, - stderr=self.process_stderr, - stdout=self.process_stderr, - close_fds=True) - - def is_test_finished(self): - retcode = self.process.poll() - if retcode is not None: - logger.info("Subprocess done its work with exit code: %s", retcode) - return abs(retcode) - else: - return -1 - - def end_test(self, retcode): - if self.process and self.process.poll() is None: - logger.warn( - "Terminating worker process with PID %s", self.process.pid) - self.process.terminate() - if self.process_stderr: - self.process_stderr.close() - else: - logger.debug("Seems subprocess finished OK") - return retcode diff --git a/yandextank/plugins/Maven/reader.py b/yandextank/plugins/Maven/reader.py deleted file mode 100644 index 3ad1e2a5d..000000000 --- a/yandextank/plugins/Maven/reader.py +++ /dev/null @@ -1,14 +0,0 @@ -class MavenReader(object): - def close(self): - pass - - def __iter__(self): - yield None - - -class MavenStatsReader(object): - def close(self): - pass - - def __iter__(self): - yield None diff --git a/yandextank/plugins/Phantom/config/schema.py b/yandextank/plugins/Phantom/config/schema.py index bd255e61a..20e54b8ce 100644 --- a/yandextank/plugins/Phantom/config/schema.py +++ b/yandextank/plugins/Phantom/config/schema.py @@ -210,6 +210,7 @@ 'description': 'Configure your load setting the number of RPS or instances (clients) as a function of time,' 'or using a prearranged schedule', "type": "dict", + 'tutorial_link': 'http://yandextank.readthedocs.io/en/latest/tutorial.html#tutorials', 'schema': { 'load_type': { 'required': True, @@ -229,7 +230,6 @@ 'line(100,200,10m)': 'linear growth from 100 to 200 instances/rps during 10 minutes', 'const(200,90s)': 'constant load of 200 instances/rps during 90s', 'test_dir/test_backend.stpd': 'path to ready schedule file'}, - 'tutorial_link': 'http://yandextank.readthedocs.io/en/latest/tutorial.html#tutorials' } }, 'required': True diff --git a/yandextank/plugins/Phantom/reader.py b/yandextank/plugins/Phantom/reader.py index 1d37937d4..2cdc7ce94 100644 --- a/yandextank/plugins/Phantom/reader.py +++ b/yandextank/plugins/Phantom/reader.py @@ -1,6 +1,8 @@ """ Phantom phout format reader. Read chunks from phout and produce data frames """ +from _csv import QUOTE_NONE + import pandas as pd import numpy as np import logging @@ -8,6 +10,11 @@ import time import datetime import itertools as itt + +from pandas.parser import CParserError + +from yandextank.common.interfaces import StatsReader + try: from StringIO import StringIO except ImportError: @@ -39,8 +46,12 @@ def string_to_df(data): start_time = time.time() - chunk = pd.read_csv( - StringIO(data), sep='\t', names=phout_columns, dtype=dtypes) + try: + chunk = pd.read_csv(StringIO(data), sep='\t', names=phout_columns, dtype=dtypes, quoting=QUOTE_NONE) + except CParserError as e: + logger.error(e.message) + logger.error('Incorrect phout data: {}'.format(data)) + return chunk['receive_ts'] = chunk.send_ts + chunk.interval_real / 1e6 chunk['receive_sec'] = chunk.receive_ts.astype(np.int64) @@ -91,7 +102,7 @@ def close(self): self.closed = True -class PhantomStatsReader(object): +class PhantomStatsReader(StatsReader): def __init__(self, filename, phantom_info, cache_size=1024 * 1024 * 50): self.phantom_info = phantom_info self.stat_buffer = "" @@ -120,13 +131,7 @@ def _decode_stat_data(self, chunk): reqps = 0 if offset >= 0 and offset < len(self.phantom_info.steps): reqps = self.phantom_info.steps[offset][0] - yield { - 'ts': chunk_date - 1, - 'metrics': { - 'instances': instances, - 'reqps': reqps - } - } + yield self.stats_item(chunk_date - 1, instances, reqps) def _read_stat_data(self, stat_file): chunk = stat_file.read(self.cache_size) diff --git a/yandextank/plugins/ShellExec/config/schema.yaml b/yandextank/plugins/ShellExec/config/schema.yaml index 4b5d675e8..7f355620a 100644 --- a/yandextank/plugins/ShellExec/config/schema.yaml +++ b/yandextank/plugins/ShellExec/config/schema.yaml @@ -1,18 +1,24 @@ catch_out: + description: show commands stdout type: boolean default: False prepare: + description: shell command to execute on prepare stage type: string default: '' start: + description: shell command to execute on start type: string default: '' end: + description: shell command to execute after test end type: string default: '' poll: + description: shell command to execute every second while test is running type: string default: '' post_process: + description: shell command to execute on post process stage type: string default: '' \ No newline at end of file diff --git a/yandextank/plugins/ShootExec/config/schema.yaml b/yandextank/plugins/ShootExec/config/schema.yaml index 3e63c4ffd..0a590d435 100644 --- a/yandextank/plugins/ShootExec/config/schema.yaml +++ b/yandextank/plugins/ShootExec/config/schema.yaml @@ -1,9 +1,13 @@ cmd: + description: command that produces test results and stats in Phantom format type: string required: true output_path: + description: path to test results type: string required: true stats_path: + description: path to tests stats type: string - default: '' + default: null + nullable: true diff --git a/yandextank/plugins/ShootExec/plugin.py b/yandextank/plugins/ShootExec/plugin.py index a68fa424b..9c863990b 100644 --- a/yandextank/plugins/ShootExec/plugin.py +++ b/yandextank/plugins/ShootExec/plugin.py @@ -5,7 +5,8 @@ import subprocess import time -from ...common.interfaces import AbstractPlugin, GeneratorPlugin, AggregateResultListener, AbstractInfoWidget +from ...common.interfaces import AbstractPlugin, GeneratorPlugin, AggregateResultListener, AbstractInfoWidget, \ + StatsReader from ...common.util import FileScanner from ..Console import Plugin as ConsolePlugin from ..Phantom import PhantomReader @@ -195,7 +196,7 @@ def on_aggregated_data(self, data, stats): pass -class _FileStatsReader(FileScanner): +class _FileStatsReader(FileScanner, StatsReader): """ Read shooting stats line by line @@ -217,17 +218,11 @@ def _read_data(self, lines): curr_ts = int(float(timestamp)) # We allow floats here, but tank expects only seconds if self.__last_ts < curr_ts: self.__last_ts = curr_ts - results.append({ - 'ts': self.__last_ts, - 'metrics': { - 'reqps': float(rps), - 'instances': float(instances), - }, - }) + results.append(self.stats_item(self.__last_ts, float(rps), float(instances))) return results -class _DummyStatsReader(object): +class _DummyStatsReader(StatsReader): """ Dummy stats reader for shooters without stats file """ @@ -240,13 +235,7 @@ def __iter__(self): while not self.__closed: cur_ts = int(time.time()) if cur_ts > self.__last_ts: - yield [{ - 'ts': cur_ts, - 'metrics': { - 'instances': 0, - 'reqps': 0, - }, - }] + yield [self.stats_item(cur_ts, 0, 0)] self.__last_ts = cur_ts else: yield [] diff --git a/yandextank/plugins/Telegraf/config/schema.yaml b/yandextank/plugins/Telegraf/config/schema.yaml index fd724ed0c..297ee6c7e 100644 --- a/yandextank/plugins/Telegraf/config/schema.yaml +++ b/yandextank/plugins/Telegraf/config/schema.yaml @@ -1,17 +1,30 @@ config: type: string default: auto + description: Path to monitoring config file. + values_description: + auto: collect default metrics from default_target host + : path to telegraf configuration file + none: disable monitoring config_contents: type: string + description: used to repeat tests from Overload, not for manual editing default_target: type: string default: localhost + description: 'host to collect default metrics from (if "config: auto" specified)' disguise_hostnames: type: boolean default: true + description: Disguise real host names - use this if you upload results to Overload and dont want others to see your hostnames ssh_timeout: type: string default: 5s + description: timeout of ssh connection to target(s) + examples: + 10s: 10 seconds + 2m: 2 minutes kill_old: type: boolean - default: false \ No newline at end of file + default: false + description: kill old hanging agents on target(s) \ No newline at end of file diff --git a/yandextank/plugins/TipsAndTricks/__init__.py b/yandextank/plugins/TipsAndTricks/__init__.py deleted file mode 100644 index 3b1af47bd..000000000 --- a/yandextank/plugins/TipsAndTricks/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .plugin import * # noqa:F401,F403 diff --git a/yandextank/plugins/TipsAndTricks/config/schema.yaml b/yandextank/plugins/TipsAndTricks/config/schema.yaml deleted file mode 100644 index 13b67c0f2..000000000 --- a/yandextank/plugins/TipsAndTricks/config/schema.yaml +++ /dev/null @@ -1,3 +0,0 @@ -disable: - type: boolean - default: false \ No newline at end of file diff --git a/yandextank/plugins/TipsAndTricks/config/tips.txt b/yandextank/plugins/TipsAndTricks/config/tips.txt deleted file mode 100644 index ff8324d5b..000000000 --- a/yandextank/plugins/TipsAndTricks/config/tips.txt +++ /dev/null @@ -1,37 +0,0 @@ -tank: Use '#! /usr/bin/tank -c' header in configs and run 'chmod +x' on them to have self-starting configs -tank: You can override any config option with command-line --option switch, like this: -o "tank.artifacts_base_dir=/tmp" -tank: You can make console logger more or less verbose with -v and -q command-line switches -tank: Lock files with will fail test with -f command-line switch, or they can be ignored with -i -tank: Place your favorite config options in ~/.yandex-tank file and they will be applied for every tool run -tank: Press Ctrl+C to initiate test shutdown, press it again to interrupt shutdown itself (may cause problems) -tank: Use tank.artifacts_base_dir=~/yandex-tank-artifacts option to have all test logs saved under that directory, this helps cleaning old and unnecessary data -tank: You can specify durations and timeouts in mixed units like 1d5h46m or 13s187ms -tank: Options from 'DEFAULT' section in INI files are inherited by all other sections. Use it wisely to shorten your configs. -tank: Visit our wiki to learn all Yandex.Tank capabilities: http://github.com/yandex-load/yandex-tank/wiki -ab: Apache Benchmark module for Yandex.Tank kinda creepy, but it exists! -ab: Remember that Apache Benchmark results available only when test is complete -autostop: An 'instances' criterion, used with phantom module is the most efficient way to tell if the service is already overloaded -autostop: You can easily write your standalone autostop criterion class and plug it into Autostop module, consult with developers for details -console: Disable full screen console with console.short_only=1 option -console: You can change relative width of right panel in full screen console with console.info_panel_width=50 option (value is percents of width) -jmeter: Sent bytes info not available in JMeter, so it is zero in JMeter module for Yandex.Tank -jmeter: There is only two states for JMeter results - successful or not, so NET codes are either 0 or 1 -jmeter: JMeter provides only full response time and latency values, and 'full minus latency' value is put into 'receive time' inside Yandex.Tank -jmeter: Path to alternate JMeter tool location can be set with jmeter.jmeter_path option -jmeter: If you need supplementary JMeter command-line start switches - add them with jmeter.args option -loadosophia: Yandex.Tank has integration features with BlazeMeter Sense - service for storing and analysing load test results -monitoring: Use monitoring.config=none option to disable monitoring -monitoring: Use monitoring.default_target=
option to start default monitoring for desired host -phantom: Use phantom.stpd_file option to set pre-generated source data file and avoid generating (stepping) process -phantom: Use phantom.config option to set predefined phantom run config (e.g. to work with multiple targets) -phantom: Use phantom.writelog=proto_warning to record only non-successful requests -phantom: Disable stepping cache with phantom.use_caching=0 option -phantom: Force stepping with phantom.force_stepping=1 option -phantom: Set phantom.cache_dir=~/yandex-tank-artifacts option to get most of stepping cache, this also helps cleaning old stepped files -phantom: Green progressbar means progress measured by schedule, light-blue means progress measured by source data -phantom: Use phantom.phout_file option for pre-existing phantom results file import -shellexec: Avoid commands that run too long (a second and more) in shellexec.poll hook -shellexec: Use shell command hooks to run test set-up and tear-down actions, like ssh calls to target or test data generation. See module help for details. -tips: Disable these tips with tips.disable=1 option -web: Yandex.Tank can start local web server and display fancy graphs in your web browser during the test, by default the address is http://localhost:8080/ -web: Change interval for web online graphs with web.interval option diff --git a/yandextank/plugins/TipsAndTricks/plugin.py b/yandextank/plugins/TipsAndTricks/plugin.py deleted file mode 100644 index 30563dd72..000000000 --- a/yandextank/plugins/TipsAndTricks/plugin.py +++ /dev/null @@ -1,67 +0,0 @@ -''' -Plugin showing tool learning hints in console -''' -import random -import textwrap - -from pkg_resources import resource_stream -from ...common.interfaces import AbstractInfoWidget, AbstractPlugin - -from ..Console import Plugin as ConsolePlugin - - -class Plugin(AbstractPlugin, AbstractInfoWidget): - ''' - Tips showing plugin - ''' - SECTION = 'tips' - - def __init__(self, core, cfg, cfg_updater): - AbstractPlugin.__init__(self, core, cfg, cfg_updater) - AbstractInfoWidget.__init__(self) - self.lines = [ - l.decode('utf-8') - for l in resource_stream(__name__, "config/tips.txt").readlines() - ] - self.disable = 0 - - line = random.choice(self.lines) - self.section, self.tip = [_.strip() for _ in line.split(':', 1)] - self.probability = 0.0 - - @staticmethod - def get_key(): - return __file__ - - def get_available_options(self): - return ["disable"] - - def configure(self): - self.disable = self.get_option('disable') - - def prepare_test(self): - if not self.disable: - try: - console = self.core.get_plugin_of_type(ConsolePlugin) - except KeyError as ex: - self.log.debug("Console not found: %s", ex) - console = None - - if console: - console.add_info_widget(self) - - def get_index(self): - return 10000 # really last index - - def render(self, screen): - if random.random() < self.probability: - self.probability = 0.0 - line = random.choice(self.lines) - self.section = line[:line.index(':')] - self.tip = line[line.index(':') + 1:].strip() - self.probability += 1e-3 - line = screen.markup.WHITE + "Tips & Tricks => " + \ - self.section + screen.markup.RESET + ":\n " - line += "\n ".join( - textwrap.wrap(self.tip, screen.right_panel_width - 2)) - return line diff --git a/yandextank/validator/docs_gen.py b/yandextank/validator/docs_gen.py index bc928d98c..78a9def52 100644 --- a/yandextank/validator/docs_gen.py +++ b/yandextank/validator/docs_gen.py @@ -347,10 +347,15 @@ def list_formatter(self, renderer, header=True): ' ' + '({} of {})'.format(self.option_kwargs.get(TYPE, LIST), schema.get(TYPE, ''))) dsc = self.format_dsc(renderer) - schema_block = renderer.field_list({'[list_element] ({})'.format(schema.get(TYPE, '')): - get_formatter({'list_element': schema})(renderer, header=False)}) body = render_body(renderer, self.option_kwargs, [TYPE, DEFAULT, REQUIRED, DESCRIPTION, SCHEMA]) - return '\n'.join([_ for _ in [hdr, dsc, schema_block, body] if _]) + if set(schema.keys()) - {TYPE}: + schema_block = renderer.field_list({ + '[list_element] ({})'.format(schema.get(TYPE, '')): + get_formatter({'list_element': schema})(renderer, header=False) + }) + return '\n'.join([_ for _ in [hdr, dsc, schema_block, body] if _]) + else: + return '\n'.join([_ for _ in [hdr, dsc, body] if _]) def __guess_formatter(self): if ANYOF in self.option_kwargs: