Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 255 additions & 0 deletions test/asynchronous/qcheck.py
Original file line number Diff line number Diff line change
@@ -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)
18 changes: 10 additions & 8 deletions test/asynchronous/test_grid_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,21 @@
import sys
import zipfile
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

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

from test import IntegrationTest, qcheck, unittest
from test.utils import EventListener, async_rs_or_single_client, rs_or_single_client
from test.utils import EventListener, async_rs_or_single_client

from bson.objectid import ObjectId
from gridfs import GridFS
from gridfs.asynchronous.grid_file import (
_SEEK_CUR,
_SEEK_END,
Expand All @@ -44,7 +48,7 @@
from gridfs.errors import NoFile
from pymongo import AsyncMongoClient
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

_IS_SYNC = False
Expand Down Expand Up @@ -407,8 +411,6 @@ async def test_multi_chunk_file(self):
g = AsyncGridOut(self.db.fs, f._id)
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):
self.files = 0
self.chunks = 0
Expand All @@ -431,7 +433,7 @@ async def helper(data):
self.assertEqual(data, await g.read(10) + await g.read(10))
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):
f = AsyncGridIn(self.db.fs, chunkSize=3)
Expand Down
7 changes: 6 additions & 1 deletion test/qcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
from bson.objectid import ObjectId
from bson.son import SON

_IS_SYNC = True

gen_target = 100
reduction_attempts = 10
examples = 5
Expand Down Expand Up @@ -221,7 +223,10 @@ def reduce(case, predicate, reductions=0):


def isnt(predicate):
return lambda x: not predicate(x)
def is_not(x):
return not predicate(x)

return is_not


def check(predicate, generator):
Expand Down
14 changes: 8 additions & 6 deletions test/test_grid_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,21 @@
import sys
import zipfile
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

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

from test import IntegrationTest, qcheck, unittest
from test.utils import EventListener, rs_or_single_client

from bson.objectid import ObjectId
from gridfs import GridFS
from gridfs.errors import NoFile
from gridfs.synchronous.grid_file import (
_SEEK_CUR,
Expand All @@ -43,7 +47,7 @@
GridOutCursor,
)
from pymongo import MongoClient
from pymongo.errors import ConfigurationError, InvalidOperation, ServerSelectionTimeoutError
from pymongo.errors import ConfigurationError, ServerSelectionTimeoutError
from pymongo.message import _CursorAddress
from pymongo.synchronous.helpers import iter, next

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

# TODO: https://jira.mongodb.org/browse/PYTHON-4708
@client_context.require_sync
def test_small_chunks(self):
self.files = 0
self.chunks = 0
Expand Down
1 change: 1 addition & 0 deletions tools/synchro.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
"conftest.py",
"pymongo_mocks.py",
"utils_spec_runner.py",
"qcheck.py",
"test_bulk.py",
"test_client.py",
"test_client_bulk_write.py",
Expand Down
Loading