prspec 0.2.5 → 0.2.6
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.
- data/lib/prspec.rb +393 -393
- metadata +2 -2
data/lib/prspec.rb
CHANGED
|
@@ -1,394 +1,394 @@
|
|
|
1
|
-
require 'thread'
|
|
2
|
-
require 'optparse'
|
|
3
|
-
require 'log4r'
|
|
4
|
-
require 'parallel'
|
|
5
|
-
require 'yaml'
|
|
6
|
-
require 'tempfile'
|
|
7
|
-
require 'fileutils'
|
|
8
|
-
include Log4r
|
|
9
|
-
|
|
10
|
-
$log = Logger.new('prspec')
|
|
11
|
-
$log.outputters = Outputter.stdout
|
|
12
|
-
level = ENV['log_level'] || ''
|
|
13
|
-
case level.downcase
|
|
14
|
-
when 'all'
|
|
15
|
-
$log.level = ALL
|
|
16
|
-
when 'debug'
|
|
17
|
-
$log.level = DEBUG
|
|
18
|
-
when 'info'
|
|
19
|
-
$log.level = INFO
|
|
20
|
-
when 'warn'
|
|
21
|
-
$log.level = WARN
|
|
22
|
-
else
|
|
23
|
-
$log.level = ERROR
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
class PRSpec
|
|
27
|
-
attr_accessor :num_threads, :processes, :tests, :output
|
|
28
|
-
SPEC_FILE_FILTER = '_spec.rb'
|
|
29
|
-
INFO_FILE = ".prspec"
|
|
30
|
-
|
|
31
|
-
def initialize(args)
|
|
32
|
-
@output = ''
|
|
33
|
-
# create tracking file
|
|
34
|
-
yml = { :running_threads => 0 }
|
|
35
|
-
File.open(INFO_FILE, 'w') { |f| f.write yml.to_yaml }
|
|
36
|
-
if (!args.nil? && args.length > 0 && !args[0].nil?)
|
|
37
|
-
opts = parse_args(args)
|
|
38
|
-
if (!opts[:help])
|
|
39
|
-
@num_threads = opts[:thread_count]
|
|
40
|
-
|
|
41
|
-
@tests = get_spec_tests(opts)
|
|
42
|
-
if (tests.length > 0)
|
|
43
|
-
process_tests = divide_spec_tests(tests)
|
|
44
|
-
$log.debug "#{tests.length} Spec tests divided among #{@num_threads} arrays."
|
|
45
|
-
else
|
|
46
|
-
$log.warn "No spec tests found. Exiting."
|
|
47
|
-
exit 1
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
$log.info "Creating array of Child Processes..."
|
|
51
|
-
@processes = build_process_array(process_tests, opts)
|
|
52
|
-
|
|
53
|
-
begin_run(@processes, opts)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
ensure
|
|
57
|
-
FileUtils.remove_file(INFO_FILE, :force => true) if File.exists?(INFO_FILE)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def parse_args(args)
|
|
61
|
-
$log.debug("Parsing arguments of: #{args}")
|
|
62
|
-
options = {
|
|
63
|
-
:dir=>'.',
|
|
64
|
-
:path=>'spec',
|
|
65
|
-
:thread_count=>get_number_of_processors,
|
|
66
|
-
:test_mode=>false,
|
|
67
|
-
:help=>false,
|
|
68
|
-
:excludes=>nil,
|
|
69
|
-
:rspec_args=>[],
|
|
70
|
-
:tag_name=>'',
|
|
71
|
-
:tag_value=>'',
|
|
72
|
-
:ignore_pending=>false,
|
|
73
|
-
:serialize=>false
|
|
74
|
-
}
|
|
75
|
-
o = OptionParser.new do |opts|
|
|
76
|
-
opts.banner = "Usage: prspec [options]"
|
|
77
|
-
opts.on("-p", "--path PATH", "Relative path from the base directory to search for spec files") do |v|
|
|
78
|
-
$log.debug "path specified... value: #{v}"
|
|
79
|
-
options[:path] = v
|
|
80
|
-
end
|
|
81
|
-
opts.on("-e", "--exclude REGEX", "Regex string used to exclude files") do |v|
|
|
82
|
-
$log.debug "excludes specified... value: #{v}"
|
|
83
|
-
options[:excludes] = v
|
|
84
|
-
end
|
|
85
|
-
opts.on("-d", "--dir DIRECTORY", "The base directory to run from") do |v|
|
|
86
|
-
$log.debug "directory specified... value: #{v}"
|
|
87
|
-
options[:dir] = v
|
|
88
|
-
end
|
|
89
|
-
opts.on("-n", "--num-threads THREADS", "The number of threads to use") do |v|
|
|
90
|
-
$log.debug "number of threads specified... value: #{v}"
|
|
91
|
-
options[:thread_count] = v.to_i
|
|
92
|
-
end
|
|
93
|
-
opts.on("-t", "--tag TAG", "A rspec tag value to filter by") do |v|
|
|
94
|
-
$log.debug "tag filter specified... value: #{v}"
|
|
95
|
-
tag = v
|
|
96
|
-
value = 'true'
|
|
97
|
-
if (v.include?(':')) # split to tag and value
|
|
98
|
-
tag_value = v.split(':')
|
|
99
|
-
tag = ":#{tag_value[0]}"
|
|
100
|
-
value = "#{tag_value[1]}"
|
|
101
|
-
end
|
|
102
|
-
options[:tag_name] = tag
|
|
103
|
-
options[:tag_value] = value
|
|
104
|
-
end
|
|
105
|
-
opts.on("-r", "--rspec-args \"RSPEC_ARGS\"", "Additional arguments to be passed to rspec (must be surrounded with double quotes)") do |v|
|
|
106
|
-
$log.debug "rspec arguments specified... value: #{v}"
|
|
107
|
-
options[:rspec_args] = v.gsub(/"/,'').split(' ') # create an array of each argument
|
|
108
|
-
end
|
|
109
|
-
opts.on("--test-mode", "Do everything except actually starting the test threads") do
|
|
110
|
-
$log.debug "test mode specified... threads will NOT be started."
|
|
111
|
-
options[:test_mode] = true
|
|
112
|
-
end
|
|
113
|
-
opts.on("-q", "--quiet", "Quiet mode. Do not display parallel thread output") do
|
|
114
|
-
$log.debug "quiet mode specified... thread output will not be displayed"
|
|
115
|
-
options[:quiet_mode] = true
|
|
116
|
-
end
|
|
117
|
-
opts.on("-h", "--help", "Display a help message") do
|
|
118
|
-
$log.debug "help message requested..."
|
|
119
|
-
options[:help] = true
|
|
120
|
-
puts opts
|
|
121
|
-
end
|
|
122
|
-
opts.on("--ignore-pending", "Ignore all pending tests") do
|
|
123
|
-
$log.debug "ignore pending specified... all pending tests will be excluded"
|
|
124
|
-
options[:ignore_pending] = true
|
|
125
|
-
end
|
|
126
|
-
opts.on("-s", "--serialize-output", "Wait for each thread to complete and then output to STDOUT serially") do
|
|
127
|
-
$log.debug "serialize output specified... all threads will output to STDOUT as they complete"
|
|
128
|
-
options[:serialize] = true
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# handle invalid options
|
|
133
|
-
begin
|
|
134
|
-
o.parse! args
|
|
135
|
-
rescue OptionParser::InvalidOption => e
|
|
136
|
-
$log.error e
|
|
137
|
-
puts o
|
|
138
|
-
exit 1
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
return options
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def get_number_of_processors
|
|
145
|
-
count = Parallel.processor_count
|
|
146
|
-
return count
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def self.get_number_of_running_threads
|
|
150
|
-
prspec_info = YAML.load_file(INFO_FILE)
|
|
151
|
-
return prspec_info[:running_threads].to_i
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def get_spec_tests(options)
|
|
155
|
-
files = get_spec_files(options)
|
|
156
|
-
tests = get_tests_from_files(files, options)
|
|
157
|
-
$log.debug "Found #{tests.length} tests in #{files.length} files"
|
|
158
|
-
return tests
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def get_spec_files(options)
|
|
162
|
-
base_dir = options[:dir]
|
|
163
|
-
path = options[:path]
|
|
164
|
-
if (path.nil? || path == '')
|
|
165
|
-
path = '.'
|
|
166
|
-
end
|
|
167
|
-
full_path = ""
|
|
168
|
-
if (path.end_with?('.rb'))
|
|
169
|
-
full_path = File.join(base_dir, path.to_s)
|
|
170
|
-
else
|
|
171
|
-
full_path = File.join(base_dir, path.to_s, '**', "*#{SPEC_FILE_FILTER}")
|
|
172
|
-
end
|
|
173
|
-
$log.debug "full_path: #{full_path}"
|
|
174
|
-
|
|
175
|
-
files = []
|
|
176
|
-
if (options[:excludes].nil?)
|
|
177
|
-
files = Dir.glob(full_path)
|
|
178
|
-
else
|
|
179
|
-
files = Dir.glob(full_path).reject { |f| f[options[:excludes]] }
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
return files
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def get_tests_from_files(files, options)
|
|
186
|
-
tests = []
|
|
187
|
-
match_test_name_format = /^[\s]*(it)[\s]*(')[\s\S]*(')[\s\S]*(do)/
|
|
188
|
-
files.each do |file|
|
|
189
|
-
lines = File.readlines(file)
|
|
190
|
-
for i in 0..lines.length-1
|
|
191
|
-
if lines[i] =~ match_test_name_format
|
|
192
|
-
m = lines[i]
|
|
193
|
-
match = true
|
|
194
|
-
if (options[:tag_name] != '')
|
|
195
|
-
if (m.rindex(options[:tag_name]).nil? || (m.rindex(options[:tag_value]) <= m.rindex(options[:tag_name])))
|
|
196
|
-
match = false
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
# if ignore_pending specified then skip tests containing 'pending' on next line
|
|
200
|
-
if (options[:ignore_pending])
|
|
201
|
-
if (i+1 < lines.length-1 && lines[i+1].include?('pending'))
|
|
202
|
-
match = false
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
if (match)
|
|
206
|
-
description = get_test_description(m)
|
|
207
|
-
tests.push("\"#{description}\"")
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
return tests
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def get_test_description(description_line)
|
|
217
|
-
# get index of first "'"
|
|
218
|
-
trim_front = description_line.sub(/[\s]*(it)[\s]*(')/,'')
|
|
219
|
-
description = trim_front.sub(/(')[\s]*(,[\s\S]*)*(do)[\s]*/,'')
|
|
220
|
-
return description.gsub(/["]/,'\"').gsub(/\\'/,"'")
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
def divide_spec_tests(tests)
|
|
224
|
-
if (tests.length < @num_threads)
|
|
225
|
-
@num_threads = tests.length
|
|
226
|
-
$log.info "reducing number of threads due to low number of spec tests found: Threads = #{@num_threads}"
|
|
227
|
-
end
|
|
228
|
-
spec_arrays = Array.new(@num_threads)
|
|
229
|
-
num_per_thread = tests.length.fdiv(@num_threads).ceil
|
|
230
|
-
$log.debug "Approximate number of tests per thread: #{num_per_thread}"
|
|
231
|
-
# ensure an even distribution
|
|
232
|
-
i = 0
|
|
233
|
-
tests.each do |tname|
|
|
234
|
-
if (i >= @num_threads)
|
|
235
|
-
i = 0
|
|
236
|
-
end
|
|
237
|
-
if (spec_arrays[i].nil?)
|
|
238
|
-
spec_arrays[i] = []
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
spec_arrays[i].push(tname)
|
|
242
|
-
|
|
243
|
-
i+=1
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
return spec_arrays
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def build_process_array(process_tests, opts)
|
|
250
|
-
processes = []
|
|
251
|
-
for i in 0..@num_threads-1
|
|
252
|
-
processes[i] = PRSpecThread.new(i, process_tests[i], {'TEST_ENV_NUMBER'=>i, 'HOME'=>nil}, opts)
|
|
253
|
-
end
|
|
254
|
-
return processes
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
def begin_run(processes, options)
|
|
258
|
-
if (!processes.nil? && processes.length > 0)
|
|
259
|
-
$log.info "Starting all Child Processes..."
|
|
260
|
-
update_running_thread_count(processes.length)
|
|
261
|
-
processes.each do |proc|
|
|
262
|
-
if (proc.is_a?(PRSpecThread) && options.is_a?(Hash))
|
|
263
|
-
proc.start unless options[:test_mode]
|
|
264
|
-
else
|
|
265
|
-
raise "Invalid datatype where PRSpecThread or Hash exepcted. Found: #{proc.class.to_s}, #{options.class.to_s}"
|
|
266
|
-
end
|
|
267
|
-
end
|
|
268
|
-
$log.info "All processes started..."
|
|
269
|
-
while processes.length > 0
|
|
270
|
-
processes.each do |proc|
|
|
271
|
-
if (!proc.done?) # confirm threads are running
|
|
272
|
-
$log.debug "Thread#{proc.id}: alive..."
|
|
273
|
-
else
|
|
274
|
-
$log.debug "Thread#{proc.id}: done."
|
|
275
|
-
if (options[:serialize])
|
|
276
|
-
puts proc.output unless options[:quiet_mode]
|
|
277
|
-
end
|
|
278
|
-
# collect thread output if in quiet mode
|
|
279
|
-
if (options[:quiet_mode])
|
|
280
|
-
@output << proc.output
|
|
281
|
-
end
|
|
282
|
-
processes.delete(proc) # remove from the array of processes so we don't count it again
|
|
283
|
-
update_running_thread_count(processes.length)
|
|
284
|
-
end
|
|
285
|
-
end
|
|
286
|
-
sleep 0.5 # wait half a second for processes to run and then re-check their status
|
|
287
|
-
end
|
|
288
|
-
$log.info "All processes complete."
|
|
289
|
-
else
|
|
290
|
-
raise "Invalid input passed to method: 'processes' must be a valid Array of PRSpecThread objects"
|
|
291
|
-
end
|
|
292
|
-
end
|
|
293
|
-
|
|
294
|
-
def update_running_thread_count(count)
|
|
295
|
-
(file = File.new(INFO_FILE,'w')).flock(File::LOCK_EX)
|
|
296
|
-
yml = { :running_threads => count }
|
|
297
|
-
file.write yml.to_yaml
|
|
298
|
-
ensure
|
|
299
|
-
file.flock(File::LOCK_UN)
|
|
300
|
-
file.close
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
def running?
|
|
304
|
-
@processes.each do |proc|
|
|
305
|
-
if (!proc.done?)
|
|
306
|
-
$log.debug "Found running process..."
|
|
307
|
-
return true
|
|
308
|
-
end
|
|
309
|
-
end
|
|
310
|
-
return false
|
|
311
|
-
end
|
|
312
|
-
|
|
313
|
-
def close
|
|
314
|
-
@processes.each do |proc|
|
|
315
|
-
proc.close
|
|
316
|
-
end
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
def self.is_windows?
|
|
320
|
-
return (RUBY_PLATFORM.match(/mingw/i)) ? true : false
|
|
321
|
-
end
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
class PRSpecThread
|
|
325
|
-
attr_accessor :thread, :id, :tests, :env, :args, :output
|
|
326
|
-
def initialize(id, tests, environment, args)
|
|
327
|
-
@id = id
|
|
328
|
-
@tests = tests
|
|
329
|
-
@env = environment
|
|
330
|
-
@args = args
|
|
331
|
-
@output = ''
|
|
332
|
-
@out = Tempfile.new("prspec-t-#{@id}.out").path
|
|
333
|
-
$log.debug("Thread#{@id} @out file: #{@out}")
|
|
334
|
-
@err = Tempfile.new("prspec-t-#{@id}.err").path
|
|
335
|
-
$log.debug("Thread#{@id} @err file: #{@err}")
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
def start
|
|
339
|
-
filter_str = @tests.join(" -e ")
|
|
340
|
-
filter_str = "-e " + filter_str
|
|
341
|
-
exports = get_exports
|
|
342
|
-
rspec_args = @args[:rspec_args].join(' ')
|
|
343
|
-
cmd = "#{exports}rspec #{@args[:path]} #{filter_str} #{rspec_args}"
|
|
344
|
-
$log.debug "Starting child process for thread#{@id}: #{cmd}"
|
|
345
|
-
@thread = Thread.new do
|
|
346
|
-
Thread.current[:id] = @id
|
|
347
|
-
Dir::chdir @args[:dir] do # change directory for process execution
|
|
348
|
-
begin
|
|
349
|
-
pid = nil
|
|
350
|
-
if (@args[:quiet_mode] || @args[:serialize])
|
|
351
|
-
pid = Process.spawn(cmd, :out=>@out, :err=>@err) # capture both sdtout and stderr
|
|
352
|
-
else
|
|
353
|
-
pid = Process.spawn(cmd)
|
|
354
|
-
end
|
|
355
|
-
Process.wait(pid)
|
|
356
|
-
@output = File.readlines(@out).join("\n") if (@args[:quiet_mode] || @args[:serialize])
|
|
357
|
-
rescue
|
|
358
|
-
if (@args[:quiet_mode] || @args[:serialize])
|
|
359
|
-
error = "ErrorCode: #{$?.errorcode}; ErrorOutput: "+File.readlines(@err).join("\n")
|
|
360
|
-
$log.error "Something bad happened while executing thread#{@id}: #{error}"
|
|
361
|
-
end
|
|
362
|
-
end
|
|
363
|
-
close
|
|
364
|
-
end
|
|
365
|
-
end
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
def done?
|
|
369
|
-
return !@thread.alive?
|
|
370
|
-
rescue
|
|
371
|
-
return true # if there's a problem with @thread we're done
|
|
372
|
-
end
|
|
373
|
-
|
|
374
|
-
def close
|
|
375
|
-
@thread.sleep
|
|
376
|
-
@thread.kill
|
|
377
|
-
@thread = nil
|
|
378
|
-
FileUtils.remove_file(@out, :force => true) unless !File.exist?(@out)
|
|
379
|
-
FileUtils.remove_file(@err, :force => true) unless !File.exist?(@err)
|
|
380
|
-
end
|
|
381
|
-
|
|
382
|
-
def get_exports
|
|
383
|
-
separator = PRSpec.is_windows? ? ' & ' : ';'
|
|
384
|
-
exports = @env.map do |k,v|
|
|
385
|
-
if PRSpec.is_windows?
|
|
386
|
-
"(SET \"#{k}=#{v}\")"
|
|
387
|
-
else
|
|
388
|
-
"export #{k}=#{v}"
|
|
389
|
-
end
|
|
390
|
-
end.join(separator)
|
|
391
|
-
|
|
392
|
-
return exports+separator
|
|
393
|
-
end
|
|
1
|
+
require 'thread'
|
|
2
|
+
require 'optparse'
|
|
3
|
+
require 'log4r'
|
|
4
|
+
require 'parallel'
|
|
5
|
+
require 'yaml'
|
|
6
|
+
require 'tempfile'
|
|
7
|
+
require 'fileutils'
|
|
8
|
+
include Log4r
|
|
9
|
+
|
|
10
|
+
$log = Logger.new('prspec')
|
|
11
|
+
$log.outputters = Outputter.stdout
|
|
12
|
+
level = ENV['log_level'] || ''
|
|
13
|
+
case level.downcase
|
|
14
|
+
when 'all'
|
|
15
|
+
$log.level = ALL
|
|
16
|
+
when 'debug'
|
|
17
|
+
$log.level = DEBUG
|
|
18
|
+
when 'info'
|
|
19
|
+
$log.level = INFO
|
|
20
|
+
when 'warn'
|
|
21
|
+
$log.level = WARN
|
|
22
|
+
else
|
|
23
|
+
$log.level = ERROR
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class PRSpec
|
|
27
|
+
attr_accessor :num_threads, :processes, :tests, :output
|
|
28
|
+
SPEC_FILE_FILTER = '_spec.rb'
|
|
29
|
+
INFO_FILE = ".prspec"
|
|
30
|
+
|
|
31
|
+
def initialize(args)
|
|
32
|
+
@output = ''
|
|
33
|
+
# create tracking file
|
|
34
|
+
yml = { :running_threads => 0 }
|
|
35
|
+
File.open(INFO_FILE, 'w') { |f| f.write yml.to_yaml }
|
|
36
|
+
if (!args.nil? && args.length > 0 && !args[0].nil?)
|
|
37
|
+
opts = parse_args(args)
|
|
38
|
+
if (!opts[:help])
|
|
39
|
+
@num_threads = opts[:thread_count]
|
|
40
|
+
|
|
41
|
+
@tests = get_spec_tests(opts)
|
|
42
|
+
if (tests.length > 0)
|
|
43
|
+
process_tests = divide_spec_tests(tests)
|
|
44
|
+
$log.debug "#{tests.length} Spec tests divided among #{@num_threads} arrays."
|
|
45
|
+
else
|
|
46
|
+
$log.warn "No spec tests found. Exiting."
|
|
47
|
+
exit 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
$log.info "Creating array of Child Processes..."
|
|
51
|
+
@processes = build_process_array(process_tests, opts)
|
|
52
|
+
|
|
53
|
+
begin_run(@processes, opts)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
ensure
|
|
57
|
+
FileUtils.remove_file(INFO_FILE, :force => true) if File.exists?(INFO_FILE)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def parse_args(args)
|
|
61
|
+
$log.debug("Parsing arguments of: #{args}")
|
|
62
|
+
options = {
|
|
63
|
+
:dir=>'.',
|
|
64
|
+
:path=>'spec',
|
|
65
|
+
:thread_count=>get_number_of_processors,
|
|
66
|
+
:test_mode=>false,
|
|
67
|
+
:help=>false,
|
|
68
|
+
:excludes=>nil,
|
|
69
|
+
:rspec_args=>[],
|
|
70
|
+
:tag_name=>'',
|
|
71
|
+
:tag_value=>'',
|
|
72
|
+
:ignore_pending=>false,
|
|
73
|
+
:serialize=>false
|
|
74
|
+
}
|
|
75
|
+
o = OptionParser.new do |opts|
|
|
76
|
+
opts.banner = "Usage: prspec [options]"
|
|
77
|
+
opts.on("-p", "--path PATH", "Relative path from the base directory to search for spec files") do |v|
|
|
78
|
+
$log.debug "path specified... value: #{v}"
|
|
79
|
+
options[:path] = v
|
|
80
|
+
end
|
|
81
|
+
opts.on("-e", "--exclude REGEX", "Regex string used to exclude files") do |v|
|
|
82
|
+
$log.debug "excludes specified... value: #{v}"
|
|
83
|
+
options[:excludes] = v
|
|
84
|
+
end
|
|
85
|
+
opts.on("-d", "--dir DIRECTORY", "The base directory to run from") do |v|
|
|
86
|
+
$log.debug "directory specified... value: #{v}"
|
|
87
|
+
options[:dir] = v
|
|
88
|
+
end
|
|
89
|
+
opts.on("-n", "--num-threads THREADS", "The number of threads to use") do |v|
|
|
90
|
+
$log.debug "number of threads specified... value: #{v}"
|
|
91
|
+
options[:thread_count] = v.to_i
|
|
92
|
+
end
|
|
93
|
+
opts.on("-t", "--tag TAG", "A rspec tag value to filter by") do |v|
|
|
94
|
+
$log.debug "tag filter specified... value: #{v}"
|
|
95
|
+
tag = v
|
|
96
|
+
value = 'true'
|
|
97
|
+
if (v.include?(':')) # split to tag and value
|
|
98
|
+
tag_value = v.split(':')
|
|
99
|
+
tag = ":#{tag_value[0]}"
|
|
100
|
+
value = "#{tag_value[1]}"
|
|
101
|
+
end
|
|
102
|
+
options[:tag_name] = tag
|
|
103
|
+
options[:tag_value] = value
|
|
104
|
+
end
|
|
105
|
+
opts.on("-r", "--rspec-args \"RSPEC_ARGS\"", "Additional arguments to be passed to rspec (must be surrounded with double quotes)") do |v|
|
|
106
|
+
$log.debug "rspec arguments specified... value: #{v}"
|
|
107
|
+
options[:rspec_args] = v.gsub(/"/,'').split(' ') # create an array of each argument
|
|
108
|
+
end
|
|
109
|
+
opts.on("--test-mode", "Do everything except actually starting the test threads") do
|
|
110
|
+
$log.debug "test mode specified... threads will NOT be started."
|
|
111
|
+
options[:test_mode] = true
|
|
112
|
+
end
|
|
113
|
+
opts.on("-q", "--quiet", "Quiet mode. Do not display parallel thread output") do
|
|
114
|
+
$log.debug "quiet mode specified... thread output will not be displayed"
|
|
115
|
+
options[:quiet_mode] = true
|
|
116
|
+
end
|
|
117
|
+
opts.on("-h", "--help", "Display a help message") do
|
|
118
|
+
$log.debug "help message requested..."
|
|
119
|
+
options[:help] = true
|
|
120
|
+
puts opts
|
|
121
|
+
end
|
|
122
|
+
opts.on("--ignore-pending", "Ignore all pending tests") do
|
|
123
|
+
$log.debug "ignore pending specified... all pending tests will be excluded"
|
|
124
|
+
options[:ignore_pending] = true
|
|
125
|
+
end
|
|
126
|
+
opts.on("-s", "--serialize-output", "Wait for each thread to complete and then output to STDOUT serially") do
|
|
127
|
+
$log.debug "serialize output specified... all threads will output to STDOUT as they complete"
|
|
128
|
+
options[:serialize] = true
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# handle invalid options
|
|
133
|
+
begin
|
|
134
|
+
o.parse! args
|
|
135
|
+
rescue OptionParser::InvalidOption => e
|
|
136
|
+
$log.error e
|
|
137
|
+
puts o
|
|
138
|
+
exit 1
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
return options
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def get_number_of_processors
|
|
145
|
+
count = Parallel.processor_count
|
|
146
|
+
return count
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def self.get_number_of_running_threads
|
|
150
|
+
prspec_info = YAML.load_file(INFO_FILE)
|
|
151
|
+
return prspec_info[:running_threads].to_i
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def get_spec_tests(options)
|
|
155
|
+
files = get_spec_files(options)
|
|
156
|
+
tests = get_tests_from_files(files, options)
|
|
157
|
+
$log.debug "Found #{tests.length} tests in #{files.length} files"
|
|
158
|
+
return tests
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def get_spec_files(options)
|
|
162
|
+
base_dir = options[:dir]
|
|
163
|
+
path = options[:path]
|
|
164
|
+
if (path.nil? || path == '')
|
|
165
|
+
path = '.'
|
|
166
|
+
end
|
|
167
|
+
full_path = ""
|
|
168
|
+
if (path.end_with?('.rb'))
|
|
169
|
+
full_path = File.join(base_dir, path.to_s)
|
|
170
|
+
else
|
|
171
|
+
full_path = File.join(base_dir, path.to_s, '**', "*#{SPEC_FILE_FILTER}")
|
|
172
|
+
end
|
|
173
|
+
$log.debug "full_path: #{full_path}"
|
|
174
|
+
|
|
175
|
+
files = []
|
|
176
|
+
if (options[:excludes].nil?)
|
|
177
|
+
files = Dir.glob(full_path)
|
|
178
|
+
else
|
|
179
|
+
files = Dir.glob(full_path).reject { |f| f[options[:excludes]] }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
return files
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def get_tests_from_files(files, options)
|
|
186
|
+
tests = []
|
|
187
|
+
match_test_name_format = /^[\s]*(it)[\s]*(')[\s\S]*(')[\s\S]*(do|{)/
|
|
188
|
+
files.each do |file|
|
|
189
|
+
lines = File.readlines(file)
|
|
190
|
+
for i in 0..lines.length-1
|
|
191
|
+
if lines[i] =~ match_test_name_format
|
|
192
|
+
m = lines[i]
|
|
193
|
+
match = true
|
|
194
|
+
if (options[:tag_name] != '')
|
|
195
|
+
if (m.rindex(options[:tag_name]).nil? || (m.rindex(options[:tag_value]) <= m.rindex(options[:tag_name])))
|
|
196
|
+
match = false
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
# if ignore_pending specified then skip tests containing 'pending' on next line
|
|
200
|
+
if (options[:ignore_pending])
|
|
201
|
+
if (i+1 < lines.length-1 && lines[i+1].include?('pending'))
|
|
202
|
+
match = false
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
if (match)
|
|
206
|
+
description = get_test_description(m)
|
|
207
|
+
tests.push("\"#{description}\"")
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
return tests
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def get_test_description(description_line)
|
|
217
|
+
# get index of first "'"
|
|
218
|
+
trim_front = description_line.sub(/[\s]*(it)[\s]*(')/,'')
|
|
219
|
+
description = trim_front.sub(/(')[\s]*(,[\s\S]*)*(do|{)[\s\S]*/,'')
|
|
220
|
+
return description.gsub(/["]/,'\"').gsub(/\\'/,"'")
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def divide_spec_tests(tests)
|
|
224
|
+
if (tests.length < @num_threads)
|
|
225
|
+
@num_threads = tests.length
|
|
226
|
+
$log.info "reducing number of threads due to low number of spec tests found: Threads = #{@num_threads}"
|
|
227
|
+
end
|
|
228
|
+
spec_arrays = Array.new(@num_threads)
|
|
229
|
+
num_per_thread = tests.length.fdiv(@num_threads).ceil
|
|
230
|
+
$log.debug "Approximate number of tests per thread: #{num_per_thread}"
|
|
231
|
+
# ensure an even distribution
|
|
232
|
+
i = 0
|
|
233
|
+
tests.each do |tname|
|
|
234
|
+
if (i >= @num_threads)
|
|
235
|
+
i = 0
|
|
236
|
+
end
|
|
237
|
+
if (spec_arrays[i].nil?)
|
|
238
|
+
spec_arrays[i] = []
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
spec_arrays[i].push(tname)
|
|
242
|
+
|
|
243
|
+
i+=1
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
return spec_arrays
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def build_process_array(process_tests, opts)
|
|
250
|
+
processes = []
|
|
251
|
+
for i in 0..@num_threads-1
|
|
252
|
+
processes[i] = PRSpecThread.new(i, process_tests[i], {'TEST_ENV_NUMBER'=>i, 'HOME'=>nil}, opts)
|
|
253
|
+
end
|
|
254
|
+
return processes
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def begin_run(processes, options)
|
|
258
|
+
if (!processes.nil? && processes.length > 0)
|
|
259
|
+
$log.info "Starting all Child Processes..."
|
|
260
|
+
update_running_thread_count(processes.length)
|
|
261
|
+
processes.each do |proc|
|
|
262
|
+
if (proc.is_a?(PRSpecThread) && options.is_a?(Hash))
|
|
263
|
+
proc.start unless options[:test_mode]
|
|
264
|
+
else
|
|
265
|
+
raise "Invalid datatype where PRSpecThread or Hash exepcted. Found: #{proc.class.to_s}, #{options.class.to_s}"
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
$log.info "All processes started..."
|
|
269
|
+
while processes.length > 0
|
|
270
|
+
processes.each do |proc|
|
|
271
|
+
if (!proc.done?) # confirm threads are running
|
|
272
|
+
$log.debug "Thread#{proc.id}: alive..."
|
|
273
|
+
else
|
|
274
|
+
$log.debug "Thread#{proc.id}: done."
|
|
275
|
+
if (options[:serialize])
|
|
276
|
+
puts proc.output unless options[:quiet_mode]
|
|
277
|
+
end
|
|
278
|
+
# collect thread output if in quiet mode
|
|
279
|
+
if (options[:quiet_mode])
|
|
280
|
+
@output << proc.output
|
|
281
|
+
end
|
|
282
|
+
processes.delete(proc) # remove from the array of processes so we don't count it again
|
|
283
|
+
update_running_thread_count(processes.length)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
sleep 0.5 # wait half a second for processes to run and then re-check their status
|
|
287
|
+
end
|
|
288
|
+
$log.info "All processes complete."
|
|
289
|
+
else
|
|
290
|
+
raise "Invalid input passed to method: 'processes' must be a valid Array of PRSpecThread objects"
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def update_running_thread_count(count)
|
|
295
|
+
(file = File.new(INFO_FILE,'w')).flock(File::LOCK_EX)
|
|
296
|
+
yml = { :running_threads => count }
|
|
297
|
+
file.write yml.to_yaml
|
|
298
|
+
ensure
|
|
299
|
+
file.flock(File::LOCK_UN)
|
|
300
|
+
file.close
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def running?
|
|
304
|
+
@processes.each do |proc|
|
|
305
|
+
if (!proc.done?)
|
|
306
|
+
$log.debug "Found running process..."
|
|
307
|
+
return true
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
return false
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def close
|
|
314
|
+
@processes.each do |proc|
|
|
315
|
+
proc.close
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def self.is_windows?
|
|
320
|
+
return (RUBY_PLATFORM.match(/mingw/i)) ? true : false
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
class PRSpecThread
|
|
325
|
+
attr_accessor :thread, :id, :tests, :env, :args, :output
|
|
326
|
+
def initialize(id, tests, environment, args)
|
|
327
|
+
@id = id
|
|
328
|
+
@tests = tests
|
|
329
|
+
@env = environment
|
|
330
|
+
@args = args
|
|
331
|
+
@output = ''
|
|
332
|
+
@out = Tempfile.new("prspec-t-#{@id}.out").path
|
|
333
|
+
$log.debug("Thread#{@id} @out file: #{@out}")
|
|
334
|
+
@err = Tempfile.new("prspec-t-#{@id}.err").path
|
|
335
|
+
$log.debug("Thread#{@id} @err file: #{@err}")
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def start
|
|
339
|
+
filter_str = @tests.join(" -e ")
|
|
340
|
+
filter_str = "-e " + filter_str
|
|
341
|
+
exports = get_exports
|
|
342
|
+
rspec_args = @args[:rspec_args].join(' ')
|
|
343
|
+
cmd = "#{exports}rspec #{@args[:path]} #{filter_str} #{rspec_args}"
|
|
344
|
+
$log.debug "Starting child process for thread#{@id}: #{cmd}"
|
|
345
|
+
@thread = Thread.new do
|
|
346
|
+
Thread.current[:id] = @id
|
|
347
|
+
Dir::chdir @args[:dir] do # change directory for process execution
|
|
348
|
+
begin
|
|
349
|
+
pid = nil
|
|
350
|
+
if (@args[:quiet_mode] || @args[:serialize])
|
|
351
|
+
pid = Process.spawn(cmd, :out=>@out, :err=>@err) # capture both sdtout and stderr
|
|
352
|
+
else
|
|
353
|
+
pid = Process.spawn(cmd)
|
|
354
|
+
end
|
|
355
|
+
Process.wait(pid)
|
|
356
|
+
@output = File.readlines(@out).join("\n") if (@args[:quiet_mode] || @args[:serialize])
|
|
357
|
+
rescue
|
|
358
|
+
if (@args[:quiet_mode] || @args[:serialize])
|
|
359
|
+
error = "ErrorCode: #{$?.errorcode}; ErrorOutput: "+File.readlines(@err).join("\n")
|
|
360
|
+
$log.error "Something bad happened while executing thread#{@id}: #{error}"
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
close
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def done?
|
|
369
|
+
return !@thread.alive?
|
|
370
|
+
rescue
|
|
371
|
+
return true # if there's a problem with @thread we're done
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def close
|
|
375
|
+
@thread.sleep
|
|
376
|
+
@thread.kill
|
|
377
|
+
@thread = nil
|
|
378
|
+
FileUtils.remove_file(@out, :force => true) unless !File.exist?(@out)
|
|
379
|
+
FileUtils.remove_file(@err, :force => true) unless !File.exist?(@err)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def get_exports
|
|
383
|
+
separator = PRSpec.is_windows? ? ' & ' : ';'
|
|
384
|
+
exports = @env.map do |k,v|
|
|
385
|
+
if PRSpec.is_windows?
|
|
386
|
+
"(SET \"#{k}=#{v}\")"
|
|
387
|
+
else
|
|
388
|
+
"export #{k}=#{v}"
|
|
389
|
+
end
|
|
390
|
+
end.join(separator)
|
|
391
|
+
|
|
392
|
+
return exports+separator
|
|
393
|
+
end
|
|
394
394
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: prspec
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.6
|
|
5
5
|
prerelease:
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2014-
|
|
12
|
+
date: 2014-06-05 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: log4r
|