actir 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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