/[thuban]/branches/WIP-pyshapelib-bramz/test/postgissupport.py
ViewVC logotype

Annotation of /branches/WIP-pyshapelib-bramz/test/postgissupport.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1605 - (hide annotations)
Tue Aug 19 11:00:40 2003 UTC (21 years, 6 months ago) by bh
Original Path: trunk/thuban/test/postgissupport.py
File MIME type: text/x-python
File size: 14834 byte(s)
Add very basic postgis database support and the corresponding test
cases. The test cases require a PostgreSQL + postgis installation
but no existing database. The database will be created
automatically by the test cases

* test/README: Add note about skipped tests and the requirements
of the postgis tests.

* Thuban/Model/postgisdb.py: New. Basic postgis database support.

* test/test_postgis_db.py: New. Test cases for the postgis
support.

* Thuban/Model/wellknowntext.py: New. Parser for well-known-text
format

* test/test_wellknowntext.py: New. Test cases for the
wellknowntext parser

* test/postgissupport.py: New. Support module for tests involving
a postgis database.

* test/support.py (execute_as_testsuite): Shut down the postmaster
if it's still running after the tests

* Thuban/Model/data.py (RAW_WKT): New constant for raw data in
well known text format

1 bh 1605 # Copyright (C) 2003 by Intevation GmbH
2     # Authors:
3     # Bernhard Herzog <[email protected]>
4     #
5     # This program is free software under the GPL (>=v2)
6     # Read the file COPYING coming with the software for details.
7    
8     """Support module for tests that use a live PostGIS database"""
9    
10     __version__ = "$Revision$"
11     # $Source$
12     # $Id$
13    
14     import sys
15     import os
16     import time
17     import popen2
18     import shutil
19     import traceback
20    
21     import support
22    
23     try:
24     import psycopg
25     except ImportError:
26     psycopg = None
27    
28     #
29     # Helper code
30     #
31    
32     def run_config_script(cmdline):
33     """Run command cmdline and return its stdout or none in case of errors"""
34     pipe = os.popen(cmdline)
35     result = pipe.read()
36     if pipe.close() is not None:
37     raise RuntimeError('Command %r failed' % cmdline)
38     return result
39    
40     def run_command(command, outfilename = None):
41     """Run command as a subprocess and send its stdout and stderr to outfile
42    
43     The subprocess is run synchroneously so the function returns once
44     the subprocess has termninated. If the process' exit code is not
45     zero raise a RuntimeError.
46    
47     If outfilename is None stdout and stderr are still captured but they
48     are ignored and not written to any file.
49     """
50     proc = popen2.Popen4(command)
51     proc.tochild.close()
52     output = proc.fromchild.read()
53     status = proc.wait()
54     if outfilename is not None:
55     outfile = open(outfilename, "w")
56     outfile.write(output)
57     outfile.close()
58     if not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 0:
59     if outfilename:
60     message = "see %s" % outfilename
61     else:
62     message = output
63     raise RuntimeError("command %r exited with code %d.\n%s"
64     % (command, status, message))
65    
66    
67     def run_boolean_command(command):
68     """
69     Run command as a subprocess silently and return whether it ran successfully
70    
71     Silently means that all output is captured and ignored. The exit
72     status is true if the command ran successfull, i.e. it terminated by
73     exiting and returned as zero exit code and false other wise
74     """
75     try:
76     run_command(command, None)
77     return 1
78     except RuntimeError:
79     pass
80     return 0
81    
82    
83     #
84     # PostgreSQL and database
85     #
86    
87     class PostgreSQLServer:
88    
89     """A PostgreSQL server
90    
91     Instances of this class represent a PostgreSQL server with postgis
92     extensions run explicitly for the test cases. Such a server has its
93     own database directory and its own directory for the unix sockets so
94     that it doesn't interfere with any other PostgreSQL server already
95     running on the system.
96     """
97    
98     def __init__(self, dbdir, port, postgis_sql, socket_dir):
99     """Initialize the PostgreSQLServer object
100    
101     Parameters:
102    
103     dbdir -- The directory for the databases
104     port -- The port to use
105     postgis_sql -- The name of the file with the SQL statements to
106     initialize a database for postgis.
107     socket_dir -- The directory for the socket files.
108    
109     When connecting to the database server use the port and host
110     instance variables.
111     """
112     self.dbdir = dbdir
113     self.port = port
114     self.postgis_sql = postgis_sql
115     self.socket_dir = socket_dir
116    
117     # For the client side the socket directory can be used as the
118     # host the name starts with a slash.
119     self.host = os.path.abspath(socket_dir)
120    
121     # Map db names to db objects
122     self.known_dbs = {}
123    
124     def createdb(self):
125     """Create the database in dbdir and start the server.
126    
127     First check whether the dbdir already exists and if necessary
128     stop an already running postmaster and remove the dbdir
129     directory completely. Then create a new database cluster in the
130     dbdir and start a postmaster.
131     """
132     if os.path.isdir(self.dbdir):
133     if self.is_running():
134     self.shutdown()
135     shutil.rmtree(self.dbdir)
136     os.mkdir(self.dbdir)
137    
138     run_command(["initdb", self.dbdir],
139     os.path.join(self.dbdir, "initdb.log"))
140    
141     extra_opts = "-p %d" % self.port
142     if self.socket_dir is not None:
143     extra_opts += " -k %s" % self.socket_dir
144     run_command(["pg_ctl", "-D", self.dbdir,
145     "-l", os.path.join(self.dbdir, "logfile"),
146     "-o", extra_opts, "start"],
147     os.path.join(self.dbdir, "pg_ctl-start.log"))
148     # the -w option of pg_ctl doesn't work properly when the port is
149     # not the default port, so we have to implement waiting for the
150     # server ourselves
151     self.wait_for_postmaster()
152    
153     def wait_for_postmaster(self):
154     """Return when the database server is running
155    
156     Internal method to wait until the postmaster process has been
157     started and is ready for client connections.
158     """
159     max_count = 60
160     count = 0
161     while count < max_count:
162     try:
163     run_command(["psql", "-l", "-p", str(self.port),
164     "-h", self.host],
165     os.path.join(self.dbdir, "psql-%d.log" % count))
166     except:
167     pass
168     else:
169     break
170     time.sleep(0.5)
171     count += 1
172     else:
173     raise RuntimeError("postmaster didn't start")
174    
175     def is_running(self):
176     """Return true a postmaster process is running on self.dbdir
177    
178     This method runs pg_ctl status on the dbdir so even if the
179     object has just been created it is possible that this method
180     returns true if there's still a postmaster process running for
181     self.dbdir.
182     """
183     return run_boolean_command(["pg_ctl", "-D", self.dbdir, "status"])
184    
185     def shutdown(self):
186     """Stop the postmaster running for self.dbdir"""
187     run_command(["pg_ctl", "-m", "fast", "-D", self.dbdir, "stop"],
188     os.path.join(self.dbdir, "pg_ctl-stop.log"))
189    
190     def new_postgis_db(self, dbname, tables = None):
191     """Create and return a new PostGISDatabase object using self as server
192     """
193     db = PostGISDatabase(self, self.postgis_sql, dbname, tables = tables)
194     db.initdb()
195     self.known_dbs[dbname] = db
196     return db
197    
198     def get_static_data_db(self, dbname, tables):
199     """Return a PostGISDatabase for a database with the given static data
200    
201     If no databasse of the name dbname exists, create a new one via
202     new_postgis_db and upload the data.
203    
204     If a database of the name dbname already exists and uses the
205     indicated data, return that. If the already existing db uses
206     different data raise a value error.
207    
208     The tables argument should be a sequence of table specifications
209     where each specifications is a (tablename, shapefilename) pair.
210     """
211     db = self.known_dbs.get(dbname)
212     if db is not None:
213     if db.has_data(tables):
214     return db
215     raise ValueError("PostGISDatabase named %r doesn't have tables %r"
216     % (dbname, tables))
217     return self.new_postgis_db(dbname, tables)
218    
219     def get_default_static_data_db(self):
220     dbname = "PostGISStaticTests"
221     tables = [("landmarks", os.path.join("..", "Data", "iceland",
222     "cultural_landmark-point.shp")),
223     ("political", os.path.join("..", "Data", "iceland",
224     "political.shp")),
225     ("roads", os.path.join("..", "Data", "iceland",
226     "roads-line.shp"))]
227     return self.get_static_data_db(dbname, tables)
228    
229    
230    
231    
232     class PostGISDatabase:
233    
234     """A PostGIS database in a PostgreSQLServer"""
235    
236     def __init__(self, server, postgis_sql, dbname, tables = None):
237     self.server = server
238     self.postgis_sql = postgis_sql
239     self.dbname = dbname
240     self.tables = tables
241    
242     def initdb(self):
243     """Remove the old db directory and create and initialize a new database
244     """
245     run_command(["createdb", "-p", str(self.server.port),
246     "-h", self.server.host, self.dbname],
247     os.path.join(self.server.dbdir, "createdb.log"))
248     run_command(["createlang", "-p", str(self.server.port),
249     "-h", self.server.host, "plpgsql", self.dbname],
250     os.path.join(self.server.dbdir, "createlang.log"))
251     # for some reason psql doesn't exit with an error code if the
252     # file given as -f doesn't exist, so we check manually by trying
253     # to open it before we run psql
254     f = open(self.postgis_sql)
255     f.close()
256     del f
257     run_command(["psql", "-f", self.postgis_sql, "-d", self.dbname,
258     "-p", str(self.server.port), "-h", self.server.host],
259     os.path.join(self.server.dbdir, "psql.log"))
260    
261     if self.tables is not None:
262     for tablename, shapefile in self.tables:
263     upload_shapefile(shapefile, self, tablename)
264    
265     def has_data(self, tables):
266     return self.tables == tables
267    
268    
269     def find_postgis_sql():
270     """Return the name of the postgis_sql file
271    
272     A postgis installation usually has the postgis_sql file in
273     PostgreSQL's datadir (i.e. the directory where PostgreSQL keeps
274     static files, not the directory containing the databases).
275     Unfortunately there's no way to determine the name of this directory
276     with pg_config so we assume here that it's
277     $bindir/../share/postgresql/.
278     """
279     bindir = run_config_script("pg_config --bindir").strip()
280     return os.path.join(bindir, "..", "share", "postgresql",
281     "contrib", "postgis.sql")
282    
283     _postgres_server = None
284     def get_test_server():
285     """Return the test database server object.
286    
287     If it doesn't exist yet, create it first.
288    
289     The server will use the directory postgis under the temp dir (as
290     defined by support.create_temp_dir()) for the database cluster.
291     Sockets will be created in tempdir.
292     """
293     global _postgres_server
294     if _postgres_server is None:
295     tempdir = support.create_temp_dir()
296     dbdir = os.path.join(tempdir, "postgis")
297     socket_dir = tempdir
298    
299     _postgres_server = PostgreSQLServer(dbdir, 6543, find_postgis_sql(),
300     socket_dir = socket_dir)
301     _postgres_server.createdb()
302    
303     return _postgres_server
304    
305     def shutdown_test_server():
306     """Shutdown the test server if it is running"""
307     global _postgres_server
308     if _postgres_server is not None:
309     _postgres_server.shutdown()
310     _postgres_server = None
311    
312    
313     def reason_for_not_running_tests():
314     """
315     Determine whether postgis tests can be run and return a reason they can't
316    
317     There's no fool-proof way to reliably determine this short of
318     actually running the tests but we try the following here:
319    
320     - test whether pg_ctl --help can be run successfully
321     - test whether the postgis_sql can be opened
322     The name of the postgis_sql file is determined by find_postgis_sql()
323     - psycopg can be imported successfully.
324     """
325     try:
326     run_command(["pg_ctl", "--help"], None)
327     except RuntimeError:
328     return "Can't run PostGIS tests because pg_ctl fails"
329    
330     try:
331     postgis_sql = find_postgis_sql()
332     except:
333     return "Can't run PostGIS tests because postgis.sql can't be found"
334    
335     try:
336     f = open(postgis_sql)
337     f.close()
338     except:
339     return "Can't run PostGIS tests because postgis.sql can't be opened"
340    
341     # The test for psycopg was already done when this module was
342     # imported so we only have to check whether it was successful
343     if psycopg is None:
344     return "Can't run PostGIS tests because psycopg can't be imported"
345    
346     return ""
347    
348    
349     _cannot_run_postgis_tests = None
350     def skip_if_no_postgis():
351     global _cannot_run_postgis_tests
352     if _cannot_run_postgis_tests is None:
353     _cannot_run_postgis_tests = reason_for_not_running_tests()
354     if _cannot_run_postgis_tests:
355     raise support.SkipTest(_cannot_run_postgis_tests)
356    
357     def point_to_wkt(coords):
358     """Return string with a WKT representation of the point in coords"""
359     x, y = coords[0]
360     return "POINT(%r %r)" % (x, y)
361    
362     def polygon_to_wkt(coords):
363     """Return string with a WKT representation of the polygon in coords"""
364     poly = []
365     for ring in coords:
366     poly.append(", ".join(["%r %r" % p for p in ring]))
367     return "POLYGON((%s))" % "), (".join(poly)
368    
369     def arc_to_wkt(coords):
370     """Return string with a WKT representation of the arc in coords"""
371     poly = []
372     for ring in coords:
373     poly.append(", ".join(["%r %r" % p for p in ring]))
374     return "MULTILINESTRING((%s))" % "), (".join(poly)
375    
376     def upload_shapefile(filename, db, tablename):
377     import dbflib, shapelib
378    
379     server = db.server
380     dbname = db.dbname
381     conn = psycopg.connect("host=%s port=%s dbname=%s"
382     % (server.host, server.port, dbname))
383     cursor = conn.cursor()
384    
385     shp = shapelib.ShapeFile(filename)
386     dbf = dbflib.DBFFile(filename)
387     typemap = {dbflib.FTString: "VARCHAR",
388     dbflib.FTInteger: "INTEGER",
389     dbflib.FTDouble: "DOUBLE PRECISION"}
390    
391     insert_formats = ["%(gid)s"]
392     fields = ["gid INT"]
393     for i in range(dbf.field_count()):
394     ftype, name, width, prec = dbf.field_info(i)
395     fields.append("%s %s" % (name, typemap[ftype]))
396     insert_formats.append("%%(%s)s" % name)
397     stmt = "CREATE TABLE %s (\n %s\n);" % (tablename,
398     ",\n ".join(fields))
399     cursor.execute(stmt)
400     #print stmt
401    
402     numshapes, shapetype, mins, maxs = shp.info()
403     if shapetype == shapelib.SHPT_POINT:
404     convert = point_to_wkt
405     wkttype = "POINT"
406     elif shapetype == shapelib.SHPT_POLYGON:
407     convert = polygon_to_wkt
408     wkttype = "POLYGON"
409     elif shapetype == shapelib.SHPT_ARC:
410     convert = arc_to_wkt
411     wkttype = "MULTILINESTRING"
412     else:
413     raise ValueError("Unsupported Shapetype %r" % shapetype)
414    
415     cursor.execute("select AddGeometryColumn('%(dbname)s',"
416     "'%(tablename)s', 'the_geom', '-1', '%(wkttype)s', 2);"
417     % locals())
418    
419     insert_formats.append("GeometryFromText(%(the_geom)s, -1)")
420    
421     insert = ("INSERT INTO %s VALUES (%s)"
422     % (tablename, ", ".join(insert_formats)))
423    
424     for i in range(numshapes):
425     data = dbf.read_record(i)
426     data["tablename"] = tablename
427     data["gid"] = i
428     data["the_geom"] = convert(shp.read_object(i).vertices())
429     #print insert % data
430     cursor.execute(insert, data)
431    
432     conn.commit()

Properties

Name Value
svn:eol-style native
svn:keywords Author Date Id Revision

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26