lazylead 0.4.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.docs/accuracy.md +4 -4
  3. data/.docs/duedate_expired.md +3 -3
  4. data/.docs/propagate_down.md +3 -3
  5. data/.gitattributes +1 -0
  6. data/.github/dependabot.yml +6 -0
  7. data/Rakefile +2 -0
  8. data/bin/lazylead +1 -1
  9. data/lazylead.gemspec +3 -2
  10. data/lib/lazylead/exchange.rb +15 -9
  11. data/lib/lazylead/model.rb +37 -3
  12. data/lib/lazylead/opts.rb +13 -1
  13. data/lib/lazylead/postman.rb +1 -2
  14. data/lib/lazylead/schedule.rb +16 -15
  15. data/lib/lazylead/system/jira.rb +43 -0
  16. data/lib/lazylead/task/accuracy/accuracy.rb +13 -8
  17. data/lib/lazylead/task/accuracy/affected_build.rb +2 -6
  18. data/lib/lazylead/task/accuracy/attachment.rb +44 -0
  19. data/lib/lazylead/task/accuracy/environment.rb +39 -0
  20. data/lib/lazylead/task/accuracy/logs.rb +40 -0
  21. data/lib/lazylead/task/accuracy/records.rb +45 -0
  22. data/lib/lazylead/task/accuracy/requirement.rb +9 -0
  23. data/lib/lazylead/task/accuracy/servers.rb +50 -0
  24. data/lib/lazylead/task/accuracy/stacktrace.rb +63 -0
  25. data/lib/lazylead/task/accuracy/testcase.rb +75 -0
  26. data/lib/lazylead/task/accuracy/wiki.rb +41 -0
  27. data/lib/lazylead/task/echo.rb +18 -0
  28. data/lib/lazylead/task/fix_version.rb +9 -2
  29. data/lib/lazylead/task/touch.rb +28 -11
  30. data/lib/lazylead/version.rb +1 -1
  31. data/lib/messages/svn_log.erb +117 -0
  32. data/lib/messages/svn_touch.erb +1 -1
  33. data/license.txt +1 -1
  34. data/readme.md +5 -5
  35. data/test/lazylead/cli/app_test.rb +11 -11
  36. data/test/lazylead/opts_test.rb +4 -0
  37. data/test/lazylead/system/jira_test.rb +38 -0
  38. data/test/lazylead/task/accuracy/accuracy_test.rb +1 -1
  39. data/test/lazylead/task/accuracy/affected_build_test.rb +2 -2
  40. data/test/lazylead/task/accuracy/attachment_test.rb +50 -0
  41. data/test/lazylead/task/accuracy/environment_test.rb +42 -0
  42. data/test/lazylead/task/accuracy/logs_test.rb +78 -0
  43. data/test/lazylead/task/accuracy/records_test.rb +60 -0
  44. data/test/lazylead/task/accuracy/score_test.rb +46 -0
  45. data/test/lazylead/task/accuracy/servers_test.rb +66 -0
  46. data/test/lazylead/task/accuracy/stacktrace_test.rb +113 -0
  47. data/test/lazylead/task/accuracy/testcase_test.rb +205 -0
  48. data/test/lazylead/task/accuracy/wiki_test.rb +40 -0
  49. data/test/lazylead/task/touch_test.rb +48 -23
  50. data/test/test.rb +25 -0
  51. data/upgrades/sqlite/001-install-main-lazylead-tables.sql +1 -5
  52. data/upgrades/sqlite/999.testdata.sql +12 -17
  53. metadata +45 -4
  54. data/.travis.yml +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8f6929a5583f40fca08c1d1293b77cf2f19ea22b1844eb9888a860b79c86b7c
4
- data.tar.gz: 07b70000fe7e00aeb7c33b321d1f35c99dccb8220f477650b90a693ea8688c0c
3
+ metadata.gz: b58ee14de8c573012698a15d145941155e6c282249bc2938bf4938c4e677d7cd
4
+ data.tar.gz: 34bed5a73b2186b9e229a121973ad11891037d604faa0134fe31d5a9380ead6a
5
5
  SHA512:
6
- metadata.gz: 1699bbdc2484d6a77715b11bb0f666e70e18e3bc5927d79ee02cee3bfcc4e6bf6d848a74a5c287990e56d059f669f07dc9e450f7af6992b63881288f481e062e
7
- data.tar.gz: 0c471418e7ed2a92d740d0e362c0bcd9ce37eca1538272eab8abe0eff4fb3b01df1559b5f16d7af9ed3dfa52d06ebb5ff1c6a10cb897df7aeab3e119f8fe63c3
6
+ metadata.gz: f1a8e4032492f97b2672078c908f37599f3d32a01bf31d7a67911732b90f682410123fb1234e07315b9ccd6d4e675e4b9d4867e06f265c06ace44f33c7d1cb1f
7
+ data.tar.gz: 22f2d3fb9b3ed8a8bb9e2e8f6f29393f65e2ab6bea033c9f0f82741775032098383614f280433f64fcd16dbd6ab6c4c46115b197307c684a9c03ffc736b95714
@@ -66,14 +66,14 @@ For simplicity, we are using [docker-compose](https://docs.docker.com/compose/):
66
66
  values (1,'{"type":"Lazylead::Jira", "username":"${jira_user}", "password":"${jira_password}", "site":"${jira_url}", "context_path":""}');
67
67
  insert into tasks (name, cron, enabled, id, system, team_id, action, properties)
68
68
  values ('Post ticket score and accuracy to the tickets',
69
- '0 8 * * 1-5',
69
+ 'cron:0 8 * * 1-5',
70
70
  'true',
71
71
  1, 1, 1,
72
72
  'Lazylead::Task::Accuracy',
73
73
  '{
74
74
  "jql": "filter=222",
75
75
  "to": "lead@fake.com",
76
- "rules": "Lazylead::RequirementAffectedBuild",
76
+ "rules": "Lazylead::AffectedBuild",
77
77
  "colors": "{ "0": "#FF4F33", "35": "#FF9F33", "57": "#19DD1E", "90": "#0FA81A" }",
78
78
  "docs": "https://github.com/dgroup/lazylead/blob/master/.github/ISSUE_TEMPLATE/bug_report.md",
79
79
  "max_results": "200",
@@ -82,7 +82,7 @@ For simplicity, we are using [docker-compose](https://docs.docker.com/compose/):
82
82
  }
83
83
  ');
84
84
  ```
85
- Yes, for task scheduling we are using [cron](https://crontab.guru).
85
+ Yes, for task scheduling we are using [cron](https://crontab.guru) here, but you may use other scheduling types from [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler).
86
86
 
87
87
  4. Once you changed `./ll.db`, please restart the container using `docker-compose -f .github/tasks.yml restart`
88
88
  ```bash
@@ -104,4 +104,4 @@ For simplicity, we are using [docker-compose](https://docs.docker.com/compose/):
104
104
 
105
105
  #### How can I add my own rules?
106
106
  The custom rules should extend `Lazylead::Requirement` class and placed it to the `lib/lazylead/task/accuracy` folder.
107
- After that, you need to mention your custom rules in `rules` option in the column `properties` from `tasks` table
107
+ After that, you need to mention your custom rules in `rules` option in the column `properties` from `tasks` table
@@ -68,16 +68,16 @@ For simplicity, we are using [docker-compose](https://docs.docker.com/compose/):
68
68
  values (1, 'Dream team with lazylead', '{}');
69
69
  insert into systems(id, properties)
70
70
  values (1,'{"type":"Lazylead::Jira", "username":"${jira_user}", "password":"${jira_password}", "site":"${jira_url}", "context_path":""}');
71
- insert into tasks (name, cron, enabled, id, system, team_id, action, properties)
71
+ insert into tasks (name, schedule, enabled, id, system, team_id, action, properties)
72
72
  values ('Expired due dates',
73
- '0 8 * * 1-5',
73
+ 'cron:0 8 * * 1-5',
74
74
  'true',
75
75
  1, 1, 1,
76
76
  'Lazylead::Task::AssigneeAlert',
77
77
  '{"sql":"filter=222", "cc":"<youremail.com>", "subject":"[LL] Expired due dates", "template":"lib/messages/due_date_expired.erb", "postman":"Lazylead::Exchange"}');
78
78
 
79
79
  ```
80
- Yes, for task scheduling we are using [cron](https://crontab.guru).
80
+ Yes, for task scheduling we are using [cron](https://crontab.guru) here, but you may use other scheduling types from [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler).
81
81
 
82
82
  4. Once you changed `./ll.db`, please restart the container using `docker-compose -f .github/tasks.yml restart`
83
83
  ```bash
@@ -58,16 +58,16 @@ For simplicity, we are using [docker-compose](https://docs.docker.com/compose/):
58
58
  values (1, 'Dream team with lazylead', '{}');
59
59
  insert into systems(id, properties)
60
60
  values (1,'{"type":"Lazylead::Jira", "username":"${jira_user}", "password":"${jira_password}", "site":"${jira_url}", "context_path":""}');
61
- insert into tasks (name, cron, enabled, id, system, team_id, action, properties)
61
+ insert into tasks (name, schedule, enabled, id, system, team_id, action, properties)
62
62
  values ('Propagate customfield_1 (External ID) to sub-tasks',
63
- '0 8 * * 1-5',
63
+ 'cron:0 8 * * 1-5',
64
64
  'true',
65
65
  1, 1, 1,
66
66
  'Lazylead::Task::PropagateDown',
67
67
  '{"jql":"filter=222", "propagate":"customfield_1"}');
68
68
 
69
69
  ```
70
- Yes, for task scheduling we are using [cron](https://crontab.guru).
70
+ Yes, for task scheduling we are using [cron](https://crontab.guru) here, but you may use other scheduling types from [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler).
71
71
 
72
72
  4. Once you changed `./ll.db`, please restart the container using `docker-compose -f .github/tasks.yml restart`
73
73
  ```bash
@@ -7,3 +7,4 @@
7
7
  *.xml ident
8
8
  *.png binary
9
9
  *.pdf binary
10
+ *.db binary
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "bundler"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "daily"
data/Rakefile CHANGED
@@ -127,4 +127,6 @@ task :docker do
127
127
  system "docker-compose -f .docker/docker-compose.yml build "\
128
128
  " --build-arg release_tags='latest 1.0'"\
129
129
  " --build-arg version=1.0"
130
+ system "docker-compose -f .docker/docker-compose.yml rm --force -s lazylead"
131
+ system "docker-compose -f .docker/docker-compose.yml up"
130
132
  end
@@ -82,7 +82,7 @@ log.debug("Memory footprint at start is #{Lazylead::Allocated.new}")
82
82
  cmd = lambda do
83
83
  Lazylead::CLI::App.new(
84
84
  log,
85
- Lazylead::Schedule.new(log),
85
+ Lazylead::Schedule.new(log: log),
86
86
  Lazylead::Smtp.new(
87
87
  log, Lazylead::Salt.new("smtp_salt"),
88
88
  smtp_host: ENV["smtp_host"],
@@ -32,7 +32,7 @@ Gem::Specification.new do |s|
32
32
  s.rubygems_version = "2.2"
33
33
  s.required_ruby_version = ">=2.6.5"
34
34
  s.name = "lazylead"
35
- s.version = "0.4.0"
35
+ s.version = "0.5.1"
36
36
  s.license = "MIT"
37
37
  s.summary = "Eliminate the annoying work within bug-trackers."
38
38
  s.description = "Ticketing systems (Github, Jira, etc.) are strongly
@@ -45,7 +45,7 @@ tasks instead of solving technical problems."
45
45
  s.authors = ["Yurii Dubinka"]
46
46
  s.email = "yurii.dubinka@gmail.com"
47
47
  s.homepage = "http://github.com/dgroup/lazylead"
48
- s.post_install_message = "Thanks for installing Lazylead v0.4.0!
48
+ s.post_install_message = "Thanks for installing Lazylead v0.5.1!
49
49
  Read our blog posts: https://lazylead.org
50
50
  Stay in touch with the community in Telegram: https://t.me/lazylead
51
51
  Follow us on Twitter: https://twitter.com/lazylead
@@ -82,6 +82,7 @@ tasks instead of solving technical problems."
82
82
  s.add_development_dependency "minitest-fail-fast", "0.1.0"
83
83
  s.add_development_dependency "minitest-hooks", "1.5.0"
84
84
  s.add_development_dependency "minitest-reporters", "1.3.6"
85
+ s.add_development_dependency "net-ping", "2.0.8"
85
86
  s.add_development_dependency "rake", "12.3.3"
86
87
  s.add_development_dependency "random-port", "0.3.1"
87
88
  s.add_development_dependency "rdoc", "6.1.1"
@@ -54,19 +54,25 @@ module Lazylead
54
54
  def send(opts)
55
55
  to = opts["to"] || opts[:to]
56
56
  to = [to] unless to.is_a? Array
57
- html = make_body(opts)
58
- msg = {
59
- subject: opts["subject"],
60
- body: html,
61
- body_type: "HTML",
62
- to_recipients: to
63
- }
57
+ if to.reject { |e| e.nil? || e.blank? }.empty?
58
+ @log.warn "Email can't be sent to '#{to}, more: '#{opts}'"
59
+ return
60
+ end
61
+ msg = make_msg(to, opts)
64
62
  msg.update(cc_recipients: opts["cc"]) if opts.key? "cc"
65
63
  add_attachments(msg, opts)
66
64
  cli.send_message msg
67
65
  close_attachments msg
68
- @log.debug "Email was generated from #{opts} and send by #{__FILE__}. " \
69
- "Here is the body: #{html}"
66
+ @log.debug "#{__FILE__} sent email based on #{opts}."
67
+ end
68
+
69
+ def make_msg(to, opts)
70
+ {
71
+ subject: opts["subject"],
72
+ body: make_body(opts),
73
+ body_type: "HTML",
74
+ to_recipients: to
75
+ }
70
76
  end
71
77
 
72
78
  def add_attachments(msg, opts)
@@ -94,11 +94,31 @@ module Lazylead
94
94
  belongs_to :team, foreign_key: "team_id"
95
95
  belongs_to :system, foreign_key: "system"
96
96
 
97
+ # Execute task
97
98
  def exec
98
99
  sys = system.connect
99
100
  opts = props
100
101
  opts = detect_cc(sys) if opts.key? "cc"
101
- action.constantize.new(log).run(sys, postman, opts)
102
+ action.constantize.new.run(sys, postman, opts)
103
+ end
104
+
105
+ # Scheduling type.
106
+ # Current implementation is based on 'rufus-scheduler' gem and supports
107
+ # the following types: 'cron', 'interval', 'in', 'at', 'every'
108
+ def type
109
+ trigger.first
110
+ end
111
+
112
+ # Scheduling unit.
113
+ # Current implementation is based on 'rufus-scheduler' gem thus each
114
+ # scheduling type has own arguments:
115
+ # 1. Scheduling type 'cron' has 'unit' = '00 09 * * *'
116
+ # 2. Scheduling type 'interval' has 'unit' = '2h'
117
+ # 3. Scheduling type 'every' has 'unit' = '3h'
118
+ # 4. Scheduling type 'in' has 'unit' = '10d'
119
+ # 5. Scheduling type 'at' has 'unit' = '2014/12/24 2000'
120
+ def unit
121
+ trigger.last
102
122
  end
103
123
 
104
124
  def detect_cc(sys)
@@ -125,13 +145,27 @@ module Lazylead
125
145
  Postman.new
126
146
  end
127
147
  end
148
+
149
+ private
150
+
151
+ # Parse scheduling #type and #unit
152
+ def trigger
153
+ @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
160
+ end
128
161
  end
129
162
 
130
163
  # A task with extended logging
131
164
  # @see Lazylead::ORM::Task
132
165
  class VerboseTask
133
166
  extend Forwardable
134
- def_delegators :@orig, :id, :name, :team, :to_s, :inspect, :props
167
+ def_delegators :@orig, :id, :name, :team, :to_s, :inspect, :props, :type,
168
+ :unit
135
169
 
136
170
  def initialize(orig, log = Log.new)
137
171
  @orig = orig
@@ -143,7 +177,7 @@ module Lazylead
143
177
  @log.debug "'#{name}' is started."
144
178
  @log.warn "No postman, stub is used." unless props.key? "postman"
145
179
  @log.warn "No team." if team.nil?
146
- @orig.exec @log
180
+ @orig.exec
147
181
  @log.debug "'#{name}' is completed"
148
182
  rescue StandardError => e
149
183
  msg = <<~MSG
@@ -23,6 +23,7 @@
23
23
  # OR OTHER DEALINGS IN THE SOFTWARE.
24
24
 
25
25
  require "forwardable"
26
+ require_relative "salt"
26
27
 
27
28
  module Lazylead
28
29
  #
@@ -33,7 +34,7 @@ module Lazylead
33
34
  # License:: MIT
34
35
  class Opts
35
36
  extend Forwardable
36
- def_delegators :@origin, :[], :[]=, :to_s, :key?, :fetch, :merge
37
+ def_delegators :@origin, :[], :[]=, :to_s, :key?, :fetch, :merge, :except
37
38
 
38
39
  def initialize(origin = {})
39
40
  @origin = origin
@@ -64,5 +65,16 @@ module Lazylead
64
65
  def jira_fields
65
66
  to_h.fetch("fields", "").split(",").map(&:to_sym)
66
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
67
79
  end
68
80
  end
@@ -65,8 +65,7 @@ module Lazylead
65
65
  end
66
66
  add_attachments mail, opts
67
67
  mail.deliver
68
- @log.debug "Email was generated from #{opts} and send by #{__FILE__}. " \
69
- "Here is the body: #{html}"
68
+ @log.debug "#{__FILE__} sent email based on #{opts}."
70
69
  end
71
70
 
72
71
  def add_attachments(mail, opts)
@@ -34,23 +34,18 @@ 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.new, 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
50
  ORM::VerboseTask.new(task, @log).exec
56
51
  end
@@ -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
@@ -80,11 +77,15 @@ module Lazylead
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
@@ -150,6 +150,11 @@ module Lazylead
150
150
  @issue.key
151
151
  end
152
152
 
153
+ def description
154
+ return "" if @issue.description.nil?
155
+ @issue.description
156
+ end
157
+
153
158
  def summary
154
159
  fields["summary"]
155
160
  end
@@ -175,9 +180,24 @@ module Lazylead
175
180
  end
176
181
 
177
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? :[]
178
187
  @issue.fields
179
188
  end
180
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
+
181
201
  def history
182
202
  return [] unless @issue.respond_to? :changelog
183
203
  return [] if @issue.changelog == nil? || @issue.changelog.empty?
@@ -206,6 +226,29 @@ module Lazylead
206
226
  def post(markdown)
207
227
  @issue.comments.build.save!(body: markdown)
208
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
+ lbl = labels
240
+ lbl << label
241
+ lbl += more if more.size.positive?
242
+ save!("fields" => { "labels" => lbl.uniq })
243
+ end
244
+
245
+ def labels
246
+ fields["labels"]
247
+ end
248
+
249
+ def save!(opts)
250
+ @issue.save(opts)
251
+ end
209
252
  end
210
253
 
211
254
  # The jira issue comments
@@ -43,7 +43,7 @@ module Lazylead
43
43
  end
44
44
 
45
45
  def run(sys, postman, opts)
46
- require_rules
46
+ Dir[File.join(__dir__, "*.rb")].sort.each { |f| require f }
47
47
  rules = opts.slice("rules", ",")
48
48
  .map(&:constantize)
49
49
  .map(&:new)
@@ -53,12 +53,6 @@ module Lazylead
53
53
  .each(&:post)
54
54
  postman.send opts.merge(tickets: raised) unless raised.empty?
55
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
56
  end
63
57
  end
64
58
 
@@ -85,7 +79,9 @@ module Lazylead
85
79
 
86
80
  # Post the comment with score and accuracy to the ticket.
87
81
  def post
88
- @issue.post(comment) unless @opts.key? "silent"
82
+ return if @opts.key? "silent"
83
+ @issue.post comment
84
+ @issue.add_label "LL.accuracy", grade(@accuracy)
89
85
  end
90
86
 
91
87
  # The jira comment in markdown format
@@ -136,5 +132,14 @@ module Lazylead
136
132
  .sort_by { |e| e[0] }
137
133
  end
138
134
  end
135
+
136
+ # Calculate grade for accuracy
137
+ # For example,
138
+ # grade(7.5) => 0
139
+ # grade(12) => 10
140
+ # grade(25.5) => 20
141
+ def grade(value)
142
+ (value / 10).floor * 10
143
+ end
139
144
  end
140
145
  end