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.
- data/lib/color_helper.rb +20 -0
- data/lib/rspec_parallel.rb +315 -0
- metadata +62 -0
data/lib/color_helper.rb
ADDED
@@ -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: []
|