actir 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,70 @@
1
+ require 'pathname'
2
+
3
+ module Actir
4
+ class Initializer
5
+
6
+ #config content
7
+ attr_accessor :config
8
+
9
+ def initialize project_path
10
+ $project_path ||= project_path
11
+ $:.unshift($project_path)
12
+ $config = load_config
13
+ load_elements
14
+ end
15
+
16
+ def load_elements
17
+ @elements_path = File.join($project_path, 'elements')
18
+ load_item
19
+ load_components
20
+ load_user
21
+ load_pages
22
+ end
23
+
24
+ def load_config
25
+ @config_path = File.join($project_path, 'config')
26
+ @config = {}
27
+ Dir.glob(File.join @config_path, '**', '*.yaml').select{ |c| c =~ /\.yaml$/ }.each do |config|
28
+ puts "#{config}" if $debug
29
+ #获取配置文件名字
30
+ config =~ /config\/(.*)\.yaml/
31
+ config_name = $1
32
+ @config.store(config_name, Actir::Config.get_content(config))
33
+ end
34
+ @config
35
+ end
36
+
37
+ def load_item
38
+ @item_path = File.join(@elements_path, 'item')
39
+ Dir.glob(File.join @item_path, '**', '*.rb').select {|p| p =~ /\.rb$/}.each do |i|
40
+ puts i if $debug
41
+ require "#{i}"
42
+ end
43
+ end
44
+
45
+ def load_user
46
+ @user_path = File.join(@elements_path, 'user')
47
+ Dir.glob(File.join @user_path, '**', '*.rb').select {|p| p =~ /\.rb$/}.each do |u|
48
+ puts u if $debug
49
+ require "#{u}"
50
+ end #each
51
+ end
52
+
53
+ def load_components
54
+ @components_path = File.join(@elements_path, 'components')
55
+ Dir.glob(File.join @components_path, '**', '*.rb').select {|p| p =~ /\.rb$/}.each do |c|
56
+ puts c if $debug
57
+ require "#{c}"
58
+ end #each
59
+ end
60
+
61
+ def load_pages
62
+ @pages_path = File.join(@elements_path, 'pages')
63
+ Dir.glob(File.join @pages_path, '**', '*.rb').select { |p| p =~ /\.rb$/ }.each do |page|
64
+ puts "#{page}"if $debug
65
+ require "#{page}"
66
+ end #each
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,323 @@
1
+ require 'optparse'
2
+ require 'tempfile'
3
+ require 'actir'
4
+
5
+ module Actir
6
+ module ParallelTests
7
+
8
+ class CLI
9
+ def run(argv)
10
+ $env = "online"
11
+ options = parse_options!(argv)
12
+ num_processes = Actir::ParallelTests.determine_number_of_processes(options[:count])
13
+ $mode = Actir::ParallelTests.determine_run_env(options[:mode])
14
+ if options[:execute]
15
+ execute_shell_command_in_parallel(options[:execute], num_processes, options)
16
+ else
17
+ run_tests_in_parallel(num_processes, options)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def execute_in_parallel(items, num_processes, options)
24
+ Tempfile.open 'parallel_tests-lock' do |lock|
25
+ return Parallel.map(items, :in_threads => num_processes) do |item|
26
+ result = yield(item)
27
+ report_output(result, lock) if options[:serialize_stdout]
28
+ result
29
+ end
30
+ end
31
+ end
32
+
33
+ def run_tests_in_parallel(num_processes, options)
34
+ test_results = nil
35
+
36
+ #修改全局变量$env至对应的预发布环境的名字
37
+ if options[:pre_name]
38
+ # 不等于当前的预发环境
39
+ if options[:pre_name] != $env
40
+ $env = options[:pre_name]
41
+ end
42
+ else
43
+ if options[:pre_name] != $env
44
+ $env = "online"
45
+ end
46
+ end
47
+
48
+ report_time_taken do
49
+ groups = @runner.tests_in_groups(options[:files], num_processes, options)
50
+
51
+ # @modify by Hub
52
+ # @Date : 2015.3.9
53
+ # 远程执行模式下获取服务器IP和端口号
54
+ address = []
55
+ if $mode == :remote
56
+ address = Actir::Remote.get_remote_address(num_processes)
57
+ num_processes = address.size
58
+ end
59
+
60
+ #更新百度支付-百付宝的cookies
61
+ if options[:update]
62
+ Actir::CookiesBaidu.update_all
63
+ end
64
+
65
+ #计算重跑次数
66
+ re_run_times = Actir::ParallelTests.determine_times_of_rerun(options[:rerun])
67
+ #报用例数
68
+ report_number_of_tests(groups)
69
+ #报执行环境
70
+ report_address_of_env(address)
71
+ #并发执行不同group中的测试用例
72
+ test_results = execute_in_parallel(groups, groups.size, options) do |group|
73
+ p_num = groups.index(group)
74
+ #执行用例脚本
75
+ result = run_tests(group, p_num, num_processes, options, address[p_num])
76
+ #从结果中取出失败用例重跑
77
+ if ( result[:exit_status] != 0 ) && ( re_run_times > 0 )
78
+ result = Actir::ParallelTests::Test::Rerun.re_run_tests(result, p_num, num_processes, options, address[p_num], re_run_times)
79
+ end
80
+ result
81
+ end
82
+
83
+ #顺序输出并发执行过程
84
+ show_process_serialize(num_processes, options)
85
+
86
+ #输出最终的执行结果
87
+ report_results(test_results)
88
+ end
89
+
90
+ abort final_fail_message if any_test_failed?(test_results)
91
+ end
92
+
93
+ def run_tests(group, process_number, num_processes, options, address)
94
+ if group.empty?
95
+ {:stdout => '', :exit_status => 0}
96
+ else
97
+ #puts pre_str + "ready to exec #{group}"
98
+ @runner.run_tests(group, process_number, num_processes, options, address)
99
+ end
100
+ end
101
+
102
+ def report_output(result, lock)
103
+ lock.flock File::LOCK_EX
104
+ $stdout.puts result[:stdout]
105
+ $stdout.flush
106
+ ensure
107
+ lock.flock File::LOCK_UN
108
+ end
109
+
110
+ def report_results(test_results)
111
+ results = @runner.find_results(test_results.map { |result| result[:stdout] }*"")
112
+ puts division_str
113
+ puts pre_str + @runner.summarize_results(results)
114
+ #生成详细报告
115
+ #detail_report(@runner.summarize_results(results),file_path)
116
+ #puts pre_str + any_test_failed?(test_results).to_s
117
+ end
118
+
119
+ def report_number_of_tests(groups)
120
+ name = @runner.test_file_name
121
+ num_processes = groups.size
122
+ num_tests = groups.map(&:size).inject(:+)
123
+ puts division_str
124
+ puts pre_str + "#{num_processes} processes for #{num_tests} #{name}s, ~ #{num_tests / groups.size} #{name}s per process"
125
+ #puts division_str
126
+ end
127
+
128
+ # add by Hub
129
+ # show test env address
130
+ def report_address_of_env(address)
131
+ if $mode == :remote
132
+ node_name = Actir::Config.get("config.test_mode.docker.name")
133
+ address.each_with_index do |ip, i|
134
+ puts " " + $env + node_name + (i+1).to_s + " : " + ip
135
+ end
136
+ else
137
+ puts " " + "local"
138
+ end
139
+ puts division_str
140
+ end
141
+
142
+ #add by Hub
143
+ #show result of every process exec testcases
144
+ #this func will last for a while due to the big logfile
145
+ def show_process_serialize(num_processes, options)
146
+ if options[:log]
147
+ puts "\n" + division_str + pre_str + "SHOW_PROCESS_LOG--START\n" + division_str
148
+ for i in 0..(num_processes-1)
149
+ Actir::ParallelTests::Test::Logger.show_log(i)
150
+ puts division_str
151
+ end
152
+ puts division_str + pre_str + "SHOW_PROCESS_LOG--END\n" + division_str
153
+ end
154
+ end
155
+
156
+ #exit with correct status code so rake parallel:test && echo 123 works
157
+ def any_test_failed?(test_results)
158
+ test_results.any? { |result| result[:exit_status] != 0 }
159
+ end
160
+
161
+ def parse_options!(argv)
162
+ options = {}
163
+ @runner = load_runner("test")
164
+ OptionParser.new do |opts|
165
+ opts.banner = <<-BANNER.gsub(/^ /, '')
166
+ Run all tests in parallel
167
+ Usage: ruby [switches] [--] [files & folders]
168
+ Options are:
169
+ BANNER
170
+ opts.on("-n [PROCESSES]", Integer, "How many processes to use, default: 1") { |n| options[:count] = n }
171
+ opts.on("--group-by [TYPE]", <<-TEXT.gsub(/^ /, '')
172
+ group tests by:
173
+ found - order of finding files
174
+ filesize - by size of the file
175
+ default - filesize
176
+ TEXT
177
+ ) { |type| options[:group_by] = type.to_sym }
178
+ opts.on("-r [TIMES]", "--rerun [TIMES]", Integer, "rerun times for failure&error testcase, default: 0") { |n| options[:rerun] = n }
179
+ #opts.on("-m [FLOAT]", "--multiply-processes [FLOAT]", Float, "use given number as a multiplier of processes to run") { |multiply| options[:multiply] = multiply }
180
+ opts.on("-i", "--isolate",
181
+ "Do not run any other tests in the group used by --single(-s)") do |pattern|
182
+ options[:isolate] = true
183
+ end
184
+ opts.on("-e", "--exec [COMMAND]", "execute this code parallel") { |path| options[:execute] = path }
185
+ opts.on("--serialize-stdout", "Serialize stdout output, nothing will be written until everything is done") { options[:serialize_stdout] = true }
186
+ opts.on("--combine-stderr", "Combine stderr into stdout, useful in conjunction with --serialize-stdout") { options[:combine_stderr] = true }
187
+ opts.on("--non-parallel", "execute same commands but do not in parallel, needs --exec") { options[:non_parallel] = true }
188
+ opts.on("--nice", "execute test commands with low priority") { options[:nice] = true }
189
+ opts.on("--verbose", "Print more output") { options[:verbose] = true }
190
+ opts.on("--log", "record exec result to logfile") { options[:log] = true}
191
+ opts.on("--remote", "run testcase in remote environment") { options[:mode] = :remote }
192
+ opts.on("--local", "run testcase in local environment") { options[:mode] = :local }
193
+ # 填写预发环境,目前只支持bjpre2-4,别的后续再添加
194
+ opts.on("-p", "--pre [PRE]", <<-TEXT.gsub(/^ /, '')
195
+ set pre environment to run testcase:
196
+ bjpre2
197
+ bjpre3
198
+ bjpre4
199
+ TEXT
200
+ ) { |pre| pre = "online" if ( pre != "bjpre2" && pre != "bjpre3" && pre != "bjpre4"); options[:pre_name] = pre }
201
+ #add by Hub
202
+ #-u commnd, update baifubao's cookies
203
+ opts.on("-u", "--update", "Update Baifubao's cookies") { options[:update] = true }
204
+ #add by Hub
205
+ #-s commnd, show test mode,and remote env ipaddress
206
+ opts.on("-s", "--show [PATH]", "Show Test Mode") do |path|
207
+ abort "Please input project directory path!" if path == nil
208
+ $project_path = File.join(Dir.pwd, path)
209
+ puts division_str
210
+ if Actir::Config.get("config.test_mode.env") == :local
211
+ puts "mode : Local"
212
+ else
213
+ puts "mode : Remote"
214
+ node_name = Actir::Config.get("config.test_mode.docker.name")
215
+ address = Actir::Remote.get_remote_address
216
+ puts "node_num : " + address.size.to_s
217
+ address.each_with_index do |address, i|
218
+ puts $env + node_name + (i+1).to_s + " : " + address
219
+ end
220
+ end
221
+ puts division_str
222
+ exit
223
+ end
224
+ opts.on("-h", "--help", "Show this.") { puts opts; exit }
225
+ end.parse!(argv)
226
+
227
+ if options[:count] == 0
228
+ options.delete(:count)
229
+ options[:non_parallel] = true
230
+ end
231
+
232
+ abort "Pass files or folders to run" if argv.empty? && !options[:execute]
233
+
234
+ #如果argv为空,则默认执行所有测试文件
235
+ #遍历testcode下所有文件,组成字符串
236
+ options[:files] = argv
237
+ get_project_path(argv)
238
+
239
+ options
240
+ end
241
+
242
+ #根据传入的测试文件/文件夹的路径,获取测试工程的路径
243
+ def get_project_path(argv)
244
+ testcode_path = ""
245
+ (argv || []).map do |file_or_folder|
246
+ test_path = File.join(Dir.pwd , file_or_folder)
247
+ #获取testcode文件夹的path
248
+ if test_path =~ /(\/.*\/testcode)/
249
+ testcode_path = $1
250
+ break
251
+ end
252
+ end
253
+ #根据testcode path 拿到project的path
254
+ #要求testcode必须是project的下一级/testcode必须是config的平级
255
+ $project_path = File.join(testcode_path, "../")
256
+ end
257
+
258
+ def load_runner(type)
259
+ require "actir/parallel_tests/#{type}/runner"
260
+ require "actir/parallel_tests/#{type}/re_run"
261
+ require "actir/parallel_tests/test/logger"
262
+ runner_classname = type.split("_").map(&:capitalize).join.sub("Rspec", "RSpec")
263
+ klass_name = "Actir::ParallelTests::#{runner_classname}::Runner"
264
+ klass_name.split('::').inject(Object) { |x, y| x.const_get(y) }
265
+ end
266
+
267
+ def execute_shell_command_in_parallel(command, num_processes, options)
268
+ runs = (0...num_processes).to_a
269
+ results = if options[:non_parallel]
270
+ runs.map do |i|
271
+ Actir::ParallelTests::Test::Runner.execute_command(command, i, num_processes, options)
272
+ end
273
+ else
274
+ execute_in_parallel(runs, num_processes, options) do |i|
275
+ Actir::ParallelTests::Test::Runner.execute_command(command, i, num_processes, options)
276
+ end
277
+ end.flatten
278
+
279
+ abort if results.any? { |r| r[:exit_status] != 0 }
280
+ end
281
+
282
+ def report_time_taken
283
+ seconds = Actir::ParallelTests.delta { yield }.to_i
284
+ puts "\n" + pre_str + "Cost #{seconds} seconds#{detailed_duration(seconds)}\n"
285
+ puts division_str
286
+ end
287
+
288
+ def detailed_duration(seconds)
289
+ parts = [ seconds / 3600, seconds % 3600 / 60, seconds % 60 ].drop_while(&:zero?)
290
+ return if parts.size < 2
291
+ parts = parts.map { |i| "%02d" % i }.join(':').sub(/^0/, '')
292
+ " (#{parts})"
293
+ end
294
+
295
+ def final_fail_message
296
+ fail_message = "#{@runner.name}s Failed\n"
297
+ fail_message = "\e[31m#{fail_message}\e[0m" if use_colors?
298
+
299
+ division_str + pre_str + fail_message + division_str
300
+ end
301
+
302
+ def use_colors?
303
+ $stdout.tty?
304
+ end
305
+
306
+ def pre_str
307
+ " Actir : "
308
+ end
309
+
310
+ # add by Hub
311
+ # division for Actir report
312
+ def division_str
313
+ "---------------------------------------------------------------------------------------------\n"
314
+ end
315
+
316
+ # def detail_report(output)
317
+
318
+ # HtmlPrinter.new(file)
319
+ # end
320
+
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,44 @@
1
+ module Actir
2
+ module ParallelTests
3
+ class Grouper
4
+ class << self
5
+
6
+ def in_even_groups_by_size(items, num_groups, options= {})
7
+ groups = Array.new(num_groups) { {:items => [], :size => 0} }
8
+
9
+ groups_to_fill = (options[:isolate] ? groups[1..-1] : groups)
10
+ group_features_by_size(items_to_group(items), groups_to_fill)
11
+
12
+ groups.map!{|g| g[:items].sort }
13
+ end
14
+
15
+ private
16
+
17
+ def largest_first(files)
18
+ files.sort_by{|item, size| size }.reverse
19
+ end
20
+
21
+ def smallest_group(groups)
22
+ groups.min_by{|g| g[:size] }
23
+ end
24
+
25
+ def add_to_group(group, item, size)
26
+ group[:items] << item
27
+ group[:size] += size
28
+ end
29
+
30
+ def group_features_by_size(items, groups_to_fill)
31
+ items.each do |item, size|
32
+ size ||= 1
33
+ smallest = smallest_group(groups_to_fill)
34
+ add_to_group(smallest, item, size)
35
+ end
36
+ end
37
+
38
+ def items_to_group(items)
39
+ items.first && items.first.size == 2 ? largest_first(items) : items
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,77 @@
1
+ require "parallel"
2
+ require 'actir/parallel_tests/cli'
3
+ require 'actir/parallel_tests/grouper'
4
+
5
+ module Actir
6
+ module ParallelTests
7
+
8
+ autoload :CLI, "cli"
9
+ autoload :Grouper, "grouper"
10
+
11
+ # 一些通用类方法
12
+ class << self
13
+ #判断执行的进程数
14
+ def determine_number_of_processes(count)
15
+ [
16
+ count,
17
+ #ENV["PARALLEL_TEST_PROCESSORS"],
18
+ #核数
19
+ #Parallel.processor_count
20
+ #count数不填模式为1
21
+ 1
22
+ ].detect{|c| not c.to_s.strip.empty? }.to_i
23
+ end
24
+
25
+ #判断失败用例重新执行的次数
26
+ def determine_times_of_rerun(times)
27
+ [
28
+ times,
29
+ 0
30
+ #Actir::Config.get("config.test_mode.re_run")
31
+ ].detect{|c| not c.to_s.strip.empty? }.to_i
32
+ end
33
+
34
+ #判断用例执行的环境是local还是remote
35
+ def determine_run_env(mode)
36
+ env_mode = :local
37
+ #判断是否存在config.yaml配置文件,如果不存在,则test_mode给默认值
38
+ if File.exist?(File.join($project_path, "config", "config.yaml"))
39
+ #刷新配置文件中的env配置项为remote模式,以防止本地调试代码改写上传后导致CI失败
40
+ if mode
41
+ unless mode == /#{Actir::Config.get("config.test_mode.env")}/
42
+ #同步修改配置文件,需要先将Symbol转换成String
43
+ mode_str = ":" + mode.to_s
44
+ Actir::Config.set("config.test_mode.env", mode_str)
45
+ end
46
+ env_mode = mode
47
+ else
48
+ env_mode = Actir::Config.get("config.test_mode.env")
49
+ end
50
+ else
51
+ if mode
52
+ env_mode = mode
53
+ end
54
+ end
55
+ ENV["mode"] = env_mode.to_s
56
+ env_mode
57
+ end
58
+
59
+ # real time even if someone messed with timecop in tests
60
+ def now
61
+ if Time.respond_to?(:now_without_mock_time) # Timecop
62
+ Time.now_without_mock_time
63
+ else
64
+ Time.now
65
+ end
66
+ end
67
+
68
+ def delta
69
+ before = now.to_f
70
+ yield
71
+ now.to_f - before
72
+ end
73
+
74
+ end
75
+
76
+ end
77
+ end