flydata 0.3.5 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/bin/flydata +1 -0
  4. data/flydata-core/lib/flydata-core/core_ext/module.rb +1 -1
  5. data/flydata-core/lib/flydata-core/core_ext/object.rb +1 -1
  6. data/flydata.gemspec +21 -5
  7. data/lib/flydata.rb +5 -8
  8. data/lib/flydata/api/data_entry.rb +2 -0
  9. data/lib/flydata/api/data_port.rb +2 -0
  10. data/lib/flydata/api/redshift_cluster.rb +2 -0
  11. data/lib/flydata/api_client.rb +3 -0
  12. data/lib/flydata/cli.rb +13 -2
  13. data/lib/flydata/command/base.rb +6 -0
  14. data/lib/flydata/command/conf.rb +3 -0
  15. data/lib/flydata/command/crontab.rb +3 -0
  16. data/lib/flydata/command/encrypt.rb +3 -0
  17. data/lib/flydata/command/kill_all.rb +3 -0
  18. data/lib/flydata/command/login.rb +2 -0
  19. data/lib/flydata/command/restart.rb +3 -0
  20. data/lib/flydata/command/routine.rb +3 -0
  21. data/lib/flydata/command/sender.rb +2 -0
  22. data/lib/flydata/command/setlogdel.rb +4 -1
  23. data/lib/flydata/command/setup.rb +7 -2
  24. data/lib/flydata/command/start.rb +3 -0
  25. data/lib/flydata/command/status.rb +3 -0
  26. data/lib/flydata/command/stop.rb +3 -0
  27. data/lib/flydata/command/sync.rb +10 -3
  28. data/lib/flydata/command/version.rb +2 -0
  29. data/lib/flydata/{command_logger.rb → command_loggable.rb} +0 -0
  30. data/lib/flydata/compatibility_check.rb +1 -1
  31. data/lib/flydata/credentials.rb +2 -0
  32. data/lib/flydata/fluent-plugins/in_mysql_binlog_flydata.rb +8 -9
  33. data/lib/flydata/fluent-plugins/mysql/alter_table_query_handler.rb +1 -1
  34. data/lib/flydata/fluent-plugins/mysql/binlog_query_dispatcher.rb +1 -1
  35. data/lib/flydata/fluent-plugins/mysql/binlog_query_handler.rb +1 -1
  36. data/lib/flydata/fluent-plugins/mysql/binlog_record_dispatcher.rb +2 -2
  37. data/lib/flydata/fluent-plugins/mysql/binlog_record_handler.rb +1 -1
  38. data/lib/flydata/fluent-plugins/mysql/ddl_query_handler.rb +1 -1
  39. data/lib/flydata/fluent-plugins/mysql/dml_record_handler.rb +1 -1
  40. data/lib/flydata/helpers.rb +0 -10
  41. data/lib/flydata/heroku.rb +3 -0
  42. data/lib/flydata/output/forwarder.rb +1 -1
  43. data/lib/flydata/parser/mysql/dump_parser.rb +29 -31
  44. data/lib/flydata/sync_file_manager.rb +230 -232
  45. data/spec/fly_data_model_spec.rb +1 -0
  46. data/spec/flydata/api/data_entry_spec.rb +1 -0
  47. data/spec/flydata/api_client_spec.rb +18 -0
  48. data/spec/flydata/cli_spec.rb +1 -0
  49. data/spec/flydata/command/base_spec.rb +44 -0
  50. data/spec/flydata/command/conf_spec.rb +21 -0
  51. data/spec/flydata/command/crontab_spec.rb +17 -0
  52. data/spec/flydata/command/encrypt_spec.rb +28 -0
  53. data/spec/flydata/command/kill_all_spec.rb +17 -0
  54. data/spec/flydata/command/login_spec.rb +21 -0
  55. data/spec/flydata/command/restart_spec.rb +17 -0
  56. data/spec/flydata/command/routine_spec.rb +29 -0
  57. data/spec/flydata/command/sender_spec.rb +7 -2
  58. data/spec/flydata/command/setlogdel_spec.rb +18 -0
  59. data/spec/flydata/command/setup_spec.rb +44 -0
  60. data/spec/flydata/command/start_spec.rb +17 -0
  61. data/spec/flydata/command/status_spec.rb +17 -0
  62. data/spec/flydata/command/stop_spec.rb +17 -0
  63. data/spec/flydata/command/sync_spec.rb +1 -0
  64. data/spec/flydata/command/version_spec.rb +14 -0
  65. data/spec/flydata/fluent-plugins/in_mysql_binlog_flydata_spec.rb +1 -1
  66. data/spec/flydata/parser/mysql/dump_parser_spec.rb +23 -73
  67. data/spec/flydata/sync_file_manager_spec.rb +150 -152
  68. metadata +19 -4
@@ -1,5 +1,6 @@
1
1
  require 'spec_helper'
2
2
  require 'stringio'
3
+ require 'fly_data_model'
3
4
 
4
5
  if defined? ActiveModel
5
6
  # TestUserModel model
@@ -1,4 +1,5 @@
1
1
  require 'spec_helper'
2
+ require 'flydata/api/data_entry'
2
3
 
3
4
  module Flydata
4
5
  module Api
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ require 'flydata/api_client'
3
+
4
+ module Flydata
5
+ describe ApiClient do
6
+ subject { described_class.instance }
7
+ let(:resource) { double('resource') }
8
+ let(:response) { JSON.parse('{"success":true}') }
9
+
10
+ describe "#post" do
11
+ it do
12
+ expect(RestClient::Resource).to receive(:new).and_return(resource)
13
+ expect(resource).to receive(:post).with(nil, accept: :json).and_return(response.to_json)
14
+ expect(subject.post('/user')).to eq(response)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,4 +1,5 @@
1
1
  require 'spec_helper'
2
+ require 'flydata/cli'
2
3
 
3
4
  module Flydata
4
5
  describe Cli do
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+ require 'flydata/command/base'
3
+
4
+ module Flydata
5
+ module Command
6
+
7
+ describe Base do
8
+ subject { described_class.new }
9
+ let(:data_entries) {
10
+ [{"id"=>4, "data_port_id"=>1, "name"=>"flydata_sync_mysql_2", "log_path"=>nil, "created_at"=>"2015-03-02T03:06:25.000Z", "updated_at"=>"2015-03-02T03:08:02.000Z", "log_deletion"=>nil, "display_name"=>"synctest", "heroku_resource_id"=>nil, "heroku_log_type"=>nil, "log_file_type"=>nil, "log_file_delimiter"=>nil, "enabled"=>true, "type"=>"RedshiftMysqlDataEntry", "tag_name"=>"flydata.c5c0eb3d_dp1.flydata_sync_mysql_2", "tag_name_dev"=>"flydata.c5c0eb3d_dp1.flydata_sync_mysql_2.dev", "data_port_key"=>"c5c0eb3d", "schema_name"=>"", "table_name"=>"", "redshift_schema_name"=>"", "redshift_table_name"=>"", "mysql_data_entry_preference"=>{"host"=>"ubertas.flydata.co", "port"=>3306, "username"=>"mak", "password"=>password, "database"=>"mak_development", "tables"=>"items,orders", "tables_append_only"=>""}}]
11
+ }
12
+ let(:flydata) { double('flydata') }
13
+ let(:path) { '/data_entries' }
14
+ let(:response_body) { data_entries }
15
+ let(:response) { double('response') }
16
+ let(:response_code) { 200 }
17
+ before do
18
+ allow(response).to receive(:code).and_return(response_code)
19
+ allow(flydata).to receive(:get).with(path).and_return(response_body)
20
+ allow(flydata).to receive(:response).and_return(response)
21
+ allow(subject).to receive(:flydata).and_return(flydata)
22
+ end
23
+
24
+ describe '#retrieve_data_entries' do
25
+ context "when MySQL password from server is encrypted" do
26
+ let(:password) { "8xRe5otrYkrnV5vQhufa6g==" }
27
+ let(:response_body) { data_entries }
28
+ before do
29
+ # instantiate response_body with the encrypted password
30
+ response_body
31
+ end
32
+ # override password which data_entries referes to
33
+ let(:password) { 'flydata' }
34
+ it "returns a data entry list with plain MySQL passwords" do
35
+ expect(subject.retrieve_data_entries).to eq(data_entries)
36
+ end
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+ end
44
+
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+ require 'flydata/command/conf'
3
+
4
+ module Flydata
5
+ module Command
6
+ describe Conf do
7
+ subject { described_class.new }
8
+ let(:de) { {'name' => 'foo'} }
9
+ let(:data_entries) { [de] }
10
+ describe "#run" do
11
+ it do
12
+ expect(subject).to receive(:retrieve_data_entries).and_return(data_entries)
13
+ expect(Flydata::Preference::DataEntryPreference).to receive(:copy_template).with(de).and_return(false)
14
+ expect(Flydata::Preference::DataEntryPreference).to receive(:configurable?).with(de).and_return(false)
15
+
16
+ subject.run
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+ require 'flydata/command/crontab'
3
+
4
+ module Flydata
5
+ module Command
6
+ describe Crontab do
7
+ subject { described_class.new }
8
+ let(:cron) { double("cron") }
9
+
10
+ it do
11
+ expect(Flydata::Cron).to receive(:new).and_return(cron)
12
+ expect(cron).to receive(:update)
13
+ subject.run
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+ require 'flydata/command/encrypt'
3
+
4
+ module Flydata
5
+ module Command
6
+ describe Encrypt do
7
+ subject { described_class.new }
8
+ let(:flydata) { double('flydata') }
9
+ let(:data_port) { double('data_port') }
10
+ let(:key) { 'abcd' }
11
+ let(:dp) { {'key' => key} }
12
+ let(:password) { 'P@ssword' }
13
+ let(:encrypted_password) { '@#$@#' }
14
+
15
+ before do
16
+ expect(subject).to receive(:flydata).and_return(flydata)
17
+ expect(flydata).to receive(:data_port).and_return(data_port)
18
+ expect(data_port).to receive(:get).and_return(dp)
19
+ expect(subject).to receive(:ask).and_return(password)
20
+ end
21
+ it "loads without an error" do
22
+ expect(Flydata::Util::Encryptor).to receive(:encrypt).with(password, key).and_return(encrypted_password)
23
+
24
+ subject.run
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+ require 'flydata/command/kill_all'
3
+
4
+ module Flydata
5
+ module Command
6
+ describe Kill_all do
7
+ subject { described_class.new }
8
+ let(:sender) { double("sender") }
9
+
10
+ it do
11
+ expect(Flydata::Command::Sender).to receive(:new).and_return(sender)
12
+ expect(sender).to receive(:kill_all)
13
+ subject.run
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+ require 'flydata/command/login'
3
+
4
+ module Flydata
5
+ module Command
6
+ describe Login do
7
+ subject { described_class.new }
8
+ let(:ask_text) { "answer" }
9
+ let(:flydata) { double('flydata') }
10
+
11
+ before do
12
+ allow(subject).to receive(:flydata).and_return(flydata)
13
+ allow(subject).to receive(:ask).and_return(ask_text)
14
+ end
15
+ it do
16
+ expect(flydata).to receive(:post)
17
+ expect(subject.run).to eq true
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+ require 'flydata/command/restart'
3
+
4
+ module Flydata
5
+ module Command
6
+ describe Restart do
7
+ subject { described_class.new }
8
+ let(:sender) { double("sender") }
9
+
10
+ it do
11
+ expect(Flydata::Command::Sender).to receive(:new).and_return(sender)
12
+ expect(sender).to receive(:restart)
13
+ subject.run
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+ require 'flydata/command/routine'
3
+
4
+ module Flydata
5
+ module Command
6
+ describe Routine do
7
+ subject { described_class.new }
8
+ let(:flydata) { double('flydata') }
9
+ let(:credentials) { double('credentials') }
10
+ let(:path) { '/tmp' }
11
+
12
+ before do
13
+ expect(subject).to receive(:flydata).and_return(flydata)
14
+ expect(flydata).to receive(:credentials).and_return(credentials)
15
+ expect(credentials).to receive(:authenticated?).and_return(true)
16
+ expect(subject).to receive(:retrieve_log_paths).and_return([path])
17
+ end
18
+ let(:log_monitor) { double('log_monitor') }
19
+ let(:setup) { double('setup') }
20
+ it "loads without an error" do
21
+ expect(Flydata::LogMonitor).to receive(:new).with(path).and_return(log_monitor)
22
+ expect(log_monitor).to receive(:setup).and_return(setup)
23
+ expect(setup).to receive(:rotate)
24
+
25
+ subject.run
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,9 +1,11 @@
1
1
  require 'spec_helper'
2
- require 'slop'
2
+ require 'flydata/command/sender'
3
3
 
4
4
  module Flydata
5
5
  module Command
6
6
  describe Sender do
7
+ let(:flydata) { double("flydata") }
8
+ let(:data_port) { double("data_port") }
7
9
  let(:opts) do
8
10
  slop = Sender.slop_start
9
11
  slop.parse(args)
@@ -16,7 +18,10 @@ module Flydata
16
18
  expect(subject).to receive(:wait_until_server_ready)
17
19
  expect(subject).to receive(:wait_until_client_ready)
18
20
  allow(Kernel).to receive(:sleep)
19
- allow_any_instance_of(Flydata::Api::DataPort).to receive(:get).and_return("Wibble")
21
+ allow(subject).to receive(:flydata).and_return(flydata)
22
+ allow(flydata).to receive(:data_port).and_return(data_port)
23
+ allow(data_port).to receive(:get).and_return("Wibble")
24
+
20
25
  allow_any_instance_of(Flydata::AgentCompatibilityCheck).to receive(:check).and_return(true)
21
26
  Flydata::Command::Sync.any_instance.should_receive(:try_mysql_sync).and_return("Wobble")
22
27
  end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ require 'flydata/command/setlogdel'
3
+
4
+ module Flydata
5
+ module Command
6
+ describe Setlogdel do
7
+ subject { described_class.new }
8
+ let(:login) { double("login") }
9
+
10
+ it "loads without an error" do
11
+ expect(Flydata::Command::Login).to receive(:new).and_return(login)
12
+ expect(login).to receive(:run)
13
+ expect(subject).to receive(:retrieve_data_entries).and_return([])
14
+ subject.run
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+ require 'flydata/command/setup'
3
+
4
+ module Flydata
5
+ module Command
6
+ describe Setup do
7
+ subject { described_class.new }
8
+ let(:sender) { double("sender") }
9
+ let(:de) { {'type' => 'RedshiftMysqlDataEntry'} }
10
+ let(:data_entries) { [ de ] }
11
+ let(:flydata) { double('flydata') }
12
+ let(:data_port) { double('data_port') }
13
+ let(:dp) { double('dp') }
14
+ let(:conf) { double('conf') }
15
+ let(:sync_fm) { double('sync_fm') }
16
+ let(:credentials) { double('credentials') }
17
+ let(:login) { double('login') }
18
+
19
+ before do
20
+ allow(subject).to receive(:retrieve_data_entries).and_return(data_entries)
21
+ allow(subject).to receive(:flydata).and_return(flydata)
22
+ expect(flydata).to receive(:data_port).and_return(data_port)
23
+ allow(flydata).to receive(:flydata_api_host).and_return('localhost')
24
+ expect(flydata).to receive(:credentials).and_return(credentials)
25
+ expect(credentials).to receive(:authenticated?).and_return(false)
26
+ expect(data_port).to receive(:get).and_return(dp)
27
+ expect(sender).to receive(:process_exist?).and_return(true)
28
+ expect(sender).to receive(:stop)
29
+ expect(sync_fm).to receive(:binlog_path).and_return('/tmp')
30
+ expect(conf).to receive(:copy_templates)
31
+ expect(login).to receive(:run)
32
+ expect(sender).to receive(:restart)
33
+ end
34
+ it do
35
+ expect(Flydata::Command::Conf).to receive(:new).and_return(conf)
36
+ expect(Flydata::SyncFileManager).to receive(:new).with(de).and_return(sync_fm)
37
+ expect(Flydata::Command::Login).to receive(:new).and_return(login)
38
+ expect(Flydata::Command::Sender).to receive(:new).and_return(sender).twice
39
+
40
+ subject.initial_run
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+ require 'flydata/command/start'
3
+
4
+ module Flydata
5
+ module Command
6
+ describe Start do
7
+ subject { described_class.new }
8
+ let(:sender) { double("sender") }
9
+
10
+ it do
11
+ expect(Flydata::Command::Sender).to receive(:new).and_return(sender)
12
+ expect(sender).to receive(:start)
13
+ subject.run
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+ require 'flydata/command/status'
3
+
4
+ module Flydata
5
+ module Command
6
+ describe Status do
7
+ subject { described_class.new }
8
+ let(:sender) { double("sender") }
9
+
10
+ it do
11
+ expect(Flydata::Command::Sender).to receive(:new).and_return(sender)
12
+ expect(sender).to receive(:status)
13
+ subject.run
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+ require 'flydata/command/stop'
3
+
4
+ module Flydata
5
+ module Command
6
+ describe Stop do
7
+ subject { described_class.new }
8
+ let(:sender) { double("sender") }
9
+
10
+ it do
11
+ expect(Flydata::Command::Sender).to receive(:new).and_return(sender)
12
+ expect(sender).to receive(:stop)
13
+ subject.run
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,5 +1,6 @@
1
1
  # coding: utf-8
2
2
  require 'spec_helper'
3
+ require 'flydata/command/sync'
3
4
 
4
5
  module Flydata
5
6
  module Command
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+ require 'flydata/command/version'
3
+
4
+ module Flydata
5
+ module Command
6
+ describe Version do
7
+ subject { described_class.new }
8
+
9
+ it do
10
+ subject.run
11
+ end
12
+ end
13
+ end
14
+ end
@@ -526,7 +526,7 @@ EOT
526
526
  # Need to make sure no event is sent... how do I do that
527
527
  let(:sync_fm) { double('sync_fm') }
528
528
  before do
529
- Flydata::FileUtil::SyncFileManager.any_instance.should_receive(:get_new_table_list).with(TEST_TABLES.split(","), "pos").and_return([TEST_TABLE])
529
+ Flydata::SyncFileManager.any_instance.should_receive(:get_new_table_list).with(TEST_TABLES.split(","), "pos").and_return([TEST_TABLE])
530
530
 
531
531
  Test.configure_plugin(plugin, TEST_CONFIG)
532
532
  plugin.event_listener(rotate_event)
@@ -82,83 +82,33 @@ module Flydata
82
82
  end
83
83
  end
84
84
  end
85
- describe MysqlDumpGeneratorMasterData do
86
- let(:status) { double(:status) }
87
- let(:dump_io) { File.open(file_path, 'r', encoding: "utf-8") }
88
- let(:default_dump_generator) { MysqlDumpGeneratorMasterData.new(default_conf) }
89
-
90
- describe '#initialize' do
91
- context 'with password' do
92
- subject { default_dump_generator.instance_variable_get(:@dump_cmd) }
93
- it { is_expected.to eq('mysqldump -h localhost -P 3306 -uadmin -p"pass" --default-character-set=utf8 --protocol=tcp --skip-lock-tables ' +
94
- '--single-transaction --hex-blob --flush-logs --master-data=2 dev users groups') }
95
- end
96
- context 'without password' do
97
- let (:dump_generator) do
98
- MysqlDumpGeneratorMasterData.new(default_conf.merge({'password' => ''}))
99
- end
100
- subject { dump_generator.instance_variable_get(:@dump_cmd) }
101
- it { is_expected.to eq('mysqldump -h localhost -P 3306 -uadmin --default-character-set=utf8 --protocol=tcp --skip-lock-tables ' +
102
- '--single-transaction --hex-blob --flush-logs --master-data=2 dev users groups') }
103
- end
104
- end
85
+ end
105
86
 
106
- describe '#dump' do
107
- context 'when exit status is not 0' do
108
- before do
109
- `touch #{file_path}`
110
- expect(status).to receive(:exitstatus).and_return 1
111
- expect(Open3).to receive(:capture3).and_return(
112
- ['(dummy std out)', '(dummy std err)', status]
113
- )
114
- end
115
- it do
116
- expect{ default_dump_generator.dump(file_path) }.to raise_error
117
- expect(File.exists?(file_path)).to be_falsey
118
- end
119
- end
120
- context 'when exit status is 0 but no file' do
121
- before do
122
- expect(status).to receive(:exitstatus).and_return 0
123
- expect(Open3).to receive(:capture3).and_return(
124
- ['(dummy std out)', '(dummy std err)', status]
125
- )
126
- end
127
- it do
128
- expect{ default_dump_generator.dump(file_path) }.to raise_error
129
- expect(File.exists?(file_path)).to be_falsey
130
- end
131
- end
132
- context 'when exit status is 0 but file size is 0' do
133
- before do
134
- `touch #{file_path}`
135
- expect(status).to receive(:exitstatus).and_return 0
136
- expect(Open3).to receive(:capture3).and_return(
137
- ['(dummy std out)', '(dummy std err)', status]
138
- )
139
- end
140
- it do
141
- expect{ default_dump_generator.dump(file_path) }.to raise_error
142
- expect(File.exists?(file_path)).to be_truthy
143
- end
144
- end
145
- context 'when exit status is 0' do
146
- before do
147
- `echo "something..." > #{file_path}`
148
- expect(status).to receive(:exitstatus).and_return 0
149
- expect(Open3).to receive(:capture3).and_return(
150
- ['(dummy std out)', '(dummy std err)', status]
151
- )
152
- end
153
- it do
154
- expect(default_dump_generator.dump(file_path)).to be_truthy
155
- expect(File.exists?(file_path)).to be_truthy
156
- end
87
+ describe FdMysqlClient do
88
+ let(:db_opts) do
89
+ {
90
+ host: 'localhost',
91
+ port: 3306,
92
+ username: 'admin',
93
+ password: 'pass',
94
+ database: 'dev'
95
+ }
96
+ end
97
+ describe '#query' do
98
+ module DummyMysqlClient
99
+ def initialize(opts = {})
157
100
  end
158
- after :each do
159
- File.delete(file_path) if File.exists?(file_path)
101
+ def query(sql, opts = {})
102
+ raise Mysql2::Error.new("Timeout waiting for a response from the last query. (waited 600 seconds)")
160
103
  end
161
104
  end
105
+ it 'raises appropriate error when query times out' do
106
+ described_class.instance_eval { include DummyMysqlClient }
107
+ sql = "FLUSH LOCAL TABLES"
108
+ test_client = described_class.new(db_opts)
109
+ expect{test_client.query(sql)}.to raise_error(RuntimeError, /query timed out when running/)
110
+ expect(test_client.last_query).to eq(sql)
111
+ end
162
112
  end
163
113
  end
164
114