prspec 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +15 -0
  2. data/bin/prspec +6 -0
  3. data/lib/prspec.rb +364 -0
  4. 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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'prspec'
5
+
6
+ PRSpec.new(ARGV)
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: