tes-request 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,82 @@
1
+ require 'yaml'
2
+ require 'java-properties'
3
+ require 'fileutils'
4
+ require_relative 'distribute'
5
+
6
+ module Tes::Request::RSpec
7
+ class CiSlicer
8
+ SUPPORT_FILE_TYPES = [:yaml, :yml, :json, :properties]
9
+
10
+ def initialize(file_type, project_dir, res_replace_map_json_file=nil)
11
+ unless SUPPORT_FILE_TYPES.include?(file_type.to_sym)
12
+ raise(ArgumentError, "Not supported file type:#{file_type}!")
13
+ end
14
+
15
+ @cfg_file_type = file_type
16
+ @project_dir = project_dir
17
+ @res_addition_attr_map = (res_replace_map_json_file && JSON.parse(File.read(res_replace_map_json_file)))
18
+ @cfg_target_dir = File.join(@project_dir, '.ci_jobs')
19
+ end
20
+
21
+ def run(ci_type, slice_count)
22
+ puts "Generate RSpec distribute jobs #{@cfg_file_type} file for CI"
23
+ rspec_distribute = Distribute.new(@project_dir)
24
+ jobs = rspec_distribute.distribute_jobs(ci_type, slice_count, @res_addition_attr_map)
25
+ save_job_files(jobs, @cfg_target_dir, @cfg_file_type)
26
+ @cfg_target_dir
27
+ end
28
+
29
+ def spec_tag_param_str(tags)
30
+ case tags
31
+ when Array
32
+ tags.map { |t| "--tag #{t}" }.join(' ')
33
+ when String
34
+ "--tag #{tags}"
35
+ when nil
36
+ nil
37
+ else
38
+ raise("不支持的类型:#{tags.class}")
39
+ end
40
+ end
41
+
42
+ def save_job_files(jobs, target_dir, file_type)
43
+ unless SUPPORT_FILE_TYPES.include?(file_type)
44
+ raise(ArgumentError, "Not supported file type:#{file_type}!")
45
+ end
46
+
47
+ job_configs_for_ci = jobs.map { |j| gen_job_ci_params(j) }
48
+ FileUtils.rm_rf(target_dir)
49
+ FileUtils.mkdir(target_dir)
50
+ case file_type
51
+ when :json
52
+ save_file = File.join(target_dir, 'ci_tasks.json')
53
+ File.open(save_file, 'w') { |f| f.write job_configs_for_ci.to_json }
54
+ puts "Generated #{jobs.size} jobs, Stored in:#{save_file} ."
55
+ when :yml, :yaml
56
+ save_file = File.join(target_dir, 'ci_tasks.yml')
57
+ File.open(save_file, 'w') { |f| f.write job_configs_for_ci.to_yaml }
58
+ puts "Generated #{jobs.size} jobs, Stored in:#{save_file} ."
59
+ when :properties
60
+ job_configs_for_ci.each_with_index do |params, i|
61
+ file = File.join(target_dir, "#{i}.properties")
62
+ JavaProperties.write(params, file)
63
+ end
64
+ puts "Generated #{jobs.size} jobs, Stored in:#{target_dir}/*.properties ."
65
+ end
66
+ end
67
+
68
+ def get_job_rspec_run_args_str(job, split=' ')
69
+ tags_str = spec_tag_param_str(job[:tag])
70
+ paths_str = job[:specs].join(split)
71
+ tags_str ? (tags_str + split + paths_str) : paths_str
72
+ end
73
+
74
+ def get_job_env_profile_str(job, split=';')
75
+ job[:profile].to_s(split)
76
+ end
77
+
78
+ def gen_job_ci_params(job)
79
+ {'RSPEC_PARAM' => get_job_rspec_run_args_str(job), 'REQUEST_ASKS' => get_job_env_profile_str(job)}
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,214 @@
1
+ require 'yaml'
2
+ require 'json'
3
+ require_relative 'profile_parser'
4
+
5
+ module Tes
6
+ module Request
7
+ module RSpec
8
+ class Distribute
9
+ include Function
10
+
11
+ DEFAULT_CI_YAML_FILE = '.ci.yaml'
12
+ EXCLUDE_CLUSTER_RES_PATTERN_PROFILE = '.ci_exclude_res_pattern'
13
+
14
+ @@ci_yaml_file = DEFAULT_CI_YAML_FILE
15
+ @@ci_exclude_cluster_pattern_file = EXCLUDE_CLUSTER_RES_PATTERN_PROFILE
16
+
17
+ # @param [String] project_dir 测试项目的根目录路径
18
+ # @param [String] ci_yaml_file 测试项目内的描述spec测试的配置文件路径(相对`project_dir`)
19
+ def initialize(project_dir, ci_yaml_file=@@ci_yaml_file)
20
+ @project_dir = project_dir
21
+ @ci_cfg = YAML.load_file(File.join(@project_dir, ci_yaml_file))
22
+ end
23
+
24
+ attr_reader :project_dir, :ci_cfg
25
+
26
+ # 生成分发任务的配置结构
27
+ # @return [Array<Hash>]
28
+ def distribute_jobs(type, count, res_addition_attr_map=nil)
29
+ task_cfg = get_rspec_task(type)
30
+ spec_paths = spec_files(type)
31
+ rspec_parser = Tes::Request::RSpec::ProfileParser.new(spec_paths)
32
+ rspec_parser.parse_profiles!
33
+
34
+ gen_pieces(rspec_parser.profiles, count).map do |piece|
35
+ profile = piece[:profile]
36
+ if res_addition_attr_map and res_addition_attr_map.size > 0
37
+ res_addition_attr_map.each do |res_filter_pattern, attr_add_map|
38
+ request_asks = profile.data.select { |ask| ask.to_s.include? res_filter_pattern }
39
+ request_asks.each do |ask|
40
+ # only add the resource attribution when no request the attribution for the resource
41
+ attr_add_map.each do |attr_name, attr_limit|
42
+ unless ask.data.include?(attr_name)
43
+ ask.data[attr_name] = Tes::Request::Expression.new("#{attr_name}#{attr_limit}")
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ specs = piece[:specs].inject([]) do |t, spec|
51
+ file_path = spec[:file].sub(/^#{@project_dir}/, '').sub(/^\//, '')
52
+ if (spec[:locations] and spec[:locations].size > 0) or (spec[:ids] and spec[:ids].size > 0)
53
+ if spec[:locations] and spec[:locations].size > 0
54
+ t.push(file_path + ':' + spec[:locations].join(':'))
55
+ end
56
+ if spec[:ids] and spec[:ids].size > 0
57
+ t.push(file_path + '[' + spec[:ids].join(',') + ']')
58
+ end
59
+ else
60
+ t.push file_path
61
+ end
62
+ t
63
+ end
64
+
65
+ {profile: profile, specs: specs}
66
+ end.map { |p| p.merge(tag: task_cfg['tag']) }
67
+ end
68
+
69
+ private
70
+ # 生产任务碎片,尽量接近传递的参数值
71
+ # @param [Fixnum] minimum_pieces
72
+ # @return [Array]
73
+ def gen_pieces(profiles, minimum_pieces)
74
+ ret = []
75
+ min_spec_count = profiles.size / minimum_pieces
76
+
77
+ profiles.each do |to_merge_spec|
78
+ # 0. 任务发布要求的特殊处理
79
+ if to_merge_spec[:distribute] && to_merge_spec[:distribute][:standalone]
80
+ ret << {profile: to_merge_spec[:profile], specs: [to_merge_spec]}
81
+ next
82
+ end
83
+
84
+ # 1. 优先相同要求的归并
85
+ join_piece = ret.find do |piece|
86
+ piece[:specs].size <= min_spec_count and
87
+ piece[:profile] == to_merge_spec[:profile]
88
+ end
89
+ if join_piece
90
+ # puts 'http://join'
91
+ join_piece[:specs] << to_merge_spec
92
+ else
93
+ # 2. 然后再是资源多少不同的归并
94
+ super_piece = ret.find do |piece|
95
+ if piece[:specs].size <= min_spec_count
96
+ cr = piece[:profile] <=> to_merge_spec[:profile]
97
+ cr && cr >= 0
98
+ else
99
+ false
100
+ end
101
+ end
102
+ if super_piece
103
+ # puts 'http://inherit'
104
+ super_piece[:specs] << to_merge_spec
105
+ else
106
+ # 3. 可整合计算的的归并,但要求已经达到的任务分片数已经达到了要求那么大,否则直接以新建来搞
107
+ if ret.size >= minimum_pieces
108
+ merge_piece = ret.find do |piece|
109
+ piece[:specs].size <= min_spec_count and
110
+ piece[:profile].merge_able?(to_merge_spec[:profile])
111
+ end
112
+ if merge_piece
113
+ # puts 'http://merge'
114
+ merge_piece[:profile] = merge_piece[:profile] + to_merge_spec[:profile]
115
+ # puts merge_piece[:profile]
116
+ merge_piece[:specs] << to_merge_spec
117
+ else
118
+ # 4. 最后再尝试独立出一个新的piece,在剩余数量达到一半要求的时候
119
+ # puts 'http://new'
120
+ ret << {profile: to_merge_spec[:profile], specs: [to_merge_spec]}
121
+ end
122
+ else
123
+ ret << {profile: to_merge_spec[:profile], specs: [to_merge_spec]}
124
+ end
125
+ end
126
+ end
127
+ end
128
+ ret
129
+ end
130
+
131
+
132
+ # @param [String] type
133
+ # @return [Hash] rspec ci task info
134
+ def get_rspec_task(type)
135
+ raise("No CI Task:#{type}") unless @ci_cfg.key?(type)
136
+ @ci_cfg[type]
137
+ end
138
+
139
+ # @return [Array<String>]
140
+ def spec_files(task_type)
141
+ spec_cfg = get_rspec_task(task_type)['spec']
142
+ spec_paths = filter_spec_by_path(spec_cfg['pattern'], spec_cfg['exclude_pattern'])
143
+
144
+ exclude_cluster_profile_file = File.join(@project_dir, @@ci_exclude_cluster_pattern_file)
145
+ if File.exists?(exclude_cluster_profile_file)
146
+ exclude_patterns = File.readlines(exclude_cluster_profile_file).map(&:strip)
147
+ exclude_spec_by_resource(spec_paths, exclude_patterns)
148
+ else
149
+ spec_paths
150
+ end
151
+ end
152
+
153
+ # @return [Array<String>]
154
+ def filter_spec_by_path(pattern, exclude_pattern=nil)
155
+ pattern_filter_lab = ->(p) do
156
+ spec_info = get_spec_path_info(p)
157
+ direct_return = (spec_info[:locations] or spec_info[:ids])
158
+ direct_return ? [File.join(@project_dir, p)] : Dir[File.join(@project_dir, p)]
159
+ end
160
+
161
+ ret = case pattern
162
+ when String
163
+ pattern_filter_lab.call(pattern)
164
+ Dir[File.join(@project_dir, pattern)]
165
+ when Array
166
+ pattern.inject([]) { |t, ep| t + pattern_filter_lab.call(ep) }
167
+ else
168
+ raise('Error pattern type')
169
+ end
170
+
171
+ return ret unless exclude_pattern
172
+
173
+ case exclude_pattern
174
+ when String
175
+ ret -= Dir[File.join(@project_dir, exclude_pattern)]
176
+ when Array
177
+ ret -= exclude_pattern.inject([]) { |t, ep| t + Dir[File.join(@project_dir, ep)] }
178
+ else
179
+ raise('Error exclude_pattern type')
180
+ end
181
+
182
+ ret
183
+ end
184
+
185
+ # 按照指定资源属性排除要求排除相应的spec路径
186
+ # @param [Array<String>] spec_paths spec的执行路径列表
187
+ # @param [Array<String>] res_exclude_patterns,
188
+ # 每一个元素的格式是这样:
189
+ # res_type: res_attr1=2,res_attr3>=4
190
+ # 或者
191
+ # type=res_type,res_attr1=2,res_attr3>=4
192
+ # @return [Array<String>] 按照`res_exclude_pattern` 剔除后的 `spec_paths`
193
+ def exclude_spec_by_resource(spec_paths, res_exclude_patterns=[])
194
+ return spec_paths if res_exclude_patterns.empty?
195
+
196
+ spec_paths.reject do |spec_path|
197
+ spec_info = get_spec_path_info(spec_path)
198
+ profile_lines = parse_spec_profile_lines(spec_info[:file])
199
+ res_exclude_patterns.any? do |exclude_pattern|
200
+ res_attrs = if exclude_pattern =~ /^\w+:\s*.+/
201
+ type, attrs = exclude_pattern.split(/:\s*/, 2)
202
+ "type=#{type}," + attrs
203
+ else
204
+ exclude_pattern
205
+ end
206
+ res_attrs = res_attrs.split(',')
207
+ profile_lines.any? { |line| res_attrs.all? { |attr| line =~ /\b#{attr}\b/ } }
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,48 @@
1
+ module Tes
2
+ module Request
3
+ module RSpec
4
+ module Function
5
+ # 对spec路径获取相关信息(行号或者序号等信息)
6
+ # @param [String] spec_path
7
+ # @param [Hash]
8
+ def get_spec_path_info(spec_path)
9
+ ids_reg = /\[([\d:,]+)\]$/
10
+ locations_reg = /:([\d:]+)$/
11
+ if spec_path =~ ids_reg
12
+ ids = spec_path.match(ids_reg)[1].split(',')
13
+ file_path = spec_path.sub(ids_reg, '')
14
+ {file: file_path, ids: ids}
15
+ elsif spec_path =~ locations_reg
16
+ locations = spec_path.match(locations_reg)[1].split(':').map(&:to_i)
17
+ file_path = spec_path.sub(locations_reg, '')
18
+ {file: file_path, locations: locations}
19
+ else
20
+ {file: spec_path}
21
+ end
22
+ end
23
+
24
+ # 解析spec文件的测试环境要求文本内容
25
+ # @return [Array<String>]
26
+ def parse_spec_profile_lines(spec_file)
27
+ ret = nil
28
+ File.open(spec_file, 'r') do |f|
29
+ f.each_line do |l|
30
+ case l
31
+ when /^\s*#\s*@env\s+begin\s*$/
32
+ ret = []
33
+ when /^\s*#\s*@end/
34
+ break
35
+ when /^\s*#/
36
+ ret << l.sub(/^\s*#\s*/, '') if ret
37
+ else
38
+ #nothing
39
+ end
40
+ end
41
+ end
42
+
43
+ ret && ret.map(&:strip).map { |l| l.split(/\s*;\s*/) }.flatten
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,132 @@
1
+ require_relative '../profile'
2
+ require_relative 'function'
3
+
4
+ # 分析rspec profile信息
5
+ module Tes
6
+ module Request
7
+ module RSpec
8
+ class ProfileParser
9
+ def initialize(spec_paths=[])
10
+ @spec_paths = spec_paths
11
+ end
12
+
13
+ attr_reader :profiles
14
+
15
+ def <<(spec_path)
16
+ @spec_paths << spec_path
17
+ end
18
+
19
+ def parse_profiles!
20
+ profile_map = {}
21
+
22
+ @spec_paths.each do |s_p|
23
+ spec_info = parse_spec(s_p)
24
+
25
+ # 如果相同文件则进行合并
26
+ if profile_map.key?(spec_info[:file])
27
+ profile_map[spec_info[:file]] = merge_spec_info(profile_map[spec_info[:file]], spec_info)
28
+ else
29
+ profile_map[spec_info[:file]] = spec_info
30
+ end
31
+ end
32
+
33
+ # 对存在的包含关系的ids尝试合并
34
+ profile_map.each do |_f, info|
35
+ info[:ids] = merge_spec_ids(info[:ids]) if info[:ids]
36
+ info[:locations].sort! if info[:locations]
37
+ end
38
+ @profiles = profile_map.values
39
+ end
40
+
41
+ private
42
+
43
+ include Function
44
+
45
+ # 合并2个spec info数据结构
46
+ # @param [Hash] a
47
+ # @param [Hash] b
48
+ # @return [Hash] merge result, same struct with `a` or `b`
49
+ def merge_spec_info(a, b)
50
+ raise('不支持合并不同spec文件的信息') unless a[:file] == b[:file]
51
+
52
+ compare_keys = [:ids, :locations]
53
+ a_compare_keys = a.keys.select { |k| compare_keys.include?(k) }.sort
54
+ b_compare_keys = b.keys.select { |k| compare_keys.include?(k) }.sort
55
+
56
+ case [a_compare_keys, b_compare_keys]
57
+ # 都有ids的情况
58
+ when [[:ids], [:ids]], [[:ids, :locations], [:ids]]
59
+ a.merge(ids: (a[:ids] + b[:ids]).uniq)
60
+ when [[:ids], [:ids, :locations]]
61
+ b.merge(ids: (a[:ids] + b[:ids]).uniq)
62
+
63
+ # 都有locations的情况
64
+ when [[:locations], [:locations]], [[:ids, :locations], [:locations]]
65
+ a.merge(locations: (a[:locations] + b[:locations]).uniq)
66
+ when [[:locations], [:ids, :locations]]
67
+ a.merge(locations: (b[:locations] + a[:locations]).uniq)
68
+
69
+ # 都有ids和locations
70
+ when [[:ids, :locations], [:ids, :locations]]
71
+ a.merge(
72
+ ids: (a[:ids] + b[:ids]).uniq,
73
+ locations: (a[:locations] + b[:locations]).uniq
74
+ )
75
+
76
+ # 互补
77
+ when [[:ids], [:locations]], [[:locations], [:ids]]
78
+ a.merge b
79
+ else
80
+ # 只剩下 a_compare_keys 为空 或者 b_compare_keys为空的情况
81
+ a_compare_keys.empty? ? a : b
82
+ end
83
+ end
84
+
85
+ # @param [Array<String>] ids
86
+ def merge_spec_ids(ids)
87
+ ids.sort.inject([]) do |t, id|
88
+ id_is_covered = (t.last && id.index(t.last) == 0)
89
+ id_is_covered ? t : t.push(id)
90
+ end
91
+ end
92
+
93
+ # @param [Array<String>] str_array, 每个元素的格式可以是这样:
94
+ # - `--xxx`
95
+ # - `-yy`
96
+ # - `-x vvv`
97
+ # - `--zz vvv`
98
+ # @return [Hash<Symbol, Object>]
99
+ def parse_distribute_profile(str_array)
100
+ str_array.inject({}) do |d_p, str|
101
+ args = str.scan(/-+[^-]+\b/).map { |arg| arg.sub(/^-+/, '').split(/\s+/) }
102
+ d_p_phrase = args.inject({}) { |h, arg| h.merge(arg[0].to_sym => arg[1] || true) }
103
+ d_p.merge d_p_phrase
104
+ end
105
+ end
106
+
107
+ # 解析指定路径spec的执行解析
108
+ # @param [String] spec_path 当前支持3种格式
109
+ # - 一种是普通的文件路径格式
110
+ # - 一种是文件格式基础上增加行数指定的格式,单个或者多个,单个格式 `:123`,多个直接拼接起来
111
+ # - 一种是文件格式基础上增加用例层次路径的格式,单个或者多个,单个格式`[1:2:3]`,多个在括号内用逗号间隔:`[1:2:3,2:3:4]`
112
+ # @return [Hash] include keys: `:file`, `:profile`, `:distribute`(optional)
113
+ def parse_spec(spec_path)
114
+ spec_info = get_spec_path_info spec_path
115
+
116
+ profile_lines = parse_spec_profile_lines(spec_info[:file])
117
+ raise("#{spec_info[:file]} 没有ci profile配置,请确认!") unless profile_lines and profile_lines.size > 0
118
+
119
+ distribute_lines = profile_lines.
120
+ select { |l| l =~ /^\s*@distribute\s+/ }.
121
+ map { |l| l.sub(/^\s*@distribute\s+/, '') }
122
+ profile_lines.reject! { |l| l =~ /^\s*@distribute\s+/ }
123
+
124
+ spec_info.merge(
125
+ profile: Profile.new(profile_lines),
126
+ distribute: parse_distribute_profile(distribute_lines)
127
+ )
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end