priority_test 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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