tes-request 0.1

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.
@@ -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