diff --git a/test/integration/cases/trivial_integration_tests.rb b/test/integration/cases/trivial_integration_tests.rb new file mode 100644 index 00000000..5d24b6f9 --- /dev/null +++ b/test/integration/cases/trivial_integration_tests.rb @@ -0,0 +1,51 @@ +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_state_cleanup do + use_datawriter + interrupt_ghostferry_when_some_batches_are_copied + + 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_state_cleanup 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..5e808f94 --- /dev/null +++ b/test/integration/ruby/ghostferry_integration/test_case.rb @@ -0,0 +1,128 @@ +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) + @logger.level = Logger::INFO + @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 + + @datawriter = DataWriter.new(@dbs.source_db_config, logger: @logger) + end + + def after_teardown + terminate_datawriter_and_ghostferry + end + + ###################### + # Test Setup Helpers # + ###################### + + # This should be a no op if ghostferry and datawriter have already been + # stopped. + def terminate_datawriter_and_ghostferry + @datawriter.stop + @datawriter.join + + @ghostferry.stop_and_cleanup + @datawriter = DataWriter.new(@dbs.source_db_config, logger: @logger) + end + + # This is useful if we need to run Ghostferry multiple times in during a + # single run, such as during an interrupt + resume cycle. + def with_state_cleanup + @datawriter = DataWriter.new(@dbs.source_db_config, logger: @logger) + @ghostferry.reset_state + yield + terminate_datawriter_and_ghostferry + end + + 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 use_datawriter(&on_write) + start_datawriter_with_ghostferry(&on_write) + stop_datawriter_during_cutover + end + + def interrupt_ghostferry_when_some_batches_are_copied(batches: 2) + batches_written = 0 + @ghostferry.on_status(Ghostferry::Status::AFTER_ROW_COPY) do + batches_written += 1 + if batches_written >= batches + @ghostferry.send_signal("TERM") + end + end + 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 + 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"