google-cloud-testutils 1.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- google_cloud_testutils-1.6.1.dist-info/LICENSE +202 -0
- google_cloud_testutils-1.6.1.dist-info/METADATA +36 -0
- google_cloud_testutils-1.6.1.dist-info/RECORD +17 -0
- google_cloud_testutils-1.6.1.dist-info/WHEEL +5 -0
- google_cloud_testutils-1.6.1.dist-info/entry_points.txt +2 -0
- google_cloud_testutils-1.6.1.dist-info/top_level.txt +1 -0
- test_utils/__init__.py +16 -0
- test_utils/imports.py +38 -0
- test_utils/lower_bound_checker/__init__.py +0 -0
- test_utils/lower_bound_checker/lower_bound_checker.py +271 -0
- test_utils/orchestrate.py +446 -0
- test_utils/prefixer.py +82 -0
- test_utils/py.typed +2 -0
- test_utils/retry.py +228 -0
- test_utils/system.py +80 -0
- test_utils/version.py +15 -0
- test_utils/vpcsc_config.py +118 -0
test_utils/retry.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Copyright 2016 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import functools
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
MAX_TRIES = 4
|
|
19
|
+
DELAY = 1
|
|
20
|
+
BACKOFF = 2
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _retry_all(_):
|
|
24
|
+
"""Retry all caught exceptions."""
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BackoffFailed(Exception):
|
|
29
|
+
"""Retry w/ backoffs did not complete successfully."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RetryBase(object):
|
|
33
|
+
"""Base for retrying calling a decorated function w/ exponential backoff.
|
|
34
|
+
|
|
35
|
+
:type max_tries: int
|
|
36
|
+
:param max_tries: Number of times to try (not retry) before giving up.
|
|
37
|
+
|
|
38
|
+
:type delay: int
|
|
39
|
+
:param delay: Initial delay between retries in seconds.
|
|
40
|
+
|
|
41
|
+
:type backoff: int
|
|
42
|
+
:param backoff: Backoff multiplier e.g. value of 2 will double the
|
|
43
|
+
delay each retry.
|
|
44
|
+
|
|
45
|
+
:type logger: logging.Logger instance
|
|
46
|
+
:param logger: Logger to use. If None, print.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, max_tries=MAX_TRIES, delay=DELAY, backoff=BACKOFF, logger=None):
|
|
50
|
+
self.max_tries = max_tries
|
|
51
|
+
self.delay = delay
|
|
52
|
+
self.backoff = backoff
|
|
53
|
+
self.logger = logger.warning if logger else print
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class RetryErrors(RetryBase):
|
|
57
|
+
"""Decorator for retrying given exceptions in testing.
|
|
58
|
+
|
|
59
|
+
:type exception: Exception or tuple of Exceptions
|
|
60
|
+
:param exception: The exception to check or may be a tuple of
|
|
61
|
+
exceptions to check.
|
|
62
|
+
|
|
63
|
+
:type error_predicate: function, takes caught exception, returns bool
|
|
64
|
+
:param error_predicate: Predicate evaluating whether to retry after a
|
|
65
|
+
caught exception.
|
|
66
|
+
|
|
67
|
+
:type max_tries: int
|
|
68
|
+
:param max_tries: Number of times to try (not retry) before giving up.
|
|
69
|
+
|
|
70
|
+
:type delay: int
|
|
71
|
+
:param delay: Initial delay between retries in seconds.
|
|
72
|
+
|
|
73
|
+
:type backoff: int
|
|
74
|
+
:param backoff: Backoff multiplier e.g. value of 2 will double the
|
|
75
|
+
delay each retry.
|
|
76
|
+
|
|
77
|
+
:type logger: logging.Logger instance
|
|
78
|
+
:param logger: Logger to use. If None, print.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
exception,
|
|
84
|
+
error_predicate=_retry_all,
|
|
85
|
+
max_tries=MAX_TRIES,
|
|
86
|
+
delay=DELAY,
|
|
87
|
+
backoff=BACKOFF,
|
|
88
|
+
logger=None,
|
|
89
|
+
):
|
|
90
|
+
super(RetryErrors, self).__init__(max_tries, delay, backoff, logger)
|
|
91
|
+
self.exception = exception
|
|
92
|
+
self.error_predicate = error_predicate
|
|
93
|
+
|
|
94
|
+
def __call__(self, to_wrap):
|
|
95
|
+
@functools.wraps(to_wrap)
|
|
96
|
+
def wrapped_function(*args, **kwargs):
|
|
97
|
+
tries = 0
|
|
98
|
+
while tries < self.max_tries:
|
|
99
|
+
try:
|
|
100
|
+
return to_wrap(*args, **kwargs)
|
|
101
|
+
except self.exception as caught_exception:
|
|
102
|
+
if not self.error_predicate(caught_exception):
|
|
103
|
+
raise
|
|
104
|
+
|
|
105
|
+
delay = self.delay * self.backoff**tries
|
|
106
|
+
msg = "%s, Trying again in %d seconds..." % (
|
|
107
|
+
caught_exception,
|
|
108
|
+
delay,
|
|
109
|
+
)
|
|
110
|
+
self.logger(msg)
|
|
111
|
+
|
|
112
|
+
time.sleep(delay)
|
|
113
|
+
tries += 1
|
|
114
|
+
return to_wrap(*args, **kwargs)
|
|
115
|
+
|
|
116
|
+
return wrapped_function
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class RetryResult(RetryBase):
|
|
120
|
+
"""Decorator for retrying based on non-error result.
|
|
121
|
+
|
|
122
|
+
:type result_predicate: function, takes result, returns bool
|
|
123
|
+
:param result_predicate: Predicate evaluating whether to retry after a
|
|
124
|
+
result is returned.
|
|
125
|
+
|
|
126
|
+
:type max_tries: int
|
|
127
|
+
:param max_tries: Number of times to try (not retry) before giving up.
|
|
128
|
+
|
|
129
|
+
:type delay: int
|
|
130
|
+
:param delay: Initial delay between retries in seconds.
|
|
131
|
+
|
|
132
|
+
:type backoff: int
|
|
133
|
+
:param backoff: Backoff multiplier e.g. value of 2 will double the
|
|
134
|
+
delay each retry.
|
|
135
|
+
|
|
136
|
+
:type logger: logging.Logger instance
|
|
137
|
+
:param logger: Logger to use. If None, print.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
result_predicate,
|
|
143
|
+
max_tries=MAX_TRIES,
|
|
144
|
+
delay=DELAY,
|
|
145
|
+
backoff=BACKOFF,
|
|
146
|
+
logger=None,
|
|
147
|
+
):
|
|
148
|
+
super(RetryResult, self).__init__(max_tries, delay, backoff, logger)
|
|
149
|
+
self.result_predicate = result_predicate
|
|
150
|
+
|
|
151
|
+
def __call__(self, to_wrap):
|
|
152
|
+
@functools.wraps(to_wrap)
|
|
153
|
+
def wrapped_function(*args, **kwargs):
|
|
154
|
+
tries = 0
|
|
155
|
+
while tries < self.max_tries:
|
|
156
|
+
result = to_wrap(*args, **kwargs)
|
|
157
|
+
if self.result_predicate(result):
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
delay = self.delay * self.backoff**tries
|
|
161
|
+
msg = "%s. Trying again in %d seconds..." % (
|
|
162
|
+
self.result_predicate.__name__,
|
|
163
|
+
delay,
|
|
164
|
+
)
|
|
165
|
+
self.logger(msg)
|
|
166
|
+
|
|
167
|
+
time.sleep(delay)
|
|
168
|
+
tries += 1
|
|
169
|
+
raise BackoffFailed()
|
|
170
|
+
|
|
171
|
+
return wrapped_function
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class RetryInstanceState(RetryBase):
|
|
175
|
+
"""Decorator for retrying based on instance state.
|
|
176
|
+
|
|
177
|
+
:type instance_predicate: function, takes instance, returns bool
|
|
178
|
+
:param instance_predicate: Predicate evaluating whether to retry after an
|
|
179
|
+
API-invoking method is called.
|
|
180
|
+
|
|
181
|
+
:type max_tries: int
|
|
182
|
+
:param max_tries: Number of times to try (not retry) before giving up.
|
|
183
|
+
|
|
184
|
+
:type delay: int
|
|
185
|
+
:param delay: Initial delay between retries in seconds.
|
|
186
|
+
|
|
187
|
+
:type backoff: int
|
|
188
|
+
:param backoff: Backoff multiplier e.g. value of 2 will double the
|
|
189
|
+
delay each retry.
|
|
190
|
+
|
|
191
|
+
:type logger: logging.Logger instance
|
|
192
|
+
:param logger: Logger to use. If None, print.
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
def __init__(
|
|
196
|
+
self,
|
|
197
|
+
instance_predicate,
|
|
198
|
+
max_tries=MAX_TRIES,
|
|
199
|
+
delay=DELAY,
|
|
200
|
+
backoff=BACKOFF,
|
|
201
|
+
logger=None,
|
|
202
|
+
):
|
|
203
|
+
super(RetryInstanceState, self).__init__(max_tries, delay, backoff, logger)
|
|
204
|
+
self.instance_predicate = instance_predicate
|
|
205
|
+
|
|
206
|
+
def __call__(self, to_wrap):
|
|
207
|
+
instance = to_wrap.__self__ # only instance methods allowed
|
|
208
|
+
|
|
209
|
+
@functools.wraps(to_wrap)
|
|
210
|
+
def wrapped_function(*args, **kwargs):
|
|
211
|
+
tries = 0
|
|
212
|
+
while tries < self.max_tries:
|
|
213
|
+
result = to_wrap(*args, **kwargs)
|
|
214
|
+
if self.instance_predicate(instance):
|
|
215
|
+
return result
|
|
216
|
+
|
|
217
|
+
delay = self.delay * self.backoff**tries
|
|
218
|
+
msg = "%s. Trying again in %d seconds..." % (
|
|
219
|
+
self.instance_predicate.__name__,
|
|
220
|
+
delay,
|
|
221
|
+
)
|
|
222
|
+
self.logger(msg)
|
|
223
|
+
|
|
224
|
+
time.sleep(delay)
|
|
225
|
+
tries += 1
|
|
226
|
+
raise BackoffFailed()
|
|
227
|
+
|
|
228
|
+
return wrapped_function
|
test_utils/system.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Copyright 2014 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import print_function
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
import google.auth.credentials # type: ignore
|
|
21
|
+
from google.auth.environment_vars import CREDENTIALS as TEST_CREDENTIALS # type: ignore
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# From shell environ. May be None.
|
|
25
|
+
CREDENTIALS = os.getenv(TEST_CREDENTIALS)
|
|
26
|
+
|
|
27
|
+
ENVIRON_ERROR_MSG = """\
|
|
28
|
+
To run the system tests, you need to set some environment variables.
|
|
29
|
+
Please check the CONTRIBUTING guide for instructions.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EmulatorCreds(google.auth.credentials.Credentials):
|
|
34
|
+
"""A mock credential object.
|
|
35
|
+
|
|
36
|
+
Used to avoid unnecessary token refreshing or reliance on the network
|
|
37
|
+
while an emulator is running.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self): # pylint: disable=super-init-not-called
|
|
41
|
+
self.token = b"seekrit"
|
|
42
|
+
self.expiry = None
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def valid(self):
|
|
46
|
+
"""Would-be validity check of the credentials.
|
|
47
|
+
|
|
48
|
+
Always is :data:`True`.
|
|
49
|
+
"""
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
def refresh(self, unused_request): # pylint: disable=unused-argument
|
|
53
|
+
"""Off-limits implementation for abstract method."""
|
|
54
|
+
raise RuntimeError("Should never be refreshed.")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def check_environ():
|
|
58
|
+
err_msg = None
|
|
59
|
+
if CREDENTIALS is None:
|
|
60
|
+
err_msg = "\nMissing variables: " + TEST_CREDENTIALS
|
|
61
|
+
elif not os.path.isfile(CREDENTIALS):
|
|
62
|
+
err_msg = "\nThe %s path %r is not a file." % (TEST_CREDENTIALS, CREDENTIALS)
|
|
63
|
+
|
|
64
|
+
if err_msg is not None:
|
|
65
|
+
msg = ENVIRON_ERROR_MSG + err_msg
|
|
66
|
+
print(msg, file=sys.stderr)
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def unique_resource_id(delimiter="_"):
|
|
71
|
+
"""A unique identifier for a resource.
|
|
72
|
+
|
|
73
|
+
Intended to help locate resources created in particular
|
|
74
|
+
testing environments and at particular times.
|
|
75
|
+
"""
|
|
76
|
+
build_id = os.getenv("CIRCLE_BUILD_NUM", "")
|
|
77
|
+
if build_id == "":
|
|
78
|
+
return "%s%d" % (delimiter, 1000 * time.time())
|
|
79
|
+
else:
|
|
80
|
+
return "%s%s%s%d" % (delimiter, build_id, delimiter, time.time())
|
test_utils/version.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2025 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
#
|
|
15
|
+
__version__ = "1.6.1"
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2019 Google LLC
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
|
|
19
|
+
import pytest # type: ignore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
INSIDE_VPCSC_ENVVAR = "GOOGLE_CLOUD_TESTS_IN_VPCSC"
|
|
23
|
+
PROJECT_INSIDE_ENVVAR = "PROJECT_ID"
|
|
24
|
+
PROJECT_OUTSIDE_ENVVAR = "GOOGLE_CLOUD_TESTS_VPCSC_OUTSIDE_PERIMETER_PROJECT"
|
|
25
|
+
BUCKET_OUTSIDE_ENVVAR = "GOOGLE_CLOUD_TESTS_VPCSC_OUTSIDE_PERIMETER_BUCKET"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class VPCSCTestConfig(object):
|
|
29
|
+
"""System test utility for VPCSC detection.
|
|
30
|
+
|
|
31
|
+
See: https://cloud.google.com/vpc-service-controls/docs/
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def inside_vpcsc(self):
|
|
36
|
+
"""Test whether the test environment is configured to run inside VPCSC.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
bool:
|
|
40
|
+
true if the environment is configured to run inside VPCSC,
|
|
41
|
+
else false.
|
|
42
|
+
"""
|
|
43
|
+
return INSIDE_VPCSC_ENVVAR in os.environ
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def project_inside(self):
|
|
47
|
+
"""Project ID for testing outside access.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
str: project ID used for testing outside access; None if undefined.
|
|
51
|
+
"""
|
|
52
|
+
return os.environ.get(PROJECT_INSIDE_ENVVAR, None)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def project_outside(self):
|
|
56
|
+
"""Project ID for testing inside access.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
str: project ID used for testing inside access; None if undefined.
|
|
60
|
+
"""
|
|
61
|
+
return os.environ.get(PROJECT_OUTSIDE_ENVVAR, None)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def bucket_outside(self):
|
|
65
|
+
"""GCS bucket for testing inside access.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
str: bucket ID used for testing inside access; None if undefined.
|
|
69
|
+
"""
|
|
70
|
+
return os.environ.get(BUCKET_OUTSIDE_ENVVAR, None)
|
|
71
|
+
|
|
72
|
+
def skip_if_inside_vpcsc(self, testcase):
|
|
73
|
+
"""Test decorator: skip if running inside VPCSC."""
|
|
74
|
+
reason = (
|
|
75
|
+
"Running inside VPCSC. "
|
|
76
|
+
"Unset the {} environment variable to enable this test."
|
|
77
|
+
).format(INSIDE_VPCSC_ENVVAR)
|
|
78
|
+
skip = pytest.mark.skipif(self.inside_vpcsc, reason=reason)
|
|
79
|
+
return skip(testcase)
|
|
80
|
+
|
|
81
|
+
def skip_unless_inside_vpcsc(self, testcase):
|
|
82
|
+
"""Test decorator: skip if running outside VPCSC."""
|
|
83
|
+
reason = (
|
|
84
|
+
"Running outside VPCSC. "
|
|
85
|
+
"Set the {} environment variable to enable this test."
|
|
86
|
+
).format(INSIDE_VPCSC_ENVVAR)
|
|
87
|
+
skip = pytest.mark.skipif(not self.inside_vpcsc, reason=reason)
|
|
88
|
+
return skip(testcase)
|
|
89
|
+
|
|
90
|
+
def skip_unless_inside_project(self, testcase):
|
|
91
|
+
"""Test decorator: skip if inside project env var not set."""
|
|
92
|
+
reason = (
|
|
93
|
+
"Project ID for running inside VPCSC not set. "
|
|
94
|
+
"Set the {} environment variable to enable this test."
|
|
95
|
+
).format(PROJECT_INSIDE_ENVVAR)
|
|
96
|
+
skip = pytest.mark.skipif(self.project_inside is None, reason=reason)
|
|
97
|
+
return skip(testcase)
|
|
98
|
+
|
|
99
|
+
def skip_unless_outside_project(self, testcase):
|
|
100
|
+
"""Test decorator: skip if outside project env var not set."""
|
|
101
|
+
reason = (
|
|
102
|
+
"Project ID for running outside VPCSC not set. "
|
|
103
|
+
"Set the {} environment variable to enable this test."
|
|
104
|
+
).format(PROJECT_OUTSIDE_ENVVAR)
|
|
105
|
+
skip = pytest.mark.skipif(self.project_outside is None, reason=reason)
|
|
106
|
+
return skip(testcase)
|
|
107
|
+
|
|
108
|
+
def skip_unless_outside_bucket(self, testcase):
|
|
109
|
+
"""Test decorator: skip if outside bucket env var not set."""
|
|
110
|
+
reason = (
|
|
111
|
+
"Bucket ID for running outside VPCSC not set. "
|
|
112
|
+
"Set the {} environment variable to enable this test."
|
|
113
|
+
).format(BUCKET_OUTSIDE_ENVVAR)
|
|
114
|
+
skip = pytest.mark.skipif(self.bucket_outside is None, reason=reason)
|
|
115
|
+
return skip(testcase)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
vpcsc_config = VPCSCTestConfig()
|