# coding: utf-8 require 'spec_helper' require 'flydata/parser/mysql/dump_parser' require 'open3' require 'mysql2' module Flydata module Parser module Mysql context "Test Dump Generators" do let(:stdin) do s = double(:stdin) allow(s).to receive(:close_write) s end let(:stdout) do s = double(:stdout) allow(s).to receive(:set_encoding) allow(s).to receive(:gets).and_return("first line") allow(s).to receive(:each_line).and_yield("another line") s end let(:file_path) { File.join('/tmp', "flydata_sync_spec_mysqldump_#{Time.now.to_i}") } let(:default_conf) do { 'host' => 'localhost', 'port' => 3306, 'username' => 'admin', 'password' => 'pass', 'database' => 'dev', 'tables' => 'users,groups', } end describe MysqlDumpGeneratorNoMasterData do let(:stderr) { double(:stderr) } let(:wait_thr) { double(:wait_thr) } let(:default_dump_generator) { MysqlDumpGeneratorNoMasterData.new(default_conf) } let(:mysql_client) do m = double(:mysql_client) allow(m).to receive(:query).with(/TABLES/) allow(m).to receive(:query).with("SHOW VARIABLES LIKE 'version'"). and_return([{'Value' => '5.1.40-0ubuntu0.12.04.1-log'}]) allow(m).to receive(:query).with("SHOW MASTER STATUS;"). and_return([{'File' => 'mysql-bin.000451', 'Position' => 89872}]) allow(m).to receive(:close) m end describe '#dump' do before do expect(Mysql2::Client).to receive(:new).and_return(mysql_client) expect(Open3).to receive(:popen3).and_yield(stdin, stdout, stderr, wait_thr) end context "when mysqldump exits with status 0" do it do expect(wait_thr).to receive(:value).and_return(0) expect(stderr).to receive(:each_line).and_yield("") expect(default_dump_generator.dump(file_path)).to be_truthy expect(File.exists?(file_path)).to be_truthy end end context "when mysqldump exits with status 1" do it do expect(wait_thr).to receive(:value).and_return(1) expect(stderr).to receive(:each_line).and_yield("") expect{ default_dump_generator.dump(file_path) }.to raise_error end end context "when mysqldump exits with status 0, but there are error in stderr" do it do expect(wait_thr).to receive(:value).and_return(0) expect(stderr).to receive(:each_line).and_yield("mysqldump error") expect{ default_dump_generator.dump(file_path) }.to raise_error end end after :each do File.delete(file_path) if File.exists?(file_path) end end end describe MysqlDumpGeneratorMasterData do let(:status) { double(:status) } let(:dump_io) { File.open(file_path, 'r', encoding: "utf-8") } let(:default_dump_generator) { MysqlDumpGeneratorMasterData.new(default_conf) } describe '#initialize' do context 'with password' do subject { default_dump_generator.instance_variable_get(:@dump_cmd) } it { is_expected.to eq('MYSQL_PWD="pass" mysqldump --default-character-set=utf8 --protocol=tcp -h localhost -P 3306 -uadmin --skip-lock-tables ' + '--single-transaction --hex-blob --flush-logs --master-data=2 dev users groups') } end context 'without password' do let (:dump_generator) do MysqlDumpGeneratorMasterData.new(default_conf.merge({'password' => ''})) end subject { dump_generator.instance_variable_get(:@dump_cmd) } it { is_expected.to eq('MYSQL_PWD="" mysqldump --default-character-set=utf8 --protocol=tcp -h localhost -P 3306 -uadmin --skip-lock-tables ' + '--single-transaction --hex-blob --flush-logs --master-data=2 dev users groups') } end end describe '#dump' do context 'when exit status is not 0' do before do `touch #{file_path}` expect(status).to receive(:exitstatus).and_return 1 expect(Open3).to receive(:capture3).and_return( ['(dummy std out)', '(dummy std err)', status] ) end it do expect{ default_dump_generator.dump(file_path) }.to raise_error expect(File.exists?(file_path)).to be_falsey end end context 'when exit status is 0 but no file' do before do expect(status).to receive(:exitstatus).and_return 0 expect(Open3).to receive(:capture3).and_return( ['(dummy std out)', '(dummy std err)', status] ) end it do expect{ default_dump_generator.dump(file_path) }.to raise_error expect(File.exists?(file_path)).to be_falsey end end context 'when exit status is 0 but file size is 0' do before do `touch #{file_path}` expect(status).to receive(:exitstatus).and_return 0 expect(Open3).to receive(:capture3).and_return( ['(dummy std out)', '(dummy std err)', status] ) end it do expect{ default_dump_generator.dump(file_path) }.to raise_error expect(File.exists?(file_path)).to be_truthy end end context 'when exit status is 0' do before do `echo "something..." > #{file_path}` expect(status).to receive(:exitstatus).and_return 0 expect(Open3).to receive(:capture3).and_return( ['(dummy std out)', '(dummy std err)', status] ) end it do expect(default_dump_generator.dump(file_path)).to be_truthy expect(File.exists?(file_path)).to be_truthy end end after :each do File.delete(file_path) if File.exists?(file_path) end end end end describe MysqlDumpParser do let(:file_path) { File.join('/tmp', "flydata_sync_spec_mysqldump_parse_#{Time.now.to_i}") } let(:dump_io) { File.open(file_path, 'r', encoding: "utf-8") } let(:default_parser) { MysqlDumpParser.new } def generate_dump_file(content) File.open(file_path, 'w') {|f| f.write(content)} end after do File.delete(file_path) if File.exists?(file_path) end describe '#parse' do DUMP_HEADER = <--5.6.13-log /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8 */; /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; /*!40103 SET TIME_ZONE='+00:00' */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- -- Position to start replication or point-in-time recovery from -- -- CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000267', MASTER_LOG_POS=120; EOT def index_after(content, string) content.index(string) + string.bytesize + 1 end let(:default_parser) { MysqlDumpParser.new } let(:default_binlog_pos) { {binfile: 'mysql-bin.000267', pos: 120 } } let(:dump_pos_after_binlog_pos) { index_after(DUMP_HEADER, 'MASTER_LOG_POS=120;') } let(:create_table_block) { double('create_table_block') } let(:insert_record_block) { double('insert_record_block') } let(:check_point_block) { double('check_point_block') } before do generate_dump_file('') end context 'when dump does not contain binlog pos' do before { generate_dump_file('dummy content') } it do expect(create_table_block).to receive(:call).never expect(insert_record_block).to receive(:call).never expect(check_point_block).to receive(:call).never binlog_pos = default_parser.parse( dump_io, create_table_block, insert_record_block, check_point_block ) expect(binlog_pos).to be_nil end end context 'when dump contains only binlog pos' do before { generate_dump_file(<