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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.travis.yml +18 -0
- data/Gemfile +2 -0
- data/Gemfile-cucumber1-3 +4 -0
- data/Gemfile-cucumber1-3.lock +33 -0
- data/Gemfile-cucumber2-4 +4 -0
- data/Gemfile-cucumber2-4.lock +37 -0
- data/Gemfile-minitest4.lock +4 -23
- data/Gemfile-minitest5 +3 -0
- data/Gemfile-minitest5.lock +19 -0
- data/Gemfile-rspec2-1 +3 -0
- data/Gemfile-rspec2-1.lock +27 -0
- data/Gemfile-rspec3-0.lock +4 -13
- data/Gemfile-rspec3-1.lock +4 -13
- data/Gemfile-rspec3-2.lock +4 -13
- data/Gemfile-testunit.lock +4 -27
- data/Gemfile.lock +4 -1
- data/README.md +4 -0
- data/bin/minitest-queue +0 -1
- data/bin/testunit-queue +0 -1
- data/lib/test_queue/iterator.rb +41 -10
- data/lib/test_queue/runner.rb +167 -58
- data/lib/test_queue/runner/cucumber.rb +81 -12
- data/lib/test_queue/runner/minitest.rb +0 -4
- data/lib/test_queue/runner/minitest4.rb +25 -2
- data/lib/test_queue/runner/minitest5.rb +36 -11
- data/lib/test_queue/runner/rspec.rb +55 -7
- data/lib/test_queue/runner/rspec2.rb +11 -8
- data/lib/test_queue/runner/rspec3.rb +10 -7
- data/lib/test_queue/runner/sample.rb +0 -2
- data/lib/test_queue/runner/testunit.rb +25 -7
- data/lib/test_queue/stats.rb +95 -0
- data/lib/test_queue/test_framework.rb +29 -0
- data/script/bootstrap +12 -0
- data/script/cibuild +19 -0
- data/script/spec +7 -0
- data/spec/stats_spec.rb +76 -0
- data/test-queue.gemspec +1 -4
- data/test/cucumber.bats +57 -0
- data/test/minitest4.bats +34 -0
- data/test/minitest5.bats +111 -0
- data/test/rspec.bats +38 -0
- data/{features → test/samples/features}/bad.feature +0 -0
- data/{features → test/samples/features}/sample.feature +0 -0
- data/{features → test/samples/features}/sample2.feature +0 -0
- data/{features → test/samples/features}/step_definitions/common.rb +5 -1
- data/test/{sample_minispec.rb → samples/sample_minispec.rb} +6 -0
- data/test/{sample_minitest4.rb → samples/sample_minitest4.rb} +5 -3
- data/test/{sample_minitest5.rb → samples/sample_minitest5.rb} +5 -3
- data/test/{sample_spec.rb → samples/sample_spec.rb} +5 -3
- data/test/samples/sample_split_spec.rb +17 -0
- data/test/{sample_testunit.rb → samples/sample_testunit.rb} +5 -3
- data/test/testlib.bash +81 -0
- data/test/testunit.bats +20 -0
- metadata +40 -60
- data/test-multi.sh +0 -8
- 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
|
-
|
56
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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)
|
@@ -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
|
-
|
36
|
-
|
37
|
-
|
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
|