Skip to content

Commit 65acd8a

Browse files
committed
test: Improved testing around flush method and exception safety.
1 parent 6693f87 commit 65acd8a

File tree

4 files changed

+96
-27
lines changed

4 files changed

+96
-27
lines changed

TODO.rst

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,24 @@
22
TODO
33
====
44

5-
Stuff to get done before 1.0.0
6-
75

86
Build Tooling
97
=============
10-
* **[HIGH]** Integrate tooling to build binaries for a matrix of operating systems,
11-
architectures and Python versions. This will also be our CI.
8+
* **[HIGH]** Integrate tooling to build binaries for a matrix of operating
9+
systems, architectures and Python versions. This will also be our CI.
1210
This should help: https://github.com/pypa/cibuildwheel
1311

14-
* **[MEDIUM]** Integrate TOX https://tox.wiki/en/latest/ to trigger our testcases.
15-
This makes it easier to eventually run tests vs. different dependency
16-
versions.
17-
18-
* **[MEDIUM]** Figure out how ``bumpversion`` works, how it cuts tags, etc.
19-
Does ``bumpversion`` help us with ``CHANGELOG.rst``?
20-
21-
* **[LOW]** Consider converting `setup.py` to `build.py` and transitioning to
22-
`poetry`. Whilst we don't have other python dependencies for now
23-
(though we will have numpy and pandas eventually), it would standardise the
24-
way we build other python packages.
25-
*This can probably wait for a future release.*
2612

2713
Docs
2814
====
29-
* **[MEDIUM]** Document on a per-version basis.
30-
3115
* **[HIGH]** Author a few examples of how to use the client.
3216
This will help people get started. The examples should be presented in Sphinx
3317
using ``.. literalinclude::``.
3418
See: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-literalinclude
3519
The examples should be in the ``examples/`` directory in the repo.
3620

21+
* **[MEDIUM]** Document on a per-version basis.
22+
3723
* **[MEDIUM]** These examples should be tested as part of the unit tests (as they
3824
are in the C client). This is to ensure they don't "bit rot" as the code
3925
changes.
@@ -43,14 +29,12 @@ Development
4329
===========
4430
* **[HIGH]** Review API naming!
4531

46-
* **[HIGH]** Implement ``tabular()`` API in the buffer.
47-
48-
* **[HIGH]** Test the flush API carefully with exceptions before / after flushing.
49-
5032
* **[HIGH]** Implement the auto-commit logic based on a watermark.
5133

5234
* **[MEDIUM]** Once we're done with them, merge in changes in the ``py_client_tweaks`` branch
5335
of the C client.
5436

37+
* **[LOW]** Implement ``tabular()`` API in the buffer.
38+
5539
* **[LOW]** Implement ``pandas()`` API in the buffer.
5640
*This can probably wait for a future release.*

proj.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ def _rmtree(path: pathlib.Path):
4848

4949

5050
def _arg2bool(arg):
51+
if isinstance(arg, bool):
52+
return arg
5153
return arg.lower() in ('true', 'yes', '1')
5254

5355

@@ -81,11 +83,11 @@ def doc(http_serve=False, port=None):
8183

8284

8385
@command
84-
def test(all=False, patch_path='1'):
86+
def test(all=False, patch_path='1', *args):
8587
env = {'TEST_QUESTDB_PATCH_PATH': patch_path}
8688
if _arg2bool(all):
8789
env['TEST_QUESTDB_INTEGRATION'] = '1'
88-
_run('python3', 'test/test.py', '-v',
90+
_run('python3', 'test/test.py', '-v', *args,
8991
env=env)
9092

9193

test/mock_server.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@ def recv(self, wait_timeout_sec=0.1):
3333
buf = b''
3434
while True:
3535
# Block for *some* data.
36-
select.select([self._client_sock], [], [])
37-
buf += self._client_sock.recv(1024)
36+
res = select.select([self._client_sock], [], [], 1.0)
37+
new_data = self._client_sock.recv(1024)
38+
if not new_data:
39+
return []
40+
buf += new_data
3841
if len(buf) < 2:
3942
continue
4043
if (buf[-1] == ord('\n')) and (buf[-2] != ord('\\')):
@@ -44,10 +47,13 @@ def recv(self, wait_timeout_sec=0.1):
4447
self.msgs.extend(new_msgs)
4548
return new_msgs
4649

47-
def __exit__(self, _ex_type, _ex_value, _ex_tb):
50+
def close(self):
4851
if self._client_sock:
4952
self._client_sock.close()
5053
self._client_sock = None
5154
if self._sock:
5255
self._sock.close()
5356
self._sock = None
57+
58+
def __exit__(self, _ex_type, _ex_value, _ex_tb):
59+
self.close()

test/test.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import unittest
77
import datetime
8+
import time
89

910
import patch_path
1011
from mock_server import Server
@@ -129,6 +130,82 @@ def test_row_before_connect(self):
129130
finally:
130131
sender.close()
131132

133+
def test_flush_1(self):
134+
with Server() as server:
135+
with ilp.Sender('localhost', server.port) as sender:
136+
server.accept()
137+
with self.assertRaisesRegex(ilp.IlpError, 'Column names'):
138+
sender.row('tbl1', symbols={'...bad name..': 'val1'})
139+
self.assertEqual(str(sender), '')
140+
sender.flush()
141+
self.assertEqual(str(sender), '')
142+
msgs = server.recv()
143+
self.assertEqual(msgs, [])
144+
145+
def test_flush_2(self):
146+
with Server() as server:
147+
with ilp.Sender('localhost', server.port) as sender:
148+
server.accept()
149+
server.close()
150+
151+
# We enter a bad state where we can't flush again.
152+
with self.assertRaises(ilp.IlpError):
153+
for _ in range(1000):
154+
time.sleep(0.01)
155+
sender.row('tbl1', symbols={'a': 'b'})
156+
sender.flush()
157+
158+
# We should still be in a bad state.
159+
with self.assertRaises(ilp.IlpError):
160+
sender.row('tbl1', symbols={'a': 'b'})
161+
sender.flush()
162+
163+
# Leaving the `with` scope will call __exit__ and here we test
164+
# that a prior exception will not cause subsequent problems.
165+
166+
def test_flush_3(self):
167+
# Same as test_flush_2, but we catch the exception _outside_ the
168+
# sender's `with` block, to ensure no exceptions get trapped.
169+
with Server() as server:
170+
with self.assertRaises(ilp.IlpError):
171+
with ilp.Sender('localhost', server.port) as sender:
172+
server.accept()
173+
server.close()
174+
for _ in range(1000):
175+
time.sleep(0.01)
176+
sender.row('tbl1', symbols={'a': 'b'})
177+
sender.flush()
178+
179+
def test_independent_buffer(self):
180+
buf = ilp.Buffer()
181+
buf.row('tbl1', symbols={'sym1': 'val1'})
182+
exp = 'tbl1,sym1=val1\n'
183+
bexp = exp[:-1].encode('utf-8')
184+
self.assertEqual(str(buf), exp)
185+
186+
with Server() as server1, Server() as server2:
187+
with ilp.Sender('localhost', server1.port) as sender1, \
188+
ilp.Sender('localhost', server2.port) as sender2:
189+
server1.accept()
190+
server2.accept()
191+
192+
sender1.flush(buf, clear=False)
193+
self.assertEqual(str(buf), exp)
194+
195+
sender2.flush(buf, clear=False)
196+
self.assertEqual(str(buf), exp)
197+
198+
msgs1 = server1.recv()
199+
msgs2 = server2.recv()
200+
self.assertEqual(msgs1, [bexp])
201+
self.assertEqual(msgs2, [bexp])
202+
203+
sender1.flush(buf)
204+
self.assertEqual(server1.recv(), [bexp])
205+
206+
# The buffer is now auto-cleared.
207+
self.assertEqual(str(buf), '')
208+
132209

133210
if __name__ == '__main__':
134211
unittest.main()

0 commit comments

Comments
 (0)