hudkins 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +23 -0
- data/.gemtest +0 -0
- data/History.txt +6 -0
- data/Manifest.txt +29 -0
- data/README.txt +98 -0
- data/Rakefile +25 -0
- data/bin/hudkins +5 -0
- data/lib/assets/config.xml +30 -0
- data/lib/assets/config_snippets/builders.xml +5 -0
- data/lib/assets/free_style_project.xml.erb +14 -0
- data/lib/assets/hudkinsrc +4 -0
- data/lib/hudkins.rb +263 -0
- data/lib/hudkins/command.rb +127 -0
- data/lib/hudkins/command/exec.rb +113 -0
- data/lib/hudkins/command/irb_start.rb +86 -0
- data/lib/hudkins/common.rb +69 -0
- data/lib/hudkins/errors.rb +7 -0
- data/lib/hudkins/job.rb +168 -0
- data/lib/hudkins/jobs.rb +86 -0
- data/lib/hudkins/mixin.rb +103 -0
- data/lib/hudkins/rake.rb +20 -0
- data/lib/hudkins/restclient.rb +130 -0
- data/lib/hudkins/sysinfo.rb +11 -0
- data/test/fixtures/config.erb +30 -0
- data/test/fixtures/jobs.erb +22 -0
- data/test/fixtures/new_project_config.erb +14 -0
- data/test/test_helper.rb +50 -0
- data/test/unit/hudkins/test_job.rb +65 -0
- data/test/unit/hudkins/test_jobs.rb +27 -0
- data/test/unit/test_hudkins.rb +74 -0
- metadata +153 -0
@@ -0,0 +1,127 @@
|
|
1
|
+
require "hudkins"
|
2
|
+
require "optparse"
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
##
|
6
|
+
# === Description
|
7
|
+
# Main class for the included bin to interact with Hudson at the command line.
|
8
|
+
#
|
9
|
+
# The Commands are setup in Hudkins::Command::Exec
|
10
|
+
#
|
11
|
+
# === Usage
|
12
|
+
# see `hudkins -h'
|
13
|
+
#
|
14
|
+
# a host option is required. Can be configured in env variable +hudkins_host+ or
|
15
|
+
# config file. all options can be specified in a yaml file in +~/.hudkinsrc+
|
16
|
+
# and/or +`pwd`/.hudkinsrc+
|
17
|
+
class Hudkins::Command
|
18
|
+
include Hudkins::Common
|
19
|
+
|
20
|
+
require "hudkins/command/exec"
|
21
|
+
# all the run_ commands
|
22
|
+
extend Hudkins::Command::Exec
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def run
|
26
|
+
parse_options
|
27
|
+
begin
|
28
|
+
return_string = send( @command )
|
29
|
+
puts return_string if return_string
|
30
|
+
rescue => e
|
31
|
+
raise_usage e.message
|
32
|
+
end
|
33
|
+
if @options[:irb]
|
34
|
+
run_start_irb
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# commands:
|
39
|
+
# helpers:
|
40
|
+
def usage_msg
|
41
|
+
usage = <<-EOB
|
42
|
+
|
43
|
+
Usage: hudkins [opts] commands [job_name]
|
44
|
+
|
45
|
+
Commands: unambiguous_partial_command
|
46
|
+
build: start a job building.
|
47
|
+
config: pretty print job config. job_name
|
48
|
+
create: create new hudson job.
|
49
|
+
host: print hudson host url
|
50
|
+
list: list jobs. [job_name used as filter].
|
51
|
+
|
52
|
+
|
53
|
+
Optional: job_name (depends on command)
|
54
|
+
may also be partial (picks first match)
|
55
|
+
|
56
|
+
EOB
|
57
|
+
end
|
58
|
+
|
59
|
+
def raise_usage msg = nil
|
60
|
+
warn msg if msg
|
61
|
+
puts usage_msg
|
62
|
+
exit 1
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse_options
|
66
|
+
# initialize cmd_list. This seems hacky, but I like having the cmd_list
|
67
|
+
# near the usage statement.
|
68
|
+
@options = {}
|
69
|
+
|
70
|
+
OptionParser.new do |opts|
|
71
|
+
opts.banner = usage_msg
|
72
|
+
|
73
|
+
opts.separator ""
|
74
|
+
opts.separator "Specific options:"
|
75
|
+
|
76
|
+
opts.on("--hud-host HOST", "Hudson host to connect to. Overrides rc file") do |h|
|
77
|
+
@options[:host] = h
|
78
|
+
end
|
79
|
+
|
80
|
+
opts.on("-i", "--irb", "Drop into irb.") do |i|
|
81
|
+
@options[:irb] = i
|
82
|
+
end
|
83
|
+
opts.on("-v", "--[no-]verbose", "Turn on verbose messages.") do |v|
|
84
|
+
$VERBOSE = @options[:verbose] = v
|
85
|
+
end
|
86
|
+
|
87
|
+
opts.separator ""
|
88
|
+
opts.separator "Common options:"
|
89
|
+
|
90
|
+
opts.on_tail("--version", "Show version") do
|
91
|
+
puts "Hudkins (#{Hudkins::VERSION}) (c) 2010"
|
92
|
+
exit
|
93
|
+
end
|
94
|
+
|
95
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
96
|
+
puts opts
|
97
|
+
exit
|
98
|
+
end
|
99
|
+
|
100
|
+
end.parse!
|
101
|
+
|
102
|
+
config.merge! @options
|
103
|
+
|
104
|
+
@command, @job_name = ARGV.shift(2)
|
105
|
+
@command ||= "default"
|
106
|
+
|
107
|
+
@hud = Hudkins.new hud_host
|
108
|
+
job @job_name
|
109
|
+
|
110
|
+
parse_command
|
111
|
+
end
|
112
|
+
|
113
|
+
def parse_command
|
114
|
+
# select unambiguous command to run.
|
115
|
+
cmds = cmd_list.select {|c| Regexp.new(@command.to_s, Regexp::IGNORECASE) === c}
|
116
|
+
case cmds.size
|
117
|
+
when 0 then
|
118
|
+
raise_usage "#{@command} is not recognized as a valid command."
|
119
|
+
when 1 then
|
120
|
+
@command = "run_" << cmds.first
|
121
|
+
else
|
122
|
+
raise_usage "Ambiguous command. Please specify:\n #{cmds.join("\n ")}\n\n"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
class Hudkins
|
2
|
+
##
|
3
|
+
# === Description
|
4
|
+
# mixin for interacting with the lib
|
5
|
+
#
|
6
|
+
# the bin is setup to run any command prefaced with `run_'
|
7
|
+
# each command should return something to be puts or nil
|
8
|
+
module Command::Exec
|
9
|
+
##
|
10
|
+
# default command. here as a place holder
|
11
|
+
def run_default
|
12
|
+
end
|
13
|
+
|
14
|
+
def run_build
|
15
|
+
"building!!!"
|
16
|
+
end
|
17
|
+
|
18
|
+
def run_config job_name = @job_name
|
19
|
+
required_params job_name => "job_name is required for this command."
|
20
|
+
[
|
21
|
+
job( job_name ).name,
|
22
|
+
job( job_name ).config
|
23
|
+
]
|
24
|
+
end
|
25
|
+
|
26
|
+
def run_create job_name = @job_name
|
27
|
+
required_params job_name => "job_name is required for this command."
|
28
|
+
"creating!!!"
|
29
|
+
end
|
30
|
+
|
31
|
+
def run_host
|
32
|
+
hud_host
|
33
|
+
end
|
34
|
+
|
35
|
+
def run_list job_name = @job_name
|
36
|
+
names( job_name || "" )
|
37
|
+
end
|
38
|
+
|
39
|
+
def run_start_irb
|
40
|
+
puts <<-EOS
|
41
|
+
|
42
|
+
---
|
43
|
+
Welcome to hudkins irb console.
|
44
|
+
type hudkins for help.
|
45
|
+
|
46
|
+
EOS
|
47
|
+
require "hudkins/command/irb_start"
|
48
|
+
# turn job names into methods
|
49
|
+
extend Hudkins::Command::Irb
|
50
|
+
IRB.start_session(nil, binding)
|
51
|
+
end
|
52
|
+
|
53
|
+
# helper commands
|
54
|
+
|
55
|
+
def hud
|
56
|
+
@hud
|
57
|
+
end
|
58
|
+
|
59
|
+
def job job_name = nil
|
60
|
+
if job_name
|
61
|
+
@job = hud.jobs.find_by_name job_name
|
62
|
+
end
|
63
|
+
@job
|
64
|
+
end
|
65
|
+
|
66
|
+
# could probably be cleaned up
|
67
|
+
def required_params values
|
68
|
+
e = Hudkins::ArgumentError.new ""
|
69
|
+
raize = false
|
70
|
+
values.each do |k,msg|
|
71
|
+
e << msg unless k
|
72
|
+
raize = true unless k
|
73
|
+
end
|
74
|
+
raise e if raize
|
75
|
+
end
|
76
|
+
|
77
|
+
def cmd_list
|
78
|
+
command_list.map {|m| m.gsub(/^run_/, '')}
|
79
|
+
end
|
80
|
+
|
81
|
+
def command_list
|
82
|
+
self.methods.select {|m| m =~ /^run_/}
|
83
|
+
end
|
84
|
+
|
85
|
+
def names name = ""
|
86
|
+
hud.jobs.names name
|
87
|
+
end
|
88
|
+
|
89
|
+
def hud_host
|
90
|
+
ENV["hudkins_host"] || config[:host] || raise_usage( "no hudkins_host defined." )
|
91
|
+
end
|
92
|
+
|
93
|
+
def config
|
94
|
+
@config ||= load_rc
|
95
|
+
end
|
96
|
+
|
97
|
+
def load_rc
|
98
|
+
hudkins_rc.inject({}) do |ret, rc|
|
99
|
+
h = YAML.load_file( rc )
|
100
|
+
ret.merge! h if h
|
101
|
+
ret
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def hudkins_rc
|
106
|
+
[
|
107
|
+
File.expand_path("~/.hudkinsrc"),
|
108
|
+
File.join(Dir.pwd, ".hudkinsrc")
|
109
|
+
].select {|f| File.size? f} # exists and is non 0
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require "irb"
|
2
|
+
|
3
|
+
# thank you!
|
4
|
+
# http://jameskilton.com/2009/04/02/embedding-irb-into-your-ruby-application/
|
5
|
+
|
6
|
+
module IRB # :nodoc:
|
7
|
+
# originally used the code snippet from the above link, which turned out to
|
8
|
+
# be a snippet from `ruby-debug'. However, I was having problems with CNTR-C
|
9
|
+
# and I realized I wanted to mimic irb, just be able to actually pass Irb.new
|
10
|
+
# a workspace param which it expected.
|
11
|
+
def self.start_session(ap_path = nil, binding = nil)
|
12
|
+
@@hack_binding = binding
|
13
|
+
IRB.start ap_path
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.get_binding
|
17
|
+
@@hack_binding
|
18
|
+
end
|
19
|
+
|
20
|
+
class Irb
|
21
|
+
alias_method :org_initialize, :initialize
|
22
|
+
def initialize *args
|
23
|
+
workspace = args.shift
|
24
|
+
workspace ||= WorkSpace.new(IRB.get_binding)
|
25
|
+
args.unshift workspace
|
26
|
+
org_initialize *args
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Hudkins
|
32
|
+
module Command::Irb
|
33
|
+
|
34
|
+
##
|
35
|
+
# In the irb console I want to access the same commands as
|
36
|
+
# Hudkins::Command::Exec but without the `run_' part.
|
37
|
+
def self.extend_object obj # :doc:
|
38
|
+
# in IRB allow run_* commands to be exec without `run_'
|
39
|
+
obj.command_list.each do |cmd|
|
40
|
+
s = <<-EOE
|
41
|
+
def self.#{cmd.gsub(/^run_/, '')} *args; #{cmd} *args; end
|
42
|
+
EOE
|
43
|
+
obj.module_eval s
|
44
|
+
end
|
45
|
+
super
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# This is cool. Override default method to add convenience methods for each
|
50
|
+
# job_name in #jobs. This allows `job_name' to be used as a method to
|
51
|
+
# access that particular job.
|
52
|
+
def method_missing sym, *args, &block
|
53
|
+
if job = @hud.jobs.find_by_name( sym )
|
54
|
+
job
|
55
|
+
else
|
56
|
+
super sym, *args, &block
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# help method in irb console
|
62
|
+
def hudkins
|
63
|
+
puts <<-EOS
|
64
|
+
|
65
|
+
self is Hudkins::Command
|
66
|
+
hudkins => This help message
|
67
|
+
reload! => reload lib
|
68
|
+
hud => Hudkins.new <hud_host>
|
69
|
+
job [job_name] => Hudkins::Job
|
70
|
+
job_name => Hudkins::Job
|
71
|
+
commands:
|
72
|
+
#{cmd_list.join("\n ")}
|
73
|
+
|
74
|
+
EOS
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# helper method in irb console to reload all the lib files.
|
79
|
+
def reload!
|
80
|
+
$".grep( /hudkins/ ).each do |f|
|
81
|
+
load f
|
82
|
+
end
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
end # Command::Irb
|
86
|
+
end # Hudkins
|
@@ -0,0 +1,69 @@
|
|
1
|
+
class Hudkins
|
2
|
+
# common methods available for all classes
|
3
|
+
module Common
|
4
|
+
|
5
|
+
# hide instance variables
|
6
|
+
def inspect( string = nil )
|
7
|
+
new_string = " " << string if string
|
8
|
+
"#<%s:0x%x%s>" % [self.class, (self.object_id << 1), new_string]
|
9
|
+
end
|
10
|
+
|
11
|
+
##
|
12
|
+
# === Description
|
13
|
+
# useful for escaping parameters to be sent to #get/#post
|
14
|
+
#
|
15
|
+
# === Parameters
|
16
|
+
# +Hash+:: converts a simple one dimentional hash to a url parameter
|
17
|
+
# query string and uri escapes it.
|
18
|
+
# +else+:: converts to string and uri escapes it.
|
19
|
+
def url_escape msg
|
20
|
+
case msg
|
21
|
+
when Hash then
|
22
|
+
URI.escape( msg.map {|k,v| "#{k}=#{v}" }.join("&") )
|
23
|
+
else
|
24
|
+
URI.escape( msg.to_s )
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
#--
|
29
|
+
# these classes are being included in the @hudkins class.
|
30
|
+
#++
|
31
|
+
|
32
|
+
def hudkins
|
33
|
+
@hudkins
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# accessor methods for child classes to get
|
38
|
+
def get *args
|
39
|
+
@hudkins.get *args
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# accessor methods for child classes to post
|
44
|
+
def post *args
|
45
|
+
@hudkins.post *args
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# TODO host is not universally accessible so this method might not belong here..
|
50
|
+
# === Notes
|
51
|
+
# My Hudson server is not publicially accessible so when I can't connect,
|
52
|
+
# Socket#getaddrinfo (RestClient) times out after more than 20 seconds! I
|
53
|
+
# created a healper class Hudkins::HostLookup to timeout Socket#getaddrinfo
|
54
|
+
# after Hudkins::HostLookup#TIMEOUT seconds (defaults to 2).
|
55
|
+
def host_available?
|
56
|
+
HostLookup.available? @host, @options[:host_timeout]
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# TODO host is not universally accessible so this method might not belong here..
|
61
|
+
# Raise TimeoutError unless hudson host is responding within specified number of seconds.
|
62
|
+
def check_host_availability
|
63
|
+
# host is URI.
|
64
|
+
msg = "`%s' did not respond within the set number of seconds." % [ host.host ]
|
65
|
+
raise TimeoutError, msg unless host_available?
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
data/lib/hudkins/job.rb
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
# === Description
|
2
|
+
# Primary class for interacting with a Hudson job
|
3
|
+
#
|
4
|
+
# === Examples
|
5
|
+
# hud = Hudkins.new "http://hudson.com"
|
6
|
+
# job = hud.jobs.find_by_name :job_name
|
7
|
+
#
|
8
|
+
# job.disabled? # => true
|
9
|
+
# job.disabled = false
|
10
|
+
# job.post_config!
|
11
|
+
#
|
12
|
+
# === attr_accessor_from_config methods
|
13
|
+
#
|
14
|
+
# I created custom attr_accessor like DSL methods that create accessor like
|
15
|
+
# methods for the Hudkins::Job object to easily interact with the xml config.
|
16
|
+
# Paradigm is to use method_name? for boolean values and method_name! for any
|
17
|
+
# methods that post updates to the server.
|
18
|
+
#
|
19
|
+
class Hudkins::Job
|
20
|
+
include Hudkins::Common
|
21
|
+
extend Hudkins::Mixin
|
22
|
+
|
23
|
+
include Comparable
|
24
|
+
|
25
|
+
attr_reader :name, :url, :color, :path, :config
|
26
|
+
|
27
|
+
#TODO figure out how to define a custom rdoc method to correctly document these
|
28
|
+
|
29
|
+
##
|
30
|
+
# :attr_accessor: scm_url
|
31
|
+
attr_accessor_from_config :scm_url, "//scm//remote"
|
32
|
+
##
|
33
|
+
# :attr_accessor: description
|
34
|
+
attr_accessor_from_config :description, "//project//descriptoin"
|
35
|
+
##
|
36
|
+
# :attr_accessor: can_roam
|
37
|
+
attr_accessor_from_config :can_roam, "//project//canRoam", :bool
|
38
|
+
##
|
39
|
+
# :attr_accessor: disabled
|
40
|
+
attr_accessor_from_config :disabled, "//project//disabled", :bool
|
41
|
+
##
|
42
|
+
# :attr_accessor: blocked_by_upstream
|
43
|
+
attr_accessor_from_config :blocked_by_upstream, "//project//blockBuildWhenUpstreamBuilding", :bool
|
44
|
+
##
|
45
|
+
# :attr_accessor: concurrent_builds
|
46
|
+
attr_accessor_from_config :concurrent_builds, "//project//concurrentBuild", :bool
|
47
|
+
attr_accessor_from_config :mail_recipients, "//publishers//recipients"
|
48
|
+
# this is really an array. I haven't added functionality for that yet
|
49
|
+
attr_accessor_from_config :shell_builders, "//builders/hudson.tasks.Shell", :array
|
50
|
+
##
|
51
|
+
# :attr_accessor: :rotate_logs_num
|
52
|
+
# Number of builds to keep
|
53
|
+
# -1 == infinite
|
54
|
+
attr_accessor_from_config :rotate_logs_num, "//logRotator/numToKeep", :int
|
55
|
+
##
|
56
|
+
# :attr_accessor: :rotate_logs_days
|
57
|
+
# Number of days to keep builds
|
58
|
+
# -1 == infinite
|
59
|
+
attr_accessor_from_config :rotate_logs_days, "//logRotator/daysToKeep", Integer
|
60
|
+
|
61
|
+
def initialize hudkins, data
|
62
|
+
@hudkins = hudkins
|
63
|
+
@name = data["name"]
|
64
|
+
@url = URI.parse data["url"]
|
65
|
+
@color = data["color"]
|
66
|
+
@path = @url.path
|
67
|
+
end
|
68
|
+
|
69
|
+
def inspect
|
70
|
+
super "@name=#{@name}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def url
|
74
|
+
@url.to_s
|
75
|
+
end
|
76
|
+
|
77
|
+
# Enumerables/Comparables...
|
78
|
+
def <=> other
|
79
|
+
# TODO how do I implement jobs.sort(&:path) ?
|
80
|
+
self.name <=> other.name
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# === Description
|
85
|
+
# get the job's config
|
86
|
+
# takes string (xml) or other's config
|
87
|
+
def update_config config = nil
|
88
|
+
config = case config
|
89
|
+
when String then
|
90
|
+
hudkins.parse_string( config, :xml )
|
91
|
+
when Nokogiri::XML::Document, NilClass then
|
92
|
+
config
|
93
|
+
else
|
94
|
+
raise "unknown config type #{config.class}"
|
95
|
+
end
|
96
|
+
@config = config || hudkins.get_parsed( path + "/config.xml", :accept => "application/xml" )
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# === Description
|
101
|
+
# accessor for job's config. Initializes then caches. Use update_config if out of date.
|
102
|
+
def config
|
103
|
+
@config ||= update_config
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# === Description
|
108
|
+
# Post the job's config back to the server to update it.
|
109
|
+
def post_config!
|
110
|
+
post path + "/config.xml", @config
|
111
|
+
end
|
112
|
+
|
113
|
+
def build!
|
114
|
+
get path + "/build"
|
115
|
+
end
|
116
|
+
|
117
|
+
def delete!
|
118
|
+
post path + "/doDelete"
|
119
|
+
end
|
120
|
+
|
121
|
+
def recreate!
|
122
|
+
hudkins.add_job name, config
|
123
|
+
end
|
124
|
+
|
125
|
+
def disable!
|
126
|
+
post path + "/disable"
|
127
|
+
end
|
128
|
+
|
129
|
+
def enable!
|
130
|
+
post path + "/enable"
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# === Description
|
135
|
+
# The remote api allows for updating just the description. I had to tweak the
|
136
|
+
# name because I still wanted description to be an attr_accessor_from_config
|
137
|
+
#
|
138
|
+
# === Example
|
139
|
+
# job.quick_description! # => "this is the description for job"
|
140
|
+
# job.quick_description! = "this is the new desc." # => Response obj
|
141
|
+
def quick_description! msg = nil
|
142
|
+
# another yuck!
|
143
|
+
if msg
|
144
|
+
post path + "/description?" + url_escape(:description => msg)
|
145
|
+
else
|
146
|
+
get( path + "/description" ).body
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
##
|
151
|
+
# === Description
|
152
|
+
# Copy job to new_job_name
|
153
|
+
#
|
154
|
+
# === Example
|
155
|
+
# new_job = job.copy "new-job-name"
|
156
|
+
# new_job.scm_url = "http://svn/new/job/path"
|
157
|
+
# new_job.post_config!
|
158
|
+
def copy new_job_name
|
159
|
+
# post /createItem?name=NEWJOBNAME&mode=copy&from=FROMJOBNAME
|
160
|
+
response = post "/createItem?" +
|
161
|
+
url_escape(:name => new_job_name, :mode => "copy", :from => name)
|
162
|
+
# return new job object
|
163
|
+
hudkins.update_jobs.find_by_name new_job_name if response.success?
|
164
|
+
end
|
165
|
+
|
166
|
+
# not sure if we want this.
|
167
|
+
# alias_method :dup, :copy
|
168
|
+
end
|