Skip to content

Commit d20c3ca

Browse files
David MartinDavid Martin
authored andcommitted
First draft for global syntax validation - report all validation errors at once
Signed-off-by: David Martin <david@chaoiq.io>
1 parent 69dd454 commit d20c3ca

File tree

14 files changed

+224
-156
lines changed

14 files changed

+224
-156
lines changed

chaoslib/activity.py

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,67 +36,73 @@ def ensure_activity_is_valid(activity: Activity):
3636
3737
In all failing cases, raises :exc:`InvalidActivity`.
3838
"""
39+
errors = []
3940
if not activity:
40-
raise InvalidActivity("empty activity is no activity")
41+
errors.append(InvalidActivity("empty activity is no activity"))
42+
return errors
4143

4244
# when the activity is just a ref, there is little to validate
4345
ref = activity.get("ref")
4446
if ref is not None:
4547
if not isinstance(ref, str) or ref == '':
46-
raise InvalidActivity(
47-
"reference to activity must be non-empty strings")
48-
return
48+
errors.append(InvalidActivity(
49+
"reference to activity must be non-empty strings"))
50+
return errors
4951

5052
activity_type = activity.get("type")
5153
if not activity_type:
52-
raise InvalidActivity("an activity must have a type")
54+
errors.append(InvalidActivity("an activity must have a type"))
5355

5456
if activity_type not in ("probe", "action"):
55-
raise InvalidActivity(
56-
"'{t}' is not a supported activity type".format(t=activity_type))
57+
errors.append(InvalidActivity(
58+
"'{t}' is not a supported activity type".format(t=activity_type)))
5759

5860
if not activity.get("name"):
59-
raise InvalidActivity("an activity must have a name")
61+
errors.append(InvalidActivity("an activity must have a name"))
6062

6163
provider = activity.get("provider")
6264
if not provider:
63-
raise InvalidActivity("an activity requires a provider")
65+
errors.append(InvalidActivity("an activity requires a provider"))
66+
provider_type = None
67+
else:
68+
provider_type = provider.get("type")
69+
if not provider_type:
70+
errors.append(InvalidActivity("a provider must have a type"))
6471

65-
provider_type = provider.get("type")
66-
if not provider_type:
67-
raise InvalidActivity("a provider must have a type")
68-
69-
if provider_type not in ("python", "process", "http"):
70-
raise InvalidActivity(
71-
"unknown provider type '{type}'".format(type=provider_type))
72-
73-
if not activity.get("name"):
74-
raise InvalidActivity("activity must have a name (cannot be empty)")
72+
if provider_type not in ("python", "process", "http"):
73+
errors.append(InvalidActivity(
74+
"unknown provider type '{type}'".format(type=provider_type)))
7575

7676
timeout = activity.get("timeout")
7777
if timeout is not None:
7878
if not isinstance(timeout, numbers.Number):
79-
raise InvalidActivity("activity timeout must be a number")
79+
errors.append(
80+
InvalidActivity("activity timeout must be a number"))
8081

8182
pauses = activity.get("pauses")
8283
if pauses is not None:
8384
before = pauses.get("before")
8485
if before is not None and not isinstance(before, numbers.Number):
85-
raise InvalidActivity("activity before pause must be a number")
86+
errors.append(
87+
InvalidActivity("activity before pause must be a number"))
8688
after = pauses.get("after")
8789
if after is not None and not isinstance(after, numbers.Number):
88-
raise InvalidActivity("activity after pause must be a number")
90+
errors.append(
91+
InvalidActivity("activity after pause must be a number"))
8992

9093
if "background" in activity:
9194
if not isinstance(activity["background"], bool):
92-
raise InvalidActivity("activity background must be a boolean")
95+
errors.append(
96+
InvalidActivity("activity background must be a boolean"))
9397

9498
if provider_type == "python":
95-
validate_python_activity(activity)
99+
errors.extend(validate_python_activity(activity))
96100
elif provider_type == "process":
97-
validate_process_activity(activity)
101+
errors.extend(validate_process_activity(activity))
98102
elif provider_type == "http":
99-
validate_http_activity(activity)
103+
errors.extend(validate_http_activity(activity))
104+
105+
return errors
100106

101107

102108
def run_activities(experiment: Experiment, configuration: Configuration,

chaoslib/control/__init__.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
from chaoslib.control.python import apply_python_control, cleanup_control, \
99
initialize_control, validate_python_control, import_control
10-
from chaoslib.exceptions import InterruptExecution, InvalidControl
10+
from chaoslib.exceptions import InterruptExecution, InvalidControl, \
11+
ChaosException
1112
from chaoslib.settings import get_loaded_settings
1213
from chaoslib.types import Settings
1314
from chaoslib.types import Activity, Configuration, Control as ControlType, \
@@ -85,12 +86,11 @@ def cleanup_controls(experiment: Experiment):
8586
cleanup_control(control)
8687

8788

88-
def validate_controls(experiment: Experiment):
89+
def validate_controls(experiment: Experiment) -> List[ChaosException]:
8990
"""
9091
Validate that all declared controls respect the specification.
91-
92-
Raises :exc:`chaoslib.exceptions.InvalidControl` when they are not valid.
9392
"""
93+
errors = []
9494
controls = get_controls(experiment)
9595
references = [
9696
c["name"] for c in get_controls(experiment)
@@ -99,26 +99,29 @@ def validate_controls(experiment: Experiment):
9999
for c in controls:
100100
if "ref" in c:
101101
if c["ref"] not in references:
102-
raise InvalidControl(
103-
"Control reference '{}' declaration cannot be found")
102+
errors.append(InvalidControl(
103+
"Control reference '{}' declaration cannot be found"))
104104

105105
if "name" not in c:
106-
raise InvalidControl("A control must have a `name` property")
106+
errors.append(
107+
InvalidControl("A control must have a `name` property"))
107108

108-
name = c["name"]
109+
name = c.get("name", '')
109110
if "provider" not in c:
110-
raise InvalidControl(
111-
"Control '{}' must have a `provider` property".format(name))
111+
errors.append(InvalidControl(
112+
"Control '{}' must have a `provider` property".format(name)))
112113

113114
scope = c.get("scope")
114115
if scope and scope not in ("before", "after"):
115-
raise InvalidControl(
116+
errors.append(InvalidControl(
116117
"Control '{}' scope property must be 'before' or "
117-
"'after' only".format(name))
118+
"'after' only".format(name)))
118119

119120
provider_type = c.get("provider", {}).get("type")
120121
if provider_type == "python":
121-
validate_python_control(c)
122+
errors.extend(validate_python_control(c))
123+
124+
return errors
122125

123126

124127
def initialize_global_controls(experiment: Experiment,

chaoslib/control/python.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from logzero import logger
88

99
from chaoslib import substitute
10-
from chaoslib.exceptions import InvalidActivity
10+
from chaoslib.exceptions import InvalidActivity, ChaosException
1111
from chaoslib.types import Activity, Configuration, Control, Experiment, \
1212
Journal, Run, Secrets, Settings
1313

@@ -83,16 +83,19 @@ def cleanup_control(control: Control):
8383
func()
8484

8585

86-
def validate_python_control(control: Control):
86+
def validate_python_control(control: Control) -> List[ChaosException]:
8787
"""
8888
Verify that a control block matches the specification
8989
"""
90+
errors = []
9091
name = control["name"]
9192
provider = control["provider"]
9293
mod_name = provider.get("module")
9394
if not mod_name:
94-
raise InvalidActivity(
95-
"Control '{}' must have a module path".format(name))
95+
errors.append(InvalidActivity(
96+
"Control '{}' must have a module path".format(name)))
97+
# can not continue any longer - must exit this function
98+
return errors
9699

97100
try:
98101
importlib.import_module(mod_name)

chaoslib/experiment.py

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,54 +48,66 @@ def ensure_experiment_is_valid(experiment: Experiment):
4848
another set of of ̀close` probes to sense the state of the system
4949
post-action.
5050
51-
This function raises :exc:`InvalidExperiment`, :exc:`InvalidProbe` or
52-
:exc:`InvalidAction` depending on where it fails.
51+
This function raises an :exc:`InvalidExperiment` error
52+
if the experiment is not valid.
53+
If multiple validation errors are found, the errors are listed
54+
as part of the exception message
5355
"""
5456
logger.info("Validating the experiment's syntax")
5557

58+
errors = []
59+
5660
if not experiment:
5761
raise InvalidExperiment("an empty experiment is not an experiment")
5862

5963
if not experiment.get("title"):
60-
raise InvalidExperiment("experiment requires a title")
64+
errors.append(InvalidExperiment("experiment requires a title"))
6165

6266
if not experiment.get("description"):
63-
raise InvalidExperiment("experiment requires a description")
67+
errors.append(InvalidExperiment("experiment requires a description"))
6468

6569
tags = experiment.get("tags")
6670
if tags:
6771
if list(filter(lambda t: t == '' or not isinstance(t, str), tags)):
68-
raise InvalidExperiment(
69-
"experiment tags must be a non-empty string")
72+
errors.append(InvalidExperiment(
73+
"experiment tags must be a non-empty string"))
7074

71-
validate_extensions(experiment)
75+
errors.extend(validate_extensions(experiment))
7276

7377
config = load_configuration(experiment.get("configuration", {}))
7478
load_secrets(experiment.get("secrets", {}), config)
7579

76-
ensure_hypothesis_is_valid(experiment)
80+
errors.extend(ensure_hypothesis_is_valid(experiment))
7781

7882
method = experiment.get("method")
7983
if not method:
80-
raise InvalidExperiment("an experiment requires a method with "
81-
"at least one activity")
82-
83-
for activity in method:
84-
ensure_activity_is_valid(activity)
85-
86-
# let's see if a ref is indeed found in the experiment
87-
ref = activity.get("ref")
88-
if ref and not lookup_activity(ref):
89-
raise InvalidActivity("referenced activity '{r}' could not be "
90-
"found in the experiment".format(r=ref))
84+
errors.append(InvalidExperiment("an experiment requires a method with "
85+
"at least one activity"))
86+
else:
87+
for activity in method:
88+
errors.extend(ensure_activity_is_valid(activity))
89+
90+
# let's see if a ref is indeed found in the experiment
91+
ref = activity.get("ref")
92+
if ref and not lookup_activity(ref):
93+
errors.append(
94+
InvalidActivity("referenced activity '{r}' could not be "
95+
"found in the experiment".format(r=ref)))
9196

9297
rollbacks = experiment.get("rollbacks", [])
9398
for activity in rollbacks:
94-
ensure_activity_is_valid(activity)
99+
errors.extend(ensure_activity_is_valid(activity))
95100

96101
warn_about_deprecated_features(experiment)
97102

98-
validate_controls(experiment)
103+
errors.extend(validate_controls(experiment))
104+
105+
if errors:
106+
full_validation_msg = 'Experiment is not valid, ' \
107+
'please fix the following errors:'
108+
for error in errors:
109+
full_validation_msg += '\n- {}'.format(error)
110+
raise InvalidExperiment(full_validation_msg)
99111

100112
logger.info("Experiment looks valid")
101113

chaoslib/extension.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,29 @@
11
# -*- coding: utf-8 -*-
2-
from typing import Optional
2+
from typing import Optional, List
33

4-
from chaoslib.exceptions import InvalidExperiment
4+
from chaoslib.exceptions import InvalidExperiment, ChaosException
55
from chaoslib.types import Experiment, Extension
66

77
__all__ = ["get_extension", "has_extension", "set_extension",
88
"merge_extension", "remove_extension", "validate_extensions"]
99

1010

11-
def validate_extensions(experiment: Experiment):
11+
def validate_extensions(experiment: Experiment) -> List[ChaosException]:
1212
"""
1313
Validate that extensions respect the specification.
1414
"""
1515
extensions = experiment.get("extensions")
1616
if not extensions:
17-
return
17+
return []
1818

19+
errors = []
1920
for ext in extensions:
2021
ext_name = ext.get('name')
2122
if not ext_name or not ext_name.strip():
22-
raise InvalidExperiment("All extensions require a non-empty name")
23+
errors.append(
24+
InvalidExperiment("All extensions require a non-empty name"))
25+
26+
return errors
2327

2428

2529
def get_extension(experiment: Experiment, name: str) -> Optional[Extension]:

chaoslib/hypothesis.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,25 @@ def ensure_hypothesis_is_valid(experiment: Experiment):
3434
"""
3535
hypo = experiment.get("steady-state-hypothesis")
3636
if hypo is None:
37-
return
37+
return []
3838

39+
errors = []
3940
if not hypo.get("title"):
40-
raise InvalidExperiment("hypothesis requires a title")
41+
errors.append(InvalidExperiment("hypothesis requires a title"))
4142

4243
probes = hypo.get("probes")
4344
if probes:
4445
for probe in probes:
45-
ensure_activity_is_valid(probe)
46+
errors.extend(ensure_activity_is_valid(probe))
4647

4748
if "tolerance" not in probe:
48-
raise InvalidActivity(
49-
"hypothesis probe must have a tolerance entry")
49+
errors.append(InvalidActivity(
50+
"hypothesis probe must have a tolerance entry"))
51+
else:
52+
errors.extend(
53+
ensure_hypothesis_tolerance_is_valid(probe["tolerance"]))
5054

51-
ensure_hypothesis_tolerance_is_valid(probe["tolerance"])
55+
return errors
5256

5357

5458
def ensure_hypothesis_tolerance_is_valid(tolerance: Tolerance):
@@ -81,6 +85,9 @@ def ensure_hypothesis_tolerance_is_valid(tolerance: Tolerance):
8185
"hypothesis probe tolerance type '{}' is unsupported".format(
8286
tolerance_type))
8387

88+
# TODO
89+
return []
90+
8491

8592
def check_regex_pattern(tolerance: Tolerance):
8693
"""

0 commit comments

Comments
 (0)