sunshine 1.2.0 → 1.2.1

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.
@@ -7,13 +7,23 @@ module Sunshine
7
7
 
8
8
  include Open4
9
9
 
10
- class TimeoutError < CriticalDeployError; end
11
-
12
10
  LOCAL_USER = `whoami`.chomp
13
11
  LOCAL_HOST = `hostname`.chomp
14
12
 
15
- SUDO_FAILED = /^Sorry, try again./
16
- SUDO_PROMPT = /^Password:/
13
+ class << self
14
+ # The message to match in stderr to determine logging in has failed.
15
+ # Defaults to:
16
+ # /^Sorry, try again./
17
+ attr_accessor :sudo_failed_matcher
18
+
19
+ # The message to match in stderr to determine a password is required.
20
+ # Defaults to:
21
+ # /^Password:/
22
+ attr_accessor :sudo_prompt_matcher
23
+ end
24
+
25
+ self.sudo_failed_matcher = /^Sorry, try again./
26
+ self.sudo_prompt_matcher = /^Password:/
17
27
 
18
28
  attr_reader :user, :host, :password, :input, :output, :mutex
19
29
  attr_accessor :env, :sudo, :timeout
@@ -33,8 +43,6 @@ module Sunshine
33
43
 
34
44
  @timeout = options[:timeout] || Sunshine.timeout
35
45
 
36
- @cmd_activity = nil
37
-
38
46
  @mutex = nil
39
47
  end
40
48
 
@@ -73,6 +81,14 @@ module Sunshine
73
81
  end
74
82
 
75
83
 
84
+ ##
85
+ # Prompt the user to make a choice.
86
+
87
+ def choose &block
88
+ sync{ @input.choose(&block) }
89
+ end
90
+
91
+
76
92
  ##
77
93
  # Close the output IO. (Required by the Logger class)
78
94
 
@@ -133,6 +149,21 @@ module Sunshine
133
149
  end
134
150
 
135
151
 
152
+ ##
153
+ # Start an interactive shell with preset permissions and env.
154
+ # Optionally pass a command to be run first.
155
+
156
+ def tty! cmd=nil
157
+ sync do
158
+ cmd = [cmd, "sh -il"].compact.join " && "
159
+ pid = fork do
160
+ exec sudo_cmd(env_cmd(cmd)).to_a.join(" ")
161
+ end
162
+ Process.waitpid pid
163
+ end
164
+ end
165
+
166
+
136
167
  ##
137
168
  # Write a file. Compatibility method with RemoteShell.
138
169
 
@@ -237,7 +268,7 @@ module Sunshine
237
268
  ##
238
269
  # Returns true if command was run successfully, otherwise returns false.
239
270
 
240
- def syscall cmd, options=nil
271
+ def system cmd, options=nil
241
272
  call(cmd, options) && true rescue false
242
273
  end
243
274
 
@@ -245,20 +276,12 @@ module Sunshine
245
276
  ##
246
277
  # Checks if timeout occurred.
247
278
 
248
- def timed_out? start_time=@cmd_activity, max_time=@timeout
279
+ def timed_out? start_time, max_time=@timeout
249
280
  return unless max_time
250
281
  Time.now.to_i - start_time.to_i > max_time
251
282
  end
252
283
 
253
284
 
254
- ##
255
- # Update the time of the last command activity
256
-
257
- def update_timeout
258
- @cmd_activity = Time.now
259
- end
260
-
261
-
262
285
  ##
263
286
  # Execute a block while setting the shell's mutex.
264
287
  # Sets the mutex to its original value on exit.
@@ -319,7 +342,9 @@ module Sunshine
319
342
  log_methods = {out => :debug, err => :error}
320
343
 
321
344
  result, status = process_streams(pid, out, err) do |stream, data|
322
- stream_name = stream == out ? :out : :err
345
+ stream_name = :out if stream == out
346
+ stream_name = :err if stream == err
347
+ stream_name = :inn if stream == inn
323
348
 
324
349
 
325
350
  # User blocks should run with sync threads to avoid badness.
@@ -353,18 +378,18 @@ module Sunshine
353
378
  private
354
379
 
355
380
  def raise_command_failed(status, cmd)
356
- raise CmdError,
357
- "Execution failed with status #{status.exitstatus}: #{[*cmd].join ' '}"
381
+ err = CmdError.new status.exitstatus, [*cmd].join(" ")
382
+ raise err
358
383
  end
359
384
 
360
385
 
361
386
  def password_required? stream_name, data
362
- stream_name == :err && data =~ SUDO_PROMPT
387
+ stream_name == :err && data =~ Shell.sudo_prompt_matcher
363
388
  end
364
389
 
365
390
 
366
391
  def send_password_to_stream inn, data
367
- prompt_for_password if data =~ SUDO_FAILED
392
+ prompt_for_password if data =~ Shell.sudo_failed_matcher
368
393
  inn.puts @password || prompt_for_password
369
394
  end
370
395
 
@@ -380,7 +405,7 @@ module Sunshine
380
405
 
381
406
  def process_streams pid, *streams
382
407
  result = Hash.new{|h,k| h[k] = []}
383
- update_timeout
408
+ start_time = Time.now
384
409
 
385
410
  # Handle process termination ourselves
386
411
  status = nil
@@ -392,13 +417,13 @@ module Sunshine
392
417
  # don't busy loop
393
418
  selected, = select streams, nil, nil, 0.1
394
419
 
395
- raise TimeoutError if timed_out?
420
+ raise TimeoutError if timed_out? start_time
396
421
 
397
422
  next if selected.nil? or selected.empty?
398
423
 
399
424
  selected.each do |stream|
400
425
 
401
- update_timeout
426
+ start_time = Time.now
402
427
 
403
428
  if stream.eof? then
404
429
  streams.delete stream if status # we've quit, so no more writing
@@ -0,0 +1,54 @@
1
+ module Sunshine
2
+
3
+ ##
4
+ # The TrapStack class handles setting multiple trap blocks as a stack.
5
+ # Once a trap block is triggered, it gets popped off the stack.
6
+
7
+ class TrapStack
8
+
9
+ ##
10
+ # Adds an INT signal trap with its description on the stack.
11
+ # Returns a trap_item Array.
12
+
13
+ def self.add_trap desc=nil, &block
14
+ @trap_stack.unshift [desc, block]
15
+ @trap_stack.first
16
+ end
17
+
18
+
19
+
20
+ ##
21
+ # Call a trap item and display it's message.
22
+
23
+ def self.call_trap trap_item
24
+ return unless trap_item
25
+
26
+ msg, trap_block = trap_item
27
+
28
+ yield msg if block_given?
29
+
30
+ trap_block.call
31
+ end
32
+
33
+
34
+ ##
35
+ # Remove a trap_item from the stack.
36
+
37
+ def self.delete_trap trap_item
38
+ @trap_stack.delete trap_item
39
+ end
40
+
41
+
42
+ ##
43
+ # Sets the default trap.
44
+
45
+ def self.trap_signal sig, &block
46
+ @trap_stack = []
47
+
48
+ trap sig do
49
+ call_trap @trap_stack.shift, &block
50
+ exit 1
51
+ end
52
+ end
53
+ end
54
+ end
@@ -25,7 +25,31 @@ http {
25
25
  client_body_temp_path <%= darwin ? '/var/tmp/nginx' : '/dev/shm' %>;
26
26
  proxy_temp_path <%= darwin ? '/var/tmp/nginx' : '/dev/shm' %>;
27
27
 
28
+ <%
29
+ mime_types = "#{File.dirname(shell.call("which nginx"))}/../conf/mime.types"
30
+
31
+ if shell.file? mime_types
32
+ -%>
28
33
  include <%= File.dirname shell.call("which nginx") %>/../conf/mime.types;
34
+
35
+ <% else -%>
36
+
37
+ types {
38
+ application/x-plist plist;
39
+ application/json json;
40
+ image/gif gif;
41
+ image/jpeg jpg;
42
+ image/png png;
43
+ image/x-icon ico;
44
+ text/css css;
45
+ text/html html;
46
+ text/plain bob;
47
+ text/plain txt;
48
+ application/vnd.android.package-archive apk;
49
+ }
50
+
51
+ <% end -%>
52
+
29
53
  default_type application/octet-stream;
30
54
 
31
55
  log_format sunshine '$remote_addr - $remote_user [$time_local] '
@@ -123,7 +123,7 @@ fi
123
123
  "--rsync-path='#{ path }' "
124
124
  end
125
125
 
126
- rsync_cmd = "rsync -azP #{rsync_path}-e \"ssh #{ds.ssh_flags.join(' ')}\""
126
+ rsync_cmd = "rsync -azrP #{rsync_path}-e \"ssh #{ds.ssh_flags.join(' ')}\""
127
127
 
128
128
  error_msg = "No such command in remote_shell log [#{ds.host}]\n#{rsync_cmd}"
129
129
  error_msg << "#{from.inspect} #{to.inspect}"
@@ -26,6 +26,7 @@ class TestApp < Test::Unit::TestCase
26
26
  end
27
27
 
28
28
  def teardown
29
+ Sunshine.exclude_paths.clear
29
30
  FileUtils.rm_rf @tmpdir
30
31
  end
31
32
 
@@ -159,8 +160,7 @@ class TestApp < Test::Unit::TestCase
159
160
 
160
161
  def test_app_deploy_error_handling
161
162
  [ MockError,
162
- Sunshine::CriticalDeployError,
163
- Sunshine::FatalDeployError ].each do |error|
163
+ Sunshine::DeployError ].each do |error|
164
164
 
165
165
  begin
166
166
  app = Sunshine::App.deploy @config do |app|
@@ -277,9 +277,51 @@ class TestApp < Test::Unit::TestCase
277
277
  end
278
278
 
279
279
 
280
+ def test_checkout_local_codebase
281
+ tmp_path = File.join Sunshine::TMP_DIR, "#{@app.name}_checkout"
282
+ @app.repo.extend MockObject
283
+ @app.repo.mock :checkout_to, :args => [tmp_path],
284
+ :return => {:test => "scm info"}
285
+
286
+ @app.each do |sa|
287
+ sa.mock :upload_codebase
288
+ end
289
+
290
+ @app.checkout_codebase :copy => true
291
+
292
+ @app.each do |sa|
293
+ assert sa.method_called?(:upload_codebase,
294
+ :args => [tmp_path, {:test => "scm info", :exclude => []}])
295
+ end
296
+ end
297
+
298
+
299
+ def test_checkout_local_codebase_with_exludes
300
+ Sunshine.exclude_paths.concat ["path1/", "path2/"]
301
+
302
+ tmp_path = File.join Sunshine::TMP_DIR, "#{@app.name}_checkout"
303
+ @app.repo.extend MockObject
304
+
305
+ @app.repo.mock :checkout_to, :args => [tmp_path],
306
+ :return => {:test => "scm info"}
307
+
308
+ @app.each do |sa|
309
+ sa.mock :upload_codebase
310
+ end
311
+
312
+ @app.checkout_codebase :copy => true, :exclude => ["path3/", "path4/"]
313
+
314
+ @app.each do |sa|
315
+ assert sa.method_called?(:upload_codebase,
316
+ :args => [tmp_path, {:test => "scm info",
317
+ :exclude => ["path1/", "path2/", "path3/", "path4/"]}])
318
+ end
319
+ end
320
+
321
+
280
322
  def test_deployed?
281
323
  set_mock_response_for @app, 0,
282
- "cat #{@app.scripts_path}/info" => [:out,
324
+ "cat #{@app.root_path}/info" => [:out,
283
325
  "---\n:deploy_name: '#{@app.deploy_name}'"]
284
326
 
285
327
  deployed = @app.deployed?
@@ -437,7 +479,7 @@ class TestApp < Test::Unit::TestCase
437
479
  returned_dirs = %w{old_deploy1 old_deploy2 old_deploy3 main_deploy}
438
480
  old_deploys = returned_dirs[0..-2].map{|d| "#{@app.deploys_path}/#{d}"}
439
481
 
440
- list_cmd = "ls -1 #{@app.deploys_path}"
482
+ list_cmd = "ls -rc1 #{@app.deploys_path}"
441
483
  rm_cmd = "rm -rf #{old_deploys.join(" ")}"
442
484
 
443
485
  set_mock_response_for @app, 0,
@@ -500,7 +542,7 @@ class TestApp < Test::Unit::TestCase
500
542
 
501
543
  @app.threaded_each do |server_app|
502
544
  if server_app.shell.host == err_host
503
- raise Sunshine::CriticalDeployError, server_app.shell.host
545
+ raise Sunshine::DeployError, server_app.shell.host
504
546
  else
505
547
  finished = finished.next
506
548
  end
@@ -508,7 +550,7 @@ class TestApp < Test::Unit::TestCase
508
550
 
509
551
  raise "Didn't raise threaded error when it should have"
510
552
 
511
- rescue Sunshine::CriticalDeployError => e
553
+ rescue Sunshine::DeployError => e
512
554
  host = @app.server_apps.first.shell.host
513
555
 
514
556
  assert_equal host, e.message
@@ -17,9 +17,9 @@ class TestDaemon < Test::Unit::TestCase
17
17
 
18
18
  begin
19
19
  daemon.start_cmd
20
- raise "Should have thrown CriticalDeployError but didn't :("
21
- rescue Sunshine::CriticalDeployError => e
22
- assert_equal "@start_cmd undefined. Can't start daemon", e.message
20
+ raise "Should have thrown DaemonError but didn't :("
21
+ rescue Sunshine::DaemonError => e
22
+ assert_equal "start_cmd undefined for daemon", e.message
23
23
  end
24
24
  end
25
25
 
@@ -87,13 +87,16 @@ class TestNginx < Test::Unit::TestCase
87
87
  ## Helper methods
88
88
 
89
89
  def start_cmd svr
90
- "#{svr.bin} -c #{svr.config_file_path}"
90
+ svr.exit_on_failure "#{svr.bin} -c #{svr.config_file_path}", 10,
91
+ "Could not start #{svr.name} for #{svr.app.name}"
91
92
  end
92
93
 
93
94
 
94
95
  def stop_cmd svr
95
- "test -f #{svr.pid} && kill -#{svr.sigkill} $(cat #{svr.pid}) && "+
96
- "sleep 1 && rm -f #{svr.pid} || "+
97
- "echo 'Could not kill #{svr.name} pid for #{svr.app.name}';"
96
+ cmd = "test -f #{svr.pid} && kill -#{svr.sigkill} $(cat #{svr.pid}) && "+
97
+ "sleep 1 && rm -f #{svr.pid}"
98
+
99
+ svr.exit_on_failure cmd, 11,
100
+ "Could not kill #{svr.name} pid for #{svr.app.name}"
98
101
  end
99
102
  end
@@ -69,7 +69,7 @@ class TestServer < Test::Unit::TestCase
69
69
  assert sa.method_called?(:install_deps, :args => ["rainbows"])
70
70
 
71
71
  assert server.method_called?(:configure_remote_dirs, :args => [sa.shell])
72
- assert server.method_called?(:touch_log_files, :args => [sa.shell])
72
+ assert server.method_called?(:chown_log_files, :args => [sa.shell])
73
73
  assert server.method_called?(:upload_config_files, :args => [sa.shell])
74
74
  end
75
75
 
@@ -121,7 +121,7 @@ class TestServer < Test::Unit::TestCase
121
121
 
122
122
  server.start do |sa|
123
123
  assert_equal @server_app, sa
124
- assert_ssh_call server.start_cmd, sa.shell, :sudo => true
124
+ assert_ssh_call server._start_cmd, sa.shell, :sudo => true
125
125
  end
126
126
 
127
127
  assert server.method_called?(:setup)
@@ -135,7 +135,7 @@ class TestServer < Test::Unit::TestCase
135
135
 
136
136
  server.start do |sa|
137
137
  assert_equal @server_app, sa
138
- assert_ssh_call server.start_cmd, sa.shell, :sudo => true
138
+ assert_ssh_call server._start_cmd, sa.shell, :sudo => true
139
139
  end
140
140
 
141
141
  assert !server.method_called?(:setup)
@@ -147,7 +147,7 @@ class TestServer < Test::Unit::TestCase
147
147
 
148
148
  server.stop do |sa|
149
149
  assert_equal @server_app, sa
150
- assert_ssh_call server.stop_cmd, sa.shell, :sudo => true
150
+ assert_ssh_call server._stop_cmd, sa.shell, :sudo => true
151
151
  end
152
152
  end
153
153
 
@@ -157,7 +157,7 @@ class TestServer < Test::Unit::TestCase
157
157
 
158
158
  server.restart do |sa|
159
159
  assert_equal @server_app, sa
160
- assert_ssh_call server.restart_cmd, sa.shell, :sudo => true
160
+ assert_ssh_call server._restart_cmd, sa.shell, :sudo => true
161
161
  end
162
162
  end
163
163
 
@@ -181,7 +181,7 @@ class TestServer < Test::Unit::TestCase
181
181
 
182
182
  server.restart do |sa|
183
183
  assert_equal @server_app, sa
184
- assert_ssh_call server.restart_cmd, sa.shell, :sudo => true
184
+ assert_ssh_call server._restart_cmd, sa.shell, :sudo => true
185
185
  end
186
186
 
187
187
  assert server.method_called?(:setup)
@@ -15,6 +15,27 @@ class TestServerApp < Test::Unit::TestCase
15
15
  end
16
16
 
17
17
 
18
+ def test_from_info_file
19
+ info_data = @sa.get_deploy_info.to_yaml
20
+ @sa.shell.mock :call, :args => "cat info/path", :return => info_data
21
+
22
+ server_app = Sunshine::ServerApp.from_info_file "info/path", @sa.shell
23
+
24
+ assert_equal @sa.root_path, server_app.root_path
25
+ assert_equal @sa.checkout_path, server_app.checkout_path
26
+ assert_equal @sa.current_path, server_app.current_path
27
+ assert_equal @sa.deploys_path, server_app.deploys_path
28
+ assert_equal @sa.log_path, server_app.log_path
29
+ assert_equal @sa.shared_path, server_app.shared_path
30
+ assert_equal @sa.scripts_path, server_app.scripts_path
31
+ assert_equal @sa.roles, server_app.roles
32
+
33
+ assert_equal @sa.shell.env, server_app.shell_env
34
+ assert_equal @sa.name, server_app.name
35
+ assert_equal @sa.deploy_name, server_app.deploy_name
36
+ end
37
+
38
+
18
39
  def test_init
19
40
  default_info = {:ports => {}}
20
41
  assert_equal default_info, @sa.info
@@ -119,7 +140,7 @@ class TestServerApp < Test::Unit::TestCase
119
140
  deploy_details = {:item => "thing"}
120
141
  other_details = {:key => "value"}
121
142
 
122
- @sa.shell.mock :call, :args => ["cat #{@sa.scripts_path}/info"],
143
+ @sa.shell.mock :call, :args => ["cat #{@sa.root_path}/info"],
123
144
  :return => other_details.to_yaml
124
145
 
125
146
  @sa.instance_variable_set "@deploy_details", deploy_details
@@ -158,8 +179,11 @@ class TestServerApp < Test::Unit::TestCase
158
179
  :deployed_as => @sa.shell.call("whoami"),
159
180
  :deployed_by => Sunshine.shell.user,
160
181
  :deploy_name => File.basename(@app.checkout_path),
182
+ :name => @sa.name,
183
+ :env => @sa.shell_env,
161
184
  :roles => @sa.roles,
162
- :path => @app.root_path
185
+ :path => @app.root_path,
186
+ :sunshine_version => Sunshine::VERSION
163
187
  }.merge @sa.info
164
188
 
165
189
  deploy_info = @sa.get_deploy_info
@@ -204,7 +228,7 @@ class TestServerApp < Test::Unit::TestCase
204
228
  @sa.install_deps nginx_dep, :type => Sunshine::Gem
205
229
  raise "Didn't raise missing dependency when it should have."
206
230
 
207
- rescue Sunshine::DependencyLib::MissingDependency => e
231
+ rescue Sunshine::MissingDependency => e
208
232
  assert_equal "No dependency 'nginx' [Sunshine::Gem]", e.message
209
233
  end
210
234
 
@@ -257,7 +281,7 @@ class TestServerApp < Test::Unit::TestCase
257
281
  deploys = %w{ploy1 ploy2 ploy3 ploy4 ploy5}
258
282
 
259
283
  @sa.shell.mock :call,
260
- :args => ["ls -1 #{@app.deploys_path}"],
284
+ :args => ["ls -rc1 #{@app.deploys_path}"],
261
285
  :return => "#{deploys.join("\n")}\n"
262
286
 
263
287
  removed = deploys[0..1].map{|d| "#{@app.deploys_path}/#{d}"}
@@ -330,6 +354,32 @@ class TestServerApp < Test::Unit::TestCase
330
354
  end
331
355
 
332
356
 
357
+ def test_running?
358
+ set_mock_response_for @sa, 0,
359
+ "#{@sa.root_path}/status" => [:out, "THE SYSTEM OK!"]
360
+
361
+ assert_equal true, @sa.running?
362
+ end
363
+
364
+
365
+ def test_not_running?
366
+ set_mock_response_for @sa, 13,
367
+ "#{@sa.root_path}/status" => [:err, "THE SYSTEM IS DOWN!"]
368
+
369
+ assert_equal false, @sa.running?
370
+ end
371
+
372
+
373
+ def test_errored_running?
374
+ set_mock_response_for @sa, 1,
375
+ "#{@sa.root_path}/status" => [:err, "KABLAM!"]
376
+
377
+ assert_raises Sunshine::CmdError do
378
+ @sa.running?
379
+ end
380
+ end
381
+
382
+
333
383
  def test_sass
334
384
  sass_files = %w{file1 file2 file3}
335
385
 
@@ -426,6 +476,50 @@ class TestServerApp < Test::Unit::TestCase
426
476
  end
427
477
 
428
478
 
479
+ def test_upload_codebase
480
+ @sa.shell.mock(:upload)
481
+
482
+ @sa.upload_codebase "tmp/thing/", :test_scm => "info"
483
+ assert_equal({:test_scm => "info"} , @sa.info[:scm])
484
+ assert @sa.shell.method_called?(
485
+ :upload, :args => ["tmp/thing/", @sa.checkout_path,
486
+ {:flags => ["--exclude .svn/","--exclude .git/"]}])
487
+ end
488
+
489
+
490
+ def test_upload_codebase_with_exclude
491
+ @sa.shell.mock(:upload)
492
+
493
+ @sa.upload_codebase "tmp/thing/",
494
+ :test_scm => "info",
495
+ :exclude => "bad/path/"
496
+
497
+ assert_equal({:test_scm => "info"} , @sa.info[:scm])
498
+ assert @sa.shell.method_called?(
499
+ :upload, :args => ["tmp/thing/", @sa.checkout_path,
500
+ {:flags => ["--exclude bad/path/",
501
+ "--exclude .svn/",
502
+ "--exclude .git/"]}])
503
+ end
504
+
505
+
506
+ def test_upload_codebase_with_excludes
507
+ @sa.shell.mock(:upload)
508
+
509
+ @sa.upload_codebase "tmp/thing/",
510
+ :test_scm => "info",
511
+ :exclude => ["bad/path/", "other/exclude/"]
512
+
513
+ assert_equal({:test_scm => "info"} , @sa.info[:scm])
514
+ assert @sa.shell.method_called?(
515
+ :upload, :args => ["tmp/thing/", @sa.checkout_path,
516
+ {:flags => ["--exclude bad/path/",
517
+ "--exclude other/exclude/",
518
+ "--exclude .svn/",
519
+ "--exclude .git/"]}])
520
+ end
521
+
522
+
429
523
  def test_write_script
430
524
  @sa.shell.mock :file?, :return => false
431
525
 
@@ -435,10 +529,19 @@ class TestServerApp < Test::Unit::TestCase
435
529
  "script contents", {:flags => "--chmod=ugo=rwx"}]
436
530
 
437
531
  assert @sa.shell.method_called?(:make_file, :args => args)
532
+ end
533
+
534
+
535
+ def test_symlink_scripts_to_root
536
+ @sa.shell.mock :call,
537
+ :args => ["ls -1 #{@sa.scripts_path}"],
538
+ :return => "script_name\n"
438
539
 
439
540
  args = ["#{@app.scripts_path}/script_name",
440
541
  "#{@app.root_path}/script_name"]
441
542
 
543
+ @sa.symlink_scripts_to_root
544
+
442
545
  assert @sa.shell.method_called?(:symlink, :args => args)
443
546
  end
444
547
 
@@ -452,10 +555,5 @@ class TestServerApp < Test::Unit::TestCase
452
555
  "script contents", {:flags => "--chmod=ugo=rwx"}]
453
556
 
454
557
  assert !@sa.shell.method_called?(:make_file, :args => args)
455
-
456
- args = ["#{@app.scripts_path}/script_name",
457
- "#{@app.root_path}/script_name"]
458
-
459
- assert @sa.shell.method_called?(:symlink, :args => args)
460
558
  end
461
559
  end