/[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 1656 - (hide annotations)
Mon Aug 25 18:26:54 2003 UTC (21 years, 6 months ago) by bh
Original Path: trunk/thuban/test/postgissupport.py
File MIME type: text/x-python
File size: 19689 byte(s)
* Thuban/Model/postgisdb.py (shapetype_map): Add MUTLIPOLYGON.

* test/test_postgis_db.py (PolygonTests): New class containing
those tests from TestPostGISShapestorePolygon that can also be
used to test MUTLIPOLYGON tables
(TestPostGISShapestorePolygon): Most tests are now in PolygonTests
so derive from that
(TestPostGISShapestoreMultiPolygon): New class with tests for
MUTLIPOLYGON tables

* test/postgissupport.py (PostGISDatabase.initdb): Allow the
tables argument to have tuples with three items to override the
WKT type used.
(PostgreSQLServer.get_default_static_data_db): Use the above to
create a polygon table with MUTLIPOLYGONs
(point_to_wkt, coords_to_point, polygon_to_wkt, coords_to_polygon)
(arc_to_wkt, coords_to_multilinestring): Rename from *_to_wkt to
coords_to*
(coords_to_multipolygon): New. Convert to MUTLIPOLYGON
(wkt_converter): New. Map WKT types to converters
(upload_shapefile): New parameter force_wkt_type to use a
different WKT type than the default

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 bh 1634 # host if the name starts with a slash.
119 bh 1605 self.host = os.path.abspath(socket_dir)
120    
121 bh 1634 # name and password for the admin and an unprivileged user
122     self.admin_name = "postgres"
123     self.admin_password = "postgres"
124     self.user_name = "observer"
125     self.user_password = "telescope"
126    
127 bh 1605 # Map db names to db objects
128     self.known_dbs = {}
129    
130     def createdb(self):
131     """Create the database in dbdir and start the server.
132    
133     First check whether the dbdir already exists and if necessary
134     stop an already running postmaster and remove the dbdir
135     directory completely. Then create a new database cluster in the
136     dbdir and start a postmaster.
137     """
138     if os.path.isdir(self.dbdir):
139     if self.is_running():
140     self.shutdown()
141     shutil.rmtree(self.dbdir)
142     os.mkdir(self.dbdir)
143    
144 bh 1634 run_command(["initdb", "-D", self.dbdir, "-U", self.admin_name],
145 bh 1605 os.path.join(self.dbdir, "initdb.log"))
146    
147     extra_opts = "-p %d" % self.port
148     if self.socket_dir is not None:
149     extra_opts += " -k %s" % self.socket_dir
150     run_command(["pg_ctl", "-D", self.dbdir,
151     "-l", os.path.join(self.dbdir, "logfile"),
152     "-o", extra_opts, "start"],
153     os.path.join(self.dbdir, "pg_ctl-start.log"))
154     # the -w option of pg_ctl doesn't work properly when the port is
155     # not the default port, so we have to implement waiting for the
156     # server ourselves
157     self.wait_for_postmaster()
158    
159 bh 1634 self.alter_user(self.admin_name, self.admin_password)
160     self.create_user(self.user_name, self.user_password)
161    
162 bh 1605 def wait_for_postmaster(self):
163     """Return when the database server is running
164    
165     Internal method to wait until the postmaster process has been
166     started and is ready for client connections.
167     """
168     max_count = 60
169     count = 0
170     while count < max_count:
171     try:
172     run_command(["psql", "-l", "-p", str(self.port),
173 bh 1634 "-h", self.host, "-U", self.admin_name],
174 bh 1605 os.path.join(self.dbdir, "psql-%d.log" % count))
175 bh 1634 except RuntimeError:
176     pass
177 bh 1605 except:
178 bh 1634 traceback.print_exc()
179 bh 1605 else:
180     break
181     time.sleep(0.5)
182     count += 1
183     else:
184     raise RuntimeError("postmaster didn't start")
185    
186     def is_running(self):
187     """Return true a postmaster process is running on self.dbdir
188    
189     This method runs pg_ctl status on the dbdir so even if the
190     object has just been created it is possible that this method
191     returns true if there's still a postmaster process running for
192     self.dbdir.
193     """
194     return run_boolean_command(["pg_ctl", "-D", self.dbdir, "status"])
195    
196     def shutdown(self):
197     """Stop the postmaster running for self.dbdir"""
198     run_command(["pg_ctl", "-m", "fast", "-D", self.dbdir, "stop"],
199     os.path.join(self.dbdir, "pg_ctl-stop.log"))
200    
201     def new_postgis_db(self, dbname, tables = None):
202     """Create and return a new PostGISDatabase object using self as server
203     """
204     db = PostGISDatabase(self, self.postgis_sql, dbname, tables = tables)
205     db.initdb()
206     self.known_dbs[dbname] = db
207     return db
208    
209     def get_static_data_db(self, dbname, tables):
210     """Return a PostGISDatabase for a database with the given static data
211    
212     If no databasse of the name dbname exists, create a new one via
213     new_postgis_db and upload the data.
214    
215     If a database of the name dbname already exists and uses the
216     indicated data, return that. If the already existing db uses
217     different data raise a value error.
218    
219     The tables argument should be a sequence of table specifications
220     where each specifications is a (tablename, shapefilename) pair.
221     """
222     db = self.known_dbs.get(dbname)
223     if db is not None:
224     if db.has_data(tables):
225     return db
226     raise ValueError("PostGISDatabase named %r doesn't have tables %r"
227     % (dbname, tables))
228     return self.new_postgis_db(dbname, tables)
229    
230     def get_default_static_data_db(self):
231     dbname = "PostGISStaticTests"
232     tables = [("landmarks", os.path.join("..", "Data", "iceland",
233     "cultural_landmark-point.shp")),
234     ("political", os.path.join("..", "Data", "iceland",
235     "political.shp")),
236 bh 1656
237     # The polygon data as a MULTIPOLYGON geometry type
238     ("political_multi", os.path.join("..", "Data", "iceland",
239     "political.shp"),
240     "MULTIPOLYGON"),
241    
242 bh 1605 ("roads", os.path.join("..", "Data", "iceland",
243     "roads-line.shp"))]
244     return self.get_static_data_db(dbname, tables)
245    
246 bh 1634 def connection_params(self, user):
247     """Return the connection parameters for the given user
248 bh 1605
249 bh 1634 The return value is a dictionary suitable as keyword argument
250     list to PostGISConnection. The user parameter may be either
251     'admin' to connect as admin or 'user' to connect as an
252     unprivileged user.
253     """
254     return {"host": self.host, "port": self.port,
255     "user": getattr(self, user + "_name"),
256     "password": getattr(self, user + "_password")}
257 bh 1605
258 bh 1634 def connection_string(self, user):
259     """Return (part of) the connection string to pass to psycopg.connect
260 bh 1605
261 bh 1634 The string contains host, port, user and password. The user
262     parameter must be either 'admin' or 'user', as for
263     connection_params.
264     """
265     params = []
266     for key, value in self.connection_params(user).items():
267     # FIXME: this doesn't do quiting correctly but that
268     # shouldn't be much of a problem (people shouldn't be using
269     # single quotes in filenames anyway :) )
270     params.append("%s='%s'" % (key, value))
271     return " ".join(params)
272    
273     def execute_sql(self, dbname, user, sql):
274     """Execute the sql statament
275    
276     The user parameter us used as in connection_params. The dbname
277     parameter must be the name of a database in the cluster.
278     """
279     conn = psycopg.connect("dbname=%s " % dbname
280     + self.connection_string(user))
281     cursor = conn.cursor()
282     cursor.execute(sql)
283     conn.commit()
284     conn.close()
285    
286     def require_authentication(self, required):
287     """Switch authentication requirements on or off
288    
289     When started for the first time no passwords are required. Some
290     tests want to explicitly test whether Thuban's password
291     infrastructure works and switch password authentication on
292     explicitly. When switching it on, there should be a
293     corresponding call to switch it off again in the test case'
294     tearDown method or in a finally: block.
295     """
296     if required:
297     contents = "local all password\n"
298     else:
299     contents = "local all trust\n"
300     f = open(os.path.join(self.dbdir, "pg_hba.conf"), "w")
301     f.write(contents)
302     f.close()
303     run_command(["pg_ctl", "-D", self.dbdir, "reload"],
304     os.path.join(self.dbdir, "pg_ctl-reload.log"))
305    
306    
307     def create_user(self, username, password):
308     """Create user username with password in the database"""
309     self.execute_sql("template1", "admin",
310     "CREATE USER %s PASSWORD '%s';" % (username,password))
311    
312     def alter_user(self, username, password):
313     """Change the user username's password in the database"""
314     self.execute_sql("template1", "admin",
315     "ALTER USER %s PASSWORD '%s';" % (username,password))
316    
317    
318 bh 1605 class PostGISDatabase:
319    
320     """A PostGIS database in a PostgreSQLServer"""
321    
322     def __init__(self, server, postgis_sql, dbname, tables = None):
323     self.server = server
324     self.postgis_sql = postgis_sql
325     self.dbname = dbname
326     self.tables = tables
327    
328     def initdb(self):
329     """Remove the old db directory and create and initialize a new database
330     """
331     run_command(["createdb", "-p", str(self.server.port),
332 bh 1634 "-h", self.server.host, "-U", self.server.admin_name,
333     self.dbname],
334 bh 1605 os.path.join(self.server.dbdir, "createdb.log"))
335     run_command(["createlang", "-p", str(self.server.port),
336 bh 1634 "-h", self.server.host, "-U", self.server.admin_name,
337     "plpgsql", self.dbname],
338 bh 1605 os.path.join(self.server.dbdir, "createlang.log"))
339     # for some reason psql doesn't exit with an error code if the
340     # file given as -f doesn't exist, so we check manually by trying
341     # to open it before we run psql
342     f = open(self.postgis_sql)
343     f.close()
344     del f
345     run_command(["psql", "-f", self.postgis_sql, "-d", self.dbname,
346 bh 1634 "-p", str(self.server.port), "-h", self.server.host,
347     "-U", self.server.admin_name],
348 bh 1605 os.path.join(self.server.dbdir, "psql.log"))
349    
350 bh 1634 self.server.execute_sql(self.dbname, "admin",
351     "GRANT SELECT ON geometry_columns TO PUBLIC;")
352    
353 bh 1605 if self.tables is not None:
354 bh 1656 for info in self.tables:
355     if len(info) == 2:
356     tablename, shapefile = info
357     wkt_type = None
358     else:
359     tablename, shapefile, wkt_type = info
360     upload_shapefile(shapefile, self, tablename,
361     force_wkt_type = wkt_type)
362 bh 1605
363     def has_data(self, tables):
364     return self.tables == tables
365    
366    
367     def find_postgis_sql():
368     """Return the name of the postgis_sql file
369    
370     A postgis installation usually has the postgis_sql file in
371     PostgreSQL's datadir (i.e. the directory where PostgreSQL keeps
372     static files, not the directory containing the databases).
373     Unfortunately there's no way to determine the name of this directory
374     with pg_config so we assume here that it's
375     $bindir/../share/postgresql/.
376     """
377     bindir = run_config_script("pg_config --bindir").strip()
378     return os.path.join(bindir, "..", "share", "postgresql",
379     "contrib", "postgis.sql")
380    
381     _postgres_server = None
382     def get_test_server():
383     """Return the test database server object.
384    
385     If it doesn't exist yet, create it first.
386    
387     The server will use the directory postgis under the temp dir (as
388     defined by support.create_temp_dir()) for the database cluster.
389     Sockets will be created in tempdir.
390     """
391     global _postgres_server
392     if _postgres_server is None:
393     tempdir = support.create_temp_dir()
394     dbdir = os.path.join(tempdir, "postgis")
395     socket_dir = tempdir
396    
397     _postgres_server = PostgreSQLServer(dbdir, 6543, find_postgis_sql(),
398     socket_dir = socket_dir)
399     _postgres_server.createdb()
400    
401     return _postgres_server
402    
403     def shutdown_test_server():
404     """Shutdown the test server if it is running"""
405     global _postgres_server
406     if _postgres_server is not None:
407     _postgres_server.shutdown()
408     _postgres_server = None
409    
410    
411     def reason_for_not_running_tests():
412     """
413     Determine whether postgis tests can be run and return a reason they can't
414    
415     There's no fool-proof way to reliably determine this short of
416     actually running the tests but we try the following here:
417    
418     - test whether pg_ctl --help can be run successfully
419     - test whether the postgis_sql can be opened
420     The name of the postgis_sql file is determined by find_postgis_sql()
421     - psycopg can be imported successfully.
422     """
423     try:
424     run_command(["pg_ctl", "--help"], None)
425     except RuntimeError:
426     return "Can't run PostGIS tests because pg_ctl fails"
427    
428     try:
429     postgis_sql = find_postgis_sql()
430     except:
431     return "Can't run PostGIS tests because postgis.sql can't be found"
432    
433     try:
434     f = open(postgis_sql)
435     f.close()
436     except:
437     return "Can't run PostGIS tests because postgis.sql can't be opened"
438    
439     # The test for psycopg was already done when this module was
440     # imported so we only have to check whether it was successful
441     if psycopg is None:
442     return "Can't run PostGIS tests because psycopg can't be imported"
443    
444     return ""
445    
446    
447     _cannot_run_postgis_tests = None
448     def skip_if_no_postgis():
449     global _cannot_run_postgis_tests
450     if _cannot_run_postgis_tests is None:
451     _cannot_run_postgis_tests = reason_for_not_running_tests()
452     if _cannot_run_postgis_tests:
453     raise support.SkipTest(_cannot_run_postgis_tests)
454    
455 bh 1656 def coords_to_point(coords):
456 bh 1605 """Return string with a WKT representation of the point in coords"""
457     x, y = coords[0]
458     return "POINT(%r %r)" % (x, y)
459    
460 bh 1656 def coords_to_polygon(coords):
461 bh 1605 """Return string with a WKT representation of the polygon in coords"""
462     poly = []
463     for ring in coords:
464     poly.append(", ".join(["%r %r" % p for p in ring]))
465     return "POLYGON((%s))" % "), (".join(poly)
466    
467 bh 1656 def coords_to_multilinestring(coords):
468 bh 1605 """Return string with a WKT representation of the arc in coords"""
469     poly = []
470     for ring in coords:
471     poly.append(", ".join(["%r %r" % p for p in ring]))
472     return "MULTILINESTRING((%s))" % "), (".join(poly)
473    
474 bh 1656 def coords_to_multipolygon(coords):
475     """Return string with a WKT representation of the polygon in coords"""
476     poly = []
477     for ring in coords:
478     poly.append(", ".join(["%r %r" % p for p in ring]))
479     return "MULTIPOLYGON(((%s)))" % ")), ((".join(poly)
480    
481     wkt_converter = {
482     "POINT": coords_to_point,
483     "MULTILINESTRING": coords_to_multilinestring,
484     "POLYGON": coords_to_polygon,
485     "MULTIPOLYGON": coords_to_multipolygon,
486     }
487    
488     def upload_shapefile(filename, db, tablename, force_wkt_type = None):
489 bh 1605 import dbflib, shapelib
490    
491 bh 1656 # We build this map here because we need shapelib which can only be
492     # imported after support.initthuban has been called which we can't
493     # easily do in this module because it's imported by support.
494     shp_to_wkt = {
495     shapelib.SHPT_POINT: "POINT",
496     shapelib.SHPT_ARC: "MULTILINESTRING",
497     shapelib.SHPT_POLYGON: "POLYGON",
498     }
499    
500 bh 1605 server = db.server
501     dbname = db.dbname
502 bh 1634 conn = psycopg.connect("dbname=%s " % dbname
503     + db.server.connection_string("admin"))
504 bh 1605 cursor = conn.cursor()
505    
506     shp = shapelib.ShapeFile(filename)
507     dbf = dbflib.DBFFile(filename)
508     typemap = {dbflib.FTString: "VARCHAR",
509     dbflib.FTInteger: "INTEGER",
510     dbflib.FTDouble: "DOUBLE PRECISION"}
511    
512     insert_formats = ["%(gid)s"]
513     fields = ["gid INT"]
514     for i in range(dbf.field_count()):
515     ftype, name, width, prec = dbf.field_info(i)
516     fields.append("%s %s" % (name, typemap[ftype]))
517     insert_formats.append("%%(%s)s" % name)
518     stmt = "CREATE TABLE %s (\n %s\n);" % (tablename,
519     ",\n ".join(fields))
520     cursor.execute(stmt)
521     #print stmt
522    
523     numshapes, shapetype, mins, maxs = shp.info()
524 bh 1656 wkttype = shp_to_wkt[shapetype]
525     if force_wkt_type:
526     wkttype = force_wkt_type
527     convert = wkt_converter[wkttype]
528 bh 1605
529     cursor.execute("select AddGeometryColumn('%(dbname)s',"
530     "'%(tablename)s', 'the_geom', '-1', '%(wkttype)s', 2);"
531     % locals())
532    
533     insert_formats.append("GeometryFromText(%(the_geom)s, -1)")
534    
535     insert = ("INSERT INTO %s VALUES (%s)"
536     % (tablename, ", ".join(insert_formats)))
537    
538     for i in range(numshapes):
539     data = dbf.read_record(i)
540     data["tablename"] = tablename
541     data["gid"] = i
542     data["the_geom"] = convert(shp.read_object(i).vertices())
543     #print insert % data
544     cursor.execute(insert, data)
545    
546 bh 1634 cursor.execute("GRANT SELECT ON %s TO PUBLIC;" % tablename)
547    
548 bh 1605 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