From 7d66c20edb087e175064a638cc90f2a644e2f93a Mon Sep 17 00:00:00 2001 From: James Henstridge Date: Tue, 6 May 2008 17:04:26 +0800 Subject: [PATCH] * tests/test_lobject.py (LargeObjectTests): add more tests, including behaviour on closed lobjects and stale lobjects. * psycopg/lobject_type.c (psyco_lobj_close): don't mark the connection closed here because it is done by lobject_close_locked(). * psycopg/lobject_int.c (lobject_open): mark objects as not closed if we successfully open them. (lobject_close_locked): mark the lobject closed here. (lobject_export): ensure we are in a transaction, since lo_export() issues multiple queries. * psycopg/lobject_type.c (lobject_setup): make lobjects start closed. --- ChangeLog | 17 ++++ psycopg/lobject_int.c | 22 +++-- psycopg/lobject_type.c | 52 +++++----- tests/test_lobject.py | 210 ++++++++++++++++++++++++++++++++++++----- 4 files changed, 244 insertions(+), 57 deletions(-) diff --git a/ChangeLog b/ChangeLog index 704fd32e..9c132555 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,20 @@ +2008-05-06 James Henstridge + + * tests/test_lobject.py (LargeObjectTests): add more tests, + including behaviour on closed lobjects and stale lobjects. + + * psycopg/lobject_type.c (psyco_lobj_close): don't mark the + connection closed here because it is done by + lobject_close_locked(). + + * psycopg/lobject_int.c (lobject_open): mark objects as not closed + if we successfully open them. + (lobject_close_locked): mark the lobject closed here. + (lobject_export): ensure we are in a transaction, since + lo_export() issues multiple queries. + + * psycopg/lobject_type.c (lobject_setup): make lobjects start closed. + 2008-05-05 James Henstridge * psycopg/lobject.h: don't export the lobjectType symbol. diff --git a/psycopg/lobject_int.c b/psycopg/lobject_int.c index 9058523a..476f236d 100644 --- a/psycopg/lobject_int.c +++ b/psycopg/lobject_int.c @@ -95,11 +95,7 @@ lobject_open(lobjectObject *self, connectionObject *conn, retvalue = -1; goto end; } - } - else { - /* this is necessary to make sure no function that needs and - fd is called on unopened lobjects */ - self->closed = 1; + self->closed = 0; } /* set the mode for future reference */ self->mode = mode; @@ -136,6 +132,7 @@ lobject_close_locked(lobjectObject *self, char **error) self->fd == -1) return 0; + self->closed = 1; retvalue = lo_close(self->conn->pgconn, self->fd); self->fd = -1; if (retvalue < 0) @@ -308,21 +305,26 @@ lobject_export(lobjectObject *self, char *filename) { PGresult *pgres = NULL; char *error = NULL; - int res; + int retvalue; Py_BEGIN_ALLOW_THREADS; pthread_mutex_lock(&(self->conn->lock)); - res = lo_export(self->conn->pgconn, self->oid, filename); - if (res < 0) + retvalue = pq_begin_locked(self->conn, &pgres, &error); + if (retvalue < 0) + goto end; + + retvalue = lo_export(self->conn->pgconn, self->oid, filename); + if (retvalue < 0) collect_error(self->conn, &error); + end: pthread_mutex_unlock(&(self->conn->lock)); Py_END_ALLOW_THREADS; - if (res < 0) + if (retvalue < 0) pq_complete_error(self->conn, &pgres, &error); - return res; + return retvalue; } diff --git a/psycopg/lobject_type.c b/psycopg/lobject_type.c index ebbcb1dc..690e4b6a 100644 --- a/psycopg/lobject_type.c +++ b/psycopg/lobject_type.c @@ -48,19 +48,17 @@ static PyObject * psyco_lobj_close(lobjectObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "")) return NULL; - + /* file-like objects can be closed multiple times and remember that - closing the current transaction is equivalent to close all the + closing the current transaction is equivalent to close all the opened large objects */ if (!self->closed && self->conn->isolation_level > 0 && self->conn->mark == self->mark) { - self->closed = 1; + Dprintf("psyco_lobj_close: closing lobject at %p", self); if (lobject_close(self) < 0) return NULL; - - Dprintf("psyco_lobj_close: lobject at %p closed", self); } Py_INCREF(Py_None); @@ -135,14 +133,14 @@ psyco_lobj_seek(lobjectObject *self, PyObject *args) if (!PyArg_ParseTuple(args, "i|i", &offset, &whence)) return NULL; - + EXC_IF_LOBJ_CLOSED(self); EXC_IF_LOBJ_LEVEL0(self); EXC_IF_LOBJ_UNMARKED(self); - + if ((pos = lobject_seek(self, offset, whence)) < 0) return NULL; - + return PyInt_FromLong((long)pos); } @@ -157,14 +155,14 @@ psyco_lobj_tell(lobjectObject *self, PyObject *args) int pos; if (!PyArg_ParseTuple(args, "")) return NULL; - + EXC_IF_LOBJ_CLOSED(self); EXC_IF_LOBJ_LEVEL0(self); EXC_IF_LOBJ_UNMARKED(self); - + if ((pos = lobject_tell(self)) < 0) return NULL; - + return PyInt_FromLong((long)pos); } @@ -177,10 +175,10 @@ static PyObject * psyco_lobj_unlink(lobjectObject *self, PyObject *args) { if (!PyArg_ParseTuple(args, "")) return NULL; - + if (lobject_unlink(self) < 0) return NULL; - + Py_INCREF(Py_None); return Py_None; } @@ -197,12 +195,14 @@ psyco_lobj_export(lobjectObject *self, PyObject *args) if (!PyArg_ParseTuple(args, "s", &filename)) return NULL; - + + EXC_IF_LOBJ_LEVEL0(self); + if (lobject_export(self, filename) < 0) return NULL; - + Py_INCREF(Py_None); - return Py_None; + return Py_None; } @@ -247,9 +247,9 @@ lobject_setup(lobjectObject *self, connectionObject *conn, Oid oid, int mode, Oid new_oid, char *new_file) { Dprintf("lobject_setup: init lobject object at %p", self); - + if (conn->isolation_level == 0) { - psyco_set_error(ProgrammingError, (PyObject*)self, + psyco_set_error(ProgrammingError, (PyObject*)self, "can't use a lobject outside of transactions", NULL, NULL); return -1; } @@ -258,14 +258,14 @@ lobject_setup(lobjectObject *self, connectionObject *conn, self->mark = conn->mark; Py_INCREF((PyObject*)self->conn); - - self->closed = 0; + + self->closed = 1; self->oid = InvalidOid; self->fd = -1; if (lobject_open(self, conn, oid, mode, new_oid, new_file) == -1) return -1; - + Dprintf("lobject_setup: good lobject object at %p, refcnt = %d", self, ((PyObject *)self)->ob_refcnt); Dprintf("lobject_setup: oid = %d, fd = %d", self->oid, self->fd); @@ -294,7 +294,7 @@ lobject_init(PyObject *obj, PyObject *args, PyObject *kwds) int mode=0; char *new_file = NULL; PyObject *conn; - + if (!PyArg_ParseTuple(args, "O|iiis", &conn, &oid, &mode, &new_oid, &new_file)) return -1; @@ -335,7 +335,7 @@ PyTypeObject lobjectType = { sizeof(lobjectObject), 0, lobject_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ + 0, /*tp_print*/ 0, /*tp_getattr*/ 0, /*tp_setattr*/ 0, /*tp_compare*/ @@ -353,7 +353,7 @@ PyTypeObject lobjectType = { Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE|Py_TPFLAGS_HAVE_ITER, /*tp_flags*/ lobjectType_doc, /*tp_doc*/ - + 0, /*tp_traverse*/ 0, /*tp_clear*/ @@ -370,11 +370,11 @@ PyTypeObject lobjectType = { 0, /*tp_getset*/ 0, /*tp_base*/ 0, /*tp_dict*/ - + 0, /*tp_descr_get*/ 0, /*tp_descr_set*/ 0, /*tp_dictoffset*/ - + lobject_init, /*tp_init*/ 0, /*tp_alloc Will be set to PyType_GenericAlloc in module init*/ lobject_new, /*tp_new*/ diff --git a/tests/test_lobject.py b/tests/test_lobject.py index 130cea8e..7f97f8f6 100644 --- a/tests/test_lobject.py +++ b/tests/test_lobject.py @@ -1,3 +1,6 @@ +import os +import shutil +import tempfile import unittest import psycopg2 @@ -10,10 +13,19 @@ class LargeObjectTests(unittest.TestCase): def setUp(self): self.conn = psycopg2.connect(tests.dsn) self.lo_oid = None + self.tmpdir = None def tearDown(self): + if self.tmpdir: + shutil.rmtree(self.tmpdir, ignore_errors=True) if self.lo_oid is not None: - self.conn.lobject(self.lo_oid).unlink() + self.conn.rollback() + try: + lo = self.conn.lobject(self.lo_oid, "n") + except psycopg2.OperationalError: + pass + else: + lo.unlink() def test_create(self): lo = self.conn.lobject() @@ -21,8 +33,11 @@ class LargeObjectTests(unittest.TestCase): self.assertEqual(lo.mode, "w") def test_open_non_existent(self): - # This test will give a false negative if Oid 42 is a large object. - self.assertRaises(psycopg2.OperationalError, self.conn.lobject, 42) + # By creating then removing a large object, we get an Oid that + # should be unused. + lo = self.conn.lobject() + lo.unlink() + self.assertRaises(psycopg2.OperationalError, self.conn.lobject, lo.oid) def test_open_existing(self): lo = self.conn.lobject() @@ -31,6 +46,47 @@ class LargeObjectTests(unittest.TestCase): self.assertEqual(lo2.oid, lo.oid) self.assertEqual(lo2.mode, "r") + def test_open_for_write(self): + lo = self.conn.lobject() + lo2 = self.conn.lobject(lo.oid, "w") + self.assertEqual(lo2.mode, "w") + lo2.write("some data") + + def test_open_mode_n(self): + # Openning an object in mode "n" gives us a closed lobject. + lo = self.conn.lobject() + lo.close() + + lo2 = self.conn.lobject(lo.oid, "n") + self.assertEqual(lo2.oid, lo.oid) + self.assertEqual(lo2.closed, True) + + def test_create_with_oid(self): + # Create and delete a large object to get an unused Oid. + lo = self.conn.lobject() + oid = lo.oid + lo.unlink() + + lo = self.conn.lobject(0, "w", oid) + self.assertEqual(lo.oid, oid) + + def test_create_with_existing_oid(self): + lo = self.conn.lobject() + lo.close() + + self.assertRaises(psycopg2.OperationalError, + self.conn.lobject, 0, "w", lo.oid) + + def test_import(self): + self.tmpdir = tempfile.mkdtemp() + filename = os.path.join(self.tmpdir, "data.txt") + fp = open(filename, "wb") + fp.write("some data") + fp.close() + + lo = self.conn.lobject(0, "r", 0, filename) + self.assertEqual(lo.read(), "some data") + def test_close(self): lo = self.conn.lobject() self.assertEqual(lo.closed, False) @@ -41,23 +97,6 @@ class LargeObjectTests(unittest.TestCase): lo = self.conn.lobject() self.assertEqual(lo.write("some data"), len("some data")) - def test_seek_tell(self): - lo = self.conn.lobject() - length = lo.write("some data") - self.assertEqual(lo.tell(), length) - lo.close() - lo = self.conn.lobject(lo.oid) - - self.assertEqual(lo.seek(5, 0), 5) - self.assertEqual(lo.tell(), 5) - - # SEEK_CUR: relative current location - self.assertEqual(lo.seek(2, 1), 7) - self.assertEqual(lo.tell(), 7) - - # SEEK_END: relative to end of file - self.assertEqual(lo.seek(-2, 2), length - 2) - def test_read(self): lo = self.conn.lobject() length = lo.write("some data") @@ -67,13 +106,142 @@ class LargeObjectTests(unittest.TestCase): self.assertEqual(lo.read(4), "some") self.assertEqual(lo.read(), " data") + def test_seek_tell(self): + lo = self.conn.lobject() + length = lo.write("some data") + self.assertEqual(lo.tell(), length) + lo.close() + lo = self.conn.lobject(lo.oid) + + self.assertEqual(lo.seek(5, 0), 5) + self.assertEqual(lo.tell(), 5) + self.assertEqual(lo.read(), "data") + + # SEEK_CUR: relative current location + lo.seek(5) + self.assertEqual(lo.seek(2, 1), 7) + self.assertEqual(lo.tell(), 7) + self.assertEqual(lo.read(), "ta") + + # SEEK_END: relative to end of file + self.assertEqual(lo.seek(-2, 2), length - 2) + self.assertEqual(lo.read(), "ta") + def test_unlink(self): lo = self.conn.lobject() - lo.close() lo.unlink() # the object doesn't exist now, so we can't reopen it. self.assertRaises(psycopg2.OperationalError, self.conn.lobject, lo.oid) + # And the object has been closed. + self.assertEquals(lo.closed, True) + + def test_export(self): + lo = self.conn.lobject() + lo.write("some data") + + self.tmpdir = tempfile.mkdtemp() + filename = os.path.join(self.tmpdir, "data.txt") + lo.export(filename) + self.assertTrue(os.path.exists(filename)) + self.assertEqual(open(filename, "rb").read(), "some data") + + def test_close_twice(self): + lo = self.conn.lobject() + lo.close() + lo.close() + + def test_write_after_close(self): + lo = self.conn.lobject() + lo.close() + self.assertRaises(psycopg2.InterfaceError, lo.write, "some data") + + def test_read_after_close(self): + lo = self.conn.lobject() + lo.close() + self.assertRaises(psycopg2.InterfaceError, lo.read, 5) + + def test_seek_after_close(self): + lo = self.conn.lobject() + lo.close() + self.assertRaises(psycopg2.InterfaceError, lo.seek, 0) + + def test_tell_after_close(self): + lo = self.conn.lobject() + lo.close() + self.assertRaises(psycopg2.InterfaceError, lo.tell) + + def test_unlink_after_close(self): + lo = self.conn.lobject() + lo.close() + # Unlink works on closed files. + lo.unlink() + + def test_export_after_close(self): + lo = self.conn.lobject() + lo.write("some data") + lo.close() + + self.tmpdir = tempfile.mkdtemp() + filename = os.path.join(self.tmpdir, "data.txt") + lo.export(filename) + self.assertTrue(os.path.exists(filename)) + self.assertEqual(open(filename, "rb").read(), "some data") + + def test_close_after_commit(self): + lo = self.conn.lobject() + self.lo_oid = lo.oid + self.conn.commit() + + # Closing outside of the transaction is okay. + lo.close() + + def test_write_after_commit(self): + lo = self.conn.lobject() + self.lo_oid = lo.oid + self.conn.commit() + + self.assertRaises(psycopg2.ProgrammingError, lo.write, "some data") + + def test_read_after_commit(self): + lo = self.conn.lobject() + self.lo_oid = lo.oid + self.conn.commit() + + self.assertRaises(psycopg2.ProgrammingError, lo.read, 5) + + def test_seek_after_commit(self): + lo = self.conn.lobject() + self.lo_oid = lo.oid + self.conn.commit() + + self.assertRaises(psycopg2.ProgrammingError, lo.seek, 0) + + def test_tell_after_commit(self): + lo = self.conn.lobject() + self.lo_oid = lo.oid + self.conn.commit() + + self.assertRaises(psycopg2.ProgrammingError, lo.tell) + + def test_unlink_after_commit(self): + lo = self.conn.lobject() + self.lo_oid = lo.oid + self.conn.commit() + + # Unlink of stale lobject is okay + lo.unlink() + + def test_export_after_commit(self): + lo = self.conn.lobject() + lo.write("some data") + self.conn.commit() + + self.tmpdir = tempfile.mkdtemp() + filename = os.path.join(self.tmpdir, "data.txt") + lo.export(filename) + self.assertTrue(os.path.exists(filename)) + self.assertEqual(open(filename, "rb").read(), "some data") def test_suite():