/[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 1634 - (hide 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 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     ("roads", os.path.join("..", "Data", "iceland",
237     "roads-line.shp"))]
238     return self.get_static_data_db(dbname, tables)
239    
240 bh 1634 def connection_params(self, user):
241     """Return the connection parameters for the given user
242 bh 1605
243 bh 1634 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 bh 1605
252 bh 1634 def connection_string(self, user):
253     """Return (part of) the connection string to pass to psycopg.connect
254 bh 1605
255 bh 1634 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 bh 1605 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 bh 1634 "-h", self.server.host, "-U", self.server.admin_name,
327     self.dbname],
328 bh 1605 os.path.join(self.server.dbdir, "createdb.log"))
329     run_command(["createlang", "-p", str(self.server.port),
330 bh 1634 "-h", self.server.host, "-U", self.server.admin_name,
331     "plpgsql", self.dbname],
332 bh 1605 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 bh 1634 "-p", str(self.server.port), "-h", self.server.host,
341     "-U", self.server.admin_name],
342 bh 1605 os.path.join(self.server.dbdir, "psql.log"))
343    
344 bh 1634 self.server.execute_sql(self.dbname, "admin",
345     "GRANT SELECT ON geometry_columns TO PUBLIC;")
346    
347 bh 1605 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 bh 1634 conn = psycopg.connect("dbname=%s " % dbname
468     + db.server.connection_string("admin"))
469 bh 1605 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 bh 1634 cursor.execute("GRANT SELECT ON %s TO PUBLIC;" % tablename)
519    
520 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