Merge lp:~allenap/postgresfixture/multi-version into lp:~lazr-developers/postgresfixture/trunk

Proposed by Gavin Panella
Status: Merged
Approved by: Graham Binns
Approved revision: 9
Merged at revision: 7
Proposed branch: lp:~allenap/postgresfixture/multi-version
Merge into: lp:~lazr-developers/postgresfixture/trunk
Diff against target: 361 lines (+131/-30)
6 files modified
postgresfixture/cluster.py (+30/-11)
postgresfixture/clusterfixture.py (+10/-7)
postgresfixture/main.py (+10/-2)
postgresfixture/tests/test_cluster.py (+24/-9)
postgresfixture/tests/test_main.py (+56/-1)
requirements.txt (+1/-0)
To merge this branch: bzr merge lp:~allenap/postgresfixture/multi-version
Reviewer Review Type Date Requested Status
Graham Binns (community) Approve
Review via email: mp+202089@code.launchpad.net

Commit message

Work with multiple version of PostgreSQL.

Previously only 9.1 was supported.

Description of the change

Get postgresfixture working with the most up-to-date PostgreSQL installation on a machine, by default, and allow selection of a different one.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Sweet. Imagine that, people hard coding dependency versions. You'd never get that at a company like Canonical.

Oh, wait...

review: Approve
10. By Gavin Panella

Use LooseVersion instead of float when comparing PostgreSQL versions.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'postgresfixture/cluster.py'
2--- postgresfixture/cluster.py 2012-07-16 20:00:28 +0000
3+++ postgresfixture/cluster.py 2014-01-17 14:25:09 +0000
4@@ -12,18 +12,22 @@
5 __metaclass__ = type
6 __all__ = [
7 "Cluster",
8+ "PG_VERSION_MAX",
9+ "PG_VERSIONS",
10 ]
11
12 from contextlib import (
13 closing,
14 contextmanager,
15 )
16+from distutils.version import LooseVersion
17 from fcntl import (
18 LOCK_EX,
19 LOCK_UN,
20 lockf,
21 )
22 from functools import wraps
23+from glob import iglob
24 from os import (
25 devnull,
26 environ,
27@@ -40,18 +44,32 @@
28 import psycopg2
29
30
31-PG_VERSION = "9.1"
32-PG_BIN = "/usr/lib/postgresql/%s/bin" % PG_VERSION
33-
34-
35-def path_with_pg_bin(exe_path):
36- """Return `exe_path` with `PG_BIN` added."""
37+PG_BASE = "/usr/lib/postgresql"
38+
39+PG_VERSION_BINS = {
40+ path.basename(pgdir): path.join(pgdir, "bin")
41+ for pgdir in iglob(path.join(PG_BASE, "*"))
42+ if path.exists(path.join(pgdir, "bin", "pg_ctl"))
43+}
44+
45+PG_VERSION_MAX = max(PG_VERSION_BINS, key=LooseVersion)
46+PG_VERSIONS = sorted(PG_VERSION_BINS, key=LooseVersion)
47+
48+
49+def get_pg_bin(version):
50+ """Return the PostgreSQL ``bin`` directory for the given `version`."""
51+ return PG_VERSION_BINS[version]
52+
53+
54+def path_with_pg_bin(exe_path, version):
55+ """Return `exe_path` with the PostgreSQL ``bin`` directory added."""
56 exe_path = [
57 elem for elem in exe_path.split(path.pathsep)
58 if len(elem) != 0 and not elem.isspace()
59 ]
60- if PG_BIN not in exe_path:
61- exe_path.insert(0, PG_BIN)
62+ pg_bin = get_pg_bin(version)
63+ if pg_bin not in exe_path:
64+ exe_path.insert(0, pg_bin)
65 return path.pathsep.join(exe_path)
66
67
68@@ -67,8 +85,9 @@
69 class Cluster:
70 """Represents a PostgreSQL cluster, running or not."""
71
72- def __init__(self, datadir):
73+ def __init__(self, datadir, version=PG_VERSION_MAX):
74 self.datadir = path.abspath(datadir)
75+ self.version = version
76 self.lockfile = path.join(
77 path.dirname(self.datadir),
78 ".%s.lock" % path.basename(self.datadir))
79@@ -92,7 +111,7 @@
80 def execute(self, *command, **options):
81 """Execute a command with an environment suitable for this cluster."""
82 env = options.pop("env", environ).copy()
83- env["PATH"] = path_with_pg_bin(env.get("PATH", ""))
84+ env["PATH"] = path_with_pg_bin(env.get("PATH", ""), self.version)
85 env["PGDATA"] = env["PGHOST"] = self.datadir
86 check_call(command, env=env, **options)
87
88@@ -121,7 +140,7 @@
89 with open(devnull, "wb") as null:
90 try:
91 self.execute("pg_ctl", "status", stdout=null)
92- except CalledProcessError, error:
93+ except CalledProcessError as error:
94 if error.returncode == 1:
95 return False
96 else:
97
98=== modified file 'postgresfixture/clusterfixture.py'
99--- postgresfixture/clusterfixture.py 2012-05-22 16:15:44 +0000
100+++ postgresfixture/clusterfixture.py 2014-01-17 14:25:09 +0000
101@@ -29,7 +29,10 @@
102 )
103
104 from fixtures import Fixture
105-from postgresfixture.cluster import Cluster
106+from postgresfixture.cluster import (
107+ Cluster,
108+ PG_VERSION_MAX,
109+ )
110
111
112 class ProcessSemaphore:
113@@ -49,7 +52,7 @@
114 def acquire(self):
115 try:
116 makedirs(self.lockdir)
117- except OSError, error:
118+ except OSError as error:
119 if error.errno != EEXIST:
120 raise
121 open(self.lockfile, "w").close()
122@@ -57,7 +60,7 @@
123 def release(self):
124 try:
125 unlink(self.lockfile)
126- except OSError, error:
127+ except OSError as error:
128 if error.errno != ENOENT:
129 raise
130
131@@ -65,7 +68,7 @@
132 def locked(self):
133 try:
134 rmdir(self.lockdir)
135- except OSError, error:
136+ except OSError as error:
137 if error.errno == ENOTEMPTY:
138 return True
139 elif error.errno == ENOENT:
140@@ -82,7 +85,7 @@
141 int(name) if name.isdigit() else name
142 for name in listdir(self.lockdir)
143 ]
144- except OSError, error:
145+ except OSError as error:
146 if error.errno == ENOENT:
147 return []
148 else:
149@@ -92,12 +95,12 @@
150 class ClusterFixture(Cluster, Fixture):
151 """A fixture for a `Cluster`."""
152
153- def __init__(self, datadir, preserve=False):
154+ def __init__(self, datadir, preserve=False, version=PG_VERSION_MAX):
155 """
156 @param preserve: Leave the cluster and its databases behind, even if
157 this fixture creates them.
158 """
159- super(ClusterFixture, self).__init__(datadir)
160+ super(ClusterFixture, self).__init__(datadir, version=version)
161 self.preserve = preserve
162 self.shares = ProcessSemaphore(
163 path.join(self.datadir, "shares"))
164
165=== modified file 'postgresfixture/main.py'
166--- postgresfixture/main.py 2012-05-22 21:51:08 +0000
167+++ postgresfixture/main.py 2014-01-17 14:25:09 +0000
168@@ -26,6 +26,10 @@
169 import sys
170 from time import sleep
171
172+from postgresfixture.cluster import (
173+ PG_VERSION_MAX,
174+ PG_VERSIONS,
175+ )
176 from postgresfixture.clusterfixture import ClusterFixture
177
178
179@@ -142,6 +146,10 @@
180 "preserve the cluster and its databases when exiting, "
181 "even if it was necessary to create and start it "
182 "(default: %(default)s)"))
183+argument_parser.add_argument(
184+ "--version", dest="version", choices=PG_VERSIONS,
185+ default=PG_VERSION_MAX, help=(
186+ "The version of PostgreSQL to use (default: %(default)s)"))
187 argument_subparsers = argument_parser.add_subparsers(
188 title="actions")
189
190@@ -191,9 +199,9 @@
191 setup()
192 cluster = ClusterFixture(
193 datadir=args.datadir,
194- preserve=args.preserve)
195+ preserve=args.preserve, version=args.version)
196 args.handler(cluster, args)
197- except CalledProcessError, error:
198+ except CalledProcessError as error:
199 raise SystemExit(error.returncode)
200 except KeyboardInterrupt:
201 pass
202
203=== modified file 'postgresfixture/tests/test_cluster.py'
204--- postgresfixture/tests/test_cluster.py 2012-05-22 22:27:47 +0000
205+++ postgresfixture/tests/test_cluster.py 2014-01-17 14:25:09 +0000
206@@ -28,11 +28,13 @@
207 import postgresfixture.cluster
208 from postgresfixture.cluster import (
209 Cluster,
210+ get_pg_bin,
211 path_with_pg_bin,
212- PG_BIN,
213+ PG_VERSIONS,
214 )
215 from postgresfixture.main import repr_pid
216 from postgresfixture.testing import TestCase
217+from testscenarios import WithScenarios
218 from testtools.content import text_content
219 from testtools.matchers import (
220 DirExists,
221@@ -42,13 +44,19 @@
222 )
223
224
225-class TestFunctions(TestCase):
226+class TestFunctions(WithScenarios, TestCase):
227+
228+ scenarios = sorted(
229+ (version, {"version": version})
230+ for version in PG_VERSIONS
231+ )
232
233 def test_path_with_pg_bin(self):
234- self.assertEqual(PG_BIN, path_with_pg_bin(""))
235+ pg_bin = get_pg_bin(self.version)
236+ self.assertEqual(pg_bin, path_with_pg_bin("", self.version))
237 self.assertEqual(
238- PG_BIN + path.pathsep + "/bin:/usr/bin",
239- path_with_pg_bin("/bin:/usr/bin"))
240+ pg_bin + path.pathsep + "/bin:/usr/bin",
241+ path_with_pg_bin("/bin:/usr/bin", self.version))
242
243 def test_repr_pid_not_a_number(self):
244 self.assertEqual("alice", repr_pid("alice"))
245@@ -62,9 +70,16 @@
246 self.assertThat(repr_pid(pid), StartsWith("%d (" % pid))
247
248
249-class TestCluster(TestCase):
250-
251- make = Cluster
252+class TestCluster(WithScenarios, TestCase):
253+
254+ scenarios = sorted(
255+ (version, {"version": version})
256+ for version in PG_VERSIONS
257+ )
258+
259+ def make(self, *args, **kwargs):
260+ kwargs.setdefault("version", self.version)
261+ return Cluster(*args, **kwargs)
262
263 def test_init(self):
264 # The datadir passed into the Cluster constructor is resolved to an
265@@ -133,7 +148,7 @@
266 self.assertEqual(cluster.datadir, env.get("PGHOST"))
267 self.assertThat(
268 env.get("PATH", ""),
269- StartsWith(PG_BIN + path.pathsep))
270+ StartsWith(get_pg_bin(self.version) + path.pathsep))
271
272 def test_exists(self):
273 cluster = self.make(self.make_dir())
274
275=== modified file 'postgresfixture/tests/test_main.py'
276--- postgresfixture/tests/test_main.py 2012-05-22 16:15:44 +0000
277+++ postgresfixture/tests/test_main.py 2014-01-17 14:25:09 +0000
278@@ -19,6 +19,10 @@
279
280 from fixtures import EnvironmentVariableFixture
281 from postgresfixture import main
282+from postgresfixture.cluster import (
283+ PG_VERSION_MAX,
284+ PG_VERSIONS,
285+ )
286 from postgresfixture.clusterfixture import ClusterFixture
287 from postgresfixture.testing import TestCase
288 from testtools.matchers import StartsWith
289@@ -35,7 +39,7 @@
290 def parse_args(self, *args):
291 try:
292 return main.argument_parser.parse_args(args)
293- except SystemExit, error:
294+ except SystemExit as error:
295 self.fail("parse_args%r failed with %r" % (args, error))
296
297 def test_run(self):
298@@ -174,3 +178,54 @@
299 sys.stderr.getvalue(), StartsWith(
300 "%s: cluster is locked by:" % cluster.datadir))
301 self.assertTrue(cluster.exists)
302+
303+
304+class TestVersion(TestCase):
305+
306+ def patch_pg_versions(self, versions):
307+ PG_VERSIONS[:] = versions
308+
309+ def test_uses_supplied_version(self):
310+ # Reset PG_VERSIONS after the test has run.
311+ self.addCleanup(self.patch_pg_versions, list(PG_VERSIONS))
312+ self.patch_pg_versions(["1.1", "2.2", "3.3"])
313+
314+ # Record calls to our patched handler.
315+ handler_calls = []
316+
317+ def handler(cluster, args):
318+ handler_calls.append((cluster, args))
319+
320+ self.patch(
321+ main.get_action("status"), "_defaults",
322+ {"handler": handler})
323+
324+ # Prevent main() from altering terminal settings.
325+ self.patch(main, "setup", lambda: None)
326+
327+ # The version chosen is picked up by the argument parser and
328+ # passed into the Cluster constructor.
329+ main.main(["--version", "2.2", "status"])
330+ self.assertEqual(
331+ [("2.2", "2.2")],
332+ [(cluster.version, args.version)
333+ for (cluster, args) in handler_calls])
334+
335+ def test_uses_default_version(self):
336+ # Record calls to our patched handler.
337+ handler_calls = []
338+
339+ def handler(cluster, args):
340+ handler_calls.append((cluster, args))
341+
342+ self.patch(
343+ main.get_action("status"), "_defaults",
344+ {"handler": handler})
345+
346+ # The argument parser has the default version and passes it into
347+ # the Cluster constructor.
348+ main.main(["status"])
349+ self.assertEqual(
350+ [(PG_VERSION_MAX, PG_VERSION_MAX)],
351+ [(cluster.version, args.version)
352+ for (cluster, args) in handler_calls])
353
354=== modified file 'requirements.txt'
355--- requirements.txt 2012-05-21 11:13:40 +0000
356+++ requirements.txt 2014-01-17 14:25:09 +0000
357@@ -1,3 +1,4 @@
358 fixtures >= 0.3.8
359 psycopg2 >= 2.4.4
360 testtools >= 0.9.14
361+testscenarios >= 0.4

Subscribers

People subscribed via source and target branches

to all changes: