Merge lp:~jaypipes/drizzle/replication-testing into lp:~drizzle-trunk/drizzle/development

Proposed by Jay Pipes
Status: Rejected
Rejected by: Brian Aker
Proposed branch: lp:~jaypipes/drizzle/replication-testing
Merge into: lp:~drizzle-trunk/drizzle/development
Prerequisite: lp:~jaypipes/drizzle/publisher
Diff against target: 846 lines
To merge this branch: bzr merge lp:~jaypipes/drizzle/replication-testing
Reviewer Review Type Date Requested Status
Eric Day (community) Approve
Brian Aker Pending
Drizzle Developers Pending
Review via email: mp+24246@code.launchpad.net

Description of the change

This patch adds a testing framework for the new replication pieces. There is currently only a single test case for verifying the information written to the publisher manifest file in the transaction log module.

To post a comment you must log in.
Revision history for this message
Eric Day (eday) wrote :

Looks great! I like the use of the Python unit testing framework, I think I've seen that before... :)

review: Approve

Unmerged revisions

1442. By Jay Pipes <jpipes@serialcoder>

Connect replication test suite into main transaction_log test suite

1441. By Jay Pipes <jpipes@serialcoder>

Adds a custom test suite and framework for testing the replication pieces. The first test case tests basic functionality of creating and writing a serialized publisher manifest file.

1440. By Jay Pipes <jpipes@serialcoder>

Publisher manifest file now written to disk properly (was using incorrect ByteSize() instead of std::string::size(). Also, manifest is read on startup

1439. By Jay Pipes <jpipes@serialcoder>

Merge Monty build fixes

1438. By Jay Pipes <jpipes@serialcoder>

remove references to /drizzled/message/replication.pb.h

1437. By Jay Pipes <jpipes@serialcoder>

Moves replication.proto out of /drizzled/message and into /plugin/transaction_log/ since it is implementation-specific.

1436. By Jay Pipes <jpipes@serialcoder>

Remove unused ZeroCopyStream buffer

1435. By Jay Pipes <jpipes@serialcoder>

TransactionLogPublisher now writes manifest file to disk and syncs on close

1434. By Jay Pipes <jpipes@serialcoder>

Merge trunk and resolve conflicts

1433. By Jay Pipes <jpipes@serialcoder>

Merge trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2010-02-24 00:19:06 +0000
3+++ .bzrignore 2010-04-27 17:46:21 +0000
4@@ -304,3 +304,4 @@
5 *.reject
6 config/pandora_vc_revinfo
7 drizzled/message/schema_writer
8+replication_pb2.py
9
10=== modified file 'plugin/replication_dictionary/tests/r/replication_publisher_channels.result'
11--- plugin/replication_dictionary/tests/r/replication_publisher_channels.result 2010-04-27 17:46:20 +0000
12+++ plugin/replication_dictionary/tests/r/replication_publisher_channels.result 2010-04-27 17:46:21 +0000
13@@ -1,4 +1,5 @@
14 use data_dictionary;
15+SET @@global.transaction_log_truncate_debug= 1;
16 SELECT * FROM data_dictionary.replication_publisher_channels;
17 PUBLISHER CHANNEL_ID LAST_APPLIED_TRANSACTION_ID LAST_APPLIED_END_TIMESTAMP
18 transaction_log_publisher 1 0 0
19
20=== modified file 'plugin/replication_dictionary/tests/t/replication_publisher_channels.test'
21--- plugin/replication_dictionary/tests/t/replication_publisher_channels.test 2010-04-27 17:46:20 +0000
22+++ plugin/replication_dictionary/tests/t/replication_publisher_channels.test 2010-04-27 17:46:21 +0000
23@@ -2,6 +2,8 @@
24
25 use data_dictionary;
26
27+SET @@global.transaction_log_truncate_debug= 1;
28+
29 SELECT * FROM data_dictionary.replication_publisher_channels;
30
31 use test;
32
33=== modified file 'plugin/transaction_log/publisher.cc'
34--- plugin/transaction_log/publisher.cc 2010-04-27 17:46:20 +0000
35+++ plugin/transaction_log/publisher.cc 2010-04-27 17:46:21 +0000
36@@ -42,6 +42,8 @@
37 #include <utility>
38
39 #include <drizzled/internal/my_sys.h> /* for internal::my_sync */
40+#include <drizzled/gettext.h>
41+#include <drizzled/errmsg_print.h>
42
43 using namespace std;
44 using namespace drizzled;
45@@ -56,14 +58,9 @@
46 channel_apply_map(),
47 manifest_file(-1)
48 {
49+ /** @todo handle errors much better here! */
50+ (void) readManifestFile();
51 manifest_file= open(MANIFEST_FILE_NAME.c_str(), O_CREAT|O_SYNC|O_WRONLY, S_IRWXU);
52- /** @todo read in serialized publisher manifest file */
53- PublisherManifest &pm= getPublisherManifest();
54- PublisherManifest::PublisherApplyInfo *appl_info= pm.add_apply_info();
55- appl_info->set_channel_id(1);
56- appl_info->set_last_applied_transaction_id(0);
57- appl_info->set_last_applied_end_timestamp(0);
58- channel_apply_map.insert(make_pair(1, appl_info));
59 pthread_mutex_init(&latch, NULL);
60 }
61
62@@ -73,21 +70,73 @@
63 {
64 (void) writeManifestFile();
65 (void) syncManifestFile();
66+ close(manifest_file);
67 }
68 catch (...)
69 {} /* enforce nothrow guarantee on destructors... */
70 pthread_mutex_destroy(&latch);
71 }
72
73+bool TransactionLogPublisher::readManifestFile()
74+{
75+ PublisherManifest &pm= getPublisherManifest();
76+ int read_file= open(MANIFEST_FILE_NAME.c_str(), O_RDONLY);
77+ if (read_file == -1)
78+ {
79+ /*
80+ * If file doesn't exist, just create a default manifest message
81+ * in memory. This message will be written to disk into the
82+ * manifest file later.
83+ */
84+ if (errno == ENOENT)
85+ {
86+ /* Create a default manifest with channel 1 and no applied transactions */
87+ PublisherManifest::PublisherApplyInfo *appl_info= pm.add_apply_info();
88+ appl_info->set_channel_id(1);
89+ appl_info->set_last_applied_transaction_id(0);
90+ appl_info->set_last_applied_end_timestamp(0);
91+ (void) writeManifestFile();
92+ (void) syncManifestFile();
93+ }
94+ else
95+ {
96+ errmsg_printf(ERRMSG_LVL_ERROR, _("Unable to read publisher's manifest file! Got error: %s\n"), strerror(errno));
97+ close(read_file);
98+ return false;
99+ }
100+ }
101+ else
102+ {
103+ if (not pm.ParseFromFileDescriptor(read_file))
104+ {
105+ errmsg_printf(ERRMSG_LVL_ERROR,
106+ _("Unable to parse publisher's manifest file!\n"
107+ "PublisherManifest was initialized, but contains corrupted data:\n%s\n"),
108+ pm.DebugString().c_str());
109+ close(read_file);
110+ return false;
111+ }
112+ }
113+ uint32_t num_channels= pm.apply_info_size();
114+ for (uint32_t x= 0; x < num_channels; ++x)
115+ {
116+ PublisherManifest::PublisherApplyInfo *apply_info= pm.mutable_apply_info(x);
117+ channel_apply_map.insert(make_pair(apply_info->channel_id(), apply_info));
118+ }
119+ close(read_file);
120+ return true;
121+}
122+
123 void TransactionLogPublisher::clear()
124 {
125- for (ChannelApplyMap::iterator iter= channel_apply_map.begin();
126- iter != channel_apply_map.end();
127- ++iter)
128- {
129- ((*iter).second)->set_last_applied_transaction_id(0);
130- ((*iter).second)->set_last_applied_end_timestamp(0);
131- }
132+ publisher_manifest.Clear();
133+ /* Create a default manifest with channel 1 and no applied transactions */
134+ PublisherManifest::PublisherApplyInfo *appl_info= publisher_manifest.add_apply_info();
135+ appl_info->set_channel_id(1);
136+ appl_info->set_last_applied_transaction_id(0);
137+ appl_info->set_last_applied_end_timestamp(0);
138+ (void) ftruncate(manifest_file, 0);
139+ (void) internal::my_sync(manifest_file, 0);
140 }
141
142 void TransactionLogPublisher::setLastApplied(const message::Transaction &applied)
143@@ -130,10 +179,14 @@
144 bool result= pm.SerializeToString(&write_buffer);
145 if (unlikely(not result))
146 {
147+ errmsg_printf(ERRMSG_LVL_ERROR,
148+ _("Unable to write publisher's manifest file!\n"
149+ "PublisherManifest contains data:\n%s\n"),
150+ pm.DebugString().c_str());
151 unlock();
152 return false;
153 }
154- uint32_t num_bytes_to_write= pm.ByteSize();
155+ uint32_t num_bytes_to_write= write_buffer.size();
156 ssize_t written= pwrite(manifest_file, write_buffer.c_str(), num_bytes_to_write, 0);
157 result= (written != static_cast<ssize_t>(num_bytes_to_write));
158 unlock();
159
160=== modified file 'plugin/transaction_log/publisher.h'
161--- plugin/transaction_log/publisher.h 2010-04-27 17:46:20 +0000
162+++ plugin/transaction_log/publisher.h 2010-04-27 17:46:21 +0000
163@@ -144,6 +144,10 @@
164 return publisher_manifest;
165 }
166 /**
167+ * Reads the publisher's manifest file on startup
168+ */
169+ bool readManifestFile();
170+ /**
171 * Writes the publisher's manifest file
172 */
173 bool writeManifestFile();
174
175=== modified file 'plugin/transaction_log/replication.proto'
176--- plugin/transaction_log/replication.proto 2010-04-27 17:46:20 +0000
177+++ plugin/transaction_log/replication.proto 2010-04-27 17:46:21 +0000
178@@ -1,5 +1,3 @@
179-option optimize_for = SPEED;
180-
181 /**
182 * A class representing this information
183 * a subscriber keeps about the data it
184
185=== added directory 'plugin/transaction_log/tests/inc'
186=== added file 'plugin/transaction_log/tests/inc/publisher.manifest'
187--- plugin/transaction_log/tests/inc/publisher.manifest 1970-01-01 00:00:00 +0000
188+++ plugin/transaction_log/tests/inc/publisher.manifest 2010-04-27 17:46:21 +0000
189@@ -0,0 +1,2 @@
190+
191+
192¹è½¢Ã¥¡
193\ No newline at end of file
194
195=== modified file 'plugin/transaction_log/tests/r/publisher_manifest.result'
196--- plugin/transaction_log/tests/r/publisher_manifest.result 2010-04-27 17:46:20 +0000
197+++ plugin/transaction_log/tests/r/publisher_manifest.result 2010-04-27 17:46:21 +0000
198@@ -9,3 +9,10 @@
199 DROP TABLE t1;
200 1var/master-data/publisher.manifest
201 SET GLOBAL transaction_log_truncate_debug= true;
202+testDefaultStartupVariables (__main__.TestPublisherManifest) ... ok
203+testWritePublisherManifest (__main__.TestPublisherManifest) ... ok
204+
205+----------------------------------------------------------------------
206+Ran 2 tests
207+
208+OK
209
210=== added directory 'plugin/transaction_log/tests/replication'
211=== added file 'plugin/transaction_log/tests/replication/COPYING'
212--- plugin/transaction_log/tests/replication/COPYING 1970-01-01 00:00:00 +0000
213+++ plugin/transaction_log/tests/replication/COPYING 2010-04-27 17:46:21 +0000
214@@ -0,0 +1,32 @@
215+Drizzle Replication Test Suite
216+
217+Copyright (C) 2010 Jay Pipes <jaypipes@gmail.com>
218+All rights reserved.
219+
220+Redistribution and use in source and binary forms, with or without
221+modification, are permitted provided that the following conditions are
222+met:
223+
224+ * Redistributions of source code must retain the above copyright
225+notice, this list of conditions and the following disclaimer.
226+
227+ * Redistributions in binary form must reproduce the above
228+copyright notice, this list of conditions and the following disclaimer
229+in the documentation and/or other materials provided with the
230+distribution.
231+
232+ * The names of its contributors may not be used to endorse or
233+promote products derived from this software without specific prior
234+written permission.
235+
236+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
237+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
238+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
239+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
240+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
241+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
242+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
243+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
244+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
245+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
246+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
247
248=== added directory 'plugin/transaction_log/tests/replication/bootstrap'
249=== added file 'plugin/transaction_log/tests/replication/bootstrap/ib_logfile0'
250Binary files plugin/transaction_log/tests/replication/bootstrap/ib_logfile0 1970-01-01 00:00:00 +0000 and plugin/transaction_log/tests/replication/bootstrap/ib_logfile0 2010-04-27 17:46:21 +0000 differ
251=== added file 'plugin/transaction_log/tests/replication/bootstrap/ib_logfile1'
252Binary files plugin/transaction_log/tests/replication/bootstrap/ib_logfile1 1970-01-01 00:00:00 +0000 and plugin/transaction_log/tests/replication/bootstrap/ib_logfile1 2010-04-27 17:46:21 +0000 differ
253=== added file 'plugin/transaction_log/tests/replication/bootstrap/ibdata1'
254Binary files plugin/transaction_log/tests/replication/bootstrap/ibdata1 1970-01-01 00:00:00 +0000 and plugin/transaction_log/tests/replication/bootstrap/ibdata1 2010-04-27 17:46:21 +0000 differ
255=== added file 'plugin/transaction_log/tests/replication/test_publisher_manifest.py'
256--- plugin/transaction_log/tests/replication/test_publisher_manifest.py 1970-01-01 00:00:00 +0000
257+++ plugin/transaction_log/tests/replication/test_publisher_manifest.py 2010-04-27 17:46:21 +0000
258@@ -0,0 +1,262 @@
259+#! /usr/bin/python
260+#
261+# Drizzle Replication Test Suite
262+#
263+# Copyright (c) 2010 Jay Pipes
264+#
265+# All rights reserved.
266+#
267+# Use and distribution licensed under the BSD license. See the
268+# COPYING file in the root project directory for full text.
269+
270+"""Drizzle Transaction Log Test Suite - Publisher Manifest
271+
272+This tests the startup process of the Drizzle server and
273+the reading of the transaction log's publisher manifest
274+"""
275+
276+__author__ = 'Jay Pipes <jaypipes@gmail.com>'
277+
278+#
279+# This file contains a test for reading the transaction
280+# log's publisher manifest file on startup.
281+#
282+# It is necessary to use this testing suite
283+# because the test-run.pl script assumes too many things
284+# about the Drizzle test environment, and we need to
285+# initialize both proper and corrupted publisher manifests
286+# in order to test the startup process correctly.
287+#
288+
289+import os
290+import sys
291+import commands
292+import unittest
293+
294+drizzled_root_dir= os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..', '..'))
295+trx_log_root_dir= os.path.join(drizzled_root_dir, 'plugin', 'transaction_log')
296+replication_test_root_dir= os.path.join(trx_log_root_dir, 'tests', 'replication')
297+
298+def compile_python_proto(protodir, protofile):
299+ """Compiles a proto into the proto_pb2.py file and puts it into the main test directory"""
300+ protocommand= "protoc --python_out=. --proto_path=%s %s" % (protodir, os.path.join(protodir, protofile))
301+ (retcode, output)= commands.getstatusoutput(protocommand)
302+ if retcode != 0:
303+ raise Exception("Failed to compile Python protobuffers for %s\nTried command: %s" % (protofile, protocommand))
304+
305+compile_python_proto(trx_log_root_dir, 'replication.proto') # proto is in /plugin/transaction_log/
306+
307+import replication_pb2 as rpb
308+
309+from testlib.server import DrizzledServer
310+from testlib.client import DrizzleClient
311+
312+class TestPublisherManifest(unittest.TestCase):
313+
314+ DEFAULT_START_OPTIONS= {
315+ 'mysql_protocol_port': DrizzledServer.DEFAULT_MYSQL_PROTOCOL_PORT,
316+ 'drizzle_protocol_port': DrizzledServer.DEFAULT_DRIZZLE_PROTOCOL_PORT,
317+ 'datadir': os.path.join(replication_test_root_dir, "var"),
318+ 'basedir': drizzled_root_dir
319+ }
320+
321+ def setUp(self):
322+ pass
323+
324+ def tearDown(self):
325+ if self.server and self.server.ping():
326+ self.server.stop()
327+ self.server.clear()
328+ self.resetDataDir()
329+
330+ def resetDataDir(self):
331+ """Copies default InnoDB data files from bootstrap into datadir to speed up starts"""
332+ (retcode, ignored)= commands.getstatusoutput("rm -rf %s" % os.path.join(replication_test_root_dir, "var", "*"))
333+
334+ def bootstrapDataFiles(self):
335+ """Copies default InnoDB data files from bootstrap into datadir to speed up starts"""
336+ (retcode, ignored)= commands.getstatusoutput("cp -r %s %s" % (os.path.join(replication_test_root_dir, "bootstrap", "*"),
337+ os.path.join(replication_test_root_dir, "var", "*")))
338+
339+ def testWritePublisherManifest(self):
340+ #
341+ # Here, we test the scenario of a newly
342+ # started server with no data history. There
343+ # should be no publisher manifest file at startup
344+ # and after plugin processing, the publisher manifest
345+ # file should exist but have no size. Once a few transactions
346+ # are applied on the server, the manifest should show data
347+ # fields with the last transaction ID and end timestamp
348+ #
349+ startup_options= {
350+ 'transaction-log-enable': '1'
351+ }
352+ for k, v in TestPublisherManifest.DEFAULT_START_OPTIONS.iteritems():
353+ startup_options[k]= v
354+
355+ self.server= DrizzledServer(startup_options)
356+
357+ if self.server and self.server.ping():
358+ self.server.stop()
359+ self.resetDataDir()
360+
361+ self.bootstrapDataFiles()
362+
363+ # Test that the publisher manifest does not exist
364+ # before startup.
365+
366+ manifest_filepath= os.path.join(replication_test_root_dir, "var", "publisher.manifest")
367+
368+ self.assertFalse(os.path.exists(manifest_filepath))
369+
370+ self.assertTrue(self.server.start())
371+ self.assertTrue(self.server.ping())
372+
373+ # Test that a publisher manifest exists after startup
374+ # but has no size since it has not been written to yet
375+
376+ self.assertTrue(os.path.exists(manifest_filepath))
377+ self.assertEqual(os.path.getsize(manifest_filepath), 0)
378+
379+ # Now add a single transaction and verify the publisher
380+ # manifest contains proper information by reading the
381+ # manifest into a protobuffer container class object
382+
383+ client_options= {
384+ 'basedir': startup_options['basedir'],
385+ 'port': startup_options['mysql_protocol_port']
386+ }
387+
388+ self.client= DrizzleClient(client_options)
389+ self.assertTrue(self.client.execute("CREATE SCHEMA test"))
390+ self.assertTrue(self.client.execute("CREATE TABLE t1 (id INT NOT NULL PRIMARY KEY)", "test"))
391+ self.assertTrue(self.client.execute("INSERT INTO t1 VALUES(1)", "test"))
392+
393+ # Create the manifest GPB class object
394+ # by reading in the publisher manifest, and then
395+ # check to ensure manifest data memebers are appropriate
396+
397+ f= open(manifest_filepath)
398+
399+ try:
400+ manifest= rpb.PublisherManifest()
401+ manifest.ParseFromString(f.read())
402+ except Exception as e:
403+ self.assertFalse(True)
404+
405+ f.close()
406+
407+ self.assertEqual(len(manifest.apply_info), 1)
408+ self.assertEqual(manifest.apply_info[0].channel_id, 1)
409+ self.assertEqual(manifest.apply_info[0].last_applied_transaction_id, 3)
410+ self.assertNotEqual(manifest.apply_info[0].last_applied_end_timestamp, 0)
411+
412+ self.assertTrue(self.server.stop())
413+ self.assertFalse(self.server.ping())
414+
415+ def testDefaultStartupVariables(self):
416+ #
417+ # Here, we test the "default" scenario of a newly
418+ # started server with no data history. We check to see
419+ # if the replication_publisher and replication_publisher_channels
420+ # views in the data_dictionary contain proper values
421+ #
422+ startup_options= {
423+ 'transaction-log-enable': '1'
424+ }
425+ for k, v in TestPublisherManifest.DEFAULT_START_OPTIONS.iteritems():
426+ startup_options[k]= v
427+
428+ client_options= {
429+ 'basedir': startup_options['basedir'],
430+ 'port': startup_options['mysql_protocol_port']
431+ }
432+
433+ self.server= DrizzledServer(startup_options)
434+
435+ if self.server and self.server.ping():
436+ self.server.stop()
437+ self.resetDataDir()
438+
439+ self.bootstrapDataFiles()
440+ self.assertTrue(self.server.start())
441+ self.assertTrue(self.server.ping())
442+ self.client= DrizzleClient(client_options)
443+
444+ expected_results_1= [
445+ {
446+ 'PUBLISHER_NAME': 'transaction_log_publisher'
447+ }
448+ ]
449+ results= self.client.fetchAllAssoc("SELECT * FROM DATA_DICTIONARY.REPLICATION_PUBLISHERS")
450+
451+ self.assertTrue(len(results) == len(expected_results_1))
452+
453+ rowno= 0
454+ for row in results:
455+ for k, v in row.iteritems():
456+ self.assertEqual(str(expected_results_1[rowno][k]), str(v))
457+ rowno+= 1
458+
459+ expected_results_2= [
460+ {
461+ 'PUBLISHER': 'transaction_log_publisher',
462+ 'CHANNEL_ID': 1,
463+ 'LAST_APPLIED_TRANSACTION_ID': 0,
464+ 'LAST_APPLIED_END_TIMESTAMP': 0
465+ }
466+ ]
467+ results= self.client.fetchAllAssoc("SELECT * FROM DATA_DICTIONARY.REPLICATION_PUBLISHER_CHANNELS")
468+
469+ self.assertTrue(len(results) == len(expected_results_2))
470+
471+ rowno= 0
472+ for row in results:
473+ for k, v in row.iteritems():
474+ self.assertEqual(str(expected_results_2[rowno][k]), str(v))
475+ rowno+= 1
476+
477+ self.assertTrue(self.server.stop())
478+ self.assertFalse(self.server.ping())
479+
480+#
481+# Code below taken verbatim from Eric Day's prototest
482+# test suite in the mysql_protocol Drizzle plugin
483+#
484+import optparse
485+
486+parser = optparse.OptionParser()
487+
488+parser.add_option("-t", "--test", dest="testcase", default=None,
489+ help="Test case to run", metavar="TESTCASE")
490+
491+(options, args) = parser.parse_args()
492+
493+class CustomTestRunner(unittest.TextTestRunner):
494+ def run(self, test):
495+ result = self._makeResult()
496+ test(result)
497+ result.printErrors()
498+ self.stream.writeln(result.separator2)
499+ run = result.testsRun
500+ self.stream.writeln("Ran %d test%s" % (run, run != 1 and "s" or ""))
501+ self.stream.writeln()
502+ if not result.wasSuccessful():
503+ self.stream.write("FAILED (")
504+ failed, errored = map(len, (result.failures, result.errors))
505+ if failed:
506+ self.stream.write("failures=%d" % failed)
507+ if errored:
508+ if failed: self.stream.write(", ")
509+ self.stream.write("errors=%d" % errored)
510+ self.stream.writeln(")")
511+ else:
512+ self.stream.writeln("OK")
513+ return result
514+
515+if __name__ == '__main__':
516+ if options.testcase is None:
517+ suite = unittest.TestLoader().loadTestsFromModule(__import__('__main__'))
518+ else:
519+ suite = unittest.TestLoader().loadTestsFromTestCase(eval(options.testcase))
520+ CustomTestRunner(stream=sys.stdout, verbosity=2).run(suite)
521
522=== added directory 'plugin/transaction_log/tests/replication/testlib'
523=== added file 'plugin/transaction_log/tests/replication/testlib/__init__.py'
524--- plugin/transaction_log/tests/replication/testlib/__init__.py 1970-01-01 00:00:00 +0000
525+++ plugin/transaction_log/tests/replication/testlib/__init__.py 2010-04-27 17:46:21 +0000
526@@ -0,0 +1,10 @@
527+#! /usr/bin/python
528+#
529+# Drizzle Replication Test Suite
530+#
531+# Copyright (c) 2010 Jay Pipes
532+#
533+# All rights reserved.
534+#
535+# Use and distribution licensed under the BSD license. See the
536+# COPYING file in the root project directory for full text.
537
538=== added file 'plugin/transaction_log/tests/replication/testlib/client.py'
539--- plugin/transaction_log/tests/replication/testlib/client.py 1970-01-01 00:00:00 +0000
540+++ plugin/transaction_log/tests/replication/testlib/client.py 2010-04-27 17:46:21 +0000
541@@ -0,0 +1,137 @@
542+#! /usr/bin/python
543+#
544+# Drizzle Replication Test Suite
545+#
546+# Copyright (c) 2010 Jay Pipes
547+#
548+# All rights reserved.
549+#
550+# Use and distribution licensed under the BSD license. See the
551+# COPYING file in the root project directory for full text.
552+
553+"""Defines the adapter for controlling a Drizzle database client"""
554+
555+import os
556+import commands
557+import sys
558+import time
559+import unittest
560+
561+class DrizzleClientException(Exception):
562+ pass
563+
564+class DrizzleClient:
565+ """A class wrapping a Drizzled client"""
566+
567+ DEFAULT_PORT= 9306
568+
569+ def __init__(self, options= {}):
570+ self.start_options= options
571+ self.cleanStartOptions()
572+
573+ def getPort(self):
574+ """Returns port the client connects to"""
575+ return self.start_options["port"]
576+
577+ def cleanStartOptions(self):
578+ """Cleans up the startup options dictionary"""
579+ clean_start_options= {}
580+ for key in self.start_options.keys():
581+ # Ensure option names are output as --option_name
582+ # and clean the self.start_options array to remove any leading --
583+ # and convert - to _
584+ value= self.start_options[key]
585+
586+ if not key.startswith("--"):
587+ clean_key= key.replace('-','_')
588+ else:
589+ clean_key= key[2:].replace("-","_")
590+ clean_start_options[clean_key]= value
591+
592+ self.start_options= clean_start_options
593+
594+ def getStartOptionString(self):
595+ """Builds the string of options to pass to drizzled on startup"""
596+ out_options= ["%s=%s" % ("--" + key, value) for key, value in self.start_options.iteritems()]
597+
598+ return " ".join(out_options)
599+
600+ def execute(self, statement, schema=None):
601+ """Executes a supplied SQL statement"""
602+ if schema is None:
603+ schema_string= ""
604+ else:
605+ schema_string= "--database=%s" % schema
606+ client_cmd= "%s --port=%d %s -e\"%s\"" % (os.path.join(self.start_options["basedir"], "client", "drizzle")
607+ , self.start_options["port"]
608+ , schema_string
609+ , statement)
610+ (retcode, output)= commands.getstatusoutput(client_cmd)
611+ if retcode != 0:
612+ print "Client on port %d failed to execute SQL:\n\"%s\"\nGot error: %s" % (self.start_options["port"], statement, output)
613+ return False
614+ return True
615+
616+ def executeFile(self, file):
617+ """Executes SQL statements in a supplied file"""
618+ client_cmd= "%s --port=%d < %s" % (os.path.join(self.start_options["basedir"], "client", "drizzle")
619+ , self.start_options["port"]
620+ , file)
621+ (retcode, output)= commands.getstatusoutput(client_cmd)
622+ if retcode != 0:
623+ print "Client on port %d failed to execute SQL in file:\n\"%s\"\nGot error: %s" % (self.start_options["port"], file, output)
624+ return False
625+ return True
626+
627+ def fetchAsDict(self, statement):
628+ """Executes a supplied SELECT or SHOW statement and returns a dictionary with first column as keys, second column as values."""
629+ client_cmd= "%s --port=%d -e\"%s\"" % (os.path.join(self.start_options["basedir"], "client", "drizzle")
630+ , self.start_options["port"]
631+ , statement)
632+ (retcode, output)= commands.getstatusoutput(client_cmd)
633+ if retcode != 0:
634+ print "Client on port %d failed to execute SQL:\n\"%s\"\nGot error: %s" % (self.start_options["port"], statement, output)
635+ return {}
636+ else:
637+ # Now we build the results dictionary...
638+ results= {}
639+ # The first line is the field names separated by a tab
640+ # Every line after is a tab-delimited field values.
641+ lines= output.split("\n")
642+ fields= lines[0].split("\t")
643+ for line in lines[1:]:
644+ data_fields= line.split("\t")
645+ results[data_fields[0]]= data_fields[1]
646+ return results
647+
648+ def fetchAllAssoc(self, statement):
649+ """Executes a supplied SELECT or SHOW statement and returns a sequence with associative dictionary of results."""
650+ client_cmd= "%s --port=%d -e\"%s\"" % (os.path.join(self.start_options["basedir"], "client", "drizzle")
651+ , self.start_options["port"]
652+ , statement)
653+ (retcode, output)= commands.getstatusoutput(client_cmd)
654+ if retcode != 0:
655+ print "Client on port %d failed to execute SQL:\n\"%s\"\nGot error: %s" % (self.start_options["port"], statement, output)
656+ return []
657+ else:
658+ # Now we build the results dictionary...
659+ results= []
660+ # The first line is the field names separated by a tab
661+ # Every line after is a tab-delimited field values.
662+ lines= output.split("\n")
663+ fields= lines[0].split("\t")
664+ for line in lines[1:]:
665+ row= {}
666+ data_fields= line.split("\t")
667+ fieldno= 0
668+ for field in fields:
669+ row[field]= data_fields[fieldno]
670+ fieldno= fieldno + 1
671+ results.append(row)
672+ return results
673+
674+class TestDrizzleClient(unittest.TestCase):
675+ pass
676+
677+if __name__ == '__main__':
678+ unittest.main()
679
680=== added file 'plugin/transaction_log/tests/replication/testlib/server.py'
681--- plugin/transaction_log/tests/replication/testlib/server.py 1970-01-01 00:00:00 +0000
682+++ plugin/transaction_log/tests/replication/testlib/server.py 2010-04-27 17:46:21 +0000
683@@ -0,0 +1,152 @@
684+#! /usr/bin/python
685+#
686+# Drizzle Replication Test Suite
687+#
688+# Copyright (c) 2010 Jay Pipes
689+#
690+# All rights reserved.
691+#
692+# Use and distribution licensed under the BSD license. See the
693+# COPYING file in the root project directory for full text.
694+
695+"""Defines the adapter for controlling a Drizzle database server"""
696+
697+import os
698+import commands
699+import sys
700+import time
701+import unittest
702+
703+class DrizzledServerStartupException(Exception):
704+ pass
705+
706+class DrizzledServerShutdownException(Exception):
707+ pass
708+
709+class DrizzledServer:
710+ """A class responsible for starting, stopping, and pinging a Drizzled server"""
711+
712+ DEFAULT_MYSQL_PROTOCOL_PORT= 9306
713+ DEFAULT_DRIZZLE_PROTOCOL_PORT= 9307
714+
715+ def __init__(self, options= {}):
716+ self.start_options= options
717+ self.defaults_file= ""
718+ self.cleanStartOptions()
719+
720+ def getPort(self, protocol="drizzle"):
721+ """Returns port the server listens for supplied protocol"""
722+ return self.start_options[protocol + "_protocol_port"]
723+
724+ def cleanStartOptions(self):
725+ """Cleans up the startup options dictionary"""
726+ clean_start_options= {}
727+ for key in self.start_options.keys():
728+ # Ensure option names are output as --option_name
729+ # and clean the self.start_options array to remove any leading --
730+ # and convert - to _
731+ value= self.start_options[key]
732+
733+ if not key.startswith("--"):
734+ clean_key= key.replace('-','_')
735+ else:
736+ clean_key= key[2:].replace("-","_")
737+
738+ # The defaults-file must ALWAYS be the first command-line
739+ # option given to a drizzled server, so we strip it and
740+ # store it locally here...
741+ if clean_key == 'defaults_file':
742+ self.defaults_file= "--defaults-file=%s" % value
743+ else:
744+ clean_start_options[clean_key]= value
745+ self.start_options= clean_start_options
746+
747+ def getStartOptionString(self):
748+ """Builds the string of options to pass to drizzled on startup"""
749+ out_options= ["%s=%s" % ("--" + key, value) for key, value in self.start_options.iteritems()]
750+
751+ return " ".join(out_options)
752+
753+ def start(self):
754+ """Starts a drizzled server"""
755+
756+ for x in ("mysql_protocol_port","basedir","datadir"):
757+ if x not in self.start_options.keys():
758+ raise DrizzledServerStartupException("Attempt to start server without required startup options")
759+
760+ start_option_string= self.getStartOptionString()
761+ start_cmd= "%s %s %s > %s 2>&1 &" % (os.path.join(self.start_options['basedir'], "drizzled", "drizzled"),
762+ self.defaults_file,
763+ start_option_string,
764+ os.path.join(self.start_options['datadir'], 'error.log'))
765+ server_output= os.system(start_cmd)
766+
767+ # Here, we sleep until the server is up and running or until a timeout occurs...
768+ timeout= 10
769+ timer= 0
770+ while not self.ping() and timer != timeout:
771+ time.sleep(.25)
772+ timer= timer + 1
773+
774+ return True
775+
776+ def ping(self, quiet= False):
777+ """Pings the server. Returns True if server is up and running, False otherwise."""
778+ ping_cmd= "%s --ping --port=%d" % (os.path.join(self.start_options['basedir'], "client", "drizzle"),
779+ self.start_options['mysql_protocol_port'])
780+
781+ (retcode, output)= commands.getstatusoutput(ping_cmd)
782+
783+ return retcode == 0
784+
785+ def clear(self):
786+ """Clears data files for the server."""
787+ (retcode, ignored)= commands.getstatusoutput("rm -rf %s; mkdir %s" % (self.start_options['datadir'], self.start_options['datadir']))
788+
789+ def stop(self):
790+ """Stops the server"""
791+ shutdown_cmd= "%s --shutdown --port=%d" % (os.path.join(self.start_options['basedir'], "client", "drizzle"),
792+ self.start_options['mysql_protocol_port'])
793+
794+ (retcode, output)= commands.getstatusoutput(shutdown_cmd)
795+
796+ return retcode == 0
797+
798+ def restart(self):
799+ self.stop()
800+ return self.start()
801+
802+ def stopAll(self):
803+ """Stops ALL drizzled servers"""
804+ (retcode, num_drizzled_running)= commands.getstatusoutput("ps aux | grep drizzled | wc -l")
805+ if retcode == 0 and int(num_drizzled_running) > 0:
806+ (retcode, ignored)= commands.getstatusoutput("killall -9 drizzled")
807+ time.sleep(3)
808+
809+class TestDrizzledServer(unittest.TestCase):
810+
811+ DEFAULT_START_OPTIONS= {
812+ 'mysql_protocol_port': DrizzledServer.DEFAULT_MYSQL_PROTOCOL_PORT,
813+ 'drizzle_protocol_port': DrizzledServer.DEFAULT_DRIZZLE_PROTOCOL_PORT,
814+ 'datadir': os.path.join(os.getcwd().partition("plugin")[0], "tests", "var"),
815+ 'basedir': os.path.join(os.getcwd().partition("plugin")[0], "drizzled")
816+ }
817+
818+ def setUp(self):
819+ self.server= DrizzledServer(TestDrizzledServer.DEFAULT_START_OPTIONS)
820+ if self.server.ping():
821+ self.server.stopAll()
822+
823+ def tearDown(self):
824+ self.server.stop()
825+
826+ def testStartAndPing(self):
827+ self.assertTrue(self.server.start())
828+ self.assertTrue(self.server.ping())
829+
830+ def testStartAndStop(self):
831+ self.assertTrue(self.server.start())
832+ self.assertTrue(self.server.stop())
833+
834+if __name__ == '__main__':
835+ unittest.main()
836
837=== added directory 'plugin/transaction_log/tests/replication/var'
838=== modified file 'plugin/transaction_log/tests/t/publisher_manifest.test'
839--- plugin/transaction_log/tests/t/publisher_manifest.test 2010-04-27 17:46:20 +0000
840+++ plugin/transaction_log/tests/t/publisher_manifest.test 2010-04-27 17:46:21 +0000
841@@ -14,3 +14,6 @@
842
843 # Truncate the log file to reset for the next test
844 --source ../plugin/transaction_log/tests/t/truncate_log.inc
845+
846+# Execute the external test suite for replication
847+--exec $DRIZZLE_TEST_DIR/../plugin/transaction_log/tests/replication/test_publisher_manifest.py