diff options
author | Erlend Egeberg Aasland <erlend.aasland@innova.no> | 2021-10-29 22:08:19 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-29 22:08:19 (GMT) |
commit | 62bf263a775f4444d8b5d5841cc09be3bd53e933 (patch) | |
tree | d92d1e79ebb70b5b5f92ab034aa45fecd2a368e4 /Lib/test/test_sqlite3/test_userfunctions.py | |
parent | c2d0ba722a7b3839685af968cf0c304a24cdf525 (diff) | |
download | cpython-62bf263a775f4444d8b5d5841cc09be3bd53e933.zip cpython-62bf263a775f4444d8b5d5841cc09be3bd53e933.tar.gz cpython-62bf263a775f4444d8b5d5841cc09be3bd53e933.tar.bz2 |
bpo-10572: Move `sqlite3` tests to `Lib/test` (GH-29304)
Automerge-Triggered-By: GH:brettcannon
Diffstat (limited to 'Lib/test/test_sqlite3/test_userfunctions.py')
-rw-r--r-- | Lib/test/test_sqlite3/test_userfunctions.py | 665 |
1 files changed, 665 insertions, 0 deletions
diff --git a/Lib/test/test_sqlite3/test_userfunctions.py b/Lib/test/test_sqlite3/test_userfunctions.py new file mode 100644 index 0000000..ad40847 --- /dev/null +++ b/Lib/test/test_sqlite3/test_userfunctions.py @@ -0,0 +1,665 @@ +# pysqlite2/test/userfunctions.py: tests for user-defined functions and +# aggregates. +# +# Copyright (C) 2005-2007 Gerhard Häring <gh@ghaering.de> +# +# This file is part of pysqlite. +# +# This software is provided 'as-is', without any express or implied +# warranty. In no event will the authors be held liable for any damages +# arising from the use of this software. +# +# Permission is granted to anyone to use this software for any purpose, +# including commercial applications, and to alter it and redistribute it +# freely, subject to the following restrictions: +# +# 1. The origin of this software must not be misrepresented; you must not +# claim that you wrote the original software. If you use this software +# in a product, an acknowledgment in the product documentation would be +# appreciated but is not required. +# 2. Altered source versions must be plainly marked as such, and must not be +# misrepresented as being the original software. +# 3. This notice may not be removed or altered from any source distribution. + +import contextlib +import functools +import gc +import io +import sys +import unittest +import unittest.mock +import sqlite3 as sqlite + +from test.support import bigmemtest + + +def with_tracebacks(strings, traceback=True): + """Convenience decorator for testing callback tracebacks.""" + if traceback: + strings.append('Traceback') + + def decorator(func): + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + # First, run the test with traceback enabled. + with check_tracebacks(self, strings): + func(self, *args, **kwargs) + + # Then run the test with traceback disabled. + func(self, *args, **kwargs) + return wrapper + return decorator + +@contextlib.contextmanager +def check_tracebacks(self, strings): + """Convenience context manager for testing callback tracebacks.""" + sqlite.enable_callback_tracebacks(True) + try: + buf = io.StringIO() + with contextlib.redirect_stderr(buf): + yield + tb = buf.getvalue() + for s in strings: + self.assertIn(s, tb) + finally: + sqlite.enable_callback_tracebacks(False) + +def func_returntext(): + return "foo" +def func_returntextwithnull(): + return "1\x002" +def func_returnunicode(): + return "bar" +def func_returnint(): + return 42 +def func_returnfloat(): + return 3.14 +def func_returnnull(): + return None +def func_returnblob(): + return b"blob" +def func_returnlonglong(): + return 1<<31 +def func_raiseexception(): + 5/0 +def func_memoryerror(): + raise MemoryError +def func_overflowerror(): + raise OverflowError + +def func_isstring(v): + return type(v) is str +def func_isint(v): + return type(v) is int +def func_isfloat(v): + return type(v) is float +def func_isnone(v): + return type(v) is type(None) +def func_isblob(v): + return isinstance(v, (bytes, memoryview)) +def func_islonglong(v): + return isinstance(v, int) and v >= 1<<31 + +def func(*args): + return len(args) + +class AggrNoStep: + def __init__(self): + pass + + def finalize(self): + return 1 + +class AggrNoFinalize: + def __init__(self): + pass + + def step(self, x): + pass + +class AggrExceptionInInit: + def __init__(self): + 5/0 + + def step(self, x): + pass + + def finalize(self): + pass + +class AggrExceptionInStep: + def __init__(self): + pass + + def step(self, x): + 5/0 + + def finalize(self): + return 42 + +class AggrExceptionInFinalize: + def __init__(self): + pass + + def step(self, x): + pass + + def finalize(self): + 5/0 + +class AggrCheckType: + def __init__(self): + self.val = None + + def step(self, whichType, val): + theType = {"str": str, "int": int, "float": float, "None": type(None), + "blob": bytes} + self.val = int(theType[whichType] is type(val)) + + def finalize(self): + return self.val + +class AggrCheckTypes: + def __init__(self): + self.val = 0 + + def step(self, whichType, *vals): + theType = {"str": str, "int": int, "float": float, "None": type(None), + "blob": bytes} + for val in vals: + self.val += int(theType[whichType] is type(val)) + + def finalize(self): + return self.val + +class AggrSum: + def __init__(self): + self.val = 0.0 + + def step(self, val): + self.val += val + + def finalize(self): + return self.val + +class AggrText: + def __init__(self): + self.txt = "" + def step(self, txt): + self.txt = self.txt + txt + def finalize(self): + return self.txt + + +class FunctionTests(unittest.TestCase): + def setUp(self): + self.con = sqlite.connect(":memory:") + + self.con.create_function("returntext", 0, func_returntext) + self.con.create_function("returntextwithnull", 0, func_returntextwithnull) + self.con.create_function("returnunicode", 0, func_returnunicode) + self.con.create_function("returnint", 0, func_returnint) + self.con.create_function("returnfloat", 0, func_returnfloat) + self.con.create_function("returnnull", 0, func_returnnull) + self.con.create_function("returnblob", 0, func_returnblob) + self.con.create_function("returnlonglong", 0, func_returnlonglong) + self.con.create_function("raiseexception", 0, func_raiseexception) + self.con.create_function("memoryerror", 0, func_memoryerror) + self.con.create_function("overflowerror", 0, func_overflowerror) + + self.con.create_function("isstring", 1, func_isstring) + self.con.create_function("isint", 1, func_isint) + self.con.create_function("isfloat", 1, func_isfloat) + self.con.create_function("isnone", 1, func_isnone) + self.con.create_function("isblob", 1, func_isblob) + self.con.create_function("islonglong", 1, func_islonglong) + self.con.create_function("spam", -1, func) + self.con.execute("create table test(t text)") + + def tearDown(self): + self.con.close() + + def test_func_error_on_create(self): + with self.assertRaises(sqlite.OperationalError): + self.con.create_function("bla", -100, lambda x: 2*x) + + def test_func_ref_count(self): + def getfunc(): + def f(): + return 1 + return f + f = getfunc() + globals()["foo"] = f + # self.con.create_function("reftest", 0, getfunc()) + self.con.create_function("reftest", 0, f) + cur = self.con.cursor() + cur.execute("select reftest()") + + def test_func_return_text(self): + cur = self.con.cursor() + cur.execute("select returntext()") + val = cur.fetchone()[0] + self.assertEqual(type(val), str) + self.assertEqual(val, "foo") + + def test_func_return_text_with_null_char(self): + cur = self.con.cursor() + res = cur.execute("select returntextwithnull()").fetchone()[0] + self.assertEqual(type(res), str) + self.assertEqual(res, "1\x002") + + def test_func_return_unicode(self): + cur = self.con.cursor() + cur.execute("select returnunicode()") + val = cur.fetchone()[0] + self.assertEqual(type(val), str) + self.assertEqual(val, "bar") + + def test_func_return_int(self): + cur = self.con.cursor() + cur.execute("select returnint()") + val = cur.fetchone()[0] + self.assertEqual(type(val), int) + self.assertEqual(val, 42) + + def test_func_return_float(self): + cur = self.con.cursor() + cur.execute("select returnfloat()") + val = cur.fetchone()[0] + self.assertEqual(type(val), float) + if val < 3.139 or val > 3.141: + self.fail("wrong value") + + def test_func_return_null(self): + cur = self.con.cursor() + cur.execute("select returnnull()") + val = cur.fetchone()[0] + self.assertEqual(type(val), type(None)) + self.assertEqual(val, None) + + def test_func_return_blob(self): + cur = self.con.cursor() + cur.execute("select returnblob()") + val = cur.fetchone()[0] + self.assertEqual(type(val), bytes) + self.assertEqual(val, b"blob") + + def test_func_return_long_long(self): + cur = self.con.cursor() + cur.execute("select returnlonglong()") + val = cur.fetchone()[0] + self.assertEqual(val, 1<<31) + + @with_tracebacks(['func_raiseexception', '5/0', 'ZeroDivisionError']) + def test_func_exception(self): + cur = self.con.cursor() + with self.assertRaises(sqlite.OperationalError) as cm: + cur.execute("select raiseexception()") + cur.fetchone() + self.assertEqual(str(cm.exception), 'user-defined function raised exception') + + @with_tracebacks(['func_memoryerror', 'MemoryError']) + def test_func_memory_error(self): + cur = self.con.cursor() + with self.assertRaises(MemoryError): + cur.execute("select memoryerror()") + cur.fetchone() + + @with_tracebacks(['func_overflowerror', 'OverflowError']) + def test_func_overflow_error(self): + cur = self.con.cursor() + with self.assertRaises(sqlite.DataError): + cur.execute("select overflowerror()") + cur.fetchone() + + def test_param_string(self): + cur = self.con.cursor() + for text in ["foo", str()]: + with self.subTest(text=text): + cur.execute("select isstring(?)", (text,)) + val = cur.fetchone()[0] + self.assertEqual(val, 1) + + def test_param_int(self): + cur = self.con.cursor() + cur.execute("select isint(?)", (42,)) + val = cur.fetchone()[0] + self.assertEqual(val, 1) + + def test_param_float(self): + cur = self.con.cursor() + cur.execute("select isfloat(?)", (3.14,)) + val = cur.fetchone()[0] + self.assertEqual(val, 1) + + def test_param_none(self): + cur = self.con.cursor() + cur.execute("select isnone(?)", (None,)) + val = cur.fetchone()[0] + self.assertEqual(val, 1) + + def test_param_blob(self): + cur = self.con.cursor() + cur.execute("select isblob(?)", (memoryview(b"blob"),)) + val = cur.fetchone()[0] + self.assertEqual(val, 1) + + def test_param_long_long(self): + cur = self.con.cursor() + cur.execute("select islonglong(?)", (1<<42,)) + val = cur.fetchone()[0] + self.assertEqual(val, 1) + + def test_any_arguments(self): + cur = self.con.cursor() + cur.execute("select spam(?, ?)", (1, 2)) + val = cur.fetchone()[0] + self.assertEqual(val, 2) + + def test_empty_blob(self): + cur = self.con.execute("select isblob(x'')") + self.assertTrue(cur.fetchone()[0]) + + # Regarding deterministic functions: + # + # Between 3.8.3 and 3.15.0, deterministic functions were only used to + # optimize inner loops, so for those versions we can only test if the + # sqlite machinery has factored out a call or not. From 3.15.0 and onward, + # deterministic functions were permitted in WHERE clauses of partial + # indices, which allows testing based on syntax, iso. the query optimizer. + @unittest.skipIf(sqlite.sqlite_version_info < (3, 8, 3), "Requires SQLite 3.8.3 or higher") + def test_func_non_deterministic(self): + mock = unittest.mock.Mock(return_value=None) + self.con.create_function("nondeterministic", 0, mock, deterministic=False) + if sqlite.sqlite_version_info < (3, 15, 0): + self.con.execute("select nondeterministic() = nondeterministic()") + self.assertEqual(mock.call_count, 2) + else: + with self.assertRaises(sqlite.OperationalError): + self.con.execute("create index t on test(t) where nondeterministic() is not null") + + @unittest.skipIf(sqlite.sqlite_version_info < (3, 8, 3), "Requires SQLite 3.8.3 or higher") + def test_func_deterministic(self): + mock = unittest.mock.Mock(return_value=None) + self.con.create_function("deterministic", 0, mock, deterministic=True) + if sqlite.sqlite_version_info < (3, 15, 0): + self.con.execute("select deterministic() = deterministic()") + self.assertEqual(mock.call_count, 1) + else: + try: + self.con.execute("create index t on test(t) where deterministic() is not null") + except sqlite.OperationalError: + self.fail("Unexpected failure while creating partial index") + + @unittest.skipIf(sqlite.sqlite_version_info >= (3, 8, 3), "SQLite < 3.8.3 needed") + def test_func_deterministic_not_supported(self): + with self.assertRaises(sqlite.NotSupportedError): + self.con.create_function("deterministic", 0, int, deterministic=True) + + def test_func_deterministic_keyword_only(self): + with self.assertRaises(TypeError): + self.con.create_function("deterministic", 0, int, True) + + def test_function_destructor_via_gc(self): + # See bpo-44304: The destructor of the user function can + # crash if is called without the GIL from the gc functions + dest = sqlite.connect(':memory:') + def md5sum(t): + return + + dest.create_function("md5", 1, md5sum) + x = dest("create table lang (name, first_appeared)") + del md5sum, dest + + y = [x] + y.append(y) + + del x,y + gc.collect() + + def test_func_return_too_large_int(self): + cur = self.con.cursor() + for value in 2**63, -2**63-1, 2**64: + self.con.create_function("largeint", 0, lambda value=value: value) + with check_tracebacks(self, ['OverflowError']): + with self.assertRaises(sqlite.DataError): + cur.execute("select largeint()") + + def test_func_return_text_with_surrogates(self): + cur = self.con.cursor() + self.con.create_function("pychr", 1, chr) + for value in 0xd8ff, 0xdcff: + with check_tracebacks(self, + ['UnicodeEncodeError', 'surrogates not allowed']): + with self.assertRaises(sqlite.OperationalError): + cur.execute("select pychr(?)", (value,)) + + @unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform') + @bigmemtest(size=2**31, memuse=3, dry_run=False) + def test_func_return_too_large_text(self, size): + cur = self.con.cursor() + for size in 2**31-1, 2**31: + self.con.create_function("largetext", 0, lambda size=size: "b" * size) + with self.assertRaises(sqlite.DataError): + cur.execute("select largetext()") + + @unittest.skipUnless(sys.maxsize > 2**32, 'requires 64bit platform') + @bigmemtest(size=2**31, memuse=2, dry_run=False) + def test_func_return_too_large_blob(self, size): + cur = self.con.cursor() + for size in 2**31-1, 2**31: + self.con.create_function("largeblob", 0, lambda size=size: b"b" * size) + with self.assertRaises(sqlite.DataError): + cur.execute("select largeblob()") + + +class AggregateTests(unittest.TestCase): + def setUp(self): + self.con = sqlite.connect(":memory:") + cur = self.con.cursor() + cur.execute(""" + create table test( + t text, + i integer, + f float, + n, + b blob + ) + """) + cur.execute("insert into test(t, i, f, n, b) values (?, ?, ?, ?, ?)", + ("foo", 5, 3.14, None, memoryview(b"blob"),)) + + self.con.create_aggregate("nostep", 1, AggrNoStep) + self.con.create_aggregate("nofinalize", 1, AggrNoFinalize) + self.con.create_aggregate("excInit", 1, AggrExceptionInInit) + self.con.create_aggregate("excStep", 1, AggrExceptionInStep) + self.con.create_aggregate("excFinalize", 1, AggrExceptionInFinalize) + self.con.create_aggregate("checkType", 2, AggrCheckType) + self.con.create_aggregate("checkTypes", -1, AggrCheckTypes) + self.con.create_aggregate("mysum", 1, AggrSum) + self.con.create_aggregate("aggtxt", 1, AggrText) + + def tearDown(self): + #self.cur.close() + #self.con.close() + pass + + def test_aggr_error_on_create(self): + with self.assertRaises(sqlite.OperationalError): + self.con.create_function("bla", -100, AggrSum) + + def test_aggr_no_step(self): + cur = self.con.cursor() + with self.assertRaises(AttributeError) as cm: + cur.execute("select nostep(t) from test") + self.assertEqual(str(cm.exception), "'AggrNoStep' object has no attribute 'step'") + + def test_aggr_no_finalize(self): + cur = self.con.cursor() + with self.assertRaises(sqlite.OperationalError) as cm: + cur.execute("select nofinalize(t) from test") + val = cur.fetchone()[0] + self.assertEqual(str(cm.exception), "user-defined aggregate's 'finalize' method raised error") + + @with_tracebacks(['__init__', '5/0', 'ZeroDivisionError']) + def test_aggr_exception_in_init(self): + cur = self.con.cursor() + with self.assertRaises(sqlite.OperationalError) as cm: + cur.execute("select excInit(t) from test") + val = cur.fetchone()[0] + self.assertEqual(str(cm.exception), "user-defined aggregate's '__init__' method raised error") + + @with_tracebacks(['step', '5/0', 'ZeroDivisionError']) + def test_aggr_exception_in_step(self): + cur = self.con.cursor() + with self.assertRaises(sqlite.OperationalError) as cm: + cur.execute("select excStep(t) from test") + val = cur.fetchone()[0] + self.assertEqual(str(cm.exception), "user-defined aggregate's 'step' method raised error") + + @with_tracebacks(['finalize', '5/0', 'ZeroDivisionError']) + def test_aggr_exception_in_finalize(self): + cur = self.con.cursor() + with self.assertRaises(sqlite.OperationalError) as cm: + cur.execute("select excFinalize(t) from test") + val = cur.fetchone()[0] + self.assertEqual(str(cm.exception), "user-defined aggregate's 'finalize' method raised error") + + def test_aggr_check_param_str(self): + cur = self.con.cursor() + cur.execute("select checkTypes('str', ?, ?)", ("foo", str())) + val = cur.fetchone()[0] + self.assertEqual(val, 2) + + def test_aggr_check_param_int(self): + cur = self.con.cursor() + cur.execute("select checkType('int', ?)", (42,)) + val = cur.fetchone()[0] + self.assertEqual(val, 1) + + def test_aggr_check_params_int(self): + cur = self.con.cursor() + cur.execute("select checkTypes('int', ?, ?)", (42, 24)) + val = cur.fetchone()[0] + self.assertEqual(val, 2) + + def test_aggr_check_param_float(self): + cur = self.con.cursor() + cur.execute("select checkType('float', ?)", (3.14,)) + val = cur.fetchone()[0] + self.assertEqual(val, 1) + + def test_aggr_check_param_none(self): + cur = self.con.cursor() + cur.execute("select checkType('None', ?)", (None,)) + val = cur.fetchone()[0] + self.assertEqual(val, 1) + + def test_aggr_check_param_blob(self): + cur = self.con.cursor() + cur.execute("select checkType('blob', ?)", (memoryview(b"blob"),)) + val = cur.fetchone()[0] + self.assertEqual(val, 1) + + def test_aggr_check_aggr_sum(self): + cur = self.con.cursor() + cur.execute("delete from test") + cur.executemany("insert into test(i) values (?)", [(10,), (20,), (30,)]) + cur.execute("select mysum(i) from test") + val = cur.fetchone()[0] + self.assertEqual(val, 60) + + def test_aggr_no_match(self): + cur = self.con.execute("select mysum(i) from (select 1 as i) where i == 0") + val = cur.fetchone()[0] + self.assertIsNone(val) + + def test_aggr_text(self): + cur = self.con.cursor() + for txt in ["foo", "1\x002"]: + with self.subTest(txt=txt): + cur.execute("select aggtxt(?) from test", (txt,)) + val = cur.fetchone()[0] + self.assertEqual(val, txt) + + +class AuthorizerTests(unittest.TestCase): + @staticmethod + def authorizer_cb(action, arg1, arg2, dbname, source): + if action != sqlite.SQLITE_SELECT: + return sqlite.SQLITE_DENY + if arg2 == 'c2' or arg1 == 't2': + return sqlite.SQLITE_DENY + return sqlite.SQLITE_OK + + def setUp(self): + self.con = sqlite.connect(":memory:") + self.con.executescript(""" + create table t1 (c1, c2); + create table t2 (c1, c2); + insert into t1 (c1, c2) values (1, 2); + insert into t2 (c1, c2) values (4, 5); + """) + + # For our security test: + self.con.execute("select c2 from t2") + + self.con.set_authorizer(self.authorizer_cb) + + def tearDown(self): + pass + + def test_table_access(self): + with self.assertRaises(sqlite.DatabaseError) as cm: + self.con.execute("select * from t2") + self.assertIn('prohibited', str(cm.exception)) + + def test_column_access(self): + with self.assertRaises(sqlite.DatabaseError) as cm: + self.con.execute("select c2 from t1") + self.assertIn('prohibited', str(cm.exception)) + + def test_clear_authorizer(self): + self.con.set_authorizer(None) + self.con.execute("select * from t2") + self.con.execute("select c2 from t1") + + +class AuthorizerRaiseExceptionTests(AuthorizerTests): + @staticmethod + def authorizer_cb(action, arg1, arg2, dbname, source): + if action != sqlite.SQLITE_SELECT: + raise ValueError + if arg2 == 'c2' or arg1 == 't2': + raise ValueError + return sqlite.SQLITE_OK + + @with_tracebacks(['authorizer_cb', 'ValueError']) + def test_table_access(self): + super().test_table_access() + + @with_tracebacks(['authorizer_cb', 'ValueError']) + def test_column_access(self): + super().test_table_access() + +class AuthorizerIllegalTypeTests(AuthorizerTests): + @staticmethod + def authorizer_cb(action, arg1, arg2, dbname, source): + if action != sqlite.SQLITE_SELECT: + return 0.0 + if arg2 == 'c2' or arg1 == 't2': + return 0.0 + return sqlite.SQLITE_OK + +class AuthorizerLargeIntegerTests(AuthorizerTests): + @staticmethod + def authorizer_cb(action, arg1, arg2, dbname, source): + if action != sqlite.SQLITE_SELECT: + return 2**32 + if arg2 == 'c2' or arg1 == 't2': + return 2**32 + return sqlite.SQLITE_OK + + +if __name__ == "__main__": + unittest.main() |