cannon 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.bundle/config +2 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/Gemfile +5 -0
- data/README +3 -0
- data/Rakefile +20 -0
- data/bin/bundler +16 -0
- data/bin/coderay +16 -0
- data/bin/htmldiff +16 -0
- data/bin/ldiff +16 -0
- data/bin/mustache +16 -0
- data/bin/pry +16 -0
- data/bin/rspec +16 -0
- data/cannon.gemspec +29 -0
- data/lib/cannon.rb +16 -0
- data/lib/cannon/app.rb +196 -0
- data/lib/cannon/concerns/path_cache.rb +48 -0
- data/lib/cannon/concerns/signature.rb +14 -0
- data/lib/cannon/config.rb +58 -0
- data/lib/cannon/cookie_jar.rb +99 -0
- data/lib/cannon/handler.rb +25 -0
- data/lib/cannon/middleware.rb +47 -0
- data/lib/cannon/middleware/content_type.rb +15 -0
- data/lib/cannon/middleware/cookies.rb +18 -0
- data/lib/cannon/middleware/files.rb +33 -0
- data/lib/cannon/middleware/flush_and_benchmark.rb +20 -0
- data/lib/cannon/middleware/request_logger.rb +13 -0
- data/lib/cannon/middleware/router.rb +19 -0
- data/lib/cannon/request.rb +36 -0
- data/lib/cannon/response.rb +165 -0
- data/lib/cannon/route.rb +80 -0
- data/lib/cannon/route_action.rb +92 -0
- data/lib/cannon/version.rb +3 -0
- data/lib/cannon/views.rb +28 -0
- data/spec/app_spec.rb +20 -0
- data/spec/config_spec.rb +41 -0
- data/spec/environments_spec.rb +68 -0
- data/spec/features/action_types_spec.rb +154 -0
- data/spec/features/cookies_spec.rb +62 -0
- data/spec/features/files_spec.rb +17 -0
- data/spec/features/method_types_spec.rb +104 -0
- data/spec/features/requests_spec.rb +59 -0
- data/spec/features/views_spec.rb +31 -0
- data/spec/fixtures/public/background.jpg +0 -0
- data/spec/fixtures/views/render_test.html +1 -0
- data/spec/fixtures/views/test.html +1 -0
- data/spec/spec_helper.rb +98 -0
- data/spec/support/cannon_test.rb +108 -0
- metadata +219 -0
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'Cookies', :cannon_app do
|
4
|
+
before(:all) do
|
5
|
+
cannon_app.get('/basic') do |request, response|
|
6
|
+
response.cookie(:simple, value: 'value')
|
7
|
+
response.send("cookie = #{request.cookies[:simple]}")
|
8
|
+
end
|
9
|
+
|
10
|
+
cannon_app.get('/cookies') do |request, response|
|
11
|
+
response.cookie(:remember_me, value: 'true')
|
12
|
+
response.cookie(:username, value: '"Luther;Martin"', expires: Time.new(2017, 10, 31, 10, 30, 05), httponly: true)
|
13
|
+
response.cookie(:password, value: 'by=faith')
|
14
|
+
response.send("username = #{request.cookies[:username]}, password = #{request.cookies[:password]}, remember_me = #{request.cookies[:remember_me]}")
|
15
|
+
end
|
16
|
+
|
17
|
+
cannon_app.get('/signed') do |request, response|
|
18
|
+
response.cookie(:secure_value, value: 'SECURE', signed: true)
|
19
|
+
response.send("secure value = #{request.cookies.signed[:secure_value]}")
|
20
|
+
end
|
21
|
+
|
22
|
+
cannon_app.listen(async: true)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'reads and writes cookies' do
|
26
|
+
get '/basic'
|
27
|
+
expect(response.body).to eq('cookie = ')
|
28
|
+
|
29
|
+
get '/basic'
|
30
|
+
expect(response.body).to eq('cookie = value')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'handles cookie options' do
|
34
|
+
get '/cookies'
|
35
|
+
expect(response.body).to eq('username = , password = , remember_me = ')
|
36
|
+
|
37
|
+
expect(cookies[:username].httponly).to be true
|
38
|
+
expect(cookies[:username].expires).to eq(Time.new(2017, 10, 31, 10, 30, 05))
|
39
|
+
expect(cookies[:password].expires).to be nil
|
40
|
+
|
41
|
+
get '/cookies'
|
42
|
+
expect(response.body).to eq('username = "Luther;Martin", password = by=faith, remember_me = true')
|
43
|
+
end
|
44
|
+
|
45
|
+
describe 'signed' do
|
46
|
+
before(:each) do
|
47
|
+
get '/signed'
|
48
|
+
expect(response.body).to eq('secure value = ')
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'will work if the cookie is not tampered' do
|
52
|
+
get '/signed'
|
53
|
+
expect(response.body).to eq('secure value = SECURE')
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'will clear the cookie if the cookie is tampered' do
|
57
|
+
cookies[:secure_value].value.gsub!('SECURE', 'SeCURE')
|
58
|
+
get '/signed'
|
59
|
+
expect(response.body).to eq('secure value = ')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'Files', :cannon_app do
|
4
|
+
before(:all) do
|
5
|
+
cannon_app.config.public_path = '../fixtures/public'
|
6
|
+
|
7
|
+
cannon_app.listen(async: true)
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'serves files' do
|
11
|
+
get '/background.jpg'
|
12
|
+
expect(response.body.size).to_not eq('')
|
13
|
+
expect(response.code).to be(200)
|
14
|
+
expect(response['Content-Type']).to eq('image/jpeg')
|
15
|
+
expect(response['Content-Length']).to eq('55697')
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'Method types', :cannon_app do
|
4
|
+
before(:all) do
|
5
|
+
cannon_app.get('/hi') do |request, response|
|
6
|
+
response.send('hi')
|
7
|
+
end
|
8
|
+
|
9
|
+
cannon_app.get('/value') do |request, response|
|
10
|
+
response.send("key = #{request.params[:key]}, place = #{request.params[:place]}")
|
11
|
+
end
|
12
|
+
|
13
|
+
cannon_app.post('/hi') do |request, response|
|
14
|
+
response.send('created!', status: :created)
|
15
|
+
end
|
16
|
+
|
17
|
+
cannon_app.post('/submit') do |request, response|
|
18
|
+
response.send("name=#{request.params[:name]}, age=#{request.params[:age]}")
|
19
|
+
end
|
20
|
+
|
21
|
+
cannon_app.patch('/update') do |request, response|
|
22
|
+
response.send("updated object #{request.params[:name]}")
|
23
|
+
end
|
24
|
+
|
25
|
+
cannon_app.put('/modify') do |request, response|
|
26
|
+
response.send("modified object #{request.params[:name]}")
|
27
|
+
end
|
28
|
+
|
29
|
+
cannon_app.delete('/object/:id') do |request, response|
|
30
|
+
response.send("deleted #{request.params[:id]}")
|
31
|
+
end
|
32
|
+
|
33
|
+
cannon_app.head('/object/:id') do |request, response|
|
34
|
+
response.header('ETag', "object_#{request.params[:id]}")
|
35
|
+
response.send('head body should be ignored')
|
36
|
+
end
|
37
|
+
|
38
|
+
cannon_app.all('/any') do |request, response|
|
39
|
+
response.send("request method = #{request.method}")
|
40
|
+
end
|
41
|
+
|
42
|
+
cannon_app.listen(async: true)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'handles get requests' do
|
46
|
+
get '/hi'
|
47
|
+
expect(response.code).to be(200)
|
48
|
+
expect(response.body).to eq('hi')
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'handles query params' do
|
52
|
+
get '/value', key: 'a value', place: '12 ave st'
|
53
|
+
expect(response.body).to eq('key = a value, place = 12 ave st')
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'handles post requests' do
|
57
|
+
post '/hi'
|
58
|
+
expect(response.body).to eq('created!')
|
59
|
+
expect(response.code).to eq(201)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'handles post params' do
|
63
|
+
post('/submit', name: 'John', age: 21)
|
64
|
+
expect(response.body).to eq('name=John, age=21')
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'handles put requests' do
|
68
|
+
put '/modify', name: 'zebra'
|
69
|
+
expect(response.body).to eq('modified object zebra')
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'handles patch requests' do
|
73
|
+
patch '/update', name: 'lion'
|
74
|
+
expect(response.body).to eq('updated object lion')
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'handles delete requests' do
|
78
|
+
delete '/object/34'
|
79
|
+
expect(response.body).to eq('deleted 34')
|
80
|
+
end
|
81
|
+
|
82
|
+
describe 'head requests' do
|
83
|
+
it 'handles head request headers' do
|
84
|
+
head '/object/45'
|
85
|
+
expect(response.headers['etag']).to eq('object_45')
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'does not return a body' do
|
89
|
+
head '/object/45'
|
90
|
+
expect(response.body).to be_nil
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'can be configured to handle all methods for routes' do
|
95
|
+
get '/any'
|
96
|
+
expect(response.body).to eq('request method = GET')
|
97
|
+
|
98
|
+
post '/any'
|
99
|
+
expect(response.body).to eq('request method = POST')
|
100
|
+
|
101
|
+
put '/any'
|
102
|
+
expect(response.body).to eq('request method = PUT')
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'Requests', :cannon_app do
|
4
|
+
before(:all) do
|
5
|
+
cannon_app.get('/basic') do |request, response|
|
6
|
+
response.send('hi')
|
7
|
+
end.handle do |request, response|
|
8
|
+
response.send(' how are you?')
|
9
|
+
end
|
10
|
+
|
11
|
+
cannon_app.get('/bad') do |response, request|
|
12
|
+
bad_fail_code
|
13
|
+
end
|
14
|
+
|
15
|
+
cannon_app.get('/resource/:id') do |request, response|
|
16
|
+
response.send("id = #{request.params[:id]}")
|
17
|
+
end
|
18
|
+
|
19
|
+
cannon_app.get('/:type/by-grouping/:grouping') do |request, response|
|
20
|
+
response.send("type=#{request.params[:type]}, grouping=#{request.params[:grouping]}, sort=#{request.params[:sort]}")
|
21
|
+
end
|
22
|
+
|
23
|
+
cannon_app.get('/object/:id') do |request, response|
|
24
|
+
response.send("view #{request.params[:id]}")
|
25
|
+
end
|
26
|
+
|
27
|
+
cannon_app.listen(async: true)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'sets the Content-Type' do
|
31
|
+
get '/basic'
|
32
|
+
expect(response['Content-Type']).to eq('text/plain; charset=us-ascii')
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'sets the Content-Length' do
|
36
|
+
get '/basic'
|
37
|
+
expect(response['Content-Length']).to eq('15')
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'handles params in routes' do
|
41
|
+
get '/resource/12'
|
42
|
+
expect(response.body).to eq('id = 12')
|
43
|
+
get '/messages/by-grouping/author', sort: 'name'
|
44
|
+
expect(response.body).to eq('type=messages, grouping=author, sort=name')
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'returns 404 for not found routes' do
|
48
|
+
get '/badroute'
|
49
|
+
expect(response.code).to be(404)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'returns 500 for errors' do
|
53
|
+
old_log_level = Cannon.config.log_level
|
54
|
+
Cannon.config.log_level = :fatal
|
55
|
+
get '/bad'
|
56
|
+
expect(response.code).to be(500)
|
57
|
+
Cannon.config.log_level = old_log_level
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'Views', :cannon_app do
|
4
|
+
before(:all) do
|
5
|
+
cannon_app.config.view_path = '../fixtures/views'
|
6
|
+
|
7
|
+
cannon_app.get('/view') do |request, response|
|
8
|
+
response.view('test.html')
|
9
|
+
end
|
10
|
+
|
11
|
+
cannon_app.get('/render') do |request, response|
|
12
|
+
response.view('render_test.html', name: 'John Calvin')
|
13
|
+
end
|
14
|
+
|
15
|
+
cannon_app.listen(async: true)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'rendering' do
|
19
|
+
it 'handles plain text' do
|
20
|
+
get '/view'
|
21
|
+
expect(response.body).to eq('Test view content')
|
22
|
+
expect(response.code).to be(200)
|
23
|
+
expect(response['Content-Type']).to eq('text/html')
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'handles mustache templates' do
|
27
|
+
get '/render'
|
28
|
+
expect(response.body).to eq('Hello John Calvin')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
Hello {{name}}
|
@@ -0,0 +1 @@
|
|
1
|
+
Test view content
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'cannon'
|
2
|
+
|
3
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
4
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
5
|
+
# The generated `.rspec` file contains `--require spec_helper` which will cause
|
6
|
+
# this file to always be loaded, without a need to explicitly require it in any
|
7
|
+
# files.
|
8
|
+
#
|
9
|
+
# Given that it is always loaded, you are encouraged to keep this file as
|
10
|
+
# light-weight as possible. Requiring heavyweight dependencies from this file
|
11
|
+
# will add to the boot time of your test suite on EVERY test run, even for an
|
12
|
+
# individual file that may not need all of that loaded. Instead, consider making
|
13
|
+
# a separate helper file that requires the additional dependencies and performs
|
14
|
+
# the additional setup, and require it from the spec files that actually need
|
15
|
+
# it.
|
16
|
+
#
|
17
|
+
# The `.rspec` file also contains a few flags that are not defaults but that
|
18
|
+
# users commonly want.
|
19
|
+
#
|
20
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
21
|
+
|
22
|
+
# Require support files
|
23
|
+
Dir[Dir.getwd + '/spec/support/**/*.rb'].each { |f| require f }
|
24
|
+
|
25
|
+
RSpec.configure do |config|
|
26
|
+
# rspec-expectations config goes here. You can use an alternate
|
27
|
+
# assertion/expectation library such as wrong or the stdlib/minitest
|
28
|
+
# assertions if you prefer.
|
29
|
+
config.expect_with :rspec do |expectations|
|
30
|
+
# This option will default to `true` in RSpec 4. It makes the `description`
|
31
|
+
# and `failure_message` of custom matchers include text for helper methods
|
32
|
+
# defined using `chain`, e.g.:
|
33
|
+
# be_bigger_than(2).and_smaller_than(4).description
|
34
|
+
# # => "be bigger than 2 and smaller than 4"
|
35
|
+
# ...rather than:
|
36
|
+
# # => "be bigger than 2"
|
37
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
38
|
+
end
|
39
|
+
|
40
|
+
# rspec-mocks config goes here. You can use an alternate test double
|
41
|
+
# library (such as bogus or mocha) by changing the `mock_with` option here.
|
42
|
+
config.mock_with :rspec do |mocks|
|
43
|
+
# Prevents you from mocking or stubbing a method that does not exist on
|
44
|
+
# a real object. This is generally recommended, and will default to
|
45
|
+
# `true` in RSpec 4.
|
46
|
+
mocks.verify_partial_doubles = true
|
47
|
+
end
|
48
|
+
|
49
|
+
# These two settings work together to allow you to limit a spec run
|
50
|
+
# to individual examples or groups you care about by tagging them with
|
51
|
+
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
52
|
+
# get run.
|
53
|
+
config.filter_run :focus
|
54
|
+
config.run_all_when_everything_filtered = true
|
55
|
+
|
56
|
+
# Allows RSpec to persist some state between runs in order to support
|
57
|
+
# the `--only-failures` and `--next-failure` CLI options. We recommend
|
58
|
+
# you configure your source control system to ignore this file.
|
59
|
+
config.example_status_persistence_file_path = "spec/examples.txt"
|
60
|
+
|
61
|
+
# Limits the available syntax to the non-monkey patched syntax that is
|
62
|
+
# recommended. For more details, see:
|
63
|
+
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
64
|
+
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
65
|
+
# - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
|
66
|
+
config.disable_monkey_patching!
|
67
|
+
|
68
|
+
# This setting enables warnings. It's recommended, but in some cases may
|
69
|
+
# be too noisy due to issues in dependencies.
|
70
|
+
# config.warnings = true
|
71
|
+
|
72
|
+
# Many RSpec users commonly either run the entire suite or an individual
|
73
|
+
# file, and it's useful to allow more verbose output when running an
|
74
|
+
# individual spec file.
|
75
|
+
if config.files_to_run.one?
|
76
|
+
# Use the documentation formatter for detailed output,
|
77
|
+
# unless a formatter has already been configured
|
78
|
+
# (e.g. via a command-line flag).
|
79
|
+
config.default_formatter = 'doc'
|
80
|
+
end
|
81
|
+
|
82
|
+
# Print the 10 slowest examples and example groups at the
|
83
|
+
# end of the spec run, to help surface which specs are running
|
84
|
+
# particularly slow.
|
85
|
+
# config.profile_examples = 10
|
86
|
+
|
87
|
+
# Run specs in random order to surface order dependencies. If you find an
|
88
|
+
# order dependency and want to debug it, you can fix the order by providing
|
89
|
+
# the seed, which is printed after each run.
|
90
|
+
# --seed 1234
|
91
|
+
config.order = :random
|
92
|
+
|
93
|
+
# Seed global randomization in this process using the `--seed` CLI option.
|
94
|
+
# Setting this allows you to use `--seed` to deterministically reproduce
|
95
|
+
# test failures related to randomization by passing the same `--seed` value
|
96
|
+
# as the one that triggered the failure.
|
97
|
+
Kernel.srand config.seed
|
98
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'http-cookie'
|
2
|
+
|
3
|
+
class MockResponse
|
4
|
+
def initialize(response)
|
5
|
+
@response = response
|
6
|
+
end
|
7
|
+
|
8
|
+
def code
|
9
|
+
@response.code.to_i
|
10
|
+
end
|
11
|
+
|
12
|
+
def headers
|
13
|
+
@headers ||= build_headers
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(sym, *args, &block)
|
17
|
+
@response.send(sym, *args, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def build_headers
|
23
|
+
headers = {}
|
24
|
+
each_header { |k, v| headers[k] = v }
|
25
|
+
headers
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
module Cannon::Test
|
30
|
+
PORT = 5031
|
31
|
+
|
32
|
+
def cannon_app
|
33
|
+
@cannon_app ||= create_cannon_app
|
34
|
+
end
|
35
|
+
|
36
|
+
def get(path, params = {})
|
37
|
+
http_request(path, Net::HTTP::Get, query_params: params)
|
38
|
+
end
|
39
|
+
|
40
|
+
def post(path, params = {})
|
41
|
+
http_request(path, Net::HTTP::Post, post_params: params)
|
42
|
+
end
|
43
|
+
|
44
|
+
def put(path, params = {})
|
45
|
+
http_request(path, Net::HTTP::Put, post_params: params)
|
46
|
+
end
|
47
|
+
|
48
|
+
def patch(path, params = {})
|
49
|
+
http_request(path, Net::HTTP::Patch, post_params: params)
|
50
|
+
end
|
51
|
+
|
52
|
+
def delete(path, params = {})
|
53
|
+
http_request(path, Net::HTTP::Delete, post_params: params)
|
54
|
+
end
|
55
|
+
|
56
|
+
def head(path, params = {})
|
57
|
+
http_request(path, Net::HTTP::Head, query_params: params)
|
58
|
+
end
|
59
|
+
|
60
|
+
def response
|
61
|
+
@response
|
62
|
+
end
|
63
|
+
|
64
|
+
def cookies
|
65
|
+
jar.inject({}) { |cookies, cookie| cookies[cookie.name.to_sym] = cookie; cookies }
|
66
|
+
end
|
67
|
+
|
68
|
+
def jar
|
69
|
+
@jar ||= HTTP::CookieJar.new
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def http_request(path, request_class, post_params: nil, query_params: nil)
|
75
|
+
uri = URI("http://127.0.0.1:#{PORT}#{path}")
|
76
|
+
uri.query = URI.encode_www_form(query_params) unless query_params.nil?
|
77
|
+
req = request_class.new(uri)
|
78
|
+
req.set_form_data(post_params) unless post_params.nil?
|
79
|
+
req['Cookie'] = HTTP::Cookie.cookie_value(jar.cookies(uri)) unless jar.empty?
|
80
|
+
|
81
|
+
@response = MockResponse.new(Net::HTTP.start(uri.hostname, uri.port) do |http|
|
82
|
+
http.request(req)
|
83
|
+
end)
|
84
|
+
|
85
|
+
if @response['Set-Cookie']
|
86
|
+
@response.get_fields('Set-Cookie').each do |cookie|
|
87
|
+
jar.parse(cookie, uri)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def create_cannon_app
|
93
|
+
app = Cannon::App.new(binding, port: PORT, ip_address: '127.0.0.1')
|
94
|
+
app.config.log_level = :error
|
95
|
+
app.config.cookies.secret = 'test'
|
96
|
+
app
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
RSpec.configure do |config|
|
101
|
+
config.include Cannon::Test
|
102
|
+
|
103
|
+
config.append_after(:context, cannon_app: true) do
|
104
|
+
cannon_app.stop
|
105
|
+
@cannon_app = nil
|
106
|
+
jar.clear
|
107
|
+
end
|
108
|
+
end
|