foreman 0.26.1 → 0.40.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.
Files changed (41) hide show
  1. data/README.md +40 -0
  2. data/bin/foreman-runner +36 -0
  3. data/data/example/Procfile +1 -0
  4. data/data/example/utf8 +11 -0
  5. data/data/export/bluepill/master.pill.erb +3 -3
  6. data/lib/foreman/cli.rb +35 -31
  7. data/lib/foreman/engine.rb +109 -82
  8. data/lib/foreman/export/base.rb +12 -5
  9. data/lib/foreman/export/bluepill.rb +5 -7
  10. data/lib/foreman/export/inittab.rb +11 -13
  11. data/lib/foreman/export/runit.rb +24 -25
  12. data/lib/foreman/export/upstart.rb +9 -11
  13. data/lib/foreman/export.rb +21 -0
  14. data/lib/foreman/helpers.rb +45 -0
  15. data/lib/foreman/process.rb +88 -6
  16. data/lib/foreman/procfile.rb +8 -7
  17. data/lib/foreman/procfile_entry.rb +22 -0
  18. data/lib/foreman/utils.rb +4 -1
  19. data/lib/foreman/version.rb +1 -1
  20. data/lib/foreman.rb +12 -1
  21. data/man/foreman.1 +17 -3
  22. data/spec/foreman/cli_spec.rb +96 -7
  23. data/spec/foreman/engine_spec.rb +52 -31
  24. data/spec/foreman/export/base_spec.rb +22 -0
  25. data/spec/foreman/export/bluepill_spec.rb +23 -7
  26. data/spec/foreman/export/inittab_spec.rb +40 -0
  27. data/spec/foreman/export/runit_spec.rb +18 -12
  28. data/spec/foreman/export/upstart_spec.rb +40 -8
  29. data/spec/foreman/export_spec.rb +22 -0
  30. data/spec/foreman/helpers_spec.rb +26 -0
  31. data/spec/foreman/process_spec.rb +131 -2
  32. data/spec/foreman_spec.rb +5 -0
  33. data/spec/helper_spec.rb +18 -0
  34. data/spec/resources/bin/utf8 +2 -0
  35. data/spec/resources/export/bluepill/app-concurrency.pill +47 -0
  36. data/spec/resources/export/bluepill/app.pill +1 -22
  37. data/spec/resources/export/inittab/inittab.concurrency +4 -0
  38. data/spec/resources/export/inittab/inittab.default +4 -0
  39. data/spec/spec_helper.rb +36 -4
  40. metadata +23 -11
  41. data/README.markdown +0 -49
@@ -3,58 +3,57 @@ require "foreman/export"
3
3
 
4
4
  class Foreman::Export::Runit < Foreman::Export::Base
5
5
  ENV_VARIABLE_REGEX = /([a-zA-Z_]+[a-zA-Z0-9_]*)=(\S+)/
6
-
7
- def export(location, options={})
6
+
7
+ def export
8
8
  error("Must specify a location") unless location
9
-
10
- app = options[:app] || File.basename(engine.directory)
11
- user = options[:user] || app
12
- log_root = options[:log] || "/var/log/#{app}"
13
- template_root = options[:template]
14
-
15
- concurrency = Foreman::Utils.parse_concurrency(options[:concurrency])
16
-
9
+
10
+ app = self.app || File.basename(engine.directory)
11
+ user = self.user || app
12
+ log_root = self.log || "/var/log/#{app}"
13
+ template_root = self.template
14
+
17
15
  run_template = export_template('runit', 'run.erb', template_root)
18
16
  log_run_template = export_template('runit', 'log_run.erb', template_root)
19
17
 
20
- engine.processes.each do |process|
21
- 1.upto(concurrency[process.name]) do |num|
18
+ engine.procfile.entries.each do |process|
19
+ 1.upto(self.concurrency[process.name]) do |num|
22
20
  process_directory = "#{location}/#{app}-#{process.name}-#{num}"
23
21
  process_env_directory = "#{process_directory}/env"
24
22
  process_log_directory = "#{process_directory}/log"
25
-
23
+
26
24
  create_directory process_directory
27
25
  create_directory process_env_directory
28
26
  create_directory process_log_directory
29
-
27
+
30
28
  run = ERB.new(run_template).result(binding)
31
29
  write_file "#{process_directory}/run", run
32
-
33
- port = engine.port_for(process, num, options[:port])
30
+ FileUtils.chmod 0755, "#{process_directory}/run"
31
+
32
+ port = engine.port_for(process, num, self.port)
34
33
  environment_variables = {'PORT' => port}.
35
34
  merge(engine.environment).
36
35
  merge(inline_variables(process.command))
37
-
36
+
38
37
  environment_variables.each_pair do |var, env|
39
38
  write_file "#{process_env_directory}/#{var.upcase}", env
40
39
  end
41
-
40
+
42
41
  log_run = ERB.new(log_run_template).result(binding)
43
42
  write_file "#{process_log_directory}/run", log_run
44
-
43
+ FileUtils.chmod 0755, "#{process_log_directory}/run"
45
44
  end
46
45
  end
47
-
46
+
48
47
  end
49
-
48
+
50
49
  private
51
50
  def create_directory(location)
52
51
  say "creating: #{location}"
53
- FileUtils.mkdir(location)
52
+ FileUtils.mkdir_p(location)
54
53
  end
55
-
54
+
56
55
  def inline_variables(command)
57
- variable_name_regex =
56
+ variable_name_regex =
58
57
  Hash[*command.scan(ENV_VARIABLE_REGEX).flatten]
59
58
  end
60
- end
59
+ end
@@ -3,37 +3,35 @@ require "foreman/export"
3
3
 
4
4
  class Foreman::Export::Upstart < Foreman::Export::Base
5
5
 
6
- def export(location, options={})
6
+ def export
7
7
  error("Must specify a location") unless location
8
8
 
9
9
  FileUtils.mkdir_p location
10
10
 
11
- app = options[:app] || File.basename(engine.directory)
12
- user = options[:user] || app
13
- log_root = options[:log] || "/var/log/#{app}"
14
- template_root = options[:template]
11
+ app = self.app || File.basename(engine.directory)
12
+ user = self.user || app
13
+ log_root = self.log || "/var/log/#{app}"
14
+ template_root = self.template
15
15
 
16
16
  Dir["#{location}/#{app}*.conf"].each do |file|
17
17
  say "cleaning up: #{file}"
18
18
  FileUtils.rm(file)
19
19
  end
20
20
 
21
- concurrency = Foreman::Utils.parse_concurrency(options[:concurrency])
22
-
23
21
  master_template = export_template("upstart", "master.conf.erb", template_root)
24
22
  master_config = ERB.new(master_template).result(binding)
25
23
  write_file "#{location}/#{app}.conf", master_config
26
24
 
27
25
  process_template = export_template("upstart", "process.conf.erb", template_root)
28
26
 
29
- engine.processes.each do |process|
30
- next if (conc = concurrency[process.name]) < 1
27
+ engine.procfile.entries.each do |process|
28
+ next if (conc = self.concurrency[process.name]) < 1
31
29
  process_master_template = export_template("upstart", "process_master.conf.erb", template_root)
32
30
  process_master_config = ERB.new(process_master_template).result(binding)
33
31
  write_file "#{location}/#{app}-#{process.name}.conf", process_master_config
34
32
 
35
- 1.upto(concurrency[process.name]) do |num|
36
- port = engine.port_for(process, num, options[:port])
33
+ 1.upto(self.concurrency[process.name]) do |num|
34
+ port = engine.port_for(process, num, self.port)
37
35
  process_config = ERB.new(process_template).result(binding)
38
36
  write_file "#{location}/#{app}-#{process.name}-#{num}.conf", process_config
39
37
  end
@@ -1,9 +1,30 @@
1
1
  require "foreman"
2
+ require "foreman/helpers"
2
3
 
3
4
  module Foreman::Export
5
+ extend Foreman::Helpers
6
+
4
7
  class Exception < ::Exception; end
8
+
9
+ def self.formatter(format)
10
+ begin
11
+ require "foreman/export/#{ format.tr('-', '_') }"
12
+ classy_format = classify(format)
13
+ formatter = constantize("Foreman::Export::#{ classy_format }")
14
+ rescue NameError => ex
15
+ error "Unknown export format: #{format} (no class Foreman::Export::#{ classy_format })."
16
+ rescue LoadError => ex
17
+ error "Unknown export format: #{format} (unable to load file 'foreman/export/#{ format.tr('-', '_') }')."
18
+ end
19
+ end
20
+
21
+ def self.error(message)
22
+ raise Foreman::Export::Exception.new(message)
23
+ end
24
+
5
25
  end
6
26
 
27
+
7
28
  require "foreman/export/base"
8
29
  require "foreman/export/inittab"
9
30
  require "foreman/export/upstart"
@@ -0,0 +1,45 @@
1
+ module Foreman::Helpers
2
+ # Copied whole sale from, https://github.com/defunkt/resque/
3
+
4
+ # Given a word with dashes, returns a camel cased version of it.
5
+ #
6
+ # classify('job-name') # => 'JobName'
7
+ def classify(dashed_word)
8
+ dashed_word.split('-').each { |part| part[0] = part[0].chr.upcase }.join
9
+ end # Tries to find a constant with the name specified in the argument string:
10
+
11
+ #
12
+ # constantize("Module") # => Module
13
+ # constantize("Test::Unit") # => Test::Unit
14
+ #
15
+ # The name is assumed to be the one of a top-level constant, no matter
16
+ # whether it starts with "::" or not. No lexical context is taken into
17
+ # account:
18
+ #
19
+ # C = 'outside'
20
+ # module M
21
+ # C = 'inside'
22
+ # C # => 'inside'
23
+ # constantize("C") # => 'outside', same as ::C
24
+ # end
25
+ #
26
+ # NameError is raised when the constant is unknown.
27
+ def constantize(camel_cased_word)
28
+ camel_cased_word = camel_cased_word.to_s
29
+
30
+ names = camel_cased_word.split('::')
31
+ names.shift if names.empty? || names.first.empty?
32
+
33
+ constant = Object
34
+ names.each do |name|
35
+ args = Module.method(:const_get).arity != 1 ? [false] : []
36
+
37
+ if constant.const_defined?(name, *args)
38
+ constant = constant.const_get(name)
39
+ else
40
+ constant = constant.const_missing(name)
41
+ end
42
+ end
43
+ constant
44
+ end
45
+ end
@@ -1,14 +1,96 @@
1
1
  require "foreman"
2
+ require "rubygems"
2
3
 
3
4
  class Foreman::Process
4
5
 
5
- attr_reader :name
6
- attr_reader :command
7
- attr_accessor :color
6
+ attr_reader :entry
7
+ attr_reader :num
8
+ attr_reader :pid
9
+ attr_reader :port
8
10
 
9
- def initialize(name, command)
10
- @name = name
11
- @command = command
11
+ def initialize(entry, num, port)
12
+ @entry = entry
13
+ @num = num
14
+ @port = port
12
15
  end
13
16
 
17
+ def run(pipe, basedir, environment)
18
+ with_environment(environment.merge("PORT" => port.to_s)) do
19
+ run_process basedir, entry.command, pipe
20
+ end
21
+ end
22
+
23
+ def name
24
+ "%s.%s" % [ entry.name, num ]
25
+ end
26
+
27
+ def kill(signal)
28
+ pid && Process.kill(signal, pid)
29
+ rescue Errno::ESRCH
30
+ false
31
+ end
32
+
33
+ def detach
34
+ pid && Process.detach(pid)
35
+ end
36
+
37
+ def alive?
38
+ kill(0)
39
+ end
40
+
41
+ def dead?
42
+ !alive?
43
+ end
44
+
45
+ private
46
+
47
+ def fork_with_io(command, basedir)
48
+ reader, writer = IO.pipe
49
+ command = replace_command_env(command)
50
+ pid = if Foreman.windows?
51
+ Dir.chdir(basedir) do
52
+ Process.spawn command, :out => writer, :err => writer
53
+ end
54
+ elsif Foreman.jruby?
55
+ require "posix/spawn"
56
+ POSIX::Spawn.spawn(Foreman.runner, "-d", basedir, command, {
57
+ :out => writer, :err => writer
58
+ })
59
+ else
60
+ fork do
61
+ writer.sync = true
62
+ $stdout.reopen writer
63
+ $stderr.reopen writer
64
+ reader.close
65
+ exec Foreman.runner, "-d", basedir, command
66
+ end
67
+ end
68
+ [ reader, pid ]
69
+ end
70
+
71
+ def run_process(basedir, command, pipe)
72
+ io, @pid = fork_with_io(command, basedir)
73
+ output pipe, "started with pid %d" % @pid
74
+ Thread.new do
75
+ until io.eof?
76
+ output pipe, io.gets
77
+ end
78
+ end
79
+ end
80
+
81
+ def output(pipe, message)
82
+ pipe.puts "%s,%s" % [ name, message ]
83
+ end
84
+
85
+ def replace_command_env(command)
86
+ command.gsub(/\$(\w+)/) { |e| ENV[e[1..-1]] }
87
+ end
88
+
89
+ def with_environment(environment)
90
+ original = ENV.to_hash
91
+ ENV.update environment
92
+ yield
93
+ ensure
94
+ ENV.replace original
95
+ end
14
96
  end
@@ -1,4 +1,5 @@
1
1
  require "foreman"
2
+ require "foreman/procfile_entry"
2
3
 
3
4
  # A valid Procfile entry is captured by this regex.
4
5
  # All other lines are ignored.
@@ -10,18 +11,18 @@ require "foreman"
10
11
  #
11
12
  class Foreman::Procfile
12
13
 
13
- attr_reader :processes
14
+ attr_reader :entries
14
15
 
15
16
  def initialize(filename)
16
- @processes = parse_procfile(filename)
17
+ @entries = parse_procfile(filename)
17
18
  end
18
19
 
19
- def process_names
20
- processes.map(&:name)
20
+ def [](name)
21
+ entries.detect { |entry| entry.name == name }
21
22
  end
22
23
 
23
- def [](name)
24
- processes.detect { |process| process.name == name }
24
+ def process_names
25
+ entries.map(&:name)
25
26
  end
26
27
 
27
28
  private
@@ -29,7 +30,7 @@ private
29
30
  def parse_procfile(filename)
30
31
  File.read(filename).split("\n").map do |line|
31
32
  if line =~ /^([A-Za-z0-9_]+):\s*(.+)$/
32
- Foreman::Process.new($1, $2)
33
+ Foreman::ProcfileEntry.new($1, $2)
33
34
  end
34
35
  end.compact
35
36
  end
@@ -0,0 +1,22 @@
1
+ require "foreman"
2
+
3
+ class Foreman::ProcfileEntry
4
+
5
+ attr_reader :name
6
+ attr_reader :command
7
+ attr_accessor :color
8
+
9
+ def initialize(name, command)
10
+ @name = name
11
+ @command = command
12
+ end
13
+
14
+ def spawn(num, pipe, basedir, environment, base_port)
15
+ (1..num).to_a.map do |n|
16
+ process = Foreman::Process.new(self, n, base_port + (n-1))
17
+ process.run(pipe, basedir, environment)
18
+ process
19
+ end
20
+ end
21
+
22
+ end
data/lib/foreman/utils.rb CHANGED
@@ -5,7 +5,10 @@ class Foreman::Utils
5
5
  def self.parse_concurrency(concurrency)
6
6
  begin
7
7
  pairs = concurrency.to_s.gsub(/\s/, "").split(",")
8
- pairs.inject(Hash.new(1)) do |hash, pair|
8
+
9
+ default = concurrency.nil? ? 1 : 0
10
+
11
+ pairs.inject(Hash.new(default)) do |hash, pair|
9
12
  process, amount = pair.split("=")
10
13
  hash.update(process => amount.to_i)
11
14
  end
@@ -1,5 +1,5 @@
1
1
  module Foreman
2
2
 
3
- VERSION = "0.26.1"
3
+ VERSION = "0.40.0"
4
4
 
5
5
  end
data/lib/foreman.rb CHANGED
@@ -4,5 +4,16 @@ module Foreman
4
4
 
5
5
  class AppDoesNotExist < Exception; end
6
6
 
7
- end
7
+ def self.runner
8
+ File.expand_path("../../bin/foreman-runner", __FILE__)
9
+ end
10
+
11
+ def self.jruby?
12
+ defined?(RUBY_PLATFORM) and RUBY_PLATFORM == "java"
13
+ end
8
14
 
15
+ def self.windows?
16
+ defined?(RUBY_PLATFORM) and RUBY_PLATFORM =~ /(win|w)32$/
17
+ end
18
+
19
+ end
data/man/foreman.1 CHANGED
@@ -1,7 +1,7 @@
1
1
  .\" generated with Ronn/v0.7.3
2
2
  .\" http://github.com/rtomayko/ronn/tree/0.7.3
3
3
  .
4
- .TH "FOREMAN" "1" "November 2011" "Foreman 0.26.0" "Foreman Manual"
4
+ .TH "FOREMAN" "1" "February 2012" "Foreman 0.39.0" "Foreman Manual"
5
5
  .
6
6
  .SH "NAME"
7
7
  \fBforeman\fR \- manage Procfile\-based applications
@@ -10,10 +10,13 @@
10
10
  \fBforeman start [process]\fR
11
11
  .
12
12
  .br
13
+ \fBforeman run <command>\fR
14
+ .
15
+ .br
13
16
  \fBforeman export <format> [location]\fR
14
17
  .
15
18
  .SH "DESCRIPTION"
16
- \fBForeman\fR is a manager for Procfile\-based applications\. Its aim is to abstract away the details of the Procfile format, and allow you to either run your application directly or export it to some other process management format\.
19
+ Foreman is a manager for Procfile\-based applications\. Its aim is to abstract away the details of the Procfile format, and allow you to either run your application directly or export it to some other process management format\.
17
20
  .
18
21
  .SH "RUNNING"
19
22
  \fBforeman start\fR is used to run your application directly from the command line\.
@@ -35,6 +38,9 @@ Specify the number of each process type to run\. The value passed in should be i
35
38
  \fB\-p\fR, \fB\-\-port\fR
36
39
  Specify which port to use as the base for this application\. Should be a multiple of 1000\.
37
40
  .
41
+ .P
42
+ \fBforeman run\fR is used to run one\-off commands using the same environment as your defined processes\.
43
+ .
38
44
  .SH "EXPORTING"
39
45
  \fBforeman export\fR is used to export your application to another process management format\.
40
46
  .
@@ -61,13 +67,21 @@ Specify the directory to place process logs in\.
61
67
  Specify which port to use as the base for this application\. Should be a multiple of 1000\.
62
68
  .
63
69
  .TP
70
+ \fB\-t\fR, \fB\-\-template\fR
71
+ Specify an alternate template to use for creating export files\. See \fIhttps://github\.com/ddollar/foreman/tree/master/data/export\fR for examples\.
72
+ .
73
+ .TP
64
74
  \fB\-u\fR, \fB\-\-user\fR
65
75
  Specify the user the application should be run as\. Defaults to the app name
66
76
  .
67
- .SH "OPTIONS"
77
+ .SH "GLOBAL OPTIONS"
68
78
  These options control all modes of foreman\'s operation\.
69
79
  .
70
80
  .TP
81
+ \fB\-d\fR, \fB\-\-directory\fR
82
+ Specify an alternate application root\. This defaults to the directory containing the Procfile\.
83
+ .
84
+ .TP
71
85
  \fB\-e\fR, \fB\-\-env\fR
72
86
  Specify an alternate environment file\. You can specify more than one file by using: \fB\-\-env file1,file2\fR\.
73
87
  .
@@ -1,8 +1,10 @@
1
1
  require "spec_helper"
2
2
  require "foreman/cli"
3
3
 
4
- describe "Foreman::CLI" do
4
+ describe "Foreman::CLI", :fakefs do
5
5
  subject { Foreman::CLI.new }
6
+ let(:engine) { subject.send(:engine) }
7
+ let(:entries) { engine.procfile.entries.inject({}) { |h,e| h.update(e.name => e) } }
6
8
 
7
9
  describe "start" do
8
10
  describe "with a non-existent Procfile" do
@@ -22,15 +24,35 @@ describe "Foreman::CLI" do
22
24
  mock.instance_of(Foreman::Engine).start
23
25
  subject.start
24
26
  end
27
+
28
+ it "can run a single process" do
29
+ dont_allow(subject).error
30
+ stub(engine).watch_for_output
31
+ stub(engine).watch_for_termination
32
+ mock(entries["alpha"]).spawn(1, is_a(IO), engine.directory, {}, 5000) { [] }
33
+ mock(entries["bravo"]).spawn(0, is_a(IO), engine.directory, {}, 5100) { [] }
34
+ subject.start("alpha")
35
+ end
25
36
  end
26
37
  end
27
38
 
28
39
  describe "export" do
29
40
  describe "options" do
41
+ it "uses .foreman" do
42
+ write_procfile
43
+ File.open(".foreman", "w") { |f| f.puts "concurrency: alpha=2" }
44
+ mock_export = mock(Foreman::Export::Upstart)
45
+ mock(Foreman::Export::Upstart).new("/upstart", is_a(Foreman::Engine), { "concurrency" => "alpha=2" }) { mock_export }
46
+ mock_export.export
47
+ foreman %{ export upstart /upstart }
48
+ end
49
+
30
50
  it "respects --env" do
31
51
  write_procfile
32
52
  write_env("envfile")
33
- mock.instance_of(Foreman::Export::Upstart).export("/upstart", { "env" => "envfile" })
53
+ mock_export = mock(Foreman::Export::Upstart)
54
+ mock(Foreman::Export::Upstart).new("/upstart", is_a(Foreman::Engine), { "env" => "envfile" }) { mock_export }
55
+ mock_export.export
34
56
  foreman %{ export upstart /upstart --env envfile }
35
57
  end
36
58
  end
@@ -47,10 +69,18 @@ describe "Foreman::CLI" do
47
69
  describe "with a Procfile" do
48
70
  before(:each) { write_procfile }
49
71
 
50
- describe "with an invalid formatter" do
72
+ describe "with a formatter with a generic error" do
73
+ before do
74
+ mock(Foreman::Export).formatter("errorful") { Class.new(Foreman::Export::Base) do
75
+ def export
76
+ raise Foreman::Export::Exception.new("foo")
77
+ end
78
+ end }
79
+ end
80
+
51
81
  it "prints an error" do
52
- mock_error(subject, "Unknown export format: invalidformatter.") do
53
- subject.export("invalidformatter")
82
+ mock_error(subject, "foo") do
83
+ subject.export("errorful")
54
84
  end
55
85
  end
56
86
  end
@@ -60,7 +90,9 @@ describe "Foreman::CLI" do
60
90
 
61
91
  it "runs successfully" do
62
92
  dont_allow(subject).error
63
- mock.instance_of(Foreman::Export::Upstart).export("/tmp/foo", {})
93
+ mock_export = mock(Foreman::Export::Upstart)
94
+ mock(Foreman::Export::Upstart).new("/tmp/foo", is_a(Foreman::Engine), {}) { mock_export }
95
+ mock_export.export
64
96
  subject.export("upstart", "/tmp/foo")
65
97
  end
66
98
  end
@@ -72,7 +104,7 @@ describe "Foreman::CLI" do
72
104
  before { write_procfile }
73
105
 
74
106
  it "displays the jobs" do
75
- mock(subject).display("valid procfile detected (alpha, bravo)")
107
+ mock(subject).puts("valid procfile detected (alpha, bravo)")
76
108
  subject.check
77
109
  end
78
110
  end
@@ -88,6 +120,63 @@ describe "Foreman::CLI" do
88
120
  end
89
121
  end
90
122
  end
123
+
124
+ describe "without a Procfile" do
125
+ it "displays an error" do
126
+ mock_error(subject, "Procfile does not exist.") do
127
+ subject.check
128
+ end
129
+ end
130
+ end
131
+ end
132
+
133
+ describe "run" do
134
+ describe "with a valid Procfile" do
135
+ before { write_procfile }
136
+
137
+ describe "and a command" do
138
+ let(:command) { ["ls", "-l"] }
139
+
140
+ before(:each) do
141
+ stub(subject).exec
142
+ end
143
+
144
+ it "should load the environment file" do
145
+ write_env
146
+ preserving_env do
147
+ subject.run *command
148
+ ENV["FOO"].should == "bar"
149
+ end
150
+
151
+ ENV["FOO"].should be_nil
152
+ end
153
+
154
+ it "should runute the command as a string" do
155
+ mock(subject).exec(command.join(" "))
156
+ subject.run *command
157
+ end
158
+ end
159
+
160
+ describe "and a non-existent command" do
161
+ let(:command) { "iuhtngrglhulhdfg" }
162
+
163
+ it "should print an error" do
164
+ mock_error(subject, "command not found: #{command}") do
165
+ subject.run command
166
+ end
167
+ end
168
+ end
169
+
170
+ describe "and a non-executable command" do
171
+ let(:command) { __FILE__ }
172
+
173
+ it "should print an error" do
174
+ mock_error(subject, "not executable: #{command}") do
175
+ subject.run command
176
+ end
177
+ end
178
+ end
179
+ end
91
180
  end
92
181
 
93
182
  end