lazylead 0.5.2 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +7 -2
  3. data/.docker/Dockerfile +17 -6
  4. data/.rubocop.yml +102 -1
  5. data/.simplecov +1 -1
  6. data/Guardfile +1 -1
  7. data/Rakefile +6 -3
  8. data/bin/lazylead +3 -1
  9. data/lazylead.gemspec +31 -26
  10. data/lib/lazylead/cc.rb +21 -20
  11. data/lib/lazylead/cli/app.rb +8 -3
  12. data/lib/lazylead/confluence.rb +9 -2
  13. data/lib/lazylead/email.rb +0 -20
  14. data/lib/lazylead/exchange.rb +16 -28
  15. data/lib/lazylead/model.rb +31 -16
  16. data/lib/lazylead/opts.rb +65 -2
  17. data/lib/lazylead/postman.rb +13 -17
  18. data/lib/lazylead/salt.rb +1 -0
  19. data/lib/lazylead/system/jira.rb +30 -6
  20. data/lib/lazylead/task/accuracy/accuracy.rb +10 -14
  21. data/lib/lazylead/task/accuracy/attachment.rb +2 -6
  22. data/lib/lazylead/task/accuracy/logs.rb +9 -5
  23. data/lib/lazylead/task/accuracy/onlyll.rb +147 -0
  24. data/lib/lazylead/task/accuracy/records.rb +1 -1
  25. data/lib/lazylead/task/accuracy/servers.rb +16 -7
  26. data/lib/lazylead/task/accuracy/stacktrace.rb +50 -10
  27. data/lib/lazylead/task/accuracy/testcase.rb +16 -9
  28. data/lib/lazylead/task/assignment.rb +96 -0
  29. data/lib/lazylead/task/fix_version.rb +6 -0
  30. data/lib/lazylead/task/propagate_down.rb +1 -1
  31. data/lib/lazylead/task/svn/diff.rb +77 -0
  32. data/lib/lazylead/task/svn/grep.rb +139 -0
  33. data/lib/lazylead/task/svn/touch.rb +99 -0
  34. data/lib/lazylead/version.rb +1 -1
  35. data/lib/messages/illegal_assignee_change.erb +123 -0
  36. data/lib/messages/illegal_fixversion_change.erb +8 -0
  37. data/lib/messages/only_ll.erb +107 -0
  38. data/lib/messages/svn_diff.erb +110 -0
  39. data/lib/messages/{svn_log.erb → svn_diff_attachment.erb} +19 -9
  40. data/lib/messages/svn_grep.erb +114 -0
  41. data/test/lazylead/cc_test.rb +1 -0
  42. data/test/lazylead/model_test.rb +20 -0
  43. data/test/lazylead/opts_test.rb +47 -0
  44. data/test/lazylead/postman_test.rb +8 -5
  45. data/test/lazylead/smoke_test.rb +13 -0
  46. data/test/lazylead/smtp_test.rb +1 -4
  47. data/test/lazylead/system/jira_test.rb +6 -7
  48. data/test/lazylead/task/accuracy/attachment_test.rb +1 -1
  49. data/test/lazylead/task/accuracy/logs_test.rb +62 -2
  50. data/test/lazylead/task/accuracy/onlyll_test.rb +138 -0
  51. data/test/lazylead/task/accuracy/servers_test.rb +2 -2
  52. data/test/lazylead/task/accuracy/stacktrace_test.rb +227 -0
  53. data/test/lazylead/task/accuracy/testcase_test.rb +39 -0
  54. data/test/lazylead/task/assignment_test.rb +53 -0
  55. data/test/lazylead/task/duedate_test.rb +3 -10
  56. data/test/lazylead/task/fix_version_test.rb +1 -0
  57. data/test/lazylead/task/propagate_down_test.rb +4 -3
  58. data/test/lazylead/task/savepoint_test.rb +9 -6
  59. data/test/lazylead/task/svn/diff_test.rb +97 -0
  60. data/test/lazylead/task/svn/grep_test.rb +103 -0
  61. data/test/lazylead/task/{touch_test.rb → svn/touch_test.rb} +7 -34
  62. data/test/test.rb +7 -8
  63. data/upgrades/sqlite/999.testdata.sql +3 -1
  64. metadata +141 -55
  65. data/lib/lazylead/task/touch.rb +0 -119
@@ -39,8 +39,6 @@ module Lazylead
39
39
  # Copyright:: Copyright (c) 2019-2020 Yurii Dubinka
40
40
  # License:: MIT
41
41
  class Exchange
42
- include Emailing
43
-
44
42
  def initialize(
45
43
  log = Log.new, salt = Salt.new("exchange_salt"), opts = ENV.to_h
46
44
  )
@@ -52,38 +50,28 @@ module Lazylead
52
50
  # Send an email.
53
51
  # :opts :: the mail configuration like from, cc, subject, template.
54
52
  def send(opts)
55
- to = opts["to"] || opts[:to]
56
- to = [to] unless to.is_a? Array
57
- if to.reject { |e| e.nil? || e.blank? }.empty?
58
- @log.warn "Email can't be sent to '#{to}, more: '#{opts}'"
59
- return
53
+ if opts.msg_to.empty?
54
+ @log.warn "ll-012: Email can't be sent to '#{opts.msg_to}," \
55
+ " more: '#{opts}'"
56
+ else
57
+ msg = make_msg opts
58
+ cli.send_message msg
59
+ msg[:file_attachments].each(&:close) unless opts.msg_attachments.empty?
60
+ @log.debug "#{__FILE__} sent '#{opts['subject']}' to '#{opts.msg_to}'."
60
61
  end
61
- msg = make_msg(to, opts)
62
- msg.update(cc_recipients: opts["cc"]) if opts.key? "cc"
63
- add_attachments(msg, opts)
64
- cli.send_message msg
65
- close_attachments msg
66
- @log.debug "#{__FILE__} sent '#{opts['subject']}' to '#{to}'."
67
62
  end
68
63
 
69
- def make_msg(to, opts)
70
- {
64
+ def make_msg(opts)
65
+ msg = {
71
66
  subject: opts["subject"],
72
- body: make_body(opts),
67
+ body: opts.msg_body,
73
68
  body_type: "HTML",
74
- to_recipients: to
69
+ to_recipients: opts.msg_to
75
70
  }
76
- end
77
-
78
- def add_attachments(msg, opts)
79
- return unless opts.key? "attachments"
80
- files = split("attachments", opts).map { |f| File.open(f, "r") }
81
- msg[:file_attachments] = files
82
- end
83
-
84
- def close_attachments(msg)
85
- return if msg[:file_attachments].nil? || msg[:file_attachments].empty?
86
- msg[:file_attachments].each(&:close)
71
+ files = opts.msg_attachments.map { |f| File.open(f, "r") }
72
+ msg[:file_attachments] = files unless files.empty?
73
+ msg[:cc_recipients] = opts["cc"] if opts.key? "cc"
74
+ msg
87
75
  end
88
76
 
89
77
  private
@@ -68,9 +68,7 @@ module Lazylead
68
68
  opts.each_with_object({}) do |e, o|
69
69
  k = e[0]
70
70
  v = e[1]
71
- if v.respond_to? :start_with?
72
- v = ENV[v.slice(2, v.length - 3)] if v.start_with? "${"
73
- end
71
+ v = ENV[v.slice(2, v.length - 3)] if v.respond_to?(:start_with?) && v.start_with?("${")
74
72
  o[k] = v
75
73
  end
76
74
  end
@@ -80,6 +78,13 @@ module Lazylead
80
78
  JSON.parse(properties).to_h
81
79
  end
82
80
 
81
+ def to_h?
82
+ return true unless to_hash.nil?
83
+ false
84
+ rescue StandardError => _e
85
+ false
86
+ end
87
+
83
88
  def to_s
84
89
  attributes.map { |k, v| "#{k}='#{v}'" }.join(", ")
85
90
  end
@@ -99,6 +104,7 @@ module Lazylead
99
104
  sys = system.connect
100
105
  opts = props
101
106
  opts = detect_cc(sys) if opts.key? "cc"
107
+ opts["system"] = second_sys if opts.numeric? "system"
102
108
  action.constantize.new.run(sys, postman, opts)
103
109
  end
104
110
 
@@ -130,12 +136,12 @@ module Lazylead
130
136
 
131
137
  def props
132
138
  @props ||= begin
133
- if team.nil?
134
- Opts.new(env(to_hash))
135
- else
136
- Opts.new(env(team.to_hash.merge(to_hash)))
137
- end
138
- end
139
+ if team.nil?
140
+ Opts.new(env(to_hash))
141
+ else
142
+ Opts.new(env(team.to_hash.merge(to_hash)))
143
+ end
144
+ end
139
145
  end
140
146
 
141
147
  def postman
@@ -146,17 +152,21 @@ module Lazylead
146
152
  end
147
153
  end
148
154
 
155
+ def second_sys
156
+ sys = System.find(props["system"])
157
+ raise "ll-014: No ticketing system found for #{self}" if sys.nil?
158
+ sys.connect
159
+ end
160
+
149
161
  private
150
162
 
151
163
  # Parse scheduling #type and #unit
152
164
  def trigger
153
165
  @trigger ||= begin
154
- trg = schedule.split(":")
155
- unless trg.size == 2
156
- raise "ll-007: illegal schedule format '#{schedule}'"
157
- end
158
- trg.map(&:strip).map(&:chomp)
159
- end
166
+ trg = schedule.split(":")
167
+ raise "ll-007: illegal schedule format '#{schedule}'" unless trg.size == 2
168
+ trg.map(&:strip).map(&:chomp)
169
+ end
160
170
  end
161
171
  end
162
172
 
@@ -172,6 +182,10 @@ module Lazylead
172
182
  @log = log
173
183
  end
174
184
 
185
+ # @todo #/DEV Remove the suppression during next refactoring (or enhancements)
186
+ # for the method below
187
+ #
188
+ # rubocop:disable Metrics/AbcSize
175
189
  def exec
176
190
  Logging.mdc["tid"] = "task #{id}"
177
191
  @log.debug "'#{name}' is started."
@@ -182,12 +196,13 @@ module Lazylead
182
196
  rescue StandardError => e
183
197
  msg = <<~MSG
184
198
  ll-006: Task ##{id} #{e} (#{e.class}) at #{self}
185
- #{Backtrace.new(e) if ARGV.include? '--trace'}"
199
+ #{Backtrace.new(e) if ARGV.include? '--trace'}
186
200
  MSG
187
201
  @log.error msg
188
202
  ensure
189
203
  Logging.mdc["tid"] = ""
190
204
  end
205
+ # rubocop:enable Metrics/AbcSize
191
206
  end
192
207
 
193
208
  # Ticketing systems to monitor.
@@ -34,7 +34,8 @@ module Lazylead
34
34
  # License:: MIT
35
35
  class Opts
36
36
  extend Forwardable
37
- def_delegators :@origin, :[], :[]=, :to_s, :key?, :fetch, :merge, :except
37
+ def_delegators :@origin, :[], :[]=, :to_s, :key?, :fetch, :except, :each,
38
+ :each_pair, :sort_by
38
39
 
39
40
  def initialize(origin = {})
40
41
  @origin = origin
@@ -42,7 +43,14 @@ module Lazylead
42
43
 
43
44
  # Split text value by delimiter, trim all spaces and reject blank items
44
45
  def slice(key, delim)
45
- to_h[key].split(delim).map(&:chomp).map(&:strip).reject(&:blank?)
46
+ return [] unless to_h.key? key
47
+ trim to_h[key].split(delim)
48
+ end
49
+
50
+ # Trim all spaces and reject blank items in array
51
+ def trim(arr)
52
+ return [] if arr.nil?
53
+ arr.map(&:chomp).map(&:strip).reject(&:blank?)
46
54
  end
47
55
 
48
56
  def blank?(key)
@@ -76,5 +84,60 @@ module Lazylead
76
84
  return Salt.new(sid).decrypt(text) if ENV.key? sid
77
85
  text
78
86
  end
87
+
88
+ def merge(args)
89
+ return self unless args.is_a? Hash
90
+ Opts.new @origin.merge(args)
91
+ end
92
+
93
+ # Construct html document from template and binds.
94
+ def msg_body(template = "template")
95
+ Email.new(
96
+ to_h[template],
97
+ to_h.merge(version: Lazylead::VERSION)
98
+ ).render
99
+ end
100
+
101
+ def msg_to(delim = ",")
102
+ sliced delim, :to, "to"
103
+ end
104
+
105
+ def msg_cc(delim = ",")
106
+ sliced delim, :cc, "cc"
107
+ end
108
+
109
+ def msg_from(delim = ",")
110
+ sliced delim, :from, "from"
111
+ end
112
+
113
+ def msg_attachments(delim = ",")
114
+ sliced(delim, :attachments, "attachments").select { |f| File.file? f }
115
+ end
116
+
117
+ #
118
+ # Find the option by key and split by delimiter
119
+ # Opts.new("key" => "a,b").sliced(",", "key") => [a, b]
120
+ # Opts.new(key: "a,b").sliced(",", :key) => [a, b]
121
+ # Opts.new(key: "a,b").sliced(",", "key", :key) => [a, b]
122
+ # Opts.new(key: "").sliced ",", :key) => []
123
+ #
124
+ def sliced(delim, *keys)
125
+ return [] if keys.empty?
126
+ key = keys.detect { |k| key? k }
127
+ val = to_h[key]
128
+ return [] if val.nil? || val.blank?
129
+ return val if val.is_a? Array
130
+ return [val] unless val.include? delim
131
+ slice key, delim
132
+ end
133
+
134
+ # Ensure that particular key from options is a positive numer
135
+ # Opts.new("key" => "1").numeric? "key" => true
136
+ # Opts.new("key" => "0").numeric? "key" => false
137
+ # Opts.new("key" => ".").numeric? "key" => false
138
+ # Opts.new("key" => "nil").numeric? "key" => false
139
+ def numeric?(key)
140
+ to_h[key].to_i.positive?
141
+ end
79
142
  end
80
143
  end
@@ -44,8 +44,6 @@ module Lazylead
44
44
  # Copyright:: Copyright (c) 2019-2020 Yurii Dubinka
45
45
  # License:: MIT
46
46
  class Postman
47
- include Emailing
48
-
49
47
  def initialize(log = Log.new)
50
48
  @log = log
51
49
  end
@@ -53,31 +51,29 @@ module Lazylead
53
51
  # Send an email.
54
52
  # :opts :: the mail configuration like to, from, cc, subject, template.
55
53
  def send(opts)
56
- mail = make_email(opts)
57
- mail.deliver
58
- @log.debug "#{__FILE__} sent '#{mail.subject}' to '#{mail.to}'."
54
+ if opts.msg_to.empty?
55
+ @log.warn "ll-013: Email can't be sent to '#{opts.msg_to}," \
56
+ " more: '#{opts}'"
57
+ else
58
+ mail = make_email(opts)
59
+ mail.deliver
60
+ @log.debug "#{__FILE__} sent '#{mail.subject}' to '#{mail.to}'."
61
+ end
59
62
  end
60
63
 
61
64
  # Construct an email based on input arguments
62
65
  def make_email(opts)
63
66
  mail = Mail.new
64
- mail.to opts[:to] || opts["to"]
65
- mail.from opts["from"]
66
- mail.cc opts["cc"] if opts.key? "cc"
67
+ mail.to opts.msg_to
68
+ mail.from opts.msg_from
69
+ mail.cc opts.msg_cc if opts.key? "cc"
67
70
  mail.subject opts["subject"]
68
- html = make_body(opts)
69
71
  mail.html_part do
70
72
  content_type "text/html; charset=UTF-8"
71
- body html
73
+ body opts.msg_body
72
74
  end
73
- add_attachments mail, opts
75
+ opts.msg_attachments.each { |f| mail.add_file f }
74
76
  mail
75
77
  end
76
-
77
- def add_attachments(mail, opts)
78
- return unless opts.key? "attachments"
79
- opts["attachments"].select { |a| File.file? a }
80
- .each { |a| mail.add_file a }
81
- end
82
78
  end
83
79
  end
@@ -39,6 +39,7 @@ module Lazylead
39
39
  #
40
40
  class Salt
41
41
  attr_reader :id
42
+
42
43
  #
43
44
  # Each salt should be defined as a environment variable with id, like
44
45
  # salt1=E1F53135E559C253
@@ -25,6 +25,7 @@
25
25
  require "jira-ruby"
26
26
  require "forwardable"
27
27
  require_relative "../salt"
28
+ require_relative "../opts"
28
29
 
29
30
  module Lazylead
30
31
  # Jira system for manipulation with issues.
@@ -45,16 +46,32 @@ module Lazylead
45
46
  " and salt #{@salt.id} (found=#{@salt.specified?})"
46
47
  end
47
48
 
48
- def issues(jql, opts = {})
49
+ # Find the jira issues by 'JQL'
50
+ # @param 'jql' - Jira search query
51
+ # @param 'opts' - Parameters for Jira search query.
52
+ # :max_results => maximum number of tickets per one iteration.
53
+ # :fields => ticket fields to be fetched like assignee, summary, etc.
54
+ def issues(jql, opts = { max_results: 50, fields: nil, expand: nil })
49
55
  raw do |jira|
50
- jira.Issue.jql(jql, opts).map { |i| Lazylead::Issue.new(i, jira) }
56
+ start = 0
57
+ tickets = []
58
+ total = jira.Issue.jql(jql, max_results: 0)
59
+ @log.debug "Found #{total} ticket(s) in '#{jql}'"
60
+ loop do
61
+ tickets.concat(jira.Issue.jql(jql, opts.merge(start_at: start))
62
+ .map { |i| Lazylead::Issue.new(i, jira) })
63
+ @log.debug "Fetched #{tickets.size}"
64
+ start += opts.fetch(:max_results, 50).to_i
65
+ break if start > total
66
+ end
67
+ tickets
51
68
  end
52
69
  end
53
70
 
54
71
  # Execute request to the ticketing system using raw client.
55
72
  # For Jira the raw client is 'jira-ruby' gem.
56
73
  def raw
57
- raise "ll-06: No block given to method" unless block_given?
74
+ raise "ll-009: No block given to method" unless block_given?
58
75
  yield client
59
76
  end
60
77
 
@@ -160,7 +177,7 @@ module Lazylead
160
177
  end
161
178
 
162
179
  def url
163
- @issue.attrs["self"].split("/rest/api/").first + "/browse/" + key
180
+ "#{@issue.attrs['self'].split('/rest/api/').first}/browse/#{key}"
164
181
  end
165
182
 
166
183
  def duedate
@@ -240,13 +257,20 @@ module Lazylead
240
257
  lbl = [] if lbl.nil?
241
258
  lbl << label
242
259
  lbl += more if more.size.positive?
243
- save!("fields" => { "labels" => lbl.uniq })
260
+ labels! lbl
244
261
  end
245
262
 
263
+ # Get the labels for a particular issue
246
264
  def labels
247
265
  fields["labels"]
248
266
  end
249
267
 
268
+ # Update the labels for a particular issue
269
+ def labels!(lbl)
270
+ return if lbl.nil? || lbl.empty?
271
+ save!("fields" => { "labels" => lbl.uniq })
272
+ end
273
+
250
274
  def save!(opts)
251
275
  @issue.save(opts)
252
276
  end
@@ -325,7 +349,7 @@ module Lazylead
325
349
 
326
350
  # Execute request to the ticketing system using raw client.
327
351
  def raw
328
- raise "ll-08: No block given to method" unless block_given?
352
+ raise "ll-008: No block given to method" unless block_given?
329
353
  yield(OpenStruct.new(Issue: self))
330
354
  end
331
355
 
@@ -70,10 +70,8 @@ module Lazylead
70
70
  # Estimate the ticket score and accuracy.
71
71
  # Accuracy is a percentage between current score and maximum possible value.
72
72
  def evaluate(digits = 2)
73
- @total = @rules.map(&:score).sum
74
- @score = @rules.select { |r| r.passed(@issue) }
75
- .map(&:score)
76
- .sum
73
+ @total = @rules.sum(&:score)
74
+ @score = @rules.select { |r| r.passed(@issue) }.sum(&:score)
77
75
  @accuracy = (score / @total * 100).round(digits)
78
76
  end
79
77
 
@@ -81,7 +79,7 @@ module Lazylead
81
79
  def post
82
80
  return if @opts.key? "silent"
83
81
  @issue.post comment
84
- @issue.add_label "LL.accuracy", grade(@accuracy)
82
+ @issue.add_label "LL.accuracy", "#{grade(@accuracy)}%"
85
83
  end
86
84
 
87
85
  # The jira comment in markdown format
@@ -114,9 +112,7 @@ module Lazylead
114
112
  end
115
113
 
116
114
  def color
117
- if colors.nil? || !defined?(@score) || !@score.is_a?(Numeric)
118
- return "#061306"
119
- end
115
+ return "#061306" if colors.nil? || !defined?(@score) || !@score.is_a?(Numeric)
120
116
  colors.reverse_each do |color|
121
117
  return color.last if @accuracy >= color.first
122
118
  end
@@ -125,12 +121,12 @@ module Lazylead
125
121
 
126
122
  def colors
127
123
  @colors ||= begin
128
- JSON.parse(@opts["colors"])
129
- .to_h
130
- .to_a
131
- .each { |e| e[0] = e[0].to_i }
132
- .sort_by { |e| e[0] }
133
- end
124
+ JSON.parse(@opts["colors"])
125
+ .to_h
126
+ .to_a
127
+ .each { |e| e[0] = e[0].to_i }
128
+ .sort_by { |e| e[0] }
129
+ end
134
130
  end
135
131
 
136
132
  # Calculate grade for accuracy