Skip to content

Commit 0e8b3fc

Browse files
gh-108550: Speed up sqlite3 tests (#108551)
Refactor the CLI so we can easily invoke it and mock command-line arguments. Adapt the CLI tests so we no longer have to launch a separate process. Disable the busy handler for all concurrency tests; we have full control over the order of the SQLite C API calls, so we can safely do this. The sqlite3 test suite now completes ~8 times faster than before. Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 8db451c commit 0e8b3fc

File tree

4 files changed

+74
-101
lines changed

4 files changed

+74
-101
lines changed

Lib/sqlite3/__main__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def runsource(self, source, filename="<input>", symbol="single"):
6262
return False
6363

6464

65-
def main():
65+
def main(*args):
6666
parser = ArgumentParser(
6767
description="Python sqlite3 CLI",
6868
prog="python -m sqlite3",
@@ -86,7 +86,7 @@ def main():
8686
version=f"SQLite version {sqlite3.sqlite_version}",
8787
help="Print underlying SQLite library version",
8888
)
89-
args = parser.parse_args()
89+
args = parser.parse_args(*args)
9090

9191
if args.filename == ":memory:":
9292
db_name = "a transient in-memory database"
@@ -120,5 +120,8 @@ def main():
120120
finally:
121121
con.close()
122122

123+
sys.exit(0)
123124

124-
main()
125+
126+
if __name__ == "__main__":
127+
main(sys.argv)

Lib/test/test_sqlite3/test_cli.py

Lines changed: 61 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,35 @@
11
"""sqlite3 CLI tests."""
2-
3-
import sqlite3 as sqlite
4-
import subprocess
5-
import sys
2+
import sqlite3
63
import unittest
74

8-
from test.support import SHORT_TIMEOUT, requires_subprocess
5+
from sqlite3.__main__ import main as cli
96
from test.support.os_helper import TESTFN, unlink
7+
from test.support import captured_stdout, captured_stderr, captured_stdin
108

119

12-
@requires_subprocess()
1310
class CommandLineInterface(unittest.TestCase):
1411

1512
def _do_test(self, *args, expect_success=True):
16-
with subprocess.Popen(
17-
[sys.executable, "-Xutf8", "-m", "sqlite3", *args],
18-
encoding="utf-8",
19-
bufsize=0,
20-
stdout=subprocess.PIPE,
21-
stderr=subprocess.PIPE,
22-
) as proc:
23-
proc.wait()
24-
if expect_success == bool(proc.returncode):
25-
self.fail("".join(proc.stderr))
26-
stdout = proc.stdout.read()
27-
stderr = proc.stderr.read()
28-
if expect_success:
29-
self.assertEqual(stderr, "")
30-
else:
31-
self.assertEqual(stdout, "")
32-
return stdout, stderr
13+
with (
14+
captured_stdout() as out,
15+
captured_stderr() as err,
16+
self.assertRaises(SystemExit) as cm
17+
):
18+
cli(args)
19+
return out.getvalue(), err.getvalue(), cm.exception.code
3320

3421
def expect_success(self, *args):
35-
out, _ = self._do_test(*args)
22+
out, err, code = self._do_test(*args)
23+
self.assertEqual(code, 0,
24+
"\n".join([f"Unexpected failure: {args=}", out, err]))
25+
self.assertEqual(err, "")
3626
return out
3727

3828
def expect_failure(self, *args):
39-
_, err = self._do_test(*args, expect_success=False)
29+
out, err, code = self._do_test(*args, expect_success=False)
30+
self.assertNotEqual(code, 0,
31+
"\n".join([f"Unexpected failure: {args=}", out, err]))
32+
self.assertEqual(out, "")
4033
return err
4134

4235
def test_cli_help(self):
@@ -45,7 +38,7 @@ def test_cli_help(self):
4538

4639
def test_cli_version(self):
4740
out = self.expect_success("-v")
48-
self.assertIn(sqlite.sqlite_version, out)
41+
self.assertIn(sqlite3.sqlite_version, out)
4942

5043
def test_cli_execute_sql(self):
5144
out = self.expect_success(":memory:", "select 1")
@@ -68,87 +61,68 @@ def test_cli_on_disk_db(self):
6861
self.assertIn("(0,)", out)
6962

7063

71-
@requires_subprocess()
7264
class InteractiveSession(unittest.TestCase):
73-
TIMEOUT = SHORT_TIMEOUT / 10.
7465
MEMORY_DB_MSG = "Connected to a transient in-memory database"
7566
PS1 = "sqlite> "
7667
PS2 = "... "
7768

78-
def start_cli(self, *args):
79-
return subprocess.Popen(
80-
[sys.executable, "-Xutf8", "-m", "sqlite3", *args],
81-
encoding="utf-8",
82-
bufsize=0,
83-
stdin=subprocess.PIPE,
84-
# Note: the banner is printed to stderr, the prompt to stdout.
85-
stdout=subprocess.PIPE,
86-
stderr=subprocess.PIPE,
87-
)
88-
89-
def expect_success(self, proc):
90-
proc.wait()
91-
if proc.returncode:
92-
self.fail("".join(proc.stderr))
69+
def run_cli(self, *args, commands=()):
70+
with (
71+
captured_stdin() as stdin,
72+
captured_stdout() as stdout,
73+
captured_stderr() as stderr,
74+
self.assertRaises(SystemExit) as cm
75+
):
76+
for cmd in commands:
77+
stdin.write(cmd + "\n")
78+
stdin.seek(0)
79+
cli(args)
80+
81+
out = stdout.getvalue()
82+
err = stderr.getvalue()
83+
self.assertEqual(cm.exception.code, 0,
84+
f"Unexpected failure: {args=}\n{out}\n{err}")
85+
return out, err
9386

9487
def test_interact(self):
95-
with self.start_cli() as proc:
96-
out, err = proc.communicate(timeout=self.TIMEOUT)
97-
self.assertIn(self.MEMORY_DB_MSG, err)
98-
self.assertIn(self.PS1, out)
99-
self.expect_success(proc)
88+
out, err = self.run_cli()
89+
self.assertIn(self.MEMORY_DB_MSG, err)
90+
self.assertIn(self.PS1, out)
10091

10192
def test_interact_quit(self):
102-
with self.start_cli() as proc:
103-
out, err = proc.communicate(input=".quit", timeout=self.TIMEOUT)
104-
self.assertIn(self.MEMORY_DB_MSG, err)
105-
self.assertIn(self.PS1, out)
106-
self.expect_success(proc)
93+
out, err = self.run_cli(commands=(".quit",))
94+
self.assertIn(self.PS1, out)
10795

10896
def test_interact_version(self):
109-
with self.start_cli() as proc:
110-
out, err = proc.communicate(input=".version", timeout=self.TIMEOUT)
111-
self.assertIn(self.MEMORY_DB_MSG, err)
112-
self.assertIn(sqlite.sqlite_version, out)
113-
self.expect_success(proc)
97+
out, err = self.run_cli(commands=(".version",))
98+
self.assertIn(self.MEMORY_DB_MSG, err)
99+
self.assertIn(sqlite3.sqlite_version, out)
114100

115101
def test_interact_valid_sql(self):
116-
with self.start_cli() as proc:
117-
out, err = proc.communicate(input="select 1;",
118-
timeout=self.TIMEOUT)
119-
self.assertIn(self.MEMORY_DB_MSG, err)
120-
self.assertIn("(1,)", out)
121-
self.expect_success(proc)
102+
out, err = self.run_cli(commands=("SELECT 1;",))
103+
self.assertIn(self.MEMORY_DB_MSG, err)
104+
self.assertIn("(1,)", out)
122105

123106
def test_interact_valid_multiline_sql(self):
124-
with self.start_cli() as proc:
125-
out, err = proc.communicate(input="select 1\n;",
126-
timeout=self.TIMEOUT)
127-
self.assertIn(self.MEMORY_DB_MSG, err)
128-
self.assertIn(self.PS2, out)
129-
self.assertIn("(1,)", out)
130-
self.expect_success(proc)
107+
out, err = self.run_cli(commands=("SELECT 1\n;",))
108+
self.assertIn(self.MEMORY_DB_MSG, err)
109+
self.assertIn(self.PS2, out)
110+
self.assertIn("(1,)", out)
131111

132112
def test_interact_invalid_sql(self):
133-
with self.start_cli() as proc:
134-
out, err = proc.communicate(input="sel;", timeout=self.TIMEOUT)
135-
self.assertIn(self.MEMORY_DB_MSG, err)
136-
self.assertIn("OperationalError (SQLITE_ERROR)", err)
137-
self.expect_success(proc)
113+
out, err = self.run_cli(commands=("sel;",))
114+
self.assertIn(self.MEMORY_DB_MSG, err)
115+
self.assertIn("OperationalError (SQLITE_ERROR)", err)
138116

139117
def test_interact_on_disk_file(self):
140118
self.addCleanup(unlink, TESTFN)
141-
with self.start_cli(TESTFN) as proc:
142-
out, err = proc.communicate(input="create table t(t);",
143-
timeout=self.TIMEOUT)
144-
self.assertIn(TESTFN, err)
145-
self.assertIn(self.PS1, out)
146-
self.expect_success(proc)
147-
with self.start_cli(TESTFN, "select count(t) from t") as proc:
148-
out = proc.stdout.read()
149-
err = proc.stderr.read()
150-
self.assertIn("(0,)", out)
151-
self.expect_success(proc)
119+
120+
out, err = self.run_cli(TESTFN, commands=("CREATE TABLE t(t);",))
121+
self.assertIn(TESTFN, err)
122+
self.assertIn(self.PS1, out)
123+
124+
out, _ = self.run_cli(TESTFN, commands=("SELECT count(t) FROM t;",))
125+
self.assertIn("(0,)", out)
152126

153127

154128
if __name__ == "__main__":

Lib/test/test_sqlite3/test_dbapi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1854,7 +1854,7 @@ def test_on_conflict_replace(self):
18541854

18551855
@requires_subprocess()
18561856
class MultiprocessTests(unittest.TestCase):
1857-
CONNECTION_TIMEOUT = SHORT_TIMEOUT / 1000. # Defaults to 30 ms
1857+
CONNECTION_TIMEOUT = 0 # Disable the busy timeout.
18581858

18591859
def tearDown(self):
18601860
unlink(TESTFN)

Lib/test/test_sqlite3/test_transactions.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,21 @@
2424
import sqlite3 as sqlite
2525
from contextlib import contextmanager
2626

27-
from test.support import LOOPBACK_TIMEOUT
2827
from test.support.os_helper import TESTFN, unlink
2928
from test.support.script_helper import assert_python_ok
3029

3130
from .util import memory_database
3231
from .util import MemoryDatabaseMixin
3332

3433

35-
TIMEOUT = LOOPBACK_TIMEOUT / 10
36-
37-
3834
class TransactionTests(unittest.TestCase):
3935
def setUp(self):
40-
self.con1 = sqlite.connect(TESTFN, timeout=TIMEOUT)
36+
# We can disable the busy handlers, since we control
37+
# the order of SQLite C API operations.
38+
self.con1 = sqlite.connect(TESTFN, timeout=0)
4139
self.cur1 = self.con1.cursor()
4240

43-
self.con2 = sqlite.connect(TESTFN, timeout=TIMEOUT)
41+
self.con2 = sqlite.connect(TESTFN, timeout=0)
4442
self.cur2 = self.con2.cursor()
4543

4644
def tearDown(self):
@@ -120,10 +118,8 @@ def test_raise_timeout(self):
120118
self.cur2.execute("insert into test(i) values (5)")
121119

122120
def test_locking(self):
123-
"""
124-
This tests the improved concurrency with pysqlite 2.3.4. You needed
125-
to roll back con2 before you could commit con1.
126-
"""
121+
# This tests the improved concurrency with pysqlite 2.3.4. You needed
122+
# to roll back con2 before you could commit con1.
127123
self.cur1.execute("create table test(i)")
128124
self.cur1.execute("insert into test(i) values (5)")
129125
with self.assertRaises(sqlite.OperationalError):

0 commit comments

Comments
 (0)