lazylead 0.3.0 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.0pdd.yml +4 -1
  3. data/.circleci/config.yml +2 -0
  4. data/.docs/accuracy.md +107 -0
  5. data/.docs/accuracy_email.jpg +0 -0
  6. data/.docs/accuracy_jira_comment.jpg +0 -0
  7. data/.docs/propagate_down.md +1 -1
  8. data/.github/dependabot.yml +6 -0
  9. data/.pdd +1 -1
  10. data/.rubocop.yml +6 -0
  11. data/bin/lazylead +9 -3
  12. data/lazylead.gemspec +5 -4
  13. data/lib/lazylead/cc.rb +12 -6
  14. data/lib/lazylead/cli/app.rb +4 -3
  15. data/lib/lazylead/exchange.rb +1 -1
  16. data/lib/lazylead/log.rb +30 -8
  17. data/lib/lazylead/model.rb +44 -18
  18. data/lib/lazylead/opts.rb +68 -0
  19. data/lib/lazylead/postman.rb +1 -1
  20. data/lib/lazylead/schedule.rb +6 -4
  21. data/lib/lazylead/smtp.rb +1 -1
  22. data/lib/lazylead/system/fake.rb +1 -1
  23. data/lib/lazylead/system/jira.rb +17 -15
  24. data/lib/lazylead/system/synced.rb +2 -1
  25. data/lib/lazylead/task/accuracy/accuracy.rb +140 -0
  26. data/lib/lazylead/task/accuracy/affected_build.rb +43 -0
  27. data/lib/lazylead/task/accuracy/requirement.rb +40 -0
  28. data/lib/lazylead/task/alert.rb +8 -6
  29. data/lib/lazylead/task/confluence_ref.rb +4 -3
  30. data/lib/lazylead/task/echo.rb +4 -0
  31. data/lib/lazylead/task/fix_version.rb +10 -6
  32. data/lib/lazylead/task/missing_comment.rb +7 -5
  33. data/lib/lazylead/task/propagate_down.rb +11 -3
  34. data/lib/lazylead/task/savepoint.rb +1 -1
  35. data/lib/lazylead/task/touch.rb +104 -0
  36. data/lib/lazylead/version.rb +1 -1
  37. data/lib/messages/accuracy.erb +118 -0
  38. data/lib/messages/svn_touch.erb +147 -0
  39. data/readme.md +19 -18
  40. data/test/lazylead/cc_test.rb +22 -2
  41. data/test/lazylead/cli/app_test.rb +2 -2
  42. data/test/lazylead/exchange_test.rb +3 -3
  43. data/test/lazylead/model_test.rb +4 -4
  44. data/test/lazylead/opts_test.rb +70 -0
  45. data/test/lazylead/postman_test.rb +1 -1
  46. data/test/lazylead/smtp_test.rb +1 -1
  47. data/test/lazylead/system/jira_test.rb +35 -1
  48. data/test/lazylead/task/accuracy/accuracy_test.rb +73 -0
  49. data/test/lazylead/task/accuracy/affected_build_test.rb +42 -0
  50. data/test/lazylead/task/assignee_alert_test.rb +2 -2
  51. data/test/lazylead/task/duedate_test.rb +37 -27
  52. data/test/lazylead/task/fix_version_test.rb +9 -6
  53. data/test/lazylead/task/missing_comment_test.rb +11 -9
  54. data/test/lazylead/task/propagate_down_test.rb +4 -2
  55. data/test/lazylead/task/touch_test.rb +61 -0
  56. data/test/test.rb +9 -0
  57. data/upgrades/sqlite/999.testdata.sql +2 -1
  58. metadata +40 -8
  59. data/todo.yml +0 -6
@@ -0,0 +1,68 @@
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
+
27
+ module Lazylead
28
+ #
29
+ # Default options for all lazylead tasks.
30
+ #
31
+ # Author:: Yurii Dubinka (yurii.dubinka@gmail.com)
32
+ # Copyright:: Copyright (c) 2019-2020 Yurii Dubinka
33
+ # License:: MIT
34
+ class Opts
35
+ extend Forwardable
36
+ def_delegators :@origin, :[], :[]=, :to_s, :key?, :fetch, :merge, :except
37
+
38
+ def initialize(origin = {})
39
+ @origin = origin
40
+ end
41
+
42
+ # Split text value by delimiter, trim all spaces and reject blank items
43
+ def slice(key, delim)
44
+ to_h[key].split(delim).map(&:chomp).map(&:strip).reject(&:blank?)
45
+ end
46
+
47
+ def blank?(key)
48
+ to_h[key].nil? || @origin[key].blank?
49
+ end
50
+
51
+ def to_h
52
+ @origin
53
+ end
54
+
55
+ # Default Jira options to use during search for all Jira-based tasks.
56
+ def jira_defaults
57
+ {
58
+ max_results: fetch("max_results", 50),
59
+ fields: jira_fields
60
+ }
61
+ end
62
+
63
+ # Default fields which to fetch within the Jira issue
64
+ def jira_fields
65
+ to_h.fetch("fields", "").split(",").map(&:to_sym)
66
+ end
67
+ end
68
+ 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
 
@@ -22,7 +22,7 @@
22
22
  # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23
23
  # OR OTHER DEALINGS IN THE SOFTWARE.
24
24
 
25
- require "json"
25
+ require "active_support"
26
26
  require "rufus-scheduler"
27
27
  require_relative "log"
28
28
  require_relative "model"
@@ -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
@@ -51,7 +51,9 @@ module Lazylead
51
51
  def register(task)
52
52
  raise "ll-002: task can't be a null" if task.nil?
53
53
  @trigger.cron task.cron do
54
- task.exec @log
54
+ ActiveRecord::Base.connection_pool.with_connection do
55
+ ORM::VerboseTask.new(task, @log).exec
56
+ end
55
57
  end
56
58
  @log.debug "Task scheduled: #{task}"
57
59
  end
@@ -73,7 +75,7 @@ module Lazylead
73
75
 
74
76
  # Fake application schedule for unit testing purposes
75
77
  class NoSchedule
76
- def initialize(log = Log::NOTHING)
78
+ def initialize(log = Log.new)
77
79
  @log = log
78
80
  end
79
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
@@ -34,7 +34,7 @@ module Lazylead
34
34
  @issues = issues
35
35
  end
36
36
 
37
- def issues(_, _)
37
+ def issues(*)
38
38
  @issues
39
39
  end
40
40
  end
@@ -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.
@@ -273,7 +275,7 @@ module Lazylead
273
275
  @issues = issues
274
276
  end
275
277
 
276
- def issues(_, _)
278
+ def issues(*)
277
279
  @issues
278
280
  end
279
281
 
@@ -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)