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
=== modified file 'postgresfixture/cluster.py'
--- postgresfixture/cluster.py 2012-07-16 20:00:28 +0000
+++ postgresfixture/cluster.py 2014-01-17 14:25:09 +0000
@@ -12,18 +12,22 @@
12__metaclass__ = type12__metaclass__ = type
13__all__ = [13__all__ = [
14 "Cluster",14 "Cluster",
15 "PG_VERSION_MAX",
16 "PG_VERSIONS",
15 ]17 ]
1618
17from contextlib import (19from contextlib import (
18 closing,20 closing,
19 contextmanager,21 contextmanager,
20 )22 )
23from distutils.version import LooseVersion
21from fcntl import (24from fcntl import (
22 LOCK_EX,25 LOCK_EX,
23 LOCK_UN,26 LOCK_UN,
24 lockf,27 lockf,
25 )28 )
26from functools import wraps29from functools import wraps
30from glob import iglob
27from os import (31from os import (
28 devnull,32 devnull,
29 environ,33 environ,
@@ -40,18 +44,32 @@
40import psycopg244import psycopg2
4145
4246
43PG_VERSION = "9.1"47PG_BASE = "/usr/lib/postgresql"
44PG_BIN = "/usr/lib/postgresql/%s/bin" % PG_VERSION48
4549PG_VERSION_BINS = {
4650 path.basename(pgdir): path.join(pgdir, "bin")
47def path_with_pg_bin(exe_path):51 for pgdir in iglob(path.join(PG_BASE, "*"))
48 """Return `exe_path` with `PG_BIN` added."""52 if path.exists(path.join(pgdir, "bin", "pg_ctl"))
53}
54
55PG_VERSION_MAX = max(PG_VERSION_BINS, key=LooseVersion)
56PG_VERSIONS = sorted(PG_VERSION_BINS, key=LooseVersion)
57
58
59def get_pg_bin(version):
60 """Return the PostgreSQL ``bin`` directory for the given `version`."""
61 return PG_VERSION_BINS[version]
62
63
64def path_with_pg_bin(exe_path, version):
65 """Return `exe_path` with the PostgreSQL ``bin`` directory added."""
49 exe_path = [66 exe_path = [
50 elem for elem in exe_path.split(path.pathsep)67 elem for elem in exe_path.split(path.pathsep)
51 if len(elem) != 0 and not elem.isspace()68 if len(elem) != 0 and not elem.isspace()
52 ]69 ]
53 if PG_BIN not in exe_path:70 pg_bin = get_pg_bin(version)
54 exe_path.insert(0, PG_BIN)71 if pg_bin not in exe_path:
72 exe_path.insert(0, pg_bin)
55 return path.pathsep.join(exe_path)73 return path.pathsep.join(exe_path)
5674
5775
@@ -67,8 +85,9 @@
67class Cluster:85class Cluster:
68 """Represents a PostgreSQL cluster, running or not."""86 """Represents a PostgreSQL cluster, running or not."""
6987
70 def __init__(self, datadir):88 def __init__(self, datadir, version=PG_VERSION_MAX):
71 self.datadir = path.abspath(datadir)89 self.datadir = path.abspath(datadir)
90 self.version = version
72 self.lockfile = path.join(91 self.lockfile = path.join(
73 path.dirname(self.datadir),92 path.dirname(self.datadir),
74 ".%s.lock" % path.basename(self.datadir))93 ".%s.lock" % path.basename(self.datadir))
@@ -92,7 +111,7 @@
92 def execute(self, *command, **options):111 def execute(self, *command, **options):
93 """Execute a command with an environment suitable for this cluster."""112 """Execute a command with an environment suitable for this cluster."""
94 env = options.pop("env", environ).copy()113 env = options.pop("env", environ).copy()
95 env["PATH"] = path_with_pg_bin(env.get("PATH", ""))114 env["PATH"] = path_with_pg_bin(env.get("PATH", ""), self.version)
96 env["PGDATA"] = env["PGHOST"] = self.datadir115 env["PGDATA"] = env["PGHOST"] = self.datadir
97 check_call(command, env=env, **options)116 check_call(command, env=env, **options)
98117
@@ -121,7 +140,7 @@
121 with open(devnull, "wb") as null:140 with open(devnull, "wb") as null:
122 try:141 try:
123 self.execute("pg_ctl", "status", stdout=null)142 self.execute("pg_ctl", "status", stdout=null)
124 except CalledProcessError, error:143 except CalledProcessError as error:
125 if error.returncode == 1:144 if error.returncode == 1:
126 return False145 return False
127 else:146 else:
128147
=== modified file 'postgresfixture/clusterfixture.py'
--- postgresfixture/clusterfixture.py 2012-05-22 16:15:44 +0000
+++ postgresfixture/clusterfixture.py 2014-01-17 14:25:09 +0000
@@ -29,7 +29,10 @@
29 )29 )
3030
31from fixtures import Fixture31from fixtures import Fixture
32from postgresfixture.cluster import Cluster32from postgresfixture.cluster import (
33 Cluster,
34 PG_VERSION_MAX,
35 )
3336
3437
35class ProcessSemaphore:38class ProcessSemaphore:
@@ -49,7 +52,7 @@
49 def acquire(self):52 def acquire(self):
50 try:53 try:
51 makedirs(self.lockdir)54 makedirs(self.lockdir)
52 except OSError, error:55 except OSError as error:
53 if error.errno != EEXIST:56 if error.errno != EEXIST:
54 raise57 raise
55 open(self.lockfile, "w").close()58 open(self.lockfile, "w").close()
@@ -57,7 +60,7 @@
57 def release(self):60 def release(self):
58 try:61 try:
59 unlink(self.lockfile)62 unlink(self.lockfile)
60 except OSError, error:63 except OSError as error:
61 if error.errno != ENOENT:64 if error.errno != ENOENT:
62 raise65 raise
6366
@@ -65,7 +68,7 @@
65 def locked(self):68 def locked(self):
66 try:69 try:
67 rmdir(self.lockdir)70 rmdir(self.lockdir)
68 except OSError, error:71 except OSError as error:
69 if error.errno == ENOTEMPTY:72 if error.errno == ENOTEMPTY:
70 return True73 return True
71 elif error.errno == ENOENT:74 elif error.errno == ENOENT:
@@ -82,7 +85,7 @@
82 int(name) if name.isdigit() else name85 int(name) if name.isdigit() else name
83 for name in listdir(self.lockdir)86 for name in listdir(self.lockdir)
84 ]87 ]
85 except OSError, error:88 except OSError as error:
86 if error.errno == ENOENT:89 if error.errno == ENOENT:
87 return []90 return []
88 else:91 else:
@@ -92,12 +95,12 @@
92class ClusterFixture(Cluster, Fixture):95class ClusterFixture(Cluster, Fixture):
93 """A fixture for a `Cluster`."""96 """A fixture for a `Cluster`."""
9497
95 def __init__(self, datadir, preserve=False):98 def __init__(self, datadir, preserve=False, version=PG_VERSION_MAX):
96 """99 """
97 @param preserve: Leave the cluster and its databases behind, even if100 @param preserve: Leave the cluster and its databases behind, even if
98 this fixture creates them.101 this fixture creates them.
99 """102 """
100 super(ClusterFixture, self).__init__(datadir)103 super(ClusterFixture, self).__init__(datadir, version=version)
101 self.preserve = preserve104 self.preserve = preserve
102 self.shares = ProcessSemaphore(105 self.shares = ProcessSemaphore(
103 path.join(self.datadir, "shares"))106 path.join(self.datadir, "shares"))
104107
=== modified file 'postgresfixture/main.py'
--- postgresfixture/main.py 2012-05-22 21:51:08 +0000
+++ postgresfixture/main.py 2014-01-17 14:25:09 +0000
@@ -26,6 +26,10 @@
26import sys26import sys
27from time import sleep27from time import sleep
2828
29from postgresfixture.cluster import (
30 PG_VERSION_MAX,
31 PG_VERSIONS,
32 )
29from postgresfixture.clusterfixture import ClusterFixture33from postgresfixture.clusterfixture import ClusterFixture
3034
3135
@@ -142,6 +146,10 @@
142 "preserve the cluster and its databases when exiting, "146 "preserve the cluster and its databases when exiting, "
143 "even if it was necessary to create and start it "147 "even if it was necessary to create and start it "
144 "(default: %(default)s)"))148 "(default: %(default)s)"))
149argument_parser.add_argument(
150 "--version", dest="version", choices=PG_VERSIONS,
151 default=PG_VERSION_MAX, help=(
152 "The version of PostgreSQL to use (default: %(default)s)"))
145argument_subparsers = argument_parser.add_subparsers(153argument_subparsers = argument_parser.add_subparsers(
146 title="actions")154 title="actions")
147155
@@ -191,9 +199,9 @@
191 setup()199 setup()
192 cluster = ClusterFixture(200 cluster = ClusterFixture(
193 datadir=args.datadir,201 datadir=args.datadir,
194 preserve=args.preserve)202 preserve=args.preserve, version=args.version)
195 args.handler(cluster, args)203 args.handler(cluster, args)
196 except CalledProcessError, error:204 except CalledProcessError as error:
197 raise SystemExit(error.returncode)205 raise SystemExit(error.returncode)
198 except KeyboardInterrupt:206 except KeyboardInterrupt:
199 pass207 pass
200208
=== modified file 'postgresfixture/tests/test_cluster.py'
--- postgresfixture/tests/test_cluster.py 2012-05-22 22:27:47 +0000
+++ postgresfixture/tests/test_cluster.py 2014-01-17 14:25:09 +0000
@@ -28,11 +28,13 @@
28import postgresfixture.cluster28import postgresfixture.cluster
29from postgresfixture.cluster import (29from postgresfixture.cluster import (
30 Cluster,30 Cluster,
31 get_pg_bin,
31 path_with_pg_bin,32 path_with_pg_bin,
32 PG_BIN,33 PG_VERSIONS,
33 )34 )
34from postgresfixture.main import repr_pid35from postgresfixture.main import repr_pid
35from postgresfixture.testing import TestCase36from postgresfixture.testing import TestCase
37from testscenarios import WithScenarios
36from testtools.content import text_content38from testtools.content import text_content
37from testtools.matchers import (39from testtools.matchers import (
38 DirExists,40 DirExists,
@@ -42,13 +44,19 @@
42 )44 )
4345
4446
45class TestFunctions(TestCase):47class TestFunctions(WithScenarios, TestCase):
48
49 scenarios = sorted(
50 (version, {"version": version})
51 for version in PG_VERSIONS
52 )
4653
47 def test_path_with_pg_bin(self):54 def test_path_with_pg_bin(self):
48 self.assertEqual(PG_BIN, path_with_pg_bin(""))55 pg_bin = get_pg_bin(self.version)
56 self.assertEqual(pg_bin, path_with_pg_bin("", self.version))
49 self.assertEqual(57 self.assertEqual(
50 PG_BIN + path.pathsep + "/bin:/usr/bin",58 pg_bin + path.pathsep + "/bin:/usr/bin",
51 path_with_pg_bin("/bin:/usr/bin"))59 path_with_pg_bin("/bin:/usr/bin", self.version))
5260
53 def test_repr_pid_not_a_number(self):61 def test_repr_pid_not_a_number(self):
54 self.assertEqual("alice", repr_pid("alice"))62 self.assertEqual("alice", repr_pid("alice"))
@@ -62,9 +70,16 @@
62 self.assertThat(repr_pid(pid), StartsWith("%d (" % pid))70 self.assertThat(repr_pid(pid), StartsWith("%d (" % pid))
6371
6472
65class TestCluster(TestCase):73class TestCluster(WithScenarios, TestCase):
6674
67 make = Cluster75 scenarios = sorted(
76 (version, {"version": version})
77 for version in PG_VERSIONS
78 )
79
80 def make(self, *args, **kwargs):
81 kwargs.setdefault("version", self.version)
82 return Cluster(*args, **kwargs)
6883
69 def test_init(self):84 def test_init(self):
70 # The datadir passed into the Cluster constructor is resolved to an85 # The datadir passed into the Cluster constructor is resolved to an
@@ -133,7 +148,7 @@
133 self.assertEqual(cluster.datadir, env.get("PGHOST"))148 self.assertEqual(cluster.datadir, env.get("PGHOST"))
134 self.assertThat(149 self.assertThat(
135 env.get("PATH", ""),150 env.get("PATH", ""),
136 StartsWith(PG_BIN + path.pathsep))151 StartsWith(get_pg_bin(self.version) + path.pathsep))
137152
138 def test_exists(self):153 def test_exists(self):
139 cluster = self.make(self.make_dir())154 cluster = self.make(self.make_dir())
140155
=== modified file 'postgresfixture/tests/test_main.py'
--- postgresfixture/tests/test_main.py 2012-05-22 16:15:44 +0000
+++ postgresfixture/tests/test_main.py 2014-01-17 14:25:09 +0000
@@ -19,6 +19,10 @@
1919
20from fixtures import EnvironmentVariableFixture20from fixtures import EnvironmentVariableFixture
21from postgresfixture import main21from postgresfixture import main
22from postgresfixture.cluster import (
23 PG_VERSION_MAX,
24 PG_VERSIONS,
25 )
22from postgresfixture.clusterfixture import ClusterFixture26from postgresfixture.clusterfixture import ClusterFixture
23from postgresfixture.testing import TestCase27from postgresfixture.testing import TestCase
24from testtools.matchers import StartsWith28from testtools.matchers import StartsWith
@@ -35,7 +39,7 @@
35 def parse_args(self, *args):39 def parse_args(self, *args):
36 try:40 try:
37 return main.argument_parser.parse_args(args)41 return main.argument_parser.parse_args(args)
38 except SystemExit, error:42 except SystemExit as error:
39 self.fail("parse_args%r failed with %r" % (args, error))43 self.fail("parse_args%r failed with %r" % (args, error))
4044
41 def test_run(self):45 def test_run(self):
@@ -174,3 +178,54 @@
174 sys.stderr.getvalue(), StartsWith(178 sys.stderr.getvalue(), StartsWith(
175 "%s: cluster is locked by:" % cluster.datadir))179 "%s: cluster is locked by:" % cluster.datadir))
176 self.assertTrue(cluster.exists)180 self.assertTrue(cluster.exists)
181
182
183class TestVersion(TestCase):
184
185 def patch_pg_versions(self, versions):
186 PG_VERSIONS[:] = versions
187
188 def test_uses_supplied_version(self):
189 # Reset PG_VERSIONS after the test has run.
190 self.addCleanup(self.patch_pg_versions, list(PG_VERSIONS))
191 self.patch_pg_versions(["1.1", "2.2", "3.3"])
192
193 # Record calls to our patched handler.
194 handler_calls = []
195
196 def handler(cluster, args):
197 handler_calls.append((cluster, args))
198
199 self.patch(
200 main.get_action("status"), "_defaults",
201 {"handler": handler})
202
203 # Prevent main() from altering terminal settings.
204 self.patch(main, "setup", lambda: None)
205
206 # The version chosen is picked up by the argument parser and
207 # passed into the Cluster constructor.
208 main.main(["--version", "2.2", "status"])
209 self.assertEqual(
210 [("2.2", "2.2")],
211 [(cluster.version, args.version)
212 for (cluster, args) in handler_calls])
213
214 def test_uses_default_version(self):
215 # Record calls to our patched handler.
216 handler_calls = []
217
218 def handler(cluster, args):
219 handler_calls.append((cluster, args))
220
221 self.patch(
222 main.get_action("status"), "_defaults",
223 {"handler": handler})
224
225 # The argument parser has the default version and passes it into
226 # the Cluster constructor.
227 main.main(["status"])
228 self.assertEqual(
229 [(PG_VERSION_MAX, PG_VERSION_MAX)],
230 [(cluster.version, args.version)
231 for (cluster, args) in handler_calls])
177232
=== modified file 'requirements.txt'
--- requirements.txt 2012-05-21 11:13:40 +0000
+++ requirements.txt 2014-01-17 14:25:09 +0000
@@ -1,3 +1,4 @@
1fixtures >= 0.3.81fixtures >= 0.3.8
2psycopg2 >= 2.4.42psycopg2 >= 2.4.4
3testtools >= 0.9.143testtools >= 0.9.14
4testscenarios >= 0.4

Subscribers

People subscribed via source and target branches

to all changes: