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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1605 - (show 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 # 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