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.
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: