Merge ~stub/postgresql-charm:upgrade-series into postgresql-charm:master
- Git
- lp:~stub/postgresql-charm
- upgrade-series
- Merge into master
Proposed by
Stuart Bishop
Status: | Merged |
---|---|
Merged at revision: | a1d109cbbfb999f9c5715e268cd0226668f7d8cf |
Proposed branch: | ~stub/postgresql-charm:upgrade-series |
Merge into: | postgresql-charm:master |
Diff against target: |
476 lines (+114/-125) 7 files modified
Makefile (+2/-19) reactive/postgresql/postgresql.py (+21/-4) reactive/postgresql/service.py (+32/-9) reactive/postgresql/upgrade.py (+6/-0) requirements.txt (+3/-2) tox.ini (+4/-3) unit_tests/test_postgresql.py (+46/-88) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Barry Price | Approve | ||
Canonical IS Reviewers | Pending | ||
PostgreSQL Charm Maintainers | Pending | ||
Review via email: mp+395355@code.launchpad.net |
Commit message
upgrade-series support, resurrect unit tests
The charm would already probably survive a series upgrade, but
these changes should ensure the service always gets back up on
its feet. PostgreSQL major version upgrades might even work too,
although that would be better left to an action.
Also gets the unit tests back running, which had atrophied.
They are still rather awkward to run (they need to run in the
built charm directory), but are passing again.
Description of the change
To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Revision history for this message
Barry Price (barryprice) wrote : | # |
Couple of nitpick questions on the Makefile, otherwise LGTM
review:
Approve
Revision history for this message
Barry Price (barryprice) wrote : | # |
Actually save the second of those questions :)
Revision history for this message
Stuart Bishop (stub) : | # |
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change cannot be self approved, setting status to needs review.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/Makefile b/Makefile |
2 | index b05ce5b..089e02e 100644 |
3 | --- a/Makefile |
4 | +++ b/Makefile |
5 | @@ -27,24 +27,7 @@ _success_ex: |
6 | true | ts |
7 | |
8 | |
9 | -# Munge the path so the requested version of Juju is found, and thus used |
10 | -# by Amulet and juju-deployer. |
11 | -export PATH := /usr/lib/juju-$(shell $(JUJU) --version | perl -p -e "s/-.*//")/bin:$(PATH) |
12 | - |
13 | - |
14 | -test: testdeps lint unittest integration |
15 | - |
16 | -testdeps: |
17 | -ifeq ($(HOST_SERIES),trusty) |
18 | - sudo apt-get install -y python-tox python3-psycopg2 bzr moreutils \ |
19 | - software-properties-common |
20 | -else |
21 | - sudo apt-get install -y tox python3-psycopg2 bzr moreutils \ |
22 | - software-properties-common |
23 | -endif |
24 | - sudo add-apt-repository -y ppa:juju/stable |
25 | - sudo apt-get install charm-tools |
26 | - |
27 | +test: lint unittest integration |
28 | |
29 | CHARM_NAME := postgresql |
30 | |
31 | @@ -69,7 +52,7 @@ $(BUILD_DIR): |
32 | # updates. |
33 | .PHONY: build |
34 | build: | $(BUILD_DIR) |
35 | - charm build -f -o $(BUILD_ROOT) -n $(CHARM_NAME) |
36 | + charm build -f -d $(BUILD_DIR)/.. -n $(CHARM_NAME) |
37 | |
38 | # Generate a fresh development build and commit it to $(TEST_BRANCH). |
39 | # Only builds work committed to $(LAYER_BRANCH). |
40 | diff --git a/reactive/postgresql/postgresql.py b/reactive/postgresql/postgresql.py |
41 | index 373080d..35d3e02 100644 |
42 | --- a/reactive/postgresql/postgresql.py |
43 | +++ b/reactive/postgresql/postgresql.py |
44 | @@ -79,6 +79,13 @@ def version(): |
45 | if version: |
46 | return version |
47 | |
48 | + # If no cached version, use the version reported by |
49 | + # pg_lsclusters |
50 | + version = lsclusters_version() |
51 | + if version: |
52 | + unitdata.kv().set("postgresql.pg_version", version) |
53 | + return version |
54 | + |
55 | # We use the charm configuration here, as multiple versions |
56 | # of PostgreSQL may be installed. |
57 | config = hookenv.config() |
58 | @@ -98,6 +105,19 @@ def version(): |
59 | return version |
60 | |
61 | |
62 | +def clear_version_cache(): |
63 | + unitdata.kv().set("postgresql.pg_version", None) |
64 | + |
65 | + |
66 | +def lsclusters_version(): |
67 | + if not os.path.exists("/usr/bin/pg_lsclusters"): |
68 | + return None |
69 | + results = subprocess.check_output(["pg_lsclusters", "--no-header"]).decode().splitlines() |
70 | + if len(results) == 1: |
71 | + return results[0].split()[0] |
72 | + return None |
73 | + |
74 | + |
75 | def point_version(): |
76 | """PostgreSQL version. major.minor.patch or major.patch, as a string.""" |
77 | output = subprocess.check_output([postgres_path(), "-V"], universal_newlines=True) |
78 | @@ -473,10 +493,7 @@ def ensure_role(con, role): |
79 | def ensure_extensions(con, extensions): |
80 | """extensions is a list of (name, schema) tuples""" |
81 | cur = con.cursor() |
82 | - cur.execute( |
83 | - """SELECT extname,nspname FROM pg_extension,pg_namespace |
84 | - WHERE pg_namespace.oid = extnamespace""" |
85 | - ) |
86 | + cur.execute("SELECT extname, nspname FROM pg_extension, pg_namespace WHERE pg_namespace.oid = extnamespace") |
87 | installed_extensions = frozenset((x[0], x[1]) for x in cur.fetchall()) |
88 | hookenv.log("ensure_extensions({}), have {}".format(extensions, installed_extensions), DEBUG) |
89 | extensions_set = frozenset(set(extensions)) |
90 | diff --git a/reactive/postgresql/service.py b/reactive/postgresql/service.py |
91 | index b7269b1..f5e1936 100644 |
92 | --- a/reactive/postgresql/service.py |
93 | +++ b/reactive/postgresql/service.py |
94 | @@ -45,6 +45,18 @@ def install(): |
95 | reactive.set_flag("postgresql.upgrade.systemd") |
96 | |
97 | |
98 | +@hook("post-series-upgrade") |
99 | +def post_series_upgrade(): |
100 | + postgresql.clear_version_cache() # PG version upgrades should work on the master, but will break standbys |
101 | + config = hookenv.config() |
102 | + if config["pgdg"]: |
103 | + add_pgdg_source() |
104 | + if host.init_is_systemd(): |
105 | + reactive.set_flag("postgresql.upgrade.systemd") |
106 | + reactive.clear_flag("postgresql.cluster.support-scripts") |
107 | + reactive.clear_flag("postgresql.cluster.configured") |
108 | + |
109 | + |
110 | @when("config.changed") |
111 | def config_changed(): |
112 | reactive.clear_flag("postgresql.cluster.support-scripts") |
113 | @@ -143,12 +155,23 @@ def configure_sources(): |
114 | |
115 | # Shortcut for the PGDG archive. |
116 | if config["pgdg"]: |
117 | - pgdg_url = "http://apt.postgresql.org/pub/repos/apt/" |
118 | - pgdg_src = "deb {} {}-pgdg main".format(pgdg_url, helpers.distro_codename()) |
119 | - pgdg_key_path = os.path.join(hookenv.charm_dir(), "lib", "pgdg.key") |
120 | + add_pgdg_source() |
121 | + |
122 | + |
123 | +def add_pgdg_source(): |
124 | + pgdg_url = "http://apt.postgresql.org/pub/repos/apt/" |
125 | + pgdg_src = "deb {} {}-pgdg main".format(pgdg_url, helpers.distro_codename()) |
126 | + pgdg_key_path = os.path.join(hookenv.charm_dir(), "lib", "pgdg.key") |
127 | + kv = unitdata.kv() |
128 | + k = "postgresql.service.pgdg_src" |
129 | + # Add the source if the key has changed, or if the src has |
130 | + # changed such as a distro upgrade. Check key change first, |
131 | + # or short circuiting will add the source twice. |
132 | + if reactive.helpers.any_file_changed([pgdg_key_path]) or pgdg_src != kv.get(k): |
133 | with open(pgdg_key_path, "r") as f: |
134 | hookenv.log("Adding PGDG archive") |
135 | apt.add_source(pgdg_src, f.read()) |
136 | + kv.set(k, pgdg_src) |
137 | |
138 | |
139 | @when("postgresql.cluster.locale.set") |
140 | @@ -192,13 +215,13 @@ def create_cluster(): |
141 | def create_pg_ctl_conf(): |
142 | contents = textwrap.dedent( |
143 | """\ |
144 | - # Managed by Juju |
145 | - # Automatic pg_ctl configuration |
146 | - # This configuration file contains cluster specific options to be passed to |
147 | - # pg_ctl(1). |
148 | + # Managed by Juju |
149 | + # Automatic pg_ctl configuration |
150 | + # This configuration file contains cluster specific options to be passed to |
151 | + # pg_ctl(1). |
152 | |
153 | - pg_ctl_options = '-w -t 3600' |
154 | - """ |
155 | + pg_ctl_options = '-w -t 3600' |
156 | + """ |
157 | ) |
158 | helpers.write( |
159 | postgresql.pg_ctl_conf_path(), |
160 | diff --git a/reactive/postgresql/upgrade.py b/reactive/postgresql/upgrade.py |
161 | index 9379c60..d056319 100644 |
162 | --- a/reactive/postgresql/upgrade.py |
163 | +++ b/reactive/postgresql/upgrade.py |
164 | @@ -26,6 +26,7 @@ from reactive import workloadstatus |
165 | from reactive.postgresql import helpers |
166 | from reactive.postgresql import postgresql |
167 | from reactive.postgresql import replication |
168 | +from reactive.postgresql import service |
169 | from reactive.postgresql import storage |
170 | |
171 | |
172 | @@ -134,6 +135,11 @@ def upgrade_charm(): |
173 | reactive.clear_flag("postgresql.cluster.is_running") |
174 | postgresql.stop_pgctlcluster() |
175 | |
176 | + # Update the PGDG source, in case the signing key has changed. |
177 | + config = hookenv.config() |
178 | + if config["pgdg"]: |
179 | + service.add_pgdg_source() |
180 | + |
181 | |
182 | def migrate_user(old_username, new_username, password, superuser=False): |
183 | if postgresql.is_primary(): |
184 | diff --git a/requirements.txt b/requirements.txt |
185 | index 8aabe77..675a41a 100644 |
186 | --- a/requirements.txt |
187 | +++ b/requirements.txt |
188 | @@ -11,5 +11,6 @@ python-swiftclient |
189 | charms.reactive |
190 | charmhelpers |
191 | juju-deployer |
192 | -pip<8.1.2 |
193 | -websocket-client<=0.40.0 |
194 | +pip |
195 | +websocket-client |
196 | +tenacity |
197 | diff --git a/tox.ini b/tox.ini |
198 | index 71565ed..cb30fb6 100644 |
199 | --- a/tox.ini |
200 | +++ b/tox.ini |
201 | @@ -29,9 +29,10 @@ commands= |
202 | basepython=python3 |
203 | # pip install -I required to ensure scripts are created |
204 | # even if packages are already installed on the system |
205 | -install_command=pip install -I {opts} {packages} |
206 | -sitepackages=True |
207 | -deps = -r{toxinidir}/requirements.txt |
208 | +sitepackages=False |
209 | +deps = |
210 | + -r{toxinidir}/requirements.txt |
211 | + psycopg2 |
212 | passenv=JUJU_* |
213 | commands= |
214 | pytest {posargs:--verbose --tb=native unit_tests/} |
215 | diff --git a/unit_tests/test_postgresql.py b/unit_tests/test_postgresql.py |
216 | index 5524745..95cf828 100644 |
217 | --- a/unit_tests/test_postgresql.py |
218 | +++ b/unit_tests/test_postgresql.py |
219 | @@ -26,47 +26,47 @@ sys.path.insert(1, ROOT) |
220 | sys.path.insert(2, os.path.join(ROOT, 'lib')) |
221 | sys.path.insert(3, os.path.join(ROOT, 'lib', 'testdeps')) |
222 | |
223 | -from charmhelpers.core import hookenv |
224 | -from charmhelpers.core import unitdata |
225 | - |
226 | +from charmhelpers.core import hookenv, host |
227 | from reactive import workloadstatus |
228 | -from reactive.postgresql import helpers |
229 | -from reactive.postgresql import postgresql |
230 | +from reactive.postgresql import helpers, postgresql |
231 | |
232 | |
233 | class TestPostgresql(unittest.TestCase): |
234 | + @patch.object(postgresql, 'lsclusters_version') |
235 | @patch.object(hookenv, 'config') |
236 | @patch.object(helpers, 'distro_codename') |
237 | - def test_version(self, codename, config): |
238 | - |
239 | - def clear_cache(): |
240 | - unitdata.kv().unset('postgresql.pg_version') |
241 | + def test_version(self, codename, config, lsclusters_version): |
242 | + # Installed version |
243 | + lsclusters_version.return_value = '9.5' |
244 | + postgresql.clear_version_cache() |
245 | + self.assertEqual(postgresql.version(), '9.5') |
246 | + lsclusters_version.return_value = None |
247 | |
248 | # Explicit version in config. |
249 | config.return_value = {'version': '23'} |
250 | - clear_cache() |
251 | + postgresql.clear_version_cache() |
252 | self.assertEqual(postgresql.version(), '23') |
253 | |
254 | config.return_value = {'version': ''} |
255 | |
256 | # Trusty default |
257 | codename.return_value = 'trusty' |
258 | - clear_cache() |
259 | + postgresql.clear_version_cache() |
260 | self.assertEqual(postgresql.version(), '9.3') |
261 | |
262 | # Xenial default |
263 | codename.return_value = 'xenial' |
264 | - clear_cache() |
265 | + postgresql.clear_version_cache() |
266 | self.assertEqual(postgresql.version(), '9.5') |
267 | |
268 | # Bionic default |
269 | codename.return_value = 'bionic' |
270 | - clear_cache() |
271 | + postgresql.clear_version_cache() |
272 | self.assertEqual(postgresql.version(), '10') |
273 | |
274 | # No other fallbacks, yet. |
275 | codename.return_value = 'whatever' |
276 | - clear_cache() |
277 | + postgresql.clear_version_cache() |
278 | with self.assertRaises(NotImplementedError): |
279 | postgresql.version() |
280 | |
281 | @@ -251,8 +251,11 @@ class TestPostgresql(unittest.TestCase): |
282 | is_secondary.return_value = False |
283 | self.assertTrue(postgresql.is_primary()) |
284 | |
285 | + @patch.object(postgresql, 'has_version') |
286 | + @patch.object(hookenv, 'config') |
287 | @patch.object(postgresql, 'recovery_conf_path') |
288 | - def test_is_secondary(self, recovery_path): |
289 | + def test_is_secondary(self, recovery_path, config, has_version): |
290 | + has_version.return_value = False |
291 | # if recovery.conf exists, we are a secondary. |
292 | with tempfile.NamedTemporaryFile() as f: |
293 | recovery_path.return_value = f.name |
294 | @@ -317,7 +320,7 @@ class TestPostgresql(unittest.TestCase): |
295 | check_call.assert_called_once_with(['pg_createcluster', |
296 | '-e', sentinel.encoding, |
297 | '--locale', sentinel.locale, |
298 | - '9.2', 'main'], |
299 | + '9.2', 'main', '--', '--data-checksums'], |
300 | universal_newlines=True) |
301 | |
302 | @patch('subprocess.check_call') |
303 | @@ -345,7 +348,7 @@ class TestPostgresql(unittest.TestCase): |
304 | cur.execute.assert_has_calls([ |
305 | call('SELECT datname FROM pg_database WHERE datname=%s', |
306 | ('hello',)), |
307 | - call('CREATE DATABASE %s', (ANY,))]) |
308 | + call('CREATE DATABASE %s OWNER %s', (ANY, ANY))]) |
309 | # The database name in that last call was correctly quoted. |
310 | quoted_dbname = cur.execute.call_args[0][1][0] |
311 | self.assertIsInstance(quoted_dbname, postgresql.AsIs) |
312 | @@ -475,14 +478,16 @@ class TestPostgresql(unittest.TestCase): |
313 | |
314 | pgidentifier.side_effect = lambda d: 'q_{}'.format(d) |
315 | |
316 | - existing_extensions = set(['extA', 'extB']) |
317 | - wanted_extensions = set(['extB', 'extC']) |
318 | + existing_extensions = set([('extA', 'public'), ('extB', 'public')]) |
319 | + wanted_extensions = set([('extB', 'public'), ('extC', 'custom')]) |
320 | |
321 | - cur.fetchall.return_value = [[x] for x in existing_extensions] |
322 | + cur.fetchall.return_value = [x for x in existing_extensions] |
323 | postgresql.ensure_extensions(con, wanted_extensions) |
324 | cur.execute.assert_has_calls([ |
325 | - call('SELECT extname FROM pg_extension'), |
326 | - call('CREATE EXTENSION %s', ('q_extC',))]) |
327 | + call('SELECT extname, nspname FROM pg_extension, pg_namespace WHERE pg_namespace.oid = extnamespace'), |
328 | + call('CREATE SCHEMA IF NOT EXISTS %s', ('q_custom',)), |
329 | + call('GRANT USAGE ON SCHEMA %s TO PUBLIC', ('q_custom',)), |
330 | + call('CREATE EXTENSION %s WITH SCHEMA %s', ('q_extC','q_custom'))]) |
331 | |
332 | def test_addr_to_range(self): |
333 | eggs = [('hostname', 'hostname'), |
334 | @@ -532,35 +537,18 @@ class TestPostgresql(unittest.TestCase): |
335 | |
336 | @patch.object(postgresql, 'emit_pg_log') |
337 | @patch.object(workloadstatus, 'status_set') |
338 | - @patch('subprocess.check_call') |
339 | + @patch.object(host, 'service_start') |
340 | @patch.object(postgresql, 'version') |
341 | - def test_start(self, version, check_call, status_set, emit_pg_log): |
342 | + def test_start(self, version, service_start, status_set, emit_pg_log): |
343 | version.return_value = '9.9' |
344 | |
345 | # When it works, it works. |
346 | postgresql.start() |
347 | - # Both -w and -t options are required to wait for startup. |
348 | - # We wait a long time, as startup might take a long time. |
349 | - # Maybe we should wait a lot longer. |
350 | - check_call.assert_called_once_with(['pg_ctlcluster', '9.9', 'main', |
351 | - 'start', '--', '-w', |
352 | - '-t', '86400'], |
353 | - universal_newlines=True) |
354 | + service_start.assert_called_once_with('postgresql@9.9-main') |
355 | self.assertFalse(emit_pg_log.called) |
356 | |
357 | - # If it is already running, pg_ctlcluster returns code 2. |
358 | - # We block, and terminate whatever hook is running. |
359 | - check_call.side_effect = subprocess.CalledProcessError(2, 'whoops') |
360 | - check_call.reset_mock() |
361 | - postgresql.start() |
362 | - check_call.assert_called_once_with(['pg_ctlcluster', '9.9', 'main', |
363 | - 'start', '--', '-w', |
364 | - '-t', '86400'], |
365 | - universal_newlines=True) |
366 | - |
367 | - # Other failures block the unit. Perhaps it is just taking too |
368 | - # perform recovery after a power outage. |
369 | - check_call.side_effect = subprocess.CalledProcessError(42, 'whoops') |
370 | + # Start failure we block, and terminate whatever hook is running. |
371 | + service_start.return_value = False |
372 | with self.assertRaises(SystemExit) as x: |
373 | postgresql.start() |
374 | status_set.assert_called_once_with('blocked', ANY) # Set blocked. |
375 | @@ -569,59 +557,29 @@ class TestPostgresql(unittest.TestCase): |
376 | |
377 | @patch.object(hookenv, 'log') |
378 | @patch.object(workloadstatus, 'status_set') |
379 | - @patch('subprocess.check_call') |
380 | + @patch.object(host, 'service_stop') |
381 | @patch.object(postgresql, 'version') |
382 | - def test_stop(self, version, check_call, status_set, log): |
383 | + def test_stop(self, version, service_stop, status_set, log): |
384 | version.return_value = '9.9' |
385 | |
386 | # Normal shutdown shuts down. |
387 | + service_stop.return_value = True |
388 | postgresql.stop() |
389 | - # -t option is required to wait for shutdown to complete. -w not |
390 | - # required unlike 'start', but lets be explicit. |
391 | - check_call.assert_called_once_with(['pg_ctlcluster', |
392 | - '--mode', 'fast', '9.9', 'main', |
393 | - 'stop', '--', '-w', '-t', '300'], |
394 | - universal_newlines=True) |
395 | - |
396 | - # If the server is not running, pg_ctlcluster(1) signals this with |
397 | - # returncode 2. |
398 | - check_call.side_effect = subprocess.CalledProcessError(2, 'whoops') |
399 | - check_call.reset_mock() |
400 | - postgresql.stop() |
401 | - # -t option is required to wait for shutdown to complete. -w not |
402 | - # required unlike 'start', but lets be explicit. |
403 | - check_call.assert_called_once_with(['pg_ctlcluster', |
404 | - '--mode', 'fast', '9.9', 'main', |
405 | - 'stop', '--', '-w', '-t', '300'], |
406 | - universal_newlines=True) |
407 | + service_stop.assert_called_once_with('postgresql@9.9-main') |
408 | |
409 | - # If 'fast' shutdown fails, we retry with an 'immediate' shutdown |
410 | - check_call.side_effect = iter([subprocess.CalledProcessError(42, 'x'), |
411 | - None]) |
412 | - check_call.reset_mock() |
413 | - postgresql.stop() |
414 | - check_call.assert_has_calls([ |
415 | - call(['pg_ctlcluster', '--mode', 'fast', '9.9', 'main', |
416 | - 'stop', '--', '-w', '-t', '300'], |
417 | - universal_newlines=True), |
418 | - call(['pg_ctlcluster', '--mode', 'immediate', '9.9', 'main', |
419 | - 'stop', '--', '-w', '-t', '300'], |
420 | - universal_newlines=True)]) |
421 | - |
422 | - # If both fail, we block the unit. |
423 | - check_call.side_effect = subprocess.CalledProcessError(42, 'x') |
424 | + # Failed shutdown blocks and terminates |
425 | + service_stop.return_value = False |
426 | with self.assertRaises(SystemExit) as x: |
427 | postgresql.stop() |
428 | status_set.assert_called_once_with('blocked', ANY) |
429 | self.assertEqual(x.exception.code, 0) # Exit cleanly |
430 | |
431 | - @patch('subprocess.check_call') |
432 | + @patch.object(host, 'service_reload') |
433 | @patch.object(postgresql, 'version') |
434 | - def test_reload_config(self, version, check_call): |
435 | + def test_reload_config(self, version, service_reload): |
436 | version.return_value = '9.9' |
437 | postgresql.reload_config() |
438 | - check_call.assert_called_once_with(['pg_ctlcluster', '9.9', 'main', |
439 | - 'reload']) |
440 | + service_reload.assert_called_once_with('postgresql@9.9-main') |
441 | |
442 | def test_parse_config(self): |
443 | valid = [(r'# A comment', dict()), |
444 | @@ -653,7 +611,7 @@ class TestPostgresql(unittest.TestCase): |
445 | |
446 | with self.assertRaises(SyntaxError) as x: |
447 | postgresql.parse_config("=") |
448 | - self.assertEqual(str(x.exception), 'Missing key (line 1)') |
449 | + self.assertEqual(str(x.exception), "Missing key '=' (line 1)") |
450 | self.assertEqual(x.exception.lineno, 1) |
451 | self.assertEqual(x.exception.text, "=") |
452 | |
453 | @@ -665,19 +623,19 @@ class TestPostgresql(unittest.TestCase): |
454 | |
455 | with self.assertRaises(SyntaxError) as x: |
456 | postgresql.parse_config("key='unterminated") |
457 | - self.assertEqual(str(x.exception), 'Badly quoted value (line 1)') |
458 | + self.assertEqual(str(x.exception), r'''Badly quoted value "'unterminated" (line 1)''') |
459 | |
460 | with self.assertRaises(SyntaxError) as x: |
461 | postgresql.parse_config("key='unterminated 2 # comment") |
462 | - self.assertEqual(str(x.exception), 'Badly quoted value (line 1)') |
463 | + self.assertEqual(str(x.exception), r'''Badly quoted value "'unterminated 2" (line 1)''') |
464 | |
465 | with self.assertRaises(SyntaxError) as x: |
466 | postgresql.parse_config("key='unte''''") |
467 | - self.assertEqual(str(x.exception), 'Badly quoted value (line 1)') |
468 | + self.assertEqual(str(x.exception), r"""Badly quoted value "'unte''''" (line 1)""") |
469 | |
470 | with self.assertRaises(SyntaxError) as x: |
471 | postgresql.parse_config(r"key='\'") |
472 | - self.assertEqual(str(x.exception), 'Badly quoted value (line 1)') |
473 | + self.assertEqual(str(x.exception), r'''Badly quoted value "'\\'" (line 1)''') |
474 | |
475 | def test_convert_unit(self): |
476 | c = postgresql.convert_unit |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.