lazylead 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -114,9 +114,7 @@ module Lazylead
114
114
  end
115
115
 
116
116
  def color
117
- if colors.nil? || !defined?(@score) || !@score.is_a?(Numeric)
118
- return "#061306"
119
- end
117
+ return "#061306" if colors.nil? || !defined?(@score) || !@score.is_a?(Numeric)
120
118
  colors.reverse_each do |color|
121
119
  return color.last if @accuracy >= color.first
122
120
  end
@@ -125,12 +123,12 @@ module Lazylead
125
123
 
126
124
  def colors
127
125
  @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
126
+ JSON.parse(@opts["colors"])
127
+ .to_h
128
+ .to_a
129
+ .each { |e| e[0] = e[0].to_i }
130
+ .sort_by { |e| e[0] }
131
+ end
134
132
  end
135
133
 
136
134
  # Calculate grade for accuracy
@@ -27,10 +27,6 @@ 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
31
  issue.attachments.any?(&method(:matching))
36
32
  end
@@ -27,18 +27,18 @@ 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
37
  def matching(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 logs.7z log.7z].any? do |l|
40
- name.end_with? l
41
- end
40
+ return true if File.extname(name).start_with? ".log", ".txt", ".out"
41
+ @files.any? { |l| name.end_with? l }
42
42
  end
43
43
  end
44
44
  end
@@ -0,0 +1,148 @@
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
+
28
+ module Lazylead
29
+ module Task
30
+ #
31
+ # Ensure that ticket accuracy evaluation labels are set only by LL, not by
32
+ # some person.
33
+ #
34
+ # The task supports the following features:
35
+ # - fetch issues from remote ticketing system by query
36
+ # - check the history of labels modification
37
+ # - remove evaluation labels if they set not by LL
38
+ class OnlyLL
39
+ def initialize(log = Log.new)
40
+ @log = log
41
+ end
42
+
43
+ def run(sys, postman, opts)
44
+ found = sys.issues(opts["jql"],
45
+ opts.jira_defaults.merge(expand: "changelog"))
46
+ .map { |i| Labels.new(i, opts) }
47
+ .select(&:exists?)
48
+ .reject(&:valid?)
49
+ .each(&:remove)
50
+ postman.send opts.merge(tickets: found) unless found.empty?
51
+ end
52
+ end
53
+ end
54
+
55
+ # The ticket with grid labels
56
+ class Labels
57
+ attr_reader :issue
58
+
59
+ def initialize(issue, opts)
60
+ @issue = issue
61
+ @opts = opts
62
+ end
63
+
64
+ # Ensure that issue has evaluation labels for accuracy rules
65
+ def exists?
66
+ return false if @issue.labels.nil? || @issue.labels.empty?
67
+ return false unless @issue.labels.is_a? Array
68
+ grid.any? { |g| @issue.labels.any? { |l| g.eql? l } }
69
+ end
70
+
71
+ # Compare the score evaluated by LL and current ticket score.
72
+ # @return true if current score equal to LL evaluation
73
+ def valid?
74
+ score.eql?(@issue.labels.sort.find { |l| grid.any? { |g| l.eql? g } })
75
+ end
76
+
77
+ # Find expected ticket score evaluated by LL.
78
+ # If LL evaluated the same ticket several times (no matter why),
79
+ # then the last score would be returned.
80
+ def score
81
+ to_l(
82
+ @issue.history
83
+ .select { |h| h["author"]["key"].eql? @opts["author"] }
84
+ .select { |h| to_l(h) }
85
+ .reverse
86
+ .first
87
+ ).fetch("toString", "").split.find { |l| grid.any? { |g| l.eql? g } }
88
+ end
89
+
90
+ # Find history record with labels changes.
91
+ def to_l(history)
92
+ return {} if history.nil? || history.empty?
93
+ history["items"].find { |f| f["field"].eql? "labels" }
94
+ end
95
+
96
+ # Detect the percentage grid for tickets, by default its 0%, 10%, 20%, etc.
97
+ def grid
98
+ @grid ||= begin
99
+ if @opts.key? "grid"
100
+ @opts.slice("grid", ",")
101
+ else
102
+ %w[0% 10% 20% 30% 40% 50% 60% 70% 80% 90% 100%]
103
+ end
104
+ end
105
+ end
106
+
107
+ # Remove score labels from the ticket.
108
+ def remove
109
+ @issue.labels!(@issue.labels - grid)
110
+ end
111
+
112
+ # Detect the violators with their changes.
113
+ # .violators => ["Tom Hhhh set 40%", "Bob Mmmm set 50%"]
114
+ def violators
115
+ @issue.history
116
+ .reject { |h| h["author"]["key"].eql? @opts["author"] }
117
+ .select { |h| grid?(h) }
118
+ .group_by { |h| h["author"]["key"] }
119
+ .map do |a|
120
+ "#{a.last.first['author']['displayName']} set #{hacked(a.last)}"
121
+ end
122
+ end
123
+
124
+ # Ensure that history record has label change related to LL grid labels.
125
+ # @return true if LL grid labels added
126
+ def grid?(record)
127
+ diff(record).any? { |d| d.find { |l| grid.any? { |g| l.eql? g } } }
128
+ end
129
+
130
+ # Detect label diff in single history record from ticket's history.
131
+ def diff(record)
132
+ record["items"].select { |f| f["field"].eql? "labels" }
133
+ .reject { |f| f["toString"].nil? || f["toString"].blank? }
134
+ .map do |f|
135
+ from = []
136
+ from = f["fromString"].split unless f["fromString"].nil?
137
+ f["toString"].split - from
138
+ end
139
+ end
140
+
141
+ # Hacked score by violator in ticket's history.
142
+ def hacked(record)
143
+ diff(record.first)
144
+ .first
145
+ .find { |l| grid.any? { |g| l.eql? g } }
146
+ end
147
+ end
148
+ end
@@ -38,13 +38,22 @@ module Lazylead
38
38
 
39
39
  def passed(issue)
40
40
  return true if @envs.empty?
41
- lines = issue["environment"].to_s + "\n" + issue.description
42
- lines.split("\n")
43
- .reject(&:blank?)
44
- .map(&:strip)
45
- .flat_map { |l| l.split(" ").map(&:strip) }
46
- .select { |w| w.include?("http://") || w.include?("https://") }
47
- .any? { |u| @envs.any? { |e| u.match? e } }
41
+ "#{issue['environment']}\n#{issue.description}".split("\n")
42
+ .reject(&:blank?)
43
+ .map(&:strip)
44
+ .flat_map { |l| l.split.map(&:strip) }
45
+ .select(&method(:url?))
46
+ .any?(&method(:match?))
47
+ end
48
+
49
+ # Ensure that particular text contains web url
50
+ def url?(text)
51
+ text.include?("http://") || text.include?("https://")
52
+ end
53
+
54
+ # Ensure that particular url matches expected servers urls
55
+ def match?(url)
56
+ @envs.any? { |e| url.match? e }
48
57
  end
49
58
  end
50
59
  end
@@ -60,7 +60,7 @@ module Lazylead
60
60
  words = desc.gsub(/{(c|C)(o|O)(d|D)(e|E)/, " {code")
61
61
  .gsub("}", "} ")
62
62
  .gsub("Caused by:", "Caused_by:")
63
- .split(" ")
63
+ .split
64
64
  .map(&:strip)
65
65
  .reject(&:blank?)
66
66
  pairs(words, "{code").map { |s| words[s.first..s.last].join("\n") }
@@ -106,7 +106,7 @@ module Lazylead
106
106
  v = a.last
107
107
  d[k] = expected[k] if v.nil? || v.blank?
108
108
  next if v.nil?
109
- d[k] = v + "," + expected[k] unless v.to_s.include? expected[k]
109
+ d[k] = "#{v},#{expected[k]}" unless v.to_s.include? expected[k]
110
110
  end
111
111
  end
112
112
 
@@ -47,21 +47,28 @@ module Lazylead
47
47
 
48
48
  # Return all svn commits for particular date range in repo
49
49
  def svn_log(opts)
50
- now = if opts.key? "now"
51
- DateTime.parse(opts["now"])
52
- else
53
- DateTime.now
54
- end
55
- start = (now.to_time - opts["period"].to_i).to_datetime
56
50
  cmd = [
57
51
  "svn log --diff --no-auth-cache",
58
52
  "--username #{opts.decrypt('svn_user', 'svn_salt')}",
59
53
  "--password #{opts.decrypt('svn_password', 'svn_salt')}",
60
- "-r {#{start}}:{#{now}} #{opts['svn_url']}"
54
+ "-r {#{from(opts)}}:{#{now(opts)}} #{opts['svn_url']}"
61
55
  ]
62
56
  stdout = `#{cmd.join(" ")}`
63
- stdout.split("-" * 72).reject(&:blank?).reverse
64
- .map { |e| Entry.new(e) }
57
+ stdout.split("-" * 72).reject(&:blank?).reverse.map { |e| Entry.new(e) }
58
+ end
59
+
60
+ # The start date & time for search range
61
+ def from(opts)
62
+ (now(opts).to_time - opts["period"].to_i).to_datetime
63
+ end
64
+
65
+ # The current date & time for search range
66
+ def now(opts)
67
+ if opts.key? "now"
68
+ DateTime.parse(opts["now"])
69
+ else
70
+ DateTime.now
71
+ end
65
72
  end
66
73
  end
67
74
  end
@@ -96,8 +103,8 @@ module Lazylead
96
103
  # The modified lines contains expected text
97
104
  def includes?(text)
98
105
  text = [text] unless text.respond_to? :each
99
- lines[4..-1].select { |l| l.start_with? "+" }
100
- .any? { |l| text.any? { |t| l.include? t } }
106
+ lines[4..].select { |l| l.start_with? "+" }
107
+ .any? { |l| text.any? { |t| l.include? t } }
101
108
  end
102
109
 
103
110
  def lines
@@ -111,12 +118,12 @@ module Lazylead
111
118
  # Detect SVN diff lines with particular text
112
119
  def diff(text)
113
120
  @diff ||= begin
114
- files = affected(text).uniq
115
- @commit.split("Index: ")
116
- .select { |i| files.any? { |f| i.start_with? f } }
117
- .map { |i| i.split "\n" }
118
- .flatten
119
- end
121
+ files = affected(text).uniq
122
+ @commit.split("Index: ")
123
+ .select { |i| files.any? { |f| i.start_with? f } }
124
+ .map { |i| i.split "\n" }
125
+ .flatten
126
+ end
120
127
  end
121
128
 
122
129
  # Detect affected files with particular text
@@ -125,7 +132,7 @@ module Lazylead
125
132
  lines[i].start_with?("+") && text.any? { |t| lines[i].include? t }
126
133
  end
127
134
  occurrences.map do |occ|
128
- lines[2..occ].reverse.find { |l| l.start_with? "Index: " }[7..-1]
135
+ lines[2..occ].reverse.find { |l| l.start_with? "Index: " }[7..]
129
136
  end
130
137
  end
131
138
  end
@@ -52,9 +52,7 @@ module Lazylead
52
52
  svn_log(opts).xpath("//logentry[paths/path[#{xpath}]]")
53
53
  .map(&method(:to_entry))
54
54
  .each do |e|
55
- if e.paths.path.respond_to? :delete_if
56
- e.paths.path.delete_if { |p| files.none? { |f| p.include? f } }
57
- end
55
+ e.paths.path.delete_if { |p| files.none? { |f| p.include? f } } if e.paths.path.respond_to? :delete_if
58
56
  end
59
57
  end
60
58
 
@@ -23,5 +23,5 @@
23
23
  # OR OTHER DEALINGS IN THE SOFTWARE.
24
24
 
25
25
  module Lazylead
26
- VERSION = "0.6.2"
26
+ VERSION = "0.7.0"
27
27
  end
@@ -0,0 +1,107 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <style> /* CSS styles taken from https://github.com/yegor256/tacit */
5
+ th {
6
+ font-weight: 600
7
+ }
8
+
9
+ table tr {
10
+ border-bottom-width: 2.16px
11
+ }
12
+
13
+ table tr th {
14
+ border-bottom-width: 2.16px
15
+ }
16
+
17
+ table tr td, table tr th {
18
+ overflow: hidden;
19
+ padding: 5.4px 3.6px;
20
+ line-height: 14px;
21
+ }
22
+
23
+ #summary {
24
+ text-align: left;
25
+ }
26
+
27
+ .auto {
28
+ min-width: auto;
29
+ white-space: nowrap;
30
+ }
31
+
32
+ a {
33
+ color: #275a90;
34
+ text-decoration: none
35
+ }
36
+
37
+ a:hover {
38
+ text-decoration: underline
39
+ }
40
+
41
+ * {
42
+ border: 0;
43
+ border-collapse: separate;
44
+ border-spacing: 0;
45
+ box-sizing: border-box;
46
+ margin: 0;
47
+ max-width: 100%;
48
+ padding: 0;
49
+ vertical-align: baseline;
50
+ font-family: system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;
51
+ font-size: 13px;
52
+ font-stretch: normal;
53
+ font-style: normal;
54
+ font-weight: 400;
55
+ line-height: 29.7px
56
+ }
57
+
58
+ html, body {
59
+ width: 100%
60
+ }
61
+
62
+ html {
63
+ height: 100%
64
+ }
65
+
66
+ body {
67
+ background: #fff;
68
+ color: #1a1919;
69
+ padding: 36px
70
+ }
71
+ </style>
72
+ <title>Only LL</title>
73
+ </head>
74
+ <body>
75
+ <p>Hi,</p>
76
+ <p>The LL accuracy labels are cleaned from the following tickets:</p>
77
+ <table summary="table with tickets triage score">
78
+ <tr>
79
+ <th id="key">Key</th>
80
+ <th id="priority">Priority</th>
81
+ <th id="reporter">Reporter</th>
82
+ <th id="violators">Violators</th>
83
+ <th id="summary">Summary</th>
84
+ <th id="labels">Labels</th>
85
+ </tr>
86
+ <% tickets.each do |t| %>
87
+ <tr>
88
+ <td>
89
+ <div class="auto">
90
+ <a href="<%= t.issue.url %>"><%= t.issue.key %></a>
91
+ </div>
92
+ </td>
93
+ <td><%= t.issue.priority %></td>
94
+ <td>
95
+ <div class="auto"><%= t.issue.reporter.name %></div>
96
+ </td>
97
+ <td><%= t.violators.join(', ') %></td>
98
+ <td><%= t.issue.summary %></td>
99
+ <td><%= t.issue.labels.join(', ') %></td>
100
+ </tr>
101
+ <% end %>
102
+ </table>
103
+ <p>Posted by
104
+ <a href="https://github.com/dgroup/lazylead">lazylead v<%= version %></a>.
105
+ </p>
106
+ </body>
107
+ </html>