Skip to content

Commit

Permalink
Merge pull request #569 from fomars/develop
Browse files Browse the repository at this point in the history
Load scheme validation
  • Loading branch information
fomars committed Apr 24, 2018
2 parents a8721a6 + af83316 commit 3964c07
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 35 deletions.
1 change: 1 addition & 0 deletions yandextank/plugins/Bfg/config/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ load_profile:
schedule:
type: string
required: true
validator: load_scheme
description: load schedule or path to stpd file
examples:
line(100,200,10m): linear growth from 100 to 200 instances/rps during 10 minutes
Expand Down
1 change: 1 addition & 0 deletions yandextank/plugins/Phantom/config/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@
'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'},
'validator': 'load_scheme'
}
},
'required': True
Expand Down
70 changes: 68 additions & 2 deletions yandextank/validator/tests/test_validator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import pytest

from yandextank.validator.validator import TankConfig, ValidationError
from yandextank.validator.validator import TankConfig, ValidationError, PatchedValidator

CFG_VER_I_0 = {
"version": "1.9.3",
Expand Down Expand Up @@ -488,7 +488,44 @@ def test_validate_all(config, expected):
},
{'phantom': {'address': ['required field'], 'load_profile': ['required field']},
'telegraf': {'config': ['must be of string type']}})
'telegraf': {'config': ['must be of string type']}}),
(
{
"core": {},
'phantom': {
'package': 'yandextank.plugins.Phantom',
'enabled': True,
'address': 'nodejs.load.yandex.net',
'uris': ['/'],
'load_profile': {'load_type': 'rps', 'schedule': 'line(1, 20, 2, 10m)'}
}
},
{'phantom': {'load_profile': [{'schedule': ['line load scheme: expected 3 arguments, found 4']}]}}),
(
{
"core": {},
'phantom': {
'package': 'yandextank.plugins.Phantom',
'enabled': True,
'address': 'nodejs.load.yandex.net',
'uris': ['/'],
'load_profile': {'load_type': 'rps', 'schedule': 'line(1, 20, 10m5m)'}
}
},
{'phantom': {'load_profile': [{'schedule': ['Load duration examples: 2h30m; 5m15; 180']}]}}),
(
{
"core": {},
'phantom': {
'package': 'yandextank.plugins.Phantom',
'enabled': True,
'address': 'nodejs.load.yandex.net',
'uris': ['/'],
'load_profile': {'load_type': 'rps', 'schedule': 'line(1n,20,100)'}
}
},
{'phantom': {'load_profile': [{'schedule': ['Argument 1n in load scheme should be a number']}]}})
])
def test_validate_all_error(config, expected):
with pytest.raises(ValidationError) as e:
Expand Down Expand Up @@ -553,3 +590,32 @@ def test_setter(config, plugin, key, value):
# },
# "plugins": plugins_conf
# }


@pytest.mark.parametrize('value', [
'step(10,200,5,180)',
'step(5,50,2.5,5m)',
'line(22,154,2h5m)',
'step(5,50,2.5,5m) line(22,154,2h5m)',
'const(10,1h4m3s)',
'const(2.5,150)',
'const(100, 1d2h)',
'line(10, 120, 300s)',
])
def test_load_scheme_validator(value):
validator = PatchedValidator({'load_type': {'type': 'string'}, 'schedule': {'validator': 'load_scheme'}})
cfg = {'load_type': 'rps', 'schedule': value}
assert validator.validate(cfg)


@pytest.mark.parametrize('value', [
'step(10,5,180)',
'step(5,50,2.5,5m,30s)',
'lien(22,154,2h5m)',
'step(5,50,2.5,5m) line(22,154,2h5m) const(10, 20, 3m)',
'const(10,1.5h)',
])
def test_negative_load_scheme_validator(value):
validator = PatchedValidator({'load_type': {'type': 'string'}, 'schedule': {'validator': 'load_scheme'}})
cfg = {'load_type': 'rps', 'schedule': value}
assert not validator.validate(cfg)
123 changes: 90 additions & 33 deletions yandextank/validator/validator.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import imp
import os
import re
import sys
import uuid

import logging
import pkg_resources
import yaml
from cerberus.validator import Validator, InspectedValidator
from cerberus.validator import Validator

from yandextank.common.util import recursive_dict_update
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -68,6 +69,92 @@ def load_schema(directory, filename=None):
directory)


class PatchedValidator(Validator):

def _validate_description(self, description, field, value):
""" {'type': 'string'} """
pass

# monkey-patch cerberus validator to allow values descriptions field
def _validate_values_description(self, values_description, field, value):
""" {'type': 'dict'} """
pass

# monkey-patch cerberus validator to allow tutorial_link field
def _validate_tutorial_link(self, tutorial_link, field, value):
""" {'type': 'string'} """
pass

# monkey-patch cerberus validator to allow examples field
def _validate_examples(self, examples, field, value):
""" {'type': 'dict'} """
pass

@staticmethod
def is_number(value):
try:
float(value)
return True
except ValueError:
return False

def validate_duration(self, field, duration):
'''
2h
2h5m
5m
180
1h4m3
:param duration:
:return:
'''
DURATION_RE = r'^(\d+d)?(\d+h)?(\d+m)?(\d+s?)?$'
if not re.match(DURATION_RE, duration):
self._error(field, 'Load duration examples: 2h30m; 5m15; 180')

def _validator_load_scheme(self, field, value):
'''
step(10,200,5,180)
step(5,50,2.5,5m)
line(22,154,2h5m)
step(5,50,2.5,5m) line(22,154,2h5m)
const(10,1h4m3s)
:param field:
:param value:
:return:
'''
# stpd file can be any value
if self.document['load_type'] in 'stpd_file':
return

PRIMARY_RE = r'(step|line|const)\((.+?)\)'
N_OF_ARGS = {
'step': 4,
'line': 3,
'const': 2,
}
matches = re.findall(PRIMARY_RE, value)
if len(matches) == 0:
self._error(field, 'Should match one of the following patterns: step(...) / line(...) / const(...)')
else:
for match in matches:
curve, params_str = match
params = [v.strip() for v in params_str.split(',')]
# check number of arguments
if not len(params) == N_OF_ARGS[curve]:
self._error(field, '{} load scheme: expected {} arguments, found {}'.format(curve,
N_OF_ARGS[curve],
len(params)))
# check arguments' types
for param in params[:-1]:
if not self.is_number(param):
self._error(field, 'Argument {} in load scheme should be a number'.format(param))
self.validate_duration(field, params[-1])

# def _normalize_coerce_load_scheme(self, value):
# pass


class TankConfig(object):
DYNAMIC_OPTIONS = {
'uuid': lambda: str(uuid.uuid4()),
Expand Down Expand Up @@ -95,7 +182,6 @@ def __init__(
self.ERROR_OUTPUT = error_output
self.BASE_SCHEMA = load_yaml_schema(pkg_resources.resource_filename('yandextank.core', 'config/schema.yaml'))
self.PLUGINS_SCHEMA = load_yaml_schema(pkg_resources.resource_filename('yandextank.core', 'config/plugins_schema.yaml'))
self.PatchedValidator = self.__get_patched_validator()

def get_option(self, section, option):
return self.validated[section][option]
Expand Down Expand Up @@ -142,35 +228,6 @@ def save_raw(self, filename):
with open(filename, 'w') as f:
yaml.dump(self.raw_config_dict, f)

@staticmethod
def __get_patched_validator():
# monkey-patch cerberus validator to allow description field
def _validate_description(self, description, field, value):
""" {'type': 'string'} """
pass

# monkey-patch cerberus validator to allow values descriptions field
def _validate_values_description(self, values_description, field, value):
""" {'type': 'dict'} """
pass

# monkey-patch cerberus validator to allow tutorial_link field
def _validate_tutorial_link(self, tutorial_link, field, value):
""" {'type': 'string'} """
pass

# monkey-patch cerberus validator to allow examples field
def _validate_examples(self, examples, field, value):
""" {'type': 'dict'} """
pass

Validator._validate_description = _validate_description
Validator._validate_values_description = _validate_values_description
Validator._validate_tutorial_link = _validate_tutorial_link
Validator._validate_examples = _validate_examples

return InspectedValidator('Validator', (Validator,), {})

def __load_multiple(self, configs):
configs_count = len(configs)
if configs_count == 0:
Expand Down Expand Up @@ -218,7 +275,7 @@ def __validate(self):
return core_validated

def __validate_core(self):
v = self.PatchedValidator(allow_unknown=self.PLUGINS_SCHEMA)
v = PatchedValidator(allow_unknown=self.PLUGINS_SCHEMA)
result = v.validate(self.raw_config_dict, self.BASE_SCHEMA)
if not result:
errors = v.errors
Expand All @@ -232,7 +289,7 @@ def __validate_core(self):

def __validate_plugin(self, config, schema):
schema.update(self.PLUGINS_SCHEMA['schema'])
v = self.PatchedValidator(schema, allow_unknown=False)
v = PatchedValidator(schema, allow_unknown=False)
# .validate() makes .errors as side effect if there's any
if not v.validate(config):
raise ValidationError(v.errors)
Expand Down

0 comments on commit 3964c07

Please sign in to comment.