actir 1.0.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 +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +11 -0
- data/README.md +56 -0
- data/Rakefile +70 -0
- data/actir.gemspec +31 -0
- data/bin/actir +9 -0
- data/bin/ants +9 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/actir.rb +41 -0
- data/lib/actir/basic_page.rb +49 -0
- data/lib/actir/config.rb +192 -0
- data/lib/actir/initializer.rb +70 -0
- data/lib/actir/parallel_tests/cli.rb +323 -0
- data/lib/actir/parallel_tests/grouper.rb +44 -0
- data/lib/actir/parallel_tests/parallel_tests.rb +77 -0
- data/lib/actir/parallel_tests/report/html_formatter.rb +417 -0
- data/lib/actir/parallel_tests/report/html_reporter.rb +145 -0
- data/lib/actir/parallel_tests/test/logger.rb +74 -0
- data/lib/actir/parallel_tests/test/re_run.rb +153 -0
- data/lib/actir/parallel_tests/test/runner.rb +224 -0
- data/lib/actir/remote.rb +80 -0
- data/lib/actir/script/cookies_baidu.rb +143 -0
- data/lib/actir/version.rb +3 -0
- data/lib/actir/webdriver/browser.rb +201 -0
- data/lib/actir/webdriver/browser_options.rb +54 -0
- data/lib/actir/webdriver/config/devices.yaml +40 -0
- data/lib/actir/webdriver/devices.rb +31 -0
- data/lib/actir/webdriver/driver.rb +42 -0
- metadata +175 -0
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'actir/parallel_tests/test/runner'
|
2
|
+
|
3
|
+
module Actir
|
4
|
+
module ParallelTests
|
5
|
+
module Test
|
6
|
+
class Logger
|
7
|
+
|
8
|
+
#为每个进程准备一个变量表示是否需要初始化log文件,暂时先定10个
|
9
|
+
@@prepared = []
|
10
|
+
#不知道为什么代码中export ENV无效,暂时先用10
|
11
|
+
#num = ENV["PARALLEL_TEST_GROUPS"].to_i
|
12
|
+
for i in 1..10
|
13
|
+
@@prepared << false
|
14
|
+
end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
|
18
|
+
def log(result, process_index)
|
19
|
+
#获取执行环境的当前进程号以及总进程数目
|
20
|
+
#process_index = env["TEST_ENV_NUMBER"]
|
21
|
+
#num_process = env["PARALLEL_TEST_GROUPS"]
|
22
|
+
prepare(process_index)
|
23
|
+
|
24
|
+
lock(process_index) do
|
25
|
+
File.open(logfile(process_index), 'a') { |f| f.puts result }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# 打印每个进程的log文件内容到屏幕上
|
30
|
+
def show_log(process_index)
|
31
|
+
separator = "\n"
|
32
|
+
File.read(logfile(process_index)).split(separator).map do |line|
|
33
|
+
if line == ""
|
34
|
+
puts line
|
35
|
+
else
|
36
|
+
puts "[process_" + process_index.to_s + "] - " + line
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# ensure folder exists + clean out previous log
|
44
|
+
# this will happen in multiple processes, but should be roughly at the same time
|
45
|
+
# so there should be no log message lost
|
46
|
+
def prepare(process_index)
|
47
|
+
return if @@prepared[process_index]
|
48
|
+
@@prepared[process_index] = true
|
49
|
+
FileUtils.mkdir_p(File.dirname(logfile(process_index)))
|
50
|
+
File.write(logfile(process_index), '')
|
51
|
+
end
|
52
|
+
|
53
|
+
def lock(process_index)
|
54
|
+
File.open(logfile(process_index), 'r') do |f|
|
55
|
+
begin
|
56
|
+
f.flock File::LOCK_EX
|
57
|
+
yield
|
58
|
+
ensure
|
59
|
+
f.flock File::LOCK_UN
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def logfile(process_index)
|
65
|
+
"tmp/parallel_test_p_#{process_index}.log"
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module Actir
|
2
|
+
module ParallelTests
|
3
|
+
module Test
|
4
|
+
class Rerun < Runner
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
#
|
9
|
+
# 重新执行失败的测试用例
|
10
|
+
#
|
11
|
+
# 用例执行通过或者达到了重试次数上限后即返回最终的执行结果
|
12
|
+
#
|
13
|
+
# 限制:不管是否在同一文件中,用例名称不能重复,即用例名称全局唯一
|
14
|
+
#
|
15
|
+
# @example : re_run_tests('店铺名称')
|
16
|
+
#
|
17
|
+
# @param test_result : [String] 商品名称的字符串
|
18
|
+
#
|
19
|
+
# num_processes : [Fixnum] 并发进程数
|
20
|
+
#
|
21
|
+
# address : [String] 执行用例的环境的地址
|
22
|
+
#
|
23
|
+
# times : [Fixnum] 执行次数
|
24
|
+
#
|
25
|
+
# process_number : [Fixnum] 暂时无用
|
26
|
+
#
|
27
|
+
# @return [String] 执行结果字符串
|
28
|
+
#
|
29
|
+
def re_run_tests(test_result, process_number, num_processes, options, address, times)
|
30
|
+
#根据重跑次数重新执行失败用例
|
31
|
+
result = re_run(test_result, process_number, num_processes, options, address, times)
|
32
|
+
#从老的执行结果输出中提取出相关数据
|
33
|
+
old_result = summarize_results(find_results(test_result[:stdout]))
|
34
|
+
#puts "old_result : " + old_result
|
35
|
+
#从新的执行结果中提取出数据,并算出总数
|
36
|
+
#因为若有多个失败用例就有多个执行结果
|
37
|
+
new_result = summarize_results(find_results(result[:stdout]))
|
38
|
+
#puts "new_result : " + new_result
|
39
|
+
#刷新最终的执行结果
|
40
|
+
if old_result == nil || old_result == ""
|
41
|
+
puts "[Debug] test_result : "
|
42
|
+
puts test_result
|
43
|
+
end
|
44
|
+
if new_result == nil || new_result == ""
|
45
|
+
puts "[Debug] result : "
|
46
|
+
puts result
|
47
|
+
end
|
48
|
+
combine_tests_results(old_result, new_result)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def re_run(test_result, process_number, num_processes, options, address, times)
|
54
|
+
result = {}
|
55
|
+
if times > 0
|
56
|
+
#先获取失败用例信息
|
57
|
+
tests = capture_failures_tests(test_result)
|
58
|
+
cmd = ""
|
59
|
+
tests.each do |testcase, testfile|
|
60
|
+
#输出一些打印信息
|
61
|
+
puts "[ Re_Run ] - [ #{testfile} -n #{testcase} ] - Left #{times-1} times - in Process[#{process_number}]"
|
62
|
+
cmd += "#{executable} #{testfile} #{address} -n #{testcase};"
|
63
|
+
end
|
64
|
+
#执行cmd,获取执行结果输出
|
65
|
+
result = execute_command(cmd, process_number, num_processes, options)
|
66
|
+
#先判断是否还是失败,且未满足重试次数
|
67
|
+
times -= 1
|
68
|
+
if any_test_failed?(result) && times > 0
|
69
|
+
#递归
|
70
|
+
result = re_run(result, process_number, num_processes, options, address, times)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
#记录log
|
74
|
+
if options[:log]
|
75
|
+
log_str = "[re_run_tests]: \n" + result[:stdout]
|
76
|
+
Actir::ParallelTests::Test::Logger.log(log_str, process_number)
|
77
|
+
end
|
78
|
+
return result
|
79
|
+
end
|
80
|
+
|
81
|
+
#从输出内容中获取失败用例文件名以及用例名称
|
82
|
+
def capture_failures_tests(test_result)
|
83
|
+
result_array = test_result[:stdout].split("\n")
|
84
|
+
failure_tests_hash = {}
|
85
|
+
testcase = ""
|
86
|
+
testfile = ""
|
87
|
+
result_array.each do |result|
|
88
|
+
#取出执行失败的用例文件名称和用例名称
|
89
|
+
case result
|
90
|
+
when failure_tests_name_reg
|
91
|
+
#范例:"testhehe(TestHehe)"
|
92
|
+
testcase = $1
|
93
|
+
when failure_tests_file_reg
|
94
|
+
#范例:"testcode/test_tt/test_hehe.rb:8:in `xxxx'"
|
95
|
+
testfile = $1
|
96
|
+
end
|
97
|
+
#至于为什么采用testcase => testfile的形式是因为…文件名会重复
|
98
|
+
if testcase != "" && testfile != ""
|
99
|
+
failure_tests_hash[testcase] = testfile
|
100
|
+
testcase = ""
|
101
|
+
testfile = ""
|
102
|
+
end
|
103
|
+
end
|
104
|
+
failure_tests_hash
|
105
|
+
end
|
106
|
+
|
107
|
+
#组合出最新的执行结果
|
108
|
+
#只需要将老结果中的failure和error的数据替换成新结果中的数据即可
|
109
|
+
def combine_tests_results(old_result, new_result)
|
110
|
+
if old_result == nil || old_result == ""
|
111
|
+
puts "new_result : " + new_result
|
112
|
+
raise "old_result is nil"
|
113
|
+
end
|
114
|
+
#取出新结果中的failure和error的数据
|
115
|
+
new_result =~ failure_error_reg
|
116
|
+
failure_error_str = $1
|
117
|
+
failure_data = $2
|
118
|
+
error_data = $3
|
119
|
+
#替换老结果中的失败数据
|
120
|
+
comb_result = old_result.gsub(failure_error_reg, failure_error_str)
|
121
|
+
#按照{:stdout => '', :exit_status => 0}的格式输出内容,不然原有代码不兼容
|
122
|
+
#其中exit_status = 0 表示用例全部执行成功,反之则有失败
|
123
|
+
exitstatus = ( (failure_data.to_i + error_data.to_i) == 0 ) ? 0 : 1
|
124
|
+
{:stdout => comb_result + "\n", :exit_status => exitstatus}
|
125
|
+
end
|
126
|
+
|
127
|
+
#判断是否有用例失败
|
128
|
+
def any_test_failed?(result)
|
129
|
+
result[:exit_status] != 0
|
130
|
+
end
|
131
|
+
|
132
|
+
#获取失败用例文件名的正则
|
133
|
+
def failure_tests_file_reg
|
134
|
+
/(.+\/test.+rb):\d+:in\s`.+'/
|
135
|
+
#/^Loaded\ssuite\s(.+)/
|
136
|
+
end
|
137
|
+
|
138
|
+
#获取失败用例名的正则
|
139
|
+
def failure_tests_name_reg
|
140
|
+
/(test.+)\(.+\)/
|
141
|
+
end
|
142
|
+
|
143
|
+
#获取失败数据的正则
|
144
|
+
def failure_error_reg
|
145
|
+
/((\d+)\sfailure.*,\s(\d+)\serror)/
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
module Actir
|
2
|
+
module ParallelTests
|
3
|
+
module Test
|
4
|
+
class Runner
|
5
|
+
NAME = 'Test'
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# --- usually overwritten by other runners
|
9
|
+
|
10
|
+
def name
|
11
|
+
NAME
|
12
|
+
end
|
13
|
+
|
14
|
+
# modify by Hub
|
15
|
+
# 修改正则表达式使得使用我们目前的测试脚本文件命名 : test_xxx.rb
|
16
|
+
def test_suffix
|
17
|
+
# /_(test|spec).rb$/
|
18
|
+
/test.*\.rb$/
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_file_name
|
22
|
+
"test"
|
23
|
+
end
|
24
|
+
|
25
|
+
# modify by Hub
|
26
|
+
# add param address to ruby script as ARGV[0]
|
27
|
+
# modify cmd to exec ruby test script
|
28
|
+
def run_tests(test_files, process_number, num_processes, options, address)
|
29
|
+
#require_list = test_files.map { |file| file.sub(" ", "\\ ") }.join(" ")
|
30
|
+
#cmd = "#{executable} -Itest -e '%w[#{require_list}].each { |f| require %{./\#{f}}}' #{address}"
|
31
|
+
#execute_command(cmd, process_number, num_processes, options)
|
32
|
+
cmd = ""
|
33
|
+
test_files.each do |file|
|
34
|
+
cmd += "#{executable} #{file} #{address};"
|
35
|
+
end
|
36
|
+
cmd += "\n"
|
37
|
+
result = execute_command(cmd, process_number, num_processes, options)
|
38
|
+
#记录log
|
39
|
+
if options[:log]
|
40
|
+
log_str = "[run_tests]: \n" + result[:stdout]
|
41
|
+
Actir::ParallelTests::Test::Logger.log(log_str, process_number)
|
42
|
+
end
|
43
|
+
result
|
44
|
+
end
|
45
|
+
|
46
|
+
def line_is_result?(line)
|
47
|
+
line.gsub!(/[.F*]/,'')
|
48
|
+
line =~ /\d+ failure/
|
49
|
+
end
|
50
|
+
|
51
|
+
# --- usually used by other runners
|
52
|
+
|
53
|
+
# finds all tests and partitions them into groups
|
54
|
+
def tests_in_groups(tests, num_groups, options={})
|
55
|
+
tests = find_tests(tests, options)
|
56
|
+
|
57
|
+
case options[:group_by]
|
58
|
+
when :found
|
59
|
+
tests.map! { |t| [t, 1] }
|
60
|
+
when :filesize
|
61
|
+
sort_by_filesize(tests)
|
62
|
+
when nil
|
63
|
+
sort_by_filesize(tests)
|
64
|
+
else
|
65
|
+
raise ArgumentError, "Unsupported option #{options[:group_by]}"
|
66
|
+
end
|
67
|
+
|
68
|
+
Grouper.in_even_groups_by_size(tests, num_groups, options)
|
69
|
+
end
|
70
|
+
|
71
|
+
def execute_command(cmd, process_number, num_processes, options)
|
72
|
+
env = (options[:env] || {}).merge(
|
73
|
+
#"TEST_ENV_NUMBER" => test_env_number(process_number),
|
74
|
+
"TEST_ENV_NUMBER" => process_number,
|
75
|
+
"PARALLEL_TEST_GROUPS" => num_processes
|
76
|
+
)
|
77
|
+
cmd = "nice #{cmd}" if options[:nice]
|
78
|
+
cmd = "#{cmd} 2>&1" if options[:combine_stderr]
|
79
|
+
puts cmd if options[:verbose]
|
80
|
+
|
81
|
+
execute_command_and_capture_output(env, cmd, options[:serialize_stdout])
|
82
|
+
end
|
83
|
+
|
84
|
+
def execute_command_and_capture_output(env, cmd, silence)
|
85
|
+
# make processes descriptive / visible in ps -ef
|
86
|
+
separator = ';'
|
87
|
+
exports = env.map do |k,v|
|
88
|
+
"export #{k}=#{v}"
|
89
|
+
end.join(separator)
|
90
|
+
cmd = "#{exports}#{separator}#{cmd}"
|
91
|
+
output = open("|#{cmd}", "r") { |output| capture_output(output, silence) }
|
92
|
+
|
93
|
+
#modify by Hub
|
94
|
+
#exitstatus = $?.exitstatus
|
95
|
+
#"$?.exitstatus" 返回的值有时有问题,不能明确标示用例执行结果是否成功
|
96
|
+
#改成判断结果数据中是否有failure和error
|
97
|
+
exitstatus = get_test_failed_num(find_results(output).join)
|
98
|
+
{:stdout => output, :exit_status => exitstatus}
|
99
|
+
end
|
100
|
+
|
101
|
+
def find_results(test_output)
|
102
|
+
test_output.split("\n").map {|line|
|
103
|
+
line.gsub!(/\e\[\d+m/,'')
|
104
|
+
next unless line_is_result?(line)
|
105
|
+
line
|
106
|
+
}.compact
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_env_number(process_number)
|
110
|
+
process_number == 0 ? '' : process_number + 1
|
111
|
+
end
|
112
|
+
|
113
|
+
def summarize_results(results)
|
114
|
+
sums = sum_up_results(results)
|
115
|
+
sums.to_a.map{|word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
|
116
|
+
#sums.sort.map{|word, number| "#{number} #{word}#{'s' if number != 1}" }.join(', ')
|
117
|
+
end
|
118
|
+
|
119
|
+
protected
|
120
|
+
|
121
|
+
def executable
|
122
|
+
ENV['PARALLEL_TESTS_EXECUTABLE'] || determine_executable
|
123
|
+
end
|
124
|
+
|
125
|
+
def determine_executable
|
126
|
+
if Actir::Remote.is_local?
|
127
|
+
"ruby"
|
128
|
+
else
|
129
|
+
#TO-DO jenkins服务器上的ruby是用rvm管理的,这里有个坑,在jenkins中调用ruby命令会报找不到
|
130
|
+
#后续考虑更换rvm至rbenv
|
131
|
+
#jenkins服务器上的ruby所在地址
|
132
|
+
"/usr/local/rvm/rubies/ruby-2.0.0-p598/bin/ruby"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
#
|
137
|
+
# 通过结果判断是否有用例失败
|
138
|
+
# 返回失败用例的数目
|
139
|
+
#
|
140
|
+
def get_test_failed_num(result)
|
141
|
+
#获取结果字符串中的failure和error用例数
|
142
|
+
failed_num = 0
|
143
|
+
result.scan(/(\d+)\s(failure|error)/).each do |failed|
|
144
|
+
failed_num += failed[0].to_i
|
145
|
+
end
|
146
|
+
failed_num
|
147
|
+
end
|
148
|
+
|
149
|
+
def sum_up_results(results)
|
150
|
+
results = results.join(' ').gsub(/s\b/,'') # combine and singularize results
|
151
|
+
#results = results.join(' ')
|
152
|
+
counts = results.scan(/(\d+) (\w+)/)
|
153
|
+
counts.inject(Hash.new(0)) do |sum, (number, word)|
|
154
|
+
sum[word] += number.to_i
|
155
|
+
sum
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# read output of the process and print it in chunks
|
160
|
+
def capture_output(out, silence)
|
161
|
+
result = ""
|
162
|
+
loop do
|
163
|
+
begin
|
164
|
+
read = out.readpartial(1000000) # read whatever chunk we can get
|
165
|
+
if Encoding.default_internal
|
166
|
+
read = read.force_encoding(Encoding.default_internal)
|
167
|
+
end
|
168
|
+
result << read
|
169
|
+
unless silence
|
170
|
+
$stdout.print read
|
171
|
+
$stdout.flush
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end rescue EOFError
|
175
|
+
result
|
176
|
+
end
|
177
|
+
|
178
|
+
def sort_by_filesize(tests)
|
179
|
+
tests.sort!
|
180
|
+
tests.map! { |test| [test, File.stat(test).size] }
|
181
|
+
end
|
182
|
+
|
183
|
+
# modify by Hub
|
184
|
+
# 由原来的包含路径的文件名直接进行正则匹配改为取出文件的文件名进行匹配,更准确,不受文件夹命名的影响
|
185
|
+
def find_tests(tests, options = {})
|
186
|
+
(tests || []).map do |file_or_folder|
|
187
|
+
if File.directory?(file_or_folder)
|
188
|
+
#取出文件和文件夹名字
|
189
|
+
files_and_folder = files_in_folder(file_or_folder, options)
|
190
|
+
#去掉文件夹名字
|
191
|
+
files = files_and_folder.grep(test_suffix)
|
192
|
+
#去掉不以test开头的测试脚本
|
193
|
+
files_2_delete = Array.new
|
194
|
+
files.each do |file|
|
195
|
+
file_name = File.basename(file)
|
196
|
+
#不能在遍历数组时进行delete操作
|
197
|
+
#记录要删除的元素名称
|
198
|
+
files_2_delete << file unless file_name =~ /^test.*\.rb$/
|
199
|
+
#files.delete(file) unless file_name =~ /^test.*\.rb$/
|
200
|
+
end
|
201
|
+
files_2_delete.each { |file_2_delete| files.delete(file_2_delete) }
|
202
|
+
files
|
203
|
+
else
|
204
|
+
file_or_folder
|
205
|
+
end
|
206
|
+
end.flatten.uniq
|
207
|
+
end
|
208
|
+
|
209
|
+
# modify by Hub
|
210
|
+
# Bug Fix
|
211
|
+
# lack of method 'glob'
|
212
|
+
def files_in_folder(folder, options={})
|
213
|
+
pattern = "**{,/*/**}/*"
|
214
|
+
# modify by Hub
|
215
|
+
# add method glod : Dir.glob
|
216
|
+
#Dir[File.join(folder, pattern)].uniq
|
217
|
+
Dir.glob(File.join(folder, pattern)).uniq
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|