blob: 82330ec075804ba0034f75dc7465b0b0cf1d0cf2 [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import copy
import re
# Note: this module is tested by a unit test config_validation_test.py,
# rather than recipe simulation tests.
_BISECT_CONFIG_SCHEMA = {
'command': {'type': 'string', 'required': True},
'good_revision': {'type': 'revision', 'required': True},
'bad_revision': {'type': 'revision', 'required': True},
'bisect_bot': {'type': 'string'},
'metric': {'type': 'string'},
'bug_id': {'type': 'integer'},
'repeat_count': {'type': 'integer'},
'max_time_minutes': {'type': 'integer'},
'bisect_mode': {'type': 'string',
'choices': ['mean', 'return_code', 'std_dev']},
'gs_bucket': {'type': 'string'},
'builder_host': {'type': 'string'},
'builder_port': {'type': 'integer'},
'test_type': {'type': 'string'},
'improvement_direction': {'type': 'integer'},
'recipe_tester_name': {'type': 'string'},
'try_job_id': {'type': 'integer'},
}
def from_properties(bisect_config_dict):
"""Converts bisect config from a dict property value.
Properties do not necessarily distinguish integers and floats, e.g.
0 may turn into 0.0. This function fixes integer values.
Does not validate the config.
"""
bisect_config_dict = copy.deepcopy(bisect_config_dict)
for p, schema in _BISECT_CONFIG_SCHEMA.iteritems():
v = bisect_config_dict.get(p)
if schema['type'] == 'integer' and isinstance(v, float):
bisect_config_dict[p] = int(v)
return bisect_config_dict
class ValidationFail(Exception):
"""An exception class that represents a failure to validate."""
def validate_bisect_config(config, schema=None):
"""Checks the correctness of the given bisect job config."""
schema = _BISECT_CONFIG_SCHEMA if schema is None else schema
for key in set(schema):
validate_key(config, schema, key)
if 'good_revision' in schema and 'bad_revision' in schema:
_validate_revisions(config.get('good_revision'), config.get('bad_revision'))
if 'bisect_mode' in schema and 'metric' in schema:
_validate_metric(config.get('bisect_mode'), config.get('metric'))
def validate_key(config, schema, key): # pragma: no cover
"""Checks the correctness of the given field in a config."""
if schema[key].get('required') and config.get(key) is None:
raise ValidationFail('Required key "%s" missing.' % key)
if config.get(key) is None:
return # Optional field.
value = config[key]
field_type = schema[key].get('type')
if field_type == 'string':
_validate_string(value, key)
elif field_type == 'integer':
_validate_integer(value, key)
elif field_type == 'revision':
_validate_revision(value, key)
elif field_type == 'boolean':
_validate_boolean(value, key)
if 'choices' in schema[key] and value not in schema[key]['choices']:
_fail(value, key)
def _fail(value, key):
raise ValidationFail('Invalid value %r for "%s".' % (value, key))
def _validate_string(value, key): # pragma: no cover
if not isinstance(value, basestring):
_fail(value, key)
def _validate_revision(value, key): # pragma: no cover
s = str(value)
if not (s.isdigit() or re.match('^[0-9A-Fa-f]{40}$', s)):
_fail(value, key)
def _validate_integer(value, key): # pragma: no cover
try:
int(value)
except ValueError:
_fail(value, key)
def _validate_boolean(value, key): # pragma: no cover
if value not in (True, False):
_fail(value, key)
def _validate_revisions(good_revision, bad_revision): # pragma: no cover
try:
earlier = int(good_revision)
later = int(bad_revision)
except ValueError:
return # The revisions could be sha1 hashes.
if earlier >= later:
raise ValidationFail('Order of good_revision (%d) and bad_revision(%d) '
'is reversed.' % (earlier, later))
def _validate_metric(bisect_mode, metric): # pragma: no cover
if bisect_mode not in ('mean', 'std_dev'):
return
if not (isinstance(metric, basestring) and ((metric.count('/') == 1) or
(metric.count('/') == 2))):
raise ValidationFail('Invalid value for "metric": %s' % metric)