test-queue 0.2.13 → 0.3.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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.travis.yml +18 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile-cucumber1-3 +4 -0
  6. data/Gemfile-cucumber1-3.lock +33 -0
  7. data/Gemfile-cucumber2-4 +4 -0
  8. data/Gemfile-cucumber2-4.lock +37 -0
  9. data/Gemfile-minitest4.lock +4 -23
  10. data/Gemfile-minitest5 +3 -0
  11. data/Gemfile-minitest5.lock +19 -0
  12. data/Gemfile-rspec2-1 +3 -0
  13. data/Gemfile-rspec2-1.lock +27 -0
  14. data/Gemfile-rspec3-0.lock +4 -13
  15. data/Gemfile-rspec3-1.lock +4 -13
  16. data/Gemfile-rspec3-2.lock +4 -13
  17. data/Gemfile-testunit.lock +4 -27
  18. data/Gemfile.lock +4 -1
  19. data/README.md +4 -0
  20. data/bin/minitest-queue +0 -1
  21. data/bin/testunit-queue +0 -1
  22. data/lib/test_queue/iterator.rb +41 -10
  23. data/lib/test_queue/runner.rb +167 -58
  24. data/lib/test_queue/runner/cucumber.rb +81 -12
  25. data/lib/test_queue/runner/minitest.rb +0 -4
  26. data/lib/test_queue/runner/minitest4.rb +25 -2
  27. data/lib/test_queue/runner/minitest5.rb +36 -11
  28. data/lib/test_queue/runner/rspec.rb +55 -7
  29. data/lib/test_queue/runner/rspec2.rb +11 -8
  30. data/lib/test_queue/runner/rspec3.rb +10 -7
  31. data/lib/test_queue/runner/sample.rb +0 -2
  32. data/lib/test_queue/runner/testunit.rb +25 -7
  33. data/lib/test_queue/stats.rb +95 -0
  34. data/lib/test_queue/test_framework.rb +29 -0
  35. data/script/bootstrap +12 -0
  36. data/script/cibuild +19 -0
  37. data/script/spec +7 -0
  38. data/spec/stats_spec.rb +76 -0
  39. data/test-queue.gemspec +1 -4
  40. data/test/cucumber.bats +57 -0
  41. data/test/minitest4.bats +34 -0
  42. data/test/minitest5.bats +111 -0
  43. data/test/rspec.bats +38 -0
  44. data/{features → test/samples/features}/bad.feature +0 -0
  45. data/{features → test/samples/features}/sample.feature +0 -0
  46. data/{features → test/samples/features}/sample2.feature +0 -0
  47. data/{features → test/samples/features}/step_definitions/common.rb +5 -1
  48. data/test/{sample_minispec.rb → samples/sample_minispec.rb} +6 -0
  49. data/test/{sample_minitest4.rb → samples/sample_minitest4.rb} +5 -3
  50. data/test/{sample_minitest5.rb → samples/sample_minitest5.rb} +5 -3
  51. data/test/{sample_spec.rb → samples/sample_spec.rb} +5 -3
  52. data/test/samples/sample_split_spec.rb +17 -0
  53. data/test/{sample_testunit.rb → samples/sample_testunit.rb} +5 -3
  54. data/test/testlib.bash +81 -0
  55. data/test/testunit.bats +20 -0
  56. metadata +40 -60
  57. data/test-multi.sh +0 -8
  58. data/test.sh +0 -23
@@ -10,10 +10,6 @@ module TestQueue
10
10
  class Runner
11
11
  class MiniTest < Runner
12
12
  def summarize_worker(worker)
13
- worker.stats.each do |s, val|
14
- stats[s.to_s] = val
15
- end
16
-
17
13
  worker.summary = worker.lines.grep(/, \d+ errors?, /).first
18
14
  failures = worker.lines.select{ |line|
19
15
  line if (line =~ /^Finished/) ... (line =~ /, \d+ errors?, /)
@@ -1,4 +1,5 @@
1
1
  require 'test_queue/runner'
2
+ require 'set'
2
3
  require 'stringio'
3
4
 
4
5
  class MiniTestQueueRunner < MiniTest::Unit
@@ -46,14 +47,20 @@ class MiniTest::Unit::TestCase
46
47
  @@test_suites.keys.reject{ |s| s.test_methods.empty? }
47
48
  end
48
49
  end
50
+
51
+ def failure_count
52
+ failures.length
53
+ end
49
54
  end
50
55
 
51
56
  module TestQueue
52
57
  class Runner
53
58
  class MiniTest < Runner
54
59
  def initialize
55
- tests = ::MiniTest::Unit::TestCase.original_test_suites.sort_by{ |s| -(stats[s.to_s] || 0) }
56
- super(tests)
60
+ if ::MiniTest::Unit::TestCase.original_test_suites.any?
61
+ fail "Do not `require` test files. Pass them via ARGV instead and they will be required as needed."
62
+ end
63
+ super(TestFramework::MiniTest.new)
57
64
  end
58
65
 
59
66
  def run_worker(iterator)
@@ -62,4 +69,20 @@ module TestQueue
62
69
  end
63
70
  end
64
71
  end
72
+
73
+ class TestFramework
74
+ class MiniTest < TestFramework
75
+ def all_suite_files
76
+ ARGV
77
+ end
78
+
79
+ def suites_from_file(path)
80
+ ::MiniTest::Unit::TestCase.reset
81
+ require File.absolute_path(path)
82
+ ::MiniTest::Unit::TestCase.original_test_suites.map { |suite|
83
+ [suite.name, suite]
84
+ }
85
+ end
86
+ end
87
+ end
65
88
  end
@@ -3,16 +3,29 @@ require 'test_queue/runner'
3
3
  module MiniTest
4
4
  def self.__run reporter, options
5
5
  suites = Runnable.runnables
6
-
7
- # Run the serial tests first after they complete, run the parallels tests
8
- # We already sort suites based on its test_order at TestQueue::Runner::Minitest#initialize.
9
6
  suites.map { |suite| suite.run reporter, options }
10
7
  end
11
8
 
9
+ class Runnable
10
+ def failure_count
11
+ failures.length
12
+ end
13
+ end
14
+
12
15
  class Test
13
16
  def self.runnables= runnables
14
17
  @@runnables = runnables
15
18
  end
19
+
20
+ # Synchronize all tests, even serial ones.
21
+ #
22
+ # Minitest runs serial tests before parallel ones to ensure the
23
+ # unsynchronized serial tests don't overlap the parallel tests. But since
24
+ # the test-queue master hands out tests without actually loading their
25
+ # code, there's no way to know which are parallel and which are serial.
26
+ # Synchronizing serial tests does add some overhead, but hopefully this is
27
+ # outweighed by the speed benefits of using test-queue.
28
+ def _synchronize; Test.io_lock.synchronize { yield }; end
16
29
  end
17
30
 
18
31
  class ProgressReporter
@@ -43,14 +56,10 @@ module TestQueue
43
56
  class Runner
44
57
  class MiniTest < Runner
45
58
  def initialize
46
- tests = ::MiniTest::Test.runnables.reject { |s|
47
- s.runnable_methods.empty?
48
- }.sort_by { |s|
49
- -(stats[s.to_s] || 0)
50
- }.partition { |s|
51
- s.test_order == :parallel
52
- }.reverse.flatten
53
- super(tests)
59
+ if ::MiniTest::Test.runnables.any? { |r| r.runnable_methods.any? }
60
+ fail "Do not `require` test files. Pass them via ARGV instead and they will be required as needed."
61
+ end
62
+ super(TestFramework::MiniTest.new)
54
63
  end
55
64
 
56
65
  def run_worker(iterator)
@@ -59,4 +68,20 @@ module TestQueue
59
68
  end
60
69
  end
61
70
  end
71
+
72
+ class TestFramework
73
+ class MiniTest < TestFramework
74
+ def all_suite_files
75
+ ARGV
76
+ end
77
+
78
+ def suites_from_file(path)
79
+ ::MiniTest::Test.reset
80
+ require File.absolute_path(path)
81
+ ::MiniTest::Test.runnables
82
+ .reject { |s| s.runnable_methods.empty? }
83
+ .map { |s| [s.name, s] }
84
+ end
85
+ end
86
+ end
62
87
  end
@@ -10,26 +10,74 @@ else
10
10
  fail 'requires rspec version 2 or 3'
11
11
  end
12
12
 
13
+ class ::RSpec::Core::ExampleGroup
14
+ def self.failure_count
15
+ examples.map {|e| e.execution_result[:status] == "failed"}.length
16
+ end
17
+ end
18
+
13
19
  module TestQueue
14
20
  class Runner
15
21
  class RSpec < Runner
16
22
  def initialize
17
- @rspec = ::RSpec::Core::QueueRunner.new
18
- super(@rspec.example_groups.sort_by{ |s| -(stats[s.to_s] || 0) })
23
+ super(TestFramework::RSpec.new)
19
24
  end
20
25
 
21
26
  def run_worker(iterator)
22
- @rspec.run_each(iterator).to_i
27
+ rspec = ::RSpec::Core::QueueRunner.new
28
+ rspec.run_each(iterator).to_i
23
29
  end
24
30
 
25
31
  def summarize_worker(worker)
26
- worker.stats.each do |s, val|
27
- stats[s] = val
28
- end
29
-
30
32
  worker.summary = worker.lines.grep(/ examples?, /).first
31
33
  worker.failure_output = worker.output[/^Failures:\n\n(.*)\n^Finished/m, 1]
32
34
  end
33
35
  end
34
36
  end
37
+
38
+ class TestFramework
39
+ class RSpec < TestFramework
40
+ def all_suite_files
41
+ options = ::RSpec::Core::ConfigurationOptions.new(ARGV)
42
+ options.parse_options if options.respond_to?(:parse_options)
43
+ options.configure(::RSpec.configuration)
44
+
45
+ ::RSpec.configuration.files_to_run.uniq
46
+ end
47
+
48
+ def suites_from_file(path)
49
+ ::RSpec.world.reset
50
+ load path
51
+ split_groups(::RSpec.world.example_groups).map { |example_or_group|
52
+ name = if example_or_group.respond_to?(:id)
53
+ example_or_group.id
54
+ elsif example_or_group.respond_to?(:full_description)
55
+ example_or_group.full_description
56
+ else
57
+ example_or_group.metadata[:example_group][:full_description]
58
+ end
59
+ [name, example_or_group]
60
+ }
61
+ end
62
+
63
+ private
64
+
65
+ def split_groups(groups)
66
+ return groups unless split_groups?
67
+
68
+ groups_to_split, groups_to_keep = [], []
69
+ groups.each do |group|
70
+ (group.metadata[:no_split] ? groups_to_keep : groups_to_split) << group
71
+ end
72
+ queue = groups_to_split.flat_map(&:descendant_filtered_examples)
73
+ queue.concat groups_to_keep
74
+ queue
75
+ end
76
+
77
+ def split_groups?
78
+ return @split_groups if defined?(@split_groups)
79
+ @split_groups = ENV['TEST_QUEUE_SPLIT_GROUPS'] && ENV['TEST_QUEUE_SPLIT_GROUPS'].strip.downcase == 'true'
80
+ end
81
+ end
82
+ end
35
83
  end
@@ -6,19 +6,22 @@ module RSpec::Core
6
6
  @configuration.error_stream = $stderr
7
7
  end
8
8
 
9
- def example_groups
10
- @options.configure(@configuration)
11
- @configuration.load_spec_files
12
- @world.announce_filters
13
- @world.example_groups
14
- end
15
-
16
9
  def run_each(iterator)
17
10
  @configuration.reporter.report(0, @configuration.randomize? ? @configuration.seed : nil) do |reporter|
18
11
  begin
19
12
  @configuration.run_hook(:before, :suite)
20
13
  iterator.map {|g|
21
- print " #{g.description}: "
14
+ if g.is_a? ::RSpec::Core::Example
15
+ print " #{g.full_description}: "
16
+ example = g
17
+ g = example.example_group
18
+ ::RSpec.world.filtered_examples.clear
19
+ examples = [example]
20
+ examples.extend(::RSpec::Core::Extensions::Ordered::Examples)
21
+ ::RSpec.world.filtered_examples[g] = examples
22
+ else
23
+ print " #{g.description}: "
24
+ end
22
25
  start = Time.now
23
26
  ret = g.run(reporter)
24
27
  diff = Time.now-start
@@ -20,17 +20,20 @@ module RSpec::Core
20
20
  super(options)
21
21
  end
22
22
 
23
- def example_groups
24
- setup($stderr, $stdout)
25
- @world.ordered_example_groups
26
- end
27
-
28
23
  def run_specs(iterator)
29
- @configuration.reporter.report(@world.ordered_example_groups.count) do |reporter|
24
+ @configuration.reporter.report(0) do |reporter|
30
25
  @configuration.with_suite_hooks do
31
26
  iterator.map { |g|
32
- print " #{g.description}: "
33
27
  start = Time.now
28
+ if g.is_a? ::RSpec::Core::Example
29
+ print " #{g.full_description}: "
30
+ example = g
31
+ g = example.example_group
32
+ ::RSpec.world.filtered_examples.clear
33
+ ::RSpec.world.filtered_examples[g] = [example]
34
+ else
35
+ print " #{g.description}: "
36
+ end
34
37
  ret = g.run(reporter)
35
38
  diff = Time.now-start
36
39
  puts(" <%.3f>" % diff)
@@ -24,8 +24,6 @@ module TestQueue
24
24
  end
25
25
 
26
26
  def summarize_worker(worker)
27
- stats.update(worker.stats)
28
-
29
27
  worker.summary = worker.output.scan(/^\s*(\d+)/).join(', ')
30
28
  worker.failure_output = ''
31
29
  end
@@ -26,31 +26,49 @@ class Test::Unit::TestSuite
26
26
  yield(FINISHED, name)
27
27
  yield(FINISHED_OBJECT, self)
28
28
  end
29
+
30
+ def failure_count
31
+ (@iterator || @tests).map {|t| t.instance_variable_get(:@_result).failure_count}.inject(0, :+)
32
+ end
29
33
  end
30
34
 
31
35
  module TestQueue
32
36
  class Runner
33
37
  class TestUnit < Runner
34
38
  def initialize
35
- @suite = Test::Unit::Collector::Descendant.new.collect
36
- tests = @suite.tests.sort_by{ |s| -(stats[s.to_s] || 0) }
37
- super(tests)
39
+ if Test::Unit::Collector::Descendant.new.collect.tests.any?
40
+ fail "Do not `require` test files. Pass them via ARGV instead and they will be required as needed."
41
+ end
42
+ super(TestFramework::TestUnit.new)
38
43
  end
39
44
 
40
45
  def run_worker(iterator)
46
+ @suite = Test::Unit::TestSuite.new("specified by test-queue master")
41
47
  @suite.iterator = iterator
42
48
  res = Test::Unit::UI::Console::TestRunner.new(@suite).start
43
49
  res.run_count - res.pass_count
44
50
  end
45
51
 
46
52
  def summarize_worker(worker)
47
- worker.stats.each do |s, val|
48
- stats[s.to_s] = val
49
- end
50
-
51
53
  worker.summary = worker.output.split("\n").grep(/^\d+ tests?/).first
52
54
  worker.failure_output = worker.output.scan(/^Failure:\n(.*)\n=======================*/m).join("\n")
53
55
  end
54
56
  end
55
57
  end
58
+
59
+ class TestFramework
60
+ class TestUnit < TestFramework
61
+ def all_suite_files
62
+ ARGV
63
+ end
64
+
65
+ def suites_from_file(path)
66
+ Test::Unit::TestCase::DESCENDANTS.clear
67
+ require File.absolute_path(path)
68
+ Test::Unit::Collector::Descendant.new.collect.tests.map { |suite|
69
+ [suite.name, suite]
70
+ }
71
+ end
72
+ end
73
+ end
56
74
  end
@@ -0,0 +1,95 @@
1
+ module TestQueue
2
+ class Stats
3
+ class Suite
4
+ attr_reader :name, :path, :duration, :last_seen_at
5
+
6
+ def initialize(name, path, duration, last_seen_at)
7
+ @name = name
8
+ @path = path
9
+ @duration = duration
10
+ @last_seen_at = last_seen_at
11
+
12
+ freeze
13
+ end
14
+
15
+ def ==(other)
16
+ other &&
17
+ name == other.name &&
18
+ path == other.path &&
19
+ duration == other.duration &&
20
+ last_seen_at == other.last_seen_at
21
+ end
22
+ alias_method :eql?, :==
23
+
24
+ def to_h
25
+ { :name => name, :path => path, :duration => duration, :last_seen_at => last_seen_at.to_i }
26
+ end
27
+
28
+ def self.from_hash(hash)
29
+ self.new(hash.fetch(:name),
30
+ hash.fetch(:path),
31
+ hash.fetch(:duration),
32
+ Time.at(hash.fetch(:last_seen_at)))
33
+ end
34
+ end
35
+
36
+ def initialize(path)
37
+ @path = path
38
+ @suites = {}
39
+ load
40
+ end
41
+
42
+ def all_suites
43
+ @suites.values
44
+ end
45
+
46
+ def suite(name)
47
+ @suites[name]
48
+ end
49
+
50
+ def record_suites(suites)
51
+ suites.each do |suite|
52
+ @suites[suite.name] = suite
53
+ end
54
+ end
55
+
56
+ def save
57
+ prune
58
+
59
+ File.open(@path, "wb") do |f|
60
+ Marshal.dump(to_h, f)
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ CURRENT_VERSION = 2
67
+
68
+ def to_h
69
+ suites = @suites.each_value.map(&:to_h)
70
+
71
+ { :version => CURRENT_VERSION, :suites => suites }
72
+ end
73
+
74
+ def load
75
+ data = begin
76
+ File.open(@path, "rb") { |f| Marshal.load(f) }
77
+ rescue Errno::ENOENT, EOFError, TypeError
78
+ end
79
+ return unless data && data.is_a?(Hash) && data[:version] == CURRENT_VERSION
80
+ data[:suites].each do |suite_hash|
81
+ suite = Suite.from_hash(suite_hash)
82
+ @suites[suite.name] = suite
83
+ end
84
+ end
85
+
86
+ EIGHT_DAYS_S = 8 * 24 * 60 * 60
87
+
88
+ def prune
89
+ earliest = Time.now - EIGHT_DAYS_S
90
+ @suites.delete_if do |name, suite|
91
+ suite.last_seen_at < earliest
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,29 @@
1
+ module TestQueue
2
+ # This class provides an abstraction over the various test frameworks we
3
+ # support. The framework-specific subclasses are defined in the various
4
+ # test_queue/runner/* files.
5
+ class TestFramework
6
+ # Return all file paths to load test suites from.
7
+ #
8
+ # An example implementation might just return files passed on the command
9
+ # line, or defer to the underlying test framework to determine which files
10
+ # to load.
11
+ #
12
+ # Returns an Enumerable of String file paths.
13
+ def all_suite_files
14
+ raise NotImplementedError
15
+ end
16
+
17
+ # Load all suites from the specified file path.
18
+ #
19
+ # path - String file path to load suites from
20
+ #
21
+ # Returns an Enumerable of tuples containing:
22
+ # suite_name - String that uniquely identifies this suite
23
+ # suite - Framework-specific object that can be used to actually
24
+ # run the suite
25
+ def suites_from_file(path)
26
+ raise NotImplementedError
27
+ end
28
+ end
29
+ end