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