rbbt-util 5.28.6 → 5.28.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/lib/rbbt/entity.rb +1 -1
  3. data/lib/rbbt/fix_width_table.rb +5 -4
  4. data/lib/rbbt/persist.rb +9 -3
  5. data/lib/rbbt/persist/tsv/adapter.rb +0 -1
  6. data/lib/rbbt/persist/tsv/fix_width_table.rb +5 -3
  7. data/lib/rbbt/resource.rb +12 -6
  8. data/lib/rbbt/tsv/accessor.rb +10 -2
  9. data/lib/rbbt/tsv/dumper.rb +14 -2
  10. data/lib/rbbt/tsv/parallel/traverse.rb +2 -0
  11. data/lib/rbbt/tsv/util.rb +5 -1
  12. data/lib/rbbt/util/R.rb +2 -2
  13. data/lib/rbbt/util/cmd.rb +10 -0
  14. data/lib/rbbt/util/config.rb +2 -1
  15. data/lib/rbbt/util/misc/bgzf.rb +1 -1
  16. data/lib/rbbt/util/misc/inspect.rb +11 -7
  17. data/lib/rbbt/util/misc/system.rb +1 -1
  18. data/lib/rbbt/util/named_array.rb +1 -1
  19. data/lib/rbbt/util/open.rb +18 -17
  20. data/lib/rbbt/workflow/accessor.rb +1 -1
  21. data/lib/rbbt/workflow/definition.rb +8 -4
  22. data/lib/rbbt/workflow/integration/ansible.rb +53 -0
  23. data/lib/rbbt/workflow/integration/ansible/workflow.rb +60 -0
  24. data/lib/rbbt/workflow/step.rb +22 -5
  25. data/lib/rbbt/workflow/step/accessor.rb +6 -6
  26. data/lib/rbbt/workflow/util/archive.rb +3 -0
  27. data/lib/rbbt/workflow/util/orchestrator.rb +228 -0
  28. data/lib/rbbt/workflow/util/provenance.rb +7 -3
  29. data/lib/rbbt/workflow/util/trace.rb +182 -0
  30. data/share/rbbt_commands/ansible +55 -0
  31. data/share/rbbt_commands/purge_job +2 -5
  32. data/share/rbbt_commands/system/status +23 -23
  33. data/share/rbbt_commands/workflow/forget_deps +10 -3
  34. data/share/rbbt_commands/workflow/prov +2 -1
  35. data/test/rbbt/association/test_index.rb +6 -6
  36. data/test/rbbt/knowledge_base/test_query.rb +3 -3
  37. data/test/rbbt/knowledge_base/test_registry.rb +1 -1
  38. data/test/rbbt/persist/tsv/test_cdb.rb +0 -7
  39. data/test/rbbt/persist/tsv/test_kyotocabinet.rb +2 -8
  40. data/test/rbbt/persist/tsv/test_leveldb.rb +0 -6
  41. data/test/rbbt/persist/tsv/test_lmdb.rb +0 -6
  42. data/test/rbbt/persist/tsv/test_tokyocabinet.rb +15 -14
  43. data/test/rbbt/test_entity.rb +0 -1
  44. data/test/rbbt/test_knowledge_base.rb +3 -4
  45. data/test/rbbt/test_persist.rb +10 -6
  46. data/test/rbbt/test_workflow.rb +17 -16
  47. data/test/rbbt/tsv/parallel/test_traverse.rb +14 -0
  48. data/test/rbbt/tsv/test_accessor.rb +11 -0
  49. data/test/rbbt/tsv/test_attach.rb +0 -2
  50. data/test/rbbt/tsv/test_index.rb +6 -7
  51. data/test/rbbt/tsv/test_manipulate.rb +22 -3
  52. data/test/rbbt/util/R/test_model.rb +2 -1
  53. data/test/rbbt/util/R/test_plot.rb +0 -2
  54. data/test/rbbt/util/concurrency/test_processes.rb +1 -1
  55. data/test/rbbt/util/misc/test_bgzf.rb +11 -7
  56. data/test/rbbt/util/misc/test_lock.rb +0 -1
  57. data/test/rbbt/util/misc/test_multipart_payload.rb +1 -1
  58. data/test/rbbt/util/misc/test_pipes.rb +0 -5
  59. data/test/rbbt/util/test_R.rb +1 -0
  60. data/test/rbbt/util/test_log.rb +4 -6
  61. data/test/rbbt/util/test_misc.rb +0 -2
  62. data/test/rbbt/util/test_open.rb +0 -1
  63. data/test/rbbt/util/test_python.rb +17 -1
  64. data/test/rbbt/workflow/test_remote_workflow.rb +1 -1
  65. data/test/rbbt/workflow/test_schedule.rb +0 -0
  66. data/test/rbbt/workflow/test_step.rb +8 -3
  67. data/test/rbbt/workflow/util/test_orchestrator.rb +273 -0
  68. metadata +11 -5
  69. data/lib/rbbt/workflow/schedule.rb +0 -238
  70. data/test/rbbt/workflow/remote/test_client.rb +0 -56
@@ -2,12 +2,14 @@ require File.join(File.expand_path(File.dirname(__FILE__)), '../../..', 'test_he
2
2
  require 'rbbt/util/misc/bgzf'
3
3
 
4
4
  class TestBgzf < Test::Unit::TestCase
5
- def test_Bgzf
5
+ def _test_Bgzf
6
6
  content = "1234567890" * 1000000
7
7
  TmpFile.with_file(content) do |file|
8
8
  compressed = file + '.gz'
9
9
  `bgzip #{file} -c > #{compressed}`
10
10
  stream = Bgzf.setup File.open(compressed)
11
+ assert_equal "1234", stream.read(4)
12
+ assert_equal "56", stream.read(2)
11
13
  stream.seek 500003
12
14
  assert_equal "4567", stream.read(4)
13
15
  assert_equal "89", stream.read(2)
@@ -20,23 +22,25 @@ class TestBgzf < Test::Unit::TestCase
20
22
  Misc.benchmark do
21
23
  tsv = TSV.open(Open.open(file))
22
24
  end
23
- compressed = file + '.bgz'
24
25
 
25
- `bgzip #{file} -c > #{compressed}`
26
- stream = Bgzf.setup File.open(compressed)
26
+ `gzip #{file}`
27
+ stream = Open.open(file + '.gz')
27
28
  Misc.benchmark do
28
29
  tsv = TSV.open(stream)
29
30
  end
30
31
 
31
- `gzip #{file}`
32
- stream = Open.open(file + '.gz')
32
+ `gunzip #{file}.gz`
33
+ compressed = file + '.bgz'
34
+ `bgzip #{file} -c > #{compressed}`
35
+ stream = Bgzf.setup File.open(compressed)
33
36
  Misc.benchmark do
34
37
  tsv = TSV.open(stream)
35
38
  end
39
+
36
40
  end
37
41
  end
38
42
 
39
- def test_bgzip
43
+ def _test_bgzip
40
44
  assert File.exist?(Bgzf.bgzip_cmd)
41
45
  assert 'bgzip', File.basename(Bgzf.bgzip_cmd)
42
46
  end
@@ -48,7 +48,6 @@ if __FILE__ == $0
48
48
  TmpFile.with_file do |dir|
49
49
  Structure.workdir = dir
50
50
  Path.setup dir
51
- Log.severity = 4
52
51
  TSV.traverse (0..size).to_a, :cpus => cpus, :type => :array, :bar => true do |i|
53
52
  begin
54
53
  v = rand(num).to_s
@@ -185,7 +185,7 @@ END
185
185
  inputs[:input2] = "Input2"
186
186
  num = 50
187
187
  cpus = 1
188
- Log.severity = 0
188
+
189
189
  Misc.bootstrap((0..num-1).to_a, cpus) do |n|
190
190
  puts mutipart
191
191
  TmpFile.with_file(mutipart, false) do |tmpfile|
@@ -131,7 +131,6 @@ row2 AA BB CC
131
131
  row3 AAA BBB CCC
132
132
  row1 A B C
133
133
  EOF
134
- Log.severity = 0
135
134
 
136
135
  text = text * 10000
137
136
  TmpFile.with_file(text) do |tmp|
@@ -159,7 +158,6 @@ row2 AA BB CC
159
158
  row3 AAA BBB CCC
160
159
  row1 A B C
161
160
  EOF
162
- Log.severity = 0
163
161
 
164
162
  text = text * 10000
165
163
  num = 5
@@ -200,7 +198,6 @@ line3
200
198
  line1
201
199
  EOF
202
200
 
203
- Log.severity = 0
204
201
  TmpFile.with_file(text1) do |file1|
205
202
  TmpFile.with_file(text2) do |file2|
206
203
  assert ! Misc.remove_lines(file1, file2, true).read.split("\n").include?("line1")
@@ -224,7 +221,6 @@ line1
224
221
  line5
225
222
  EOF
226
223
 
227
- Log.severity = 0
228
224
  TmpFile.with_file(text1) do |file1|
229
225
  TmpFile.with_file(text2) do |file2|
230
226
  assert Misc.select_lines(file1, file2, true).read.split("\n").include?("line1")
@@ -258,7 +254,6 @@ line3
258
254
  line4
259
255
  EOF
260
256
 
261
- Log.severity = 0
262
257
  TmpFile.with_file(text) do |file|
263
258
  io = Open.open(file)
264
259
  lines = Set.new
@@ -9,6 +9,7 @@ class TestR < Test::Unit::TestCase
9
9
  def test_tsv_R
10
10
  tsv = TSV.setup({:a => 1, :b => 2})
11
11
  tsv2 = tsv.R <<-EOF
12
+ str(data)
12
13
  data = data + 1
13
14
  EOF
14
15
  assert_equal "2", tsv2["a"].first
@@ -2,7 +2,7 @@ require File.join(File.expand_path(File.dirname(__FILE__)), '../..', 'test_helpe
2
2
  require 'rbbt/util/log'
3
3
 
4
4
  class TestLog < Test::Unit::TestCase
5
- def _test_get_level
5
+ def test_get_level
6
6
  assert_equal 0, Log.get_level(:debug)
7
7
  assert_equal 1, Log.get_level(:low)
8
8
  assert_equal 1, Log.get_level("LOW")
@@ -10,18 +10,17 @@ class TestLog < Test::Unit::TestCase
10
10
  assert_equal 0, Log.get_level(nil)
11
11
  end
12
12
 
13
- def _test_color
13
+ def test_color
14
14
  assert Log.color(:green, "green")
15
15
  end
16
16
 
17
- def _test_no_stderr
17
+ def test_no_stderr
18
18
  Log.ignore_stderr do
19
19
  STDERR.puts "NOPRINT"
20
20
  end
21
21
  end
22
22
 
23
- def _test_trap_stderr
24
- Log.severity = 0
23
+ def test_trap_stderr
25
24
  Log.trap_stderr do
26
25
  STDERR.puts "NOPRINT"
27
26
  STDERR.puts "NOPRINT"
@@ -40,7 +39,6 @@ class TestLog < Test::Unit::TestCase
40
39
  end
41
40
 
42
41
  def test_trap_std
43
- Log.severity = 0
44
42
  Log.trap_std do
45
43
  STDERR.puts "NOPRINT STDERR"
46
44
  STDOUT.puts "NOPRINT STDOUT"
@@ -475,7 +475,6 @@ eum fugiat quo voluptas nulla pariatur?"
475
475
  end
476
476
 
477
477
  def test_bootstrap
478
- Log.severity = 0
479
478
  res = Misc.bootstrap((1..10).to_a, 2, :bar => "Test bootstrap ticks", :respawn => :always, :into => []) do |num|
480
479
  sleep 1 + rand(2)
481
480
  num
@@ -550,7 +549,6 @@ eum fugiat quo voluptas nulla pariatur?"
550
549
  end
551
550
 
552
551
  def __test_bench_log
553
- Log.severity = 1
554
552
  Misc.benchmark(1000) do
555
553
  Log.info { "Hola" }
556
554
  end
@@ -169,7 +169,6 @@ class TestOpen < Test::Unit::TestCase
169
169
  end
170
170
 
171
171
  def test_write_stream_repo
172
- Log.severity = 0
173
172
  TmpFile.with_file do |tmpdir|
174
173
  tmpdir = Rbbt.tmp.repo_dir.find
175
174
  repo = File.join(tmpdir, 'repo')
@@ -42,7 +42,6 @@ def python_test(a, b):
42
42
  end
43
43
 
44
44
  def test_run_log
45
- Log.severity = 0
46
45
  TmpFile.with_file do |tmpdir|
47
46
  code =<<-EOF
48
47
  import sys
@@ -66,3 +65,20 @@ def python_print():
66
65
  end
67
66
  end
68
67
 
68
+ def test_keras
69
+ defined = RbbtPython.run do
70
+ pyimport "keras.models", as: :km
71
+ defined?(km.Sequential)
72
+ end
73
+ assert defined
74
+ end
75
+
76
+ def test_keras_import
77
+ defined = RbbtPython.run do
78
+ pyfrom "keras.models", import: :Sequential
79
+ defined?(self::Sequential)
80
+ end
81
+ assert defined
82
+ end
83
+ end
84
+
@@ -88,7 +88,7 @@ class TestRemoteWorkflow < Test::Unit::TestCase
88
88
  end
89
89
 
90
90
 
91
- def _test_ssh
91
+ def test_ssh
92
92
  Log.severity = 0
93
93
  client = RemoteWorkflow.new "ssh://#{ENV["HOSTNAME"]}:Translation", "Translation"
94
94
  job = client.job("translate", "SSH-TEST-1", :genes => ["TP53","KRAS"])
File without changes
@@ -1,8 +1,10 @@
1
1
  require File.join(File.expand_path(File.dirname(__FILE__)), '../..', 'test_helper.rb')
2
+ require 'rbbt/workflow'
2
3
  require 'rbbt/workflow/task'
3
4
  require 'rbbt/workflow/step'
4
5
  require 'rbbt/tsv'
5
6
  require 'rbbt'
7
+ require 'rbbt-util'
6
8
 
7
9
  class TestStep < Test::Unit::TestCase
8
10
 
@@ -23,8 +25,9 @@ class TestStep < Test::Unit::TestCase
23
25
  str2 = "TEST2"
24
26
  TmpFile.with_file do |tmpfile|
25
27
 
26
- task1 = Task.setup :result_type => nil do
28
+ task1 = Task.setup :result_type => :string do
27
29
  Open.write(tmpfile, str);
30
+ "done"
28
31
  end
29
32
  step1 = Step.new tmpfile + 'step1', task1
30
33
 
@@ -50,9 +53,10 @@ class TestStep < Test::Unit::TestCase
50
53
  def __test_dependency_log_relay
51
54
  str = "TEST"
52
55
  TmpFile.with_file do |tmpfile|
53
- task1 = Task.setup :result_type => nil, :name => :task1 do
56
+ task1 = Task.setup :result_type => :string, :name => :task1 do
54
57
  log(:starting_task1, "Starting Task1")
55
58
  Open.write(tmpfile, str);
59
+ "done"
56
60
  end
57
61
  step1 = Step.new tmpfile + 'step1', task1
58
62
 
@@ -69,9 +73,10 @@ class TestStep < Test::Unit::TestCase
69
73
  def test_log_relay_step
70
74
  str = "TEST"
71
75
  TmpFile.with_file do |tmpfile|
72
- task1 = Task.setup :result_type => nil, :name => :task1 do
76
+ task1 = Task.setup :result_type => :string, :name => :task1 do
73
77
  log(:starting_task1, "Starting Task1")
74
78
  Open.write(tmpfile, str);
79
+ "done"
75
80
  end
76
81
  step1 = Step.new tmpfile + 'step1', task1
77
82
 
@@ -0,0 +1,273 @@
1
+ require File.join(File.expand_path(File.dirname(__FILE__)), '../../..', 'test_helper.rb')
2
+ require 'rbbt/workflow/util/orchestrator'
3
+ require 'rbbt/workflow/util/trace'
4
+ require 'rbbt-util'
5
+ require 'rbbt/workflow'
6
+
7
+ module TestWF
8
+ extend Workflow
9
+
10
+ MULT = 0.1
11
+ task :a => :text do
12
+ sleep(TestWF::MULT * (rand(10) + 2))
13
+ end
14
+
15
+ dep :a
16
+ task :b => :text do
17
+ sleep(TestWF::MULT * (rand(10) + 2))
18
+ end
19
+
20
+ dep :a
21
+ dep :b
22
+ task :c => :text do
23
+ sleep(TestWF::MULT * (rand(10) + 2))
24
+ end
25
+
26
+ dep :c
27
+ task :d => :text do
28
+ sleep(TestWF::MULT * (rand(10) + 2))
29
+ end
30
+
31
+ dep :c
32
+ task :e => :text do
33
+ sleep(TestWF::MULT * (rand(10) + 2))
34
+ end
35
+ end
36
+
37
+ class TestClass < Test::Unit::TestCase
38
+ def test_orchestrate_resources
39
+
40
+ jobs =[]
41
+
42
+ num = 10
43
+ num.times do |i|
44
+ jobs.concat %w(TEST1 TEST2).collect{|name| TestWF.job(:d, name + " #{i}") }
45
+ end
46
+ jobs.each do |j| j.recursive_clean end
47
+
48
+ rules = YAML.load <<-EOF
49
+ defaults:
50
+ log: 4
51
+ default_resources:
52
+ IO: 1
53
+ TestWF:
54
+ a:
55
+ resources:
56
+ cpus: 7
57
+ b:
58
+ resources:
59
+ cpus: 2
60
+ c:
61
+ resources:
62
+ cpus: 10
63
+ d:
64
+ resources:
65
+ cpus: 15
66
+ EOF
67
+
68
+ orchestrator = Workflow::Orchestrator.new(TestWF::MULT, "cpus" => 30, "IO" => 4, "size" => 10 )
69
+ Log.with_severity 0 do
70
+ orchestrator.process(rules, jobs)
71
+ end
72
+
73
+ data = Workflow.trace jobs, :plot_data => true
74
+ eend = data.column("End.second").values.collect{|v| v.to_f}.max
75
+ second_cpus = TSV.setup({}, "Second~CPUS#:type=:single#:cast=:to_f")
76
+ (0..eend.to_i).each do |second|
77
+ tasks = data.select("Start.second"){|s| s <= second}.select("End.second"){|s| s > second}
78
+ cpus = 0
79
+ tasks.through :key, ["Workflow", "Task"] do |k, values|
80
+ workflow, task = values
81
+ cpus += rules[workflow][task.to_s]["resources"]["cpus"]
82
+ end
83
+ second_cpus[second] = cpus
84
+ end
85
+
86
+ assert Misc.mean(second_cpus.values) > 15
87
+ assert Misc.mean(second_cpus.values) < 30
88
+ end
89
+
90
+ def test_orchestrate_erase
91
+
92
+ jobs =[]
93
+
94
+ num = 10
95
+ num.times do |i|
96
+ jobs.concat %w(TEST1 TEST2).collect{|name| TestWF.job(:d, name + " #{i}") }
97
+ end
98
+ jobs.each do |j| j.recursive_clean end
99
+
100
+ rules = YAML.load <<-EOF
101
+ defaults:
102
+ log: 4
103
+ default_resources:
104
+ IO: 1
105
+ TestWF:
106
+ a:
107
+ erase: true
108
+ resources:
109
+ cpus: 7
110
+ b:
111
+ erase: true
112
+ resources:
113
+ cpus: 2
114
+ c:
115
+ resources:
116
+ cpus: 10
117
+ d:
118
+ resources:
119
+ cpus: 15
120
+ EOF
121
+
122
+ orchestrator = Workflow::Orchestrator.new(TestWF::MULT, "cpus" => 30, "IO" => 4, "size" => 10 )
123
+ Log.with_severity 3 do
124
+ orchestrator.process(rules, jobs)
125
+ end
126
+
127
+ jobs.each do |job|
128
+ assert job.step(:c).dependencies.empty?
129
+ assert job.step(:c).info[:archived_info].keys.select{|k| k.include?("TestWF/a/")}.any?
130
+ assert job.step(:c).info[:archived_info].keys.select{|k| k.include?("TestWF/b/")}.any?
131
+ end
132
+
133
+ end
134
+
135
+ def test_orchestrate_default
136
+
137
+ jobs =[]
138
+
139
+ num = 3
140
+ num.times do |i|
141
+ jobs.concat %w(TEST1 TEST2).collect{|name| TestWF.job(:d, name + " #{i}") }
142
+ end
143
+ jobs.each do |j| j.recursive_clean end
144
+
145
+ rules = YAML.load <<-EOF
146
+ defaults:
147
+ erase: true
148
+ log: 4
149
+ default_resources:
150
+ IO: 1
151
+ TestWF:
152
+ a:
153
+ erase: true
154
+ resources:
155
+ cpus: 7
156
+ b:
157
+ erase: true
158
+ resources:
159
+ cpus: 2
160
+ c:
161
+ erase: false
162
+ resources:
163
+ cpus: 10
164
+ d:
165
+ resources:
166
+ cpus: 15
167
+ EOF
168
+
169
+ orchestrator = Workflow::Orchestrator.new(TestWF::MULT, "cpus" => 30, "IO" => 4, "size" => 10 )
170
+ Log.with_severity 3 do
171
+ orchestrator.process(rules, jobs)
172
+ end
173
+
174
+ jobs.each do |job|
175
+ assert job.step(:c).dependencies.empty?
176
+ assert job.step(:c).info[:archived_info].keys.select{|k| k.include?("TestWF/a/")}.any?
177
+ assert job.step(:c).info[:archived_info].keys.select{|k| k.include?("TestWF/b/")}.any?
178
+ end
179
+
180
+ end
181
+
182
+ def test_orchestrate_top_level
183
+
184
+ jobs =[]
185
+
186
+ num = 3
187
+ num.times do |i|
188
+ jobs.concat %w(TEST1 TEST2).collect{|name| TestWF.job(:d, name + " #{i}") }
189
+ jobs.concat %w(TEST1 TEST2).collect{|name| TestWF.job(:c, name + " #{i}") }
190
+ end
191
+ jobs.each do |j| j.recursive_clean end
192
+
193
+ rules = YAML.load <<-EOF
194
+ defaults:
195
+ erase: true
196
+ log: 4
197
+ default_resources:
198
+ IO: 1
199
+ TestWF:
200
+ a:
201
+ resources:
202
+ cpus: 7
203
+ b:
204
+ resources:
205
+ cpus: 2
206
+ c:
207
+ resources:
208
+ cpus: 10
209
+ d:
210
+ resources:
211
+ cpus: 15
212
+ EOF
213
+
214
+ orchestrator = Workflow::Orchestrator.new(TestWF::MULT, "cpus" => 30, "IO" => 4, "size" => 10 )
215
+ Log.with_severity 3 do
216
+ orchestrator.process(rules, jobs)
217
+ end
218
+
219
+ jobs.each do |job|
220
+ next unless job.task_name.to_s == 'd'
221
+ assert job.step(:c).dependencies.empty?
222
+ assert job.step(:c).info[:archived_info].keys.select{|k| k.include?("TestWF/a/")}.any?
223
+ assert job.step(:c).info[:archived_info].keys.select{|k| k.include?("TestWF/b/")}.any?
224
+ end
225
+
226
+ end
227
+
228
+ def test_orchestrate_top_level_double_dep
229
+
230
+ jobs =[]
231
+
232
+ num = 10
233
+ num.times do |i|
234
+ jobs.concat %w(TEST1 TEST2).collect{|name| TestWF.job(:e, name + " #{i}") }
235
+ jobs.concat %w(TEST1 TEST2).collect{|name| TestWF.job(:d, name + " #{i}") }
236
+ end
237
+ jobs.each do |j| j.recursive_clean end
238
+
239
+ rules = YAML.load <<-EOF
240
+ defaults:
241
+ erase: true
242
+ log: 4
243
+ default_resources:
244
+ IO: 1
245
+ TestWF:
246
+ a:
247
+ resources:
248
+ cpus: 7
249
+ b:
250
+ resources:
251
+ cpus: 2
252
+ c:
253
+ resources:
254
+ cpus: 10
255
+ d:
256
+ resources:
257
+ cpus: 15
258
+ EOF
259
+
260
+ orchestrator = Workflow::Orchestrator.new(TestWF::MULT, "cpus" => 30, "IO" => 4, "size" => 10 )
261
+ Log.with_severity 3 do
262
+ orchestrator.process(rules, jobs)
263
+ end
264
+
265
+ jobs.each do |job|
266
+ next unless job.task_name.to_s == 'd' || job.task_name.to_s == 'e'
267
+ assert job.info[:archived_info].keys.select{|k| k.include?("TestWF/c/")}.any?
268
+ assert job.info[:archived_info].keys.select{|k| k.include?("TestWF/c/")}.any?
269
+ end
270
+
271
+ end
272
+ end
273
+