megatest 0.1.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/CHANGELOG.md +5 -0
- data/README.md +156 -0
- data/TODO.md +17 -0
- data/exe/megatest +7 -0
- data/lib/megatest/assertions.rb +474 -0
- data/lib/megatest/backtrace.rb +70 -0
- data/lib/megatest/cli.rb +249 -0
- data/lib/megatest/compat.rb +74 -0
- data/lib/megatest/config.rb +281 -0
- data/lib/megatest/differ.rb +136 -0
- data/lib/megatest/dsl.rb +164 -0
- data/lib/megatest/executor.rb +104 -0
- data/lib/megatest/multi_process.rb +263 -0
- data/lib/megatest/output.rb +158 -0
- data/lib/megatest/patience_diff.rb +340 -0
- data/lib/megatest/pretty_print.rb +309 -0
- data/lib/megatest/queue.rb +239 -0
- data/lib/megatest/queue_monitor.rb +35 -0
- data/lib/megatest/queue_reporter.rb +42 -0
- data/lib/megatest/redis_queue.rb +459 -0
- data/lib/megatest/reporters.rb +266 -0
- data/lib/megatest/runner.rb +119 -0
- data/lib/megatest/runtime.rb +168 -0
- data/lib/megatest/selector.rb +293 -0
- data/lib/megatest/state.rb +708 -0
- data/lib/megatest/subprocess/main.rb +8 -0
- data/lib/megatest/subprocess.rb +48 -0
- data/lib/megatest/test.rb +115 -0
- data/lib/megatest/test_task.rb +132 -0
- data/lib/megatest/version.rb +5 -0
- data/lib/megatest.rb +123 -0
- metadata +80 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :stopdoc:
|
|
4
|
+
|
|
5
|
+
module Megatest
|
|
6
|
+
class Runner
|
|
7
|
+
def initialize(config)
|
|
8
|
+
@config = config
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def execute(test_case)
|
|
12
|
+
if test_case.tag(:isolated)
|
|
13
|
+
isolate(test_case) do
|
|
14
|
+
run(test_case)
|
|
15
|
+
end
|
|
16
|
+
else
|
|
17
|
+
run(test_case)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if Megatest.fork?
|
|
22
|
+
def isolate(test_case)
|
|
23
|
+
read, write = IO.pipe.each(&:binmode)
|
|
24
|
+
pid = Process.fork do
|
|
25
|
+
read.close
|
|
26
|
+
result = yield
|
|
27
|
+
Marshal.dump(result, write)
|
|
28
|
+
write.close
|
|
29
|
+
# We don't want to run at_exit hooks the app may have
|
|
30
|
+
# installed.
|
|
31
|
+
Process.exit!(0)
|
|
32
|
+
end
|
|
33
|
+
write.close
|
|
34
|
+
result = begin
|
|
35
|
+
Marshal.load(read)
|
|
36
|
+
rescue EOFError
|
|
37
|
+
TestCaseResult.new(test_case).lost
|
|
38
|
+
end
|
|
39
|
+
Process.wait(pid)
|
|
40
|
+
result
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
def isolate(test_case)
|
|
44
|
+
parent_read, child_write = IO.pipe.each(&:binmode)
|
|
45
|
+
child_read, parent_write = IO.pipe.each(&:binmode)
|
|
46
|
+
pid = Subprocess.spawn(child_read, child_write, "run_test")
|
|
47
|
+
child_read.close
|
|
48
|
+
child_write.close
|
|
49
|
+
|
|
50
|
+
Marshal.dump(@config, parent_write)
|
|
51
|
+
Marshal.dump(test_case.source_file, parent_write)
|
|
52
|
+
Marshal.dump(test_case.id, parent_write)
|
|
53
|
+
parent_write.close
|
|
54
|
+
|
|
55
|
+
result = begin
|
|
56
|
+
Marshal.load(parent_read)
|
|
57
|
+
rescue EOFError
|
|
58
|
+
TestCaseResult.new(test_case).lost
|
|
59
|
+
end
|
|
60
|
+
Process.wait(pid)
|
|
61
|
+
result
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def run(test_case)
|
|
66
|
+
result = TestCaseResult.new(test_case)
|
|
67
|
+
runtime = Runtime.new(@config, test_case, result)
|
|
68
|
+
instance = test_case.klass.new(runtime)
|
|
69
|
+
|
|
70
|
+
# We always reset the seed before running any test as to have the most consistent
|
|
71
|
+
# result as possible, especially on retries.
|
|
72
|
+
Random.srand(@config.seed)
|
|
73
|
+
|
|
74
|
+
result.record_time do
|
|
75
|
+
ran = false
|
|
76
|
+
failed = false
|
|
77
|
+
recursive_callbacks(test_case.around_callbacks) do
|
|
78
|
+
ran = true
|
|
79
|
+
return result if runtime.record_failures { instance.before_setup }
|
|
80
|
+
|
|
81
|
+
test_case.each_setup_callback do |callback|
|
|
82
|
+
failed ||= runtime.record_failures(downlevel: 2) { instance.instance_exec(&callback) }
|
|
83
|
+
end
|
|
84
|
+
failed ||= runtime.record_failures { instance.setup }
|
|
85
|
+
failed ||= runtime.record_failures { instance.after_setup }
|
|
86
|
+
|
|
87
|
+
failed ||= test_case.execute(runtime, instance)
|
|
88
|
+
|
|
89
|
+
result.ensure_assertions unless @config.minitest_compatibility
|
|
90
|
+
ensure
|
|
91
|
+
runtime.record_failures do
|
|
92
|
+
instance.before_teardown
|
|
93
|
+
end
|
|
94
|
+
test_case.each_teardown_callback do |callback|
|
|
95
|
+
runtime.record_failures(downlevel: 2) do
|
|
96
|
+
instance.instance_exec(&callback)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
runtime.record_failures do
|
|
100
|
+
instance.teardown
|
|
101
|
+
end
|
|
102
|
+
runtime.record_failures do
|
|
103
|
+
instance.after_teardown
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
result.did_not_run("Test wasn't run. Did an around block failed to yield?") unless ran
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def recursive_callbacks(callbacks, &block)
|
|
112
|
+
if callback = callbacks.pop
|
|
113
|
+
callback.call(-> { recursive_callbacks(callbacks, &block) })
|
|
114
|
+
else
|
|
115
|
+
yield
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :stopdoc:
|
|
4
|
+
|
|
5
|
+
module Megatest
|
|
6
|
+
class Runtime
|
|
7
|
+
attr_reader :test_case, :result
|
|
8
|
+
|
|
9
|
+
def initialize(config, test_case, result)
|
|
10
|
+
@config = config
|
|
11
|
+
@test_case = test_case
|
|
12
|
+
@result = result
|
|
13
|
+
@asserting = false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
support_locations = begin
|
|
17
|
+
error = StandardError.new
|
|
18
|
+
# Ruby 3.4: https://github.com/ruby/ruby/pull/10017
|
|
19
|
+
error.set_backtrace(caller_locations(1, 1))
|
|
20
|
+
true
|
|
21
|
+
rescue TypeError
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
if support_locations
|
|
26
|
+
def assert(uplevel: 1)
|
|
27
|
+
if @asserting
|
|
28
|
+
yield
|
|
29
|
+
else
|
|
30
|
+
@asserting = true
|
|
31
|
+
@result.assertions_count += 1
|
|
32
|
+
begin
|
|
33
|
+
yield
|
|
34
|
+
rescue Assertion => failure
|
|
35
|
+
if failure.backtrace.empty?
|
|
36
|
+
failure.set_backtrace(caller_locations(uplevel + 2))
|
|
37
|
+
end
|
|
38
|
+
raise
|
|
39
|
+
ensure
|
|
40
|
+
@asserting = false
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def strip_backtrace(error, yield_file, yield_line, downlevel)
|
|
46
|
+
if backtrace = error.backtrace_locations
|
|
47
|
+
rindex = backtrace.rindex { |l| l.lineno == yield_line && l.path == yield_file }
|
|
48
|
+
backtrace = backtrace.slice(0..rindex)
|
|
49
|
+
backtrace.pop(downlevel) unless downlevel.zero?
|
|
50
|
+
error.set_backtrace(backtrace)
|
|
51
|
+
elsif backtrace = error.backtrace
|
|
52
|
+
yield_point = "#{yield_file}:#{yield_line}:"
|
|
53
|
+
rindex = backtrace.rindex { |l| l.start_with?(yield_point) }
|
|
54
|
+
backtrace = backtrace.slice(0..rindex)
|
|
55
|
+
backtrace.pop(downlevel) unless downlevel.zero?
|
|
56
|
+
error.set_backtrace(backtrace)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
error
|
|
60
|
+
end
|
|
61
|
+
else
|
|
62
|
+
def assert(uplevel: 1)
|
|
63
|
+
if @asserting
|
|
64
|
+
yield
|
|
65
|
+
else
|
|
66
|
+
@asserting = true
|
|
67
|
+
@result.assertions_count += 1
|
|
68
|
+
begin
|
|
69
|
+
yield
|
|
70
|
+
rescue Assertion => failure
|
|
71
|
+
if failure.backtrace.empty?
|
|
72
|
+
failure.set_backtrace(caller(uplevel + 2))
|
|
73
|
+
end
|
|
74
|
+
raise
|
|
75
|
+
ensure
|
|
76
|
+
@asserting = false
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def strip_backtrace(error, yield_file, yield_line, downlevel)
|
|
82
|
+
if backtrace = error.backtrace
|
|
83
|
+
yield_point = "#{yield_file}:#{yield_line}:"
|
|
84
|
+
rindex = backtrace.rindex { |l| l.start_with?(yield_point) }
|
|
85
|
+
backtrace = backtrace.slice(0..rindex)
|
|
86
|
+
backtrace.pop(downlevel) unless downlevel.zero?
|
|
87
|
+
error.set_backtrace(backtrace)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
error
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def msg(positional, keyword)
|
|
95
|
+
if positional.nil?
|
|
96
|
+
keyword
|
|
97
|
+
elsif !keyword.nil?
|
|
98
|
+
raise ArgumentError, "Can't pass both a positional and keyword assertion message"
|
|
99
|
+
else
|
|
100
|
+
positional # TODO: deprecation mecanism
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def expect_no_failures
|
|
105
|
+
was_asserting = @asserting
|
|
106
|
+
@asserting = false
|
|
107
|
+
yield
|
|
108
|
+
rescue Assertion, *Megatest::IGNORED_ERRORS
|
|
109
|
+
raise # Exceptions we shouldn't rescue
|
|
110
|
+
rescue Exception => unexpected_error
|
|
111
|
+
raise UnexpectedError, unexpected_error, EMPTY_BACKTRACE
|
|
112
|
+
ensure
|
|
113
|
+
@asserting = was_asserting
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
EMPTY_BACKTRACE = [].freeze
|
|
117
|
+
|
|
118
|
+
def fail(user_message, *message)
|
|
119
|
+
message = build_message(message)
|
|
120
|
+
if user_message
|
|
121
|
+
user_message = user_message.call if user_message.respond_to?(:call)
|
|
122
|
+
user_message = String(user_message)
|
|
123
|
+
if message && !user_message.end_with?("\n")
|
|
124
|
+
user_message += "\n"
|
|
125
|
+
end
|
|
126
|
+
message = "#{user_message}#{message}"
|
|
127
|
+
end
|
|
128
|
+
raise(Assertion, message, EMPTY_BACKTRACE)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def build_message(strings)
|
|
132
|
+
return if strings.empty?
|
|
133
|
+
|
|
134
|
+
if (strings.size + strings.sum(&:size)) < 80
|
|
135
|
+
strings.join(" ")
|
|
136
|
+
else
|
|
137
|
+
strings.join("\n\n")
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def minitest_compatibility?
|
|
142
|
+
@config.minitest_compatibility
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def pp(object)
|
|
146
|
+
@config.pretty_print(object)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def diff(expected, actual)
|
|
150
|
+
@config.diff(expected, actual)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def record_failures(downlevel: 1, &block)
|
|
154
|
+
expect_no_failures(&block)
|
|
155
|
+
rescue Assertion => assertion
|
|
156
|
+
error = assertion
|
|
157
|
+
while error
|
|
158
|
+
error = strip_backtrace(error, __FILE__, __LINE__ - 4, downlevel + 2)
|
|
159
|
+
error = error.cause
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
@result.failures << Failure.new(assertion)
|
|
163
|
+
true
|
|
164
|
+
else
|
|
165
|
+
false
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# :stopdoc:
|
|
4
|
+
|
|
5
|
+
module Megatest
|
|
6
|
+
module Selector
|
|
7
|
+
class List
|
|
8
|
+
def initialize(loaders, filters)
|
|
9
|
+
@loaders = loaders
|
|
10
|
+
if loaders.empty?
|
|
11
|
+
@loaders = [Loader.new("test")]
|
|
12
|
+
end
|
|
13
|
+
@filters = filters
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def main_paths
|
|
17
|
+
paths = @loaders.map(&:path)
|
|
18
|
+
paths.compact!
|
|
19
|
+
paths.uniq!
|
|
20
|
+
paths
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def paths(random:)
|
|
24
|
+
paths = @loaders.reduce([]) do |paths_to_load, loader|
|
|
25
|
+
loader.append_paths(paths_to_load)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
paths.uniq!
|
|
29
|
+
paths.sort!
|
|
30
|
+
paths.shuffle!(random: random) if random
|
|
31
|
+
paths
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def select(registry, random:)
|
|
35
|
+
# If any of the selector points to an exact test or a subset of a suite,
|
|
36
|
+
# then each selector is responsible for shuffling the group of tests it selects,
|
|
37
|
+
# so that tests are shuffled inside groups, but groups are ordered.
|
|
38
|
+
test_cases = if @loaders.any?(&:partial?)
|
|
39
|
+
@loaders.reduce([]) do |tests_to_run, loader|
|
|
40
|
+
loader.append_tests(tests_to_run, registry, random: random)
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
# Otherwise, we do one big shuffle at the end, all groups are mixed.
|
|
44
|
+
test_cases = registry.test_cases
|
|
45
|
+
test_cases.sort!
|
|
46
|
+
test_cases.shuffle!(random: random) if random
|
|
47
|
+
test_cases
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@filters.reduce(test_cases) do |cases, filter|
|
|
51
|
+
filter.select(cases)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class Loader
|
|
57
|
+
attr_reader :path
|
|
58
|
+
|
|
59
|
+
def initialize(path, filter = nil)
|
|
60
|
+
@path = File.expand_path(path)
|
|
61
|
+
if @directory = File.directory?(@path)
|
|
62
|
+
@path = File.join(@path, "/")
|
|
63
|
+
@paths = Megatest.glob(@path)
|
|
64
|
+
else
|
|
65
|
+
@paths = [@path]
|
|
66
|
+
end
|
|
67
|
+
@filter = filter
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def partial?
|
|
71
|
+
!!@filter
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def append_tests(tests_to_run, registry, random:)
|
|
75
|
+
test_cases = select(registry)
|
|
76
|
+
if partial?
|
|
77
|
+
test_cases.sort!
|
|
78
|
+
test_cases.shuffle!(random: random) if random
|
|
79
|
+
end
|
|
80
|
+
tests_to_run.concat(test_cases)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def append_paths(paths_to_load)
|
|
84
|
+
paths_to_load.concat(@paths)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def select(registry)
|
|
88
|
+
test_cases = if @directory
|
|
89
|
+
registry.test_cases.select do |test_case|
|
|
90
|
+
test_case.source_file.start_with?(@path)
|
|
91
|
+
end
|
|
92
|
+
else
|
|
93
|
+
registry.test_cases_by_path(@path)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if @filter
|
|
97
|
+
@filter.select(test_cases)
|
|
98
|
+
else
|
|
99
|
+
test_cases
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class NegativeLoader
|
|
105
|
+
def initialize(loader)
|
|
106
|
+
@loader = loader
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def partial?
|
|
110
|
+
@loader.partial?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def append_paths(paths_to_load)
|
|
114
|
+
if @loader.partial?
|
|
115
|
+
paths_to_load
|
|
116
|
+
else
|
|
117
|
+
paths_to_not_load = @loader.append_paths([])
|
|
118
|
+
paths_to_load - paths_to_not_load
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def append_tests(tests_to_run, registry, random:)
|
|
123
|
+
tests_to_not_run = @loader.append_tests([], registry, random: nil)
|
|
124
|
+
tests_to_run - tests_to_not_run
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
class TagFilter
|
|
129
|
+
class << self
|
|
130
|
+
def parse(arg)
|
|
131
|
+
if match = arg.match(/\A@([\w-]+)(?:=(.*))?\z/)
|
|
132
|
+
new(match[1], match[2])
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def initialize(tag, value)
|
|
138
|
+
@tag = tag.to_sym
|
|
139
|
+
@value = value
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def select(test_cases)
|
|
143
|
+
if @value
|
|
144
|
+
test_cases.select do |test_case|
|
|
145
|
+
test_case.tag(@tag).to_s == @value
|
|
146
|
+
end
|
|
147
|
+
else
|
|
148
|
+
test_cases.select do |test_case|
|
|
149
|
+
test_case.tag(@tag)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
class ExactLineFilter
|
|
156
|
+
class << self
|
|
157
|
+
def parse(arg)
|
|
158
|
+
if match = arg.match(/\A(\d+)(?:~(\d+))?\z/)
|
|
159
|
+
new(Integer(match[1]), match[2]&.to_i)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def initialize(line, index)
|
|
165
|
+
@line = line
|
|
166
|
+
@index = index
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def select(test_cases)
|
|
170
|
+
test_cases = test_cases.sort { |a, b| b.source_line <=> a.source_line }
|
|
171
|
+
test_cases = test_cases.drop_while { |t| t.source_line > @line }
|
|
172
|
+
|
|
173
|
+
# Line not found, fallback to run the whole file?
|
|
174
|
+
return [] if test_cases.empty?
|
|
175
|
+
|
|
176
|
+
real_line = test_cases.first&.source_line
|
|
177
|
+
test_cases = test_cases.take_while { |t| t.source_line == real_line }
|
|
178
|
+
|
|
179
|
+
if @index
|
|
180
|
+
test_cases.select! { |t| t.index == @index }
|
|
181
|
+
end
|
|
182
|
+
test_cases
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
class NameMatchFilter
|
|
187
|
+
class << self
|
|
188
|
+
def parse(arg)
|
|
189
|
+
if match = arg.match(%r{\A/(.+)\z})
|
|
190
|
+
new(match[1])
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def initialize(pattern)
|
|
196
|
+
@pattern = Regexp.new(pattern)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def select(test_cases)
|
|
200
|
+
test_cases.select do |t|
|
|
201
|
+
@pattern.match?(t.name) || @pattern.match?(t.id)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
class NameFilter
|
|
207
|
+
class << self
|
|
208
|
+
def parse(arg)
|
|
209
|
+
if match = arg.match(/\A#(.+)\z/)
|
|
210
|
+
new(match[1])
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def initialize(name)
|
|
216
|
+
@name = name
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def select(test_cases)
|
|
220
|
+
test_cases.select do |t|
|
|
221
|
+
@name == t.name || @name == t.id
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
class NegativeFilter
|
|
227
|
+
def initialize(filter)
|
|
228
|
+
@filter = filter
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def select(test_cases)
|
|
232
|
+
test_cases - @filter.select(test_cases)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
FILTERS = [
|
|
237
|
+
ExactLineFilter,
|
|
238
|
+
TagFilter,
|
|
239
|
+
NameMatchFilter,
|
|
240
|
+
NameFilter,
|
|
241
|
+
].freeze
|
|
242
|
+
|
|
243
|
+
class << self
|
|
244
|
+
def parse(argv)
|
|
245
|
+
if argv.empty?
|
|
246
|
+
return List.new([], [])
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
argv = argv.dup
|
|
250
|
+
loaders = []
|
|
251
|
+
filters = []
|
|
252
|
+
|
|
253
|
+
negative = false
|
|
254
|
+
|
|
255
|
+
until argv.empty?
|
|
256
|
+
case argument = argv.shift
|
|
257
|
+
when "!"
|
|
258
|
+
negative = true
|
|
259
|
+
else
|
|
260
|
+
loader_str, filter_str = argument.split(":", 2)
|
|
261
|
+
loader_str = nil if loader_str.empty?
|
|
262
|
+
|
|
263
|
+
filter = nil
|
|
264
|
+
if filter_str
|
|
265
|
+
FILTERS.each do |filter_class|
|
|
266
|
+
if filter = filter_class.parse(filter_str)
|
|
267
|
+
break
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
if loader_str
|
|
273
|
+
loader = Loader.new(loader_str, filter)
|
|
274
|
+
if negative
|
|
275
|
+
loader = NegativeLoader.new(loader)
|
|
276
|
+
negative = false
|
|
277
|
+
end
|
|
278
|
+
loaders << loader
|
|
279
|
+
else
|
|
280
|
+
if negative
|
|
281
|
+
filter = NegativeFilter.new(filter)
|
|
282
|
+
negative = false
|
|
283
|
+
end
|
|
284
|
+
filters << filter
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
List.new(loaders, filters)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|