From 2ef8987da5d17a2a22fb37cdd19980b0a994c754 Mon Sep 17 00:00:00 2001 From: Shuhao Wu Date: Mon, 17 Sep 2018 10:51:24 -0400 Subject: [PATCH] Added some trivial integration tests Ported some very basic tests as well as created two interrupt/resume tests. --- .../cases/trivial_integration_tests.rb | 84 +++++++++++ .../ruby/ghostferry_integration/test_case.rb | 139 ++++++++++++++++++ test/integration/test.rb | 6 + 3 files changed, 229 insertions(+) create mode 100644 test/integration/cases/trivial_integration_tests.rb create mode 100644 test/integration/ruby/ghostferry_integration/test_case.rb create mode 100644 test/integration/test.rb diff --git a/test/integration/cases/trivial_integration_tests.rb b/test/integration/cases/trivial_integration_tests.rb new file mode 100644 index 00000000..c13b7299 --- /dev/null +++ b/test/integration/cases/trivial_integration_tests.rb @@ -0,0 +1,84 @@ +require "json" +require "ghostferry_integration" + +class TrivialIntegrationTests < GhostferryIntegration::TestCase + def ghostferry_main_path + "go/minimal.go" + end + + def test_copy_data_without_any_writes_to_source + @dbs.seed_simple_database_with_single_table + @ghostferry.run + assert_test_table_is_identical + end + + def test_copy_data_with_writes_to_source + use_datawriter + + @dbs.seed_simple_database_with_single_table + + @ghostferry.run + assert_test_table_is_identical + end + + def test_interrupt_resume_with_writes_to_source + @dbs.seed_simple_database_with_single_table + + dumped_state = nil + with_isolated_setup_and_teardown do + use_datawriter + + batches_written = 0 + @ghostferry.on_status(Ghostferry::Status::AFTER_ROW_COPY) do + batches_written += 1 + if batches_written >= 2 + @ghostferry.send_signal("TERM") + end + end + + dumped_state = @ghostferry.run_expecting_interrupt + assert_basic_fields_exist_in_dumped_state(dumped_state) + end + + # We want to write some data to the source database while Ghostferry is down + # to verify that it is copied over. + 5.times do + @datawriter.insert_data(@dbs.source) + @datawriter.update_data(@dbs.source) + @datawriter.delete_data(@dbs.source) + end + + with_isolated_setup_and_teardown do + use_datawriter + @ghostferry.run(dumped_state) + + assert_test_table_is_identical + end + end + + def test_interrupt_resume_when_table_has_completed + @dbs.seed_simple_database_with_single_table + dumped_state = nil + + results = @dbs.source.query("SELECT COUNT(*) as cnt FROM #{GhostferryIntegration::DbManager::DEFAULT_FULL_TABLE_NAME}") + rows = results.first["cnt"] + + with_isolated_setup_and_teardown do + use_datawriter + + @ghostferry.on_status(Ghostferry::Status::ROW_COPY_COMPLETED) do + @ghostferry.send_signal("TERM") + end + + dumped_state = @ghostferry.run_expecting_interrupt + assert_basic_fields_exist_in_dumped_state(dumped_state) + end + + with_isolated_setup_and_teardown do + use_datawriter + @ghostferry.run(dumped_state) + + assert_test_table_is_identical + end + end +end diff --git a/test/integration/ruby/ghostferry_integration/test_case.rb b/test/integration/ruby/ghostferry_integration/test_case.rb new file mode 100644 index 00000000..c97efada --- /dev/null +++ b/test/integration/ruby/ghostferry_integration/test_case.rb @@ -0,0 +1,139 @@ +require "logger" +require "minitest" +require "minitest/hooks/test" + +module GhostferryIntegration + class TestCase < Minitest::Test + include Minitest::Hooks + include GhostferryIntegration + + ############## + # Test Hooks # + ############## + + def before_all + @logger = Logger.new(STDOUT) + if ENV["DEBUG"] == "1" + @logger.level = Logger::DEBUG + else + @logger.level = Logger::INFO + end + @ghostferry = Ghostferry.new(ghostferry_main_path, logger: @logger) + end + + def after_all + @ghostferry.remove_binary + end + + def before_setup + @dbs = DbManager.new(logger: @logger) + @dbs.reset_data + + setup_ghostferry_datawriter + end + + def after_teardown + teardown_ghostferry_datawriter + end + + ###################### + # Test Setup Helpers # + ###################### + + # If multiple Ghostferry runs are needed within a single test, such as in + # the case of interrupt/resume testing, we will need to wrap each + # @ghostferry.run within a block for this method. + # + # This method doesn't destroy the database state like before_setup and + # after_teardown does. + def with_isolated_setup_and_teardown + setup_ghostferry_datawriter + yield + teardown_ghostferry_datawriter + end + + # This setup the datawriter to start when Ghostferry start and stop when + # cutover is about to take place. + # + # The on_write block is called everytime the datawriter writes a row with + # the argument op, id. + # + # op: "INSERT"/"UPDATE"/"DELETE" + # id: the primary id of the row inserted + def use_datawriter(&on_write) + start_datawriter_with_ghostferry(&on_write) + stop_datawriter_during_cutover + end + + ##################### + # Assertion Helpers # + ##################### + + def assert_test_table_is_identical + source, target = @dbs.source_and_target_table_metrics + + assert source[DbManager::DEFAULT_FULL_TABLE_NAME][:row_count] > 0 + assert target[DbManager::DEFAULT_FULL_TABLE_NAME][:row_count] > 0 + + assert_equal( + source[DbManager::DEFAULT_FULL_TABLE_NAME][:checksum], + target[DbManager::DEFAULT_FULL_TABLE_NAME][:checksum], + ) + + assert_equal( + source[DbManager::DEFAULT_FULL_TABLE_NAME][:sample_row], + target[DbManager::DEFAULT_FULL_TABLE_NAME][:sample_row], + ) + end + + # Use this method to assert the validity of the structure of the dumped + # state. + # + # To actually assert the validity of the data within the dumped state, you + # have to do it manually. + def assert_basic_fields_exist_in_dumped_state(dumped_state) + refute dumped_state.nil? + refute dumped_state["GhostferryVersion"].nil? + refute dumped_state["LastKnownTableSchemaCache"].nil? + refute dumped_state["LastSuccessfulPrimaryKeys"].nil? + refute dumped_state["CompletedTables"].nil? + refute dumped_state["LastWrittenBinlogPosition"].nil? + end + + protected + def ghostferry_main_path + raise NotImplementedError + end + + private + + def start_datawriter_with_ghostferry(&on_write) + @ghostferry.on_status(Ghostferry::Status::READY) do + @datawriter.start(&on_write) + end + end + + def stop_datawriter_during_cutover + @ghostferry.on_status(Ghostferry::Status::ROW_COPY_COMPLETED) do + # At the start of the cutover phase, we have to set the database to + # read-only. This is done by stopping the datawriter. + @datawriter.stop + @datawriter.join + end + end + + def setup_ghostferry_datawriter + @ghostferry.reset_state + @datawriter = DataWriter.new(@dbs.source_db_config, logger: @logger) + end + + # This should be a no op if ghostferry and datawriter have already been + # stopped. + def teardown_ghostferry_datawriter + @datawriter.stop + @datawriter.join + + @ghostferry.stop_and_cleanup + end + end +end diff --git a/test/integration/test.rb b/test/integration/test.rb new file mode 100644 index 00000000..ece2b0d3 --- /dev/null +++ b/test/integration/test.rb @@ -0,0 +1,6 @@ +require "minitest/autorun" + +ruby_path = File.join(File.absolute_path(File.dirname(__FILE__)), "ruby") +$LOAD_PATH.unshift(ruby_path) unless $LOAD_PATH.include?(ruby_path) + +require_relative "cases/trivial_integration_tests"