PYTHON-4708 - Convert test.qcheck to async (#1832)

This commit is contained in:
Noah Stapp 2024-09-05 10:20:32 -04:00 committed by GitHub
parent 26c55048d4
commit 6e9bf1e4a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 280 additions and 15 deletions

255
test/asynchronous/qcheck.py Normal file
View File

@ -0,0 +1,255 @@
# Copyright 2009-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License 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.
from __future__ import annotations
import datetime
import random
import re
import sys
import traceback
sys.path[0:0] = [""]
from bson.dbref import DBRef
from bson.objectid import ObjectId
from bson.son import SON
_IS_SYNC = False
gen_target = 100
reduction_attempts = 10
examples = 5
def lift(value):
return lambda: value
def choose_lifted(generator_list):
return lambda: random.choice(generator_list)
def my_map(generator, function):
return lambda: function(generator())
def choose(list):
return lambda: random.choice(list)()
def gen_range(start, stop):
return lambda: random.randint(start, stop)
def gen_int():
max_int = 2147483647
return lambda: random.randint(-max_int - 1, max_int)
def gen_float():
return lambda: (random.random() - 0.5) * sys.maxsize
def gen_boolean():
return lambda: random.choice([True, False])
def gen_printable_char():
return lambda: chr(random.randint(32, 126))
def gen_printable_string(gen_length):
return lambda: "".join(gen_list(gen_printable_char(), gen_length)())
def gen_char(set=None):
return lambda: bytes([random.randint(0, 255)])
def gen_string(gen_length):
return lambda: b"".join(gen_list(gen_char(), gen_length)())
def gen_unichar():
return lambda: chr(random.randint(1, 0xFFF))
def gen_unicode(gen_length):
return lambda: "".join([x for x in gen_list(gen_unichar(), gen_length)() if x not in ".$"])
def gen_list(generator, gen_length):
return lambda: [generator() for _ in range(gen_length())]
def gen_datetime():
return lambda: datetime.datetime(
random.randint(1970, 2037),
random.randint(1, 12),
random.randint(1, 28),
random.randint(0, 23),
random.randint(0, 59),
random.randint(0, 59),
random.randint(0, 999) * 1000,
)
def gen_dict(gen_key, gen_value, gen_length):
def a_dict(gen_key, gen_value, length):
result = {}
for _ in range(length):
result[gen_key()] = gen_value()
return result
return lambda: a_dict(gen_key, gen_value, gen_length())
def gen_regexp(gen_length):
# TODO our patterns only consist of one letter.
# this is because of a bug in CPython's regex equality testing,
# which I haven't quite tracked down, so I'm just ignoring it...
def pattern():
return "".join(gen_list(choose_lifted("a"), gen_length)())
def gen_flags():
flags = 0
if random.random() > 0.5:
flags = flags | re.IGNORECASE
if random.random() > 0.5:
flags = flags | re.MULTILINE
if random.random() > 0.5:
flags = flags | re.VERBOSE
return flags
return lambda: re.compile(pattern(), gen_flags())
def gen_objectid():
return lambda: ObjectId()
def gen_dbref():
collection = gen_unicode(gen_range(0, 20))
return lambda: DBRef(collection(), gen_mongo_value(1, True)())
def gen_mongo_value(depth, ref):
choices = [
gen_unicode(gen_range(0, 50)),
gen_printable_string(gen_range(0, 50)),
my_map(gen_string(gen_range(0, 1000)), bytes),
gen_int(),
gen_float(),
gen_boolean(),
gen_datetime(),
gen_objectid(),
lift(None),
]
if ref:
choices.append(gen_dbref())
if depth > 0:
choices.append(gen_mongo_list(depth, ref))
choices.append(gen_mongo_dict(depth, ref))
return choose(choices)
def gen_mongo_list(depth, ref):
return gen_list(gen_mongo_value(depth - 1, ref), gen_range(0, 10))
def gen_mongo_dict(depth, ref=True):
return my_map(
gen_dict(gen_unicode(gen_range(0, 20)), gen_mongo_value(depth - 1, ref), gen_range(0, 10)),
SON,
)
def simplify(case): # TODO this is a hack
if isinstance(case, SON) and "$ref" not in case:
simplified = SON(case) # make a copy!
if random.choice([True, False]):
# delete
simplified_keys = list(simplified)
if not len(simplified_keys):
return (False, case)
simplified.pop(random.choice(simplified_keys))
return (True, simplified)
else:
# simplify a value
simplified_items = list(simplified.items())
if not len(simplified_items):
return (False, case)
(key, value) = random.choice(simplified_items)
(success, value) = simplify(value)
simplified[key] = value
return (success, success and simplified or case)
if isinstance(case, list):
simplified = list(case)
if random.choice([True, False]):
# delete
if not len(simplified):
return (False, case)
simplified.pop(random.randrange(len(simplified)))
return (True, simplified)
else:
# simplify an item
if not len(simplified):
return (False, case)
index = random.randrange(len(simplified))
(success, value) = simplify(simplified[index])
simplified[index] = value
return (success, success and simplified or case)
return (False, case)
async def reduce(case, predicate, reductions=0):
for _ in range(reduction_attempts):
(reduced, simplified) = simplify(case)
if reduced and not await predicate(simplified):
return await reduce(simplified, predicate, reductions + 1)
return (reductions, case)
async def isnt(predicate):
async def is_not(x):
return not await predicate(x)
return is_not
async def check(predicate, generator):
counter_examples = []
for _ in range(gen_target):
case = generator()
try:
if not await predicate(case):
reduction = await reduce(case, predicate)
counter_examples.append("after {} reductions: {!r}".format(*reduction))
except:
counter_examples.append(f"{case!r} : {traceback.format_exc()}")
return counter_examples
async def check_unittest(test, predicate, generator):
counter_examples = await check(predicate, generator)
if counter_examples:
failures = len(counter_examples)
message = "\n".join([" -> %s" % f for f in counter_examples[:examples]])
message = "found %d counter examples, displaying first %d:\n%s" % (
failures,
min(failures, examples),
message,
)
test.fail(message)

View File

@ -21,17 +21,21 @@ import io
import sys import sys
import zipfile import zipfile
from io import BytesIO from io import BytesIO
from test.asynchronous import AsyncIntegrationTest, AsyncUnitTest, async_client_context from test.asynchronous import (
AsyncIntegrationTest,
AsyncUnitTest,
async_client_context,
qcheck,
unittest,
)
from pymongo.asynchronous.database import AsyncDatabase from pymongo.asynchronous.database import AsyncDatabase
sys.path[0:0] = [""] sys.path[0:0] = [""]
from test import IntegrationTest, qcheck, unittest from test.utils import EventListener, async_rs_or_single_client
from test.utils import EventListener, async_rs_or_single_client, rs_or_single_client
from bson.objectid import ObjectId from bson.objectid import ObjectId
from gridfs import GridFS
from gridfs.asynchronous.grid_file import ( from gridfs.asynchronous.grid_file import (
_SEEK_CUR, _SEEK_CUR,
_SEEK_END, _SEEK_END,
@ -44,7 +48,7 @@ from gridfs.asynchronous.grid_file import (
from gridfs.errors import NoFile from gridfs.errors import NoFile
from pymongo import AsyncMongoClient from pymongo import AsyncMongoClient
from pymongo.asynchronous.helpers import aiter, anext from pymongo.asynchronous.helpers import aiter, anext
from pymongo.errors import ConfigurationError, InvalidOperation, ServerSelectionTimeoutError from pymongo.errors import ConfigurationError, ServerSelectionTimeoutError
from pymongo.message import _CursorAddress from pymongo.message import _CursorAddress
_IS_SYNC = False _IS_SYNC = False
@ -407,8 +411,6 @@ class AsyncTestGridFile(AsyncIntegrationTest):
g = AsyncGridOut(self.db.fs, f._id) g = AsyncGridOut(self.db.fs, f._id)
self.assertEqual(random_string, await g.read()) self.assertEqual(random_string, await g.read())
# TODO: https://jira.mongodb.org/browse/PYTHON-4708
@async_client_context.require_sync
async def test_small_chunks(self): async def test_small_chunks(self):
self.files = 0 self.files = 0
self.chunks = 0 self.chunks = 0
@ -431,7 +433,7 @@ class AsyncTestGridFile(AsyncIntegrationTest):
self.assertEqual(data, await g.read(10) + await g.read(10)) self.assertEqual(data, await g.read(10) + await g.read(10))
return True return True
qcheck.check_unittest(self, helper, qcheck.gen_string(qcheck.gen_range(0, 20))) await qcheck.check_unittest(self, helper, qcheck.gen_string(qcheck.gen_range(0, 20)))
async def test_seek(self): async def test_seek(self):
f = AsyncGridIn(self.db.fs, chunkSize=3) f = AsyncGridIn(self.db.fs, chunkSize=3)

View File

@ -25,6 +25,8 @@ from bson.dbref import DBRef
from bson.objectid import ObjectId from bson.objectid import ObjectId
from bson.son import SON from bson.son import SON
_IS_SYNC = True
gen_target = 100 gen_target = 100
reduction_attempts = 10 reduction_attempts = 10
examples = 5 examples = 5
@ -221,7 +223,10 @@ def reduce(case, predicate, reductions=0):
def isnt(predicate): def isnt(predicate):
return lambda x: not predicate(x) def is_not(x):
return not predicate(x)
return is_not
def check(predicate, generator): def check(predicate, generator):

View File

@ -21,17 +21,21 @@ import io
import sys import sys
import zipfile import zipfile
from io import BytesIO from io import BytesIO
from test import IntegrationTest, UnitTest, client_context from test import (
IntegrationTest,
UnitTest,
client_context,
qcheck,
unittest,
)
from pymongo.synchronous.database import Database from pymongo.synchronous.database import Database
sys.path[0:0] = [""] sys.path[0:0] = [""]
from test import IntegrationTest, qcheck, unittest
from test.utils import EventListener, rs_or_single_client from test.utils import EventListener, rs_or_single_client
from bson.objectid import ObjectId from bson.objectid import ObjectId
from gridfs import GridFS
from gridfs.errors import NoFile from gridfs.errors import NoFile
from gridfs.synchronous.grid_file import ( from gridfs.synchronous.grid_file import (
_SEEK_CUR, _SEEK_CUR,
@ -43,7 +47,7 @@ from gridfs.synchronous.grid_file import (
GridOutCursor, GridOutCursor,
) )
from pymongo import MongoClient from pymongo import MongoClient
from pymongo.errors import ConfigurationError, InvalidOperation, ServerSelectionTimeoutError from pymongo.errors import ConfigurationError, ServerSelectionTimeoutError
from pymongo.message import _CursorAddress from pymongo.message import _CursorAddress
from pymongo.synchronous.helpers import iter, next from pymongo.synchronous.helpers import iter, next
@ -405,8 +409,6 @@ class TestGridFile(IntegrationTest):
g = GridOut(self.db.fs, f._id) g = GridOut(self.db.fs, f._id)
self.assertEqual(random_string, g.read()) self.assertEqual(random_string, g.read())
# TODO: https://jira.mongodb.org/browse/PYTHON-4708
@client_context.require_sync
def test_small_chunks(self): def test_small_chunks(self):
self.files = 0 self.files = 0
self.chunks = 0 self.chunks = 0

View File

@ -159,6 +159,7 @@ converted_tests = [
"conftest.py", "conftest.py",
"pymongo_mocks.py", "pymongo_mocks.py",
"utils_spec_runner.py", "utils_spec_runner.py",
"qcheck.py",
"test_bulk.py", "test_bulk.py",
"test_client.py", "test_client.py",
"test_client_bulk_write.py", "test_client_bulk_write.py",