priority_test 0.0.1 → 0.0.2

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 (48) hide show
  1. data/.gitignore +1 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +0 -1
  4. data/README.md +64 -1
  5. data/Rakefile +9 -12
  6. data/bin/pt +25 -0
  7. data/lib/priority_test/autorun.rb +2 -0
  8. data/lib/priority_test/core/all_tests.rb +30 -0
  9. data/lib/priority_test/core/configuration.rb +23 -0
  10. data/lib/priority_test/core/configuration_options.rb +19 -0
  11. data/lib/priority_test/core/option_parser.rb +84 -0
  12. data/lib/priority_test/core/priority.rb +62 -0
  13. data/lib/priority_test/core/priority_sort_element.rb +23 -0
  14. data/lib/priority_test/core/runner.rb +26 -0
  15. data/lib/priority_test/core/service.rb +14 -0
  16. data/lib/priority_test/core/test.rb +53 -0
  17. data/lib/priority_test/core/test_result.rb +25 -0
  18. data/lib/priority_test/core/test_result_collector.rb +40 -0
  19. data/lib/priority_test/core/validations_helper.rb +17 -0
  20. data/lib/priority_test/core.rb +41 -0
  21. data/lib/priority_test/gateway/migrations/001_create_tests.rb +11 -0
  22. data/lib/priority_test/gateway/migrations/002_create_test_results.rb +12 -0
  23. data/lib/priority_test/gateway/sequel.rb +33 -0
  24. data/lib/priority_test/gateway.rb +13 -0
  25. data/lib/priority_test/rspec.rb +19 -0
  26. data/lib/priority_test/rspec2/example_group_sorter.rb +24 -0
  27. data/lib/priority_test/rspec2/example_sorter.rb +12 -0
  28. data/lib/priority_test/rspec2/formatter.rb +35 -0
  29. data/lib/priority_test/rspec2/patch/example_group.rb +18 -0
  30. data/lib/priority_test/rspec2/patch/world.rb +7 -0
  31. data/lib/priority_test/rspec2/relative_path.rb +13 -0
  32. data/lib/priority_test/rspec2.rb +20 -0
  33. data/lib/priority_test/version.rb +1 -1
  34. data/lib/priority_test.rb +25 -2
  35. data/priority_test.gemspec +4 -5
  36. data/spec/core/all_tests_spec.rb +53 -0
  37. data/spec/core/config_spec.rb +53 -0
  38. data/spec/core/configuration_options_spec.rb +16 -0
  39. data/spec/core/option_parser_spec.rb +42 -0
  40. data/spec/core/priority_spec.rb +9 -0
  41. data/spec/core/service_spec.rb +55 -0
  42. data/spec/core/test_result_spec.rb +29 -0
  43. data/spec/core/test_spec.rb +121 -0
  44. data/spec/gateways/sequel_spec.rb +43 -0
  45. data/spec/rspec2/formatter_spec.rb +67 -0
  46. data/spec/spec_helper.rb +18 -0
  47. data/spec/support/rspec_factory.rb +17 -0
  48. metadata +80 -5
data/.gitignore CHANGED
@@ -2,3 +2,4 @@
2
2
  .bundle
3
3
  Gemfile.lock
4
4
  pkg/*
5
+ .priority-test.db
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile CHANGED
@@ -1,4 +1,3 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- # Specify your gem's dependencies in priority_test.gemspec
4
3
  gemspec
data/README.md CHANGED
@@ -3,8 +3,71 @@ PriorityTest
3
3
 
4
4
  # DESCRIPTION
5
5
 
6
- # ASSUMPTION
6
+ PriorityTest is a gem that delivers fast feedback for your tests by
7
+ prioritizing them.
8
+ It prioritizes tests based on two assumptions discovered by [Kent Beck](https://twitter.com/#!/kentbeck) in his tool [JUnit Max](http://junitmax.com/):
9
+
10
+ > Test run times generally follow a power law distribution - lots of very short tests and a few very long ones. This means that by running the short tests first you can get most of the feedback in a fraction of the runtime of the whole suite.
11
+
12
+ > Failures are not randomly distributed. A test that failed recently is more likely to fail than one that has run correctly a bazillion times in a row. By putting recently failed (and newly written) tests first in the queue, you maximize the information density of that critical first second of feedback.
13
+
14
+ PriorityTest inherits from these two assumptions with a simple
15
+ algorithm and prioritizes your tests by looking at the test running history.
16
+
17
+ # ALGORITHM
18
+
19
+ PriorityTest captures and stores your test running hisotry.
20
+ Before each test runs, it looks back X number of the previous test results to calculate the test's Degree of Significant (DoS).
21
+ It then prioritizes the running order of all the tests based on each test's DoS.
22
+ Two factors determines a test's DoS: test run time and recent failure times.
7
23
 
8
24
  # INSTALLATION
9
25
 
26
+ ## RubyGems
27
+
28
+ [sudo] gem install priority_test
29
+
30
+ ## RSpec
31
+
32
+ In your ```Gemfile```, insert the following line:
33
+
34
+ ```ruby
35
+ gem 'priority_test'
36
+ ```
37
+
38
+ In ```spec_helper.rb```, require the RSpec adapter:
39
+
40
+ ```ruby
41
+ require 'priority_test/rspec'
42
+ ```
43
+
10
44
  # USAGE
45
+
46
+ Getting help:
47
+
48
+ $ pt -h
49
+ Usage: pt <test-framework> [options] [files or directories]
50
+
51
+ Test framework:
52
+ * rspec
53
+
54
+ Options:
55
+ --priority Filter and run priority tests
56
+ -h, --help Show help
57
+ -v, --version Show version
58
+
59
+ Run tests in priority order:
60
+
61
+ $ pt rspec spec/a_spec
62
+
63
+ Filter and run priority tests:
64
+
65
+ $ pt rspec spec/a_spec --priority
66
+
67
+ Directly passing arguments to RSpec:
68
+
69
+ $ pt rspec spec/a_spec --priority -fp
70
+
71
+ Run tests in a Rake task:
72
+
73
+ $ rake spec PT_OPTS="--priority"
data/Rakefile CHANGED
@@ -41,21 +41,18 @@ end
41
41
  #
42
42
  #############################################################################
43
43
 
44
- task :default => :test
44
+ task :default => :spec
45
45
 
46
- require 'rake/testtask'
47
- Rake::TestTask.new(:test) do |test|
48
- test.libs << 'lib' << 'test'
49
- test.pattern = 'test/**/test_*.rb'
50
- test.verbose = true
46
+ require 'rspec/core/rake_task'
47
+ RSpec::Core::RakeTask.new(:spec) do |t|
48
+ t.pattern = "./spec/**/*_spec.rb"
51
49
  end
52
50
 
53
51
  desc "Generate RCov test coverage and open in your browser"
54
- task :coverage do
55
- require 'rcov'
56
- sh "rm -fr coverage"
57
- sh "rcov test/test_*.rb"
58
- sh "open coverage/index.html"
52
+ RSpec::Core::RakeTask.new(:rcov) do |t|
53
+ t.rcov = true
54
+ t.pattern = "./spec/**/*_spec.rb"
55
+ t.rcov_opts = '--exclude /gems/,/Library/,/usr/,lib/tasks,.bundle,config,/lib/rspec/,/lib/rspec-,spec'
59
56
  end
60
57
 
61
58
  require 'rdoc/task'
@@ -68,7 +65,7 @@ end
68
65
 
69
66
  desc "Open an irb session preloaded with this library"
70
67
  task :console do
71
- sh "irb -rubygems -r ./lib/#{name}.rb"
68
+ sh "irb -rubygems -r ./lib/#{name}.rb -I ./lib"
72
69
  end
73
70
 
74
71
  #############################################################################
data/bin/pt ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'priority_test/autorun'
5
+ rescue LoadError
6
+ $stderr.puts <<-EOS
7
+ #{'*'*50}
8
+ Could not find 'priority/autorun'
9
+
10
+ This may happen if you're using rubygems as your package manager, but it is not
11
+ being required through some mechanism before executing the priority_test command.
12
+
13
+ You may need to do one of the following in your shell:
14
+
15
+ # for bash/zsh
16
+ export RUBYOPT=rubygems
17
+
18
+ # for csh, etc.
19
+ set RUBYOPT=rubygems
20
+
21
+ For background, please see http://gist.github.com/54177.
22
+ #{'*'*50}
23
+ EOS
24
+ exit(1)
25
+ end
@@ -0,0 +1,2 @@
1
+ require 'priority_test'
2
+ PriorityTest::Core::Runner.autorun
@@ -0,0 +1,30 @@
1
+ module PriorityTest
2
+ module Core
3
+ class AllTests
4
+ def initialize(tests=[])
5
+ @test_hash = Hash[tests.collect { |t| [t.identifier, t] }]
6
+ end
7
+
8
+ def add_test(test_params)
9
+ test = Test.create(test_params)
10
+ @test_hash[test.identifier] = test
11
+ end
12
+
13
+ def get_test(identifier)
14
+ @test_hash[identifier]
15
+ end
16
+
17
+ def add_test_result(identifier, test_result_params)
18
+ test = get_test(identifier)
19
+ if test
20
+ test.add_result(test_result_params)
21
+ test.update_statistics
22
+ end
23
+ end
24
+
25
+ def size
26
+ @test_hash.size
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,23 @@
1
+ module PriorityTest
2
+ module Core
3
+ class Configuration
4
+ def self.add_setting(name, opts={})
5
+ define_method("#{name}=") { |val| settings[name] = val}
6
+ define_method(name) { settings.has_key?(name) ? settings[name] : opts[:default] }
7
+ define_method("#{name}?") { send name }
8
+ end
9
+
10
+ add_setting :test_framework
11
+ add_setting :database, :default => "sqlite://#{File.join(File.expand_path('.'), '.priority-test.db')}"
12
+ add_setting :priority, :default => false
13
+
14
+ def settings
15
+ @settings ||= {}
16
+ end
17
+
18
+ def add_setting(name, opts={})
19
+ self.class.add_setting(name, opts)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ module PriorityTest
2
+ module Core
3
+ class ConfigurationOptions
4
+ def initialize(args=[])
5
+ @args = args
6
+ end
7
+
8
+ def configure(config)
9
+ options.each do |key, value|
10
+ config.send("#{key}=", options[key]) if config.respond_to?("#{key}=")
11
+ end
12
+ end
13
+
14
+ def options
15
+ @options ||= OptionParser.parse!(@args)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,84 @@
1
+ require 'optparse'
2
+
3
+ module PriorityTest
4
+ module Core
5
+ class OptionParser
6
+ AVAILABLE_TEST_FRAMEWORKS = ['rspec']
7
+ OPTIONS = ['--priority']
8
+
9
+ def self.parse!(args)
10
+ new.parse!(args)
11
+ end
12
+
13
+ def self.parse_options(args)
14
+ new.parse_options(args)
15
+ end
16
+
17
+ def parse_options(args)
18
+ return {} if args.empty?
19
+
20
+ options = {}
21
+ opts_parser = parser(options)
22
+ begin
23
+ opts_parser.parse!(args.clone)
24
+ rescue ::OptionParser::InvalidOption
25
+ end
26
+
27
+ remove_parsed_options(args)
28
+
29
+ options
30
+ end
31
+
32
+ def parse!(args)
33
+ return {} if args.empty?
34
+
35
+ options = parse_options(args)
36
+ options[:test_framework] = parse_test_framework(args)
37
+
38
+ options
39
+ end
40
+
41
+ def parser(options)
42
+ ::OptionParser.new do |parser|
43
+ parser.banner = <<-EOS
44
+ Usage: pt <test-framework> [options] [files or directories]
45
+
46
+ Test framework:
47
+ * rspec
48
+
49
+ Options:
50
+ EOS
51
+
52
+ parser.on('--priority', 'Filter and run priority tests') do |o|
53
+ options[:priority] = true
54
+ end
55
+
56
+ parser.on_tail("-h", "--help", "Show help") do
57
+ puts parser
58
+ exit
59
+ end
60
+
61
+ parser.on_tail('-v', '--version', 'Show version') do
62
+ puts PriorityTest::VERSION
63
+ exit
64
+ end
65
+ end
66
+ end
67
+
68
+ def parse_test_framework(args)
69
+ test_framework = args.shift
70
+ if AVAILABLE_TEST_FRAMEWORKS.include?(test_framework)
71
+ test_framework
72
+ else
73
+ puts "Invalid test framework: #{test_framework}"
74
+ puts "Run `pt -h` for more info"
75
+ raise ::OptionParser::InvalidArgument
76
+ end
77
+ end
78
+
79
+ def remove_parsed_options(args)
80
+ args.reject! { |arg| OPTIONS.include?(arg) }
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,62 @@
1
+ module PriorityTest
2
+ module Core
3
+ module Priority
4
+ PRIORITY_SET_RANKINGS = {
5
+ "FFFFF" => 1,
6
+ "FFFFP" => 2,
7
+ "FFFPF" => 3,
8
+ "FFFPP" => 4,
9
+
10
+ "FFPFF" => 5,
11
+ "FFPFP" => 6,
12
+ "FFPPF" => 7,
13
+ "FFPPP" => 8,
14
+
15
+ "FPFFF" => 9,
16
+ "FPFFP" => 10,
17
+ "FPFPF" => 11,
18
+ "FPFPP" => 12,
19
+
20
+ "FPPFF" => 13,
21
+ "FPPFP" => 14,
22
+ "FPPPF" => 15,
23
+ "FPPPP" => 16,
24
+
25
+ "PFFFF" => 17,
26
+ "PFFFP" => 18,
27
+ "PFFPF" => 19,
28
+ "PFFPP" => 20,
29
+
30
+ "PFPFF" => 21,
31
+ "PFPFP" => 22,
32
+ "PFPPF" => 23,
33
+ "PFPPP" => 24,
34
+ }
35
+
36
+ NON_PRIORITY_SET_RANKINGS = {
37
+ "PPFFF" => 25,
38
+ "PPFFP" => 26,
39
+ "PPFPF" => 27,
40
+ "PPFPP" => 28,
41
+
42
+ "PPPFF" => 29,
43
+ "PPPFP" => 30,
44
+ "PPPPF" => 31,
45
+ "PPPPP" => 32
46
+ }
47
+
48
+ # ranking for the last 5 test results
49
+ PRIORITY_RANKINGS = PRIORITY_SET_RANKINGS.merge(NON_PRIORITY_SET_RANKINGS)
50
+
51
+ PRIORITY_THRESHOLD = 24
52
+
53
+ def self.[](key)
54
+ PRIORITY_RANKINGS[key]
55
+ end
56
+
57
+ def self.in_priority_set?(priority)
58
+ priority <= PRIORITY_THRESHOLD
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,23 @@
1
+ module PriorityTest
2
+ module Core
3
+ class PrioritySortElement
4
+ include Comparable
5
+
6
+ attr_reader :test
7
+
8
+ def initialize(identifier)
9
+ @test = PriorityTest.all_tests.get_test(identifier)
10
+ end
11
+
12
+ def <=>(other)
13
+ if test.nil? && !other.test.nil?
14
+ 1
15
+ elsif !test.nil? && other.test.nil?
16
+ -1
17
+ else
18
+ test <=> other.test
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ module PriorityTest
2
+ module Core
3
+ module Runner
4
+ def self.autorun
5
+ return if installed_at_exit?
6
+ run(ARGV)
7
+ @installed_at_exit = true
8
+ end
9
+ AT_EXIT_HOOK_BACKTRACE_LINE = "#{__FILE__}:#{__LINE__ - 2}:in `autorun'"
10
+
11
+ def self.installed_at_exit?
12
+ @installed_at_exit ||= false
13
+ end
14
+
15
+ def self.run(args)
16
+ config_options = ConfigurationOptions.new(args)
17
+ config = PriorityTest.configuration
18
+ config_options.configure(config)
19
+
20
+ if config.test_framework == 'rspec'
21
+ require 'rspec/autorun'
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ module PriorityTest
2
+ module Core
3
+ class Service
4
+ def initialize(all_tests)
5
+ @all_tests = all_tests
6
+ end
7
+
8
+ def priority_test?(identifier)
9
+ test = @all_tests.get_test(identifier)
10
+ test.nil? ? true : test.priority?
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,53 @@
1
+ module PriorityTest
2
+ module Core
3
+ class Test < ::Sequel::Model
4
+ include PriorityTest::Core::ValidationsHelper
5
+ include Comparable
6
+
7
+ NUMBER_OF_RESULTS = 5
8
+
9
+ one_to_many :results, :class => PriorityTest::Core::TestResult.name, :class => PriorityTest::Core::TestResult.name do |ds|
10
+ ds.order(:started_at.desc).limit(NUMBER_OF_RESULTS) # make it configurable
11
+ end
12
+
13
+ def self.all_in_priority_order
14
+ eager(:results).order(:priority, :avg_run_time).all
15
+ end
16
+
17
+ def <=>(other)
18
+ result = (priority <=> other.priority)
19
+ result = (avg_run_time <=> other.avg_run_time) if result == 0 || !result
20
+ result
21
+ end
22
+
23
+ def validate
24
+ validates_presence [ :identifier, :file_path ]
25
+ end
26
+
27
+ def results_key
28
+ results_key = results.collect { |r| r.passed? ? 'P' : 'F' }.join
29
+ results_key << 'P' * (NUMBER_OF_RESULTS - results_key.size) if results_key.size < NUMBER_OF_RESULTS
30
+ results_key[0..(NUMBER_OF_RESULTS-1)]
31
+ end
32
+
33
+ def update_statistics
34
+ stats_to_update = {}
35
+ prio = Priority[results_key]
36
+ stats_to_update.merge!(:priority => prio) if prio
37
+ stats_to_update.merge!(:avg_run_time => calculate_avg_run_time) if results.size > 0
38
+
39
+ self.update(stats_to_update)
40
+ end
41
+
42
+ def priority?
43
+ Priority.in_priority_set?(priority)
44
+ end
45
+
46
+ private
47
+
48
+ def calculate_avg_run_time
49
+ results.collect(&:run_time).reduce(:+) / results.size
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,25 @@
1
+ module PriorityTest
2
+ module Core
3
+ class TestResult < ::Sequel::Model
4
+ include PriorityTest::Core::ValidationsHelper
5
+
6
+ PASSED_STATUS = 'passed'
7
+ FAILEDED_STATUS = 'failed'
8
+
9
+ many_to_one :context, :class => PriorityTest::Core::Test.name, :key => :test_id
10
+
11
+ def passed?
12
+ status == PASSED_STATUS
13
+ end
14
+
15
+ def failed?
16
+ not passed?
17
+ end
18
+
19
+ def validate
20
+ validates_presence [ :status, :started_at, :run_time ]
21
+ validates_includes [ PASSED_STATUS, FAILEDED_STATUS ], :status
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,40 @@
1
+ module PriorityTest
2
+ module Core
3
+ class TestResultCollector
4
+ attr_reader :all_tests
5
+
6
+ def initialize(all_tests)
7
+ @all_tests = all_tests
8
+ end
9
+
10
+ def add_result(test_result_hash)
11
+ identifier = test_result_hash[:identifier]
12
+ return unless identifier
13
+
14
+ @all_tests.get_test(identifier) || @all_tests.add_test(test_params(test_result_hash))
15
+ @all_tests.add_test_result(identifier, test_result_params(test_result_hash))
16
+ end
17
+
18
+ def finish
19
+ # nothing
20
+ end
21
+
22
+ private
23
+
24
+ def test_params(test_result_hash)
25
+ {
26
+ :identifier => test_result_hash[:identifier],
27
+ :file_path => test_result_hash[:file_path]
28
+ }
29
+ end
30
+
31
+ def test_result_params(test_result_hash)
32
+ {
33
+ :status => test_result_hash[:status],
34
+ :started_at => test_result_hash[:started_at],
35
+ :run_time => test_result_hash[:run_time]
36
+ }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,17 @@
1
+ module PriorityTest
2
+ module Core
3
+ module ValidationsHelper
4
+ def validates_presence(args)
5
+ args.each do |arg|
6
+ value = send(arg)
7
+ errors.add(arg, 'cannot be empty') if !value || value.to_s.empty?
8
+ end
9
+ end
10
+
11
+ def validates_includes(includes, arg)
12
+ value = send(arg)
13
+ errors.add(arg, "should be in list #{includes}") unless includes.include?(value)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,41 @@
1
+ require_path 'core/configuration'
2
+ require_path 'core/configuration_options'
3
+ require_path 'core/option_parser'
4
+ require_path 'core/runner'
5
+ require_path 'core/priority'
6
+ require_path 'core/validations_helper'
7
+ require_path 'core/all_tests'
8
+ require_path 'core/service'
9
+ require_path 'core/test_result_collector'
10
+ require_path 'core/priority_sort_element'
11
+
12
+ module PriorityTest
13
+ module Core
14
+ # Remove dependency of Sequel so that it can move out of autoload
15
+ autoload :Test, 'priority_test/core/test'
16
+ autoload :TestResult, 'priority_test/core/test_result'
17
+ end
18
+
19
+ def self.configure
20
+ yield configuration if block_given?
21
+ end
22
+
23
+ def self.configuration
24
+ @configuration ||= Core::Configuration.new
25
+ end
26
+
27
+ def self.all_tests
28
+ @all_tests ||= begin
29
+ tests = Core::Test.all_in_priority_order
30
+ Core::AllTests.new(tests)
31
+ end
32
+ end
33
+
34
+ def self.service
35
+ @service ||= Core::Service.new(all_tests)
36
+ end
37
+
38
+ def self.test_result_collector
39
+ @collector ||= Core::TestResultCollector.new(all_tests)
40
+ end
41
+ end
@@ -0,0 +1,11 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:tests) do
4
+ primary_key :id
5
+ String :identifier, :text => true, :null => false
6
+ String :file_path, :text => true, :null => false
7
+ Integer :priority, :default => 0, :null => false
8
+ Numeric :avg_run_time, :size => [10, 6], :default => 0, :null => false
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:test_results) do
4
+ primary_key :id
5
+ String :status, :null => false
6
+ DateTime :started_at, :null => false
7
+ Numeric :run_time, :size => [10, 6], :null => false
8
+ foreign_key :test_id, :tests
9
+ index :test_id
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,33 @@
1
+ require 'sequel'
2
+ require 'sequel/extensions/migration'
3
+
4
+ module PriorityTest
5
+ module Gateway
6
+ class Sequel
7
+ class << self
8
+ attr_reader :database
9
+ end
10
+
11
+ def self.setup
12
+ ::Sequel.database_timezone = :utc
13
+ @database ||= ::Sequel.connect(PriorityTest.configuration.database)
14
+ run_migration(database)
15
+ end
16
+
17
+ def self.teardown
18
+ @database.disconnect if @database
19
+ @database = nil
20
+ end
21
+
22
+ def self.run_migration(database)
23
+ ::Sequel::Migrator.apply(database, migrations_dir)
24
+ end
25
+
26
+ private
27
+
28
+ def self.migrations_dir
29
+ File.join(File.expand_path(File.dirname(__FILE__)), 'migrations')
30
+ end
31
+ end
32
+ end
33
+ end