captivate 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (163) hide show
  1. data/.document +5 -0
  2. data/.gitignore +22 -0
  3. data/LICENSE +20 -0
  4. data/README.markdown +79 -0
  5. data/Rakefile +45 -0
  6. data/VERSION +1 -0
  7. data/bin/captivate +4 -0
  8. data/captivate-0.0.0.gem +0 -0
  9. data/captivate.gemspec +263 -0
  10. data/lib/captivate.rb +199 -0
  11. data/spec/acceptance_spec.rb +55 -0
  12. data/spec/captivate_spec.rb +269 -0
  13. data/spec/fixtures/rails_apps/multi_stage/Capfile +4 -0
  14. data/spec/fixtures/rails_apps/multi_stage/README +243 -0
  15. data/spec/fixtures/rails_apps/multi_stage/Rakefile +10 -0
  16. data/spec/fixtures/rails_apps/multi_stage/app/controllers/application_controller.rb +10 -0
  17. data/spec/fixtures/rails_apps/multi_stage/app/helpers/application_helper.rb +3 -0
  18. data/spec/fixtures/rails_apps/multi_stage/config/boot.rb +110 -0
  19. data/spec/fixtures/rails_apps/multi_stage/config/captivate.yml +4 -0
  20. data/spec/fixtures/rails_apps/multi_stage/config/database.yml +22 -0
  21. data/spec/fixtures/rails_apps/multi_stage/config/deploy.rb +27 -0
  22. data/spec/fixtures/rails_apps/multi_stage/config/deploy/staging.rb +0 -0
  23. data/spec/fixtures/rails_apps/multi_stage/config/environment.rb +41 -0
  24. data/spec/fixtures/rails_apps/multi_stage/config/environments/development.rb +17 -0
  25. data/spec/fixtures/rails_apps/multi_stage/config/environments/production.rb +28 -0
  26. data/spec/fixtures/rails_apps/multi_stage/config/environments/test.rb +28 -0
  27. data/spec/fixtures/rails_apps/multi_stage/config/initializers/backtrace_silencers.rb +7 -0
  28. data/spec/fixtures/rails_apps/multi_stage/config/initializers/inflections.rb +10 -0
  29. data/spec/fixtures/rails_apps/multi_stage/config/initializers/mime_types.rb +5 -0
  30. data/spec/fixtures/rails_apps/multi_stage/config/initializers/new_rails_defaults.rb +21 -0
  31. data/spec/fixtures/rails_apps/multi_stage/config/initializers/session_store.rb +15 -0
  32. data/spec/fixtures/rails_apps/multi_stage/config/locales/en.yml +5 -0
  33. data/spec/fixtures/rails_apps/multi_stage/config/routes.rb +43 -0
  34. data/spec/fixtures/rails_apps/multi_stage/db/seeds.rb +7 -0
  35. data/spec/fixtures/rails_apps/multi_stage/doc/README_FOR_APP +2 -0
  36. data/spec/fixtures/rails_apps/multi_stage/log/development.log +0 -0
  37. data/spec/fixtures/rails_apps/multi_stage/log/production.log +0 -0
  38. data/spec/fixtures/rails_apps/multi_stage/log/server.log +0 -0
  39. data/spec/fixtures/rails_apps/multi_stage/log/test.log +0 -0
  40. data/spec/fixtures/rails_apps/multi_stage/public/404.html +30 -0
  41. data/spec/fixtures/rails_apps/multi_stage/public/422.html +30 -0
  42. data/spec/fixtures/rails_apps/multi_stage/public/500.html +30 -0
  43. data/spec/fixtures/rails_apps/multi_stage/public/favicon.ico +0 -0
  44. data/spec/fixtures/rails_apps/multi_stage/public/images/rails.png +0 -0
  45. data/spec/fixtures/rails_apps/multi_stage/public/index.html +275 -0
  46. data/spec/fixtures/rails_apps/multi_stage/public/javascripts/application.js +2 -0
  47. data/spec/fixtures/rails_apps/multi_stage/public/javascripts/controls.js +963 -0
  48. data/spec/fixtures/rails_apps/multi_stage/public/javascripts/dragdrop.js +973 -0
  49. data/spec/fixtures/rails_apps/multi_stage/public/javascripts/effects.js +1128 -0
  50. data/spec/fixtures/rails_apps/multi_stage/public/javascripts/prototype.js +4320 -0
  51. data/spec/fixtures/rails_apps/multi_stage/public/robots.txt +5 -0
  52. data/spec/fixtures/rails_apps/multi_stage/script/about +4 -0
  53. data/spec/fixtures/rails_apps/multi_stage/script/console +3 -0
  54. data/spec/fixtures/rails_apps/multi_stage/script/dbconsole +3 -0
  55. data/spec/fixtures/rails_apps/multi_stage/script/destroy +3 -0
  56. data/spec/fixtures/rails_apps/multi_stage/script/generate +3 -0
  57. data/spec/fixtures/rails_apps/multi_stage/script/performance/benchmarker +3 -0
  58. data/spec/fixtures/rails_apps/multi_stage/script/performance/profiler +3 -0
  59. data/spec/fixtures/rails_apps/multi_stage/script/plugin +3 -0
  60. data/spec/fixtures/rails_apps/multi_stage/script/runner +3 -0
  61. data/spec/fixtures/rails_apps/multi_stage/script/server +3 -0
  62. data/spec/fixtures/rails_apps/multi_stage/test/performance/browsing_test.rb +9 -0
  63. data/spec/fixtures/rails_apps/multi_stage/test/test_helper.rb +38 -0
  64. data/spec/fixtures/rails_apps/no_cap/README +243 -0
  65. data/spec/fixtures/rails_apps/no_cap/Rakefile +10 -0
  66. data/spec/fixtures/rails_apps/no_cap/app/controllers/application_controller.rb +10 -0
  67. data/spec/fixtures/rails_apps/no_cap/app/helpers/application_helper.rb +3 -0
  68. data/spec/fixtures/rails_apps/no_cap/config/boot.rb +110 -0
  69. data/spec/fixtures/rails_apps/no_cap/config/database.yml +22 -0
  70. data/spec/fixtures/rails_apps/no_cap/config/environment.rb +41 -0
  71. data/spec/fixtures/rails_apps/no_cap/config/environments/development.rb +17 -0
  72. data/spec/fixtures/rails_apps/no_cap/config/environments/production.rb +28 -0
  73. data/spec/fixtures/rails_apps/no_cap/config/environments/test.rb +28 -0
  74. data/spec/fixtures/rails_apps/no_cap/config/initializers/backtrace_silencers.rb +7 -0
  75. data/spec/fixtures/rails_apps/no_cap/config/initializers/inflections.rb +10 -0
  76. data/spec/fixtures/rails_apps/no_cap/config/initializers/mime_types.rb +5 -0
  77. data/spec/fixtures/rails_apps/no_cap/config/initializers/new_rails_defaults.rb +21 -0
  78. data/spec/fixtures/rails_apps/no_cap/config/initializers/session_store.rb +15 -0
  79. data/spec/fixtures/rails_apps/no_cap/config/locales/en.yml +5 -0
  80. data/spec/fixtures/rails_apps/no_cap/config/routes.rb +43 -0
  81. data/spec/fixtures/rails_apps/no_cap/db/seeds.rb +7 -0
  82. data/spec/fixtures/rails_apps/no_cap/doc/README_FOR_APP +2 -0
  83. data/spec/fixtures/rails_apps/no_cap/log/development.log +0 -0
  84. data/spec/fixtures/rails_apps/no_cap/log/production.log +0 -0
  85. data/spec/fixtures/rails_apps/no_cap/log/server.log +0 -0
  86. data/spec/fixtures/rails_apps/no_cap/log/test.log +0 -0
  87. data/spec/fixtures/rails_apps/no_cap/public/404.html +30 -0
  88. data/spec/fixtures/rails_apps/no_cap/public/422.html +30 -0
  89. data/spec/fixtures/rails_apps/no_cap/public/500.html +30 -0
  90. data/spec/fixtures/rails_apps/no_cap/public/favicon.ico +0 -0
  91. data/spec/fixtures/rails_apps/no_cap/public/images/rails.png +0 -0
  92. data/spec/fixtures/rails_apps/no_cap/public/index.html +275 -0
  93. data/spec/fixtures/rails_apps/no_cap/public/javascripts/application.js +2 -0
  94. data/spec/fixtures/rails_apps/no_cap/public/javascripts/controls.js +963 -0
  95. data/spec/fixtures/rails_apps/no_cap/public/javascripts/dragdrop.js +973 -0
  96. data/spec/fixtures/rails_apps/no_cap/public/javascripts/effects.js +1128 -0
  97. data/spec/fixtures/rails_apps/no_cap/public/javascripts/prototype.js +4320 -0
  98. data/spec/fixtures/rails_apps/no_cap/public/robots.txt +5 -0
  99. data/spec/fixtures/rails_apps/no_cap/script/about +4 -0
  100. data/spec/fixtures/rails_apps/no_cap/script/console +3 -0
  101. data/spec/fixtures/rails_apps/no_cap/script/dbconsole +3 -0
  102. data/spec/fixtures/rails_apps/no_cap/script/destroy +3 -0
  103. data/spec/fixtures/rails_apps/no_cap/script/generate +3 -0
  104. data/spec/fixtures/rails_apps/no_cap/script/performance/benchmarker +3 -0
  105. data/spec/fixtures/rails_apps/no_cap/script/performance/profiler +3 -0
  106. data/spec/fixtures/rails_apps/no_cap/script/plugin +3 -0
  107. data/spec/fixtures/rails_apps/no_cap/script/runner +3 -0
  108. data/spec/fixtures/rails_apps/no_cap/script/server +3 -0
  109. data/spec/fixtures/rails_apps/no_cap/test/performance/browsing_test.rb +9 -0
  110. data/spec/fixtures/rails_apps/no_cap/test/test_helper.rb +38 -0
  111. data/spec/fixtures/rails_apps/single_stage/Capfile +4 -0
  112. data/spec/fixtures/rails_apps/single_stage/README +243 -0
  113. data/spec/fixtures/rails_apps/single_stage/Rakefile +10 -0
  114. data/spec/fixtures/rails_apps/single_stage/app/controllers/application_controller.rb +10 -0
  115. data/spec/fixtures/rails_apps/single_stage/app/helpers/application_helper.rb +3 -0
  116. data/spec/fixtures/rails_apps/single_stage/config/boot.rb +110 -0
  117. data/spec/fixtures/rails_apps/single_stage/config/captivate.yml +2 -0
  118. data/spec/fixtures/rails_apps/single_stage/config/database.yml +22 -0
  119. data/spec/fixtures/rails_apps/single_stage/config/deploy.rb +24 -0
  120. data/spec/fixtures/rails_apps/single_stage/config/environment.rb +41 -0
  121. data/spec/fixtures/rails_apps/single_stage/config/environments/development.rb +17 -0
  122. data/spec/fixtures/rails_apps/single_stage/config/environments/production.rb +28 -0
  123. data/spec/fixtures/rails_apps/single_stage/config/environments/test.rb +28 -0
  124. data/spec/fixtures/rails_apps/single_stage/config/initializers/backtrace_silencers.rb +7 -0
  125. data/spec/fixtures/rails_apps/single_stage/config/initializers/inflections.rb +10 -0
  126. data/spec/fixtures/rails_apps/single_stage/config/initializers/mime_types.rb +5 -0
  127. data/spec/fixtures/rails_apps/single_stage/config/initializers/new_rails_defaults.rb +21 -0
  128. data/spec/fixtures/rails_apps/single_stage/config/initializers/session_store.rb +15 -0
  129. data/spec/fixtures/rails_apps/single_stage/config/locales/en.yml +5 -0
  130. data/spec/fixtures/rails_apps/single_stage/config/routes.rb +43 -0
  131. data/spec/fixtures/rails_apps/single_stage/db/seeds.rb +7 -0
  132. data/spec/fixtures/rails_apps/single_stage/doc/README_FOR_APP +2 -0
  133. data/spec/fixtures/rails_apps/single_stage/log/development.log +0 -0
  134. data/spec/fixtures/rails_apps/single_stage/log/production.log +0 -0
  135. data/spec/fixtures/rails_apps/single_stage/log/server.log +0 -0
  136. data/spec/fixtures/rails_apps/single_stage/log/test.log +0 -0
  137. data/spec/fixtures/rails_apps/single_stage/public/404.html +30 -0
  138. data/spec/fixtures/rails_apps/single_stage/public/422.html +30 -0
  139. data/spec/fixtures/rails_apps/single_stage/public/500.html +30 -0
  140. data/spec/fixtures/rails_apps/single_stage/public/favicon.ico +0 -0
  141. data/spec/fixtures/rails_apps/single_stage/public/images/rails.png +0 -0
  142. data/spec/fixtures/rails_apps/single_stage/public/index.html +275 -0
  143. data/spec/fixtures/rails_apps/single_stage/public/javascripts/application.js +2 -0
  144. data/spec/fixtures/rails_apps/single_stage/public/javascripts/controls.js +963 -0
  145. data/spec/fixtures/rails_apps/single_stage/public/javascripts/dragdrop.js +973 -0
  146. data/spec/fixtures/rails_apps/single_stage/public/javascripts/effects.js +1128 -0
  147. data/spec/fixtures/rails_apps/single_stage/public/javascripts/prototype.js +4320 -0
  148. data/spec/fixtures/rails_apps/single_stage/public/robots.txt +5 -0
  149. data/spec/fixtures/rails_apps/single_stage/script/about +4 -0
  150. data/spec/fixtures/rails_apps/single_stage/script/console +3 -0
  151. data/spec/fixtures/rails_apps/single_stage/script/dbconsole +3 -0
  152. data/spec/fixtures/rails_apps/single_stage/script/destroy +3 -0
  153. data/spec/fixtures/rails_apps/single_stage/script/generate +3 -0
  154. data/spec/fixtures/rails_apps/single_stage/script/performance/benchmarker +3 -0
  155. data/spec/fixtures/rails_apps/single_stage/script/performance/profiler +3 -0
  156. data/spec/fixtures/rails_apps/single_stage/script/plugin +3 -0
  157. data/spec/fixtures/rails_apps/single_stage/script/runner +3 -0
  158. data/spec/fixtures/rails_apps/single_stage/script/server +3 -0
  159. data/spec/fixtures/rails_apps/single_stage/test/performance/browsing_test.rb +9 -0
  160. data/spec/fixtures/rails_apps/single_stage/test/test_helper.rb +38 -0
  161. data/spec/spec.opts +1 -0
  162. data/spec/spec_helper.rb +8 -0
  163. metadata +294 -0
@@ -0,0 +1,199 @@
1
+ require 'tempfile'
2
+ module Captivate
3
+ class Command
4
+ attr_reader :path
5
+
6
+ def initialize(args = nil)
7
+ @args = args
8
+ end
9
+
10
+ def invoke
11
+ config = Config.new(config_options).to_hash
12
+ @host = config['host']
13
+ @path = config['working_path']
14
+ @remote_dir = RemoteDir.new(:host => @host, :path => @path)
15
+ @remote_dir.upload('Capfile')
16
+ @remote_config_dir = @remote_dir.new_subdir('config')
17
+ @remote_config_dir.upload('config/deploy.rb')
18
+ @remote_config_dir.upload('config/deploy') if File.directory?('config/deploy')
19
+ @remote_script = RemoteScript.new(:command => command, :remote_path => @path)
20
+ remote_script_path = @remote_script.local_path
21
+ File.chmod(0555, remote_script_path)
22
+ @remote_dir.upload(remote_script_path, 'captivate-remote.sh')
23
+ SystemCall.new(%{ssh -t #{@host} screen #{@path}/captivate-remote.sh})
24
+ end
25
+
26
+ private
27
+
28
+ def self.remote_script_path
29
+ File.join(File.dirname(__FILE__), '/../bin/captivate-remote.sh')
30
+ end
31
+
32
+ def command
33
+ @args * ' '
34
+ end
35
+
36
+ def config_options
37
+ {:command => command, :rails_root => rails_root}
38
+ end
39
+
40
+ def rails_root
41
+ FileUtils.pwd
42
+ end
43
+ end
44
+
45
+ class Config
46
+ def initialize(options = {})
47
+ @command = options[:command] or raise(ConfigError, "Missing 'command' option")
48
+ @rails_root = options[:rails_root] or raise(ConfigError, "Missing 'rails_root' option")
49
+ end
50
+
51
+ def to_hash
52
+ validate_yaml_config
53
+ { 'working_path' => working_path }.merge(yaml_config[environment])
54
+ end
55
+
56
+ private
57
+
58
+ def working_path
59
+ '/tmp/captivate.%s' % [timestamp, app_name, environment, user].join('.')
60
+ end
61
+
62
+ def timestamp
63
+ Time.now.to_i
64
+ end
65
+
66
+ def app_name
67
+ File.basename(@rails_root)
68
+ end
69
+
70
+ def environment
71
+ if config_stages.include?(commands.first) && multistage_stages.include?(commands.first)
72
+ commands.first
73
+ else
74
+ 'production'
75
+ end
76
+ end
77
+
78
+ def config_stages
79
+ yaml_config.keys
80
+ end
81
+
82
+ def multistage_stages
83
+ if multistage?
84
+ set_stages_code = deploy_file_contents[/set :stages,\s?([^\n]+)\n/, 1] or
85
+ raise(ConfigError, "'set :stages' definition not found")
86
+ stages_array = eval(set_stages_code)
87
+ else
88
+ []
89
+ end
90
+ end
91
+
92
+ def deploy_file_contents
93
+ File.read(deploy_file_path)
94
+ end
95
+
96
+ def deploy_file_path
97
+ "#{@rails_root}/config/deploy.rb"
98
+ end
99
+
100
+ def multistage?
101
+ deploy_file_contents.match(%r(\A\s*require (['"])capistrano/ext/multistage\1)) ||
102
+ deploy_file_contents.match(%r(\n\s*require (['"])capistrano/ext/multistage\1))
103
+ end
104
+
105
+ def commands
106
+ @command.split(/\s+/)
107
+ end
108
+
109
+ def user
110
+ Etc.getlogin
111
+ end
112
+
113
+ def validate_yaml_config
114
+ raise(ConfigError, yaml_config_path) unless File.exists?(yaml_config_path)
115
+ raise(ConfigError, "Missing #{environment} environment") unless yaml_config.keys.include?(environment)
116
+ unless yaml_config[environment].keys.include?('host')
117
+ raise(ConfigError, "Missing 'host' for #{environment} environment")
118
+ end
119
+ end
120
+
121
+ def yaml_config
122
+ @yaml_config ||= YAML.load_file(yaml_config_path)
123
+ end
124
+
125
+ def yaml_config_path
126
+ File.join(@rails_root, 'config/captivate.yml')
127
+ end
128
+ end
129
+
130
+ class RemoteDir
131
+ def initialize(options)
132
+ @host, @path = options[:host], options[:path]
133
+ SystemCall.new("ssh #{@host} mkdir #{@path}")
134
+ end
135
+
136
+ def upload(file_path, dest_filename = file_path)
137
+ raise(Errno::ENOENT, file_path) unless File.exists?(file_path)
138
+ if File.directory?(file_path)
139
+ SystemCall.new("scp -r #{file_path} #{@host}:#{@path}/#{File.basename(dest_filename)}")
140
+ else
141
+ SystemCall.new("scp #{file_path} #{@host}:#{@path}/#{File.basename(dest_filename)}")
142
+ end
143
+ end
144
+
145
+ def new_subdir(dirname)
146
+ self.class.new(:host => @host, :path => "#{@path}/#{dirname}")
147
+ end
148
+ end
149
+
150
+ class RemoteScript
151
+ attr_reader :local_path, :remote_path, :command
152
+ def initialize(options)
153
+ @command = options[:command]
154
+ @remote_path = options[:remote_path]
155
+ Tempfile.open('captivate_remote_script') do |f|
156
+ @local_path = f.path
157
+ f.write(converted_template)
158
+ end
159
+ end
160
+
161
+ private
162
+
163
+ def converted_template
164
+ template.gsub('### CAP COMMANDS ###', command).gsub('### REMOTE PATH ###', remote_path)
165
+ end
166
+
167
+ def template
168
+ <<-EOS
169
+ #!/usr/bin/env bash
170
+ # Deploys app. Executed remotely from deploy host.
171
+ if [ -f ~/.bashrc ]; then
172
+ . ~/.bashrc
173
+ fi
174
+ if [ -f ~/.profile ]; then
175
+ . ~/.profile
176
+ fi
177
+ if [ -f ~/.bash_profile ]; then
178
+ . ~/.bash_profile
179
+ fi
180
+ trap 'echo "--> Aborted. Press Enter to continue..."; read; exit' ERR
181
+ cd ### REMOTE PATH ###
182
+ echo "--> Executing: "
183
+ echo "cap ### CAP COMMANDS ###"
184
+ cap ### CAP COMMANDS ###
185
+ echo "--> Completed. Press Enter to continue..."
186
+ read
187
+ EOS
188
+ end
189
+ end
190
+
191
+ class SystemCall
192
+ def initialize(command)
193
+ system(command)
194
+ end
195
+ end
196
+
197
+ class ConfigError < StandardError
198
+ end
199
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe Captivate do
4
+ FIXTURES_PATH = File.expand_path(File.join(File.dirname(__FILE__), '/fixtures'))
5
+
6
+ before do
7
+ # FIXME: FileUtils.rm_rf isn't deleting.
8
+ # FileUtils.rm_rf('/tmp/captivate*')
9
+ system('rm -fr /tmp/captivate*')
10
+ @file_to_appear = '/tmp/captivate-test-file'
11
+ @touched_file_exists = lambda { File.exists?(@file_to_appear) }
12
+ @command = Captivate::Command.new([%{ROLES=app COMMAND='touch #{@file_to_appear}' invoke}])
13
+ end
14
+
15
+ context 'when non-multistage rails app' do
16
+ before do
17
+ @rails_root = File.join(FIXTURES_PATH, 'rails_apps/single_stage')
18
+ end
19
+
20
+ it "should execute cap task and write log file" do
21
+ lambda do
22
+ FileUtils.cd(@rails_root)
23
+ @command.invoke
24
+ end.should change(@touched_file_exists, :call).from(false).to(true)
25
+ end
26
+
27
+ it "should write log file" do
28
+ pending
29
+ FileUtils.cd(@rails_root)
30
+ @command.invoke
31
+ log_file_path = File.join(@command.path, 'captivate.log')
32
+ File.exists?(log_file_path).should be_true
33
+ end
34
+ end
35
+
36
+ context 'when multistage rails app' do
37
+ before do
38
+ @rails_root = File.join(FIXTURES_PATH, 'rails_apps/multi_stage')
39
+ end
40
+
41
+ it "should fail when attempting cap task to nonexistent server" do
42
+ lambda do
43
+ FileUtils.cd(@rails_root)
44
+ Captivate::Command.new([%{ROLES=nonexistent COMMAND='touch #{@file_to_appear}' invoke}]).invoke
45
+ end.should_not raise_error
46
+ end
47
+
48
+ it "should execute cap task via screen" do
49
+ lambda do
50
+ FileUtils.cd(@rails_root)
51
+ Captivate::Command.new([%{staging ROLES=app COMMAND='touch #{@file_to_appear}' invoke}]).invoke
52
+ end.should change(@touched_file_exists, :call).from(false).to(true)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,269 @@
1
+ require 'spec_helper'
2
+
3
+ describe Captivate::Command do
4
+ before do
5
+ Captivate::Config.stub!(:new).and_return('host' => 'the_host', 'working_path' => 'the_path')
6
+ @remote_dir = mock('remote_dir')
7
+ @remote_dir.stub!(:upload)
8
+ @remote_subdir = mock('subdir')
9
+ @remote_subdir.stub!(:upload)
10
+ @remote_dir.stub!(:new_subdir).and_return(@remote_subdir)
11
+ Captivate::RemoteDir.stub!(:new).and_return(@remote_dir)
12
+ Captivate::SystemCall.stub!(:new)
13
+ @remote_script = mock('remote_script')
14
+ @remote_script.stub!(:local_path)
15
+ Captivate::RemoteScript.stub!(:new).and_return(@remote_script)
16
+ @command = Captivate::Command.new(['some cap commands'])
17
+ File.stub!(:chmod)
18
+ end
19
+
20
+ it "should pass rails_root and command to config" do
21
+ FileUtils.stub!(:pwd).and_return('path_to_my_app')
22
+ Captivate::Config.should_receive(:new).with(:command => 'some cap commands', :rails_root => 'path_to_my_app')
23
+ @command.invoke
24
+ end
25
+
26
+ it "should create a remote working directory" do
27
+ Captivate::Config.should_receive(:new).and_return('host' => 'the_host', 'working_path' => 'the_path')
28
+ Captivate::RemoteDir.should_receive(:new).with(:host => 'the_host', :path => 'the_path').and_return(@remote_dir)
29
+ @command.invoke
30
+ end
31
+
32
+ it "should upload Capfile" do
33
+ @remote_dir.should_receive(:upload).with('Capfile')
34
+ @command.invoke
35
+ end
36
+
37
+ it "should create a config subdirectory" do
38
+ @remote_dir.should_receive(:new_subdir).with('config')
39
+ @command.invoke
40
+ end
41
+
42
+ it "should upload deploy.rb to config" do
43
+ @remote_subdir.should_receive(:upload).with('config/deploy.rb')
44
+ @command.invoke
45
+ end
46
+
47
+ it "shouldn't upload config/deploy if doesn't exist" do
48
+ File.should_receive(:directory?).with('config/deploy').and_return(false)
49
+ @remote_subdir.should_not_receive(:upload).with('config/deploy')
50
+ @command.invoke
51
+ end
52
+
53
+ it "should upload config/deploy if exists" do
54
+ File.should_receive(:directory?).with('config/deploy').and_return(true)
55
+ @remote_subdir.should_receive(:upload).with('config/deploy')
56
+ @command.invoke
57
+ end
58
+
59
+ it "should write remote script locally" do
60
+ Captivate::RemoteScript.should_receive(:new).with(:command => 'some cap commands', :remote_path => 'the_path')
61
+ @command.invoke
62
+ end
63
+
64
+ it "should upload remote script" do
65
+ @remote_script.should_receive(:local_path).and_return('path_to_local_script')
66
+ @remote_dir.should_receive(:upload).with('path_to_local_script', 'captivate-remote.sh')
67
+ @command.invoke
68
+ end
69
+
70
+ it "should invoke remote script" do
71
+ Captivate::SystemCall.should_receive(:new).with(%{ssh -t the_host screen the_path/captivate-remote.sh})
72
+ @command.invoke
73
+ end
74
+
75
+ it "should make remote script executable" do
76
+ @remote_script.should_receive(:local_path).and_return('some_path')
77
+ File.should_receive(:chmod).with(0555, 'some_path')
78
+ @command.invoke
79
+ end
80
+ end
81
+
82
+ describe Captivate::Config do
83
+ before do
84
+ @rails_root = 'path/to/my_app'
85
+ @config = Captivate::Config.new(:command => 'some cap commands', :rails_root => @rails_root)
86
+ @yaml_config_path = 'path/to/my_app/config/captivate.yml'
87
+ end
88
+
89
+ it "should raise an error when 'command' not passed in" do
90
+ lambda do
91
+ Captivate::Config.new(:rails_root => 'the_path').invoke.should
92
+ end.should raise_error(Captivate::ConfigError, "Missing 'command' option")
93
+ end
94
+
95
+ it "should raise an error when 'rails_root' not passed in" do
96
+ lambda do
97
+ Captivate::Config.new(:command => 'the_command').invoke.should
98
+ end.should raise_error(Captivate::ConfigError, "Missing 'rails_root' option")
99
+ end
100
+
101
+ context 'without YAML' do
102
+ it "should raise error" do
103
+ lambda { @config.to_hash }.should raise_error(Captivate::ConfigError, @yaml_config_path)
104
+ end
105
+ end
106
+
107
+ context 'with YAML' do
108
+ before do
109
+ File.should_receive(:exists?).with(@yaml_config_path).and_return(true)
110
+ @multistage_config = {
111
+ 'production' => {'host' => 'the_host'},
112
+ 'staging' => {'host' => 'another_host'},
113
+ }
114
+ YAML.stub!(:load_file).with(@yaml_config_path).and_return(@multistage_config)
115
+ end
116
+
117
+ it "should return a hash" do
118
+ @config.to_hash.should be_a(Hash)
119
+ end
120
+
121
+ it "should retrieve host from YAML config, defaulting to production" do
122
+ @config.to_hash['host'].should == 'the_host'
123
+ end
124
+
125
+ it "should not accept non-production stages if multistage gem not present" do
126
+ @config = Captivate::Config.new(:command => 'staging some cap commands', :rails_root => @rails_root)
127
+ @config.stub!(:deploy_file_contents).and_return('deploy_config')
128
+ @config.to_hash['host'].should == 'the_host'
129
+ end
130
+
131
+ it "should raise error if multistage gem present but stages not found" do
132
+ @config = Captivate::Config.new(:command => 'staging some cap commands', :rails_root => @rails_root)
133
+ @config.stub!(:deploy_file_contents).and_return(<<-EOS)
134
+ require 'capistrano/ext/multistage'
135
+ some cap commands
136
+ EOS
137
+ lambda { @config.to_hash }.should raise_error(Captivate::ConfigError, "'set :stages' definition not found")
138
+ end
139
+
140
+ it "should detect valid stage as first parameter if valid stage and multistage gem present" do
141
+ @config = Captivate::Config.new(:command => 'staging some cap commands', :rails_root => @rails_root)
142
+ @config.stub!(:deploy_file_contents).and_return(<<-EOS)
143
+ require 'capistrano/ext/multistage'
144
+ set :stages, %w(production staging showcase)
145
+ some cap commands
146
+ EOS
147
+ @config.to_hash['host'].should == 'another_host'
148
+ end
149
+
150
+ it "should detect valid stage as first parameter if valid stage and multistage gem present but commented out" do
151
+ @config = Captivate::Config.new(:command => 'staging some cap commands', :rails_root => @rails_root)
152
+ @config.stub!(:deploy_file_contents).and_return(<<-EOS)
153
+ # require 'capistrano/ext/multistage'
154
+ some cap commands
155
+ EOS
156
+ @config.to_hash['host'].should == 'the_host'
157
+ end
158
+
159
+ # Not defined in YAML config (@multistage_config['showcase'])
160
+ it "should ignore invalid stage as first parameter" do
161
+ @config = Captivate::Config.new(:command => 'showcase some cap commands', :rails_root => @rails_root)
162
+ @config.to_hash['host'].should == 'the_host'
163
+ end
164
+
165
+ it "should contain timestamp, app_name, environment, user in remote working directory name" do
166
+ Etc.stub!(:getlogin).and_return('the_user')
167
+ @config.to_hash['working_path'].should == "/tmp/captivate.#{Time.now.to_i}.my_app.production.the_user"
168
+ end
169
+
170
+ it "should merge YAML config" do
171
+ @config.stub!(:yaml_config).and_return('production' => {'host' => 'the_host', 'working_path' => 'custom_path'})
172
+ @config.to_hash['working_path'].should == 'custom_path'
173
+ end
174
+
175
+ it "should raise an error if missing config for environment" do
176
+ YAML.stub!(:load_file).with(@yaml_config_path).and_return({})
177
+ lambda do
178
+ @config.to_hash
179
+ end.should raise_error(Captivate::ConfigError, 'Missing production environment')
180
+ end
181
+
182
+ it "should raise an error if host missing from config" do
183
+ YAML.stub!(:load_file).with(@yaml_config_path).and_return('production' => {})
184
+ lambda do
185
+ @config.to_hash
186
+ end.should raise_error(Captivate::ConfigError, "Missing 'host' for production environment")
187
+ end
188
+ end
189
+ end
190
+
191
+ describe Captivate::RemoteDir do
192
+ before do
193
+ Captivate::SystemCall.stub!(:new)
194
+ end
195
+
196
+ it "should create a directory" do
197
+ Captivate::SystemCall.should_receive(:new).with('ssh the_host mkdir the_path')
198
+ Captivate::RemoteDir.new(:host => 'the_host', :path => 'the_path')
199
+ end
200
+
201
+ it "should raise an error if missing file" do
202
+ @remote_dir = Captivate::RemoteDir.new(:host => 'the_host', :path => 'the_path')
203
+ lambda do
204
+ @remote_dir.upload('local_path/a_file')
205
+ end.should raise_error(Errno::ENOENT, 'No such file or directory - local_path/a_file')
206
+ end
207
+
208
+ it "should create a subdirectory" do
209
+ @remote_dir = Captivate::RemoteDir.new(:host => 'the_host', :path => 'the_path')
210
+ Captivate::RemoteDir.should_receive(:new).with(:host => 'the_host', :path => 'the_path/a_dir')
211
+ @remote_dir.new_subdir('a_dir')
212
+ end
213
+
214
+ context 'when uploading file which exists' do
215
+ before do
216
+ File.stub!(:exists?).and_return(true)
217
+ end
218
+
219
+ it "should upload a file" do
220
+ @remote_dir = Captivate::RemoteDir.new(:host => 'the_host', :path => 'the_path')
221
+ Captivate::SystemCall.should_receive(:new).with('scp local_path/a_file the_host:the_path/a_file')
222
+ @remote_dir.upload('local_path/a_file')
223
+ end
224
+
225
+ it "should upload a file and rename" do
226
+ @remote_dir = Captivate::RemoteDir.new(:host => 'the_host', :path => 'the_path')
227
+ Captivate::SystemCall.should_receive(:new).with('scp a_file the_host:the_path/new_name')
228
+ @remote_dir.upload('a_file', 'new_name')
229
+ end
230
+
231
+ it "should upload a directory" do
232
+ @remote_dir = Captivate::RemoteDir.new(:host => 'the_host', :path => 'the_path')
233
+ Captivate::SystemCall.should_receive(:new).with('scp -r local_path/a_dir the_host:the_path/a_dir')
234
+ File.should_receive(:directory?).with('local_path/a_dir').and_return(true)
235
+ @remote_dir.upload('local_path/a_dir')
236
+ end
237
+ end
238
+ end
239
+
240
+ describe Captivate::RemoteScript do
241
+ before do
242
+ @file = mock('file')
243
+ @file.stub!(:path)
244
+ @file.stub!(:write)
245
+ Tempfile.should_receive(:open).and_yield(@file)
246
+ @remote_script = lambda do
247
+ Captivate::RemoteScript.new(:command => 'some cap commands', :remote_path => 'the_remote_path')
248
+ end
249
+ end
250
+
251
+ it "should contain cap command in script" do
252
+ @file.should_receive(:write) do |shell_script|
253
+ shell_script.should include('some cap commands')
254
+ end
255
+ @remote_script.call
256
+ end
257
+
258
+ it "should contain remote script path in script" do
259
+ @file.should_receive(:write) do |shell_script|
260
+ shell_script.should include('the_remote_path')
261
+ end
262
+ @remote_script.call
263
+ end
264
+
265
+ it "should return path" do
266
+ @file.should_receive(:path).and_return('some_path')
267
+ @remote_script.call
268
+ end
269
+ end