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
data/lib/test_queue/runner.rb
CHANGED
@@ -1,19 +1,25 @@
|
|
1
|
+
require 'set'
|
1
2
|
require 'socket'
|
2
3
|
require 'fileutils'
|
3
4
|
require 'securerandom'
|
5
|
+
require 'test_queue/stats'
|
6
|
+
require 'test_queue/test_framework'
|
4
7
|
|
5
8
|
module TestQueue
|
6
9
|
class Worker
|
7
|
-
attr_accessor :pid, :status, :output, :
|
10
|
+
attr_accessor :pid, :status, :output, :num, :host
|
8
11
|
attr_accessor :start_time, :end_time
|
9
12
|
attr_accessor :summary, :failure_output
|
10
13
|
|
14
|
+
# Array of TestQueue::Stats::Suite recording all the suites this worker ran.
|
15
|
+
attr_reader :suites
|
16
|
+
|
11
17
|
def initialize(pid, num)
|
12
18
|
@pid = pid
|
13
19
|
@num = num
|
14
20
|
@start_time = Time.now
|
15
21
|
@output = ''
|
16
|
-
@
|
22
|
+
@suites = []
|
17
23
|
end
|
18
24
|
|
19
25
|
def lines
|
@@ -22,21 +28,40 @@ module TestQueue
|
|
22
28
|
end
|
23
29
|
|
24
30
|
class Runner
|
25
|
-
attr_accessor :concurrency
|
31
|
+
attr_accessor :concurrency, :exit_when_done
|
32
|
+
attr_reader :stats
|
33
|
+
|
34
|
+
def initialize(test_framework, concurrency=nil, socket=nil, relay=nil)
|
35
|
+
@test_framework = test_framework
|
36
|
+
@stats = Stats.new(stats_file)
|
37
|
+
|
38
|
+
if ENV['TEST_QUEUE_EARLY_FAILURE_LIMIT']
|
39
|
+
begin
|
40
|
+
@early_failure_limit = Integer(ENV['TEST_QUEUE_EARLY_FAILURE_LIMIT'])
|
41
|
+
rescue ArgumentError
|
42
|
+
raise ArgumentError, 'TEST_QUEUE_EARLY_FAILURE_LIMIT could not be parsed as an integer'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
@procline = $0
|
47
|
+
|
48
|
+
@whitelist = Set.new
|
26
49
|
|
27
|
-
|
28
|
-
|
50
|
+
all_files = @test_framework.all_suite_files.to_set
|
51
|
+
@queue = @stats.all_suites
|
52
|
+
.select { |suite| all_files.include?(suite.path) }
|
53
|
+
.sort_by { |suite| -suite.duration }
|
54
|
+
.map { |suite| [suite.name, suite.path] }
|
29
55
|
|
30
56
|
if forced = ENV['TEST_QUEUE_FORCE']
|
31
57
|
forced = forced.split(/\s*,\s*/)
|
32
|
-
whitelist
|
33
|
-
queue
|
34
|
-
queue.sort_by!{ |
|
58
|
+
@whitelist.merge(forced)
|
59
|
+
@queue.select! { |suite_name, path| @whitelist.include?(suite_name) }
|
60
|
+
@queue.sort_by! { |suite_name, path| forced.index(suite_name) }
|
35
61
|
end
|
36
62
|
|
37
|
-
@
|
38
|
-
@
|
39
|
-
@suites = queue.inject(Hash.new){ |hash, suite| hash.update suite.to_s => suite }
|
63
|
+
@whitelist.freeze
|
64
|
+
@original_queue = Set.new(@queue).freeze
|
40
65
|
|
41
66
|
@workers = {}
|
42
67
|
@completed = []
|
@@ -51,6 +76,9 @@ module TestQueue
|
|
51
76
|
else
|
52
77
|
2
|
53
78
|
end
|
79
|
+
unless @concurrency > 0
|
80
|
+
raise ArgumentError, "Worker count (#{@concurrency}) must be greater than 0"
|
81
|
+
end
|
54
82
|
|
55
83
|
@slave_connection_timeout =
|
56
84
|
(ENV['TEST_QUEUE_RELAY_TIMEOUT'] && ENV['TEST_QUEUE_RELAY_TIMEOUT'].to_i) ||
|
@@ -75,26 +103,27 @@ module TestQueue
|
|
75
103
|
elsif @relay
|
76
104
|
@queue = []
|
77
105
|
end
|
78
|
-
end
|
79
106
|
|
80
|
-
|
81
|
-
@stats ||=
|
82
|
-
if File.exists?(file = stats_file)
|
83
|
-
Marshal.load(IO.binread(file)) || {}
|
84
|
-
else
|
85
|
-
{}
|
86
|
-
end
|
107
|
+
@exit_when_done = true
|
87
108
|
end
|
88
109
|
|
110
|
+
# Run the tests.
|
111
|
+
#
|
112
|
+
# If exit_when_done is true, exit! will be called before this method
|
113
|
+
# completes. If exit_when_done is false, this method will return an Integer
|
114
|
+
# number of failures.
|
89
115
|
def execute
|
90
116
|
$stdout.sync = $stderr.sync = true
|
91
117
|
@start_time = Time.now
|
92
118
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
119
|
+
execute_internal
|
120
|
+
exitstatus = summarize_internal
|
121
|
+
|
122
|
+
if exit_when_done
|
123
|
+
exit! exitstatus
|
124
|
+
else
|
125
|
+
exitstatus
|
126
|
+
end
|
98
127
|
end
|
99
128
|
|
100
129
|
def summarize_internal
|
@@ -104,13 +133,16 @@ module TestQueue
|
|
104
133
|
|
105
134
|
@failures = ''
|
106
135
|
@completed.each do |worker|
|
136
|
+
@stats.record_suites(worker.suites)
|
137
|
+
|
107
138
|
summarize_worker(worker)
|
139
|
+
|
108
140
|
@failures << worker.failure_output if worker.failure_output
|
109
141
|
|
110
142
|
puts " [%2d] %60s %4d suites in %.4fs (pid %d exit %d%s)" % [
|
111
143
|
worker.num,
|
112
144
|
worker.summary,
|
113
|
-
worker.
|
145
|
+
worker.suites.size,
|
114
146
|
worker.end_time - worker.start_time,
|
115
147
|
worker.pid,
|
116
148
|
worker.status.exitstatus,
|
@@ -127,17 +159,13 @@ module TestQueue
|
|
127
159
|
|
128
160
|
puts
|
129
161
|
|
130
|
-
|
131
|
-
File.open(stats_file, 'wb') do |f|
|
132
|
-
f.write Marshal.dump(stats)
|
133
|
-
end
|
134
|
-
end
|
162
|
+
@stats.save
|
135
163
|
|
136
164
|
summarize
|
137
165
|
|
138
166
|
estatus = @completed.inject(0){ |s, worker| s + worker.status.exitstatus }
|
139
167
|
estatus = 255 if estatus > 255
|
140
|
-
|
168
|
+
estatus
|
141
169
|
end
|
142
170
|
|
143
171
|
def summarize
|
@@ -148,27 +176,18 @@ module TestQueue
|
|
148
176
|
'.test_queue_stats'
|
149
177
|
end
|
150
178
|
|
151
|
-
def
|
152
|
-
exit! run_worker(@queue)
|
153
|
-
end
|
154
|
-
|
155
|
-
def execute_parallel
|
179
|
+
def execute_internal
|
156
180
|
start_master
|
157
181
|
prepare(@concurrency)
|
158
182
|
@prepared_time = Time.now
|
159
183
|
start_relay if relay?
|
184
|
+
discover_suites
|
160
185
|
spawn_workers
|
161
186
|
distribute_queue
|
162
187
|
ensure
|
163
188
|
stop_master
|
164
189
|
|
165
|
-
|
166
|
-
Process.kill 'KILL', pid
|
167
|
-
end
|
168
|
-
|
169
|
-
until @workers.empty?
|
170
|
-
reap_worker
|
171
|
-
end
|
190
|
+
kill_workers
|
172
191
|
end
|
173
192
|
|
174
193
|
def start_master
|
@@ -223,7 +242,7 @@ module TestQueue
|
|
223
242
|
pid = fork do
|
224
243
|
@server.close if @server
|
225
244
|
|
226
|
-
iterator = Iterator.new(relay?? @relay : @socket,
|
245
|
+
iterator = Iterator.new(@test_framework, relay?? @relay : @socket, method(:around_filter), early_failure_limit: @early_failure_limit)
|
227
246
|
after_fork_internal(num, iterator)
|
228
247
|
ret = run_worker(iterator) || 0
|
229
248
|
cleanup_worker
|
@@ -234,6 +253,37 @@ module TestQueue
|
|
234
253
|
end
|
235
254
|
end
|
236
255
|
|
256
|
+
def discover_suites
|
257
|
+
return if relay?
|
258
|
+
@discovering_suites_pid = fork do
|
259
|
+
@test_framework.all_suite_files.each do |path|
|
260
|
+
@test_framework.suites_from_file(path).each do |suite_name, suite|
|
261
|
+
@server.connect_address.connect do |sock|
|
262
|
+
sock.puts("NEW SUITE #{Marshal.dump([suite_name, path])}")
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
Kernel.exit! 0
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def enqueue_discovered_suite(suite_name, path)
|
272
|
+
if @whitelist.any? && !@whitelist.include?(suite_name)
|
273
|
+
return
|
274
|
+
end
|
275
|
+
|
276
|
+
if @original_queue.include?([suite_name, path])
|
277
|
+
# This suite was already added to the queue some other way.
|
278
|
+
return
|
279
|
+
end
|
280
|
+
|
281
|
+
# We don't know how long new suites will take to run, so we put them at
|
282
|
+
# the front of the queue. It's better to run a fast suite early than to
|
283
|
+
# run a slow suite late.
|
284
|
+
@queue.unshift [suite_name, path]
|
285
|
+
end
|
286
|
+
|
237
287
|
def after_fork_internal(num, iterator)
|
238
288
|
srand
|
239
289
|
|
@@ -268,8 +318,7 @@ module TestQueue
|
|
268
318
|
# Entry point for internal runner implementations. The iterator will yield
|
269
319
|
# jobs from the shared queue on the master.
|
270
320
|
#
|
271
|
-
# Returns
|
272
|
-
# exits N on error, where N is the number of failures.
|
321
|
+
# Returns an Integer number of failures.
|
273
322
|
def run_worker(iterator)
|
274
323
|
iterator.each do |item|
|
275
324
|
puts " #{item.inspect}"
|
@@ -286,27 +335,34 @@ module TestQueue
|
|
286
335
|
worker.failure_output = ''
|
287
336
|
end
|
288
337
|
|
289
|
-
def
|
290
|
-
|
338
|
+
def reap_workers(blocking=true)
|
339
|
+
@workers.delete_if do |_, worker|
|
340
|
+
if Process.waitpid(worker.pid, blocking ? 0 : Process::WNOHANG).nil?
|
341
|
+
next false
|
342
|
+
end
|
343
|
+
|
291
344
|
worker.status = $?
|
292
345
|
worker.end_time = Time.now
|
293
346
|
|
294
|
-
if File.exists?(file = "/tmp/test_queue_worker_#{pid}_output")
|
347
|
+
if File.exists?(file = "/tmp/test_queue_worker_#{worker.pid}_output")
|
295
348
|
worker.output = IO.binread(file)
|
296
349
|
FileUtils.rm(file)
|
297
350
|
end
|
298
351
|
|
299
|
-
if File.exists?(file = "/tmp/test_queue_worker_#{pid}
|
300
|
-
worker.
|
352
|
+
if File.exists?(file = "/tmp/test_queue_worker_#{worker.pid}_suites")
|
353
|
+
worker.suites.replace(Marshal.load(IO.binread(file)))
|
301
354
|
FileUtils.rm(file)
|
302
355
|
end
|
303
356
|
|
304
357
|
relay_to_master(worker) if relay?
|
305
358
|
worker_completed(worker)
|
359
|
+
|
360
|
+
true
|
306
361
|
end
|
307
362
|
end
|
308
363
|
|
309
364
|
def worker_completed(worker)
|
365
|
+
return if @aborting
|
310
366
|
@completed << worker
|
311
367
|
puts worker.output if ENV['TEST_QUEUE_VERBOSE'] || worker.status.exitstatus != 0
|
312
368
|
end
|
@@ -315,9 +371,17 @@ module TestQueue
|
|
315
371
|
return if relay?
|
316
372
|
remote_workers = 0
|
317
373
|
|
318
|
-
until @queue.empty? && remote_workers == 0
|
374
|
+
until @discovering_suites_pid.nil? && @queue.empty? && remote_workers == 0
|
375
|
+
queue_status(@start_time, @queue.size, @workers.size, remote_workers)
|
376
|
+
|
377
|
+
# Make sure our discovery process is still doing OK.
|
378
|
+
if @discovering_suites_pid && Process.waitpid(@discovering_suites_pid, Process::WNOHANG) != nil
|
379
|
+
@discovering_suites_pid = nil
|
380
|
+
abort("Discovering suites failed.") unless $?.success?
|
381
|
+
end
|
382
|
+
|
319
383
|
if IO.select([@server], nil, nil, 0.1).nil?
|
320
|
-
|
384
|
+
reap_workers(false) # check for worker deaths
|
321
385
|
else
|
322
386
|
sock = @server.accept
|
323
387
|
cmd = sock.gets.strip
|
@@ -325,8 +389,10 @@ module TestQueue
|
|
325
389
|
when /^POP/
|
326
390
|
# If we have a slave from a different test run, don't respond, and it will consider the test run done.
|
327
391
|
if obj = @queue.shift
|
328
|
-
data = Marshal.dump(obj
|
392
|
+
data = Marshal.dump(obj)
|
329
393
|
sock.write(data)
|
394
|
+
elsif @discovering_suites_pid
|
395
|
+
sock.write(Marshal.dump("WAIT"))
|
330
396
|
end
|
331
397
|
when /^SLAVE (\d+) ([\w\.-]+) (\w+)(?: (.+))?/
|
332
398
|
num = $1.to_i
|
@@ -349,16 +415,20 @@ module TestQueue
|
|
349
415
|
worker = Marshal.load(data)
|
350
416
|
worker_completed(worker)
|
351
417
|
remote_workers -= 1
|
418
|
+
when /^NEW SUITE (.+)/
|
419
|
+
suite_name, path = Marshal.load($1)
|
420
|
+
enqueue_discovered_suite(suite_name, path)
|
421
|
+
when /^KABOOM/
|
422
|
+
# worker reporting an abnormal number of test failures;
|
423
|
+
# stop everything immediately and report the results.
|
424
|
+
break
|
352
425
|
end
|
353
426
|
sock.close
|
354
427
|
end
|
355
428
|
end
|
356
429
|
ensure
|
357
430
|
stop_master
|
358
|
-
|
359
|
-
until @workers.empty?
|
360
|
-
reap_worker
|
361
|
-
end
|
431
|
+
reap_workers
|
362
432
|
end
|
363
433
|
|
364
434
|
def relay?
|
@@ -391,5 +461,44 @@ module TestQueue
|
|
391
461
|
ensure
|
392
462
|
sock.close if sock
|
393
463
|
end
|
464
|
+
|
465
|
+
def kill_workers
|
466
|
+
@workers.each do |pid, worker|
|
467
|
+
Process.kill 'KILL', pid
|
468
|
+
end
|
469
|
+
|
470
|
+
reap_workers
|
471
|
+
end
|
472
|
+
|
473
|
+
# Stop the test run immediately.
|
474
|
+
#
|
475
|
+
# message - String message to print to the console when exiting.
|
476
|
+
#
|
477
|
+
# Doesn't return.
|
478
|
+
def abort(message)
|
479
|
+
@aborting = true
|
480
|
+
kill_workers
|
481
|
+
Kernel::abort("Aborting: #{message}")
|
482
|
+
end
|
483
|
+
|
484
|
+
# Subclasses can override to monitor the status of the queue.
|
485
|
+
#
|
486
|
+
# For example, you may want to record metrics about how quickly remote
|
487
|
+
# workers connect, or abort the build if not enough connect.
|
488
|
+
#
|
489
|
+
# This method is called very frequently during the test run, so don't do
|
490
|
+
# anything expensive/blocking.
|
491
|
+
#
|
492
|
+
# This method is not called on remote masters when using remote workers,
|
493
|
+
# only on the central master.
|
494
|
+
#
|
495
|
+
# start_time - Time when the test run began
|
496
|
+
# queue_size - Integer number of suites left in the queue
|
497
|
+
# local_worker_count - Integer number of active local workers
|
498
|
+
# remote_worker_count - Integer number of active remote workers
|
499
|
+
#
|
500
|
+
# Returns nothing.
|
501
|
+
def queue_status(start_time, queue_size, local_worker_count, remote_worker_count)
|
502
|
+
end
|
394
503
|
end
|
395
504
|
end
|
@@ -14,32 +14,101 @@ module Cucumber
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
17
|
+
|
18
|
+
class Runtime
|
19
|
+
if defined?(::Cucumber::Runtime::FeaturesLoader)
|
20
|
+
# Without this module, Runtime#features would load all features specified
|
21
|
+
# on the command line. We want to avoid that and load only the features
|
22
|
+
# each worker needs ourselves, so we override the default behavior to let
|
23
|
+
# us put our iterator in place without loading any features directly.
|
24
|
+
module InjectableFeatures
|
25
|
+
def features
|
26
|
+
return @features if defined?(@features)
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def features=(iterator)
|
31
|
+
@features = ::Cucumber::Ast::Features.new
|
32
|
+
@features.features = iterator
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
prepend InjectableFeatures
|
37
|
+
else
|
38
|
+
attr_writer :features
|
39
|
+
end
|
40
|
+
end
|
17
41
|
end
|
18
42
|
|
19
43
|
module TestQueue
|
20
44
|
class Runner
|
21
45
|
class Cucumber < Runner
|
22
46
|
def initialize
|
23
|
-
|
24
|
-
@runtime = ::Cucumber::Runtime.new(@cli.configuration)
|
25
|
-
@features_loader = @runtime.send(:features)
|
26
|
-
features = @features_loader.features.sort_by{ |s| -(stats[s.to_s] || 0) }
|
27
|
-
super(features)
|
47
|
+
super(TestFramework::Cucumber.new)
|
28
48
|
end
|
29
49
|
|
30
50
|
def run_worker(iterator)
|
31
|
-
|
32
|
-
|
51
|
+
runtime = @test_framework.runtime
|
52
|
+
runtime.features = iterator
|
53
|
+
|
54
|
+
@test_framework.cli.execute!(runtime)
|
55
|
+
|
56
|
+
if runtime.respond_to?(:summary_report, true)
|
57
|
+
runtime.send(:summary_report).test_cases.total_failed
|
58
|
+
else
|
59
|
+
runtime.results.scenarios(:failed).size
|
60
|
+
end
|
33
61
|
end
|
34
62
|
|
35
63
|
def summarize_worker(worker)
|
36
|
-
worker.
|
37
|
-
|
64
|
+
output = worker.output.gsub(/\e\[\d+./, '')
|
65
|
+
worker.summary = output.split("\n").grep(/^\d+ (scenarios?|steps?)/).first
|
66
|
+
worker.failure_output = output.scan(/^Failing Scenarios:\n(.*)\n\d+ scenarios?/m).join("\n")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class TestFramework
|
72
|
+
class Cucumber < TestFramework
|
73
|
+
class FakeKernel
|
74
|
+
def exit(n)
|
75
|
+
if $!
|
76
|
+
# Let Cucumber exit for raised exceptions.
|
77
|
+
Kernel.exit(n)
|
78
|
+
end
|
79
|
+
# Don't let Cucumber exit to indicate test failures. We want to
|
80
|
+
# return the number of failures from #run_worker instead.
|
38
81
|
end
|
82
|
+
end
|
39
83
|
|
40
|
-
|
41
|
-
|
42
|
-
|
84
|
+
def cli
|
85
|
+
@cli ||= ::Cucumber::Cli::Main.new(ARGV.dup, $stdin, $stdout, $stderr, FakeKernel.new)
|
86
|
+
end
|
87
|
+
|
88
|
+
def runtime
|
89
|
+
@runtime ||= ::Cucumber::Runtime.new(cli.configuration)
|
90
|
+
end
|
91
|
+
|
92
|
+
def all_suite_files
|
93
|
+
if runtime.respond_to?(:feature_files, true)
|
94
|
+
runtime.send(:feature_files)
|
95
|
+
else
|
96
|
+
cli.configuration.feature_files
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def suites_from_file(path)
|
101
|
+
if defined?(::Cucumber::Core::Gherkin::Document)
|
102
|
+
source = ::Cucumber::Runtime::NormalisedEncodingFile.read(path)
|
103
|
+
doc = ::Cucumber::Core::Gherkin::Document.new(path, source)
|
104
|
+
[[File.basename(doc.uri), doc]]
|
105
|
+
else
|
106
|
+
loader =
|
107
|
+
::Cucumber::Runtime::FeaturesLoader.new([path],
|
108
|
+
cli.configuration.filters,
|
109
|
+
cli.configuration.tag_expression)
|
110
|
+
loader.features.map { |feature| [feature.title, feature] }
|
111
|
+
end
|
43
112
|
end
|
44
113
|
end
|
45
114
|
end
|