xmigra 1.5.1 → 1.6.0

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.
@@ -1,4 +1,5 @@
1
1
  require 'xmigra/console'
2
+ require 'xmigra/migration_chain'
2
3
 
3
4
  module XMigra
4
5
  module GitSpecifics
@@ -7,6 +8,7 @@ module XMigra
7
8
  MASTER_HEAD_ATTRIBUTE = 'xmigra-master'
8
9
  MASTER_BRANCH_SUBDIR = 'xmigra-master'
9
10
  PRODUCTION_CHAIN_EXTENSION_COMMAND = 'xmigra-on-production-chain-extended'
11
+ ATTRIBUTE_UNSPECIFIED = 'unspecified'
10
12
 
11
13
  class AttributesFile
12
14
  def initialize(effect_root, access=:shared)
@@ -161,7 +163,16 @@ module XMigra
161
163
  end
162
164
 
163
165
  def git(*args)
164
- Dir.chdir(self.path) do |pwd|
166
+ _path = begin
167
+ self.path
168
+ rescue NameError
169
+ begin
170
+ self.schema_dir
171
+ rescue NameError
172
+ Pathname(self.file_path).dirname
173
+ end
174
+ end
175
+ Dir.chdir(_path) do |pwd|
165
176
  GitSpecifics.run_git(*args)
166
177
  end
167
178
  end
@@ -211,7 +222,13 @@ module XMigra
211
222
  end
212
223
 
213
224
  def branch_identifier
214
- return (if self.production
225
+ for_production = begin
226
+ self.production
227
+ rescue NameError
228
+ false
229
+ end
230
+
231
+ return (if for_production
215
232
  self.git_branch_info[0]
216
233
  else
217
234
  return @git_branch_identifier if defined? @git_branch_identifier
@@ -227,9 +244,25 @@ module XMigra
227
244
  if commit
228
245
  self.git_fetch_master_branch
229
246
 
230
- # If there are no commits between the master head and *commit*, then
231
- # *commit* is production-ish
232
- return (self.git_commits_in? self.git_master_local_branch..commit) ? :development : :production
247
+ # If there are commits between the master head and *commit*, then
248
+ # *commit* is not production-ish
249
+ if self.git_commits_in? self.git_master_local_branch..commit
250
+ return :development
251
+ end
252
+
253
+ # Otherwise, look to see if all migrations in the migration chain for
254
+ # commit are in the master head with no diffs -- the migration chain
255
+ # is a "prefix" of the chain in the master head:
256
+ migration_chain = RepoStoredMigrationChain.new(
257
+ commit,
258
+ Pathname(path).join(SchemaManipulator::STRUCTURE_SUBDIR),
259
+ )
260
+ return :production if self.git(
261
+ :diff, '--name-only',
262
+ self.git_master_local_branch, commit, '--',
263
+ *migration_chain.map(&:file_path)
264
+ ).empty?
265
+ return :development
233
266
  end
234
267
 
235
268
  return nil unless self.git_master_head(:required=>false)
@@ -253,6 +286,15 @@ module XMigra
253
286
  return nil
254
287
  end
255
288
 
289
+ def vcs_contents(path, options={})
290
+ args = []
291
+
292
+ commit = options.fetch(:revision, 'HEAD')
293
+ args << "#{commit}:#{path}"
294
+
295
+ git(:show, *args)
296
+ end
297
+
256
298
  def vcs_prod_chain_extension_handler
257
299
  attr_val = GitSpecifics.attr_values(
258
300
  PRODUCTION_CHAIN_EXTENSION_COMMAND,
@@ -273,6 +315,124 @@ module XMigra
273
315
  return attr_val
274
316
  end
275
317
 
318
+ def vcs_uncommitted?
319
+ git_status == '??'
320
+ end
321
+
322
+ class VersionComparator
323
+ # vcs_object.kind_of?(GitSpecifics)
324
+ def initialize(vcs_object, options={})
325
+ @object = vcs_object
326
+ @expected_content_method = options[:expected_content_method]
327
+ @path_statuses = Hash.new do |h, file_path|
328
+ file_path = Pathname(file_path).expand_path
329
+ next h[file_path] if h.has_key?(file_path)
330
+ h[file_path] = @object.git_retrieve_status(file_path)
331
+ end
332
+ end
333
+
334
+ def relative_version(file_path)
335
+ # Comparing @object.file_path (a) to file_path (b)
336
+ #
337
+ # returns: :newer, :equal, :older, or :missing
338
+
339
+ b_status = @path_statuses[file_path]
340
+
341
+ return :missing if b_status.nil? || b_status.include?('D')
342
+
343
+ a_status = @path_statuses[@object.file_path]
344
+
345
+ if a_status == '??' || a_status[0] == 'A'
346
+ if b_status == '??' || b_status[0] == 'A' || b_status.include?('M')
347
+ return relative_version_by_content(file_path)
348
+ end
349
+
350
+ return :older
351
+ elsif a_status == ' '
352
+ return :newer unless b_status == ' '
353
+
354
+ return begin
355
+ a_commit = latest_commit(@object.file_path)
356
+ b_commit = latest_commit(file_path)
357
+
358
+ if @object.git_commits_in? a_commit..b_commit, file_path
359
+ :newer
360
+ elsif @object.git_commits_in? b_commit..a_commit, @object.file_path
361
+ :older
362
+ else
363
+ :equal
364
+ end
365
+ end
366
+ elsif b_status == ' '
367
+ return :older
368
+ else
369
+ return relative_version_by_content(file_path)
370
+ end
371
+ end
372
+
373
+ def latest_commit(file_path)
374
+ @object.git(
375
+ :log,
376
+ '--pretty=format:%H',
377
+ '-1',
378
+ '--',
379
+ file_path
380
+ )
381
+ end
382
+
383
+ def relative_version_by_content(file_path)
384
+ ec_method = @expected_content_method
385
+ if !ec_method || @object.send(ec_method, file_path)
386
+ return :equal
387
+ else
388
+ return :newer
389
+ end
390
+ end
391
+ end
392
+
393
+ def vcs_comparator(options={})
394
+ VersionComparator.new(self, options)
395
+ end
396
+
397
+ def vcs_latest_revision(a_file=nil)
398
+ if a_file.nil? && defined? @vcs_latest_revision
399
+ return @vcs_latest_revision
400
+ end
401
+
402
+ git(
403
+ :log,
404
+ '-n1',
405
+ '--pretty=format:%H',
406
+ '--',
407
+ a_file || file_path,
408
+ :quiet=>true
409
+ ).chomp.tap do |val|
410
+ @vcs_latest_revision = val if a_file.nil?
411
+ end
412
+ end
413
+
414
+ def vcs_changes_from(from_commit, file_path)
415
+ git(:diff, from_commit, '--', file_path)
416
+ end
417
+
418
+ def vcs_most_recent_committed_contents(file_path)
419
+ git(:show, "HEAD:#{file_path}", :quiet=>true)
420
+ end
421
+
422
+ def git_status
423
+ @git_status ||= git_retrieve_status(file_path)
424
+ end
425
+
426
+ def git_retrieve_status(a_path)
427
+ return nil unless Pathname(a_path).exist?
428
+
429
+ if git('status', '--porcelain', a_path.to_s) =~ /^.+?(?= \S)/
430
+ $&
431
+ else
432
+ ' '
433
+ end
434
+ end
435
+
276
436
  def production_pattern
277
437
  ".+"
278
438
  end
@@ -333,17 +493,17 @@ module XMigra
333
493
  :required=>options[:required]
334
494
  )
335
495
  return nil if master_head.nil?
336
- return @git_master_head = master_head
496
+ return @git_master_head = (master_head if master_head != GitSpecifics::ATTRIBUTE_UNSPECIFIED)
337
497
  end
338
498
 
339
499
  def git_branch
340
500
  return @git_branch if defined? @git_branch
341
- return @git_branch = git('rev-parse', %w{--abbrev-ref HEAD}).chomp
501
+ return @git_branch = git('rev-parse', %w{--abbrev-ref HEAD}, :quiet=>true).chomp
342
502
  end
343
503
 
344
504
  def git_schema_commit
345
505
  return @git_commit if defined? @git_commit
346
- reported_commit = git(:log, %w{-n1 --format=%H --}, self.path).chomp
506
+ reported_commit = git(:log, %w{-n1 --format=%H --}, self.path, :quiet=>true).chomp
347
507
  raise VersionControlError, "Schema not committed" if reported_commit.empty?
348
508
  return @git_commit = reported_commit
349
509
  end
@@ -412,5 +572,26 @@ module XMigra
412
572
  path || self.path
413
573
  ) != ''
414
574
  end
575
+
576
+ class RepoStoredMigrationChain < MigrationChain
577
+ def initialize(branch, path, options={})
578
+ @branch = branch
579
+ options[:vcs_specifics] = GitSpecifics
580
+ super(path, options)
581
+ end
582
+
583
+ protected
584
+ def yaml_of_file(fpath)
585
+ fdir, fname = Pathname(fpath).split
586
+ file_contents = Dir.chdir(fdir) do |pwd|
587
+ GitSpecifics.run_git(:show, "#{@branch}:./#{fname}")
588
+ end
589
+ begin
590
+ YAML.load(file_contents, fpath.to_s)
591
+ rescue
592
+ raise XMigra::Error, "Error loading/parsing #{fpath}"
593
+ end
594
+ end
595
+ end
415
596
  end
416
597
  end
@@ -31,7 +31,7 @@ module XMigra
31
31
  )
32
32
  cmd_str = cmd_parts.join(' ')
33
33
 
34
- output = `#{cmd_str}`
34
+ output = `#{cmd_str} 2>/dev/null`
35
35
  raise(VersionControlError, "Subversion command failed with exit code #{$?.exitstatus}") unless $?.success?
36
36
  return output if raw_result && !no_result
37
37
  return REXML::Document.new(output) unless no_result
@@ -222,6 +222,75 @@ END_OF_MESSAGE
222
222
  subversion(:resolve, '--accept=working', path, :get_result=>false)
223
223
  end
224
224
 
225
+ def vcs_uncommitted?
226
+ status = subversion_retrieve_status(file_path).elements['entry/wc-status']
227
+ status.nil? || status.attributes['item'] == 'unversioned'
228
+ end
229
+
230
+ class VersionComparator
231
+ # vcs_object.kind_of?(SubversionSpecifics)
232
+ def initialize(vcs_object, options={})
233
+ @object = vcs_object
234
+ @expected_content_method = options[:expected_content_method]
235
+ @path_status = Hash.new do |h, file_path|
236
+ file_path = Pathname(file_path).expand_path
237
+ next h[file_path] if h.has_key?(file_path)
238
+ h[file_path] = @object.subversion_retrieve_status(file_path)
239
+ end
240
+ end
241
+
242
+ def relative_version(file_path)
243
+ # Comparing @object.file_path (a) to file_path (b)
244
+ #
245
+ # returns: :newer, :equal, :older, or :missing
246
+
247
+ b_status = @path_status[file_path].elements['entry/wc-status']
248
+
249
+ return :missing if b_status.nil? || ['deleted', 'missing'].include?(b_status.attributes['item'])
250
+
251
+ a_status = @path_status[@object.file_path].elements['entry/wc-status']
252
+
253
+ if ['unversioned', 'added'].include? a_status.attributes['item']
254
+ if ['unversioned', 'added', 'modified'].include? b_status.attributes['item']
255
+ return relative_version_by_content(file_path)
256
+ end
257
+
258
+ return :older
259
+ elsif a_status.attributes['item'] == 'normal'
260
+ return :newer unless b_status.attributes['item'] == 'normal'
261
+
262
+ return begin
263
+ a_revision = a_status.elements['commit'].attributes['revision'].to_i
264
+ b_revision = b_status.elements['commit'].attributes['revision'].to_i
265
+
266
+ if a_revision < b_revision
267
+ :newer
268
+ elsif b_revision < a_revision
269
+ :older
270
+ else
271
+ :equal
272
+ end
273
+ end
274
+ elsif b_status.attributes['item'] == 'normal'
275
+ return :older
276
+ else
277
+ return relative_version_by_content(file_path)
278
+ end
279
+ end
280
+
281
+ def relative_version_by_content(file_path)
282
+ ec_method = @expected_content_method
283
+ if !ec_method || @object.send(ec_method, file_path)
284
+ return :equal
285
+ else
286
+ return :newer
287
+ end
288
+ end
289
+ end
290
+
291
+ def vcs_comparator(options={})
292
+ VersionComparator.new(self, options)
293
+ end
225
294
 
226
295
  def vcs_move(old_path, new_path)
227
296
  subversion(:move, old_path, new_path, :get_result=>false)
@@ -264,6 +333,43 @@ END_OF_MESSAGE
264
333
  subversion(:cat, "-r#{tracer.earliest_loaded_revision}", path.to_s)
265
334
  end
266
335
  end
336
+
337
+ def vcs_contents(path, options={})
338
+ args = []
339
+
340
+ if options[:revision]
341
+ args << "-r#{options[:revision]}"
342
+ end
343
+
344
+ args << path.to_s
345
+
346
+ subversion(:cat, *args)
347
+ end
348
+
349
+ def vcs_latest_revision(a_file=nil)
350
+ if a_file.nil? && defined? @vcs_latest_revision
351
+ return @vcs_latest_revision
352
+ end
353
+
354
+ val = subversion(:status, '-v', a_file || file_path).elements[
355
+ 'string(status/target/entry/wc-status/commit/@revision)'
356
+ ]
357
+ (val.nil? ? val : val.to_i).tap do |val|
358
+ @vcs_latest_revision = val if a_file.nil?
359
+ end
360
+ end
361
+
362
+ def vcs_changes_from(from_revision, file_path)
363
+ subversion(:diff, '-r', from_revision, file_path, :raw=>true)
364
+ end
365
+
366
+ def vcs_most_recent_committed_contents(file_path)
367
+ subversion(:cat, file_path)
368
+ end
369
+
370
+ def subversion_retrieve_status(file_path)
371
+ subversion(:status, '-v', file_path).elements['status/target']
372
+ end
267
373
  end
268
374
 
269
375
  class SvnHistoryTracer
@@ -1,3 +1,3 @@
1
1
  module XMigra
2
- VERSION = "1.5.1"
2
+ VERSION = "1.6.0"
3
3
  end
data/lib/xmigra.rb CHANGED
@@ -251,8 +251,47 @@ module XMigra
251
251
  end
252
252
  end
253
253
 
254
- def secure_digest(s)
255
- [Digest::MD5.digest(s)].pack('m0').chomp
254
+ def secure_digest(s, options={:encoding=>:base64})
255
+ digest_value = Digest::MD5.digest(s)
256
+ case options[:encoding]
257
+ when nil
258
+ digest_value
259
+ when :base64
260
+ [digest_value].pack('m0').chomp
261
+ when :base32
262
+ base32encoding(digest_value)
263
+ end
264
+ end
265
+
266
+ BASE_32_ENCODING = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
267
+ def base32encoding(bytes)
268
+ carry = 0
269
+ carry_bits = 0
270
+ ''.tap do |result|
271
+ bytes.each_byte do |b|
272
+ # From b we need the (5 - carry_bits) most significant bits
273
+ needed_bits = 5 - carry_bits
274
+ code_unit = (carry << needed_bits) | b >> (8 - needed_bits)
275
+ result << BASE_32_ENCODING[code_unit]
276
+
277
+ if needed_bits <= 3
278
+ # Extra character out of this byte
279
+ code_unit = (b >> (3 - needed_bits)) & 0x1F
280
+ result << BASE_32_ENCODING[code_unit]
281
+ carry_bits = (3 - needed_bits)
282
+ else
283
+ carry_bits = 8 - needed_bits
284
+ end
285
+ carry = b & ((1 << carry_bits) - 1)
286
+ end
287
+
288
+ if carry_bits > 0
289
+ code_unit = carry << (5 - carry_bits)
290
+ result << BASE_32_ENCODING[code_unit]
291
+ end
292
+
293
+ result << '=' * (7 - ((result.length + 7) % 8))
294
+ end
256
295
  end
257
296
  end
258
297
 
@@ -300,6 +339,9 @@ module XMigra
300
339
  ARGV,
301
340
  :error=>proc do |e|
302
341
  STDERR.puts("#{e} (#{e.class})") unless e.is_a?(XMigra::Program::QuietError)
342
+ if e.class.const_defined? :COMMAND_LINE_HELP
343
+ STDERR.puts(XMigra.program_message(e.class::COMMAND_LINE_HELP))
344
+ end
303
345
  exit(2) if e.is_a?(OptionParser::ParseError)
304
346
  exit(2) if e.is_a?(XMigra::Program::ArgumentError)
305
347
  exit(1)
@@ -322,6 +364,7 @@ require 'xmigra/function'
322
364
  require 'xmigra/plugin'
323
365
 
324
366
  require 'xmigra/access_artifact_collection'
367
+ require 'xmigra/impdecl_migration_adder'
325
368
  require 'xmigra/index'
326
369
  require 'xmigra/index_collection'
327
370
  require 'xmigra/migration'
@@ -331,6 +374,8 @@ require 'xmigra/branch_upgrade'
331
374
  require 'xmigra/schema_manipulator'
332
375
  require 'xmigra/schema_updater'
333
376
  require 'xmigra/new_migration_adder'
377
+ require 'xmigra/new_index_adder'
378
+ require 'xmigra/new_access_artifact_adder'
334
379
  require 'xmigra/permission_script_writer'
335
380
  require 'xmigra/source_tree_initializer'
336
381
 
data/test/git_vcs.rb CHANGED
@@ -167,10 +167,6 @@ if GIT_PRESENT
167
167
 
168
168
  `git clone "#{upstream.expand_path}" "#{repo}" 2>/dev/null`
169
169
 
170
- Dir.chdir(upstream) do
171
- commit_a_migration "foo table"
172
- end
173
-
174
170
  Dir.chdir(repo) do
175
171
  commit_a_migration "bar table"
176
172
 
@@ -268,4 +264,68 @@ if GIT_PRESENT
268
264
  end
269
265
  end
270
266
  end
267
+
268
+ run_test "XMigra does not put grants in upgrade by default" do
269
+ 1.temp_dirs do |repo|
270
+ initialize_git_repo(repo)
271
+
272
+ Dir.chdir(repo) do
273
+ commit_a_migration "first table"
274
+ File.open(XMigra::SchemaManipulator::PERMISSIONS_FILE, 'w') do |grants_file|
275
+ YAML.dump(
276
+ {
277
+ "foo" => {
278
+ "alice" => "ALL",
279
+ "bob" => "SELECT",
280
+ "candace" => ["INSERT", "SELECT", "UPDATE"],
281
+ },
282
+ },
283
+ grants_file
284
+ )
285
+ end
286
+
287
+ XMigra::SchemaUpdater.new('.').tap do |tool|
288
+ sql = tool.update_sql
289
+ assert_not_include sql, /GRANT\s+ALL.*?alice/
290
+ assert_not_include sql, /GRANT\s+SELECT.*?bob/
291
+ assert_not_include sql, /GRANT\s+INSERT.*?candace/
292
+ assert_not_include sql, /GRANT\s+SELECT.*?candace/
293
+ assert_not_include sql, /GRANT\s+UPDATE.*?candace/
294
+ end
295
+ end
296
+ end
297
+ end
298
+
299
+ run_test "XMigra puts grants in upgrade when requested" do
300
+ 1.temp_dirs do |repo|
301
+ initialize_git_repo(repo)
302
+
303
+ Dir.chdir(repo) do
304
+ commit_a_migration "first table"
305
+ File.open(XMigra::SchemaManipulator::PERMISSIONS_FILE, 'w') do |grants_file|
306
+ YAML.dump(
307
+ {
308
+ "foo" => {
309
+ "alice" => "ALL",
310
+ "bob" => "SELECT",
311
+ "candace" => ["INSERT", "SELECT", "UPDATE"],
312
+ },
313
+ },
314
+ grants_file
315
+ )
316
+ end
317
+
318
+ XMigra::SchemaUpdater.new('.').tap do |tool|
319
+ tool.include_grants = true
320
+
321
+ sql = tool.update_sql
322
+ assert_include sql, /GRANT\s+ALL.*?alice/
323
+ assert_include sql, /GRANT\s+SELECT.*?bob/
324
+ assert_include sql, /GRANT\s+INSERT.*?candace/
325
+ assert_include sql, /GRANT.*?SELECT.*?candace/
326
+ assert_include sql, /GRANT.*?UPDATE.*?candace/
327
+ end
328
+ end
329
+ end
330
+ end
271
331
  end
data/test/new_files.rb ADDED
@@ -0,0 +1,14 @@
1
+
2
+ XMigra::DatabaseSupportModules.each do |db_module|
3
+ ["migration", "index", "view", "function", "procedure"].each do |file_type_flag|
4
+ run_test "#{db_module::SYSTEM_NAME} support for new #{file_type_flag} file" do
5
+ in_xmigra_schema(:db_info=>{'system'=>db_module::SYSTEM_NAME}) do
6
+ begin
7
+ XMigra::Program.run(["new", "--no-edit", "--#{file_type_flag}", "foobarbaz"])
8
+ rescue XMigra::NewAccessArtifactAdder::UnsupportedArtifactType
9
+ # This is an acceptable error
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
data/test/runner.rb CHANGED
@@ -8,6 +8,8 @@ require 'tmpdir'
8
8
  TESTS = %w[
9
9
  git_vcs
10
10
  reversions
11
+ new_files
12
+ structure_declarative
11
13
  ]
12
14
 
13
15
  $:.unshift Pathname(__FILE__).expand_path.dirname.dirname + 'lib'
@@ -38,7 +40,13 @@ def run_test(name, &block)
38
40
 
39
41
  $test_count += 1
40
42
 
43
+ msg_receiver, msg_sender = IO.pipe
44
+
41
45
  if child_pid = Process.fork
46
+ msg_sender.close
47
+
48
+ # Must read to EOF or child may hang if pipe is filled
49
+ test_message = msg_receiver.read
42
50
  Process.wait(child_pid)
43
51
 
44
52
  if $?.success?
@@ -48,7 +56,14 @@ def run_test(name, &block)
48
56
  print 'F'
49
57
  $tests_failed << name
50
58
  end
59
+
60
+ if test_message.length > 0
61
+ ($test_messages ||= {})[name] = test_message
62
+ end
63
+ msg_receiver.close
51
64
  else
65
+ msg_receiver.close
66
+
52
67
  begin
53
68
  prev_stdout = $stdout
54
69
  $stdout = StringIO.new
@@ -59,11 +74,16 @@ def run_test(name, &block)
59
74
  end
60
75
  exit! 0
61
76
  rescue AssertionFailure
77
+ msg_sender.puts $!
62
78
  exit! 2
63
79
  rescue
64
- puts
65
- puts "Exception: #{$!}"
66
- puts $!.backtrace
80
+ msg_sender.puts "#{$!.class}: #{$!}"
81
+ msg_sender.puts $!.backtrace
82
+ $!.each_causing_exception do |ex|
83
+ msg_sender.puts
84
+ msg_sender.puts "Caused by #{ex.class}: #{ex}"
85
+ msg_sender.puts ex.backtrace
86
+ end
67
87
  exit! 1
68
88
  end
69
89
  end
@@ -96,8 +116,25 @@ def assert_neq(actual, expected)
96
116
  assert(proc {"Value #{actual.inspect} was unexpected"}) {actual != expected}
97
117
  end
98
118
 
119
+ def include_test(container, item)
120
+ case
121
+ when item.kind_of?(Regexp) && container.kind_of?(String)
122
+ item.match container
123
+ else
124
+ container.include? item
125
+ end
126
+ end
127
+
99
128
  def assert_include(container, item)
100
- assert(proc {"#{item.inspect} was not in #{container.inspect}"}) {container.include? item}
129
+ assert(proc {"#{item.inspect} was not in #{container.inspect}"}) do
130
+ include_test container, item
131
+ end
132
+ end
133
+
134
+ def assert_not_include(container, item)
135
+ assert(proc {"#{item.inspect} was in #{container.inspect}"}) do
136
+ !include_test container, item
137
+ end
101
138
  end
102
139
 
103
140
  def assert_raises(expected_exception)
@@ -155,3 +192,11 @@ puts
155
192
  puts "#{$test_successes}/#{$test_count} succeeded" if $test_options.show_counts
156
193
  puts "Failed tests:" unless $tests_failed.empty?
157
194
  $tests_failed.each {|name| puts " #{name}"}
195
+
196
+ ($test_messages || {}).each_pair do |test_name, message|
197
+ puts
198
+ puts "----- #{test_name} -----"
199
+ message.each_line do |msg_line|
200
+ puts msg_line.chomp
201
+ end
202
+ end