mirage 2.2.3 → 2.3.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/Gemfile +2 -0
- data/Gemfile.lock +11 -2
- data/HISTORY +1 -0
- data/VERSION +1 -1
- data/bin/mirage +6 -8
- data/features/client/clear.feature +8 -8
- data/features/client/{response.feature → preview_responses.feature} +0 -0
- data/features/client/put.feature +9 -1
- data/features/client/{request.feature → requests.feature} +0 -0
- data/features/client/running.feature +49 -0
- data/features/client/{command_line_interface.feature → start.feature} +0 -12
- data/features/client/stop.feature +85 -0
- data/features/server/commandline_interface/help.feature +16 -0
- data/features/server/commandline_interface/start.feature +30 -0
- data/features/server/commandline_interface/stop.feature +42 -0
- data/features/server/prime.feature +8 -7
- data/features/step_definitions/my_steps.rb +20 -16
- data/features/support/command_line.rb +22 -0
- data/features/support/env.rb +11 -121
- data/features/support/hooks.rb +30 -0
- data/features/support/mirage.rb +8 -0
- data/lib/mirage/client/client.rb +124 -0
- data/lib/mirage/client/error.rb +22 -0
- data/lib/mirage/client/response.rb +29 -0
- data/lib/mirage/client/runner.rb +142 -0
- data/lib/mirage/client.rb +4 -206
- data/mirage.gemspec +25 -8
- data/mirage_server.rb +15 -10
- data/rakefile +7 -1
- data/spec/running_via_api_spec.rb +147 -0
- data/spec/running_via_api_windows_spec.rb +187 -0
- data/test.rb +21 -4
- metadata +66 -33
- data/features/client/mirage_client.feature +0 -36
- data/features/server/command_line_iterface.feature +0 -45
- data/lib/mirage/cli.rb +0 -69
data/features/support/env.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
ROOT_DIR = File.expand_path("#{File.dirname(__FILE__)}/../..")
|
2
|
+
SOURCE_PATH = "#{ROOT_DIR}/lib"
|
3
|
+
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift(SOURCE_PATH)
|
3
6
|
require 'rubygems'
|
4
7
|
require 'mirage/client'
|
5
8
|
require 'cucumber'
|
@@ -7,130 +10,17 @@ require 'rspec'
|
|
7
10
|
require 'mechanize'
|
8
11
|
require 'childprocess'
|
9
12
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
ENV['OS'] == 'Windows_NT'
|
15
|
-
end
|
16
|
-
end
|
17
|
-
World OsSupport
|
18
|
-
include OsSupport
|
19
|
-
|
20
|
-
SCRATCH = './scratch'
|
21
|
-
RUBY_CMD = RUBY_PLATFORM == 'JAVA' ? 'jruby' : 'ruby'
|
13
|
+
SCRATCH = "#{ROOT_DIR}/scratch"
|
14
|
+
RUBY_CMD = ChildProcess.jruby? ? 'jruby' : 'ruby'
|
15
|
+
BLANK_RUBYOPT_CMD = ChildProcess.windows? ? 'set RUBYOPT=' : "export RUBYOPT=''"
|
16
|
+
ENV['RUBYOPT'] = ''
|
22
17
|
|
23
18
|
|
24
|
-
BLANK_RUBYOPT_CMD = windows? ? 'set RUBYOPT=' : "export RUBYOPT=''"
|
25
|
-
|
26
19
|
if 'regression' == ENV['mode']
|
27
|
-
MIRAGE_CMD = windows? ? `where mirage.bat`.chomp : 'mirage'
|
20
|
+
MIRAGE_CMD = ChildProcess.windows? ? `where mirage.bat`.chomp : 'mirage'
|
28
21
|
else
|
29
22
|
MIRAGE_CMD = "#{RUBY_CMD} ../bin/mirage"
|
30
23
|
end
|
31
24
|
|
25
|
+
World(Mirage::Web)
|
32
26
|
|
33
|
-
|
34
|
-
module CommandLine
|
35
|
-
COMAND_LINE_OUTPUT_PATH = "#{File.dirname(__FILE__)}/../../#{SCRATCH}/commandline_output.txt"
|
36
|
-
module Windows
|
37
|
-
def run command
|
38
|
-
command = "#{MIRAGE_CMD} #{command.split(' ').drop(1).join(' ')}" if command =~ /^mirage/
|
39
|
-
command = "#{command} > #{COMAND_LINE_OUTPUT_PATH}"
|
40
|
-
Dir.chdir(SCRATCH)
|
41
|
-
`#{BLANK_RUBYOPT_CMD}`
|
42
|
-
process = ChildProcess.build(*(command.split(' ')))
|
43
|
-
process.start
|
44
|
-
sleep 0.5 until process.exited?
|
45
|
-
Dir.chdir('../')
|
46
|
-
File.read(COMAND_LINE_OUTPUT_PATH)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
module Linux
|
51
|
-
def run command
|
52
|
-
`#{BLANK_RUBYOPT_CMD} && cd #{SCRATCH} && #{command}`
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
|
58
|
-
module Web
|
59
|
-
include Mirage::Web
|
60
|
-
|
61
|
-
def normalise text
|
62
|
-
text.gsub(/[\n]/, ' ').gsub(/\s+/, ' ')
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
|
67
|
-
module Regression
|
68
|
-
include CommandLine
|
69
|
-
|
70
|
-
def run command
|
71
|
-
execute(command)
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
module Mirage
|
76
|
-
module Runner
|
77
|
-
def stop_mirage
|
78
|
-
system "cd #{SCRATCH} && #{MIRAGE_CMD} stop"
|
79
|
-
end
|
80
|
-
|
81
|
-
def start_mirage
|
82
|
-
if windows?
|
83
|
-
|
84
|
-
puts "starting mirage"
|
85
|
-
Dir.chdir(SCRATCH)
|
86
|
-
process = ChildProcess.build(MIRAGE_CMD, "start")
|
87
|
-
process.start
|
88
|
-
sleep 0.5 until process.exited?
|
89
|
-
Dir.chdir '../'
|
90
|
-
puts "finished starting mirage"
|
91
|
-
else
|
92
|
-
system "cd #{SCRATCH} && #{MIRAGE_CMD} start"
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
|
99
|
-
module IntelliJ
|
100
|
-
include CommandLine
|
101
|
-
def run command
|
102
|
-
execute "#{RUBY_CMD} #{command}"
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
include Mirage::Runner
|
107
|
-
|
108
|
-
World(Web)
|
109
|
-
World(Mirage::Runner)
|
110
|
-
windows? ? World(CommandLine::Windows) : World(CommandLine::Linux)
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
Before do
|
115
|
-
FileUtils.mkdir_p(SCRATCH)
|
116
|
-
$mirage = Mirage::Client.new
|
117
|
-
if $mirage.running?
|
118
|
-
$mirage.clear
|
119
|
-
else
|
120
|
-
start_mirage
|
121
|
-
end
|
122
|
-
|
123
|
-
Dir["#{SCRATCH}/*"].each do |file|
|
124
|
-
FileUtils.rm_rf(file) unless file == "#{SCRATCH}/mirage.log"
|
125
|
-
end
|
126
|
-
|
127
|
-
if File.exists? "#{SCRATCH}/mirage.log"
|
128
|
-
@mirage_log_file = File.open("#{SCRATCH}/mirage.log")
|
129
|
-
@mirage_log_file.seek(0, IO::SEEK_END)
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
|
134
|
-
at_exit do
|
135
|
-
stop_mirage if $mirage.running?
|
136
|
-
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
Before do
|
2
|
+
FileUtils.mkdir_p(SCRATCH)
|
3
|
+
|
4
|
+
if Mirage.running?
|
5
|
+
$mirage.clear
|
6
|
+
else
|
7
|
+
$mirage = start_mirage_in_scratch_dir
|
8
|
+
end
|
9
|
+
|
10
|
+
Dir["#{SCRATCH}/*"].each do |file|
|
11
|
+
FileUtils.rm_rf(file) unless file == "#{SCRATCH}/mirage.log"
|
12
|
+
end
|
13
|
+
|
14
|
+
if File.exists? "#{SCRATCH}/mirage.log"
|
15
|
+
@mirage_log_file = File.open("#{SCRATCH}/mirage.log")
|
16
|
+
@mirage_log_file.seek(0, IO::SEEK_END)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Before ('@command_line') do
|
21
|
+
Mirage.stop :all
|
22
|
+
end
|
23
|
+
|
24
|
+
After('@command_line') do
|
25
|
+
Mirage.stop :all
|
26
|
+
end
|
27
|
+
|
28
|
+
at_exit do
|
29
|
+
Mirage.stop :all
|
30
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'uri'
|
2
|
+
module Mirage
|
3
|
+
class Client
|
4
|
+
include Mirage::Web
|
5
|
+
attr_reader :url
|
6
|
+
|
7
|
+
# Creates an instance of the Mirage client that can be used to interact with the Mirage Server
|
8
|
+
#
|
9
|
+
# Client.new => a client that is configured to connect to Mirage on http://localhost:7001/mirage (the default settings for Mirage)
|
10
|
+
# Client.new(URL) => a client that is configured to connect to an instance of Mirage running on the specified url.
|
11
|
+
def initialize url="http://localhost:7001/mirage"
|
12
|
+
@url = url
|
13
|
+
end
|
14
|
+
|
15
|
+
def stop
|
16
|
+
Mirage.stop :port => URI.parse(@url).port
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
# Set a text or file based response template, to be hosted at a given end point. A block can be specified to configure the template
|
21
|
+
# client.set(endpoint, response, &block) => unique id that can be used to call back to the server
|
22
|
+
#
|
23
|
+
# Examples:
|
24
|
+
# client.put('greeting', 'hello')
|
25
|
+
#
|
26
|
+
# client.put('greeting', 'hello') do |response|
|
27
|
+
# response.pattern = 'pattern' #regex or string literal applied against the request querystring and body
|
28
|
+
# response.method = :post #By default templates will respond to get requests
|
29
|
+
# response.content_type = 'text/html' #defaults text/plain
|
30
|
+
# response.default = true # defaults to false. setting to true will allow this template to respond to request made to sub resources should it match.
|
31
|
+
# end
|
32
|
+
def put endpoint, response_value, &block
|
33
|
+
response = Mirage::Response.new response_value
|
34
|
+
|
35
|
+
yield response if block_given?
|
36
|
+
|
37
|
+
build_response(http_put("#{@url}/templates/#{endpoint}", response.value, response.headers))
|
38
|
+
end
|
39
|
+
|
40
|
+
# Use to look to preview the content of a response template would return to a client without actually triggering.
|
41
|
+
# client.response(response_id) => response held on the server as a String
|
42
|
+
def response response_id
|
43
|
+
response = build_response(http_get("#{@url}/templates/#{response_id}"))
|
44
|
+
case response
|
45
|
+
when String then
|
46
|
+
return response
|
47
|
+
when Mirage::Web::FileResponse then
|
48
|
+
return response.response.body
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
# Clear Content from Mirage
|
54
|
+
#
|
55
|
+
# If a response id is not valid, a ResponseNotFound exception will be thrown
|
56
|
+
#
|
57
|
+
# Example Usage:
|
58
|
+
# client.clear -> clear all responses and associated requests
|
59
|
+
# client.clear(response_id) -> Clear the response and tracked request for a given response id
|
60
|
+
# client.clear(:requests) -> Clear all tracked request information
|
61
|
+
# client.clear(:request => response_id) -> Clear the tracked request for a given response id
|
62
|
+
def clear thing=nil
|
63
|
+
|
64
|
+
case thing
|
65
|
+
when :requests
|
66
|
+
http_delete("#{@url}/requests")
|
67
|
+
when Numeric then
|
68
|
+
http_delete("#{@url}/templates/#{thing}")
|
69
|
+
when Hash then
|
70
|
+
puts "deleteing request #{thing[:request]}"
|
71
|
+
http_delete("#{@url}/requests/#{thing[:request]}") if thing[:request]
|
72
|
+
else
|
73
|
+
NilClass
|
74
|
+
http_delete("#{@url}/templates")
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
# Retrieve the last request that triggered a response to be returned. If the request contained content in its body, this is returned. If the
|
81
|
+
# request did not have any content in its body then what ever was in the request query string is returned instead
|
82
|
+
#
|
83
|
+
# Example Usage
|
84
|
+
# client.request(response_id) -> Tracked request as a String
|
85
|
+
def request response_id
|
86
|
+
build_response(http_get("#{@url}/requests/#{response_id}"))
|
87
|
+
end
|
88
|
+
|
89
|
+
# Save the state of the Mirage server so that it can be reverted back to that exact state at a later time.
|
90
|
+
def save
|
91
|
+
http_put("#{@url}/backup", '').code == 200
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
# Revert the state of Mirage back to the state that was last saved
|
96
|
+
# If there is no snapshot to rollback to, nothing happens
|
97
|
+
def revert
|
98
|
+
http_put(@url, '').code == 200
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
# Check to see if mirage is running on the url that the client is pointing to
|
103
|
+
def running?
|
104
|
+
Mirage.running?(@url)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Clear down the Mirage Server and load any defaults that are in Mirages default responses directory.
|
108
|
+
def prime
|
109
|
+
build_response(http_put("#{@url}/defaults", ''))
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
def build_response response
|
114
|
+
case response.code.to_i
|
115
|
+
when 500 then
|
116
|
+
raise ::Mirage::InternalServerException.new(response.body, response.code.to_i)
|
117
|
+
when 404 then
|
118
|
+
raise ::Mirage::ResponseNotFound.new(response.body, response.code.to_i)
|
119
|
+
else
|
120
|
+
response.body
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Mirage
|
2
|
+
class MirageError < ::Exception
|
3
|
+
attr_reader :code
|
4
|
+
|
5
|
+
def initialize message, code
|
6
|
+
super message
|
7
|
+
@code = message, code
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class InternalServerException < MirageError;
|
12
|
+
end
|
13
|
+
|
14
|
+
class ResponseNotFound < MirageError;
|
15
|
+
end
|
16
|
+
|
17
|
+
class ClientError < ::Exception
|
18
|
+
def initialize message
|
19
|
+
super message
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
module Mirage
|
3
|
+
class Response
|
4
|
+
|
5
|
+
attr_accessor :content_type,:method, :response_code, :pattern, :default, :status, :delay
|
6
|
+
attr_reader :value
|
7
|
+
|
8
|
+
def initialize response
|
9
|
+
@content_type = 'text/plain'
|
10
|
+
@value = response
|
11
|
+
@method = :get
|
12
|
+
@status = 200
|
13
|
+
@delay = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def headers
|
17
|
+
headers = {}
|
18
|
+
headers['Content-Type']=@content_type
|
19
|
+
headers['X-mirage-file'] = 'true' if @response.kind_of?(IO)
|
20
|
+
headers['X-mirage-method'] = @method
|
21
|
+
headers['X-mirage-pattern'] = @pattern if @pattern
|
22
|
+
headers['X-mirage-default'] = @default if @default == true
|
23
|
+
headers['X-mirage-status'] = @status
|
24
|
+
headers['X-mirage-delay'] = @delay
|
25
|
+
headers
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'waitforit'
|
3
|
+
require 'childprocess'
|
4
|
+
require 'uri'
|
5
|
+
module Mirage
|
6
|
+
class << self
|
7
|
+
include Web
|
8
|
+
|
9
|
+
# Start Mirage locally on a given port
|
10
|
+
# Example Usage:
|
11
|
+
#
|
12
|
+
# Mirage.start :port => 9001 -> Configured MirageClient ready to use.
|
13
|
+
def start options={:port => 7001}
|
14
|
+
Runner.new.invoke(:start, [], options)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Stop locally running instance(s) of Mirage
|
18
|
+
#
|
19
|
+
# Example Usage:
|
20
|
+
# Mirage.stop -> Will stop mirage if there is only instance running. Can be running on any port.
|
21
|
+
# Mirage.stop :port => port -> stop mirage on a given port
|
22
|
+
# Mirage.stop :port => [port1, port2...] -> stops multiple running instances of Mirage
|
23
|
+
def stop options={}
|
24
|
+
options = {:port => :all} if options == :all
|
25
|
+
|
26
|
+
if options[:port]
|
27
|
+
options[:port] = [options[:port]] unless options[:port].is_a?(Array)
|
28
|
+
end
|
29
|
+
|
30
|
+
Runner.new.invoke(:stop, [], options)
|
31
|
+
rescue ClientError => e
|
32
|
+
raise ClientError.new("Mirage is running multiple ports, please specify the port(s) see api/tests for details")
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# Detect if Mirage is running on a URL or a local port
|
37
|
+
#
|
38
|
+
# Example Usage:
|
39
|
+
# Mirage.running? -> boolean indicating whether Mirage is running on *locally* on port 7001
|
40
|
+
# Mirage.running? :port => port -> boolean indicating whether Mirage is running on *locally* on the given port
|
41
|
+
# Mirage.running? url -> boolean indicating whether Mirage is running on the given URL
|
42
|
+
def running? options_or_url = {:port => 7001}
|
43
|
+
url = options_or_url.is_a?(Hash) ? "http://localhost:#{options_or_url[:port]}/mirage" : options_or_url
|
44
|
+
http_get(url) and return true
|
45
|
+
rescue Errno::ECONNREFUSED
|
46
|
+
return false
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
class Runner < Thor
|
52
|
+
include ::Mirage::Web
|
53
|
+
RUBY_CMD = ChildProcess.jruby? ? 'jruby' : 'ruby'
|
54
|
+
|
55
|
+
desc "start", "Starts mirage"
|
56
|
+
method_option :port, :aliases => "-p", :type => :numeric, :default => 7001, :desc => "port that mirage should be started on"
|
57
|
+
method_option :defaults, :aliases => "-d", :type => :string, :default => 'responses', :desc => "location to load default responses from"
|
58
|
+
method_option :debug, :type => :boolean, :default => false, :desc => "run in debug mode"
|
59
|
+
|
60
|
+
def start
|
61
|
+
unless mirage_process_ids([options[:port]]).empty?
|
62
|
+
puts "Mirage is already running: #{mirage_process_ids([options[:port]]).values.join(",")}"
|
63
|
+
return
|
64
|
+
end
|
65
|
+
|
66
|
+
mirage_server_file = "#{File.dirname(__FILE__)}/../../../mirage_server.rb"
|
67
|
+
|
68
|
+
if ChildProcess.windows?
|
69
|
+
command = ["cmd", "/C", "start", "mirage server port #{options[:port]}", RUBY_CMD, mirage_server_file]
|
70
|
+
else
|
71
|
+
command = [RUBY_CMD, mirage_server_file]
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
command = command.concat(options.to_a).flatten.collect { |arg| arg.to_s }
|
76
|
+
ChildProcess.build(*command).start
|
77
|
+
|
78
|
+
mirage_client = Mirage::Client.new "http://localhost:#{options[:port]}/mirage"
|
79
|
+
wait_until(:timeout_after => 30.seconds) { mirage_client.running? }
|
80
|
+
|
81
|
+
begin
|
82
|
+
mirage_client.prime
|
83
|
+
rescue Mirage::InternalServerException => e
|
84
|
+
puts "WARN: #{e.message}"
|
85
|
+
end
|
86
|
+
mirage_client
|
87
|
+
end
|
88
|
+
|
89
|
+
desc "stop", "Stops mirage"
|
90
|
+
method_option :port, :aliases => "-p", :type => :array, :banner => "[port_1 port_2|all]", :desc => "port(s) of mirage instance(s). ALL stops all running instances"
|
91
|
+
|
92
|
+
def stop
|
93
|
+
ports = options[:port] || []
|
94
|
+
if ports.empty?
|
95
|
+
mirage_process_ids = mirage_process_ids([:all])
|
96
|
+
raise ClientError.new("Mirage is running on ports #{mirage_process_ids.keys.sort.join(", ")}. Please run mirage stop -p [PORT(s)] instead") if mirage_process_ids.size > 1
|
97
|
+
end
|
98
|
+
|
99
|
+
ports = case ports
|
100
|
+
when %w(all), [:all], []
|
101
|
+
[:all]
|
102
|
+
else
|
103
|
+
ports.collect { |port| port.to_i }
|
104
|
+
end
|
105
|
+
|
106
|
+
mirage_process_ids(ports).values.each do |process_id|
|
107
|
+
ChildProcess.windows? ? `taskkill /F /T /PID #{process_id}` : IO.popen("kill -9 #{process_id}")
|
108
|
+
end
|
109
|
+
|
110
|
+
wait_until do
|
111
|
+
mirage_process_ids(ports).empty?
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def processes_with_name name
|
118
|
+
if ChildProcess.windows?
|
119
|
+
|
120
|
+
`tasklist /V | findstr "#{name.gsub(" ", '\\ ')}"`
|
121
|
+
else
|
122
|
+
IO.popen("ps aux | grep '#{name}' | grep -v grep | grep -v #{$$}")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def mirage_process_ids *ports
|
127
|
+
ports.flatten!
|
128
|
+
mirage_instances = {}
|
129
|
+
["Mirage Server", "mirage_server", "mirage server"].each do |process_name|
|
130
|
+
processes_with_name(process_name).lines.collect { |line| line.chomp }.each do |process_line|
|
131
|
+
pid = process_line.split(' ')[1]
|
132
|
+
port = process_line[/port (\d+)/, 1]
|
133
|
+
mirage_instances[port] = pid
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
return mirage_instances if ports.first.to_s.downcase == "all"
|
138
|
+
Hash[mirage_instances.find_all { |port, pid| ports.include?(port.to_i) }]
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|