data_miner 3.0.0.alpha → 3.0.0.beta

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.
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/CHANGELOG CHANGED
@@ -1,3 +1,15 @@
1
+ 3.0.0.beta / 2013-07-26
2
+
3
+ * Enhancements
4
+
5
+ * test steps with after: and every: options, referring to row counts in the previous step
6
+ * :limit option for import steps - oh yeah
7
+ * :random_skip option - goes nicely with :limit if you're debugging
8
+
9
+ * Bug fixes
10
+
11
+ * Make sure to stringify data_miner(:append) option
12
+
1
13
  3.0.0.alpha / 2013-07-24
2
14
 
3
15
  * breaking changes
data/data_miner.gemspec CHANGED
@@ -25,7 +25,9 @@ Gem::Specification.new do |s|
25
25
  s.add_runtime_dependency 'posix-spawn'
26
26
  s.add_runtime_dependency 'unix_utils'
27
27
  s.add_runtime_dependency 'roo', '>=1.10.3'
28
-
28
+ s.add_runtime_dependency 'rspec-expectations'
29
+
30
+ s.add_development_dependency 'rspec'
29
31
  s.add_development_dependency 'pry'
30
32
  s.add_development_dependency 'active_record_inline_schema'
31
33
  s.add_development_dependency 'fuzzy_match'
data/lib/data_miner.rb CHANGED
@@ -21,6 +21,7 @@ require 'data_miner/step'
21
21
  require 'data_miner/step/import'
22
22
  require 'data_miner/step/process'
23
23
  require 'data_miner/step/sql'
24
+ require 'data_miner/step/test'
24
25
 
25
26
  # A singleton class that holds global configuration for data mining.
26
27
  #
@@ -91,6 +91,7 @@ class DataMiner
91
91
  #
92
92
  # @return [nil]
93
93
  def data_miner(options = {}, &blk)
94
+ options = options.stringify_keys
94
95
  unless options['append']
95
96
  @data_miner_script = nil
96
97
  end
@@ -92,6 +92,36 @@ class DataMiner
92
92
  append(:process, method_id_or_description, &blk)
93
93
  end
94
94
 
95
+ # A step that runs tests and stops the data miner on failures.
96
+ #
97
+ # rspec-expectations are automatically included.
98
+ #
99
+ # @see DataMiner::ActiveRecordClassMethods#data_miner Overview of how to define data miner scripts inside of ActiveRecord models.
100
+ # @see DataMiner::Step::Test The actual Test class.
101
+ #
102
+ # @param [String] description A description of what the block does.
103
+ # @param [Hash] settings Settings
104
+ # @option settings [String] :after After how many rows of the previous step to run the tests.
105
+ # @yield [] Tests to be run
106
+ #
107
+ # @example Tests
108
+ # data_miner do
109
+ # [...]
110
+ # test "make sure something works" do
111
+ # expect(Pet.count).to be > 10
112
+ # end
113
+ # [...]
114
+ # test "make sure something works", after: 20 do
115
+ # [...]
116
+ # end
117
+ # [...]
118
+ # end
119
+ #
120
+ # @return [nil]
121
+ def test(description, settings = {}, &blk)
122
+ append(:test, description, settings, &blk)
123
+ end
124
+
95
125
  # Import rows into your model.
96
126
  #
97
127
  # As long as...
@@ -217,6 +247,11 @@ class DataMiner
217
247
  Script.current_stack.clear
218
248
  end
219
249
  Script.current_stack << model_name
250
+ steps.each do |step|
251
+ steps.each do |other|
252
+ other.register step
253
+ end
254
+ end
220
255
  steps.each do |step|
221
256
  step.start
222
257
  model.reset_column_information
@@ -12,5 +12,21 @@ class DataMiner
12
12
  def model
13
13
  script.model
14
14
  end
15
+
16
+ def pos
17
+ script.steps.index self
18
+ end
19
+
20
+ def register(step)
21
+ # noop
22
+ end
23
+
24
+ def notify(*args)
25
+ # noop
26
+ end
27
+
28
+ def target?(*args)
29
+ false
30
+ end
15
31
  end
16
32
  end
@@ -20,6 +20,17 @@ class DataMiner
20
20
  # @return [String]
21
21
  attr_reader :description
22
22
 
23
+ # Max number of rows to import.
24
+ # @return [Numeric]
25
+ attr_reader :limit
26
+
27
+ # Number from zero to one representing what percentage of rows to skip. Defaults to 0, of course :)
28
+ # @return [Numeric]
29
+ attr_reader :random_skip
30
+
31
+ # @private
32
+ attr_reader :listeners
33
+
23
34
  # @private
24
35
  def initialize(script, description, settings, &blk)
25
36
  settings = settings.stringify_keys
@@ -41,6 +52,9 @@ class DataMiner
41
52
  @table_settings = settings.dup
42
53
  @table_settings['streaming'] = true
43
54
  @table_mutex = ::Mutex.new
55
+ @limit = settings.fetch 'limit', (1.0/0)
56
+ @random_skip = settings['random_skip']
57
+ @listeners = []
44
58
  instance_eval(&blk)
45
59
  end
46
60
 
@@ -94,6 +108,12 @@ class DataMiner
94
108
  @validate_query == true
95
109
  end
96
110
 
111
+ def register(step)
112
+ if step.target?(self)
113
+ listeners << step
114
+ end
115
+ end
116
+
97
117
  private
98
118
 
99
119
  def upsert_enabled?
@@ -110,7 +130,9 @@ class DataMiner
110
130
  count = 0
111
131
  Upsert.stream(c, model.table_name) do |upsert|
112
132
  table.each do |row|
133
+ next if random_skip and random_skip > Kernel.rand
113
134
  $stderr.puts "#{count}..." if count_every > 0 and count % count_every == 0
135
+ break if count > limit
114
136
  count += 1
115
137
  selector = @key ? { @key => attributes[@key].read(row) } : { model.primary_key => nil }
116
138
  document = attrs_except_key.inject({}) do |memo, attr|
@@ -125,6 +147,9 @@ class DataMiner
125
147
  memo
126
148
  end
127
149
  upsert.row selector, document
150
+ listeners.select! do |listener|
151
+ listener.notify self, count
152
+ end
128
153
  end
129
154
  end
130
155
  model.connection_pool.checkin c
@@ -133,11 +158,16 @@ class DataMiner
133
158
  def save_with_find_or_initialize
134
159
  count = 0
135
160
  table.each do |row|
161
+ next if random_skip and random_skip > Kernel.rand
136
162
  $stderr.puts "#{count}..." if count_every > 0 and count % count_every == 0
163
+ break if count > limit
137
164
  count += 1
138
165
  record = @key ? model.send("find_or_initialize_by_#{@key}", attributes[@key].read(row)) : model.new
139
166
  attributes.each { |_, attr| attr.set_from_row record, row }
140
167
  record.save!
168
+ listeners.select! do |listener|
169
+ listener.notify self, count
170
+ end
141
171
  end
142
172
  end
143
173
 
@@ -0,0 +1,77 @@
1
+ require 'rspec-expectations'
2
+
3
+ class DataMiner
4
+ class Step
5
+ # A step that runs tests and stops the data miner on failures.
6
+ #
7
+ # Create these by calling +test+ inside a +data_miner+ block.
8
+ #
9
+ # @see DataMiner::ActiveRecordClassMethods#data_miner Overview of how to define data miner scripts inside of ActiveRecord models.
10
+ # @see DataMiner::Script#test Creating a test step by calling DataMiner::Script#test from inside a data miner script
11
+ class Test < Step
12
+ include ::RSpec::Expectations
13
+ include ::RSpec::Matchers
14
+
15
+ # A description of what the block does. Doesn't exist when a single class method is specified using a Symbol.
16
+ # @return [String]
17
+ attr_reader :description
18
+
19
+ # The block of arbitrary code to be run.
20
+ # @return [Proc]
21
+ attr_reader :blk
22
+
23
+ # After how many rows of the previous step to run the tests.
24
+ # @return [Numeric]
25
+ attr_reader :after
26
+
27
+ # Every how many rows to run tests
28
+ # @return [Numeric]
29
+ attr_reader :every
30
+
31
+ alias :block_description :description
32
+
33
+ # @private
34
+ def initialize(script, description, settings, &blk)
35
+ @script = script
36
+ @description = description
37
+ @blk = blk
38
+ @after = settings[:after]
39
+ @every = settings[:every]
40
+ raise "can't do both after and every" if after and every
41
+ end
42
+
43
+ # @private
44
+ def start
45
+ if inline?
46
+ eval_catching_errors
47
+ end
48
+ nil
49
+ end
50
+
51
+ def target?(step)
52
+ !inline? and (step.pos == pos - 1)
53
+ end
54
+
55
+ def notify(step, count)
56
+ if count % (after || every) == 0
57
+ eval_catching_errors
58
+ !after # if it's an after, return false, so that we stop getting informed
59
+ else
60
+ true
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def inline?
67
+ not (after or every)
68
+ end
69
+
70
+ def eval_catching_errors
71
+ DataMiner::Script.uniq { instance_eval(&blk) }
72
+ rescue ::RSpec::Expectations::ExpectationNotMetError
73
+ raise RuntimeError, "FAILED: #{description} (#{$!.inspect})"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -1,3 +1,3 @@
1
1
  class DataMiner
2
- VERSION = '3.0.0.alpha'
2
+ VERSION = '3.0.0.beta'
3
3
  end
@@ -0,0 +1,25 @@
1
+ require 'bundler/setup'
2
+
3
+ # This file was generated by the `rspec --init` command. Conventionally, all
4
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
5
+ # Require this file using `require "spec_helper"` to ensure that it is only
6
+ # loaded once.
7
+ #
8
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
9
+ RSpec.configure do |config|
10
+ config.treat_symbols_as_metadata_keys_with_true_values = true
11
+ config.run_all_when_everything_filtered = true
12
+ config.filter_run :focus
13
+
14
+ # Run specs in random order to surface order dependencies. If you find an
15
+ # order dependency and want to debug it, you can fix the order by providing
16
+ # the seed, which is printed after each run.
17
+ # --seed 1234
18
+ config.order = 'random'
19
+ end
20
+
21
+ require_relative '../test/support/database'
22
+ init_database
23
+ init_models
24
+
25
+ require 'data_miner'
@@ -0,0 +1,134 @@
1
+ require 'spec_helper'
2
+
3
+ class PetTest1 < ActiveRecord::Base
4
+ self.primary_key = "name"
5
+ col :name
6
+ col :favorite_food
7
+ data_miner do
8
+ process :auto_upgrade!
9
+ import("A list of pets", :url => "file://#{Pet::PETS}") do
10
+ key :name
11
+ store :favorite_food
12
+ end
13
+ test "Jerry likes cheese" do
14
+ expect(PetTest1.find('Jerry').favorite_food).to eq 'cheese'
15
+ end
16
+ end
17
+ end
18
+
19
+ class PetTest2 < ActiveRecord::Base
20
+ self.primary_key = "name"
21
+ col :name
22
+ col :favorite_food
23
+ data_miner do
24
+ process :auto_upgrade!
25
+ import("A list of pets", :url => "file://#{Pet::PETS}") do
26
+ key :name
27
+ store :favorite_food
28
+ end
29
+ test "Jerry likes veggies" do
30
+ expect(PetTest2.find('Jerry').favorite_food).to eq 'veggies'
31
+ end
32
+ end
33
+ end
34
+
35
+ class PetTest3 < ActiveRecord::Base
36
+ self.primary_key = "name"
37
+ col :name
38
+ col :favorite_food
39
+ data_miner do
40
+ process :auto_upgrade!
41
+ import("A list of pets", :url => "file://#{Pet::PETS}") do
42
+ key :name
43
+ store :favorite_food
44
+ end
45
+ test "First few have somebody named Pierre", after: 2 do
46
+ expect(PetTest3.count).to eq 2
47
+ expect(PetTest3.where(name: 'Pierre').count).to be > 0
48
+ end
49
+ end
50
+ end
51
+
52
+ class PetTest4 < ActiveRecord::Base
53
+ self.primary_key = "name"
54
+ col :name
55
+ col :favorite_food
56
+ data_miner do
57
+ process :auto_upgrade!
58
+ import("A list of pets", :url => "file://#{Pet::PETS}") do
59
+ key :name
60
+ store :favorite_food
61
+ end
62
+ test "First few have somebody named Johnny", after: 2 do
63
+ expect(PetTest4.count).to eq 2 # that's where we are
64
+ expect(PetTest4.where(name: 'Johnny').count).to be > 0
65
+ end
66
+ end
67
+ end
68
+
69
+ $pet_test_5_i = 0
70
+ class PetTest5 < ActiveRecord::Base
71
+ self.primary_key = "name"
72
+ col :name
73
+ col :favorite_food
74
+ data_miner do
75
+ process :auto_upgrade!
76
+ import("A list of pets", :url => "file://#{Pet::PETS}") do
77
+ key :name
78
+ store :favorite_food
79
+ end
80
+ test "Everybody has a name", every: 2 do
81
+ $pet_test_5_i += 1
82
+ expect(PetTest5.count).to eq $pet_test_5_i*2
83
+ expect(PetTest5.where(name: nil).count).to be 0
84
+ end
85
+ end
86
+ end
87
+
88
+ $pet_test_6_i = 0
89
+ class PetTest6 < ActiveRecord::Base
90
+ self.primary_key = "name"
91
+ col :name
92
+ col :favorite_food
93
+ data_miner do
94
+ process :auto_upgrade!
95
+ import("A list of pets", :url => "file://#{Pet::PETS}") do
96
+ key :name
97
+ store :favorite_food
98
+ end
99
+ test "Everybody has a favorite food", every: 2 do
100
+ $pet_test_6_i += 1
101
+ expect(PetTest6.count).to eq $pet_test_6_i*2
102
+ expect(PetTest6.where(favorite_food: nil).count).to be 0
103
+ end
104
+ end
105
+ end
106
+
107
+ describe DataMiner::Step::Test do
108
+ it "keeps going if it passes" do
109
+ PetTest1.run_data_miner!
110
+ expect(PetTest1.count).to be > 0
111
+ end
112
+ it "stops on failure" do
113
+ expect { PetTest2.run_data_miner! }.to raise_error(/Jerry.*veggies/)
114
+ expect(PetTest2.count).to be > 0 # still populated tho
115
+ end
116
+ it "can be run in the middle of the previous step" do
117
+ PetTest3.run_data_miner!
118
+ expect(PetTest3.count).to be > 0
119
+ end
120
+ it "can be run in the middle of the previous step - failing" do
121
+ expect { PetTest4.run_data_miner! }.to raise_error(/First few.*Johnny/)
122
+ expect(PetTest4.count).to be 2 # stopped after 2
123
+ end
124
+ it "can be run every 2" do
125
+ PetTest5.run_data_miner!
126
+ expect($pet_test_5_i).to eq 2
127
+ expect(PetTest5.count).to be 5
128
+ end
129
+ it "can be run every 2 - failing" do
130
+ expect { PetTest6.run_data_miner! }.to raise_error(/Everybody has a favorite food/)
131
+ expect($pet_test_6_i).to eq 2
132
+ expect(PetTest6.count).to be $pet_test_6_i*2
133
+ end
134
+ end
data/test/helper.rb CHANGED
@@ -1,4 +1,3 @@
1
- require 'rubygems'
2
1
  require 'bundler/setup'
3
2
 
4
3
  if Bundler.definition.specs['debugger'].first
@@ -12,53 +11,6 @@ require 'minitest/autorun'
12
11
  require 'minitest/reporters'
13
12
  MiniTest::Reporters.use!
14
13
 
15
- require 'active_record'
16
- require 'logger'
17
- ActiveRecord::Base.logger = Logger.new $stderr
18
- ActiveRecord::Base.logger.level = (ENV['VERBOSE'] == 'true') ? Logger::DEBUG : Logger::INFO
19
-
20
- ActiveRecord::Base.mass_assignment_sanitizer = :strict
21
-
22
- require 'active_record_inline_schema'
14
+ require_relative 'support/database'
23
15
 
24
16
  require 'data_miner'
25
-
26
- def init_database
27
- case ENV['DATABASE']
28
- when /postgr/i
29
- system %{dropdb test_data_miner}
30
- system %{createdb test_data_miner}
31
- ActiveRecord::Base.establish_connection(
32
- 'adapter' => 'postgresql',
33
- 'encoding' => 'utf8',
34
- 'database' => 'test_data_miner',
35
- 'username' => `whoami`.chomp
36
- )
37
- when /sqlite/i
38
- ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
39
- else
40
- system %{mysql -u root -ppassword -e "DROP DATABASE test_data_miner"}
41
- system %{mysql -u root -ppassword -e "CREATE DATABASE test_data_miner CHARSET utf8"}
42
- ActiveRecord::Base.establish_connection(
43
- 'adapter' => (RUBY_PLATFORM == 'java' ? 'mysql' : 'mysql2'),
44
- 'encoding' => 'utf8',
45
- 'database' => 'test_data_miner',
46
- 'username' => 'root',
47
- 'password' => 'password'
48
- )
49
- end
50
- end
51
-
52
- def init_models
53
- require 'support/breed'
54
- require 'support/pet'
55
- require 'support/pet2'
56
- require 'support/pet3'
57
- Pet.auto_upgrade!
58
- Pet2.auto_upgrade!
59
- Pet3.auto_upgrade!
60
-
61
- ActiveRecord::Base.descendants.each do |model|
62
- model.attr_accessible nil
63
- end
64
- end
@@ -0,0 +1,50 @@
1
+ require 'active_record'
2
+ require 'logger'
3
+ ActiveRecord::Base.logger = Logger.new $stderr
4
+ ActiveRecord::Base.logger.level = (ENV['VERBOSE'] == 'true') ? Logger::DEBUG : Logger::INFO
5
+
6
+ ActiveRecord::Base.mass_assignment_sanitizer = :strict
7
+
8
+ require 'active_record_inline_schema'
9
+
10
+ require 'data_miner'
11
+
12
+ def init_database
13
+ case ENV['DATABASE']
14
+ when /postgr/i
15
+ system %{dropdb test_data_miner}
16
+ system %{createdb test_data_miner}
17
+ ActiveRecord::Base.establish_connection(
18
+ 'adapter' => 'postgresql',
19
+ 'encoding' => 'utf8',
20
+ 'database' => 'test_data_miner',
21
+ 'username' => `whoami`.chomp
22
+ )
23
+ when /sqlite/i
24
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
25
+ else
26
+ system %{mysql -u root -ppassword -e "DROP DATABASE test_data_miner"}
27
+ system %{mysql -u root -ppassword -e "CREATE DATABASE test_data_miner CHARSET utf8"}
28
+ ActiveRecord::Base.establish_connection(
29
+ 'adapter' => (RUBY_PLATFORM == 'java' ? 'mysql' : 'mysql2'),
30
+ 'encoding' => 'utf8',
31
+ 'database' => 'test_data_miner',
32
+ 'username' => 'root',
33
+ 'password' => 'password'
34
+ )
35
+ end
36
+ end
37
+
38
+ def init_models
39
+ require_relative 'breed'
40
+ require_relative 'pet'
41
+ require_relative 'pet2'
42
+ require_relative 'pet3'
43
+ Pet.auto_upgrade!
44
+ Pet2.auto_upgrade!
45
+ Pet3.auto_upgrade!
46
+
47
+ ActiveRecord::Base.descendants.each do |model|
48
+ model.attr_accessible nil
49
+ end
50
+ end
data/test/support/pet.rb CHANGED
@@ -15,9 +15,15 @@ class Pet < ActiveRecord::Base
15
15
  col :command_phrase
16
16
  col :emphatic_command_phrase
17
17
  belongs_to :breed
18
+
19
+
20
+
18
21
  data_miner do
22
+
19
23
  process :auto_upgrade!
24
+
20
25
  process :run_data_miner_on_parent_associations!
26
+
21
27
  import("A list of pets", :url => "file://#{PETS}") do
22
28
  key :name
23
29
  store :age
@@ -31,5 +37,6 @@ class Pet < ActiveRecord::Base
31
37
  (row['command_phrase'] + "!!!!!") if row['command_phrase']
32
38
  end
33
39
  end
40
+
34
41
  end
35
42
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: data_miner
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.alpha
4
+ version: 3.0.0.beta
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2013-07-25 00:00:00.000000000 Z
16
+ date: 2013-07-26 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: activerecord
@@ -143,6 +143,38 @@ dependencies:
143
143
  - - ! '>='
144
144
  - !ruby/object:Gem::Version
145
145
  version: 1.10.3
146
+ - !ruby/object:Gem::Dependency
147
+ name: rspec-expectations
148
+ requirement: !ruby/object:Gem::Requirement
149
+ none: false
150
+ requirements:
151
+ - - ! '>='
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ type: :runtime
155
+ prerelease: false
156
+ version_requirements: !ruby/object:Gem::Requirement
157
+ none: false
158
+ requirements:
159
+ - - ! '>='
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ - !ruby/object:Gem::Dependency
163
+ name: rspec
164
+ requirement: !ruby/object:Gem::Requirement
165
+ none: false
166
+ requirements:
167
+ - - ! '>='
168
+ - !ruby/object:Gem::Version
169
+ version: '0'
170
+ type: :development
171
+ prerelease: false
172
+ version_requirements: !ruby/object:Gem::Requirement
173
+ none: false
174
+ requirements:
175
+ - - ! '>='
176
+ - !ruby/object:Gem::Version
177
+ version: '0'
146
178
  - !ruby/object:Gem::Dependency
147
179
  name: pry
148
180
  requirement: !ruby/object:Gem::Requirement
@@ -333,6 +365,7 @@ extensions: []
333
365
  extra_rdoc_files: []
334
366
  files:
335
367
  - .gitignore
368
+ - .rspec
336
369
  - .yardopts
337
370
  - CHANGELOG
338
371
  - Gemfile
@@ -348,7 +381,10 @@ files:
348
381
  - lib/data_miner/step/import.rb
349
382
  - lib/data_miner/step/process.rb
350
383
  - lib/data_miner/step/sql.rb
384
+ - lib/data_miner/step/test.rb
351
385
  - lib/data_miner/version.rb
386
+ - spec/spec_helper.rb
387
+ - spec/test_step_spec.rb
352
388
  - test/data_miner/step/test_sql.rb
353
389
  - test/data_miner/test_attribute.rb
354
390
  - test/helper.rb
@@ -356,6 +392,7 @@ files:
356
392
  - test/support/breed_by_license_number.csv
357
393
  - test/support/breeds.xls
358
394
  - test/support/data_miner_with_alchemist.rb
395
+ - test/support/database.rb
359
396
  - test/support/pet.rb
360
397
  - test/support/pet2.rb
361
398
  - test/support/pet3.rb
@@ -390,6 +427,8 @@ specification_version: 3
390
427
  summary: Download, pull out of a ZIP/TAR/GZ/BZ2 archive, parse, correct, and import
391
428
  XLS, ODS, XML, CSV, HTML, etc. into your ActiveRecord models.
392
429
  test_files:
430
+ - spec/spec_helper.rb
431
+ - spec/test_step_spec.rb
393
432
  - test/data_miner/step/test_sql.rb
394
433
  - test/data_miner/test_attribute.rb
395
434
  - test/helper.rb
@@ -397,6 +436,7 @@ test_files:
397
436
  - test/support/breed_by_license_number.csv
398
437
  - test/support/breeds.xls
399
438
  - test/support/data_miner_with_alchemist.rb
439
+ - test/support/database.rb
400
440
  - test/support/pet.rb
401
441
  - test/support/pet2.rb
402
442
  - test/support/pet3.rb