move to an all-in-one command shell interface
authorLunar <lunar@anargeek.net>
Fri, 8 Mar 2013 21:21:23 +0000 (22:21 +0100)
committerLunar <lunar@anargeek.net>
Thu, 14 Mar 2013 09:16:59 +0000 (10:16 +0100)
Instead of having several different commands, we now move to a model where
there is one `coquelicot` command with several sub-commands.

`coquelicot-collect-garbage` is replaced by `coquelicot gc`.

`coquelicot-migrate-jyraphe` is replaced by coquelicot migrate-jyraphe`.

And two new sub-commands have been added: `start` and `stop`. They respectively
start and stop the Rainbows! web server, properly configured to run Coquelicot.

The configuration file for Coquelicot has gained new settings for web server
configuration:

 * `pid`: a path to the pid file,
 * `log`: a path to the log file,
 * `listen`: addresses on which requests should be accepted.

All of them are passed unmodified to Rainbows!: they are provided for the
administrator's convenience in order to keep all settings for Coquelicot in a
single configuration file.

Because there is no different environment for development and production,
`show_exceptions` is also mentioned in the default configuration file. Its
default value has been adjusted to false.

README
bin/coquelicot [moved from bin/coquelicot-collect-garbage with 96% similarity]
bin/coquelicot-migrate-jyraphe [deleted file]
conf/settings-default.yml
config-development.ru [deleted file]
config.ru [deleted file]
lib/coquelicot/app.rb
lib/coquelicot/jyraphe_migrator.rb
spec/coquelicot/app_spec.rb
spec/coquelicot/jyraphe_migrator_spec.rb

diff --git a/README b/README
index f39495d..9eaffc1 100644 (file)
--- a/README
+++ b/README
@@ -117,7 +117,7 @@ Once Bundler is available, please issue:
 
 Then, to start Coquelicot use:
 
-    $ bin/rainbows -c rainbows.conf -E none config.ru
+    $ bin/coquelicot start
 
 Coquelicot is intended to be run on a fully encrypted system and
 accessible only through HTTPS. To configure Apache as a reverse proxy,
@@ -127,8 +127,9 @@ you will need to add the following directives:
     SetEnv proxy-sendchunks 1
     RequestHeader set X-Forwarded-SSL "on"
 
-You can also run Coquelicot with mod_passenger, Mongrel, Thin or any
-Rack compatible webserver, but please read below about buffered input.
+Coquelicot has been written to use Rainbows! as its webserver. You can
+also run Coquelicot with mod_passenger, Mongrel, Thin or any Rack
+compatible webserver, but please read below about buffered input.
 
 ### Configuration
 
@@ -153,25 +154,31 @@ Further settings example:
 You can copy one of these examples to `conf/settings.yml` and adjust
 them according to your environment.
 
+A different location for the configuration file can be specified using
+the `-c` option when running `bin/coquelicot`.
+
 ### Garbage collection
 
 To cleanup files automatically when they expired, coquelicot comes with
 a cleanup script, that does the garbage collection for you. The easiest
-way is to run `bin/coquelicot-collect-gabage` as a cron job that runs
-every 5 minutes (or so).
+way is to set up a cron job that will run every 5 minutes (or so):
+
+    bin/coquelicot gc
 
 ### Migrate from Jyraphe
 
 [Jyraphe] is another free software web file sharing application.
 Coquelicot provides a migration script to import Jyraphe 0.5
-repositories as `bin/coquelicot-migrate-jyraphe`:
+repositories as `bin/coquelicot migrate-jyraphe`:
 
-    Usage: coquelicot-migrate-jyraphe [options] jyraphe-var > rewrite-rules
+    Usage: coquelicot [options] migrate-jyraphe \ 
+                      [command options] JYRAPHE_VAR > REWRITE_RULES
 
     Options:
         -c, --config FILE            read settings from FILE
+
+    Command options:
         -p, --rewrite-prefix PREFIX  prefix URL in rewrite rules
-        -h, --help                   show this message
 
 The last argument must be a path to the `var` directory of the Jyraphe
 installation. After migrating the files to Coquelicot, directives for
@@ -205,7 +212,7 @@ just a matter of typing:
 
 Running a test server can be done with:
 
-    bundle exec rackup config-development.ru
+    bundle exec coquelicot start --no-daemon
 
 To update the translation source files, use:
 
similarity index 96%
rename from bin/coquelicot-collect-garbage
rename to bin/coquelicot
index dee0737..5142fff 100755 (executable)
@@ -22,4 +22,4 @@ Bundler.setup
 
 require 'coquelicot'
 
-Coquelicot.collect_garbage!(ARGV)
+Coquelicot.run!(ARGV)
diff --git a/bin/coquelicot-migrate-jyraphe b/bin/coquelicot-migrate-jyraphe
deleted file mode 100755 (executable)
index e0c1b71..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/env ruby1.8
-# Coquelicot: "one-click" file sharing with a focus on users' privacy.
-# Copyright © 2010-2012 potager.org <jardiniers@potager.org>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-require 'rubygems'
-require 'bundler'
-Bundler.setup
-
-require 'coquelicot'
-
-Coquelicot.migrate_jyraphe!(ARGV)
index 7a77ffb..d644113 100644 (file)
@@ -77,6 +77,34 @@ about_text: ""
 # Path to an additional stylesheet
 additional_css: ""
 
+# Path to the PID file of the web server
+pid: "./tmp/coquelicot.pid"
+
+# Path to Coquelicot log file
+#
+#   Set to an empty string to disable logging.
+#
+log: "./tmp/coquelicot.log"
+
+# Listening addresses of the web server
+#
+#   Each entries may be a port number for a TCP port, an “IP_ADDRESS:PORT” for
+#   TCP listeners or a pathname for UNIX domain sockets.
+#
+#   Examples:
+#    - "51161"                 # listen to port 51161 on all TCP interfaces
+#    - "127.0.0.1:51161"       # listen to port 51161 on the loopback interface
+#    - "/tmp/.coquelicot.sock" # listen on the given Unix domain socket
+#    - "[::1]:51161"           # listen to port 51161 on the IPv6 loopback interface
+#
+listen:
+ - "127.0.0.1:51161"
+
+# Display debugging data in the browser when an exception is raised
+#
+#   This should only be turned on when doing development.
+show_exceptions: false
+
 # Authentication method
 #
 #   Please have look at `conf/settings-simplepass.yml` and
diff --git a/config-development.ru b/config-development.ru
deleted file mode 100644 (file)
index c9aea89..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-# Coquelicot: "one-click" file sharing with a focus on users' privacy.
-# Copyright © 2012 potager.org <jardiniers@potager.org>
-#           © 2011 mh / immerda.ch <mh+coquelicot@immerda.ch>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-require 'rubygems'
-require 'bundler'
-
-Bundler.require(:development)
-
-$:.unshift File.join(File.dirname(__FILE__), 'lib')
-require 'coquelicot'
-
-app = Coquelicot::Application
-
-app.set :environment, :development
-app.set :raise_errors, true
-app.set :logging, true
-app.disable :run
-
-run app
diff --git a/config.ru b/config.ru
deleted file mode 100644 (file)
index dc962cb..0000000
--- a/config.ru
+++ /dev/null
@@ -1,52 +0,0 @@
-# Coquelicot: "one-click" file sharing with a focus on users' privacy.
-# Copyright © 2010-2012 potager.org <jardiniers@potager.org>
-#           © 2011 mh / immerda.ch <mh+coquelicot@immerda.ch>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-require 'rubygems'
-require 'bundler'
-
-Bundler.require
-
-$:.unshift File.join(File.dirname(__FILE__), 'lib')
-require 'coquelicot'
-
-if defined? Rainbows::Client
-  # This implements the behaviour outlined in Section 8 of
-  # <http://ftp.ics.uci.edu/pub/ietf/http/draft-ietf-http-connection-00.txt>.
-  #
-  # Half-closing the write part first and draining our input makes sure the
-  # client will properly receive an error message instead of TCP RST (a.k.a.
-  # "Connection reset by peer") when we interrupt it in the middle of a POST
-  # request.
-  #
-  # Thanks Eric Wong for these few lines. See
-  # <http://rubyforge.org/pipermail/rainbows-talk/2012-February/000328.html> for
-  # the discussion that lead him to propose what follows.
-  class Rainbows::Client
-    def close
-      close_write
-      buf = ""
-      loop do
-        kgio_wait_readable(2)
-        break unless kgio_tryread(512, buf)
-      end
-    ensure
-      super
-    end
-  end
-end
-
-run Coquelicot::Application
index d443ee8..f31df7a 100644 (file)
@@ -23,6 +23,7 @@ require 'digest/sha1'
 require 'fast_gettext'
 require 'upr'
 require 'moneta'
+require 'unicorn/launcher'
 require 'rainbows'
 require 'optparse'
 
@@ -35,17 +36,164 @@ module Coquelicot
       @depot = Depot.new(settings.depot_path) if @depot.nil? || settings.depot_path != @depot.path
       @depot
     end
-    # Called by +coquelicot-collect-garbage+ script.
-    def collect_garbage!(args = [])
-      parser ||= OptionParser.new do |opts|
-        opts.banner = "Usage: #{$0} [options]"
+    # Called by the +coquelicot+ script.
+    def run!(args = [])
+      parser = OptionParser.new do |opts|
+        opts.banner = "Usage: #{opts.program_name} [options] COMMAND [command options]"
 
         opts.separator ""
-        opts.separator "Options:"
+        opts.separator "Common options:"
 
         opts.on "-c", "--config FILE", "read settings from FILE" do |file|
-          settings.config_file file
+          if File.readable? file
+            settings.config_file file
+          else
+            $stderr.puts "#{opts.program_name}: cannot access configuration file '#{file}'."
+            exit 1
+          end
         end
+        opts.on("-h", "--help", "show this message") do
+          $stderr.puts opts.to_s
+          exit
+        end
+        opts.separator ""
+        opts.separator "Available commands:"
+        opts.separator "    start             Start web server"
+        opts.separator "    stop              Stop web server"
+        opts.separator "    gc                Run garbage collection"
+        opts.separator "    migrate-jyraphe   Migrate a Jyraphe repository"
+        opts.separator ""
+        opts.separator "See '#{opts.program_name} COMMAND --help' for more information on a specific command."
+      end
+      begin
+        parser.order!(args) do |command|
+          if %w{start stop gc migrate-jyraphe}.include? command
+            return self.send("#{command.gsub(/-/, '_')}!", args)
+          else
+            $stderr.puts("#{parser.program_name}: '#{command}' is not a valid command. " +
+                         "See '#{parser.program_name} --help'.")
+            exit 1
+          end
+        end
+      rescue OptionParser::InvalidOption => ex
+        $stderr.puts("#{parser.program_name}: '#{ex.args[0]}' is not a valid option. " +
+                     "See '#{parser.program_name} --help'.")
+        exit 1
+      end
+      # if we reach here, no command was given
+      $stderr.puts parser.to_s
+      exit
+    end
+    def start!(args)
+      options = {}
+      parser = OptionParser.new do |opts|
+        opts.banner = "Usage: #{opts.program_name} [options] start [command options]"
+        opts.separator ""
+        opts.separator "'#{opts.program_name} start' will start the web server in background."
+        opts.separator "Use '#{opts.program_name} stop' to stop it when done serving."
+        opts.separator ""
+        opts.separator "Command options:"
+        opts.on_tail("-n", "--no-daemon", "do not daemonize (stay in foreground)") do
+          options[:no_daemon] = true
+        end
+        opts.on_tail("-h", "--help", "show this message") do
+          $stderr.puts opts.to_s
+          exit
+        end
+      end
+      parser.parse!(args)
+
+      Unicorn::Configurator::DEFAULTS.merge!({
+        :pid => settings.pid,
+        :listeners => settings.listen,
+        :use => :ThreadSpawn,
+        :rewindable_input => false,
+        :client_max_body_size => nil
+      })
+      unless options[:no_daemon]
+        if settings.log
+          Unicorn::Configurator::DEFAULTS.merge!({
+            :stdout_path => settings.log,
+            :stderr_path => settings.log
+          })
+        end
+      end
+
+      # daemonize! and start pass data around through rainbows_opts
+      rainbows_opts = {}
+      ::Unicorn::Launcher.daemonize!(rainbows_opts) unless options[:no_daemon]
+
+      app = lambda do
+          ::Rack::Builder.new do
+            # This implements the behaviour outlined in Section 8 of
+            # <http://ftp.ics.uci.edu/pub/ietf/http/draft-ietf-http-connection-00.txt>.
+            #
+            # Half-closing the write part first and draining our input makes sure the
+            # client will properly receive an error message instead of TCP RST (a.k.a.
+            # "Connection reset by peer") when we interrupt it in the middle of a POST
+            # request.
+            #
+            # Thanks Eric Wong for these few lines. See
+            # <http://rubyforge.org/pipermail/rainbows-talk/2012-February/000328.html> for
+            # the discussion that lead him to propose what follows.
+            Rainbows::Client.class_eval <<-END_OF_METHOD
+              def close
+                close_write
+                buf = ""
+                loop do
+                  kgio_wait_readable(2)
+                  break unless kgio_tryread(512, buf)
+                end
+              ensure
+                super
+              end
+            END_OF_METHOD
+
+            use ::Rack::ContentLength
+            use ::Rack::Chunked
+            use ::Rack::CommonLogger, $stderr
+            run Application
+          end.to_app
+        end
+
+      server = ::Rainbows::HttpServer.new(app, rainbows_opts)
+      server.start.join
+    end
+    def stop!(args)
+      parser = OptionParser.new do |opts|
+        opts.banner = "Usage: #{opts.program_name} [options] stop [command options]"
+        opts.separator ""
+        opts.separator "'#{opts.program_name} stop' will stop the web server."
+        opts.separator ""
+        opts.separator "Command options:"
+        opts.on_tail("-h", "--help", "show this message") do
+          $stderr.puts opts.to_s
+          exit
+        end
+      end
+      parser.parse!(args)
+
+      unless File.readable? settings.pid
+        $stderr.puts "Unable to read #{settings.pid}. Are you sure Coquelicot is started?"
+        exit 1
+      end
+
+      pid = File.read(settings.pid).to_i
+      if pid == 0
+        $stderr.puts "Bad PID file #{settings.pid}."
+        exit 1
+      end
+
+      Process.kill(:TERM, pid)
+    end
+    def gc!(args)
+      parser = OptionParser.new do |opts|
+        opts.banner = "Usage: #{opts.program_name} [options] gc [command options]"
+        opts.separator ""
+        opts.separator "'#{opts.program_name} gc' will clean up expired files from the current depot."
+        opts.separator "Depot is currently set to '#{Coquelicot.depot.path}'"
+        opts.separator ""
+        opts.separator "Command options:"
         opts.on_tail("-h", "--help", "show this message") do
           $stderr.puts opts.to_s
           exit
@@ -54,7 +202,6 @@ module Coquelicot
       parser.parse!(args)
       depot.gc!
     end
-    # Called by +coquelicot-migrate-jyraphe+ script.
     def migrate_jyraphe!(args = [])
       require 'coquelicot/jyraphe_migrator'
       Coquelicot::JyrapheMigrator.run! args
@@ -81,6 +228,10 @@ module Coquelicot
     set :random_pass_length, 16
     set :about_text, ''
     set :additional_css, ''
+    set :pid, Proc.new { File.join(root, 'tmp/coquelicot.pid') }
+    set :log, Proc.new { File.join(root, 'tmp/coquelicot.log') }
+    set :listen, [ "127.0.0.1:51161" ]
+    set :show_exceptions, false
     set :authentication_method, :name => :simplepass,
                                 :upload_password => 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'
 
index 8eac432..f0a01b5 100644 (file)
@@ -52,14 +52,11 @@ module Coquelicot
 
       def parser
         @parser ||= OptionParser.new do |opts|
-          opts.banner = "Usage: #{$0} [options] jyraphe-var > rewrite-rules"
+          opts.banner = "Usage: #{opts.program_name} [options] migrate-jyraphe [command options] JYRAPHE_VAR > REWRITE_RULES"
 
           opts.separator ""
-          opts.separator "Options:"
+          opts.separator "Command options:"
 
-          opts.on "-c", "--config FILE", "read settings from FILE" do |file|
-            Coquelicot.settings.config_file file
-          end
           opts.on "-p", "--rewrite-prefix PREFIX", "prefix URL in rewrite rules" do |prefix|
             options[:rewrite_prefix] = prefix
           end
index 65101b7..2437ade 100644 (file)
@@ -200,70 +200,198 @@ describe Coquelicot::Application do
   end
 end
 
-describe Coquelicot, '.collect_garbage!' do
-  context 'when given no option' do
-    include_context 'with Coquelicot::Application'
+describe Coquelicot, '.run!' do
+  include_context 'with Coquelicot::Application'
 
-    it 'should use the default depot path' do
-      Coquelicot::Depot.should_receive(:new).
-        with(@depot_path).
-        and_return(double.as_null_object)
-      Coquelicot.collect_garbage!
+  context 'when given no option' do
+    it 'should display help and exit' do
+      stderr = capture(:stderr) do
+        expect { Coquelicot.run! %w{} }.to raise_error(SystemExit)
+      end
+      stderr.should =~ /Usage:/
     end
-    it 'should call gc!' do
-      depot = double('Depot').as_null_object
-      depot.should_receive(:gc!)
-      Coquelicot::Depot.stub(:new).and_return(depot)
-      Coquelicot.collect_garbage!
+  end
+  context 'when using "-h"' do
+    it 'should display help and exit' do
+      stderr = capture(:stderr) do
+        expect { Coquelicot.run! %w{-h} }.to raise_error(SystemExit)
+      end
+      stderr.should =~ /Usage:/
     end
   end
   context 'when using "-c <settings.yml>"' do
-    around(:each) do |example|
-      settings = Tempfile.new('coquelicot')
-      begin
-        settings.write(YAML.dump({ 'depot_path' => '/nonexistent' }))
-        settings.close
-        @settings_path = settings.path
-        example.run
-      ensure
-        settings.unlink
+    it 'should use the given setting file' do
+      settings_file = File.expand_path('../../../conf/settings-default.yml', __FILE__)
+      Coquelicot::Application.should_receive(:config_file).with(settings_file)
+      stderr = capture(:stderr) do
+        expect { Coquelicot.run! ['-c', settings_file] }.to raise_error(SystemExit)
       end
     end
-    it 'should use the depot path defined in the given settings' do
-      Coquelicot::Depot.should_receive(:new).
-        with('/nonexistent').
-        and_return(double.as_null_object)
-      Coquelicot.collect_garbage! ['-c', @settings_path]
+    context 'when the given settings file exists' do
+      around(:each) do |example|
+        settings = Tempfile.new('coquelicot')
+        begin
+          settings.write(YAML.dump({ 'depot_path' => '/nonexistent' }))
+          settings.close
+          @settings_path = settings.path
+          example.run
+        ensure
+          settings.unlink
+        end
+      end
+      it 'should use the depot path defined in the given settings' do
+        # We don't give a command, so exit is expected
+        stderr = capture(:stderr) do
+          expect { Coquelicot.run! ['-c', @settings_path] }.to raise_error(SystemExit)
+        end
+        Coquelicot.settings.depot_path.should == '/nonexistent'
+      end
     end
-    it 'should call gc!' do
-      depot = double('Depot').as_null_object
-      depot.should_receive(:gc!)
-      Coquelicot::Depot.stub(:new).and_return(depot)
-      Coquelicot.collect_garbage! ['-c', @settings_path]
+    context 'when the given settings file does not exist' do
+      it 'should display an error' do
+        stderr = capture(:stderr) do
+          expect { Coquelicot.run! %w{-c non-existent.yml} }.to raise_error(SystemExit)
+        end
+        stderr.should =~ /cannot access/
+      end
     end
   end
-  context 'when using "-h"' do
-    it 'should display help and exit' do
+  context 'when given an invalid option' do
+    it 'should display an error' do
       stderr = capture(:stderr) do
-        expect { Coquelicot.collect_garbage! ['-h'] }.to raise_error(SystemExit)
+        expect { Coquelicot.run! %w{--invalid-option} }.to raise_error(SystemExit)
       end
-      stderr.should =~ /Usage:/
+      stderr.should =~ /not a valid option/
     end
-    it 'should not call gc!' do
-      depot = double('Depot').as_null_object
-      depot.should_not_receive(:gc!)
-      Coquelicot::Depot.stub(:new).and_return(depot)
-      capture(:stderr) do
-        expect { Coquelicot.collect_garbage! ['-h'] }.to raise_error(SystemExit)
+  end
+  context 'when given "whatever"' do
+    it 'should display an error' do
+      stderr = capture(:stderr) do
+        expect { Coquelicot.run! %w{whatever} }.to raise_error(SystemExit)
       end
+      stderr.should =~ /not a valid command/
     end
   end
-end
+  shared_context 'command accepts options' do
+    context 'when given "--help" option' do
+      it 'should display help and exit' do
+        stderr = capture(:stderr) do
+          expect { Coquelicot.run!([command, '--help']) }.to raise_error(SystemExit)
+        end
+        stderr.should =~ /Usage:/
+      end
+    end
+    context 'when given an invalid option' do
+      it 'should display an error' do
+        stderr = capture(:stderr) do
+          expect { Coquelicot.run!([command, '--invalid-option']) }.to raise_error(SystemExit)
+        end
+        stderr.should =~ /not a valid option/
+      end
+    end
+  end
+  context 'when given "start"' do
+    let(:command) { 'start' }
+    include_context 'command accepts options'
 
-describe Coquelicot, '.migrate_jyraphe!' do
-  it 'should call the migrator' do
-    args = ['whatever']
-    Coquelicot::JyrapheMigrator.should_receive(:run!).with(args)
-    Coquelicot.migrate_jyraphe! args
+    before(:each) do
+      # :stdout_path and :stderr_path should not be set, otherwise RSpec will break!
+      app.set :log, nil
+    end
+    context 'with default options' do
+      it 'should daemonize' do
+        ::Unicorn::Launcher.should_receive(:daemonize!)
+        ::Rainbows::HttpServer.stub(:new).and_return(double('HttpServer').as_null_object)
+        Coquelicot.run! %w{start}
+      end
+      it 'should start the web server' do
+        ::Unicorn::Launcher.stub(:daemonize!)
+        server = double('HttpServer')
+        server.should_receive(:start).and_return(double('Thread').as_null_object)
+        ::Rainbows::HttpServer.stub(:new).and_return(server)
+        Coquelicot.run! %w{start}
+      end
+    end
+    context 'when given the --no-daemon option' do
+      it 'should not daemonize' do
+        ::Unicorn::Launcher.should_receive(:daemonize!).never
+        ::Rainbows::HttpServer.stub(:new).and_return(double('HttpServer').as_null_object)
+        Coquelicot.run! %w{start --no-daemon}
+      end
+      it 'should set the default configuration' do
+        app.set :pid, @depot_path
+        app.set :listen, ['127.0.0.1:42']
+        ::Rainbows::HttpServer.any_instance.stub(:start) do
+          server = ::Rainbows.server
+          server.config.set[:pid].should == @depot_path
+          server.config.set[:listeners].should == ['127.0.0.1:42']
+          double('Thread').as_null_object
+        end
+        Coquelicot.run! %w{start --no-daemon}
+      end
+      it 'should start the web server' do
+        server = double('HttpServer')
+        server.should_receive(:start).and_return(double('Thread').as_null_object)
+        ::Rainbows::HttpServer.stub(:new).and_return(server)
+        Coquelicot.run! %w{start --no-daemon}
+      end
+    end
+  end
+  context 'when given "stop"' do
+    let(:command) { 'stop' }
+    include_context 'command accepts options'
+
+    context 'when the pid file is correct' do
+      let(:pid) { 42 }
+      before(:each) do
+        File.open("#{@depot_path}/pid", 'w') do |f|
+          f.write(pid.to_s)
+        end
+        app.set :pid, "#{@depot_path}/pid"
+      end
+      it 'should stop the web server' do
+        Process.should_receive(:kill).with(:TERM, pid)
+        Coquelicot.run! %w{stop}
+      end
+    end
+    context 'when the pid file does not exist' do
+      it 'should error out' do
+        app.set :pid, '/nonexistent'
+        stderr = capture(:stderr) do
+          expect { Coquelicot.run! %w{stop} }.to raise_error(SystemExit)
+        end
+        stderr.should =~ /Unable to read/
+      end
+    end
+    context 'when the pid file contains garbage' do
+      before(:each) do
+        File.open("#{@depot_path}/pid", 'w') do |f|
+          f.write('The queerest of the queer')
+        end
+        app.set :pid, "#{@depot_path}/pid"
+      end
+      it 'should errour out' do
+        stderr = capture(:stderr) do
+          expect { Coquelicot.run! %w{stop} }.to raise_error(SystemExit)
+        end
+        stderr.should =~ /Bad PID file/
+      end
+    end
+  end
+  context 'when given "gc"' do
+    let(:command) { 'gc' }
+    include_context 'command accepts options'
+
+    it 'should call gc!' do
+      Coquelicot.depot.should_receive(:gc!).once
+      Coquelicot.run! %w{gc}
+    end
+  end
+  context 'when given "migrate-jyraphe"' do
+    let(:args) { %w{all args} }
+    it 'should call the migrator' do
+      Coquelicot::JyrapheMigrator.should_receive(:run!).with(args)
+      Coquelicot.run!(%w{migrate-jyraphe} + args)
+    end
   end
 end
index a9a504e..bec2dc0 100644 (file)
@@ -372,34 +372,6 @@ module Coquelicot
           stdout.strip.should == 'rules'
         end
       end
-      context 'when using "-c <settings.yml>"' do
-        around(:each) do |example|
-          settings = Tempfile.new('coquelicot')
-          begin
-            settings.write(YAML.dump({ 'depot_path' => '/nonexistent' }))
-            settings.close
-            @settings_path = settings.path
-            example.run
-          ensure
-            settings.unlink
-          end
-        end
-        it 'should use the depot path defined in the given settings' do
-          JyrapheMigrator.stub(:new).and_return(double.as_null_object)
-          capture(:stdout) do
-            JyrapheMigrator.run! ['-c', @settings_path, @jyraphe_var_path]
-          end
-          Coquelicot.settings.depot_path.should == '/nonexistent'
-        end
-        it 'should migrate' do
-          migrator = double('JyrapheMigrator').as_null_object
-          migrator.should_receive(:migrate!)
-          JyrapheMigrator.stub(:new).and_return(migrator)
-          capture(:stdout) do
-            JyrapheMigrator.run! ['-c', @settings_path, @jyraphe_var_path]
-          end
-        end
-      end
       context 'when using "-p"' do
         it 'should print rewrite rules using the given prefix' do
           migrator = double('JyrapheMigrator').as_null_object