/[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 1634 - (show annotations)
Fri Aug 22 16:55:19 2003 UTC (21 years, 6 months ago) by bh
Original Path: trunk/thuban/test/postgissupport.py
File MIME type: text/x-python
File size: 18597 byte(s)
Prepare the test suite for tests with required authentication

* test/postgissupport.py (PostgreSQLServer.__init__): Add instance
variables with two predefined users/passwords, one for the admin
and one for a non-privileged user.
(PostgreSQLServer.createdb): Pass the admin name to initdb and add
the non-privileged user to the database and set the admin password
(PostgreSQLServer.wait_for_postmaster): Use the admin user name.
Better error reporting
(PostgreSQLServer.connection_params)
(PostgreSQLServer.connection_string): New methods to return
information about how to connect to the server
(PostgreSQLServer.execute_sql): New. Convenience method to execute
SQL statements
(PostgreSQLServer.require_authentication): Toggle whether the
server requires authentication
(PostgreSQLServer.create_user, PostgreSQLServer.alter_user): New.
Add or alter users
(PostGISDatabase.initdb): Pass the admin name one the
subprocesses' command lines. Grant select rights on
geometry_columns to everybody.
(upload_shapefile): Use the admin name and password when
connecting. Grant select rights on the new table to everybody.

* test/test_viewport.py (TestViewportWithPostGIS.setUp): Use the
server's new methods to get the connection parameters.

* test/test_postgis_session.py (TestSessionWithPostGIS.setUp)
(TestSessionWithPostGIS.test_remove_dbconn_exception): Use the
server's new methods to get the connection parameters.

* test/test_postgis_db.py
(TestPostGISConnection.test_gis_tables_empty)
(TestPostGISConnection.test_gis_tables_non_empty)
(PostGISStaticTests.setUp): Use the server's new methods to get
the connection parameters.

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 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):
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 ("roads", os.path.join("..", "Data", "iceland",
237 "roads-line.shp"))]
238 return self.get_static_data_db(dbname, tables)
239
240 def connection_params(self, user):
241 """Return the connection parameters for the given user
242
243 The return value is a dictionary suitable as keyword argument
244 list to PostGISConnection. The user parameter may be either
245 'admin' to connect as admin or 'user' to connect as an
246 unprivileged user.
247 """
248 return {"host": self.host, "port": self.port,
249 "user": getattr(self, user + "_name"),
250 "password": getattr(self, user + "_password")}
251
252 def connection_string(self, user):
253 """Return (part of) the connection string to pass to psycopg.connect
254
255 The string contains host, port, user and password. The user
256 parameter must be either 'admin' or 'user', as for
257 connection_params.
258 """
259 params = []
260 for key, value in self.connection_params(user).items():
261 # FIXME: this doesn't do quiting correctly but that
262 # shouldn't be much of a problem (people shouldn't be using
263 # single quotes in filenames anyway :) )
264 params.append("%s='%s'" % (key, value))
265 return " ".join(params)
266
267 def execute_sql(self, dbname, user, sql):
268 """Execute the sql statament
269
270 The user parameter us used as in connection_params. The dbname
271 parameter must be the name of a database in the cluster.
272 """
273 conn = psycopg.connect("dbname=%s " % dbname
274 + self.connection_string(user))
275 cursor = conn.cursor()
276 cursor.execute(sql)
277 conn.commit()
278 conn.close()
279
280 def require_authentication(self, required):
281 """Switch authentication requirements on or off
282
283 When started for the first time no passwords are required. Some
284 tests want to explicitly test whether Thuban's password
285 infrastructure works and switch password authentication on
286 explicitly. When switching it on, there should be a
287 corresponding call to switch it off again in the test case'
288 tearDown method or in a finally: block.
289 """
290 if required:
291 contents = "local all password\n"
292 else:
293 contents = "local all trust\n"
294 f = open(os.path.join(self.dbdir, "pg_hba.conf"), "w")
295 f.write(contents)
296 f.close()
297 run_command(["pg_ctl", "-D", self.dbdir, "reload"],
298 os.path.join(self.dbdir, "pg_ctl-reload.log"))
299
300
301 def create_user(self, username, password):
302 """Create user username with password in the database"""
303 self.execute_sql("template1", "admin",
304 "CREATE USER %s PASSWORD '%s';" % (username,password))
305
306 def alter_user(self, username, password):
307 """Change the user username's password in the database"""
308 self.execute_sql("template1", "admin",
309 "ALTER USER %s PASSWORD '%s';" % (username,password))
310
311
312 class PostGISDatabase:
313
314 """A PostGIS database in a PostgreSQLServer"""
315
316 def __init__(self, server, postgis_sql, dbname, tables = None):
317 self.server = server
318 self.postgis_sql = postgis_sql
319 self.dbname = dbname
320 self.tables = tables
321
322 def initdb(self):
323 """Remove the old db directory and create and initialize a new database
324 """
325 run_command(["createdb", "-p", str(self.server.port),
326 "-h", self.server.host, "-U", self.server.admin_name,
327 self.dbname],
328 os.path.join(self.server.dbdir, "createdb.log"))
329 run_command(["createlang", "-p", str(self.server.port),
330 "-h", self.server.host, "-U", self.server.admin_name,
331 "plpgsql", self.dbname],
332 os.path.join(self.server.dbdir, "createlang.log"))
333 # for some reason psql doesn't exit with an error code if the
334 # file given as -f doesn't exist, so we check manually by trying
335 # to open it before we run psql
336 f = open(self.postgis_sql)
337 f.close()
338 del f
339 run_command(["psql", "-f", self.postgis_sql, "-d", self.dbname,
340 "-p", str(self.server.port), "-h", self.server.host,
341 "-U", self.server.admin_name],
342 os.path.join(self.server.dbdir, "psql.log"))
343
344 self.server.execute_sql(self.dbname, "admin",
345 "GRANT SELECT ON geometry_columns TO PUBLIC;")
346
347 if self.tables is not None:
348 for tablename, shapefile in self.tables:
349 upload_shapefile(shapefile, self, tablename)
350
351 def has_data(self, tables):
352 return self.tables == tables
353
354
355 def find_postgis_sql():
356 """Return the name of the postgis_sql file
357
358 A postgis installation usually has the postgis_sql file in
359 PostgreSQL's datadir (i.e. the directory where PostgreSQL keeps
360 static files, not the directory containing the databases).
361 Unfortunately there's no way to determine the name of this directory
362 with pg_config so we assume here that it's
363 $bindir/../share/postgresql/.
364 """
365 bindir = run_config_script("pg_config --bindir").strip()
366 return os.path.join(bindir, "..", "share", "postgresql",
367 "contrib", "postgis.sql")
368
369 _postgres_server = None
370 def get_test_server():
371 """Return the test database server object.
372
373 If it doesn't exist yet, create it first.
374
375 The server will use the directory postgis under the temp dir (as
376 defined by support.create_temp_dir()) for the database cluster.
377 Sockets will be created in tempdir.
378 """
379 global _postgres_server
380 if _postgres_server is None:
381 tempdir = support.create_temp_dir()
382 dbdir = os.path.join(tempdir, "postgis")
383 socket_dir = tempdir
384
385 _postgres_server = PostgreSQLServer(dbdir, 6543, find_postgis_sql(),
386 socket_dir = socket_dir)
387 _postgres_server.createdb()
388
389 return _postgres_server
390
391 def shutdown_test_server():
392 """Shutdown the test server if it is running"""
393 global _postgres_server
394 if _postgres_server is not None:
395 _postgres_server.shutdown()
396 _postgres_server = None
397
398
399 def reason_for_not_running_tests():
400 """
401 Determine whether postgis tests can be run and return a reason they can't
402
403 There's no fool-proof way to reliably determine this short of
404 actually running the tests but we try the following here:
405
406 - test whether pg_ctl --help can be run successfully
407 - test whether the postgis_sql can be opened
408 The name of the postgis_sql file is determined by find_postgis_sql()
409 - psycopg can be imported successfully.
410 """
411 try:
412 run_command(["pg_ctl", "--help"], None)
413 except RuntimeError:
414 return "Can't run PostGIS tests because pg_ctl fails"
415
416 try:
417 postgis_sql = find_postgis_sql()
418 except:
419 return "Can't run PostGIS tests because postgis.sql can't be found"
420
421 try:
422 f = open(postgis_sql)
423 f.close()
424 except:
425 return "Can't run PostGIS tests because postgis.sql can't be opened"
426
427 # The test for psycopg was already done when this module was
428 # imported so we only have to check whether it was successful
429 if psycopg is None:
430 return "Can't run PostGIS tests because psycopg can't be imported"
431
432 return ""
433
434
435 _cannot_run_postgis_tests = None
436 def skip_if_no_postgis():
437 global _cannot_run_postgis_tests
438 if _cannot_run_postgis_tests is None:
439 _cannot_run_postgis_tests = reason_for_not_running_tests()
440 if _cannot_run_postgis_tests:
441 raise support.SkipTest(_cannot_run_postgis_tests)
442
443 def point_to_wkt(coords):
444 """Return string with a WKT representation of the point in coords"""
445 x, y = coords[0]
446 return "POINT(%r %r)" % (x, y)
447
448 def polygon_to_wkt(coords):
449 """Return string with a WKT representation of the polygon in coords"""
450 poly = []
451 for ring in coords:
452 poly.append(", ".join(["%r %r" % p for p in ring]))
453 return "POLYGON((%s))" % "), (".join(poly)
454
455 def arc_to_wkt(coords):
456 """Return string with a WKT representation of the arc in coords"""
457 poly = []
458 for ring in coords:
459 poly.append(", ".join(["%r %r" % p for p in ring]))
460 return "MULTILINESTRING((%s))" % "), (".join(poly)
461
462 def upload_shapefile(filename, db, tablename):
463 import dbflib, shapelib
464
465 server = db.server
466 dbname = db.dbname
467 conn = psycopg.connect("dbname=%s " % dbname
468 + db.server.connection_string("admin"))
469 cursor = conn.cursor()
470
471 shp = shapelib.ShapeFile(filename)
472 dbf = dbflib.DBFFile(filename)
473 typemap = {dbflib.FTString: "VARCHAR",
474 dbflib.FTInteger: "INTEGER",
475 dbflib.FTDouble: "DOUBLE PRECISION"}
476
477 insert_formats = ["%(gid)s"]
478 fields = ["gid INT"]
479 for i in range(dbf.field_count()):
480 ftype, name, width, prec = dbf.field_info(i)
481 fields.append("%s %s" % (name, typemap[ftype]))
482 insert_formats.append("%%(%s)s" % name)
483 stmt = "CREATE TABLE %s (\n %s\n);" % (tablename,
484 ",\n ".join(fields))
485 cursor.execute(stmt)
486 #print stmt
487
488 numshapes, shapetype, mins, maxs = shp.info()
489 if shapetype == shapelib.SHPT_POINT:
490 convert = point_to_wkt
491 wkttype = "POINT"
492 elif shapetype == shapelib.SHPT_POLYGON:
493 convert = polygon_to_wkt
494 wkttype = "POLYGON"
495 elif shapetype == shapelib.SHPT_ARC:
496 convert = arc_to_wkt
497 wkttype = "MULTILINESTRING"
498 else:
499 raise ValueError("Unsupported Shapetype %r" % shapetype)
500
501 cursor.execute("select AddGeometryColumn('%(dbname)s',"
502 "'%(tablename)s', 'the_geom', '-1', '%(wkttype)s', 2);"
503 % locals())
504
505 insert_formats.append("GeometryFromText(%(the_geom)s, -1)")
506
507 insert = ("INSERT INTO %s VALUES (%s)"
508 % (tablename, ", ".join(insert_formats)))
509
510 for i in range(numshapes):
511 data = dbf.read_record(i)
512 data["tablename"] = tablename
513 data["gid"] = i
514 data["the_geom"] = convert(shp.read_object(i).vertices())
515 #print insert % data
516 cursor.execute(insert, data)
517
518 cursor.execute("GRANT SELECT ON %s TO PUBLIC;" % tablename)
519
520 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