Skip to content
Browse files

Imported code

  • Loading branch information...
1 parent 05b8bf7 commit ae4ee0f809910e74a61fd4dcf11dc00c73045531 @alloy alloy committed Jul 8, 2009
Showing with 243 additions and 0 deletions.
  1. +98 −0 lib/executioner.rb
  2. +139 −0 test/executioner_test.rb
  3. +6 −0 test/test_helper.rb
View
98 lib/executioner.rb
@@ -0,0 +1,98 @@
+require 'open3'
+
+module Executioner
+ class ExecutionerError < StandardError; end
+ class ProcessError < ExecutionerError; end
+ class ExecutableNotFoundError < ExecutionerError; end
+
+ SEARCH_PATHS = %w{ /bin /usr/bin /usr/local/bin /opt/local/bin }
+
+ class << self
+ attr_accessor :logger
+
+ def included(klass)
+ klass.extend ClassMethods
+ end
+ end
+
+ def execute(command, options={})
+ options[:switch_stdout_and_stderr] = false if options[:switch_stdout_and_stderr].nil?
+
+ Executioner.logger.debug("Executing: `#{command}'") if Executioner.logger
+
+ output = nil
+ Open3.popen3(command) do |stdin, stdout, stderr|
+ stdout, stderr = stderr, stdout if options[:switch_stdout_and_stderr]
+
+ output = stdout.gets(nil)
+ if output.nil? && (error_message = stderr.gets(nil))
+ if error_message =~ /:in\s`exec':\s(.+)\s\(.+\)$/
+ error_message = $1
+ end
+ raise ProcessError, "Command: \"#{command}\"\nOutput: \"#{error_message.chomp}\""
+ end
+ end
+ output
+ end
+ module_function :execute
+
+ def concat_args(args)
+ args.map { |a,v| "-#{a} #{v}" }.join(' ')
+ end
+
+ def queue(command)
+ @commands ||= []
+ @commands << command
+ end
+
+ def queued_commands
+ @commands ? @commands.join(' && ') : ''
+ end
+
+ def execute_queued(options={})
+ execute(queued_commands, options)
+ @commands = []
+ end
+
+ module ClassMethods
+ def executable(executable, options={})
+ options[:switch_stdout_and_stderr] = false if options[:switch_stdout_and_stderr].nil?
+ options[:use_queue] = false if options[:use_queue].nil?
+
+ executable = executable.to_s if executable.is_a? Symbol
+ use_queue = options.delete(:use_queue)
+
+ if selection_proc = options.delete(:select_if)
+ advance_from = nil
+ while executable_path = find_executable(executable, advance_from)
+ break if selection_proc.call(executable_path)
+ advance_from = File.dirname(executable_path)
+ end
+ else
+ executable_path = options[:path] || find_executable(executable)
+ end
+
+ if executable_path
+ if use_queue
+ body = "queue(\"#{executable_path} \#{args}\")"
+ else
+ body = "execute(\"#{executable_path} \#{args}\", #{options.inspect})"
+ end
+ else
+ body = "raise Executioner::ExecutableNotFoundError, \"Unable to find the executable '#{executable}' in: #{Executioner::SEARCH_PATHS.join(', ')}\""
+ end
+
+ class_eval "def #{executable.gsub(/-/, '_')}(args); #{body}; end", __FILE__, __LINE__
+ end
+
+ def find_executable(executable, advance_from = nil)
+ search_paths = Executioner::SEARCH_PATHS
+ search_paths = search_paths[(search_paths.index(advance_from) + 1)..-1] if advance_from
+
+ if executable_in_path = search_paths.find { |path| File.exist? File.join(path, executable) }
+ File.join executable_in_path, executable
+ end
+ end
+ module_function :find_executable
+ end
+end
View
139 test/executioner_test.rb
@@ -0,0 +1,139 @@
+require File.expand_path('../test_helper', __FILE__)
+
+class AClassThatUsesSubshells
+ include Executioner
+ executable :sh
+ executable :doesnotexistforsure
+ executable 'executable-with-dash'
+ executable :with_path, :path => '/path/to/executable'
+end
+
+describe "Executioner, the module" do
+ it "should assign an optional logger" do
+ begin
+ logger = Object.new
+ Executioner.logger = logger
+ Executioner.logger.should.be logger
+ ensure
+ Executioner.logger = nil
+ end
+ end
+end
+
+describe "Executioner, instance methods" do
+ before do
+ @object = AClassThatUsesSubshells.new
+ end
+
+ it "should raise a Executioner::ProcessError if a command could not be executed" do
+ proc = lambda { @object.send(:execute, "/bin/sh -M") }
+
+ proc.should.raise Executioner::ProcessError
+
+ begin
+ proc.call
+ rescue Executioner::ProcessError => error
+ error.message.should =~ %r%Command: "/bin/sh -M"%
+ error.message.should =~ %r%Output: "/bin/sh: -M: invalid option%
+ end
+ end
+
+ it "should be able to switch stdout and stderr, for instance for ffmpeg" do
+ lambda { @object.send(:execute, "/bin/sh -M", :switch_stdout_and_stderr => true) }.should.not.raise Executioner::ProcessError
+ end
+
+ it "should help concat arguments" do
+ @object.send(:concat_args, [[:foo, 'foo'], [:bar, 'bar']]).should == "-foo foo -bar bar"
+ end
+
+ it "should queue one command" do
+ @object.send(:queue, 'ls')
+ @object.send(:queued_commands).should == 'ls'
+ end
+
+ it "should queue multiple commands" do
+ @object.send(:queue, 'ls')
+ @object.send(:queue, 'cat')
+ @object.send(:queued_commands).should == 'ls && cat'
+ end
+
+ it "should execute queued commands" do
+ @object.send(:queue, 'ls')
+ @object.send(:queue, 'ls')
+ @object.expects(:execute).with(@object.send(:queued_commands), {})
+ @object.send(:execute_queued)
+ @object.send(:queued_commands).should == ''
+ end
+end
+
+describe "Executioner, class methods" do
+ before do
+ @object = AClassThatUsesSubshells.new
+ end
+
+ it "should define an instance method for the specified binary that's needed" do
+ AClassThatUsesSubshells.instance_methods.should.include 'sh'
+ end
+
+ it "should define an instance method which calls #execute with the correct path to the executable" do
+ @object.expects(:execute).with('/bin/sh with some args', {:switch_stdout_and_stderr => false})
+ @object.sh 'with some args'
+ end
+
+ it "should define an instance method for an executable with dashes replaced by underscores" do
+ @object.should.respond_to :executable_with_dash
+ end
+
+ it "should be possible to switch stdin and stderr" do
+ AClassThatUsesSubshells.class_eval { executable(:sh, {:switch_stdout_and_stderr => true}) }
+ @object.expects(:execute).with('/bin/sh with some args', {:switch_stdout_and_stderr => true})
+ @object.sh 'with some args'
+ end
+
+ it "should be possible to use the queue by default" do
+ AClassThatUsesSubshells.class_eval { executable(:sh, {:use_queue => true}) }
+ @object.expects(:execute).with('/bin/sh arg1 && /bin/sh arg2', {})
+ @object.sh 'arg1'
+ @object.sh 'arg2'
+ @object.execute_queued
+ end
+
+ it "should be possible to specify the path to the executable" do
+ @object.expects(:execute).with { |command, options| command == "/path/to/executable arg1" }
+ @object.with_path 'arg1'
+ end
+
+ it "should be possible to find an executable" do
+ File.stubs(:exist?).with('/bin/sh').returns(true)
+ Executioner::ClassMethods.find_executable('sh').should == '/bin/sh'
+ end
+
+ it "should be possible to find an executable advancing from a given path" do
+ File.stubs(:exist?).with('/usr/bin/sh').returns(true)
+ Executioner::ClassMethods.find_executable('sh', '/bin').should == '/usr/bin/sh'
+ end
+
+ it "should yield all found executables, but use the one for which the proc returns a truthful value" do
+ File.stubs(:exist?).with('/bin/with_selection_proc').returns(true)
+ File.stubs(:exist?).with('/usr/bin/with_selection_proc').returns(true)
+ File.stubs(:exist?).with('/usr/local/bin/with_selection_proc').returns(true)
+ File.stubs(:exist?).with('/opt/local/bin/with_selection_proc').returns(true)
+
+ AClassThatUsesSubshells.executable(:with_selection_proc, :select_if => lambda { |executable| nil })
+ lambda { @object.with_selection_proc('foo') }.should.raise Executioner::ExecutableNotFoundError
+
+ AClassThatUsesSubshells.executable(:with_selection_proc, :select_if => lambda { |executable| executable == '/usr/bin/with_selection_proc' })
+ @object.expects(:execute).with("/usr/bin/with_selection_proc foo", {:switch_stdout_and_stderr => false})
+ @object.with_selection_proc('foo')
+
+ AClassThatUsesSubshells.executable(:with_selection_proc, :select_if => lambda { |executable| executable == '/opt/local/bin/with_selection_proc' })
+ @object.expects(:execute).with("/opt/local/bin/with_selection_proc foo", {:switch_stdout_and_stderr => false})
+ @object.with_selection_proc('foo')
+ end
+
+ it "should define an instance method which raises a Executioner::ExecutableNotFoundError error if the executable could not be found" do
+ lambda {
+ @object.doesnotexistforsure 'right?'
+ }.should.raise Executioner::ExecutableNotFoundError
+ end
+end
View
6 test/test_helper.rb
@@ -0,0 +1,6 @@
+require "rubygems"
+require "test/spec"
+require "mocha"
+
+$:.unshift File.expand_path('../../lib', __FILE__)
+require "executioner"

0 comments on commit ae4ee0f

Please sign in to comment.
Something went wrong with that request. Please try again.