blazing 0.0.16 → 0.1.0.alpha1
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/.gitignore +2 -0
- data/.rvmrc +1 -1
- data/Gemfile +0 -10
- data/Gemfile.lock +53 -44
- data/Guardfile +8 -0
- data/README.md +23 -99
- data/Rakefile +21 -4
- data/bin/blazing +23 -5
- data/blazing.gemspec +19 -8
- data/features/help_banner.feature +13 -0
- data/features/step_definitions/blazing_steps.rb +1 -0
- data/features/support/env.rb +9 -0
- data/lib/blazing.rb +4 -13
- data/lib/blazing/config.rb +38 -73
- data/lib/blazing/dsl_setter.rb +20 -0
- data/lib/blazing/recipe.rb +5 -78
- data/lib/blazing/runner.rb +44 -2
- data/lib/blazing/shell.rb +7 -0
- data/lib/blazing/target.rb +44 -134
- data/lib/blazing/templates/config.erb +21 -0
- data/lib/blazing/templates/hook.erb +43 -0
- data/lib/blazing/version.rb +1 -1
- data/spec/blazing/config_spec.rb +104 -48
- data/spec/blazing/integration/init_spec.rb +8 -37
- data/spec/blazing/integration/setup_local_spec.rb +27 -0
- data/spec/blazing/integration/setup_remote_spec.rb +32 -0
- data/spec/blazing/integration/update_spec.rb +38 -0
- data/spec/blazing/recipe_spec.rb +5 -66
- data/spec/blazing/runner_spec.rb +41 -6
- data/spec/blazing/target_spec.rb +26 -99
- data/spec/spec_helper.rb +18 -12
- data/spec/support/{config.rb → empty_config.rb} +0 -0
- metadata +170 -97
- data/TODO +0 -3
- data/lib/blazing/base.rb +0 -41
- data/lib/blazing/bootstrap.rb +0 -27
- data/lib/blazing/cli/base.rb +0 -64
- data/lib/blazing/cli/create.rb +0 -32
- data/lib/blazing/cli/hook.rb +0 -22
- data/lib/blazing/cli/templates/blazing.tt +0 -7
- data/lib/blazing/cli/templates/post-hook.tt +0 -25
- data/lib/blazing/core_ext/object.rb +0 -8
- data/lib/blazing/logger.rb +0 -31
- data/lib/blazing/recipes/bundler_recipe.rb +0 -20
- data/lib/blazing/recipes/rvm_recipe.rb +0 -11
- data/spec/blazing/binary_spec.rb +0 -11
- data/spec/blazing/cli/base_spec.rb +0 -94
- data/spec/blazing/cli/create_spec.rb +0 -27
- data/spec/blazing/cli/hook_spec.rb +0 -16
- data/spec/blazing/logger_spec.rb +0 -50
- data/spec/blazing/recipes/bundler_recipe_spec.rb +0 -32
- data/spec/blazing/recipes/passenger_recipe_spec.rb +0 -6
- data/spec/blazing/recipes/rvm_recipe_spec.rb +0 -11
- data/spec/blazing/remote_spec.rb +0 -136
@@ -0,0 +1,21 @@
|
|
1
|
+
#
|
2
|
+
# Blazing Configuration File
|
3
|
+
#
|
4
|
+
|
5
|
+
# Specify the location of your repository:
|
6
|
+
# repository 'git@github.com:path/to/your/repository'
|
7
|
+
|
8
|
+
# Define possible deploy targets:
|
9
|
+
# target :production, 'blazing@production.server.com:/where/your/code/is/shipped/to'
|
10
|
+
# target :staging, 'blazing@production.server.com:/where/your/code/is/shipped/to', :default => true
|
11
|
+
|
12
|
+
# Define which recipes should be used
|
13
|
+
# recipes [:rvm, :bundler]
|
14
|
+
|
15
|
+
# Define an rvm ruby/gemset to be used remotely
|
16
|
+
# rvm 'ruby-1.9.2@agemset'
|
17
|
+
# Or use the rvm specified in .rvmrc
|
18
|
+
# rvm :rvmrc
|
19
|
+
|
20
|
+
# Defina a rake task that should be run after deployment
|
21
|
+
# rake :a_rake_task_to_call_after_deployment
|
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
cd ..
|
4
|
+
GIT_DIR='.git'
|
5
|
+
|
6
|
+
git reset --hard HEAD
|
7
|
+
|
8
|
+
<% if @config.rvm %>
|
9
|
+
# Load RVM into a shell session *as a function*
|
10
|
+
if [[ -s "$HOME/.rvm/scripts/rvm" ]] ; then
|
11
|
+
|
12
|
+
# First try to load from a user install
|
13
|
+
source "$HOME/.rvm/scripts/rvm"
|
14
|
+
|
15
|
+
<% if @config.rvm == :rvmrc %>
|
16
|
+
source .rvmrc
|
17
|
+
<% else %>
|
18
|
+
rvm use <%= @config.rvm %>
|
19
|
+
<% end %>
|
20
|
+
|
21
|
+
elif [[ -s "/usr/local/rvm/scripts/rvm" ]] ; then
|
22
|
+
|
23
|
+
# Then try to load from a root install
|
24
|
+
source "/usr/local/rvm/scripts/rvm"
|
25
|
+
|
26
|
+
<% if @config.rvm == :rvmrc %>
|
27
|
+
source .rvmrc
|
28
|
+
<% else %>
|
29
|
+
rvm use <%= @config.rvm %>
|
30
|
+
<% end %>
|
31
|
+
|
32
|
+
else
|
33
|
+
printf "ERROR: RVM was enabled in config but no RVM installation was not found.\n"
|
34
|
+
fi
|
35
|
+
<% end %>
|
36
|
+
|
37
|
+
<% if @config.rake %>
|
38
|
+
rake <%= @config.rake %>
|
39
|
+
<% end %>
|
40
|
+
|
41
|
+
<%# bundle --deployment%>
|
42
|
+
<%# bundle exec rake RAILS_ENV=production RAILS_GROUPS=assets assets:precompile%>
|
43
|
+
<%# touch tmp/restart.txt%>
|
data/lib/blazing/version.rb
CHANGED
data/spec/blazing/config_spec.rb
CHANGED
@@ -3,80 +3,136 @@ require 'blazing/config'
|
|
3
3
|
|
4
4
|
describe Blazing::Config do
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
describe '#initialize' do
|
7
|
+
it 'takes the path of the config file as an argument' do
|
8
|
+
config = Blazing::Config.new('/some/where/config.rb')
|
9
|
+
config.file.should == '/some/where/config.rb'
|
10
|
+
end
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
@config.targets.size.should == 1
|
12
|
+
it 'takes the default config path if no path is specified' do
|
13
|
+
config = Blazing::Config.new
|
14
|
+
config.file.should == 'config/blazing.rb'
|
14
15
|
end
|
15
16
|
end
|
16
17
|
|
17
|
-
describe '
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
describe '.parse' do
|
19
|
+
|
20
|
+
it 'returns a config object' do
|
21
|
+
Blazing::Config.parse('spec/support/empty_config.rb').should be_a Blazing::Config
|
21
22
|
end
|
23
|
+
|
22
24
|
end
|
23
25
|
|
24
|
-
describe '#
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
26
|
+
describe '#default_target' do
|
27
|
+
|
28
|
+
it 'retunrs a target object if only one is present' do
|
29
|
+
config = Blazing::Config.new
|
30
|
+
config.target :sometarget, 'somewhere'
|
31
|
+
config.default_target.name.should be :sometarget
|
30
32
|
end
|
31
33
|
|
32
|
-
context 'when
|
33
|
-
it 'returns the target defined in config if there is only one' do
|
34
|
-
@config.target(:test_target, :deploy_to => 'smoeone@somewhere:/asdasdasd')
|
35
|
-
@config.find_target.name.should == 'test_target'
|
36
|
-
end
|
34
|
+
context 'when more than 1 target defined in config' do
|
37
35
|
|
38
|
-
it '
|
39
|
-
|
40
|
-
|
41
|
-
|
36
|
+
it 'raises an error when none is default' do
|
37
|
+
config = Blazing::Config.new
|
38
|
+
config.target :sometarget, 'somewhere'
|
39
|
+
config.target :someothertarget, 'somewhere'
|
40
|
+
lambda { config.default_target }.should raise_error
|
42
41
|
end
|
43
42
|
|
44
|
-
it '
|
45
|
-
|
43
|
+
it 'retunrs the target object with :default => true option if more than 1 target present' do
|
44
|
+
config = Blazing::Config.new
|
45
|
+
config.target :sometarget, 'somewhere', :default => true
|
46
|
+
config.target :someothertarget, 'somewhere'
|
47
|
+
config.default_target.name.should be :sometarget
|
46
48
|
end
|
49
|
+
|
47
50
|
end
|
48
51
|
end
|
49
52
|
|
50
|
-
describe '
|
53
|
+
describe 'DSL' do
|
54
|
+
|
51
55
|
before :each do
|
52
|
-
@config.
|
53
|
-
|
56
|
+
@config = Blazing::Config.new
|
57
|
+
end
|
58
|
+
|
59
|
+
describe 'target' do
|
60
|
+
|
61
|
+
it 'creates a target object for each target call' do
|
62
|
+
@config.target :somename, 'someuser@somehost:/path/to/deploy/to'
|
63
|
+
@config.target :someothername, 'someuser@somehost:/path/to/deploy/to'
|
64
|
+
|
65
|
+
@config.targets.each { |t| t.should be_a Blazing::Target }
|
66
|
+
@config.targets.size.should be 2
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'does not allow the creation of two targets with the same name' do
|
70
|
+
@config.target :somename, 'someuser@somehost:/path/to/deploy/to'
|
71
|
+
lambda { @config.target :somename, 'someuser@somehost:/path/to/deploy/to' }.should raise_error
|
54
72
|
end
|
55
73
|
end
|
56
74
|
|
57
|
-
|
58
|
-
|
59
|
-
|
75
|
+
describe 'repository' do
|
76
|
+
it 'assigns the repository if a value is given' do
|
77
|
+
@config.repository('somewhere')
|
78
|
+
@config.instance_variable_get("@repository").should == 'somewhere'
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'returns the repository string if no value is given' do
|
82
|
+
@config.repository('somewhere')
|
83
|
+
@config.repository.should == 'somewhere'
|
84
|
+
end
|
60
85
|
end
|
61
86
|
|
62
|
-
|
63
|
-
|
64
|
-
|
87
|
+
describe 'recipes' do
|
88
|
+
|
89
|
+
before :each do
|
90
|
+
class Blazing::Recipe::Dummy < Blazing::Recipe
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'is an empty recipe if nothing was set' do
|
95
|
+
@config.recipes.should be_a Array
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'accepts a single recipe as argument' do
|
99
|
+
@config.recipes :dummy
|
100
|
+
@config.recipes.first.should be_a Blazing::Recipe::Dummy
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'accepts an array of recipes as argument' do
|
104
|
+
@config.recipes [:dummy, :dummy]
|
105
|
+
@config.recipes.size.should be 2
|
106
|
+
@config.recipes.each { |r| r.should be_a Blazing::Recipe::Dummy }
|
107
|
+
end
|
65
108
|
end
|
66
|
-
end
|
67
109
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
110
|
+
describe 'rake' do
|
111
|
+
|
112
|
+
it 'takes the name of the rake task as argument' do
|
113
|
+
@config.rake :post_deploy
|
114
|
+
@config.instance_variable_get('@rake').should be :post_deploy
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'returns the name of the task if no argument given' do
|
118
|
+
@config.rake :post_deploy
|
119
|
+
@config.rake.should be :post_deploy
|
120
|
+
end
|
121
|
+
|
73
122
|
end
|
74
|
-
end
|
75
123
|
|
76
|
-
|
77
|
-
|
78
|
-
|
124
|
+
describe 'rvm' do
|
125
|
+
|
126
|
+
it 'takes an rvm string as argument' do
|
127
|
+
@config.rvm 'ruby-1.9.2@rails31'
|
128
|
+
@config.instance_variable_get('@rvm').should == 'ruby-1.9.2@rails31'
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'returns the rvm string if no argument given' do
|
132
|
+
@config.rvm 'ruby-1.9.2@rails31'
|
133
|
+
@config.rvm.should == 'ruby-1.9.2@rails31'
|
134
|
+
end
|
135
|
+
|
79
136
|
end
|
80
137
|
end
|
81
|
-
|
82
138
|
end
|
@@ -1,53 +1,24 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require 'blazing'
|
3
|
-
require '
|
2
|
+
require 'blazing/config'
|
3
|
+
require 'blazing/runner'
|
4
4
|
|
5
5
|
describe 'blazing init' do
|
6
6
|
|
7
7
|
before :each do
|
8
|
-
|
9
|
-
|
10
|
-
@blazing = Blazing::CLI::Base.new
|
11
|
-
|
12
|
-
# Sometimes, when specs failed, the sandbox would stick around
|
13
|
-
FileUtils.rm_rf(@sandbox_directory) if Dir.exists?(@sandbox_directory)
|
14
|
-
|
15
|
-
# Setup Sandbox
|
16
|
-
Dir.mkdir(@sandbox_directory)
|
17
|
-
Dir.chdir(@sandbox_directory)
|
18
|
-
|
19
|
-
# Setup empty repository
|
20
|
-
@repository_dir = 'repo_without_config'
|
21
|
-
Dir.mkdir(@repository_dir)
|
22
|
-
Dir.chdir(@repository_dir)
|
23
|
-
`git init`
|
8
|
+
setup_sandbox
|
9
|
+
capture(:stdout) { Blazing::Runner.new.exec('init') }
|
24
10
|
end
|
25
11
|
|
26
12
|
after :each do
|
27
|
-
|
28
|
-
Dir.chdir(@blazing_root)
|
29
|
-
FileUtils.rm_rf(@sandbox_directory)
|
13
|
+
teardown_sandbox
|
30
14
|
end
|
31
15
|
|
32
|
-
it 'creates
|
33
|
-
silence(:stdout) { @blazing.init }
|
16
|
+
it 'creates a config directory if none exists yet' do
|
34
17
|
Dir.exists?('config').should be true
|
35
18
|
end
|
36
19
|
|
37
|
-
it '
|
38
|
-
|
39
|
-
end
|
40
|
-
|
41
|
-
it 'attempts to use the origin remote to setup the repository in the config' do
|
42
|
-
@origin = 'git@github.com:someone/somerepo.git'
|
43
|
-
`git remote add origin #{@origin}`
|
44
|
-
silence(:stdout) { @blazing.init }
|
45
|
-
Blazing::Config.parse.repository.should == @origin
|
46
|
-
end
|
47
|
-
|
48
|
-
it 'sets a dummy repository config when no remote was found' do
|
49
|
-
silence(:stdout) { @blazing.init }
|
50
|
-
Blazing::Config.parse.repository.should == 'user@host:/some/path'
|
20
|
+
it 'creates a config file in config/blazing.rb' do
|
21
|
+
File.exists?('config/blazing.rb').should be true
|
51
22
|
end
|
52
23
|
|
53
24
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'blazing/config'
|
3
|
+
require 'blazing/runner'
|
4
|
+
require 'grit'
|
5
|
+
|
6
|
+
describe 'blazing setup:local' do
|
7
|
+
|
8
|
+
before :each do
|
9
|
+
setup_sandbox
|
10
|
+
@production_url = 'user@host:/some/where'
|
11
|
+
@staging_url = 'user@host:/some/where/else'
|
12
|
+
|
13
|
+
@config = Blazing::Config.new
|
14
|
+
@config.target :production, @production_url
|
15
|
+
@config.target :staging, @staging_url, :default => true
|
16
|
+
capture(:stdout) { Blazing::Runner.new(@config).exec('setup:local') }
|
17
|
+
end
|
18
|
+
|
19
|
+
after :each do
|
20
|
+
teardown_sandbox
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'adds a git remote for each target' do
|
24
|
+
Grit::Repo.new(Dir.pwd).config['remote.production.url'].should == @production_url
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'blazing/config'
|
3
|
+
require 'blazing/runner'
|
4
|
+
require 'grit'
|
5
|
+
|
6
|
+
describe 'blazing setup:remote' do
|
7
|
+
|
8
|
+
before :each do
|
9
|
+
setup_sandbox
|
10
|
+
@production_url = 'user@host:/some/where'
|
11
|
+
@staging_url = 'user@host:/some/where/else'
|
12
|
+
|
13
|
+
@config = Blazing::Config.new
|
14
|
+
@config.target :production, @production_url
|
15
|
+
@config.target :staging, @staging_url, :default => true
|
16
|
+
@runner = Blazing::Runner.new(@config)
|
17
|
+
end
|
18
|
+
|
19
|
+
after :each do
|
20
|
+
teardown_sandbox
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'clones the repository, configures it and updates the target' do
|
24
|
+
@shell = Blazing::Shell.new
|
25
|
+
@target = @config.default_target
|
26
|
+
@target.instance_variable_set('@shell', @shell)
|
27
|
+
@shell.should_receive(:run).with("ssh user@host 'git clone /some/where/else && cd /some/where/else && git config receive.denyCurrentBranch ignore'")
|
28
|
+
@target.should_receive(:update)
|
29
|
+
capture(:stdout) { @runner.exec('setup:remote') }
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'blazing/config'
|
3
|
+
require 'blazing/runner'
|
4
|
+
require 'grit'
|
5
|
+
|
6
|
+
describe 'blazing update' do
|
7
|
+
|
8
|
+
before :each do
|
9
|
+
setup_sandbox
|
10
|
+
@production_url = 'user@host:/some/where'
|
11
|
+
@staging_url = 'user@host:/some/where/else'
|
12
|
+
|
13
|
+
@config = Blazing::Config.new
|
14
|
+
@config.target :production, @production_url
|
15
|
+
@config.target :staging, @staging_url, :default => true
|
16
|
+
@runner = Blazing::Runner.new(@config)
|
17
|
+
end
|
18
|
+
|
19
|
+
after :each do
|
20
|
+
teardown_sandbox
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'generates the hook' do
|
24
|
+
capture(:stdout) { @runner.exec('update') }
|
25
|
+
File.exists?(Blazing::TMP_HOOK).should be true
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'copies the hook to the targets repository and makes it exectuable' do
|
29
|
+
@shell_double = double('shell')#Blazing::Shell.new
|
30
|
+
@target = @config.default_target
|
31
|
+
@target.instance_variable_set('@shell', @shell_double)
|
32
|
+
|
33
|
+
@shell_double.should_receive(:run).once.with("scp /tmp/post-receive user@host:/some/where/else/.git/hooks/post-receive")
|
34
|
+
@shell_double.should_receive(:run).once.with('ssh user@host chmod +x /some/where/else/.git/hooks/post-receive')
|
35
|
+
capture(:stdout) { @runner.exec('update') }
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
data/spec/blazing/recipe_spec.rb
CHANGED
@@ -3,76 +3,15 @@ require 'blazing/recipe'
|
|
3
3
|
|
4
4
|
describe Blazing::Recipe do
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
recipe.name.should == 'some_recipe'
|
10
|
-
end
|
11
|
-
|
12
|
-
it 'takes a symbol as name parameter and converts it to a string' do
|
13
|
-
recipe = Blazing::Recipe.new(:some_recipe)
|
14
|
-
recipe.name.should == 'some_recipe'
|
15
|
-
end
|
16
|
-
|
17
|
-
it 'accepts options' do
|
18
|
-
recipe = Blazing::Recipe.new(:some_recipe, :an_option => 'yeah')
|
19
|
-
recipe.options[:an_option].should == 'yeah'
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
context 'recipe discovery' do
|
24
|
-
|
25
|
-
around :each do |example|
|
26
|
-
# Make sure specs dont interfere with recipe discovery mechanism
|
27
|
-
recipes = Blazing::Recipe.list
|
28
|
-
recipes.each { |r| Blazing.send(:remove_const, r.name.to_s.gsub(/^.*::/, '')) rescue NameError }
|
29
|
-
example.run
|
30
|
-
recipes.each { |r| Blazing.send(:remove_const, r.name.to_s.gsub(/^.*::/, '')) rescue NameError }
|
31
|
-
end
|
32
|
-
|
33
|
-
it 'before loading them, no recipes are known' do
|
34
|
-
lambda { Blazing::RvmRecipe }.should raise_error NameError
|
35
|
-
end
|
36
|
-
|
37
|
-
it 'can discover available recipes' do
|
38
|
-
recipes = Blazing::Recipe.list
|
39
|
-
recipes.should be_all { |recipe| recipe.superclass.should == Blazing::Recipe }
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
describe '#recipe_class' do
|
44
|
-
it 'construct the correct classname to use from recipe name' do
|
45
|
-
Blazing::Recipe.load_builtin_recipes
|
46
|
-
Blazing::Recipe.new(:rvm).recipe_class.should == Blazing::RvmRecipe
|
47
|
-
end
|
48
|
-
|
49
|
-
context 'when trying to load an unknown recipe' do
|
50
|
-
it 'does not raise a NameError' do
|
51
|
-
lambda { Blazing::Recipe.new('weirdname').recipe_class }.should_not raise_error NameError
|
52
|
-
end
|
53
|
-
|
54
|
-
it 'logs an error message' do
|
55
|
-
@logger = double('logger')
|
56
|
-
@logger.should_receive(:log).with(:error, "unable to load weirdname recipe")
|
57
|
-
Blazing::Recipe.new('weirdname', :_logger => @logger).recipe_class
|
6
|
+
describe '.init_by_name' do
|
7
|
+
before :each do
|
8
|
+
class Blazing::Recipe::Dummy < Blazing::Recipe
|
58
9
|
end
|
59
10
|
end
|
60
|
-
end
|
61
11
|
|
62
|
-
|
63
|
-
|
64
|
-
class Blazing::BlahRecipe < Blazing::Recipe; end
|
65
|
-
lambda { Blazing::Recipe.new(:blah).run }.should raise_error RuntimeError
|
12
|
+
it 'initializes the correct recipe' do
|
13
|
+
Blazing::Recipe.init_by_name(:dummy).should be_a Blazing::Recipe::Dummy
|
66
14
|
end
|
67
15
|
end
|
68
16
|
|
69
|
-
context 'builtin recipes' do
|
70
|
-
it 'include an rvm recipe' do
|
71
|
-
lambda { Blazing::RvmRecipe }.should_not raise_error NameError
|
72
|
-
end
|
73
|
-
|
74
|
-
it 'include a bundler recipe' do
|
75
|
-
lambda { Blazing::BundlerRecipe }.should_not raise_error NameError
|
76
|
-
end
|
77
|
-
end
|
78
17
|
end
|