lazylead 0.6.0 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) 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 -27
  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 +64 -2
  17. data/lib/lazylead/postman.rb +13 -18
  18. data/lib/lazylead/salt.rb +1 -0
  19. data/lib/lazylead/system/jira.rb +9 -2
  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 -4
  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 +1 -1
  27. data/lib/lazylead/task/accuracy/testcase.rb +8 -7
  28. data/lib/lazylead/task/fix_version.rb +4 -2
  29. data/lib/lazylead/task/propagate_down.rb +1 -1
  30. data/lib/lazylead/task/svn/diff.rb +13 -12
  31. data/lib/lazylead/task/svn/grep.rb +40 -12
  32. data/lib/lazylead/task/svn/touch.rb +2 -4
  33. data/lib/lazylead/version.rb +1 -1
  34. data/lib/messages/illegal_fixversion_change.erb +8 -2
  35. data/lib/messages/only_ll.erb +107 -0
  36. data/lib/messages/svn_diff.erb +9 -9
  37. data/lib/messages/svn_diff_attachment.erb +19 -9
  38. data/lib/messages/svn_grep.erb +1 -1
  39. data/test/lazylead/cc_test.rb +1 -0
  40. data/test/lazylead/model_test.rb +20 -0
  41. data/test/lazylead/opts_test.rb +47 -0
  42. data/test/lazylead/postman_test.rb +8 -5
  43. data/test/lazylead/smoke_test.rb +13 -0
  44. data/test/lazylead/smtp_test.rb +1 -4
  45. data/test/lazylead/task/accuracy/attachment_test.rb +1 -1
  46. data/test/lazylead/task/accuracy/logs_test.rb +12 -0
  47. data/test/lazylead/task/accuracy/onlyll_test.rb +138 -0
  48. data/test/lazylead/task/accuracy/servers_test.rb +2 -2
  49. data/test/lazylead/task/accuracy/testcase_test.rb +56 -0
  50. data/test/lazylead/task/duedate_test.rb +3 -10
  51. data/test/lazylead/task/fix_version_test.rb +1 -0
  52. data/test/lazylead/task/propagate_down_test.rb +4 -3
  53. data/test/lazylead/task/savepoint_test.rb +9 -6
  54. data/test/lazylead/task/svn/grep_test.rb +43 -1
  55. data/test/test.rb +7 -8
  56. data/upgrades/sqlite/999.testdata.sql +3 -1
  57. metadata +117 -57
@@ -25,26 +25,6 @@
25
25
  require "tilt"
26
26
 
27
27
  module Lazylead
28
- # Email notifications utilities.
29
- module Emailing
30
- # Construct html document from template and binds.
31
- def make_body(opts)
32
- Email.new(
33
- opts["template"],
34
- opts.merge(version: Lazylead::VERSION)
35
- ).render
36
- end
37
-
38
- # Split text with email addresses by ',' and trim all elements if needed.
39
- def split(type, opts)
40
- if opts[type].include? ","
41
- opts[type].split(",").map(&:strip).reject(&:empty?)
42
- else
43
- [opts[type]]
44
- end
45
- end
46
- end
47
-
48
28
  # An email regarding tickets based on file with markup.
49
29
  #
50
30
  # The 'tilt' gem was used as a template engine.
@@ -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
@@ -43,7 +44,13 @@ module Lazylead
43
44
  # Split text value by delimiter, trim all spaces and reject blank items
44
45
  def slice(key, delim)
45
46
  return [] unless to_h.key? key
46
- to_h[key].split(delim).map(&:chomp).map(&:strip).reject(&:blank?)
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?)
47
54
  end
48
55
 
49
56
  def blank?(key)
@@ -77,5 +84,60 @@ module Lazylead
77
84
  return Salt.new(sid).decrypt(text) if ENV.key? sid
78
85
  text
79
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
80
142
  end
81
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,32 +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") || opts.key?(:attachments)
79
- attach = opts["attachments"] || opts[:attachments]
80
- return if attach.nil?
81
- attach.select { |a| File.file? a }.each { |a| mail.add_file a }
82
- end
83
78
  end
84
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
@@ -177,7 +177,7 @@ module Lazylead
177
177
  end
178
178
 
179
179
  def url
180
- @issue.attrs["self"].split("/rest/api/").first + "/browse/" + key
180
+ "#{@issue.attrs['self'].split('/rest/api/').first}/browse/#{key}"
181
181
  end
182
182
 
183
183
  def duedate
@@ -257,13 +257,20 @@ module Lazylead
257
257
  lbl = [] if lbl.nil?
258
258
  lbl << label
259
259
  lbl += more if more.size.positive?
260
- save!("fields" => { "labels" => lbl.uniq })
260
+ labels! lbl
261
261
  end
262
262
 
263
+ # Get the labels for a particular issue
263
264
  def labels
264
265
  fields["labels"]
265
266
  end
266
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
+
267
274
  def save!(opts)
268
275
  @issue.save(opts)
269
276
  end
@@ -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)}%", "#{@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
@@ -27,17 +27,13 @@ require_relative "requirement"
27
27
  module Lazylead
28
28
  # Check that ticket has an attachment.
29
29
  class Attachment < Lazylead::Requirement
30
- def initialize(desc, score, field)
31
- super(desc, score, field)
32
- end
33
-
34
30
  def passed(issue)
35
- issue.attachments.any?(&method(:matching))
31
+ issue.attachments.any? { |a| matches?(a) }
36
32
  end
37
33
 
38
34
  # Check a single attachment from ticket.
39
35
  # Potential extension point for custom verification logic.
40
- def matching(attachment)
36
+ def matches?(attachment)
41
37
  !attachment.nil?
42
38
  end
43
39
  end
@@ -27,16 +27,21 @@ require_relative "attachment"
27
27
  module Lazylead
28
28
  # Check that ticket has log file(s) in attachment.
29
29
  class Logs < Lazylead::Attachment
30
- def initialize
30
+ def initialize(files = %w[log.zip logs.zip log.gz logs.gz log.tar.gz
31
+ logs.tar.gz log.7z logs.7z log.tar logs.tar])
31
32
  super("Log files", 2, "Attachments")
33
+ @files = files
32
34
  end
33
35
 
34
36
  # Ensure that ticket has a '*.log' file more '5KB'
35
- def matching(attachment)
37
+ def matches?(attachment)
36
38
  name = attachment.attrs["filename"].downcase
37
39
  return false unless attachment.attrs["size"].to_i > 5120
38
- return true if File.extname(name).start_with?(".log", ".txt")
39
- %w[.log.zip .log.gz .log.tar.gz].any? { |l| name.end_with? l }
40
+ return true if File.extname(name).start_with? ".log", ".txt", ".out"
41
+ return true if @files.any? { |l| name.end_with? l }
42
+ %w[.zip .7z .gz tar.gz].any? do |l|
43
+ name.end_with?(l) && name.split(/(?:[^a-zA-Z0-9](?<![^\x00-\x7F]))+/).include?("logs")
44
+ end
40
45
  end
41
46
  end
42
47
  end