Add InvalidAttributeError and MissingAttributeError.

The point of this patch is to begin promoting a more consistent approach to
producing configuration error messages.

Change-Id: I5cb79c05d791091694731f2034d9205f7eeb76b4
This commit is contained in:
Wayne 2015-03-22 23:14:15 -07:00 committed by Khai Do
parent e1cc03e606
commit 9fff49a7d2
4 changed files with 157 additions and 43 deletions

View File

@ -1,9 +1,62 @@
"""Exception classes for jenkins_jobs errors"""
import inspect
def is_sequence(arg):
return (not hasattr(arg, "strip") and
(hasattr(arg, "__getitem__") or
hasattr(arg, "__iter__")))
class JenkinsJobsException(Exception):
pass
class ModuleError(JenkinsJobsException):
def get_module_name(self):
frame = inspect.currentframe()
co_name = frame.f_code.co_name
while frame and co_name != 'run':
if co_name == 'dispatch':
data = frame.f_locals
module_name = "%s.%s" % (data['component_type'], data['name'])
break
frame = frame.f_back
co_name = frame.f_code.co_name
return module_name
class InvalidAttributeError(ModuleError):
def __init__(self, attribute_name, value, valid_values=None):
message = "'{0}' is an invalid value for attribute {1}.{2}".format(
value, self.get_module_name(), attribute_name)
if is_sequence(valid_values):
message += "\nValid values include: {0}".format(
', '.join("'{0}'".format(value)
for value in valid_values))
super(InvalidAttributeError, self).__init__(message)
class MissingAttributeError(ModuleError):
def __init__(self, missing_attribute):
if is_sequence(missing_attribute):
message = "One of {0} must be present in {1}".format(
', '.join("'{0}'".format(value)
for value in missing_attribute),
self.get_module_name())
else:
message = "Missing {0} from an instance of {1}".format(
missing_attribute, self.get_module_name())
super(MissingAttributeError, self).__init__(message)
class YAMLFormatError(JenkinsJobsException):
pass

View File

@ -41,7 +41,9 @@ import xml.etree.ElementTree as XML
import jenkins_jobs.modules.base
from jenkins_jobs.modules import hudson_model
from jenkins_jobs.modules.helpers import config_file_provider_settings
from jenkins_jobs.errors import JenkinsJobsException
from jenkins_jobs.errors import (JenkinsJobsException,
MissingAttributeError,
InvalidAttributeError)
import logging
logger = logging.getLogger(__name__)
@ -152,10 +154,9 @@ def copyartifact(parser, xml_parent, data):
'workspace-latest': 'WorkspaceSelector',
'build-param': 'ParameterizedBuildSelector'}
if select not in selectdict:
raise JenkinsJobsException("which-build entered is not valid must be "
"one of: last-successful, specific-build, "
"last-saved, upstream-build, permalink, "
"workspace-latest, or build-param")
raise InvalidAttributeError('which-build',
select,
selectdict.keys())
permalink = data.get('permalink', 'last')
permalinkdict = {'last': 'lastBuild',
'last-stable': 'lastStableBuild',
@ -164,10 +165,9 @@ def copyartifact(parser, xml_parent, data):
'last-unstable': 'lastUnstableBuild',
'last-unsuccessful': 'lastUnsuccessfulBuild'}
if permalink not in permalinkdict:
raise JenkinsJobsException("permalink entered is not valid must be "
"one of: last, last-stable, "
"last-successful, last-failed, "
"last-unstable, or last-unsuccessful")
raise InvalidAttributeError('permalink',
permalink,
permalinkdict.keys())
selector = XML.SubElement(t, 'selector',
{'class': 'hudson.plugins.copyartifact.' +
selectdict[select]})
@ -446,8 +446,9 @@ def trigger_builds(parser, xml_parent, data):
for factory in project_def['parameter-factories']:
if factory['factory'] not in supported_factories:
raise JenkinsJobsException("factory must be one of %s" %
", ".join(supported_factories))
raise InvalidAttributeError('factory',
factory['factory'],
supported_factories)
if factory['factory'] == 'filebuild':
params = XML.SubElement(
@ -471,9 +472,9 @@ def trigger_builds(parser, xml_parent, data):
noFilesFoundActionValue = str(factory.get(
'no-files-found-action', 'SKIP'))
if noFilesFoundActionValue not in supported_actions:
raise JenkinsJobsException(
"no-files-found-action must be one of %s" %
", ".join(supported_actions))
raise InvalidAttributeError('no-files-found-action',
noFilesFoundActionValue,
supported_actions)
noFilesFoundAction.text = noFilesFoundActionValue
if factory['factory'] == 'counterbuild':
params = XML.SubElement(
@ -493,9 +494,9 @@ def trigger_builds(parser, xml_parent, data):
validationFailValue = str(factory.get(
'validation-fail', 'FAIL'))
if validationFailValue not in supported_actions:
raise JenkinsJobsException(
"validation-fail action must be one of %s" %
", ".join(supported_actions))
raise InvalidAttributeError('validation-fail',
validationFailValue,
supported_actions)
validationFail.text = validationFailValue
if factory['factory'] == 'allnodesforlabel':
params = XML.SubElement(
@ -547,9 +548,9 @@ def trigger_builds(parser, xml_parent, data):
if tvalue.lower() == supported_threshold_values[0]:
continue
if tvalue.upper() not in supported_threshold_values:
raise JenkinsJobsException(
"threshold value must be one of (%s)" %
", ".join(supported_threshold_values))
raise InvalidAttributeError(toptname,
tvalue,
supported_threshold_values)
th = XML.SubElement(block, txmltag)
XML.SubElement(th, 'name').text = hudson_model.THRESHOLDS[
tvalue.upper()]['name']
@ -1512,13 +1513,10 @@ def shining_panda(parser, xml_parent, data):
try:
buildenv = data['build-environment']
except KeyError:
raise JenkinsJobsException("A build-environment is required")
raise MissingAttributeError('build-environment')
if buildenv not in envs:
errorstring = ("build-environment '%s' is invalid. Must be one of %s."
% (buildenv, ', '.join("'{0}'".format(env)
for env in envs)))
raise JenkinsJobsException(errorstring)
raise InvalidAttributeError('build-environment', buildenv, envs)
t = XML.SubElement(xml_parent, '%s%s' %
(pluginelementpart, buildenvdict[buildenv]))
@ -1549,10 +1547,7 @@ def shining_panda(parser, xml_parent, data):
nature = data.get('nature', 'shell')
naturetuple = ('shell', 'xshell', 'python')
if nature not in naturetuple:
errorstring = ("nature '%s' is not valid: must be one of %s."
% (nature, ', '.join("'{0}'".format(naturevalue)
for naturevalue in naturetuple)))
raise JenkinsJobsException(errorstring)
raise InvalidAttributeError('nature', nature, naturetuple)
XML.SubElement(t, 'nature').text = nature
XML.SubElement(t, 'command').text = data.get("command", "")
ignore_exit_code = data.get('ignore-exit-code', False)
@ -1590,15 +1585,13 @@ def managed_script(parser, xml_parent, data):
step = 'WinBatchBuildStep'
script_tag = 'command'
else:
raise JenkinsJobsException("type entered is not valid must be "
"one of: script or batch")
raise InvalidAttributeError('type', step_type, ['script', 'batch'])
ms = XML.SubElement(xml_parent,
'org.jenkinsci.plugins.managedscripts.' + step)
try:
script_id = data['script-id']
except KeyError:
raise JenkinsJobsException("A script-id is required for "
"managed-script")
raise MissingAttributeError('script-id')
XML.SubElement(ms, script_tag).text = script_id
args = XML.SubElement(ms, 'buildStepArgs')
for arg in data.get('args', []):
@ -1673,8 +1666,7 @@ def cmake(parser, xml_parent, data):
try:
source_dir.text = data['source-dir']
except KeyError:
raise JenkinsJobsException("'source-dir' must be set for CMake "
"builder")
raise MissingAttributeError('source-dir')
build_dir = XML.SubElement(cmake, 'buildDir')
build_dir.text = data.get('build-dir', '')
@ -1773,8 +1765,7 @@ def dsl(parser, xml_parent, data):
XML.SubElement(dsl, 'target').text = data.get('target')
XML.SubElement(dsl, 'usingScriptText').text = 'false'
else:
raise JenkinsJobsException("You must specify either script-text or "
"a target")
raise MissingAttributeError(['script-text', 'target'])
XML.SubElement(dsl, 'ignoreExisting').text = str(data.get(
'ignore-existing', False)).lower()
@ -1783,24 +1774,27 @@ def dsl(parser, xml_parent, data):
removedJobAction = data.get('removed-job-action',
supportedJobActions[0])
if removedJobAction not in supportedJobActions:
raise JenkinsJobsException("removed-job-action must be one "
"of %s" % ", ".join(supportedJobActions))
raise InvalidAttributeError('removed-job-action',
removedJobAction,
supportedJobActions)
XML.SubElement(dsl, 'removedJobAction').text = removedJobAction
supportedViewActions = ['IGNORE', 'DELETE']
removedViewAction = data.get('removed-view-action',
supportedViewActions[0])
if removedViewAction not in supportedViewActions:
raise JenkinsJobsException("removed-view-action must be one "
"of %s" % ", ".join(supportedViewActions))
raise InvalidAttributeError('removed-view-action',
removedViewAction,
supportedViewActions)
XML.SubElement(dsl, 'removedViewAction').text = removedViewAction
supportedLookupActions = ['JENKINS_ROOT', 'SEED_JOB']
lookupStrategy = data.get('lookup-strategy',
supportedLookupActions[0])
if lookupStrategy not in supportedLookupActions:
raise JenkinsJobsException("lookup-strategy must be one "
"of %s" % ", ".join(supportedLookupActions))
raise InvalidAttributeError('lookup-strategy',
lookupStrategy,
supportedLookupActions)
XML.SubElement(dsl, 'lookupStrategy').text = lookupStrategy
XML.SubElement(dsl, 'additionalClasspath').text = data.get(

0
tests/errors/__init__.py Normal file
View File

View File

@ -0,0 +1,67 @@
from testtools import ExpectedException, TestCase
from jenkins_jobs import errors
def dispatch(exc, *args):
component_type = "type" # noqa
name = "name"
for value in [component_type, name]:
# prevent pep8 F841 "Unused Variable"
pass
raise exc(*args)
class TestInvalidAttributeError(TestCase):
def test_no_valid_values(self):
# When given no valid values, InvalidAttributeError simply displays a
# message indicating the invalid value, the component type, the
# component name, and the attribute name.
message = "'{0}' is an invalid value for attribute {1}.{2}".format(
"fnord", "type.name", "fubar")
with ExpectedException(errors.InvalidAttributeError, message):
dispatch(errors.InvalidAttributeError, "fubar", "fnord")
def test_with_valid_values(self):
# When given valid values, InvalidAttributeError displays a message
# indicating the invalid value, the component type, the component name,
# and the attribute name; additionally, it lists the valid values for
# the current component type & name.
valid_values = ['herp', 'derp']
message = "'{0}' is an invalid value for attribute {1}.{2}".format(
"fnord", "type.name", "fubar")
message += "\nValid values include: {0}".format(
', '.join("'{0}'".format(value) for value in valid_values))
with ExpectedException(errors.InvalidAttributeError, message):
dispatch(errors.InvalidAttributeError, "fubar", "fnord",
valid_values)
class TestMissingAttributeError(TestCase):
def test_with_single_missing_attribute(self):
# When passed a single missing attribute, display a message indicating
# * the missing attribute
# * which component type and component name is missing it.
missing_attribute = 'herp'
message = "Missing {0} from an instance of {1}".format(
missing_attribute, 'type.name')
with ExpectedException(errors.MissingAttributeError, message):
dispatch(errors.MissingAttributeError, missing_attribute)
def test_with_multiple_missing_attributes(self):
# When passed multiple missing attributes, display a message indicating
# * the missing attributes
# * which component type and component name is missing it.
missing_attribute = ['herp', 'derp']
message = "One of {0} must be present in {1}".format(
', '.join("'{0}'".format(value) for value in missing_attribute),
'type.name')
with ExpectedException(errors.MissingAttributeError, message):
dispatch(errors.MissingAttributeError, missing_attribute)