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.
@@ -0,0 +1,446 @@
1
+ # Copyright 2018 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
+ # https://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 itertools
16
+ import math
17
+ import queue
18
+ import sys
19
+ import threading
20
+ import tokenize
21
+
22
+
23
+ def orchestrate(*tests, **kwargs):
24
+ """
25
+ Orchestrate a deterministic concurrency test.
26
+
27
+ Runs test functions in separate threads, with each thread taking turns running up
28
+ until predefined syncpoints in a deterministic order. All possible orderings are
29
+ tested.
30
+
31
+ Most of the time, we try to use logic, best practices, and static analysis to insure
32
+ correct operation of concurrent code. Sometimes our powers of reasoning fail us and,
33
+ either through non-determistic stress testing or running code in production, a
34
+ concurrent bug is discovered. When this occurs, we'd like to have a regression test
35
+ to insure we've understood the problem and implemented a correct solution.
36
+ `orchestrate` provides a means of deterministically testing concurrent code so we
37
+ can write robust regression tests for complex concurrent scenarios.
38
+
39
+ `orchestrate` runs each passed in test function in its own thread. Threads then
40
+ "take turns" running. Turns are defined by setting syncpoints in the code under
41
+ test, using comments containing "pragma: SYNCPOINT". `orchestrate` will scan the
42
+ code under test and add syncpoints where it finds these comments.
43
+
44
+ For example, let's say you have the following code in production::
45
+
46
+ def hither_and_yon(destination):
47
+ hither(destination)
48
+ yon(destination)
49
+
50
+ You've found there's a concurrency bug when two threads execute this code with the
51
+ same argument, and you think that by adding a syncpoint between the calls to
52
+ `hither` and `yon` you can reproduce the problem in a regression test. First add a
53
+ comment with "pragma: SYNCPOINT" to the code under test::
54
+
55
+ def hither_and_yon(destination):
56
+ hither(destination) # pragma: SYNCPOINT
57
+ yon(destination)
58
+
59
+ When testing with orchestrate, there will now be a syncpoint, or a pause, after the
60
+ call to `hither` and before the call to `yon`. Now you can write a test to exercise
61
+ `hither_and_yon` running in parallel::
62
+
63
+ from unittest import mock
64
+ from tests.unit import orchestrate
65
+
66
+ from myorg.myproj.sales import travel
67
+
68
+ def test_concurrent_hither_and_yon():
69
+
70
+ def test_hither_and_yon():
71
+ assert something
72
+ travel.hither_and_yon("Raleigh")
73
+ assert something_else
74
+
75
+ counts = orchestrate.orchestrate(test_hither_and_yon, test_hither_and_yon)
76
+ assert counts == (2, 2)
77
+
78
+ What `orchestrate` will do now is take each of the two test functions passed in
79
+ (actually the same function, twice, in this case), run them serially, and count the
80
+ number of turns it takes to run each test to completion. In this example, it will
81
+ take two turns for each test: one turn to start the thread and execute up until the
82
+ syncpoint, and then another turn to execute from the syncpoint to the end of the
83
+ test. The number of turns will always be one greater than the number of syncpoints
84
+ encountered when executing the test.
85
+
86
+ Once the counts have been taken, `orchestrate` will construct a test sequence that
87
+ represents all of the turns taken by the passed in tests, with each value in the
88
+ sequence representing the index of the test whose turn it is in the sequence. In
89
+ this example, then, it would produce::
90
+
91
+ [0, 0, 1, 1]
92
+
93
+ This represents the first test taking both of its turns, followed by the second test
94
+ taking both of its turns. At this point this scenario has already been tested,
95
+ because this is what was run to produce the counts and the initial test sequence.
96
+ Now `orchestrate` will run all of the remaining scenarios by finding all the
97
+ permutations of the test sequence and executing those, in turn::
98
+
99
+ [0, 1, 0, 1]
100
+ [0, 1, 1, 0]
101
+ [1, 0, 0, 1]
102
+ [1, 0, 1, 0]
103
+ [1, 1, 0, 0]
104
+
105
+ You'll notice in our example that since both test functions are actually the same
106
+ function, that although it tested 6 scenarios there are effectively only really 3
107
+ unique scenarios. For the time being, though, `orchestrate` doesn't attempt to
108
+ detect this condition or optimize for it.
109
+
110
+ There are some performance considerations that should be taken into account when
111
+ writing tests. The number of unique test sequences grows quite quickly with the
112
+ number of turns taken by the functions under test. Our simple example with two
113
+ threads each taking two turns, only yielded 6 scenarios, but two threads each taking
114
+ 6 turns, for example, yields 924 scenarios. Add another six step thread, for a total
115
+ of three threads, and now you have over 17 thousand scenarios. In general, use the
116
+ least number of steps/threads you can get away with and still expose the behavior
117
+ you want to correct.
118
+
119
+ For the same reason as above, it is recommended that if you have many concurrent
120
+ tests, that you name your syncpoints so that you're not accidentally using
121
+ syncpoints intended for other tests, as this will add steps to your tests. While
122
+ it's not problematic from a testing standpoint to have extra steps in your tests, it
123
+ can use computing resources unnecessarily. A name can be added to any syncpoint
124
+ after the `SYNCPOINT` keyword in the pragma definition::
125
+
126
+ def hither_and_yon(destination):
127
+ hither(destination) # pragma: SYNCPOINT hither and yon
128
+ yon(destination)
129
+
130
+ In your test, then, pass that name to `orchestrate` to cause it to use only
131
+ syncpoints with that name::
132
+
133
+ orchestrate.orchestrate(
134
+ test_hither_and_yon, test_hither_and_yon, name="hither and yon"
135
+ )
136
+
137
+ As soon as any error or failure is detected, no more scenarios are run
138
+ and that error is propagated to the main thread.
139
+
140
+ One limitation of `orchestrate` is that it cannot really be used with `coverage`,
141
+ since both tools use `sys.set_trace`. Any code that needs verifiable test coverage
142
+ should have additional tests that do not use `orchestrate`, since code that is run
143
+ under orchestrate will not show up in a coverage report generated by `coverage`.
144
+
145
+ Args:
146
+ tests (Tuple[Callable]): Test functions to be run. These functions will not be
147
+ called with any arguments, so they must not have any required arguments.
148
+ name (Optional[str]): Only use syncpoints with the given name. If omitted, only
149
+ unnamed syncpoints will be used.
150
+
151
+ Returns:
152
+ Tuple[int]: A tuple of the count of the number turns for test passed in. Can be
153
+ used a sanity check in tests to make sure you understand what's actually
154
+ happening during a test.
155
+ """
156
+ name = kwargs.pop("name", None)
157
+ if kwargs:
158
+ raise TypeError(
159
+ "Unexpected keyword arguments: {}".format(", ".join(kwargs.keys()))
160
+ )
161
+
162
+ # Produce an initial test sequence. The fundamental question we're always trying to
163
+ # answer is "whose turn is it?" First we'll find out how many "turns" each test
164
+ # needs to complete when run serially and use that to construct a sequence of
165
+ # indexes. When a test's index appears in the sequence, it is that test's turn to
166
+ # run. We'll start by constructing a sequence that would run each test through to
167
+ # completion serially, one after the other.
168
+ test_sequence = []
169
+ counts = []
170
+ for index, test in enumerate(tests):
171
+ thread = _TestThread(test, name)
172
+ for count in itertools.count(1): # pragma: NO BRANCH
173
+ # Pragma is required because loop never finishes naturally.
174
+ thread.go()
175
+ if thread.finished:
176
+ break
177
+
178
+ counts.append(count)
179
+ test_sequence += [index] * count
180
+
181
+ # Now we can take that initial sequence and generate all of its permutations,
182
+ # running each one to try to uncover concurrency bugs
183
+ sequences = iter(_permutations(test_sequence))
184
+
185
+ # We already tested the first sequence getting our counts, so we can discard it
186
+ next(sequences)
187
+
188
+ # Test each sequence
189
+ for test_sequence in sequences:
190
+ threads = [_TestThread(test, name) for test in tests]
191
+ try:
192
+ for index in test_sequence:
193
+ threads[index].go()
194
+
195
+ # Its possible for number of turns to vary from one test run to the other,
196
+ # especially if there is some undiscovered concurrency bug. Go ahead and
197
+ # finish running each test to completion, if not already complete.
198
+ for thread in threads:
199
+ while not thread.finished:
200
+ thread.go()
201
+
202
+ except Exception:
203
+ # If an exception occurs, we still need to let any threads that are still
204
+ # going finish up. Additional exceptions are silently ignored.
205
+ for thread in threads:
206
+ thread.finish()
207
+ raise
208
+
209
+ return tuple(counts)
210
+
211
+
212
+ _local = threading.local()
213
+
214
+
215
+ class _Conductor:
216
+ """Coordinate communication between main thread and a test thread.
217
+
218
+ Two way communicaton is maintained between the main thread and a test thread using
219
+ two synchronized queues (`queue.Queue`) each with a size of one.
220
+ """
221
+
222
+ def __init__(self):
223
+ self._notify = queue.Queue(1)
224
+ self._go = queue.Queue(1)
225
+
226
+ def notify(self):
227
+ """Called from test thread to let us know it's finished or is ready for its next
228
+ turn."""
229
+ self._notify.put(None)
230
+
231
+ def standby(self):
232
+ """Called from test thread in order to block until told to go."""
233
+ self._go.get()
234
+
235
+ def wait(self):
236
+ """Called from main thread to wait for test thread to either get to the
237
+ next syncpoint or finish."""
238
+ self._notify.get()
239
+
240
+ def go(self):
241
+ """Called from main thread to tell test thread to go."""
242
+ self._go.put(None)
243
+
244
+
245
+ _SYNCPOINTS = {}
246
+ """Dict[str, Dict[str, Set[int]]]: Dict mapping source fileneme to a dict mapping
247
+ syncpoint name to set of line numbers where syncpoints with that name occur in the
248
+ source file.
249
+ """
250
+
251
+
252
+ def _get_syncpoints(filename):
253
+ """Find syncpoints in a source file.
254
+
255
+ Does a simple tokenization of the source file, looking for comments with "pragma:
256
+ SYNCPOINT", and populates _SYNCPOINTS using the syncpoint name and line number in
257
+ the source file.
258
+ """
259
+ _SYNCPOINTS[filename] = syncpoints = {}
260
+
261
+ # Use tokenize to find pragma comments
262
+ with open(filename, "r") as pyfile:
263
+ tokens = tokenize.generate_tokens(pyfile.readline)
264
+ for type, value, start, end, line in tokens:
265
+ if type == tokenize.COMMENT and "pragma: SYNCPOINT" in value:
266
+ name = value.split("SYNCPOINT", 1)[1].strip()
267
+ if not name:
268
+ name = None
269
+
270
+ if name not in syncpoints:
271
+ syncpoints[name] = set()
272
+
273
+ lineno, column = start
274
+ syncpoints[name].add(lineno)
275
+
276
+
277
+ class _TestThread:
278
+ """A thread for a test function."""
279
+
280
+ thread = None
281
+ finished = False
282
+ error = None
283
+ at_syncpoint = False
284
+
285
+ def __init__(self, test, name):
286
+ self.test = test
287
+ self.name = name
288
+ self.conductor = _Conductor()
289
+
290
+ def _run(self):
291
+ sys.settrace(self._trace)
292
+ _local.conductor = self.conductor
293
+ try:
294
+ self.test()
295
+ except Exception as error:
296
+ self.error = error
297
+ finally:
298
+ self.finished = True
299
+ self.conductor.notify()
300
+
301
+ def _sync(self):
302
+ # Tell main thread we're finished, for now
303
+ self.conductor.notify()
304
+
305
+ # Wait for the main thread to tell us to go again
306
+ self.conductor.standby()
307
+
308
+ def _trace(self, frame, event, arg):
309
+ """Argument to `sys.settrace`.
310
+
311
+ Handles frames during test run, syncing at syncpoints, when found.
312
+
313
+ Returns:
314
+ `None` if no more tracing is required for the function call, `self._trace`
315
+ if tracing should continue.
316
+ """
317
+ if self.at_syncpoint:
318
+ # We hit a syncpoint on the previous call, so now we sync.
319
+ self._sync()
320
+ self.at_syncpoint = False
321
+
322
+ filename = frame.f_globals.get("__file__")
323
+ if not filename:
324
+ # Can't trace code without a source file
325
+ return
326
+
327
+ if filename.endswith(".pyc"):
328
+ filename = filename[:-1]
329
+
330
+ if filename not in _SYNCPOINTS:
331
+ _get_syncpoints(filename)
332
+
333
+ syncpoints = _SYNCPOINTS[filename].get(self.name)
334
+ if not syncpoints:
335
+ # This file doesn't contain syncpoints, don't continue to trace
336
+ return
337
+
338
+ # We've hit a syncpoint. Execute whatever line the syncpoint is on and then
339
+ # sync next time this gets called.
340
+ if frame.f_lineno in syncpoints:
341
+ self.at_syncpoint = True
342
+
343
+ return self._trace
344
+
345
+ def go(self):
346
+ if self.finished:
347
+ return
348
+
349
+ if self.thread is None:
350
+ self.thread = threading.Thread(target=self._run)
351
+ self.thread.start()
352
+
353
+ else:
354
+ self.conductor.go()
355
+
356
+ self.conductor.wait()
357
+
358
+ if self.error:
359
+ raise self.error
360
+
361
+ def finish(self):
362
+ while not self.finished:
363
+ try:
364
+ self.go()
365
+ except Exception:
366
+ pass
367
+
368
+
369
+ class _permutations:
370
+ """Generates a sequence of all permutations of `sequence`.
371
+
372
+ Permutations are returned in lexicographic order using the "Generation in
373
+ lexicographic order" algorithm described in `the Wikipedia article on "Permutation"
374
+ <https://en.wikipedia.org/wiki/Permutation>`_.
375
+
376
+ This implementation differs significantly from `itertools.permutations` in that the
377
+ value of individual elements is taken into account, thus eliminating redundant
378
+ orderings that would be produced by `itertools.permutations`.
379
+
380
+ Args:
381
+ sequence (Sequence[Any]): Sequence must be finite and orderable.
382
+
383
+ Returns:
384
+ Sequence[Sequence[Any]]: Set of all permutations of `sequence`.
385
+ """
386
+
387
+ def __init__(self, sequence):
388
+ self._start = tuple(sorted(sequence))
389
+
390
+ def __len__(self):
391
+ """Compute the number of permutations.
392
+
393
+ Let the number of elements in a sequence N and the number of repetitions for
394
+ individual members of the sequence be n1, n2, ... nx. The number of unique
395
+ permutations is: N! / n1! / n2! / ... / nx!.
396
+
397
+ For example, let `sequence` be [1, 2, 3, 1, 2, 3, 1, 2, 3]. The number of unique
398
+ permutations is: 9! / 3! / 3! / 3! = 1680.
399
+
400
+ See: "Permutations of multisets" in `the Wikipedia article on "Permutation"
401
+ <https://en.wikipedia.org/wiki/Permutation>`_.
402
+ """
403
+ repeats = [len(list(group)) for value, group in itertools.groupby(self._start)]
404
+ length = math.factorial(len(self._start))
405
+ for repeat in repeats:
406
+ length /= math.factorial(repeat)
407
+
408
+ return int(length)
409
+
410
+ def __iter__(self):
411
+ """Iterate over permutations.
412
+
413
+ See: "Generation in lexicographic order" algorithm described in `the Wikipedia
414
+ article on "Permutation" <https://en.wikipedia.org/wiki/Permutation>`_.
415
+ """
416
+ current = list(self._start)
417
+ size = len(current)
418
+
419
+ while True:
420
+ yield tuple(current)
421
+
422
+ # 1. Find the largest index i such that a[i] < a[i + 1].
423
+ for i in range(size - 2, -1, -1):
424
+ if current[i] < current[i + 1]:
425
+ break
426
+
427
+ else:
428
+ # If no such index exists, the permutation is the last permutation.
429
+ return
430
+
431
+ # 2. Find the largest index j greater than i such that a[i] < a[j].
432
+ for j in range(size - 1, i, -1):
433
+ if current[i] < current[j]:
434
+ break
435
+
436
+ else: # pragma: NO COVER
437
+ raise RuntimeError("Broken algorithm")
438
+
439
+ # 3. Swap the value of a[i] with that of a[j].
440
+ temp = current[i]
441
+ current[i] = current[j]
442
+ current[j] = temp
443
+
444
+ # 4. Reverse the sequence from a[i + 1] up to and including the final
445
+ # element a[n].
446
+ current = current[: i + 1] + list(reversed(current[i + 1 :]))
test_utils/prefixer.py ADDED
@@ -0,0 +1,82 @@
1
+ # Copyright 2021 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 datetime
16
+ import random
17
+ import re
18
+
19
+ from typing import Union
20
+
21
+ _RESOURCE_DATE_FORMAT = "%Y%m%d%H%M%S"
22
+ _RESOURCE_DATE_LENGTH = 4 + 2 + 2 + 2 + 2 + 2
23
+ _RE_SEPARATORS = re.compile(r"[/\-\\_]")
24
+
25
+
26
+ def _common_prefix(repo, relative_dir, separator="_"):
27
+ repo = _RE_SEPARATORS.sub(separator, repo)
28
+ relative_dir = _RE_SEPARATORS.sub(separator, relative_dir)
29
+ return f"{repo}{separator}{relative_dir}"
30
+
31
+
32
+ class Prefixer(object):
33
+ """Create/manage resource IDs for system testing.
34
+
35
+ Usage:
36
+
37
+ Creating resources:
38
+
39
+ >>> import test_utils.prefixer
40
+ >>> prefixer = test_utils.prefixer.Prefixer("python-bigquery", "samples/snippets")
41
+ >>> dataset_id = prefixer.create_prefix() + "my_sample"
42
+
43
+ Cleaning up resources:
44
+
45
+ >>> @pytest.fixture(scope="session", autouse=True)
46
+ ... def cleanup_datasets(bigquery_client: bigquery.Client):
47
+ ... for dataset in bigquery_client.list_datasets():
48
+ ... if prefixer.should_cleanup(dataset.dataset_id):
49
+ ... bigquery_client.delete_dataset(
50
+ ... dataset, delete_contents=True, not_found_ok=True
51
+ """
52
+
53
+ def __init__(
54
+ self, repo, relative_dir, separator="_", cleanup_age=datetime.timedelta(days=1)
55
+ ):
56
+ self._separator = separator
57
+ self._cleanup_age = cleanup_age
58
+ self._prefix = _common_prefix(repo, relative_dir, separator=separator)
59
+
60
+ def create_prefix(self) -> str:
61
+ now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
62
+ timestamp = now.strftime(_RESOURCE_DATE_FORMAT)
63
+ random_string = hex(random.randrange(0x1000000))[2:]
64
+ return f"{self._prefix}{self._separator}{timestamp}{self._separator}{random_string}"
65
+
66
+ def _name_to_date(self, resource_name: str) -> Union[datetime.datetime, None]:
67
+ start_date = len(self._prefix) + len(self._separator)
68
+ date_string = resource_name[start_date : start_date + _RESOURCE_DATE_LENGTH]
69
+ try:
70
+ parsed_date = datetime.datetime.strptime(date_string, _RESOURCE_DATE_FORMAT)
71
+ return parsed_date
72
+ except ValueError:
73
+ return None
74
+
75
+ def should_cleanup(self, resource_name: str) -> bool:
76
+ now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None)
77
+ yesterday = now - self._cleanup_age
78
+ if not resource_name.startswith(self._prefix):
79
+ return False
80
+
81
+ created_date = self._name_to_date(resource_name)
82
+ return created_date is not None and created_date < yesterday
test_utils/py.typed ADDED
@@ -0,0 +1,2 @@
1
+ # Marker file for PEP 561.
2
+ # The test_utils package uses inline types.