Skip to content

Commit 6e9bf1e

Browse files
authored
PYTHON-4708 - Convert test.qcheck to async (#1832)
1 parent 26c5504 commit 6e9bf1e

File tree

5 files changed

+280
-15
lines changed

5 files changed

+280
-15
lines changed

test/asynchronous/qcheck.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
# Copyright 2009-present MongoDB, Inc.
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+
from __future__ import annotations
15+
16+
import datetime
17+
import random
18+
import re
19+
import sys
20+
import traceback
21+
22+
sys.path[0:0] = [""]
23+
24+
from bson.dbref import DBRef
25+
from bson.objectid import ObjectId
26+
from bson.son import SON
27+
28+
_IS_SYNC = False
29+
30+
gen_target = 100
31+
reduction_attempts = 10
32+
examples = 5
33+
34+
35+
def lift(value):
36+
return lambda: value
37+
38+
39+
def choose_lifted(generator_list):
40+
return lambda: random.choice(generator_list)
41+
42+
43+
def my_map(generator, function):
44+
return lambda: function(generator())
45+
46+
47+
def choose(list):
48+
return lambda: random.choice(list)()
49+
50+
51+
def gen_range(start, stop):
52+
return lambda: random.randint(start, stop)
53+
54+
55+
def gen_int():
56+
max_int = 2147483647
57+
return lambda: random.randint(-max_int - 1, max_int)
58+
59+
60+
def gen_float():
61+
return lambda: (random.random() - 0.5) * sys.maxsize
62+
63+
64+
def gen_boolean():
65+
return lambda: random.choice([True, False])
66+
67+
68+
def gen_printable_char():
69+
return lambda: chr(random.randint(32, 126))
70+
71+
72+
def gen_printable_string(gen_length):
73+
return lambda: "".join(gen_list(gen_printable_char(), gen_length)())
74+
75+
76+
def gen_char(set=None):
77+
return lambda: bytes([random.randint(0, 255)])
78+
79+
80+
def gen_string(gen_length):
81+
return lambda: b"".join(gen_list(gen_char(), gen_length)())
82+
83+
84+
def gen_unichar():
85+
return lambda: chr(random.randint(1, 0xFFF))
86+
87+
88+
def gen_unicode(gen_length):
89+
return lambda: "".join([x for x in gen_list(gen_unichar(), gen_length)() if x not in ".$"])
90+
91+
92+
def gen_list(generator, gen_length):
93+
return lambda: [generator() for _ in range(gen_length())]
94+
95+
96+
def gen_datetime():
97+
return lambda: datetime.datetime(
98+
random.randint(1970, 2037),
99+
random.randint(1, 12),
100+
random.randint(1, 28),
101+
random.randint(0, 23),
102+
random.randint(0, 59),
103+
random.randint(0, 59),
104+
random.randint(0, 999) * 1000,
105+
)
106+
107+
108+
def gen_dict(gen_key, gen_value, gen_length):
109+
def a_dict(gen_key, gen_value, length):
110+
result = {}
111+
for _ in range(length):
112+
result[gen_key()] = gen_value()
113+
return result
114+
115+
return lambda: a_dict(gen_key, gen_value, gen_length())
116+
117+
118+
def gen_regexp(gen_length):
119+
# TODO our patterns only consist of one letter.
120+
# this is because of a bug in CPython's regex equality testing,
121+
# which I haven't quite tracked down, so I'm just ignoring it...
122+
def pattern():
123+
return "".join(gen_list(choose_lifted("a"), gen_length)())
124+
125+
def gen_flags():
126+
flags = 0
127+
if random.random() > 0.5:
128+
flags = flags | re.IGNORECASE
129+
if random.random() > 0.5:
130+
flags = flags | re.MULTILINE
131+
if random.random() > 0.5:
132+
flags = flags | re.VERBOSE
133+
134+
return flags
135+
136+
return lambda: re.compile(pattern(), gen_flags())
137+
138+
139+
def gen_objectid():
140+
return lambda: ObjectId()
141+
142+
143+
def gen_dbref():
144+
collection = gen_unicode(gen_range(0, 20))
145+
return lambda: DBRef(collection(), gen_mongo_value(1, True)())
146+
147+
148+
def gen_mongo_value(depth, ref):
149+
choices = [
150+
gen_unicode(gen_range(0, 50)),
151+
gen_printable_string(gen_range(0, 50)),
152+
my_map(gen_string(gen_range(0, 1000)), bytes),
153+
gen_int(),
154+
gen_float(),
155+
gen_boolean(),
156+
gen_datetime(),
157+
gen_objectid(),
158+
lift(None),
159+
]
160+
if ref:
161+
choices.append(gen_dbref())
162+
if depth > 0:
163+
choices.append(gen_mongo_list(depth, ref))
164+
choices.append(gen_mongo_dict(depth, ref))
165+
return choose(choices)
166+
167+
168+
def gen_mongo_list(depth, ref):
169+
return gen_list(gen_mongo_value(depth - 1, ref), gen_range(0, 10))
170+
171+
172+
def gen_mongo_dict(depth, ref=True):
173+
return my_map(
174+
gen_dict(gen_unicode(gen_range(0, 20)), gen_mongo_value(depth - 1, ref), gen_range(0, 10)),
175+
SON,
176+
)
177+
178+
179+
def simplify(case): # TODO this is a hack
180+
if isinstance(case, SON) and "$ref" not in case:
181+
simplified = SON(case) # make a copy!
182+
if random.choice([True, False]):
183+
# delete
184+
simplified_keys = list(simplified)
185+
if not len(simplified_keys):
186+
return (False, case)
187+
simplified.pop(random.choice(simplified_keys))
188+
return (True, simplified)
189+
else:
190+
# simplify a value
191+
simplified_items = list(simplified.items())
192+
if not len(simplified_items):
193+
return (False, case)
194+
(key, value) = random.choice(simplified_items)
195+
(success, value) = simplify(value)
196+
simplified[key] = value
197+
return (success, success and simplified or case)
198+
if isinstance(case, list):
199+
simplified = list(case)
200+
if random.choice([True, False]):
201+
# delete
202+
if not len(simplified):
203+
return (False, case)
204+
simplified.pop(random.randrange(len(simplified)))
205+
return (True, simplified)
206+
else:
207+
# simplify an item
208+
if not len(simplified):
209+
return (False, case)
210+
index = random.randrange(len(simplified))
211+
(success, value) = simplify(simplified[index])
212+
simplified[index] = value
213+
return (success, success and simplified or case)
214+
return (False, case)
215+
216+
217+
async def reduce(case, predicate, reductions=0):
218+
for _ in range(reduction_attempts):
219+
(reduced, simplified) = simplify(case)
220+
if reduced and not await predicate(simplified):
221+
return await reduce(simplified, predicate, reductions + 1)
222+
return (reductions, case)
223+
224+
225+
async def isnt(predicate):
226+
async def is_not(x):
227+
return not await predicate(x)
228+
229+
return is_not
230+
231+
232+
async def check(predicate, generator):
233+
counter_examples = []
234+
for _ in range(gen_target):
235+
case = generator()
236+
try:
237+
if not await predicate(case):
238+
reduction = await reduce(case, predicate)
239+
counter_examples.append("after {} reductions: {!r}".format(*reduction))
240+
except:
241+
counter_examples.append(f"{case!r} : {traceback.format_exc()}")
242+
return counter_examples
243+
244+
245+
async def check_unittest(test, predicate, generator):
246+
counter_examples = await check(predicate, generator)
247+
if counter_examples:
248+
failures = len(counter_examples)
249+
message = "\n".join([" -> %s" % f for f in counter_examples[:examples]])
250+
message = "found %d counter examples, displaying first %d:\n%s" % (
251+
failures,
252+
min(failures, examples),
253+
message,
254+
)
255+
test.fail(message)

test/asynchronous/test_grid_file.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,21 @@
2121
import sys
2222
import zipfile
2323
from io import BytesIO
24-
from test.asynchronous import AsyncIntegrationTest, AsyncUnitTest, async_client_context
24+
from test.asynchronous import (
25+
AsyncIntegrationTest,
26+
AsyncUnitTest,
27+
async_client_context,
28+
qcheck,
29+
unittest,
30+
)
2531

2632
from pymongo.asynchronous.database import AsyncDatabase
2733

2834
sys.path[0:0] = [""]
2935

30-
from test import IntegrationTest, qcheck, unittest
31-
from test.utils import EventListener, async_rs_or_single_client, rs_or_single_client
36+
from test.utils import EventListener, async_rs_or_single_client
3237

3338
from bson.objectid import ObjectId
34-
from gridfs import GridFS
3539
from gridfs.asynchronous.grid_file import (
3640
_SEEK_CUR,
3741
_SEEK_END,
@@ -44,7 +48,7 @@
4448
from gridfs.errors import NoFile
4549
from pymongo import AsyncMongoClient
4650
from pymongo.asynchronous.helpers import aiter, anext
47-
from pymongo.errors import ConfigurationError, InvalidOperation, ServerSelectionTimeoutError
51+
from pymongo.errors import ConfigurationError, ServerSelectionTimeoutError
4852
from pymongo.message import _CursorAddress
4953

5054
_IS_SYNC = False
@@ -407,8 +411,6 @@ async def test_multi_chunk_file(self):
407411
g = AsyncGridOut(self.db.fs, f._id)
408412
self.assertEqual(random_string, await g.read())
409413

410-
# TODO: https://jira.mongodb.org/browse/PYTHON-4708
411-
@async_client_context.require_sync
412414
async def test_small_chunks(self):
413415
self.files = 0
414416
self.chunks = 0
@@ -431,7 +433,7 @@ async def helper(data):
431433
self.assertEqual(data, await g.read(10) + await g.read(10))
432434
return True
433435

434-
qcheck.check_unittest(self, helper, qcheck.gen_string(qcheck.gen_range(0, 20)))
436+
await qcheck.check_unittest(self, helper, qcheck.gen_string(qcheck.gen_range(0, 20)))
435437

436438
async def test_seek(self):
437439
f = AsyncGridIn(self.db.fs, chunkSize=3)

test/qcheck.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
from bson.objectid import ObjectId
2626
from bson.son import SON
2727

28+
_IS_SYNC = True
29+
2830
gen_target = 100
2931
reduction_attempts = 10
3032
examples = 5
@@ -221,7 +223,10 @@ def reduce(case, predicate, reductions=0):
221223

222224

223225
def isnt(predicate):
224-
return lambda x: not predicate(x)
226+
def is_not(x):
227+
return not predicate(x)
228+
229+
return is_not
225230

226231

227232
def check(predicate, generator):

test/test_grid_file.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,21 @@
2121
import sys
2222
import zipfile
2323
from io import BytesIO
24-
from test import IntegrationTest, UnitTest, client_context
24+
from test import (
25+
IntegrationTest,
26+
UnitTest,
27+
client_context,
28+
qcheck,
29+
unittest,
30+
)
2531

2632
from pymongo.synchronous.database import Database
2733

2834
sys.path[0:0] = [""]
2935

30-
from test import IntegrationTest, qcheck, unittest
3136
from test.utils import EventListener, rs_or_single_client
3237

3338
from bson.objectid import ObjectId
34-
from gridfs import GridFS
3539
from gridfs.errors import NoFile
3640
from gridfs.synchronous.grid_file import (
3741
_SEEK_CUR,
@@ -43,7 +47,7 @@
4347
GridOutCursor,
4448
)
4549
from pymongo import MongoClient
46-
from pymongo.errors import ConfigurationError, InvalidOperation, ServerSelectionTimeoutError
50+
from pymongo.errors import ConfigurationError, ServerSelectionTimeoutError
4751
from pymongo.message import _CursorAddress
4852
from pymongo.synchronous.helpers import iter, next
4953

@@ -405,8 +409,6 @@ def test_multi_chunk_file(self):
405409
g = GridOut(self.db.fs, f._id)
406410
self.assertEqual(random_string, g.read())
407411

408-
# TODO: https://jira.mongodb.org/browse/PYTHON-4708
409-
@client_context.require_sync
410412
def test_small_chunks(self):
411413
self.files = 0
412414
self.chunks = 0

tools/synchro.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@
159159
"conftest.py",
160160
"pymongo_mocks.py",
161161
"utils_spec_runner.py",
162+
"qcheck.py",
162163
"test_bulk.py",
163164
"test_client.py",
164165
"test_client_bulk_write.py",

0 commit comments

Comments
 (0)