@@ 0,0 1,89 @@
+#!/usr/bin/ruby
+#
+# The base class for a background Task.
+# Background tasks are executed in separate threads, and are intended
+# to be used for computationally expensive jobs that don't require
+# immediate feedback to the end user.
+#
+# == Synopsis
+#
+# require 'thingfish/task'
+#
+# class MyTask < ThingFish::Task
+# end
+#
+#
+# == Version
+#
+# $Id$
+#
+# == Authors
+#
+# * Michael Granger <mgranger@laika.com>
+# * Mahlon E. Smith <mahlon@laika.com>
+#
+# :include: LICENSE
+#
+#---
+#
+# Please see the file LICENSE in the top-level directory for licensing details.
+#
+
+require 'thingfish'
+require 'thingfish/mixins'
+
+### Base class for ThingFish tasks
+class ThingFish::Task
+ include ThingFish::Loggable,
+ ThingFish::Constants,
+ ThingFish::AbstractClass
+
+ # SVN Revision
+ SVNRev = %q$Rev$
+
+ # SVN Id
+ SVNId = %q$Id$
+
+ #################################################################
+ ### I N S T A N C E M E T H O D S
+ #################################################################
+
+ ### Set up a new Task object that represents a body of code to be
+ ### executed at a unspecified later time.
+ def initialize( label=nil )
+ @data = {}
+ @label = label
+ @creation_time = Time.now
+ end
+
+ ######
+ public
+ ######
+
+ # The time when this task was instantiated. Comparing this against
+ # the time is is pulled off the queue is an indicator of how long it
+ # waited in line before being acted upon.
+ attr_reader :creation_time
+
+ # An arbitrary string for logging, to help disambiguate multiple tasks
+ # of the same class.
+ attr_reader :label
+
+ # Data to inject into the object, presumably to use within the run method.
+ attr_reader :data
+
+ ### Mandatory Task API -- code to be executed!
+ virtual :run
+
+ ### Return a human-readable version of the object
+ def inspect
+ return "#<%s:0x%07x%s>" % [
+ self.class.name,
+ self.object_id * 2,
+ self.label ? " '#{self.label}'" : nil
+ ]
+ end
+end # class ThingFish::Task
+
+# vim: set nosta noet ts=4 sw=4:
+
@@ 0,0 1,298 @@
+#!/usr/bin/env ruby
+
+BEGIN {
+ require 'pathname'
+ basedir = Pathname.new( __FILE__ ).dirname.parent.parent
+
+ libdir = basedir + "lib"
+
+ $LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
+}
+
+begin
+ require 'rbconfig'
+ require 'spec/runner'
+ require 'spec/lib/constants'
+ require 'spec/lib/handler_behavior'
+ require 'thingfish'
+ require 'thingfish/handler'
+rescue LoadError
+ unless Object.const_defined?( :Gem )
+ require 'rubygems'
+ retry
+ end
+ raise
+end
+
+include ThingFish::TestConstants
+
+# Expose some protected methods for testing
+class ThingFish::Handler
+ public :build_method_not_allowed_response
+end
+
+# Some subclasses just for testing
+class TestHandler < ThingFish::Handler
+ public :log_request
+end
+
+class GetDeleteTestHandler < ThingFish::Handler
+ def handle_get_request( *args )
+ return :get_response, args
+ end
+
+ def handle_delete_request( *args )
+ return :delete_response, args
+ end
+end
+
+include ThingFish::Constants
+
+
+#####################################################################
+### C O N T E X T S
+#####################################################################
+describe ThingFish::Handler do
+ include ThingFish::SpecHelpers
+
+ before( :all ) do
+ setup_logging( :fatal )
+ end
+
+ after( :all ) do
+ reset_logging()
+ end
+
+
+ it "should register subclasses as plugins" do
+ ThingFish::Handler.get_subclass( 'test' ).should == TestHandler
+ end
+
+
+ describe "concrete subclass instance mounted at /" do
+
+ before( :each ) do
+ @handler = ThingFish::Handler.create( 'test', '/' )
+ @datadir = Config::CONFIG['datadir']
+
+ @request = mock( "request object" )
+ @response = mock( "response object" )
+ end
+
+ it "does not raise an exception when asked to create path_info for a request whose " +
+ "path is an empty string" do
+ uri = URI.parse( 'http://thingfish.example.com:3474/')
+ @request.should_receive( :uri ).at_least( :once ).and_return( uri )
+ lambda {
+ @handler.path_info( @request )
+ }.should_not raise_error( ThingFish::DispatchError, %r{uri \S+ does not include}i )
+ end
+
+
+ it "delegation defaults to yielding back to the caller" do
+ yielded = nil
+
+ results = @handler.delegate( @request, @response ) do |*args|
+ yielded = args
+ [:other_request, :other_response]
+ end
+
+ yielded.should == [ @request, @response ]
+ results.should == [ :other_request, :other_response ]
+ end
+
+ end
+
+ describe "concrete subclass instance mounted at /test" do
+
+ before( :each ) do
+ @handler = ThingFish::Handler.create( 'test', '/test' )
+ @datadir = Config::CONFIG['datadir']
+ end
+
+
+ ### Shared behaviors
+ it_should_behave_like "A Handler"
+
+
+ ### Specs
+ it "knows what its normalized name is" do
+ @handler.plugin_name.should == 'test'
+ end
+
+
+ it "can return the request's path relative to itself" do
+ uri = URI.parse( 'http://thingfish.example.com:3474/test/cranglock')
+ request = mock( "request object" )
+ request.should_receive( :uri ).and_return( uri )
+ @handler.path_info( request ).should == 'cranglock'
+ end
+
+
+ it "doesn't include query arguments when determining path_info" do
+ uri = URI.parse( 'http://thingfish.example.com:3474/test/cranglockin/around?plenty=true')
+ request = mock( "request object" )
+ request.should_receive( :uri ).and_return( uri )
+ @handler.path_info( request ).should == 'cranglockin/around'
+ end
+
+
+ it "returns the empty string when asked to create path_info for a request whose URI " +
+ "path matches its own exactly" do
+ uri = URI.parse( 'http://thingfish.example.com:3474/test')
+ request = mock( "request object" )
+ request.should_receive( :uri ).and_return( uri )
+ @handler.path_info( request ).should == ''
+ end
+
+
+ it "raises an exception when asked to create path_info for a request whose URI does not " +
+ "include the handler's mountpoint" do
+ uri = URI.parse( 'http://thingfish.example.com:3474/crang/the/petite/weasel')
+ request = mock( "request object" )
+ request.should_receive( :uri ).at_least( :once ).and_return( uri )
+ lambda {
+ @handler.path_info( request )
+ }.should raise_error( ThingFish::DispatchError, %r{uri \S+ does not include}i )
+ end
+
+
+ it "outputs a consistent request log message" do
+ logfile = StringIO.new('')
+ ThingFish.logger = Logger.new( logfile )
+
+ params = {
+ 'REQUEST_METHOD' => :POST,
+ 'REMOTE_ADDR' => '127.0.0.1',
+ 'REQUEST_URI' => '/poon',
+ }
+ mockheaders = mock( "Mock Headers" )
+
+ request = mock( "request object" )
+ request.should_receive( :remote_addr ).
+ at_least(:once).
+ and_return( IPAddr.new('127.0.0.1') )
+ request.should_receive( :http_method ).
+ at_least(:once).
+ and_return( :POST )
+ request.should_receive( :uri ).
+ at_least(:once).
+ and_return( URI.parse('/poon') )
+ request.should_receive( :headers ).
+ at_least(:once).
+ and_return( mockheaders )
+
+ mockheaders.should_receive( :user_agent ).
+ and_return( "rspec/1.0" )
+
+ @handler.log_request( request )
+ logfile.rewind
+ logfile.read.should =~ %r<\S+: 127.0.0.1 POST /poon \{rspec/1.0\}>
+ end
+
+
+ it "gets references to the metastore and filestore from the daemon startup callback" do
+ mock_daemon = mock( "daemon", :null_object => true )
+ mock_daemon.should_receive( :filestore ).and_return( :filestore_obj )
+ mock_daemon.should_receive( :metastore ).and_return( :metastore_obj )
+
+ @handler.on_startup( mock_daemon )
+ end
+
+
+ it "doesn't add to the index content by default" do
+ @handler.make_index_content( '/foo' ).should be_nil
+ end
+
+ end
+
+
+ describe "that handles GET and DELETE requests" do
+
+ before(:each) do
+ ThingFish.logger.level = Logger::FATAL
+
+ @request = mock( "request object" )
+ @request.stub!( :uri ).and_return( URI.parse('http://localhost/getdelete/something') )
+
+ @response = mock( "response object" )
+ @response_headers = mock( "response headers" )
+ @response.stub!( :headers ).and_return( @response_headers )
+
+ @handler = ThingFish::Handler.create( 'getdeletetest', '/getdelete' )
+ @datadir = Config::CONFIG['datadir']
+ end
+
+
+ ### Shared behaviors
+ it_should_behave_like "A Handler"
+
+
+ ### Specs
+
+ it "calls its #handle_get_request method on a GET request" do
+ @response.stub!( :handlers ).and_return( [] )
+ @request.should_receive( :http_method ).
+ at_least(:once).
+ and_return( :GET )
+
+ @handler.process( @request, @response ).
+ should == [:get_response, ['something', @request, @response ]]
+ end
+
+
+ it "calls its #handle_get_request method on a HEAD request" do
+ @response.stub!( :handlers ).and_return( [] )
+ @request.should_receive( :http_method ).
+ at_least(:once).
+ and_return( :HEAD )
+
+ @handler.process( @request, @response ).
+ should == [:get_response, ['something', @request, @response ]]
+ end
+
+
+ it "responds with a METHOD_NOT_ALLOWED response on a POST request" do
+ @request.should_receive( :http_method ).
+ at_least(:once).
+ and_return( :POST )
+ @handler.stub!( :methods ).
+ and_return(['handle_get_request', 'handle_delete_request'])
+
+ @response.should_receive( :status= ).
+ with( HTTP::METHOD_NOT_ALLOWED )
+ @response_headers.should_receive( :[]= ).
+ with( :allow, 'DELETE, GET, HEAD' )
+ @response.should_receive( :body= ).
+ with( /POST.*not allowed/ )
+
+ @handler.process( @request, @response )
+ end
+
+
+ it "uses a list of valid methods if one is provided" do
+ @response.should_receive( :status= ).
+ with( HTTP::METHOD_NOT_ALLOWED )
+ @response_headers.should_receive( :[]= ).
+ with( :allow, 'DELETE, GET, HEAD' )
+ @response.should_receive( :body= ).
+ with( /PUT.*not allowed/ )
+
+ @handler.build_method_not_allowed_response( @response, :PUT, %w{DELETE GET HEAD} )
+ end
+
+
+ it "adds HEAD to a list of provided methods if it includes :GET" do
+ @response.should_receive( :status= ).
+ with( HTTP::METHOD_NOT_ALLOWED )
+ @response_headers.should_receive( :[]= ).
+ with( :allow, 'DELETE, GET, HEAD' )
+ @response.should_receive( :body= ).
+ with( /PUT.*not allowed/ )
+
+ @handler.build_method_not_allowed_response( @response, :PUT, %w{DELETE GET} )
+ end
+ end
+end
+
+# vim: set nosta noet ts=4 sw=4: