rspec_parallel 0.1.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 (3) hide show
  1. data/lib/color_helper.rb +20 -0
  2. data/lib/rspec_parallel.rb +315 -0
  3. metadata +62 -0
@@ -0,0 +1,20 @@
1
+ # Displaying text in color
2
+ module ColorHelpers
3
+
4
+ def red(text)
5
+ "\e[31m#{text}\e[0m"
6
+ end
7
+
8
+ def green(text)
9
+ "\e[32m#{text}\e[0m"
10
+ end
11
+
12
+ def yellow(text)
13
+ "\e[33m#{text}\e[0m"
14
+ end
15
+
16
+ def cyan(text)
17
+ "\e[36m#{text}\e[0m"
18
+ end
19
+
20
+ end
@@ -0,0 +1,315 @@
1
+ #encoding: UTF-8
2
+ $LOAD_PATH << File.dirname(__FILE__)
3
+ require 'progressbar'
4
+ require 'color_helper'
5
+ require 'thread'
6
+ include ColorHelpers
7
+
8
+ class Rspec_parallel
9
+ def initialize(thread_number, case_folder, filter_options = {}, env_list = [], show_pending = false)
10
+ @thread_number = thread_number
11
+ if thread_number < 1
12
+ puts red("threads_number can't be less than 1")
13
+ return
14
+ end
15
+ @case_folder = case_folder
16
+ @filter_options = filter_options
17
+ @env_list = env_list
18
+ @show_pending = show_pending
19
+ @queue = Queue.new
20
+ end
21
+
22
+ def run_tests()
23
+ # use lock to avoid output mess up
24
+ @lock = Mutex.new
25
+ # timer of rspec task
26
+ start_time = Time.now
27
+
28
+ puts yellow("threads number: #{@thread_number}\n")
29
+ parse_case_list
30
+
31
+ pbar = ProgressBar.new("0/#{@queue.size}", @queue.size, $stdout)
32
+ pbar.format_arguments = [:title, :percentage, :bar, :stat]
33
+ case_number = 0
34
+ failure_number = 0
35
+ pending_number = 0
36
+ failure_list = []
37
+ pending_list = []
38
+
39
+ Thread.abort_on_exception = false
40
+ threads = []
41
+
42
+ @thread_number.times do |i|
43
+ threads << Thread.new do
44
+ until @queue.empty?
45
+ task = @queue.pop
46
+ env_extras = {}
47
+ if @env_list && @env_list[i]
48
+ env_extras = @env_list[i]
49
+ end
50
+ task_output = run_task(task, env_extras)
51
+
52
+ if task_output =~ /Failures/ || task_output =~ /0 examples/
53
+ @lock.synchronize do
54
+ failure_number += 1
55
+ failure_log = parse_failure_log(task_output)
56
+ failure_list << [task, failure_log]
57
+
58
+ # print failure immediately during the execution
59
+ $stdout.print "\e[K"
60
+ if failure_number == 1
61
+ puts "Failures:"
62
+ end
63
+ puts " #{failure_number}) #{failure_log}\n"
64
+ puts red(" (Failure time: #{Time.now})\n\n")
65
+ end
66
+ elsif task_output =~ /Pending/
67
+ @lock.synchronize do
68
+ pending_number += 1
69
+ pending_list << [task, parse_pending_log(task_output)]
70
+ end
71
+ end
72
+ @lock.synchronize do
73
+ case_number += 1
74
+ pbar.inc
75
+ pbar.instance_variable_set("@title", "#{pbar.current + 1}/#{pbar.total}")
76
+ end
77
+ end
78
+ end
79
+ # ramp up user threads one by one
80
+ sleep 0.1
81
+ end
82
+
83
+ threads.each { |t| t.join }
84
+ pbar.finish
85
+
86
+ # print pending cases if configured
87
+ $stdout.print "\n"
88
+ if @show_pending && pending_number > 0
89
+ puts "Pending:"
90
+ pending_list.each {|p|
91
+ puts " #{p[1]}\n"
92
+ }
93
+ end
94
+ $stdout.print "\n"
95
+ t2 = Time.now
96
+ puts green("Finished in #{format_time(t2-start_time)}\n")
97
+ if failure_number > 0
98
+ $stdout.print red("#{case_number} examples, #{failure_number} failures")
99
+ $stdout.print red(", #{pending_number} pending") if pending_number > 0
100
+ else
101
+ $stdout.print yellow("#{case_number} examples, #{failure_number} failures")
102
+ $stdout.print yellow(", #{pending_number} pending") if pending_number > 0
103
+ end
104
+ $stdout.print "\n"
105
+
106
+ # record failed rspec examples to rerun.sh
107
+ unless failure_list.empty?
108
+ rerun_file = File.new('./rerun.sh', 'w', 0777)
109
+ $stdout.print "\nFailed examples:\n\n"
110
+ failure_list.each_with_index do |log, i|
111
+ case_desc = ''
112
+ log[1].each_line {|line|
113
+ case_desc = line
114
+ break
115
+ }
116
+
117
+ rerun_cmd = 'rspec .' + log[0].match(/\/spec\/.*_spec\.rb:\d{1,4}/).to_s
118
+ rerun_file.puts "echo ----#{case_desc}"
119
+ rerun_file.puts rerun_cmd + " # #{case_desc}"
120
+ $stdout.print red(rerun_cmd)
121
+ $stdout.print cyan(" # #{case_desc}")
122
+ end
123
+ rerun_file.close
124
+ $stdout.print "\n"
125
+ end
126
+ end
127
+
128
+ def get_case_list
129
+ file_list = `grep -rl '' #{@case_folder}`
130
+ case_list = []
131
+ file_list.each_line { |filename|
132
+ unless filename.include? "_spec.rb"
133
+ next
134
+ end
135
+ f = File.read(filename.strip).force_encoding("ISO-8859-1").encode("utf-8", replace: nil)
136
+
137
+ # try to get tags of describe level
138
+ describe_text = f.scan(/describe [\s\S]*? do/)[0]
139
+ describe_tags = []
140
+ temp = describe_text.scan(/[,\s]:(\w+)/)
141
+ unless temp == nil
142
+ temp.each do |t|
143
+ describe_tags << t[0]
144
+ end
145
+ end
146
+
147
+ # get cases of normal format: "it ... do"
148
+ cases = f.scan(/(it (["'])([\s\S]*?)\2[\s\S]*? do)/)
149
+ line_number = 0
150
+ if cases
151
+ cases.each { |c1|
152
+ c = c1[0]
153
+ tags = []
154
+ draft_tags = c.scan(/[,\s]:(\w+)/)
155
+ draft_tags.each { |tag|
156
+ tags << tag[0]
157
+ }
158
+ tags += describe_tags
159
+ tags.uniq
160
+
161
+ i = 0
162
+ cross_line = false
163
+ f.each_line { |line|
164
+ i += 1
165
+ if i <= line_number && line_number > 0
166
+ next
167
+ end
168
+ if line.include? c1[2]
169
+ if line.strip.end_with? " do"
170
+ case_hash = {"line" => "#{filename.strip}:#{i}", "tags" => tags}
171
+ case_list << case_hash
172
+ line_number = i
173
+ cross_line = false
174
+ break
175
+ else
176
+ cross_line = true
177
+ end
178
+ end
179
+ if cross_line && (line.strip.end_with? " do")
180
+ case_hash = {"line" => "#{filename.strip}:#{i}", "tags" => tags}
181
+ case_list << case_hash
182
+ line_number = i
183
+ cross_line = false
184
+ break
185
+ end
186
+ }
187
+ }
188
+ end
189
+
190
+ # get cases of another format: "it {...}"
191
+ cases = f.scan(/it \{[\s\S]*?\}/)
192
+ line_number = 0
193
+ if cases
194
+ cases.each { |c|
195
+ i = 0
196
+ f.each_line { |line|
197
+ i += 1
198
+ if i <= line_number && line_number > 0
199
+ next
200
+ end
201
+ if line.include? c
202
+ case_hash = {"line" => "#{filename.strip}:#{i}", "tags" => describe_tags}
203
+ case_list << case_hash
204
+ line_number = i
205
+ break
206
+ end
207
+ }
208
+ }
209
+ end
210
+ }
211
+ case_list
212
+ end
213
+
214
+ def parse_case_list()
215
+ all_case_list = get_case_list
216
+ pattern_filter_list = []
217
+ tags_filter_list = []
218
+
219
+ if @filter_options["pattern"]
220
+ all_case_list.each { |c|
221
+ if c["line"].match(@filter_options["pattern"])
222
+ pattern_filter_list << c
223
+ end
224
+ }
225
+ else
226
+ pattern_filter_list = all_case_list
227
+ end
228
+
229
+ if @filter_options["tags"]
230
+ include_tags = []
231
+ exclude_tags = []
232
+ all_tags = @filter_options["tags"].split(",")
233
+ all_tags.each { |tag|
234
+ if tag.start_with? "~"
235
+ exclude_tags << tag.gsub("~", "")
236
+ else
237
+ include_tags << tag
238
+ end
239
+ }
240
+ pattern_filter_list.each { |c|
241
+ if (include_tags.length == 0 || (c["tags"] - include_tags).length < c["tags"].length) &&
242
+ ((c["tags"] - exclude_tags).length == c["tags"].length)
243
+ tags_filter_list << c
244
+ end
245
+ }
246
+ else
247
+ tags_filter_list = pattern_filter_list
248
+ end
249
+
250
+ tags_filter_list.each { |t|
251
+ @queue << t["line"]
252
+ }
253
+ end
254
+
255
+ def run_task(task, env_extras)
256
+ cmd = [] # Preparing command for popen
257
+ cmd << ENV.to_hash.merge(env_extras)
258
+ cmd += ["bundle", "exec", "rspec", "--color", task]
259
+ cmd
260
+
261
+ output = ""
262
+ IO.popen(cmd, :err => [:child, :out]) do |io|
263
+ output << io.read
264
+ end
265
+
266
+ output
267
+ end
268
+
269
+ def format_time(t)
270
+ time_str = ''
271
+ time_str += (t / 3600).to_i.to_s + " hours " if t > 3600
272
+ time_str += (t % 3600 / 60).to_i.to_s + " minutes " if t > 60
273
+ time_str += (t % 60).to_f.round(2).to_s + " seconds"
274
+ time_str
275
+ end
276
+
277
+ def parse_failure_log(str)
278
+ return str if str =~ /0 examples/
279
+ index1 = str.index('1) ')
280
+ index2 = str.index('Finished in')
281
+ output = ""
282
+ temp = str.slice(index1+3..index2-1).strip
283
+ first_line = true
284
+ temp.each_line { |line|
285
+ if first_line
286
+ output += line
287
+ elsif line.strip.start_with? "# "
288
+ output += cyan(line)
289
+ else
290
+ output += red(line)
291
+ end
292
+ first_line = false
293
+ }
294
+ output
295
+ end
296
+
297
+ def parse_pending_log(str)
298
+ index1 = str.index('Pending:')
299
+ index2 = str.index('Finished in')
300
+ output = ""
301
+ temp = str.slice(index1+8..index2-1).strip
302
+ first_line = true
303
+ temp.each_line { |line|
304
+ if first_line
305
+ output += yellow(line)
306
+ elsif line.strip.start_with? "# "
307
+ output += cyan(line)
308
+ else
309
+ output += line
310
+ end
311
+ first_line = false
312
+ }
313
+ output
314
+ end
315
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec_parallel
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Michael Zhang
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: progressbar
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.11.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 0.11.0
30
+ description: parallel all rspec examples
31
+ email: zhchshy@hotmail.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - lib/rspec_parallel.rb
37
+ - lib/color_helper.rb
38
+ homepage: http://rubygems.org/gems/rspec_parallel
39
+ licenses: []
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ! '>='
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubyforge_project:
58
+ rubygems_version: 1.8.24
59
+ signing_key:
60
+ specification_version: 3
61
+ summary: rspec parallel
62
+ test_files: []