/[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 2057 - (show annotations)
Tue Feb 10 15:51:57 2004 UTC (21 years ago) by bh
Original Path: trunk/thuban/test/postgissupport.py
File MIME type: text/x-python
File size: 24449 byte(s)
* Thuban/Model/postgisdb.py (quote_identifier): Fix typo in
doc-string
(PostGISShapeStore._fetch_table_information): New. Extend
inherited method to retrieve srid
(PostGISShapeStore.BoundingBox): Handle tables without data.
extent yields NULL for those
(PostGISShapeStore.ShapesInRegion): Use the srid of the table.

* test/test_postgis_db.py
(TestPostGISSpecialCases.test_shapestore_empty_table): New test
for the special case of a table without any data
(TestPostGISShapestorePointSRID): New class with tests for a table
that uses srids
(PolygonTests): Fix a doc-string typo

* test/postgissupport.py (PostGISDatabase.__init__): New parameter
reference_systems with a specification of spacial reference
systems to create in the new db.
(PostgreSQLServer.new_postgis_db)
(PostgreSQLServer.get_static_data_db): New parameter
reference_systems to be passed through ultimately to
PostGISDatabase.  In new_postgis_db also check whether an existing
db already has the right srids
(PostgreSQLServer.get_default_static_data_db): Add srids and a
table that uses srids
(PostGISDatabase.initdb): Create the entries for the reference
systems
(PostGISDatabase.has_data): Add reference_systems parameter to
check for those too
(upload_shapefile): New parameter srid to create tables with a
specific srid

1 # Copyright (C) 2003, 2004 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 if the name starts with a slash.
119 self.host = os.path.abspath(socket_dir)
120
121 # 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 # 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 run_command(["initdb", "-D", self.dbdir, "-U", self.admin_name],
145 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 self.alter_user(self.admin_name, self.admin_password)
160 self.create_user(self.user_name, self.user_password)
161
162 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 "-h", self.host, "-U", self.admin_name],
174 os.path.join(self.dbdir, "psql-%d.log" % count))
175 except RuntimeError:
176 pass
177 except:
178 traceback.print_exc()
179 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, reference_systems = 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 reference_systems = reference_systems)
206 db.initdb()
207 self.known_dbs[dbname] = db
208 return db
209
210 def get_static_data_db(self, dbname, tables, reference_systems):
211 """Return a PostGISDatabase for a database with the given static data
212
213 If no databasse of the name dbname exists, create a new one via
214 new_postgis_db and upload the data.
215
216 If a database of the name dbname already exists and uses the
217 indicated data, return that. If the already existing db uses
218 different data raise a value error.
219
220 If the database doesn't exist, create a new one via
221 self.new_postgis_db.
222
223 The parameters tables and reference_systems have the same
224 meaning as for new_postgis_db.
225 """
226 db = self.known_dbs.get(dbname)
227 if db is not None:
228 if db.has_data(tables, reference_systems):
229 return db
230 raise ValueError("PostGISDatabase named %r doesn't have tables %r"
231 % (dbname, tables))
232 return self.new_postgis_db(dbname, tables, reference_systems)
233
234 def get_default_static_data_db(self):
235 dbname = "PostGISStaticTests"
236 srids = [(1, "proj=longlat datum=WGS84")]
237 tables = [
238 # Direct copies of the shapefiles. The shapeids are exactly
239 # the same, except where changed with "gid_offset", of
240 # course
241 ("landmarks", os.path.join("..", "Data", "iceland",
242 "cultural_landmark-point.shp"),
243 [("gid_offset", 1000)]),
244 ("political", os.path.join("..", "Data", "iceland",
245 "political.shp")),
246 ("roads", os.path.join("..", "Data", "iceland",
247 "roads-line.shp")),
248
249 # The polygon data as a MULTIPOLYGON geometry type
250 ("political_multi", os.path.join("..", "Data", "iceland",
251 "political.shp"),
252 [("force_wkt_type", "MULTIPOLYGON")]),
253
254 # Copy of landmarks but using an srid
255 ("landmarks_srid", os.path.join("..", "Data", "iceland",
256 "cultural_landmark-point.shp"),
257 [("gid_offset", 1000),
258 ("srid", 1)]),
259 ]
260 return self.get_static_data_db(dbname, tables, srids)
261
262 def connection_params(self, user):
263 """Return the connection parameters for the given user
264
265 The return value is a dictionary suitable as keyword argument
266 list to PostGISConnection. The user parameter may be either
267 'admin' to connect as admin or 'user' to connect as an
268 unprivileged user.
269 """
270 return {"host": self.host, "port": self.port,
271 "user": getattr(self, user + "_name"),
272 "password": getattr(self, user + "_password")}
273
274 def connection_string(self, user):
275 """Return (part of) the connection string to pass to psycopg.connect
276
277 The string contains host, port, user and password. The user
278 parameter must be either 'admin' or 'user', as for
279 connection_params.
280 """
281 params = []
282 for key, value in self.connection_params(user).items():
283 # FIXME: this doesn't do quiting correctly but that
284 # shouldn't be much of a problem (people shouldn't be using
285 # single quotes in filenames anyway :) )
286 params.append("%s='%s'" % (key, value))
287 return " ".join(params)
288
289 def execute_sql(self, dbname, user, sql):
290 """Execute the sql statament
291
292 The user parameter us used as in connection_params. The dbname
293 parameter must be the name of a database in the cluster.
294 """
295 conn = psycopg.connect("dbname=%s " % dbname
296 + self.connection_string(user))
297 cursor = conn.cursor()
298 cursor.execute(sql)
299 conn.commit()
300 conn.close()
301
302 def require_authentication(self, required):
303 """Switch authentication requirements on or off
304
305 When started for the first time no passwords are required. Some
306 tests want to explicitly test whether Thuban's password
307 infrastructure works and switch password authentication on
308 explicitly. When switching it on, there should be a
309 corresponding call to switch it off again in the test case'
310 tearDown method or in a finally: block.
311 """
312 if required:
313 contents = "local all password\n"
314 else:
315 contents = "local all trust\n"
316 f = open(os.path.join(self.dbdir, "pg_hba.conf"), "w")
317 f.write(contents)
318 f.close()
319 run_command(["pg_ctl", "-D", self.dbdir, "reload"],
320 os.path.join(self.dbdir, "pg_ctl-reload.log"))
321
322
323 def create_user(self, username, password):
324 """Create user username with password in the database"""
325 self.execute_sql("template1", "admin",
326 "CREATE USER %s PASSWORD '%s';" % (username,password))
327
328 def alter_user(self, username, password):
329 """Change the user username's password in the database"""
330 self.execute_sql("template1", "admin",
331 "ALTER USER %s PASSWORD '%s';" % (username,password))
332
333
334 class PostGISDatabase:
335
336 """A PostGIS database in a PostgreSQLServer"""
337
338 def __init__(self, server, postgis_sql, dbname, tables = None,
339 reference_systems = ()):
340 """Initialize the PostGISDatabase
341
342 Parameters:
343
344 server -- The PostgreSQLServer instance containing the
345 database
346
347 postgis_sql -- Filename of the postgis.sql file with the
348 postgis initialization code
349
350 dbname -- The name of the database
351
352 tables -- Optional description of tables to create in the
353 new database. If given it should be a list of
354 (tablename, shapefilename) pairs meaning that a table
355 tablename will be created with the contents of the given
356 shapefile or (tablename, shapefilename, extraargs)
357 triples. The extraargs should be a list of key, value
358 pairs to use as keyword arguments to upload_shapefile.
359
360 reference_systems -- Optional description of spatial
361 reference systems. If given, it should be a sequence of
362 (srid, params) pairs where srid is the srid defined by
363 the proj4 paramter string params. The srid can be given
364 as an extra parameter in the tables list.
365 """
366 self.server = server
367 self.postgis_sql = postgis_sql
368 self.dbname = dbname
369 self.tables = tables
370 if reference_systems:
371 self.reference_systems = reference_systems
372 else:
373 # Make sure that it's a sequence we can iterate over even if
374 # the parameter's None
375 self.reference_systems = ()
376
377 def initdb(self):
378 """Remove the old db directory and create and initialize a new database
379 """
380 run_command(["createdb", "-p", str(self.server.port),
381 "-h", self.server.host, "-U", self.server.admin_name,
382 self.dbname],
383 os.path.join(self.server.dbdir, "createdb.log"))
384 run_command(["createlang", "-p", str(self.server.port),
385 "-h", self.server.host, "-U", self.server.admin_name,
386 "plpgsql", self.dbname],
387 os.path.join(self.server.dbdir, "createlang.log"))
388 # for some reason psql doesn't exit with an error code if the
389 # file given as -f doesn't exist, so we check manually by trying
390 # to open it before we run psql
391 f = open(self.postgis_sql)
392 f.close()
393 del f
394 run_command(["psql", "-f", self.postgis_sql, "-d", self.dbname,
395 "-p", str(self.server.port), "-h", self.server.host,
396 "-U", self.server.admin_name],
397 os.path.join(self.server.dbdir, "psql.log"))
398
399 self.server.execute_sql(self.dbname, "admin",
400 "GRANT SELECT ON geometry_columns TO PUBLIC;")
401 self.server.execute_sql(self.dbname, "admin",
402 "GRANT SELECT ON spatial_ref_sys TO PUBLIC;")
403
404 for srid, params in self.reference_systems:
405 self.server.execute_sql(self.dbname, "admin",
406 "INSERT INTO spatial_ref_sys VALUES"
407 " (%d, '', %d, '', '%s');"
408 % (srid, srid, params))
409 if self.tables is not None:
410 def unpack(item):
411 extra = {"force_wkt_type": None, "gid_offset": 0,
412 "srid": -1}
413 if len(info) == 2:
414 tablename, shapefile = info
415 else:
416 tablename, shapefile, kw = info
417 for key, val in kw:
418 extra[key] = val
419 return tablename, shapefile, extra
420
421 for info in self.tables:
422 tablename, shapefile, kw = unpack(info)
423 upload_shapefile(shapefile, self, tablename, **kw)
424
425 def has_data(self, tables, reference_systems):
426 return (self.tables == tables
427 and self.reference_systems == reference_systems)
428
429
430 def find_postgis_sql():
431 """Return the name of the postgis_sql file
432
433 A postgis installation usually has the postgis_sql file in
434 PostgreSQL's datadir (i.e. the directory where PostgreSQL keeps
435 static files, not the directory containing the databases).
436 Unfortunately there's no way to determine the name of this directory
437 with pg_config so we assume here that it's
438 $bindir/../share/postgresql/.
439 """
440 bindir = run_config_script("pg_config --bindir").strip()
441 return os.path.join(bindir, "..", "share", "postgresql",
442 "contrib", "postgis.sql")
443
444 _postgres_server = None
445 def get_test_server():
446 """Return the test database server object.
447
448 If it doesn't exist yet, create it first.
449
450 The server will use the directory postgis under the temp dir (as
451 defined by support.create_temp_dir()) for the database cluster.
452 Sockets will be created in tempdir.
453 """
454 global _postgres_server
455 if _postgres_server is None:
456 tempdir = support.create_temp_dir()
457 dbdir = os.path.join(tempdir, "postgis")
458 socket_dir = tempdir
459
460 _postgres_server = PostgreSQLServer(dbdir, 6543, find_postgis_sql(),
461 socket_dir = socket_dir)
462 _postgres_server.createdb()
463
464 return _postgres_server
465
466 def shutdown_test_server():
467 """Shutdown the test server if it is running"""
468 global _postgres_server
469 if _postgres_server is not None:
470 _postgres_server.shutdown()
471 _postgres_server = None
472
473
474 def reason_for_not_running_tests():
475 """
476 Determine whether postgis tests can be run and return a reason they can't
477
478 There's no fool-proof way to reliably determine this short of
479 actually running the tests but we try the following here:
480
481 - test whether pg_ctl --help can be run successfully
482 - test whether the postgis_sql can be opened
483 The name of the postgis_sql file is determined by find_postgis_sql()
484 - psycopg can be imported successfully.
485 """
486 # run_command currently uses Popen4 which is not available under
487 # Windows, for example.
488 if not hasattr(popen2, "Popen4"):
489 return "Can't run PostGIS test because popen2.Popen4 does not exist"
490
491 try:
492 run_command(["pg_ctl", "--help"], None)
493 except RuntimeError:
494 return "Can't run PostGIS tests because pg_ctl fails"
495
496 try:
497 postgis_sql = find_postgis_sql()
498 except:
499 return "Can't run PostGIS tests because postgis.sql can't be found"
500
501 try:
502 f = open(postgis_sql)
503 f.close()
504 except:
505 return "Can't run PostGIS tests because postgis.sql can't be opened"
506
507 # The test for psycopg was already done when this module was
508 # imported so we only have to check whether it was successful
509 if psycopg is None:
510 return "Can't run PostGIS tests because psycopg can't be imported"
511
512 return ""
513
514
515 _cannot_run_postgis_tests = None
516 def skip_if_no_postgis():
517 global _cannot_run_postgis_tests
518 if _cannot_run_postgis_tests is None:
519 _cannot_run_postgis_tests = reason_for_not_running_tests()
520 if _cannot_run_postgis_tests:
521 raise support.SkipTest(_cannot_run_postgis_tests)
522
523 def skip_if_addgeometrycolumn_does_not_use_quote_ident():
524 """Skip a test if the AddGeometryColumn function doesn't use quote_ident
525
526 If the AddGeometryColumn function doesn't use quote_ident it doesn't
527 support unusual table or column names properly, that is, it will
528 fail with errors for names that contain spaces or double quotes.
529
530 The test performed by this function is a bit simplistic because it
531 only tests whether the string 'quote_ident' occurs anywhere in the
532 postgis.sql file. This will hopefully works because when this was
533 fixed in postgis CVS AddGeometryColumn was the first function to use
534 quote_ident.
535 """
536 f = file(find_postgis_sql())
537 content = f.read()
538 f.close()
539 if content.find("quote_ident") < 0:
540 raise support.SkipTest("AddGeometryColumn doesn't use quote_ident")
541
542 def coords_to_point(coords):
543 """Return string with a WKT representation of the point in coords"""
544 x, y = coords[0]
545 return "POINT(%r %r)" % (x, y)
546
547 def coords_to_polygon(coords):
548 """Return string with a WKT representation of the polygon in coords"""
549 poly = []
550 for ring in coords:
551 poly.append(", ".join(["%r %r" % p for p in ring]))
552 return "POLYGON((%s))" % "), (".join(poly)
553
554 def coords_to_multilinestring(coords):
555 """Return string with a WKT representation of the arc in coords"""
556 poly = []
557 for ring in coords:
558 poly.append(", ".join(["%r %r" % p for p in ring]))
559 return "MULTILINESTRING((%s))" % "), (".join(poly)
560
561 def coords_to_multipolygon(coords):
562 """Return string with a WKT representation of the polygon in coords"""
563 poly = []
564 for ring in coords:
565 poly.append(", ".join(["%r %r" % p for p in ring]))
566 return "MULTIPOLYGON(((%s)))" % ")), ((".join(poly)
567
568 wkt_converter = {
569 "POINT": coords_to_point,
570 "MULTILINESTRING": coords_to_multilinestring,
571 "POLYGON": coords_to_polygon,
572 "MULTIPOLYGON": coords_to_multipolygon,
573 }
574
575 def upload_shapefile(filename, db, tablename, force_wkt_type = None,
576 gid_offset = 0, srid = -1):
577 """Upload a shapefile into a new database table
578
579 Parameters:
580
581 filename -- The name of the shapefile
582
583 db -- The PostGISDatabase instance representing the database
584
585 tablename -- The name of the table to create and into which the data
586 is to be inserted
587
588 force_wkt_type -- If given the real WKT geometry type to use instead
589 of the default that would be chosen based on the type of
590 the shapefile
591
592 gid_offset -- A number to add to the shapeid to get the value for
593 the gid column (default 0)
594
595 srid -- The srid of the spatial references system used by the table
596 and the data
597 """
598 import dbflib, shapelib
599
600 # We build this map here because we need shapelib which can only be
601 # imported after support.initthuban has been called which we can't
602 # easily do in this module because it's imported by support.
603 shp_to_wkt = {
604 shapelib.SHPT_POINT: "POINT",
605 shapelib.SHPT_ARC: "MULTILINESTRING",
606 shapelib.SHPT_POLYGON: "POLYGON",
607 }
608
609 server = db.server
610 dbname = db.dbname
611 conn = psycopg.connect("dbname=%s " % dbname
612 + db.server.connection_string("admin"))
613 cursor = conn.cursor()
614
615 shp = shapelib.ShapeFile(filename)
616 dbf = dbflib.DBFFile(filename)
617 typemap = {dbflib.FTString: "VARCHAR",
618 dbflib.FTInteger: "INTEGER",
619 dbflib.FTDouble: "DOUBLE PRECISION"}
620
621 insert_formats = ["%(gid)s"]
622 fields = ["gid INT"]
623 for i in range(dbf.field_count()):
624 ftype, name, width, prec = dbf.field_info(i)
625 fields.append("%s %s" % (name, typemap[ftype]))
626 insert_formats.append("%%(%s)s" % name)
627 stmt = "CREATE TABLE %s (\n %s\n);" % (tablename,
628 ",\n ".join(fields))
629 cursor.execute(stmt)
630 #print stmt
631
632 numshapes, shapetype, mins, maxs = shp.info()
633 wkttype = shp_to_wkt[shapetype]
634 if force_wkt_type:
635 wkttype = force_wkt_type
636 convert = wkt_converter[wkttype]
637
638 cursor.execute("select AddGeometryColumn('%(dbname)s',"
639 "'%(tablename)s', 'the_geom', %(srid)d, '%(wkttype)s', 2);"
640 % locals())
641
642 insert_formats.append("GeometryFromText(%(the_geom)s, %(srid)d)")
643
644 insert = ("INSERT INTO %s VALUES (%s)"
645 % (tablename, ", ".join(insert_formats)))
646
647 for i in range(numshapes):
648 data = dbf.read_record(i)
649 data["tablename"] = tablename
650 data["gid"] = i + gid_offset
651 data["srid"] = srid
652 data["the_geom"] = convert(shp.read_object(i).vertices())
653 #print insert % data
654 cursor.execute(insert, data)
655
656 cursor.execute("GRANT SELECT ON %s TO PUBLIC;" % tablename)
657
658 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