jenkins_api_client 0.8.1 → 0.9.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.
- data/.travis.yml +1 -0
- data/CHANGELOG.md +12 -0
- data/Rakefile +5 -0
- data/lib/jenkins_api_client/build_queue.rb +9 -6
- data/lib/jenkins_api_client/cli/base.rb +0 -1
- data/lib/jenkins_api_client/cli/system.rb +6 -0
- data/lib/jenkins_api_client/client.rb +73 -35
- data/lib/jenkins_api_client/exceptions.rb +6 -2
- data/lib/jenkins_api_client/job.rb +198 -124
- data/lib/jenkins_api_client/system.rb +7 -1
- data/lib/jenkins_api_client/version.rb +2 -2
- data/lib/jenkins_api_client/view.rb +2 -0
- data/scripts/login_with_irb.rb +1 -3
- data/spec/func_tests/client_spec.rb +25 -0
- data/spec/func_tests/job_spec.rb +31 -12
- data/spec/func_tests/node_spec.rb +18 -2
- data/spec/func_tests/system_spec.rb +10 -0
- data/spec/func_tests/view_spec.rb +166 -10
- data/spec/unit_tests/build_queue_spec.rb +148 -0
- data/spec/unit_tests/client_spec.rb +59 -35
- data/spec/unit_tests/job_spec.rb +16 -0
- data/spec/unit_tests/system_spec.rb +8 -0
- metadata +4 -3
@@ -65,11 +65,17 @@ module JenkinsApi
|
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
68
|
+
# Reload the Jenkins server
|
69
|
+
#
|
70
|
+
def reload
|
71
|
+
@client.api_post_request("/reload")
|
72
|
+
end
|
73
|
+
|
68
74
|
# This method waits till the server becomes ready after a start
|
69
75
|
# or restart.
|
70
76
|
#
|
71
77
|
def wait_for_ready
|
72
|
-
Timeout::timeout(
|
78
|
+
Timeout::timeout(@client.timeout) do
|
73
79
|
while true do
|
74
80
|
response = @client.get_root
|
75
81
|
puts "[INFO] Waiting for jenkins to restart..." if @client.debug
|
data/scripts/login_with_irb.rb
CHANGED
@@ -8,9 +8,7 @@ require 'yaml'
|
|
8
8
|
require 'irb'
|
9
9
|
|
10
10
|
begin
|
11
|
-
@client = JenkinsApi::Client.new(YAML.load_file(File.expand_path(
|
12
|
-
'~/.jenkins_api_client/spec.yml', __FILE__))
|
13
|
-
)
|
11
|
+
@client = JenkinsApi::Client.new(YAML.load_file(File.expand_path('~/.jenkins_api_client/login.yml', __FILE__)))
|
14
12
|
puts "logged-in to the Jenkins API, use the '@client' variable to use the client"
|
15
13
|
end
|
16
14
|
|
@@ -60,7 +60,26 @@ describe JenkinsApi::Client do
|
|
60
60
|
end
|
61
61
|
end
|
62
62
|
end
|
63
|
+
describe "#get_jenkins_version" do
|
64
|
+
it "Should the jenkins version" do
|
65
|
+
@client.get_jenkins_version.class.should == String
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "#get_hudson_version" do
|
70
|
+
it "Should get the hudson version" do
|
71
|
+
@client.get_hudson_version.class.should == String
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#get_server_date" do
|
76
|
+
it "Should return the server date" do
|
77
|
+
@client.get_server_date.class.should == String
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
63
81
|
|
82
|
+
describe "SubClassAccessorMethods" do
|
64
83
|
describe "#job" do
|
65
84
|
it "Should return a job object on call" do
|
66
85
|
@client.job.class.should == JenkinsApi::Client::Job
|
@@ -84,6 +103,12 @@ describe JenkinsApi::Client do
|
|
84
103
|
@client.system.class.should == JenkinsApi::Client::System
|
85
104
|
end
|
86
105
|
end
|
106
|
+
|
107
|
+
describe "#queue" do
|
108
|
+
it "Should return a build queue object on call" do
|
109
|
+
@client.queue.class.should == JenkinsApi::Client::BuildQueue
|
110
|
+
end
|
111
|
+
end
|
87
112
|
end
|
88
113
|
|
89
114
|
end
|
data/spec/func_tests/job_spec.rb
CHANGED
@@ -53,7 +53,9 @@ describe JenkinsApi::Client::Job do
|
|
53
53
|
describe "#create" do
|
54
54
|
it "Should be able to create a job by getting an xml" do
|
55
55
|
xml = @helper.create_job_xml
|
56
|
-
|
56
|
+
name = "qwerty_nonexistent_job"
|
57
|
+
@client.job.create(name, xml).to_i.should == 200
|
58
|
+
@client.job.list(name).include?(name).should be_true
|
57
59
|
end
|
58
60
|
end
|
59
61
|
|
@@ -225,12 +227,23 @@ describe JenkinsApi::Client::Job do
|
|
225
227
|
end
|
226
228
|
end
|
227
229
|
|
230
|
+
describe "#add_email_notification" do
|
231
|
+
it "Should accept email address and add to existing job" do
|
232
|
+
name = "email_notification_test_job"
|
233
|
+
params = {:name => name}
|
234
|
+
@client.job.create_freestyle(params).to_i.should == 200
|
235
|
+
@client.job.add_email_notification(
|
236
|
+
:name => name,
|
237
|
+
:notification_email => "testuser@testdomain.com"
|
238
|
+
).to_i.should == 200
|
239
|
+
@client.job.delete(name).to_i.should == 302
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
228
243
|
describe "#add_skype_notification" do
|
229
244
|
it "Should accept skype configuration and add to existing job" do
|
230
245
|
name = "skype_notification_test_job"
|
231
|
-
params = {
|
232
|
-
:name => name
|
233
|
-
}
|
246
|
+
params = {:name => name}
|
234
247
|
@client.job.create_freestyle(params).to_i.should == 200
|
235
248
|
@client.job.add_skype_notification(
|
236
249
|
:name => name,
|
@@ -239,6 +252,7 @@ describe JenkinsApi::Client::Job do
|
|
239
252
|
@client.job.delete(name).to_i.should == 302
|
240
253
|
end
|
241
254
|
end
|
255
|
+
|
242
256
|
describe "#rename" do
|
243
257
|
it "Should accept new and old job names and rename the job" do
|
244
258
|
xml = @helper.create_job_xml
|
@@ -335,12 +349,14 @@ describe JenkinsApi::Client::Job do
|
|
335
349
|
it "Should obtain the current build status for the specified job" do
|
336
350
|
build_status = @client.job.get_current_build_status(@job_name)
|
337
351
|
build_status.class.should == String
|
338
|
-
valid_build_status = [
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
352
|
+
valid_build_status = [
|
353
|
+
"not_run",
|
354
|
+
"aborted",
|
355
|
+
"success",
|
356
|
+
"failure",
|
357
|
+
"unstable",
|
358
|
+
"running"
|
359
|
+
]
|
344
360
|
valid_build_status.include?(build_status).should be_true
|
345
361
|
end
|
346
362
|
end
|
@@ -398,8 +414,11 @@ describe JenkinsApi::Client::Job do
|
|
398
414
|
it "Should be able to chain jobs based on the specified criteria" do
|
399
415
|
jobs = @client.job.list(@filter)
|
400
416
|
jobs.class.should == Array
|
401
|
-
start_jobs = @client.job.chain(
|
402
|
-
|
417
|
+
start_jobs = @client.job.chain(
|
418
|
+
jobs,
|
419
|
+
'failure',
|
420
|
+
["not_run", "aborted", 'failure'],
|
421
|
+
3
|
403
422
|
)
|
404
423
|
start_jobs.class.should == Array
|
405
424
|
start_jobs.length.should == 3
|
@@ -43,14 +43,30 @@ describe JenkinsApi::Client::Node do
|
|
43
43
|
end
|
44
44
|
|
45
45
|
describe "#create_dump_slave" do
|
46
|
+
|
47
|
+
def test_and_validate(params)
|
48
|
+
name = params[:name]
|
49
|
+
@client.node.create_dump_slave(params).to_i.should == 302
|
50
|
+
@client.node.list(name).include?(name).should be_true
|
51
|
+
@client.node.delete(params[:name]).to_i.should == 302
|
52
|
+
@client.node.list(name).include?(name).should be_false
|
53
|
+
end
|
54
|
+
|
46
55
|
it "accepts required params and creates the slave on jenkins" do
|
47
56
|
params = {
|
48
57
|
:name => "func_test_slave",
|
49
58
|
:slave_host => "10.10.10.10",
|
50
59
|
:private_key_file => "/root/.ssh/id_rsa"
|
51
60
|
}
|
52
|
-
|
53
|
-
|
61
|
+
test_and_validate(params)
|
62
|
+
end
|
63
|
+
it "accepts spaces and other characters in node name" do
|
64
|
+
params = {
|
65
|
+
:name => "slave with spaces and {special characters}",
|
66
|
+
:slave_host => "10.10.10.10",
|
67
|
+
:private_key_file => "/root/.ssh/id_rsa"
|
68
|
+
}
|
69
|
+
test_and_validate(params)
|
54
70
|
end
|
55
71
|
it "fails if name is missing" do
|
56
72
|
params = {
|
@@ -49,6 +49,16 @@ describe JenkinsApi::Client::System do
|
|
49
49
|
@client.system.wait_for_ready.should == true
|
50
50
|
end
|
51
51
|
end
|
52
|
+
|
53
|
+
describe "#reload" do
|
54
|
+
it "Should be able to reload a Jenkins server" do
|
55
|
+
@client.system.reload.to_i.should == 302
|
56
|
+
end
|
57
|
+
it "Should be able to wait after a force restart" do
|
58
|
+
@client.system.wait_for_ready.should == true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
52
62
|
end
|
53
63
|
|
54
64
|
end
|
@@ -19,6 +19,9 @@ describe JenkinsApi::Client::View do
|
|
19
19
|
puts "WARNING: Credentials are not set properly."
|
20
20
|
puts e.message
|
21
21
|
end
|
22
|
+
|
23
|
+
# Create a view that can be used for tests
|
24
|
+
@client.view.create("general_purpose_view").to_i.should == 302
|
22
25
|
end
|
23
26
|
|
24
27
|
describe "InstanceMethods" do
|
@@ -29,25 +32,178 @@ describe JenkinsApi::Client::View do
|
|
29
32
|
end
|
30
33
|
end
|
31
34
|
|
35
|
+
describe "#create" do
|
36
|
+
it "accepts the name of the view and creates the view" do
|
37
|
+
name = "test_view"
|
38
|
+
@client.view.create(name).to_i.should == 302
|
39
|
+
@client.view.list(name).include?(name).should be_true
|
40
|
+
@client.view.delete(name).to_i.should == 302
|
41
|
+
end
|
42
|
+
it "accepts spaces and other characters in the view name" do
|
43
|
+
name = "test view with spaces and {special characters}"
|
44
|
+
@client.view.create(name).to_i.should == 302
|
45
|
+
@client.view.list(name).include?(name).should be_true
|
46
|
+
@client.view.delete(name).to_i.should == 302
|
47
|
+
end
|
48
|
+
it "accepts the name of view and creates a listview" do
|
49
|
+
name = "test_view"
|
50
|
+
@client.view.create(name, "listview").to_i.should == 302
|
51
|
+
@client.view.list(name).include?(name).should be_true
|
52
|
+
@client.view.delete(name).to_i.should == 302
|
53
|
+
end
|
54
|
+
it "accepts the name of view and creates a myview" do
|
55
|
+
name = "test_view"
|
56
|
+
@client.view.create(name, "myview").to_i.should == 302
|
57
|
+
@client.view.list(name).include?(name).should be_true
|
58
|
+
@client.view.delete(name).to_i.should == 302
|
59
|
+
end
|
60
|
+
it "raises an error when unsupported view type is specified" do
|
61
|
+
expect(
|
62
|
+
lambda { @client.view.create(name, "awesomeview") }
|
63
|
+
).to raise_error
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "#create_list_view" do
|
68
|
+
|
69
|
+
def test_and_validate(params)
|
70
|
+
name = params[:name]
|
71
|
+
@client.view.create_list_view(params).to_i.should == 302
|
72
|
+
@client.view.list(name).include?(name).should be_true
|
73
|
+
@client.view.delete(name).to_i.should == 302
|
74
|
+
@client.view.list(name).include?(name).should be_false
|
75
|
+
end
|
76
|
+
|
77
|
+
it "accepts just the name of the view and creates the view" do
|
78
|
+
params = {
|
79
|
+
:name => "test_list_view"
|
80
|
+
}
|
81
|
+
test_and_validate(params)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "accepts description as an option" do
|
85
|
+
params = {
|
86
|
+
:name => "test_list_view",
|
87
|
+
:description => "test list view created for functional test"
|
88
|
+
}
|
89
|
+
test_and_validate(params)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "accepts filter_queue as an option" do
|
93
|
+
params = {
|
94
|
+
:name => "test_list_view",
|
95
|
+
:filter_queue => true
|
96
|
+
}
|
97
|
+
test_and_validate(params)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "accepts filter_executors as an option" do
|
101
|
+
params = {
|
102
|
+
:name => "test_list_view",
|
103
|
+
:filter_executors => true
|
104
|
+
}
|
105
|
+
test_and_validate(params)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "accepts regex as an option" do
|
109
|
+
params = {
|
110
|
+
:name => "test_list_view",
|
111
|
+
:regex => "^test.*"
|
112
|
+
}
|
113
|
+
test_and_validate(params)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
describe "#delete" do
|
118
|
+
name = "test_view_to_delete"
|
119
|
+
before(:all) do
|
120
|
+
@client.view.create(name).to_i.should == 302
|
121
|
+
end
|
122
|
+
it "accepts the name of the view and deletes from Jenkins" do
|
123
|
+
@client.view.list(name).include?(name).should be_true
|
124
|
+
@client.view.delete(name).to_i.should == 302
|
125
|
+
@client.view.list(name).include?(name).should be_false
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe "#list_jobs" do
|
130
|
+
it "accepts the view name and lists all jobs in the view" do
|
131
|
+
@client.view.list_jobs("general_purpose_view").class.should == Array
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe "#exists?" do
|
136
|
+
it "accepts the vie name and returns true if the view exists" do
|
137
|
+
@client.view.exists?("general_purpose_view").should be_true
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe "#add_job" do
|
142
|
+
before(:all) do
|
143
|
+
@client.job.create_freestyle(
|
144
|
+
:name => "test_job_for_view"
|
145
|
+
).to_i.should == 200
|
146
|
+
end
|
147
|
+
it "accepts the job and and adds it to the specified view" do
|
148
|
+
@client.view.add_job(
|
149
|
+
"general_purpose_view",
|
150
|
+
"test_job_for_view"
|
151
|
+
).to_i.should == 200
|
152
|
+
@client.view.list_jobs(
|
153
|
+
"general_purpose_view"
|
154
|
+
).include?("test_job_for_view").should be_true
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
describe "#remove_job" do
|
159
|
+
before(:all) do
|
160
|
+
unless @client.job.exists?("test_job_for_view")
|
161
|
+
@client.job.create_freestyle(
|
162
|
+
:name => "test_job_for_view"
|
163
|
+
).to_i.should == 200
|
164
|
+
end
|
165
|
+
unless @client.view.list_jobs(
|
166
|
+
"general_purpose_view").include?("test_job_for_view")
|
167
|
+
@client.view.add_job(
|
168
|
+
"general_purpose_job",
|
169
|
+
"test_job_for_view"
|
170
|
+
).to_i.should == 200
|
171
|
+
end
|
172
|
+
end
|
173
|
+
it "accepts the job name and removes it from the specified view" do
|
174
|
+
@client.view.remove_job(
|
175
|
+
"general_purpose_view",
|
176
|
+
"test_job_for_view"
|
177
|
+
).to_i.should == 200
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
32
181
|
describe "#get_config" do
|
33
182
|
it "obtaines the view config.xml from the server" do
|
34
|
-
|
35
|
-
|
36
|
-
|
183
|
+
expect(
|
184
|
+
lambda { @client.view.get_config("general_purpose_view") }
|
185
|
+
).not_to raise_error
|
37
186
|
end
|
38
187
|
end
|
39
188
|
|
40
189
|
describe "#post_config" do
|
41
190
|
it "posts the given config.xml to the jenkins server's view" do
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
191
|
+
expect(
|
192
|
+
lambda {
|
193
|
+
xml = @client.view.get_config("general_purpose_view")
|
194
|
+
@client.view.post_config("general_purpose_view", xml)
|
195
|
+
}
|
196
|
+
).not_to raise_error
|
48
197
|
end
|
49
198
|
end
|
50
|
-
|
51
199
|
end
|
200
|
+
|
201
|
+
after(:all) do
|
202
|
+
@client.view.delete("general_purpose_view").to_i.should == 302
|
203
|
+
if @client.job.exists?("test_job_for_view")
|
204
|
+
@client.job.delete("test_job_for_view").to_i.should == 302
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
52
208
|
end
|
53
209
|
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe JenkinsApi::Client::BuildQueue do
|
4
|
+
context "With properly initialized Client" do
|
5
|
+
before do
|
6
|
+
@client = mock
|
7
|
+
@queue = JenkinsApi::Client::BuildQueue.new(@client)
|
8
|
+
@sample_queue_json = {
|
9
|
+
"items" => [
|
10
|
+
{
|
11
|
+
"actions" => [
|
12
|
+
{
|
13
|
+
"causes" => [
|
14
|
+
{
|
15
|
+
|
16
|
+
}
|
17
|
+
]
|
18
|
+
}
|
19
|
+
],
|
20
|
+
"blocked" => true,
|
21
|
+
"buildable" => false,
|
22
|
+
"id" => 2,
|
23
|
+
"inQueueSince" => 1362906942731,
|
24
|
+
"params" => "",
|
25
|
+
"stuck" => false,
|
26
|
+
"task" => {
|
27
|
+
"name" => "queue_test",
|
28
|
+
"url" => "http://localhost:8080/job/queue_test/",
|
29
|
+
"color" => "grey_anime"
|
30
|
+
},
|
31
|
+
"why" => "Build #1 is already in progress (ETA:N/A)",
|
32
|
+
"buildStartMilliseconds" => 1362906942832
|
33
|
+
}
|
34
|
+
]
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "InstanceMethods" do
|
39
|
+
describe "#initialize" do
|
40
|
+
it "initializes by receiving an instance of client object" do
|
41
|
+
expect(
|
42
|
+
lambda{ JenkinsApi::Client::BuildQueue.new(@client) }
|
43
|
+
).not_to raise_error
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#size" do
|
48
|
+
it "returns the size of the queue" do
|
49
|
+
@client.should_receive(:api_get_request).with("/queue").and_return(
|
50
|
+
@sample_queue_json
|
51
|
+
)
|
52
|
+
@queue.size
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#list" do
|
57
|
+
it "returns the list of tasks in the queue" do
|
58
|
+
@client.should_receive(:api_get_request).with("/queue").and_return(
|
59
|
+
@sample_queue_json
|
60
|
+
)
|
61
|
+
@queue.list.class.should == Array
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "#get_age" do
|
66
|
+
it "returns the age of a task" do
|
67
|
+
@client.should_receive(:api_get_request).with("/queue").and_return(
|
68
|
+
@sample_queue_json
|
69
|
+
)
|
70
|
+
@queue.get_age("queue_test").class.should == Float
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "#get_details" do
|
75
|
+
it "returns the details of a task in the queue" do
|
76
|
+
@client.should_receive(:api_get_request).with("/queue").and_return(
|
77
|
+
@sample_queue_json
|
78
|
+
)
|
79
|
+
@queue.get_details("queue_test").class.should == Hash
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe "#get_causes" do
|
84
|
+
it "returns the causes of a task in queue" do
|
85
|
+
@client.should_receive(:api_get_request).with("/queue").and_return(
|
86
|
+
@sample_queue_json
|
87
|
+
)
|
88
|
+
@queue.get_causes("queue_test").class.should == Array
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "#get_reason" do
|
93
|
+
it "returns the reason of a task in queue" do
|
94
|
+
@client.should_receive(:api_get_request).with("/queue").and_return(
|
95
|
+
@sample_queue_json
|
96
|
+
)
|
97
|
+
@queue.get_reason("queue_test").class.should == String
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "#get_eta" do
|
102
|
+
it "returns the ETA of a task in queue" do
|
103
|
+
@client.should_receive(:api_get_request).with("/queue").and_return(
|
104
|
+
@sample_queue_json
|
105
|
+
)
|
106
|
+
@queue.get_eta("queue_test").class.should == String
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
describe "#get_params" do
|
111
|
+
it "returns the params of a task in queue" do
|
112
|
+
@client.should_receive(:api_get_request).with("/queue").and_return(
|
113
|
+
@sample_queue_json
|
114
|
+
)
|
115
|
+
@queue.get_params("queue_test").class.should == String
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe "#is_buildable?" do
|
120
|
+
it "returns true if the job is buildable" do
|
121
|
+
@client.should_receive(:api_get_request).with("/queue").and_return(
|
122
|
+
@sample_queue_json
|
123
|
+
)
|
124
|
+
@queue.is_buildable?("queue_test").should == false
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe "#is_blocked?" do
|
129
|
+
it "returns true if the job is blocked" do
|
130
|
+
@client.should_receive(:api_get_request).with("/queue").and_return(
|
131
|
+
@sample_queue_json
|
132
|
+
)
|
133
|
+
@queue.is_blocked?("queue_test").should == true
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe "#is_stuck?" do
|
138
|
+
it "returns true if the job is stuck" do
|
139
|
+
@client.should_receive(:api_get_request).with("/queue").and_return(
|
140
|
+
@sample_queue_json
|
141
|
+
)
|
142
|
+
@queue.is_stuck?("queue_test").should == false
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|