larrow-runner 0.0.2 → 0.0.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 649d405f97c00eba29947141c7c23f97da18b783
4
- data.tar.gz: ab1fe09db103b64cb52fcc1003cf3ba3e4e0faa4
3
+ metadata.gz: 2c95b382b38fc64696236c9787628559fc98370b
4
+ data.tar.gz: 885f40783bd3a22542e4093833d96bbe75d3b776
5
5
  SHA512:
6
- metadata.gz: 0ca859ab0d4d0f713a0a3e86d0340ca1b91d76e941acfe97e4318ead3e09fe9347d99f65118a357629be6a045edbc907e23ac9055da03c0eb10933765524dc23
7
- data.tar.gz: f85aaf2b37ddfd2b8e0ef2daed67bbc591b3c8b75343efbf0b8f1c8105f30505d678e9b4dbcb8170fe3df1e887b55ee73d5a0c96ae3f46f8277519039163e8bc
6
+ metadata.gz: 8b7a87e391867ed386e7e53cb4a5db5064eb5e475640e6dfff776ba7474a099966c236071db37eec2aef5157442c4ec114860019e91d54dac03e4852b09c0cc3
7
+ data.tar.gz: 5f9629bc468942d32aa3570e32802db24397b516627fc77f19af0408f99045f835704cda3286262145c7eedcbddafc2a29f35118cc2203ffd85d5309f00cce8c
data/RELEASE ADDED
@@ -0,0 +1,16 @@
1
+ 2014.11.02. version 0.0.3
2
+ =========================
3
+
4
+ * clean dependency: remove `active_support`
5
+ * open verbose mode on tests
6
+ * delete resource file when cleanup
7
+ * ensure node start( use `nmap` to detect)
8
+ * use larrow-qingcloud v0.0.2
9
+
10
+ 2014.10.19. version 0.0.2
11
+ =========================
12
+
13
+ * hotfix: undefined method `address` for Node
14
+ * remove some third party libs
15
+ * check cloud login before execute
16
+ * some changes for user convenience
@@ -33,7 +33,7 @@ Gem::Specification.new do |spec|
33
33
 
34
34
  spec.add_runtime_dependency "faraday", '~> 0.9'
35
35
 
36
- spec.add_runtime_dependency 'larrow-qingcloud', '~> 0'
36
+ spec.add_runtime_dependency 'larrow-qingcloud', '~> 0.0', '>= 0.0.2'
37
37
  spec.add_runtime_dependency 'promising', '~> 0.3'
38
38
 
39
39
  end
@@ -1,7 +1 @@
1
- require 'active_support/core_ext/hash/compact'
2
- require 'active_support/core_ext/hash/deep_merge'
3
- require 'active_support/core_ext/hash/except'
4
1
  require 'active_support/core_ext/hash/indifferent_access'
5
- require 'active_support/core_ext/hash/keys'
6
- require 'active_support/core_ext/hash/reverse_merge'
7
- require 'active_support/core_ext/hash/slice'
@@ -0,0 +1 @@
1
+ require 'active_support/core_ext/string/indent'
@@ -0,0 +1,43 @@
1
+ class String
2
+ # Same as +indent+, except it indents the receiver in-place.
3
+ #
4
+ # Returns the indented string, or +nil+ if there was nothing to indent.
5
+ def indent!(amount, indent_string=nil, indent_empty_lines=false)
6
+ indent_string = indent_string || self[/^[ \t]/] || ' '
7
+ re = indent_empty_lines ? /^/ : /^(?!$)/
8
+ gsub!(re, indent_string * amount)
9
+ end
10
+
11
+ # Indents the lines in the receiver:
12
+ #
13
+ # <<EOS.indent(2)
14
+ # def some_method
15
+ # some_code
16
+ # end
17
+ # EOS
18
+ # # =>
19
+ # def some_method
20
+ # some_code
21
+ # end
22
+ #
23
+ # The second argument, +indent_string+, specifies which indent string to
24
+ # use. The default is +nil+, which tells the method to make a guess by
25
+ # peeking at the first indented line, and fallback to a space if there is
26
+ # none.
27
+ #
28
+ # " foo".indent(2) # => " foo"
29
+ # "foo\n\t\tbar".indent(2) # => "\t\tfoo\n\t\t\t\tbar"
30
+ # "foo".indent(2, "\t") # => "\t\tfoo"
31
+ #
32
+ # While +indent_string+ is typically one space or tab, it may be any string.
33
+ #
34
+ # The third argument, +indent_empty_lines+, is a flag that says whether
35
+ # empty lines should be indented. Default is false.
36
+ #
37
+ # "foo\n\nbar".indent(2) # => " foo\n\n bar"
38
+ # "foo\n\nbar".indent(2, nil, true) # => " foo\n \n bar"
39
+ #
40
+ def indent(amount, indent_string=nil, indent_empty_lines=false)
41
+ dup.tap {|_| _.indent!(amount, indent_string, indent_empty_lines)}
42
+ end
43
+ end
@@ -18,7 +18,7 @@ You can save it as .larrow.yml on the project root folder.
18
18
 
19
19
  desc 'resource','show all resource in Resource.yml'
20
20
  long_desc <<-EOF.gsub("\n", "\x5")
21
- Read .larrow.resource from current directory, show information.
21
+ Read #{ResourcePath} from current directory, show information.
22
22
  resource: instance, eip, etc...
23
23
  EOF
24
24
  def resource
@@ -27,7 +27,7 @@ resource: instance, eip, etc...
27
27
 
28
28
  desc 'cleanup','cleanup all resource in Resource.yml'
29
29
  long_desc <<-EOF.gsub("\n", "\x5")
30
- Read .larrow.resource from current directory, and release all resources.
30
+ Read #{ResourcePath} from current directory, and release all resources.
31
31
  resource: instance, eip, etc...
32
32
  EOF
33
33
  def cleanup
@@ -77,7 +77,7 @@ module Larrow::Runner
77
77
 
78
78
  def store_resource
79
79
  resource = app.dump
80
- File.write '.larrow.resource', YAML.dump(resource)
80
+ File.write ResourcePath, YAML.dump(resource)
81
81
  RunLogger.title 'store resource'
82
82
  end
83
83
 
@@ -102,6 +102,7 @@ module Larrow::Runner
102
102
  resource_iterator do |clazz, array|
103
103
  clazz.cleanup array
104
104
  end
105
+ File.delete ResourcePath rescue nil
105
106
  RunLogger.title 'resource cleaned'
106
107
  end
107
108
 
@@ -25,7 +25,7 @@ module Larrow::Runner
25
25
 
26
26
  def build_language
27
27
  return if data[:language].nil?
28
- clazz = eval data[:language].camelize
28
+ clazz = eval data[:language].capitalize
29
29
  clazz.fulfill(data,configuration)
30
30
  end
31
31
  end
@@ -81,9 +81,12 @@ module Larrow::Runner
81
81
  self.title = title
82
82
  end
83
83
 
84
- def run_on node
84
+ def run_on node, verbose:nil
85
+ verbose = title.to_s.end_with?('test') if verbose.nil?
85
86
  scripts.each do |script|
86
- node.execute script.actual_command, base_dir: script.base_dir
87
+ node.execute(script.actual_command,
88
+ base_dir: script.base_dir,
89
+ verbose: verbose)
87
90
  end
88
91
  end
89
92
  end
@@ -99,7 +102,7 @@ module Larrow::Runner
99
102
  self.block = block
100
103
  end
101
104
 
102
- def run_on node
105
+ def run_on node, *args
103
106
  block.call node
104
107
  end
105
108
  end
@@ -16,12 +16,13 @@ module Larrow::Runner
16
16
  end
17
17
 
18
18
  def action group
19
+ verbose = RunOption.key?(:debug) ? true : nil
19
20
  configuration.steps_for(group) do |a_step|
20
21
  RunLogger.title "[#{a_step.title}]"
21
22
  begin_at = Time.new
22
- a_step.run_on node
23
+ a_step.run_on node, verbose: verbose
23
24
  during = sprintf('%.2f',Time.new - begin_at)
24
- RunLogger.level(1).detail "#{a_step.title} complete (#{during}s)"
25
+ RunLogger.level(1).info "#{a_step.title} complete (#{during}s)"
25
26
  end
26
27
  end
27
28
 
@@ -14,7 +14,7 @@ module Larrow::Runner
14
14
  @executor = Executor.new host, user, nil, nil
15
15
  end
16
16
 
17
- def execute command, base_dir:nil
17
+ def execute command, base_dir:nil,verbose:nil
18
18
  block = if block_given?
19
19
  -> (data) { yield data }
20
20
  else
@@ -24,7 +24,7 @@ module Larrow::Runner
24
24
  end
25
25
  }
26
26
  end
27
- @executor.execute command, base_dir: base_dir, &block
27
+ @executor.execute command, base_dir: base_dir, verbose:verbose, &block
28
28
  end
29
29
 
30
30
  def stop
@@ -30,6 +30,8 @@ module Larrow
30
30
  RunLogger.level(1).detail "bind ip: #{eips[i].address}"
31
31
  eips[i] = eips[i].associate instances[i].id
32
32
  [ instances[i], eips[i] ]
33
+ end.tap do |list|
34
+ list.each{|instance,eip| ping eip.address,30}
33
35
  end
34
36
  end
35
37
 
@@ -54,6 +56,17 @@ module Larrow
54
56
  Qingcloud.remove_connection
55
57
  raise $!
56
58
  end
59
+
60
+ def ping host, time, port=22
61
+ Timeout::timeout(time) do
62
+ loop do
63
+ if system("nmap #{host} -p #{port} -Pn | grep -q open")
64
+ return true
65
+ end
66
+ end
67
+ end
68
+ end
69
+
57
70
  end
58
71
  end
59
72
  end
@@ -15,13 +15,13 @@ module Larrow
15
15
  @dlogger = RunLogger #::Logger.new "#{ip}_cmd.log"
16
16
  end
17
17
 
18
- def execute cmd, base_dir:nil
18
+ def execute cmd, base_dir:nil,verbose:nil
19
19
  connection.open_channel do |ch|
20
20
  RunLogger.level(1).detail "# #{cmd}"
21
21
  cmd = "cd #{base_dir}; #{cmd}" unless base_dir.nil?
22
22
  errmsg = ''
23
23
  ch.exec cmd do |ch,success|
24
- if RunOption.key? :debug
24
+ if verbose
25
25
  ch.on_data{ |c, data| yield data }
26
26
  ch.on_extended_data{ |c, type, data| yield data }
27
27
  else
@@ -27,7 +27,7 @@ module Larrow::Runner
27
27
  def update_source node, target_dir
28
28
  command = rsync_command node.user, node.host,target_dir
29
29
  invoke command
30
- invoke "ssh-keygen -R #{node.host} 2>&1"
30
+ `ssh-keygen -R #{node.host} 2>&1 >/dev/null`
31
31
  end
32
32
 
33
33
  def rsync_command user, host, target_dir
@@ -40,19 +40,30 @@ module Larrow::Runner
40
40
  unshift('.git'). # .git itself is ignored
41
41
  map{|s| "--exclude '#{s}'" } # build rsync exclude arguments
42
42
 
43
- ssh_options = "-e 'ssh -o StrictHostKeyChecking=no'"
44
-
45
- rsync_options = "-az #{ssh_options} #{excludes.join ' '}"
43
+ rsync_options = "-az -e 'ssh #{ssh_options}' #{excludes.join ' '}"
46
44
  rsync_options += ' -v' if RunOption.key? :debug
47
45
 
48
- "rsync #{rsync_options} #{project_folder}/ '#{ssh_path}' 2>&1"
46
+ "rsync #{rsync_options} #{project_folder}/ '#{ssh_path}'"
49
47
  end
48
+
50
49
  def invoke command
51
- `#{command}`.split(/\r?\n/).each do |msg|
52
- RunLogger.level(1).info msg
50
+ RunLogger.level(1).info command
51
+ time = Time.new
52
+ `#{command} 2>&1`.split(/\r?\n/).each do |msg|
53
+ RunLogger.level(2).detail msg
53
54
  end
55
+ RunLogger.level(1).detail "invoke time: #{Time.new - time}"
54
56
  end
55
57
 
58
+ def ssh_options
59
+ {
60
+ 'GSSAPIAuthentication' => 'no',
61
+ 'StrictHostKeyChecking' => 'no',
62
+ 'LogLevel' => 'ERROR'
63
+ }.map do |k,v|
64
+ "-o #{k}=#{v}"
65
+ end.join(' ')
66
+ end
56
67
  end
57
68
  end
58
69
  end
@@ -1,5 +1,5 @@
1
1
  module Larrow
2
2
  module Runner
3
- VERSION = "0.0.2"
3
+ VERSION = "0.0.3"
4
4
  end
5
5
  end
@@ -1,4 +1,4 @@
1
- require_relative '../spec_helper.rb'
1
+ require_relative '../../spec_helper.rb'
2
2
 
3
3
  module Larrow::Runner::Manifest
4
4
  describe Travis do
@@ -9,7 +9,7 @@ module Larrow::Runner::Manifest
9
9
  end
10
10
 
11
11
  def get _filename
12
- path = File.expand_path "../../fixtures/#{filename}", __FILE__
12
+ path = File.expand_path "../../../fixtures/#{filename}", __FILE__
13
13
  File.read(path)
14
14
  end
15
15
 
@@ -1,15 +1,15 @@
1
- require_relative '../spec_helper.rb'
1
+ require_relative '../../spec_helper.rb'
2
2
 
3
3
  module Larrow::Runner::Model
4
4
  describe Node do
5
5
  subject{ Node.new nil, OpenStruct.new(address: 'localhost'), `whoami`.chomp }
6
6
  before do
7
- Larrow::Runner::RunOption[:debug] = nil
7
+ Larrow::Runner::RunOption[:debug] = true
8
8
  end
9
9
  it 'can execute' do
10
10
  outputs = ""
11
11
  script = OpenStruct.new(actual_command: 'pwd')
12
- subject.execute('pwd') do |data|
12
+ subject.execute('pwd',verbose:true) do |data|
13
13
  outputs << data
14
14
  end
15
15
  expect(outputs).to include(ENV['HOME'])
@@ -1,19 +1,16 @@
1
- require_relative '../spec_helper.rb'
1
+ require_relative '../../spec_helper.rb'
2
2
  module Larrow::Runner::Service
3
3
  describe Executor do
4
4
  subject{ Executor.new 'localhost', `whoami`.chomp, 22, nil }
5
- before do
6
- Larrow::Runner::RunOption[:debug] = nil
7
- end
8
5
  it 'normal command run'do
9
6
  outputs = ''
10
- subject.execute('echo aaa'){|data| outputs << data}
7
+ subject.execute('echo aaa',verbose:true){|data| outputs << data}
11
8
  expect(outputs).to eq "aaa\n"
12
9
  end
13
10
 
14
11
  it 'command with base dir' do
15
12
  outputs = ''
16
- subject.execute('pwd', base_dir: '/opt') do |data|
13
+ subject.execute('pwd', base_dir: '/opt',verbose:true) do |data|
17
14
  outputs << data
18
15
  end
19
16
  expect(outputs).to eq "/opt\n"
@@ -1,4 +1,4 @@
1
- require_relative '../spec_helper.rb'
1
+ require_relative '../../spec_helper.rb'
2
2
  module Larrow
3
3
  module Runner
4
4
  module Vcs
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: larrow-runner
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - fsword
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-18 00:00:00.000000000 Z
11
+ date: 2014-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -182,14 +182,20 @@ dependencies:
182
182
  requirements:
183
183
  - - "~>"
184
184
  - !ruby/object:Gem::Version
185
- version: '0'
185
+ version: '0.0'
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ version: 0.0.2
186
189
  type: :runtime
187
190
  prerelease: false
188
191
  version_requirements: !ruby/object:Gem::Requirement
189
192
  requirements:
190
193
  - - "~>"
191
194
  - !ruby/object:Gem::Version
192
- version: '0'
195
+ version: '0.0'
196
+ - - ">="
197
+ - !ruby/object:Gem::Version
198
+ version: 0.0.2
193
199
  - !ruby/object:Gem::Dependency
194
200
  name: promising
195
201
  requirement: !ruby/object:Gem::Requirement
@@ -218,17 +224,15 @@ files:
218
224
  - Gemfile
219
225
  - LICENSE.txt
220
226
  - README.md
227
+ - RELEASE
221
228
  - Rakefile
222
229
  - bin/larrow
223
230
  - larrow-runner.gemspec
224
231
  - lib/active_support/core_ext/hash.rb
225
- - lib/active_support/core_ext/hash/compact.rb
226
- - lib/active_support/core_ext/hash/deep_merge.rb
227
- - lib/active_support/core_ext/hash/except.rb
228
232
  - lib/active_support/core_ext/hash/indifferent_access.rb
229
233
  - lib/active_support/core_ext/hash/keys.rb
230
- - lib/active_support/core_ext/hash/reverse_merge.rb
231
- - lib/active_support/core_ext/hash/slice.rb
234
+ - lib/active_support/core_ext/string.rb
235
+ - lib/active_support/core_ext/string/indent.rb
232
236
  - lib/active_support/hash_with_indifferent_access.rb
233
237
  - lib/larrow/runner.rb
234
238
  - lib/larrow/runner/cli.rb
@@ -260,11 +264,11 @@ files:
260
264
  - spec/fixtures/travis_ruby.yml
261
265
  - spec/integration/build_cmds_spec.rb
262
266
  - spec/integration/test_cmds_spec.rb
263
- - spec/manifest/travis_spec.rb
264
- - spec/model/node_spec.rb
265
- - spec/service/executor_spec.rb
266
267
  - spec/spec_helper.rb
267
- - spec/vcs/github_spec.rb
268
+ - spec/unit_test/manifest/travis_spec.rb
269
+ - spec/unit_test/model/node_spec.rb
270
+ - spec/unit_test/service/executor_spec.rb
271
+ - spec/unit_test/vcs/github_spec.rb
268
272
  homepage: http://github.com/fsword/larrow-core
269
273
  licenses:
270
274
  - MIT
@@ -294,8 +298,8 @@ test_files:
294
298
  - spec/fixtures/travis_ruby.yml
295
299
  - spec/integration/build_cmds_spec.rb
296
300
  - spec/integration/test_cmds_spec.rb
297
- - spec/manifest/travis_spec.rb
298
- - spec/model/node_spec.rb
299
- - spec/service/executor_spec.rb
300
301
  - spec/spec_helper.rb
301
- - spec/vcs/github_spec.rb
302
+ - spec/unit_test/manifest/travis_spec.rb
303
+ - spec/unit_test/model/node_spec.rb
304
+ - spec/unit_test/service/executor_spec.rb
305
+ - spec/unit_test/vcs/github_spec.rb
@@ -1,20 +0,0 @@
1
- class Hash
2
- # Returns a hash with non +nil+ values.
3
- #
4
- # hash = { a: true, b: false, c: nil}
5
- # hash.compact # => { a: true, b: false}
6
- # hash # => { a: true, b: false, c: nil}
7
- # { c: nil }.compact # => {}
8
- def compact
9
- self.select { |_, value| !value.nil? }
10
- end
11
-
12
- # Replaces current hash with non +nil+ values.
13
- #
14
- # hash = { a: true, b: false, c: nil}
15
- # hash.compact! # => { a: true, b: false}
16
- # hash # => { a: true, b: false}
17
- def compact!
18
- self.reject! { |_, value| value.nil? }
19
- end
20
- end
@@ -1,38 +0,0 @@
1
- class Hash
2
- # Returns a new hash with +self+ and +other_hash+ merged recursively.
3
- #
4
- # h1 = { a: true, b: { c: [1, 2, 3] } }
5
- # h2 = { a: false, b: { x: [3, 4, 5] } }
6
- #
7
- # h1.deep_merge(h2) #=> { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } }
8
- #
9
- # Like with Hash#merge in the standard library, a block can be provided
10
- # to merge values:
11
- #
12
- # h1 = { a: 100, b: 200, c: { c1: 100 } }
13
- # h2 = { b: 250, c: { c1: 200 } }
14
- # h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val }
15
- # # => { a: 100, b: 450, c: { c1: 300 } }
16
- def deep_merge(other_hash, &block)
17
- dup.deep_merge!(other_hash, &block)
18
- end
19
-
20
- # Same as +deep_merge+, but modifies +self+.
21
- def deep_merge!(other_hash, &block)
22
- other_hash.each_pair do |current_key, other_value|
23
- this_value = self[current_key]
24
-
25
- self[current_key] = if this_value.is_a?(Hash) && other_value.is_a?(Hash)
26
- this_value.deep_merge(other_value, &block)
27
- else
28
- if block_given? && key?(current_key)
29
- block.call(current_key, this_value, other_value)
30
- else
31
- other_value
32
- end
33
- end
34
- end
35
-
36
- self
37
- end
38
- end
@@ -1,15 +0,0 @@
1
- class Hash
2
- # Returns a hash that includes everything but the given keys. This is useful for
3
- # limiting a set of parameters to everything but a few known toggles:
4
- #
5
- # @person.update(params[:person].except(:admin))
6
- def except(*keys)
7
- dup.except!(*keys)
8
- end
9
-
10
- # Replaces the hash without the given keys.
11
- def except!(*keys)
12
- keys.each { |key| delete(key) }
13
- self
14
- end
15
- end
@@ -1,22 +0,0 @@
1
- class Hash
2
- # Merges the caller into +other_hash+. For example,
3
- #
4
- # options = options.reverse_merge(size: 25, velocity: 10)
5
- #
6
- # is equivalent to
7
- #
8
- # options = { size: 25, velocity: 10 }.merge(options)
9
- #
10
- # This is particularly useful for initializing an options hash
11
- # with default values.
12
- def reverse_merge(other_hash)
13
- other_hash.merge(self)
14
- end
15
-
16
- # Destructive +reverse_merge+.
17
- def reverse_merge!(other_hash)
18
- # right wins if there is no left
19
- merge!( other_hash ){|key,left,right| left }
20
- end
21
- alias_method :reverse_update, :reverse_merge!
22
- end
@@ -1,42 +0,0 @@
1
- class Hash
2
- # Slice a hash to include only the given keys. This is useful for
3
- # limiting an options hash to valid keys before passing to a method:
4
- #
5
- # def search(criteria = {})
6
- # criteria.assert_valid_keys(:mass, :velocity, :time)
7
- # end
8
- #
9
- # search(options.slice(:mass, :velocity, :time))
10
- #
11
- # If you have an array of keys you want to limit to, you should splat them:
12
- #
13
- # valid_keys = [:mass, :velocity, :time]
14
- # search(options.slice(*valid_keys))
15
- def slice(*keys)
16
- keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
17
- keys.each_with_object(self.class.new) { |k, hash| hash[k] = self[k] if has_key?(k) }
18
- end
19
-
20
- # Replaces the hash with only the given keys.
21
- # Returns a hash containing the removed key/value pairs.
22
- #
23
- # { a: 1, b: 2, c: 3, d: 4 }.slice!(:a, :b)
24
- # # => {:c=>3, :d=>4}
25
- def slice!(*keys)
26
- keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
27
- omit = slice(*self.keys - keys)
28
- hash = slice(*keys)
29
- hash.default = default
30
- hash.default_proc = default_proc if default_proc
31
- replace(hash)
32
- omit
33
- end
34
-
35
- # Removes and returns the key/value pairs matching the given keys.
36
- #
37
- # { a: 1, b: 2, c: 3, d: 4 }.extract!(:a, :b) # => {:a=>1, :b=>2}
38
- # { a: 1, b: 2 }.extract!(:a, :x) # => {:a=>1}
39
- def extract!(*keys)
40
- keys.each_with_object(self.class.new) { |key, result| result[key] = delete(key) if has_key?(key) }
41
- end
42
- end