lazylead 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) 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/propagate_down.md +1 -1
  7. data/.pdd +1 -1
  8. data/.rubocop.yml +6 -0
  9. data/bin/lazylead +6 -3
  10. data/lazylead.gemspec +4 -4
  11. data/lib/lazylead/exchange.rb +1 -1
  12. data/lib/lazylead/log.rb +30 -8
  13. data/lib/lazylead/model.rb +44 -22
  14. data/lib/lazylead/opts.rb +68 -0
  15. data/lib/lazylead/postman.rb +1 -1
  16. data/lib/lazylead/schedule.rb +3 -3
  17. data/lib/lazylead/smtp.rb +1 -1
  18. data/lib/lazylead/system/jira.rb +16 -14
  19. data/lib/lazylead/system/synced.rb +2 -1
  20. data/lib/lazylead/task/accuracy/accuracy.rb +140 -0
  21. data/lib/lazylead/task/accuracy/affected_build.rb +43 -0
  22. data/lib/lazylead/task/accuracy/requirement.rb +40 -0
  23. data/lib/lazylead/task/alert.rb +8 -6
  24. data/lib/lazylead/task/confluence_ref.rb +4 -3
  25. data/lib/lazylead/task/echo.rb +4 -0
  26. data/lib/lazylead/task/fix_version.rb +10 -6
  27. data/lib/lazylead/task/missing_comment.rb +7 -5
  28. data/lib/lazylead/task/propagate_down.rb +11 -3
  29. data/lib/lazylead/task/savepoint.rb +1 -1
  30. data/lib/lazylead/task/touch.rb +102 -0
  31. data/lib/lazylead/version.rb +1 -1
  32. data/lib/messages/accuracy.erb +118 -0
  33. data/lib/messages/svn_touch.erb +147 -0
  34. data/readme.md +17 -16
  35. data/test/lazylead/cc_test.rb +2 -2
  36. data/test/lazylead/cli/app_test.rb +2 -2
  37. data/test/lazylead/exchange_test.rb +3 -3
  38. data/test/lazylead/model_test.rb +4 -4
  39. data/test/lazylead/opts_test.rb +66 -0
  40. data/test/lazylead/postman_test.rb +1 -1
  41. data/test/lazylead/smtp_test.rb +1 -1
  42. data/test/lazylead/system/jira_test.rb +35 -1
  43. data/test/lazylead/task/accuracy/accuracy_test.rb +73 -0
  44. data/test/lazylead/task/accuracy/affected_build_test.rb +42 -0
  45. data/test/lazylead/task/assignee_alert_test.rb +2 -2
  46. data/test/lazylead/task/duedate_test.rb +36 -26
  47. data/test/lazylead/task/fix_version_test.rb +9 -6
  48. data/test/lazylead/task/missing_comment_test.rb +11 -9
  49. data/test/lazylead/task/propagate_down_test.rb +4 -2
  50. data/test/lazylead/task/touch_test.rb +63 -0
  51. data/upgrades/sqlite/999.testdata.sql +2 -1
  52. metadata +25 -7
@@ -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
 
@@ -40,7 +40,7 @@ module Lazylead
40
40
  # schedule some task once or at particular time period like in next 200ms).
41
41
  # For cron expressions we should define separate test suite which will test
42
42
  # in parallel without blocking main CI process.
43
- def initialize(log = Log::NOTHING, cling = true)
43
+ def initialize(log = Log.new, cling = true)
44
44
  @log = log
45
45
  @cling = cling
46
46
  @trigger = Rufus::Scheduler.new
@@ -52,7 +52,7 @@ module Lazylead
52
52
  raise "ll-002: task can't be a null" if task.nil?
53
53
  @trigger.cron task.cron do
54
54
  ActiveRecord::Base.connection_pool.with_connection do
55
- task.exec @log
55
+ ORM::VerboseTask.new(task, @log).exec
56
56
  end
57
57
  end
58
58
  @log.debug "Task scheduled: #{task}"
@@ -75,7 +75,7 @@ module Lazylead
75
75
 
76
76
  # Fake application schedule for unit testing purposes
77
77
  class NoSchedule
78
- def initialize(log = Log::NOTHING)
78
+ def initialize(log = Log.new)
79
79
  @log = log
80
80
  end
81
81
 
@@ -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
@@ -201,6 +202,10 @@ module Lazylead
201
202
  def status
202
203
  @issue.status.attrs["name"]
203
204
  end
205
+
206
+ def post(markdown)
207
+ @issue.comments.build.save!(body: markdown)
208
+ end
204
209
  end
205
210
 
206
211
  # The jira issue comments
@@ -247,24 +252,21 @@ module Lazylead
247
252
  # Jira instance without authentication in order to access public filters
248
253
  # or dashboards.
249
254
  class NoAuthJira
250
- def initialize(url, path = "", log = Log::NOTHING)
255
+ extend Forwardable
256
+ def_delegators :@jira, :issues, :raw
257
+
258
+ def initialize(url, path = "", log = Log.new)
251
259
  @jira = Jira.new(
252
- { username: nil, password: nil, site: url, context_path: path },
260
+ {
261
+ username: nil,
262
+ password: nil,
263
+ site: url,
264
+ context_path: path
265
+ },
253
266
  NoSalt.new,
254
267
  log
255
268
  )
256
269
  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
270
  end
269
271
 
270
272
  # 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,140 @@
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
+ require_rules
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
+
57
+ # Load all ticket accuracy rules for future verification
58
+ def require_rules
59
+ rules = File.dirname(__FILE__)
60
+ $LOAD_PATH.unshift(rules) unless $LOAD_PATH.include?(rules)
61
+ end
62
+ end
63
+ end
64
+
65
+ # The ticket score based on fields content.
66
+ class Score
67
+ attr_reader :issue, :total, :score, :accuracy
68
+
69
+ def initialize(issue, opts, rules)
70
+ @issue = issue
71
+ @link = opts["docs"]
72
+ @opts = opts
73
+ @rules = rules
74
+ end
75
+
76
+ # Estimate the ticket score and accuracy.
77
+ # Accuracy is a percentage between current score and maximum possible value.
78
+ def evaluate(digits = 2)
79
+ @total = @rules.map(&:score).sum
80
+ @score = @rules.select { |r| r.passed(@issue) }
81
+ .map(&:score)
82
+ .sum
83
+ @accuracy = (score / @total * 100).round(digits)
84
+ end
85
+
86
+ # Post the comment with score and accuracy to the ticket.
87
+ def post
88
+ @issue.post(comment) unless @opts.key? "silent"
89
+ end
90
+
91
+ # The jira comment in markdown format
92
+ def comment
93
+ comment = [
94
+ "Hi [~#{@issue.reporter.id}],",
95
+ "",
96
+ "The triage accuracy is '{color:#{color}}#{@score}{color}'" \
97
+ " (~{color:#{color}}#{@accuracy}%{color}), here are the reasons why:",
98
+ "|| Ticket requirement || Status || Field ||"
99
+ ]
100
+ @rules.each do |r|
101
+ comment << "|#{r.desc}|#{r.passed(@issue) ? '(/)' : '(-)'}|#{r.field}|"
102
+ end
103
+ comment << docs_link
104
+ comment << ""
105
+ comment << "Posted by [lazylead v#{Lazylead::VERSION}|" \
106
+ "https://bit.ly/2NjdndS]."
107
+ comment.join("\r\n")
108
+ end
109
+
110
+ # Link to ticket formatting rules
111
+ def docs_link
112
+ if @link.nil? || @link.blank?
113
+ ""
114
+ else
115
+ "The requirements/examples of ticket formatting rules you may find " \
116
+ "[here|#{@link}]."
117
+ end
118
+ end
119
+
120
+ def color
121
+ if colors.nil? || !defined?(@score) || !@score.is_a?(Numeric)
122
+ return "#061306"
123
+ end
124
+ colors.reverse_each do |color|
125
+ return color.last if @accuracy >= color.first
126
+ end
127
+ "#061306"
128
+ end
129
+
130
+ def colors
131
+ @colors ||= begin
132
+ JSON.parse(@opts["colors"])
133
+ .to_h
134
+ .to_a
135
+ .each { |e| e[0] = e[0].to_i }
136
+ .sort_by { |e| e[0] }
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,43 @@
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 "../../email"
27
+ require_relative "../../version"
28
+ require_relative "../../postman"
29
+ require_relative "requirement"
30
+
31
+ module Lazylead
32
+ # A requirement that Jira field "Affects Version/s" provided by the reporter.
33
+ class RequirementAffectedBuild < Requirement
34
+ def initialize(score = 0.5)
35
+ super "Affected build", score, "Affects Version/s"
36
+ end
37
+
38
+ # @return true if an issue has non-empty "Affects Version/s" field
39
+ def passed(issue)
40
+ !issue.fields["versions"].nil? && !issue.fields["versions"].empty?
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,40 @@
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
+ module Lazylead
26
+ # An single requirement regarding ticket format.
27
+ class Requirement
28
+ attr_reader :score, :desc, :field
29
+
30
+ def initialize(desc, score, field)
31
+ @desc = desc
32
+ @score = score
33
+ @field = field
34
+ end
35
+
36
+ def passed(_)
37
+ true
38
+ end
39
+ end
40
+ end
@@ -23,6 +23,7 @@
23
23
  # OR OTHER DEALINGS IN THE SOFTWARE.
24
24
 
25
25
  require_relative "../log"
26
+ require_relative "../opts"
26
27
  require_relative "../email"
27
28
  require_relative "../version"
28
29
  require_relative "../postman"
@@ -43,12 +44,13 @@ module Lazylead
43
44
  # - prepare email based on predefined template (*.erb)
44
45
  # - send the required notifications pre-defined "addressee".
45
46
  class Alert
46
- def initialize(log = Log::NOTHING)
47
+ def initialize(log = Log.new)
47
48
  @log = log
48
49
  end
49
50
 
50
51
  def run(sys, postman, opts)
51
- postman.send opts.merge(tickets: sys.issues(opts["sql"]))
52
+ tickets = sys.issues(opts["sql"], opts.jira_defaults)
53
+ postman.send opts.merge(tickets: tickets) unless tickets.empty?
52
54
  end
53
55
  end
54
56
 
@@ -64,12 +66,12 @@ module Lazylead
64
66
  # The email message is sending to the assignee regarding all his/her issues,
65
67
  # not like one email per each issue.
66
68
  class AssigneeAlert
67
- def initialize(log = Log::NOTHING)
69
+ def initialize(log = Log.new)
68
70
  @log = log
69
71
  end
70
72
 
71
73
  def run(sys, postman, opts)
72
- sys.issues(opts["sql"])
74
+ sys.issues(opts["sql"], opts.jira_defaults)
73
75
  .group_by(&:assignee)
74
76
  .each do |a, t|
75
77
  postman.send opts.merge(to: a.email, addressee: a.name, tickets: t)
@@ -89,12 +91,12 @@ module Lazylead
89
91
  # The email message is sending to the assignee regarding all his/her issues,
90
92
  # not like one email per each issue.
91
93
  class ReporterAlert
92
- def initialize(log = Log::NOTHING)
94
+ def initialize(log = Log.new)
93
95
  @log = log
94
96
  end
95
97
 
96
98
  def run(sys, postman, opts)
97
- sys.issues(opts["sql"])
99
+ sys.issues(opts["sql"], opts.jira_defaults)
98
100
  .group_by(&:reporter)
99
101
  .each do |a, t|
100
102
  postman.send opts.merge(to: a.email, addressee: a.name, tickets: t)
@@ -26,6 +26,7 @@ require "json"
26
26
  require "faraday"
27
27
  require_relative "../system/jira"
28
28
  require_relative "../log"
29
+ require_relative "../opts"
29
30
  require_relative "../confluence"
30
31
 
31
32
  module Lazylead
@@ -35,14 +36,14 @@ module Lazylead
35
36
  # @todo #/DEV Support sub-task for link search. Potentially, the issue
36
37
  # might have sub-tasks where discussion ongoing.
37
38
  class ConfluenceRef
38
- def initialize(log = Log::NOTHING)
39
+ def initialize(log = Log.new)
39
40
  @log = log
40
41
  end
41
42
 
42
43
  def run(sys, _, opts)
43
44
  confluences = confluences(opts)
44
45
  return if confluences.empty?
45
- sys.issues(opts["jql"])
46
+ sys.issues(opts["jql"], opts.jira_defaults)
46
47
  .map { |i| Link.new(i, sys, confluences) }
47
48
  .each(&:fetch_links)
48
49
  .select(&:need_link?)
@@ -50,7 +51,7 @@ module Lazylead
50
51
  end
51
52
 
52
53
  def confluences(opts)
53
- return [] if opts["confluences"].nil? || opts["confluences"].blank?
54
+ return [] if opts.blank? "confluences"
54
55
  JSON.parse(opts["confluences"], object_class: OpenStruct)
55
56
  .map { |c| Confluence.new(c) }
56
57
  end
@@ -30,6 +30,10 @@ module Lazylead
30
30
  # Copyright:: Copyright (c) 2019-2020 Yurii Dubinka
31
31
  # License:: MIT
32
32
  class Echo
33
+ def initialize(log = Log.new)
34
+ @log = log
35
+ end
36
+
33
37
  def run(_, _, _)
34
38
  self.class.to_s
35
39
  end
@@ -23,24 +23,28 @@
23
23
  # OR OTHER DEALINGS IN THE SOFTWARE.
24
24
 
25
25
  require "date"
26
- require_relative "../system/jira"
27
26
  require_relative "../log"
27
+ require_relative "../opts"
28
+ require_relative "../system/jira"
28
29
 
29
30
  module Lazylead
30
31
  module Task
31
32
  # @todo #/DEV Each task should verify input arguments.
32
33
  # The common API should be provided for each task.
33
34
  class FixVersion
34
- def initialize(log = Log::NOTHING)
35
+ def initialize(log = Log.new)
35
36
  @log = log
36
37
  end
37
38
 
38
39
  def run(sys, postman, opts)
39
- allowed = opts["allowed"].split(",").map(&:strip).reject(&:blank?)
40
+ allowed = opts.slice("allowed", ",")
41
+ issues = sys.issues(
42
+ opts["jql"], opts.jira_defaults.merge(expand: "changelog")
43
+ )
44
+ return if issues.empty?
40
45
  postman.send opts.merge(
41
- versions: sys.issues(opts["jql"], expand: "changelog")
42
- .map { |i| Version.new(i, allowed) }
43
- .select(&:changed?)
46
+ versions: issues.map { |i| Version.new(i, allowed) }
47
+ .select(&:changed?)
44
48
  )
45
49
  end
46
50
  end