File: test_waiter_config.py

package info (click to toggle)
python-botocore 1.12.103%2Brepack-1
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 41,552 kB
  • sloc: python: 43,119; xml: 15,052; makefile: 131
file content (174 lines) | stat: -rw-r--r-- 7,350 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import jmespath
from jsonschema import Draft4Validator

import botocore.session
from botocore.exceptions import UnknownServiceError
from botocore.utils import ArgumentGenerator


WAITER_SCHEMA = {
    "type": "object",
    "properties": {
        "version": {"type": "number"},
        "waiters": {
            "type": "object",
            "additionalProperties": {
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": ["api"]
                    },
                    "operation": {"type": "string"},
                    "description": {"type": "string"},
                    "delay": {
                        "type": "number",
                        "minimum": 0,
                    },
                    "maxAttempts": {
                        "type": "integer",
                        "minimum": 1
                    },
                    "acceptors": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "state": {
                                    "type": "string",
                                    "enum": ["success", "retry", "failure"]
                                },
                                "matcher": {
                                    "type": "string",
                                    "enum": [
                                        "path", "pathAll", "pathAny",
                                        "status", "error"
                                    ]
                                },
                                "argument": {"type": "string"},
                                "expected": {
                                    "oneOf": [
                                        {"type": "string"},
                                        {"type": "number"},
                                        {"type": "boolean"}
                                    ]
                                }
                            },
                            "required": [
                                "state", "matcher", "expected"
                            ],
                            "additionalProperties": False
                        }
                    }
                },
                "required": ["operation", "delay", "maxAttempts", "acceptors"],
                "additionalProperties": False
            }
        }
    },
    "additionalProperties": False
}


def test_lint_waiter_configs():
    session = botocore.session.get_session()
    validator = Draft4Validator(WAITER_SCHEMA)
    for service_name in session.get_available_services():
        client = session.create_client(service_name, 'us-east-1')
        service_model = client.meta.service_model
        try:
            # We use the loader directly here because we need the entire
            # json document, not just the portions exposed (either
            # internally or externally) by the WaiterModel class.
            loader = session.get_component('data_loader')
            waiter_model = loader.load_service_model(
                service_name, 'waiters-2')
        except UnknownServiceError:
            # The service doesn't have waiters
            continue
        yield _validate_schema, validator, waiter_model
        for waiter_name in client.waiter_names:
            yield _lint_single_waiter, client, waiter_name, service_model


def _lint_single_waiter(client, waiter_name, service_model):
    try:
        waiter = client.get_waiter(waiter_name)
        # The 'acceptors' property is dynamic and will create
        # the acceptor configs when first accessed.  This is still
        # considered a failure to construct the waiter which is
        # why it's in this try/except block.
        # This catches things like:
        # * jmespath expression compiles
        # * matcher has a known value
        acceptors = waiter.config.acceptors
    except Exception as e:
        raise AssertionError("Could not create waiter '%s': %s"
                             % (waiter_name, e))
    operation_name = waiter.config.operation
    # Needs to reference an existing operation name.
    if operation_name not in service_model.operation_names:
        raise AssertionError("Waiter config references unknown "
                             "operation: %s" % operation_name)
    # Needs to have at least one acceptor.
    if not waiter.config.acceptors:
        raise AssertionError("Waiter config must have at least "
                             "one acceptor state: %s" % waiter.name)
    op_model = service_model.operation_model(operation_name)
    for acceptor in acceptors:
        _validate_acceptor(acceptor, op_model, waiter.name)

    if not waiter.name.isalnum():
        raise AssertionError(
            "Waiter name %s is not alphanumeric." % waiter_name
        )


def _validate_schema(validator, waiter_json):
    errors = list(e.message for e in validator.iter_errors(waiter_json))
    if errors:
        raise AssertionError('\n'.join(errors))


def _validate_acceptor(acceptor, op_model, waiter_name):
    if acceptor.matcher.startswith('path'):
        expression = acceptor.argument
        # The JMESPath expression should have the potential to match something
        # in the response shape.
        output_shape = op_model.output_shape
        assert output_shape is not None, (
            "Waiter '%s' has JMESPath expression with no output shape: %s"
            % (waiter_name, op_model))
        # We want to check if the JMESPath expression makes sense.
        # To do this, we'll generate sample output and evaluate the
        # JMESPath expression against the output.  We'll then
        # check a few things about this returned search result.
        search_result = _search_jmespath_expression(expression, op_model)
        if not search_result:
            raise AssertionError("JMESPath expression did not match "
                                 "anything for waiter '%s': %s"
                                 % (waiter_name, expression))
        if acceptor.matcher in ['pathAll', 'pathAny']:
            assert isinstance(search_result, list), \
                    ("Attempted to use '%s' matcher in waiter '%s' "
                     "with non list result in JMESPath expression: %s"
                     % (acceptor.matcher, waiter_name, expression))


def _search_jmespath_expression(expression, op_model):
    arg_gen = ArgumentGenerator(use_member_names=True)
    sample_output = arg_gen.generate_skeleton(op_model.output_shape)
    search_result = jmespath.search(expression, sample_output)
    return search_result