lazylead 0.3.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.0pdd.yml +4 -1
  3. data/.docs/accuracy.md +107 -0
  4. data/.docs/accuracy_email.jpg +0 -0
  5. data/.docs/accuracy_jira_comment.jpg +0 -0
  6. data/.docs/duedate_expired.md +3 -3
  7. data/.docs/propagate_down.md +4 -4
  8. data/.gitattributes +1 -0
  9. data/.github/dependabot.yml +6 -0
  10. data/.pdd +1 -1
  11. data/.rubocop.yml +6 -0
  12. data/Rakefile +2 -0
  13. data/bin/lazylead +7 -4
  14. data/lazylead.gemspec +5 -4
  15. data/lib/lazylead/exchange.rb +16 -9
  16. data/lib/lazylead/log.rb +30 -8
  17. data/lib/lazylead/model.rb +78 -22
  18. data/lib/lazylead/opts.rb +80 -0
  19. data/lib/lazylead/postman.rb +1 -1
  20. data/lib/lazylead/schedule.rb +18 -17
  21. data/lib/lazylead/smtp.rb +1 -1
  22. data/lib/lazylead/system/jira.rb +55 -14
  23. data/lib/lazylead/system/synced.rb +2 -1
  24. data/lib/lazylead/task/accuracy/accuracy.rb +136 -0
  25. data/lib/lazylead/task/accuracy/affected_build.rb +39 -0
  26. data/lib/lazylead/task/accuracy/attachment.rb +44 -0
  27. data/lib/lazylead/task/accuracy/environment.rb +39 -0
  28. data/lib/lazylead/task/accuracy/logs.rb +40 -0
  29. data/lib/lazylead/task/accuracy/records.rb +45 -0
  30. data/lib/lazylead/task/accuracy/requirement.rb +49 -0
  31. data/lib/lazylead/task/accuracy/servers.rb +50 -0
  32. data/lib/lazylead/task/accuracy/stacktrace.rb +63 -0
  33. data/lib/lazylead/task/accuracy/testcase.rb +75 -0
  34. data/lib/lazylead/task/accuracy/wiki.rb +41 -0
  35. data/lib/lazylead/task/alert.rb +8 -6
  36. data/lib/lazylead/task/confluence_ref.rb +4 -3
  37. data/lib/lazylead/task/echo.rb +22 -0
  38. data/lib/lazylead/task/fix_version.rb +18 -7
  39. data/lib/lazylead/task/missing_comment.rb +7 -5
  40. data/lib/lazylead/task/propagate_down.rb +11 -3
  41. data/lib/lazylead/task/savepoint.rb +1 -1
  42. data/lib/lazylead/task/touch.rb +119 -0
  43. data/lib/lazylead/version.rb +1 -1
  44. data/lib/messages/accuracy.erb +118 -0
  45. data/lib/messages/svn_log.erb +117 -0
  46. data/lib/messages/svn_touch.erb +147 -0
  47. data/license.txt +1 -1
  48. data/readme.md +20 -19
  49. data/test/lazylead/cc_test.rb +2 -2
  50. data/test/lazylead/cli/app_test.rb +12 -12
  51. data/test/lazylead/exchange_test.rb +3 -3
  52. data/test/lazylead/model_test.rb +4 -4
  53. data/test/lazylead/opts_test.rb +70 -0
  54. data/test/lazylead/postman_test.rb +1 -1
  55. data/test/lazylead/smtp_test.rb +1 -1
  56. data/test/lazylead/system/jira_test.rb +65 -1
  57. data/test/lazylead/task/accuracy/accuracy_test.rb +73 -0
  58. data/test/lazylead/task/accuracy/affected_build_test.rb +42 -0
  59. data/test/lazylead/task/accuracy/attachment_test.rb +50 -0
  60. data/test/lazylead/task/accuracy/environment_test.rb +42 -0
  61. data/test/lazylead/task/accuracy/logs_test.rb +78 -0
  62. data/test/lazylead/task/accuracy/records_test.rb +60 -0
  63. data/test/lazylead/task/accuracy/servers_test.rb +66 -0
  64. data/test/lazylead/task/accuracy/stacktrace_test.rb +113 -0
  65. data/test/lazylead/task/accuracy/testcase_test.rb +205 -0
  66. data/test/lazylead/task/accuracy/wiki_test.rb +40 -0
  67. data/test/lazylead/task/assignee_alert_test.rb +2 -2
  68. data/test/lazylead/task/duedate_test.rb +36 -26
  69. data/test/lazylead/task/fix_version_test.rb +9 -6
  70. data/test/lazylead/task/missing_comment_test.rb +11 -9
  71. data/test/lazylead/task/propagate_down_test.rb +4 -2
  72. data/test/lazylead/task/touch_test.rb +88 -0
  73. data/test/test.rb +25 -0
  74. data/upgrades/sqlite/001-install-main-lazylead-tables.sql +1 -5
  75. data/upgrades/sqlite/999.testdata.sql +12 -16
  76. metadata +65 -8
  77. data/.travis.yml +0 -16
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License
4
+ #
5
+ # Copyright (c) 2019-2020 Yurii Dubinka
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"),
9
+ # to deal in the Software without restriction, including without limitation
10
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
11
+ # and/or sell copies of the Software, and to permit persons to whom
12
+ # the Software is furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included
15
+ # in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
22
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23
+ # OR OTHER DEALINGS IN THE SOFTWARE.
24
+
25
+ require "forwardable"
26
+ require_relative "salt"
27
+
28
+ module Lazylead
29
+ #
30
+ # Default options for all lazylead tasks.
31
+ #
32
+ # Author:: Yurii Dubinka (yurii.dubinka@gmail.com)
33
+ # Copyright:: Copyright (c) 2019-2020 Yurii Dubinka
34
+ # License:: MIT
35
+ class Opts
36
+ extend Forwardable
37
+ def_delegators :@origin, :[], :[]=, :to_s, :key?, :fetch, :merge, :except
38
+
39
+ def initialize(origin = {})
40
+ @origin = origin
41
+ end
42
+
43
+ # Split text value by delimiter, trim all spaces and reject blank items
44
+ def slice(key, delim)
45
+ to_h[key].split(delim).map(&:chomp).map(&:strip).reject(&:blank?)
46
+ end
47
+
48
+ def blank?(key)
49
+ to_h[key].nil? || @origin[key].blank?
50
+ end
51
+
52
+ def to_h
53
+ @origin
54
+ end
55
+
56
+ # Default Jira options to use during search for all Jira-based tasks.
57
+ def jira_defaults
58
+ {
59
+ max_results: fetch("max_results", 50),
60
+ fields: jira_fields
61
+ }
62
+ end
63
+
64
+ # Default fields which to fetch within the Jira issue
65
+ def jira_fields
66
+ to_h.fetch("fields", "").split(",").map(&:to_sym)
67
+ end
68
+
69
+ # Decrypt particular option using cryptography salt
70
+ # @param key option to be decrypted
71
+ # @param sid the name of the salt to be used for the description
72
+ # @see Lazylead::Salt
73
+ def decrypt(key, sid)
74
+ text = to_h[key]
75
+ return text if text.blank? || text.nil?
76
+ return Salt.new(sid).decrypt(text) if ENV.key? sid
77
+ text
78
+ end
79
+ end
80
+ end
@@ -46,7 +46,7 @@ module Lazylead
46
46
  class Postman
47
47
  include Emailing
48
48
 
49
- def initialize(log = Log::NOTHING)
49
+ def initialize(log = Log.new)
50
50
  @log = log
51
51
  end
52
52
 
@@ -34,25 +34,20 @@ module Lazylead
34
34
  # Copyright:: Copyright (c) 2019-2020 Yurii Dubinka
35
35
  # License:: MIT
36
36
  class Schedule
37
- # @todo #/DEV New scheduling types like 'at', 'once' is required.
38
- # The minimum time period for cron is 1 minute and it's not suitable for
39
- # unit testing, thus its better to introduce new types which allows to
40
- # schedule some task once or at particular time period like in next 200ms).
41
- # For cron expressions we should define separate test suite which will test
42
- # in parallel without blocking main CI process.
43
- def initialize(log = Log::NOTHING, cling = true)
37
+ def initialize(log: Log.new, cling: true, trigger: Rufus::Scheduler.new)
44
38
  @log = log
45
39
  @cling = cling
46
- @trigger = Rufus::Scheduler.new
40
+ @trigger = trigger
47
41
  end
48
42
 
49
- # @todo #/DEV error code is required for reach 'raise' statement within the
50
- # application.
43
+ # @todo #/DEV error code is required for each 'raise' statement within the
44
+ # application. Align the naming of existing one, the error code should be
45
+ # like ll-xxx.
51
46
  def register(task)
52
47
  raise "ll-002: task can't be a null" if task.nil?
53
- @trigger.cron task.cron do
48
+ @trigger.method(task.type).call(task.unit) do
54
49
  ActiveRecord::Base.connection_pool.with_connection do
55
- task.exec @log
50
+ ORM::VerboseTask.new(task, @log).exec
56
51
  end
57
52
  end
58
53
  @log.debug "Task scheduled: #{task}"
@@ -60,7 +55,9 @@ module Lazylead
60
55
 
61
56
  # @todo #/DEV inspect the current execution status. This method should
62
57
  # support several format for output, by default is `json`.
63
- def ps; end
58
+ def ps
59
+ @log.debug "#{self}#ps"
60
+ end
64
61
 
65
62
  def join
66
63
  @trigger.join if @cling
@@ -75,16 +72,20 @@ module Lazylead
75
72
 
76
73
  # Fake application schedule for unit testing purposes
77
74
  class NoSchedule
78
- def initialize(log = Log::NOTHING)
75
+ def initialize(log = Log.new)
79
76
  @log = log
80
77
  end
81
78
 
82
79
  def register(task)
83
- @log.debug("Task registered: #{task}")
80
+ @log.debug "Task registered: #{task}"
84
81
  end
85
82
 
86
- def ps; end
83
+ def ps
84
+ @log.debug "#{self}#ps"
85
+ end
87
86
 
88
- def join; end
87
+ def join
88
+ @log.debug "#{self}#join"
89
+ end
89
90
  end
90
91
  end
@@ -34,7 +34,7 @@ module Lazylead
34
34
  # Copyright:: Copyright (c) 2019-2020 Yurii Dubinka
35
35
  # License:: MIT
36
36
  class Smtp
37
- def initialize(log = Log::NOTHING, salt = NoSalt.new, opts = {})
37
+ def initialize(log = Log.new, salt = NoSalt.new, opts = {})
38
38
  @log = log
39
39
  @salt = salt
40
40
  @opts = opts
@@ -23,6 +23,7 @@
23
23
  # OR OTHER DEALINGS IN THE SOFTWARE.
24
24
 
25
25
  require "jira-ruby"
26
+ require "forwardable"
26
27
  require_relative "../salt"
27
28
 
28
29
  module Lazylead
@@ -35,7 +36,7 @@ module Lazylead
35
36
  # @todo #57/DEV The debug method should be moved outside of ctor.
36
37
  # This was moved here from 'client' method because Rubocop failed the build
37
38
  # due to 'Metrics/AbcSize' violation.
38
- def initialize(opts, salt = NoSalt.new, log = Log::NOTHING)
39
+ def initialize(opts, salt = NoSalt.new, log = Log.new)
39
40
  @opts = opts
40
41
  @salt = salt
41
42
  @log = log
@@ -149,6 +150,11 @@ module Lazylead
149
150
  @issue.key
150
151
  end
151
152
 
153
+ def description
154
+ return "" if @issue.description.nil?
155
+ @issue.description
156
+ end
157
+
152
158
  def summary
153
159
  fields["summary"]
154
160
  end
@@ -174,9 +180,24 @@ module Lazylead
174
180
  end
175
181
 
176
182
  def fields
183
+ return {} if @issue.nil?
184
+ return {} unless @issue.respond_to? :fields
185
+ return {} if @issue.fields.nil?
186
+ return {} unless @issue.fields.respond_to? :[]
177
187
  @issue.fields
178
188
  end
179
189
 
190
+ def [](name)
191
+ return "" if fields[name].nil? || fields[name].blank?
192
+ fields[name]
193
+ end
194
+
195
+ def components
196
+ return [] unless @issue.respond_to? :components
197
+ return [] if @issue.components.nil?
198
+ @issue.components.map(&:name)
199
+ end
200
+
180
201
  def history
181
202
  return [] unless @issue.respond_to? :changelog
182
203
  return [] if @issue.changelog == nil? || @issue.changelog.empty?
@@ -201,6 +222,29 @@ module Lazylead
201
222
  def status
202
223
  @issue.status.attrs["name"]
203
224
  end
225
+
226
+ def post(markdown)
227
+ @issue.comments.build.save!(body: markdown)
228
+ end
229
+
230
+ def remote_links
231
+ @issue.remotelink.all
232
+ end
233
+
234
+ def attachments
235
+ @issue.attachments
236
+ end
237
+
238
+ def add_label(label, *more)
239
+ labels = @issue.labels
240
+ labels << label
241
+ labels += more if more.size.positive?
242
+ save!("fields" => { "labels" => labels.uniq })
243
+ end
244
+
245
+ def save!(opts)
246
+ @issue.save(opts)
247
+ end
204
248
  end
205
249
 
206
250
  # The jira issue comments
@@ -247,24 +291,21 @@ module Lazylead
247
291
  # Jira instance without authentication in order to access public filters
248
292
  # or dashboards.
249
293
  class NoAuthJira
250
- def initialize(url, path = "", log = Log::NOTHING)
294
+ extend Forwardable
295
+ def_delegators :@jira, :issues, :raw
296
+
297
+ def initialize(url, path = "", log = Log.new)
251
298
  @jira = Jira.new(
252
- { username: nil, password: nil, site: url, context_path: path },
299
+ {
300
+ username: nil,
301
+ password: nil,
302
+ site: url,
303
+ context_path: path
304
+ },
253
305
  NoSalt.new,
254
306
  log
255
307
  )
256
308
  end
257
-
258
- def issues(jql, opts = {})
259
- @jira.issues(jql, opts)
260
- end
261
-
262
- # Execute request to the ticketing system using raw client.
263
- # For Jira the raw client is 'jira-ruby' gem.
264
- def raw(&block)
265
- raise "ll-07: No block given to method" unless block_given?
266
- @jira.raw(&block)
267
- end
268
309
  end
269
310
 
270
311
  # A fake jira system which allows to work with sub-tasks.
@@ -30,7 +30,8 @@ module Lazylead
30
30
  @sys = sys
31
31
  end
32
32
 
33
- # @todo #/DEV Unit tests for 'issues' function
33
+ # @todo #/DEV Unit tests for 'issues' function, moreover the other methods
34
+ # from ticketing system obj are required
34
35
  #
35
36
  def issues(jql)
36
37
  @mutex.synchronize do
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License
4
+ #
5
+ # Copyright (c) 2019-2020 Yurii Dubinka
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"),
9
+ # to deal in the Software without restriction, including without limitation
10
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
11
+ # and/or sell copies of the Software, and to permit persons to whom
12
+ # the Software is furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included
15
+ # in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
22
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23
+ # OR OTHER DEALINGS IN THE SOFTWARE.
24
+
25
+ require_relative "../../log"
26
+ require_relative "../../opts"
27
+ require_relative "../../email"
28
+ require_relative "../../version"
29
+ require_relative "../../postman"
30
+
31
+ module Lazylead
32
+ module Task
33
+ #
34
+ # Evaluate ticket format and accuracy
35
+ #
36
+ # The task supports the following features:
37
+ # - fetch issues from remote ticketing system by query
38
+ # - evaluate each field within the ticket
39
+ # - post the score to the ticket
40
+ class Accuracy
41
+ def initialize(log = Log.new)
42
+ @log = log
43
+ end
44
+
45
+ def run(sys, postman, opts)
46
+ Dir[File.join(__dir__, "*.rb")].sort.each { |f| require f }
47
+ rules = opts.slice("rules", ",")
48
+ .map(&:constantize)
49
+ .map(&:new)
50
+ raised = sys.issues(opts["jql"], opts.jira_defaults)
51
+ .map { |i| Score.new(i, opts, rules) }
52
+ .each(&:evaluate)
53
+ .each(&:post)
54
+ postman.send opts.merge(tickets: raised) unless raised.empty?
55
+ end
56
+ end
57
+ end
58
+
59
+ # The ticket score based on fields content.
60
+ class Score
61
+ attr_reader :issue, :total, :score, :accuracy
62
+
63
+ def initialize(issue, opts, rules)
64
+ @issue = issue
65
+ @link = opts["docs"]
66
+ @opts = opts
67
+ @rules = rules
68
+ end
69
+
70
+ # Estimate the ticket score and accuracy.
71
+ # Accuracy is a percentage between current score and maximum possible value.
72
+ def evaluate(digits = 2)
73
+ @total = @rules.map(&:score).sum
74
+ @score = @rules.select { |r| r.passed(@issue) }
75
+ .map(&:score)
76
+ .sum
77
+ @accuracy = (score / @total * 100).round(digits)
78
+ end
79
+
80
+ # Post the comment with score and accuracy to the ticket.
81
+ def post
82
+ return if @opts.key? "silent"
83
+ @issue.post comment
84
+ @issue.add_label "LL.accuracy", "#{(@accuracy / 10) * 10}%"
85
+ end
86
+
87
+ # The jira comment in markdown format
88
+ def comment
89
+ comment = [
90
+ "Hi [~#{@issue.reporter.id}],",
91
+ "",
92
+ "The triage accuracy is '{color:#{color}}#{@score}{color}'" \
93
+ " (~{color:#{color}}#{@accuracy}%{color}), here are the reasons why:",
94
+ "|| Ticket requirement || Status || Field ||"
95
+ ]
96
+ @rules.each do |r|
97
+ comment << "|#{r.desc}|#{r.passed(@issue) ? '(/)' : '(-)'}|#{r.field}|"
98
+ end
99
+ comment << docs_link
100
+ comment << ""
101
+ comment << "Posted by [lazylead v#{Lazylead::VERSION}|" \
102
+ "https://bit.ly/2NjdndS]."
103
+ comment.join("\r\n")
104
+ end
105
+
106
+ # Link to ticket formatting rules
107
+ def docs_link
108
+ if @link.nil? || @link.blank?
109
+ ""
110
+ else
111
+ "The requirements/examples of ticket formatting rules you may find " \
112
+ "[here|#{@link}]."
113
+ end
114
+ end
115
+
116
+ def color
117
+ if colors.nil? || !defined?(@score) || !@score.is_a?(Numeric)
118
+ return "#061306"
119
+ end
120
+ colors.reverse_each do |color|
121
+ return color.last if @accuracy >= color.first
122
+ end
123
+ "#061306"
124
+ end
125
+
126
+ def colors
127
+ @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
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License
4
+ #
5
+ # Copyright (c) 2019-2020 Yurii Dubinka
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"),
9
+ # to deal in the Software without restriction, including without limitation
10
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
11
+ # and/or sell copies of the Software, and to permit persons to whom
12
+ # the Software is furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included
15
+ # in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
22
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23
+ # OR OTHER DEALINGS IN THE SOFTWARE.
24
+
25
+ require_relative "requirement"
26
+
27
+ module Lazylead
28
+ # A requirement that Jira field "Affects Version/s" provided by the reporter.
29
+ class AffectedBuild < Requirement
30
+ def initialize(score = 0.5)
31
+ super "Affected build", score, "Affects Version/s"
32
+ end
33
+
34
+ # @return true if an issue has non-empty "Affects Version/s" field
35
+ def passed(issue)
36
+ non_blank? issue, "versions"
37
+ end
38
+ end
39
+ end