~stub/charms/precise/postgresql/bug-1205286

28.6.1 by Stuart Bishop
Sketching out test framework
1
#!/usr/bin/python
2
28.6.12 by Stuart Bishop
Basic test working
3
"""
46.5.32 by Stuart Bishop
Fix election test
4
Test the PostgreSQL charm.
5
6
Usage:
7
    juju bootstrap
8
    TEST_DEBUG_FILE=test-debug.log TEST_TIMEOUT=900 ./test.py -v
9
    juju destroy-environment
28.6.12 by Stuart Bishop
Basic test working
10
"""
11
28.6.1 by Stuart Bishop
Sketching out test framework
12
import fixtures
13
import json
28.6.2 by Stuart Bishop
Define local repository
14
import os.path
28.6.1 by Stuart Bishop
Sketching out test framework
15
import subprocess
16
import testtools
28.6.11 by Stuart Bishop
WIP
17
from testtools.content import text_content
28.6.12 by Stuart Bishop
Basic test working
18
import time
28.6.1 by Stuart Bishop
Sketching out test framework
19
import unittest
20
21
28.6.2 by Stuart Bishop
Define local repository
22
SERIES = 'precise'
28.6.8 by Stuart Bishop
WIP
23
TEST_CHARM = 'local:postgresql'
46.5.27 by Stuart Bishop
WIP
24
PSQL_CHARM = 'cs:postgresql-psql'
28.6.2 by Stuart Bishop
Define local repository
25
26
28.6.12 by Stuart Bishop
Basic test working
27
def DEBUG(msg):
28
    """Allow us to watch these slow tests as they run."""
29
    debug_file = os.environ.get('TEST_DEBUG_FILE', '')
30
    if debug_file:
31
        with open(debug_file, 'a') as f:
32
            f.write('{}> {}\n'.format(time.ctime(), msg))
33
            f.flush()
34
35
28.6.11 by Stuart Bishop
WIP
36
def _run(detail_collector, cmd, input=''):
28.6.12 by Stuart Bishop
Basic test working
37
    DEBUG("Running {}".format(' '.join(cmd)))
38
    try:
39
        proc = subprocess.Popen(
40
            cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
41
            stderr=subprocess.PIPE)
42
    except subprocess.CalledProcessError, x:
43
        DEBUG("exception: {!r}".format(x))
44
        DEBUG("stderr: {}".format(proc.stderr.read()))
45
        raise
46
28.6.11 by Stuart Bishop
WIP
47
    (out, err) = proc.communicate(input)
48
    if out:
28.6.12 by Stuart Bishop
Basic test working
49
        DEBUG("stdout: {}".format(out))
28.6.11 by Stuart Bishop
WIP
50
        detail_collector.addDetail('stdout', text_content(out))
51
    if err:
28.6.12 by Stuart Bishop
Basic test working
52
        DEBUG("stderr: {}".format(err))
28.6.11 by Stuart Bishop
WIP
53
        detail_collector.addDetail('stderr', text_content(err))
54
    if proc.returncode != 0:
28.6.12 by Stuart Bishop
Basic test working
55
        DEBUG("rv: {}".format(proc.returncode))
28.6.11 by Stuart Bishop
WIP
56
        raise subprocess.CalledProcessError(
57
            proc.returncode, cmd, err)
58
    return out
59
60
28.6.1 by Stuart Bishop
Sketching out test framework
61
class JujuFixture(fixtures.Fixture):
28.6.2 by Stuart Bishop
Define local repository
62
    """Interact with juju. Assumes juju environment is bootstrapped."""
46.2.13 by Stuart Bishop
Smarter deploy to save a few mins startup overhead
63
    _deployed_charms = set()
64
28.6.1 by Stuart Bishop
Sketching out test framework
65
    def do(self, cmd):
28.6.11 by Stuart Bishop
WIP
66
        cmd = ['juju'] + cmd
67
        _run(self, cmd)
28.6.1 by Stuart Bishop
Sketching out test framework
68
69
    def get_result(self, cmd):
28.6.11 by Stuart Bishop
WIP
70
        cmd = ['juju'] + cmd + ['--format=json']
71
        out = _run(self, cmd)
72
        if out:
73
            return json.loads(out)
28.6.1 by Stuart Bishop
Sketching out test framework
74
        return None
75
46.2.13 by Stuart Bishop
Smarter deploy to save a few mins startup overhead
76
    def deploy(self, charm, name=None, num_units=1):
77
        # The first time we deploy a charm in the test run, it needs to
78
        # deploy with --update to ensure we are testing the desired
79
        # revision of the charm. Subsequent deploys we do not use
80
        # --update to avoid overhead and needless incrementing of the
81
        # revision number.
46.5.27 by Stuart Bishop
WIP
82
        if charm.startswith('cs:') or charm in self._deployed_charms:
83
            cmd = ['deploy']
84
        else:
46.2.13 by Stuart Bishop
Smarter deploy to save a few mins startup overhead
85
            cmd = ['deploy', '-u']
86
            self._deployed_charms.add(charm)
87
88
        if num_units > 1:
89
            cmd.extend(['-n', str(num_units)])
90
46.2.14 by Stuart Bishop
Fix deploy helper
91
        cmd.append(charm)
92
46.2.13 by Stuart Bishop
Smarter deploy to save a few mins startup overhead
93
        if name:
94
            cmd.append(name)
95
96
        self.do(cmd)
97
28.6.8 by Stuart Bishop
WIP
98
    # The most recent environment status, updated by refresh_status()
99
    status = None
100
101
    def refresh_status(self):
102
        self.status = self.get_result(['status'])
28.6.11 by Stuart Bishop
WIP
103
        return self.status
28.6.8 by Stuart Bishop
WIP
104
105
    def wait_until_ready(self):
106
        ready = False
107
        while not ready:
108
            self.refresh_status()
109
            ready = True
28.6.11 by Stuart Bishop
WIP
110
            for service in self.status['services']:
46.2.5 by Stuart Bishop
Fixes for gojuju
111
                if self.status['services'][service].get('life', '') == 'dying':
112
                    ready = False
113
                units = self.status['services'][service].get('units', {})
114
                for unit in units.keys():
115
                    agent_state = units[unit].get('agent-state', '')
46.5.15 by Stuart Bishop
WIP
116
                    if agent_state == 'error':
117
                        raise RuntimeError('{} error: {}'.format(
118
                            unit, units[unit].get('agent-state-info','')))
28.6.11 by Stuart Bishop
WIP
119
                    if agent_state != 'started':
120
                        ready = False
46.5.28 by Stuart Bishop
WIP
121
        # Wait a little longer, as we have no way of telling
122
        # if relationship hooks have finished running.
123
        time.sleep(10)
28.6.2 by Stuart Bishop
Define local repository
124
125
    def setUp(self):
28.6.14 by Stuart Bishop
A little logging
126
        DEBUG("JujuFixture.setUp()")
28.6.2 by Stuart Bishop
Define local repository
127
        super(JujuFixture, self).setUp()
28.6.11 by Stuart Bishop
WIP
128
        self.reset()
28.6.2 by Stuart Bishop
Define local repository
129
        self.addCleanup(self.reset)
130
131
    def reset(self):
28.6.14 by Stuart Bishop
A little logging
132
        DEBUG("JujuFixture.reset()")
28.6.1 by Stuart Bishop
Sketching out test framework
133
        # Tear down any services left running.
46.2.13 by Stuart Bishop
Smarter deploy to save a few mins startup overhead
134
        found_services = False
28.6.11 by Stuart Bishop
WIP
135
        self.refresh_status()
136
        for service in self.status['services']:
46.2.13 by Stuart Bishop
Smarter deploy to save a few mins startup overhead
137
            found_services = True
46.2.5 by Stuart Bishop
Fixes for gojuju
138
            # It is an error to destroy a dying service.
139
            if self.status['services'][service].get('life', '') != 'dying':
140
                self.do(['destroy-service', service])
141
46.2.12 by Stuart Bishop
Update comments from bug feedback and better initialization
142
        # Per Bug #1190250 (WONTFIX), we need to wait for dying services
143
        # to die before we can continue.
46.2.13 by Stuart Bishop
Smarter deploy to save a few mins startup overhead
144
        if found_services:
145
            self.wait_until_ready()
46.2.5 by Stuart Bishop
Fixes for gojuju
146
46.2.6 by Stuart Bishop
Improve comment
147
        # We shouldn't reuse machines, as we have no guarantee they are
46.2.12 by Stuart Bishop
Update comments from bug feedback and better initialization
148
        # still in a usable state, so tear them down too. Per
149
        # Bug #1190492 (INVALID), in the future this will be much nicer
150
        # when we can use containers for isolation and can happily reuse
151
        # machines.
46.2.5 by Stuart Bishop
Fixes for gojuju
152
        dirty_machines = [
153
            m for m in self.status['machines'].keys() if m != '0']
154
        if dirty_machines:
46.2.20 by Stuart Bishop
Prefer syntax compatible with pyjuju
155
            self.do(['terminate-machine'] + dirty_machines)
28.6.1 by Stuart Bishop
Sketching out test framework
156
157
28.6.2 by Stuart Bishop
Define local repository
158
class LocalCharmRepositoryFixture(fixtures.Fixture):
159
    """Create links so the given directory can be deployed as a charm."""
160
    def __init__(self, path=None):
161
        if path is None:
162
            path = os.getcwd()
163
        self.local_repo_path = os.path.abspath(path)
164
165
    def setUp(self):
166
        super(LocalCharmRepositoryFixture, self).setUp()
167
168
        series_dir = os.path.join(self.local_repo_path, SERIES)
28.6.8 by Stuart Bishop
WIP
169
        charm_dir = os.path.join(series_dir, TEST_CHARM)
28.6.2 by Stuart Bishop
Define local repository
170
171
        if not os.path.exists(series_dir):
172
            os.mkdir(series_dir, 0o700)
173
            self.addCleanup(os.rmdir, series_dir)
174
175
        if not os.path.exists(charm_dir):
176
            os.symlink(self.local_repo_path, charm_dir)
177
            self.addCleanup(os.remove, charm_dir)
178
179
        self.useFixture(fixtures.EnvironmentVariable(
180
            'JUJU_REPOSITORY', self.local_repo_path))
181
182
28.6.1 by Stuart Bishop
Sketching out test framework
183
class PostgreSQLCharmTestCase(testtools.TestCase, fixtures.TestWithFixtures):
184
185
    def setUp(self):
186
        super(PostgreSQLCharmTestCase, self).setUp()
28.6.11 by Stuart Bishop
WIP
187
46.2.13 by Stuart Bishop
Smarter deploy to save a few mins startup overhead
188
        self.juju = self.useFixture(JujuFixture())
28.6.13 by Stuart Bishop
Reset juju environment between tests
189
28.6.11 by Stuart Bishop
WIP
190
        ## Disabled until postgresql-psql is in the charm store.
191
        ## Otherwise, we need to make the local:postgresql-psql charm
192
        ## discoverable.
193
        ## self.useFixture(LocalCharmRepositoryFixture())
194
195
        # If the charms fail, we don't want tests to hang indefinitely.
196
        # We might need to increase this in some environments or if the
197
        # environment doesn't have enough machines warmed up.
28.4.13 by Stuart Bishop
Failover tests, work in progress
198
        timeout = int(os.environ.get('TEST_TIMEOUT', 900))
28.6.12 by Stuart Bishop
Basic test working
199
        self.useFixture(fixtures.Timeout(timeout, gentle=True))
28.6.11 by Stuart Bishop
WIP
200
46.5.27 by Stuart Bishop
WIP
201
    def sql(self, sql, postgres_unit=None, psql_unit=None, dbname=None):
28.6.11 by Stuart Bishop
WIP
202
        '''Run some SQL on postgres_unit from psql_unit.
203
204
        Uses a random psql_unit and postgres_unit if not specified.
205
28.4.13 by Stuart Bishop
Failover tests, work in progress
206
        postgres_unit may be set to an explicit unit name, 'master' or
207
        'hot standby'.
208
28.6.11 by Stuart Bishop
WIP
209
        A db-admin relation is used if dbname is specified. Otherwise,
210
        a standard db relation is used.
211
        '''
212
        if psql_unit is None:
213
            psql_unit = (
214
                self.juju.status['services']['psql']['units'].keys()[0])
215
28.6.12 by Stuart Bishop
Basic test working
216
        # The psql statements we are going to execute.
217
        sql = sql.strip()
218
        if not sql.endswith(';'):
219
            sql += ';'
220
        sql += '\n\\q\n'
221
28.6.11 by Stuart Bishop
WIP
222
        # The command we run to connect psql to the desired database.
223
        if postgres_unit is None:
224
            postgres_unit = (
225
                self.juju.status['services']['postgresql']['units'].keys()[0])
28.4.13 by Stuart Bishop
Failover tests, work in progress
226
        elif postgres_unit == 'hot standby':
46.5.14 by Stuart Bishop
Fixing replication, WIP
227
            postgres_unit = 'hot-standby'  # Munge for generating script name.
28.6.11 by Stuart Bishop
WIP
228
        if dbname is None:
28.6.12 by Stuart Bishop
Basic test working
229
            psql_cmd = [
46.2.11 by Stuart Bishop
Get tests infrastructure running with juju 1.11
230
                'psql-db-{}'.format(postgres_unit.replace('/', '-'))]
28.6.11 by Stuart Bishop
WIP
231
        else:
232
            psql_cmd = [
46.2.11 by Stuart Bishop
Get tests infrastructure running with juju 1.11
233
                'psql-db-admin-{}'.format(
28.6.12 by Stuart Bishop
Basic test working
234
                    postgres_unit.replace('/', '-')), '-d', dbname]
28.6.11 by Stuart Bishop
WIP
235
        psql_args = [
236
            '--quiet', '--tuples-only', '--no-align', '--no-password',
237
            '--field-separator=,', '--file=-']
46.2.11 by Stuart Bishop
Get tests infrastructure running with juju 1.11
238
        cmd = [
239
            'juju', 'ssh', psql_unit,
240
            # Due to Bug #1191079, we need to send the whole remote command
241
            # as a single argument.
242
            ' '.join(psql_cmd + psql_args)]
46.5.27 by Stuart Bishop
WIP
243
        DEBUG("SQL {}".format(sql))
28.6.11 by Stuart Bishop
WIP
244
        out = _run(self, cmd, input=sql)
46.5.27 by Stuart Bishop
WIP
245
        DEBUG("OUT {}".format(out))
28.6.12 by Stuart Bishop
Basic test working
246
        result = [line.split(',') for line in out.splitlines()]
247
        self.addDetail('sql', text_content(repr((sql, result))))
248
        return result
28.6.8 by Stuart Bishop
WIP
249
46.5.29 by Stuart Bishop
Failover test passing
250
    def pg_ctlcluster(self, unit, command):
251
        cmd = ['juju', 'ssh', unit,
252
            # Due to Bug #1191079, we need to send the whole remote command
253
            # as a single argument.
254
            'sudo pg_ctlcluster 9.1 main -force {}'.format(command)]
255
        _run(self, cmd)
256
28.6.2 by Stuart Bishop
Define local repository
257
    def test_basic(self):
46.7.2 by Stuart Bishop
Fixes
258
        '''Connect to a a single unit service via the db relationship.'''
46.2.13 by Stuart Bishop
Smarter deploy to save a few mins startup overhead
259
        self.juju.deploy(TEST_CHARM, 'postgresql')
260
        self.juju.deploy(PSQL_CHARM, 'psql')
28.6.8 by Stuart Bishop
WIP
261
        self.juju.do(['add-relation', 'postgresql:db', 'psql:db'])
262
        self.juju.wait_until_ready()
28.6.12 by Stuart Bishop
Basic test working
263
264
        # There a race condition here, as hooks may still be running
265
        # from adding the relation. I'm protected here as 'juju status'
266
        # takes about 25 seconds to run from here to my test cloud but
267
        # others might not be so 'lucky'.
28.6.11 by Stuart Bishop
WIP
268
        result = self.sql('SELECT TRUE')
28.6.12 by Stuart Bishop
Basic test working
269
        self.assertEqual(result, [['t']])
28.6.1 by Stuart Bishop
Sketching out test framework
270
46.7.2 by Stuart Bishop
Fixes
271
    def test_basic_admin(self):
272
        '''Connect to a single unit service via the db-admin relationship.'''
273
        self.juju.deploy(TEST_CHARM, 'postgresql')
274
        self.juju.deploy(PSQL_CHARM, 'psql')
275
        self.juju.do(['add-relation', 'postgresql:db-admin', 'psql:db-admin'])
276
        self.juju.wait_until_ready()
277
278
        result = self.sql('SELECT TRUE', dbname='postgres')
279
        self.assertEqual(result, [['t']])
280
281
46.5.28 by Stuart Bishop
WIP
282
    def is_master(self, postgres_unit, dbname=None):
46.5.29 by Stuart Bishop
Failover test passing
283
        is_master = self.sql(
284
            'SELECT NOT pg_is_in_recovery()',
285
            postgres_unit, dbname=dbname)[0][0]
286
        return (is_master == 't')
28.4.13 by Stuart Bishop
Failover tests, work in progress
287
288
    def test_failover(self):
46.5.14 by Stuart Bishop
Fixing replication, WIP
289
        """Set up a multi-unit service and perform failovers."""
46.5.29 by Stuart Bishop
Failover test passing
290
        self.juju.deploy(TEST_CHARM, 'postgresql', num_units=3)
46.5.14 by Stuart Bishop
Fixing replication, WIP
291
        self.juju.deploy(PSQL_CHARM, 'psql')
46.5.29 by Stuart Bishop
Failover test passing
292
        self.juju.do(['add-relation', 'postgresql:db', 'psql:db'])
28.4.13 by Stuart Bishop
Failover tests, work in progress
293
        self.juju.wait_until_ready()
294
46.5.29 by Stuart Bishop
Failover test passing
295
        # On a freshly setup service, lowest numbered unit is always the
296
        # master.
28.4.13 by Stuart Bishop
Failover tests, work in progress
297
        units = unit_sorted(
298
            self.juju.status['services']['postgresql']['units'].keys())
46.5.29 by Stuart Bishop
Failover test passing
299
        master_unit, standby_unit_1, standby_unit_2 = units
300
301
        self.assertIs(True, self.is_master(master_unit))
302
        self.assertIs(False, self.is_master(standby_unit_1))
303
        self.assertIs(False, self.is_master(standby_unit_2))
304
305
        self.sql('CREATE TABLE Token (x int)', master_unit)
306
307
        # Some simple helper to send data via the master and check if it
308
        # was replicated to the hot standbys.
46.5.27 by Stuart Bishop
WIP
309
        _counter = [0]
310
311
        def send_token(unit):
312
            _counter[0] += 1
46.5.29 by Stuart Bishop
Failover test passing
313
            self.sql("INSERT INTO Token VALUES (%d)" % _counter[0], unit)
46.5.27 by Stuart Bishop
WIP
314
315
        def token_received(unit):
46.5.30 by Stuart Bishop
Test fixes
316
            # async replocation can lag, so retry for a little while to
317
            # give the databases a chance to get their act together.
318
            start = time.time()
319
            timeout = start + 60
320
            while time.time() <= timeout:
321
                r = self.sql(
322
                    "SELECT TRUE FROM Token WHERE x=%d" % _counter[0], unit)
323
                if r == [['t']]:
324
                    return True
325
            return False
46.5.27 by Stuart Bishop
WIP
326
327
        # Confirm that replication is actually happening.
328
        send_token(master_unit)
329
        self.assertIs(True, token_received(standby_unit_1))
330
        self.assertIs(True, token_received(standby_unit_2))
331
28.4.13 by Stuart Bishop
Failover tests, work in progress
332
        # Remove the master unit.
333
        self.juju.do(['remove-unit', master_unit])
334
        self.juju.wait_until_ready()
46.5.27 by Stuart Bishop
WIP
335
46.5.29 by Stuart Bishop
Failover test passing
336
        # When we failover, the unit that has received the most WAL
337
        # information from the old master (most in sync) is elected the
338
        # new master.
339
        standby_unit_1_is_master = self.is_master(standby_unit_1)
340
        standby_unit_2_is_master = self.is_master(standby_unit_2)
341
        self.assertNotEqual(
342
            standby_unit_1_is_master, standby_unit_2_is_master)
46.5.27 by Stuart Bishop
WIP
343
344
        if standby_unit_1_is_master:
345
            master_unit = standby_unit_1
46.5.29 by Stuart Bishop
Failover test passing
346
            standby_unit = standby_unit_2
46.5.27 by Stuart Bishop
WIP
347
        else:
46.5.29 by Stuart Bishop
Failover test passing
348
            master_unit = standby_unit_2
46.5.27 by Stuart Bishop
WIP
349
            standby_unit = standby_unit_1
350
46.5.29 by Stuart Bishop
Failover test passing
351
        # Confirm replication is still working.
46.5.27 by Stuart Bishop
WIP
352
        send_token(master_unit)
353
        self.assertIs(True, token_received(standby_unit))
354
46.5.29 by Stuart Bishop
Failover test passing
355
        # Remove the master again, leaving a single unit.
356
        self.juju.do(['remove-unit', master_unit])
357
        self.juju.wait_until_ready()
358
359
        # Last unit is a working, standalone database.
360
        self.is_master(standby_unit)
361
        send_token(standby_unit)
362
363
        # We can tell it is correctly reporting that it is standalone by
364
        # seeing if the -master and -hot-standby scripts no longer exist
365
        # on the psql unit.
366
        self.assertRaises(
367
            subprocess.CalledProcessError,
368
            self.sql, 'SELECT TRUE', 'master')
369
        self.assertRaises(
370
            subprocess.CalledProcessError,
371
            self.sql, 'SELECT TRUE', 'hot standby')
372
373
    def test_failover_election(self):
374
        """Ensure master elected in a failover is the best choice"""
375
        self.juju.deploy(TEST_CHARM, 'postgresql', num_units=3)
376
        self.juju.deploy(PSQL_CHARM, 'psql')
377
        self.juju.do(['add-relation', 'postgresql:db-admin', 'psql:db-admin'])
378
        self.juju.wait_until_ready()
379
380
        # On a freshly setup service, lowest numbered unit is always the
381
        # master.
382
        units = unit_sorted(
383
            self.juju.status['services']['postgresql']['units'].keys())
384
        master_unit, standby_unit_1, standby_unit_2 = units
385
386
        # Shutdown PostgreSQL on standby_unit_1 and ensure
387
        # standby_unit_2 will have received more WAL information from
388
        # the master.
46.5.30 by Stuart Bishop
Test fixes
389
        self.pg_ctlcluster(standby_unit_1, 'stop')
46.5.29 by Stuart Bishop
Failover test passing
390
        self.sql("SELECT pg_switch_xlog()", master_unit, dbname='postgres')
391
46.5.32 by Stuart Bishop
Fix election test
392
        # Break replication so when we bring standby_unit_1 up, it has
393
        # no way of catching up.
394
        self.sql(
395
            "ALTER ROLE juju_replication NOREPLICATION",
396
            master_unit, dbname='postgres')
397
        self.pg_ctlcluster(master_unit, 'restart')
46.5.29 by Stuart Bishop
Failover test passing
398
46.5.32 by Stuart Bishop
Fix election test
399
        # Restart standby_unit_1 now it has no way or resyncing.
46.5.30 by Stuart Bishop
Test fixes
400
        self.pg_ctlcluster(standby_unit_1, 'start')
46.5.29 by Stuart Bishop
Failover test passing
401
46.5.32 by Stuart Bishop
Fix election test
402
        # Failover.
46.5.29 by Stuart Bishop
Failover test passing
403
        self.juju.do(['remove-unit', master_unit])
404
        self.juju.wait_until_ready()
405
46.5.32 by Stuart Bishop
Fix election test
406
        # Fix replication.
407
        self.sql(
408
            "ALTER ROLE juju_replication REPLICATION",
409
            standby_unit_2, dbname='postgres')
410
46.5.29 by Stuart Bishop
Failover test passing
411
        # Ensure the election went as predicted.
46.5.32 by Stuart Bishop
Fix election test
412
        self.assertIs(True, self.is_master(standby_unit_2, 'postgres'))
413
        self.assertIs(False, self.is_master(standby_unit_1, 'postgres'))
46.5.15 by Stuart Bishop
WIP
414
28.4.13 by Stuart Bishop
Failover tests, work in progress
415
416
def unit_sorted(units):
417
    """Return a correctly sorted list of unit names."""
418
    return sorted(
419
        units, lambda a,b:
420
            cmp(int(a.split('/')[-1]), int(b.split('/')[-1])))
28.6.1 by Stuart Bishop
Sketching out test framework
421
46.5.14 by Stuart Bishop
Fixing replication, WIP
422
28.6.1 by Stuart Bishop
Sketching out test framework
423
if __name__ == '__main__':
28.6.2 by Stuart Bishop
Define local repository
424
    raise SystemExit(unittest.main())