Michael Granger avatar Michael Granger committed c6e3a86

Snapshot of conversion to RSpec 2

Comments (0)

Files changed (87)

data/thingfish/plugin_templates/lib/thingfish/filter/TEMPLATE.rb.erb

 		return {
 			'version'   => [1,0],
 			'supports'  => [],
-			'rev'       => VCSRev.match( /: (\w+)/ )[1] || 0,
+			'rev'       => VCSRev[ /: (\w+)/, 1 ] || 0,
 			'accepts'   => accepts,
 			'generates' => [],
 		  }

data/thingfish/plugin_templates/spec/lib/filestore_behavior.rb

 	$LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
 }
 
-require 'spec'
-require 'spec/lib/constants'
+require 'rspec'
+
 require 'spec/lib/helpers'
 
 require 'thingfish'
 
 describe "A FileStore", :shared => true do
 	include ThingFish::TestConstants
-	include ThingFish::SpecHelpers
 
 	it "returns nil for non-existant entry" do
 	    @fs.fetch( TEST_UUID ).should == nil

data/thingfish/plugin_templates/spec/lib/filter_behavior.rb

 	$LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
 }
 
-require 'spec'
-require 'spec/lib/constants'
+require 'rspec'
+
 require 'spec/lib/helpers'
 
 require 'thingfish'
 
 describe "A Filter", :shared => true do
 	include ThingFish::TestConstants
-	include ThingFish::SpecHelpers
 
 
 	it "knows what types it handles" do

data/thingfish/plugin_templates/spec/lib/handler_behavior.rb

 	$LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
 }
 
-require 'spec'
-require 'spec/lib/constants'
+require 'rspec'
+
 require 'spec/lib/helpers'
 
 require 'thingfish'
 
 describe "A Handler", :shared => true do
 	include ThingFish::TestConstants
-	include ThingFish::SpecHelpers
 
 
 	after( :all ) do
-		ThingFish.reset_logger
+		reset_logging()
 	end
 
 end

data/thingfish/plugin_templates/spec/lib/helpers.rb

 #!/usr/bin/env ruby
 # coding: utf-8
 
-require 'spec/lib/constants'
+require 'thingfish/testconstants'
 
 require 'digest/md5'
 require 'net/http'
 require 'thingfish/utils'
 
 
-include ThingFish::TestConstants
-include ThingFish::Constants
 
 module ThingFish::SpecHelpers
 

data/thingfish/plugin_templates/spec/lib/metastore_behavior.rb

 	$LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
 }
 
-require 'spec'
-require 'spec/lib/constants'
+require 'rspec'
+
 require 'spec/lib/helpers'
 
 require 'thingfish'

data/thingfish/plugin_templates/spec/thingfish/filestore/TEMPLATE_spec.rb.erb

 	$LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
 }
 
-require 'spec'
-require 'spec/lib/constants'
+require 'rspec'
+
 require 'spec/lib/helpers'
 require 'spec/lib/filestore_behavior'
 
 include ThingFish::Constants
 
 describe ThingFish::<%= @name.capitalize %>FileStore do
-	include ThingFish::SpecHelpers
 
 	before( :all ) do
 		setup_logging( :fatal )

data/thingfish/plugin_templates/spec/thingfish/filter/TEMPLATE_spec.rb.erb

 	$LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
 }
 
-require 'spec'
-require 'spec/lib/constants'
+require 'rspec'
+
 require 'spec/lib/helpers'
 require 'spec/lib/filter_behavior'
 
 include ThingFish::Constants
 
 describe ThingFish::<%= @name.capitalize %><%= @type.capitalize %> do
-	include ThingFish::SpecHelpers
 
 	before( :all ) do
 		setup_logging( :fatal )
 	end
 
 	### Shared behaviors
-	it_should_behave_like "A Filter"
+	it_should_behave_like "a filter"
 
 	### Implementation-specific Examples
 	it "is well tested"

data/thingfish/plugin_templates/spec/thingfish/handler/TEMPLATE_spec.rb.erb

 	$LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
 }
 
-require 'spec'
-require 'spec/lib/constants'
+require 'rspec'
+
 require 'spec/lib/helpers'
 require 'spec/lib/handler_behavior'
 
 #####################################################################
 
 describe ThingFish::<%= @name.capitalize %>Handler do
-	include ThingFish::SpecHelpers
 
 	before( :all ) do
 		setup_logging( :fatal )
 	before(:each) do
 		resdir = @basedir + 'resources'
 	    @handler  = ThingFish::Handler.create( '<%= @name %>', 'resource_dir' => resdir )
-		@request  = mock( "request", :null_object => true )
-		@response = mock( "response", :null_object => true )
+		@request  = mock( "request" ).as_null_object
+		@response = mock( "response" ).as_null_object
 
-		@request_headers  = mock( "request headers", :null_object => true )
+		@request_headers  = mock( "request headers" ).as_null_object
 		@request.stub!( :headers ).and_return( @request_headers )
-		@response_headers  = mock( "response headers", :null_object => true )
+		@response_headers  = mock( "response headers" ).as_null_object
 		@response.stub!( :headers ).and_return( @response_headers )
-		@response_data  = mock( "response data", :null_object => true )
+		@response_data  = mock( "response data" ).as_null_object
 		@response.stub!( :data ).and_return( @response_data )
 
-		@daemon = mock( "daemon object", :null_object => true )
+		@daemon = mock( "daemon object" ).as_null_object
 		@handler.on_startup( @daemon )
 	end
 
 	
 
 	### Shared behaviors
-	it_should_behave_like "A Handler"
+	it_should_behave_like "a handler"
 
 	
 	### Implementation-specific Examples

data/thingfish/plugin_templates/spec/thingfish/metastore/TEMPLATE_spec.rb.erb

 	$LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
 }
 
-require 'spec'
-require 'spec/lib/constants'
+require 'rspec'
+
 require 'spec/lib/helpers'
 require 'spec/lib/metastore_behavior'
 

docs/manual/src/Hackers_Guide/writing-filters.page

 		return {
 			'version'   => yaml_rb_version,
 			'supports'  => supported_yaml_version,
-			'rev'       => VCSRev.match( /: (\w+)/ )[1] || 0,
+			'rev'       => VCSRev[ /: (\w+)/, 1 ] || 0,
 			'accepts'   => [YAML_MIMETYPE],
 			'generates' => [YAML_MIMETYPE]
 		}
 		return {
 			'version'   => [1,0],
 			'supports'  => Mp3Info::VERSION.split('.'),
-			'rev'       => VCSRev.match( /: (\w+)/ )[1] || 0,
+			'rev'       => VCSRev[ /: (\w+)/, 1 ] || 0,
 			'accepts'   => accepts,
 			'generates' => [],
 		  }
 		return {
 			'version'   => ruby_version,
 			'supports'  => supported_marshal_version,
-			'rev'       => VCSRev.match( /: (\w+)/ )[1] || 0,
+			'rev'       => VCSRev[ /: (\w+)/, 1 ] || 0,
 			'accepts'   => mimetypes,
 			'generates' => mimetypes,
 		  }
 	def self::version_string( include_buildnum=false )
 		vstring = "%s %s" % [ self.name, VERSION ]
 		if include_buildnum
-			build = VCSRev.match( /: (\w+)/ )[1] || 0
+			build = VCSRev[ /: (\w+)/, 1 ] || 0
 			vstring << " (build %s)" % [ build ]
 		end
 		return vstring

lib/thingfish/behavior/advanced_metastore.rb

+#!/usr/bin/env ruby
+
+require 'rspec'
+
+require 'thingfish'
+require 'thingfish/behavior/metastore'
+require 'thingfish/constants'
+require 'thingfish/metastore'
+
+
+share_examples_for "an advanced metastore" do
+
+	it_should_behave_like "a metastore"
+
+	TEST_TITLE = 'Wearable the Walleye Goes It Alone'
+
+
+	context "set-fetching API" do
+
+		before( :each ) do
+
+			@uuids = []
+			@uuid_titles = {}
+
+			20.times do |i|
+				uuid = UUIDTools::UUID.timestamp_create
+				title = "%s (%s)" % [ TEST_TITLE, Digest::MD5.hexdigest(uuid) ]
+
+				metastore.set_property( uuid,  :title, title )
+				metastore.set_property( uuid,  :bitrate, '160' )
+				metastore.set_property( uuid,  :namespace, 'devlibrary' )
+
+				@uuids << uuid.to_s
+				@uuid_titles[ uuid.to_s ] = title
+			end
+		end
+
+		it "can fetch a set of resources in natural order with no limit" do
+			results = metastore.get_resource_set
+
+			results.should have( 20 ).members
+
+			results.collect {|tuple| tuple[0] }.should include( *@uuids )
+			results.collect {|tuple| tuple[1][:namespace] }.uniq.should == ['devlibrary']
+			results.collect {|tuple| tuple[1][:title] }.should include( *@uuid_titles.values )
+			results.collect {|tuple| tuple[1][:bitrate] }.uniq.should == ['160']
+		end
+
+		it "can fetch a set of resources in natural order with a limit" do
+			results = metastore.get_resource_set( [], 5 )
+
+			results.should have( 5 ).members
+
+			results.collect {|tuple| tuple[0] }.should == @uuids[ 0, 5 ]
+			results.collect {|tuple| tuple[1][:namespace] }.uniq.should == ['devlibrary']
+			results.collect {|tuple| tuple[1][:title] }.
+				should == @uuid_titles.values_at( *@uuids[0, 5] )
+			results.collect {|tuple| tuple[1][:bitrate] }.uniq.should == ['160']
+		end
+
+		it "can fetch a set of resources in natural order with a limit and offset" do
+			results = metastore.get_resource_set( [], 5, 5 )
+
+			results.should have( 5 ).members
+
+			results.collect {|tuple| tuple[0] }.should == @uuids[ 5, 5 ]
+			results.collect {|tuple| tuple[1][:namespace] }.uniq.should == ['devlibrary']
+			results.collect {|tuple| tuple[1][:title] }.
+				should == @uuid_titles.values_at( *@uuids[5, 5] )
+			results.collect {|tuple| tuple[1][:bitrate] }.uniq.should == ['160']
+		end
+
+		it "can fetch a set of resources in natural order with an offset, but no limit" do
+			results = metastore.get_resource_set( [], 0, 5 )
+
+			results.should have( 15 ).members
+
+			results.collect {|tuple| tuple[0] }.should == @uuids[ 5..-1 ]
+			results.collect {|tuple| tuple[1][:namespace] }.uniq.should == ['devlibrary']
+			results.collect {|tuple| tuple[1][:title] }.
+				should == @uuid_titles.values_at( *@uuids[5..-1] )
+			results.collect {|tuple| tuple[1][:bitrate] }.uniq.should == ['160']
+		end
+
+		it "can fetch a set of resources in sorted order with no limit" do
+			results = metastore.get_resource_set( [:title] )
+
+			results.should have( 20 ).members
+
+			results.collect {|tuple| tuple[0] }.should include( *@uuids )
+			results.collect {|tuple| tuple[1][:namespace] }.uniq.should == ['devlibrary']
+			results.collect {|tuple| tuple[1][:title] }.should == @uuid_titles.values.sort
+			results.collect {|tuple| tuple[1][:bitrate] }.uniq.should == ['160']
+		end
+
+		it "can fetch a set of resources in sorted order with a limit" do
+			results = metastore.get_resource_set( [:title], 5 )
+
+			results.should have( 5 ).members
+
+			expected_titles = @uuid_titles.values.sort[ 0, 5 ]
+			title_uuids = @uuid_titles.invert.values_at( *expected_titles )
+
+			results.collect {|tuple| tuple[0] }.should == title_uuids
+			results.collect {|tuple| tuple[1][:namespace] }.uniq.should == ['devlibrary']
+			results.collect {|tuple| tuple[1][:title] }.should == expected_titles
+			results.collect {|tuple| tuple[1][:bitrate] }.uniq.should == ['160']
+		end
+
+		it "can fetch a set of resources in sorted order with a limit and offset" do
+			results = metastore.get_resource_set( [:title], 5, 5 )
+
+			results.should have( 5 ).members
+
+			expected_titles = @uuid_titles.values.sort[ 5, 5 ]
+			title_uuids = @uuid_titles.invert.values_at( *expected_titles )
+
+			results.collect {|tuple| tuple[0] }.should == title_uuids
+			results.collect {|tuple| tuple[1][:namespace] }.uniq.should == ['devlibrary']
+			results.collect {|tuple| tuple[1][:title] }.should == expected_titles
+			results.collect {|tuple| tuple[1][:bitrate] }.uniq.should == ['160']
+		end
+
+		it "can fetch a set of resources in sorted order with an offset, but no limit" do
+			results = metastore.get_resource_set( [:title], 0, 5 )
+
+			results.should have( 15 ).members
+
+			expected_titles = @uuid_titles.values.sort[ 5..-1 ]
+			title_uuids = @uuid_titles.invert.values_at( *expected_titles )
+
+			results.collect {|tuple| tuple[0] }.should == title_uuids
+			results.collect {|tuple| tuple[1][:namespace] }.uniq.should == ['devlibrary']
+			results.collect {|tuple| tuple[1][:title] }.should == expected_titles
+			results.collect {|tuple| tuple[1][:bitrate] }.uniq.should == ['160']
+		end
+
+	end
+
+end
+

lib/thingfish/behavior/filestore.rb

+#!/usr/bin/env ruby
+
+require 'rspec'
+require 'stringio'
+
+require 'thingfish'
+require 'thingfish/filestore'
+require 'thingfish/testconstants'
+
+
+share_examples_for "a filestore" do
+
+	include ThingFish::TestConstants
+
+	let( :testio ) do
+		StringIO.new( TEST_RESOURCE_CONTENT )
+	end
+
+	it "returns nil for non-existant entry" do
+	    filestore.fetch( TEST_UUID ).should == nil
+	end
+
+	it "returns data for existing entries" do
+	    filestore.store_io( TEST_UUID, testio )
+		filestore.fetch_io( TEST_UUID ) do |io|
+			io.read.should == TEST_RESOURCE_CONTENT
+		end
+	end
+
+	it "returns data for existing entries stored by UUID object" do
+	    filestore.store_io( TEST_UUID_OBJ, testio )
+		filestore.fetch_io( TEST_UUID ) do |io|
+			io.read.should == TEST_RESOURCE_CONTENT
+		end
+	end
+
+	it "returns data for existing entries fetched by UUID object" do
+	    filestore.store_io( TEST_UUID, testio )
+		filestore.fetch_io( TEST_UUID_OBJ ) do |io|
+			io.read.should == TEST_RESOURCE_CONTENT
+		end
+	end
+
+	it "deletes requested items" do
+		filestore.store( TEST_UUID, TEST_RESOURCE_CONTENT )
+		filestore.delete( TEST_UUID ).should be_true
+		filestore.fetch( TEST_UUID ).should be_nil
+	end
+
+	it "deletes requested items by UUID object" do
+		filestore.store( TEST_UUID, TEST_RESOURCE_CONTENT )
+		filestore.delete( TEST_UUID_OBJ ).should be_true
+		filestore.fetch( TEST_UUID ).should be_nil
+	end
+
+	it "silently ignores deletes of non-existant keys" do
+		filestore.delete( 'porksausage' ).should be_false
+	end
+
+	it "returns false when checking has_file? for a file it does not have" do
+		filestore.has_file?( TEST_UUID ).should be_false
+	end
+
+	it "returns true when checking has_file? for a file it has" do
+		filestore.store( TEST_UUID, TEST_RESOURCE_CONTENT )
+		filestore.has_file?( TEST_UUID ).should be_true
+	end
+
+	it "returns true when checking has_file? for a file it has by UUID object" do
+		filestore.store( TEST_UUID, TEST_RESOURCE_CONTENT )
+		filestore.has_file?( TEST_UUID_OBJ ).should be_true
+	end
+
+	it "returns false when checking has_file? for a file which has been deleted" do
+		filestore.store( TEST_UUID, TEST_RESOURCE_CONTENT )
+		filestore.delete( TEST_UUID )
+		filestore.has_file?( TEST_UUID ).should be_false
+	end
+
+	it "knows what the size of any of its stored resources is" do
+		filestore.store( TEST_UUID, TEST_RESOURCE_CONTENT )
+		filestore.size( TEST_UUID ).should == TEST_RESOURCE_CONTENT.length
+		filestore.size( TEST_UUID_OBJ ).should == TEST_RESOURCE_CONTENT.length
+	end
+
+	it "returns nil when asked for the size of a resource it doesn't contain" do
+		filestore.size( TEST_UUID ).should == nil
+	end
+
+end
+

lib/thingfish/behavior/filter.rb

+#!/usr/bin/env ruby
+
+require 'rspec'
+
+require 'thingfish'
+require 'thingfish/filter'
+require 'thingfish/testconstants'
+
+
+share_examples_for "a filter" do
+	include ThingFish::TestConstants
+
+	it "knows what types it handles" do
+		filter.handled_types.should be_an_instance_of( Array )
+		filter.handled_types.each {|f| f.should be_an_instance_of(ThingFish::AcceptParam) }
+	end
+
+	it "provides information about itself through its introspection interface" do
+		filter.info.should be_an_instance_of( Hash )
+		filter.info.should have_key( 'version' )
+		filter.info['version'].should be_an_instance_of( Array )
+		filter.info.should have_key( 'accepts' )
+		filter.info['accepts'].should be_an_instance_of( Array )
+		filter.info.should have_key( 'generates' )
+		filter.info['generates'].should be_an_instance_of( Array )
+		filter.info.should have_key( 'supports' )
+		filter.info['supports'].should be_an_instance_of( Array )
+		filter.info.should have_key( 'rev' )
+		filter.info['rev'].to_s.should =~ /^[[:xdigit:]]+$/
+	end
+
+
+	it "knows what its plugin name is" do
+		described_class.plugin_name.should be_an_instance_of( String )
+	end
+
+end
+

lib/thingfish/behavior/handler.rb

+#!/usr/bin/env ruby
+
+require 'rspec'
+
+require 'thingfish'
+require 'thingfish/handler'
+require 'thingfish/testconstants'
+
+
+share_examples_for "a handler" do
+	include ThingFish::TestConstants
+
+end
+

lib/thingfish/behavior/metastore.rb

+#!/usr/bin/env ruby
+
+require 'rspec'
+
+require 'thingfish'
+require 'thingfish/metastore'
+require 'thingfish/testconstants'
+
+
+share_examples_for "a metastore" do
+	include ThingFish::TestConstants
+
+	unless defined?( TEST_DUMPSTRUCT )
+		TEST_DUMPSTRUCT = {
+			ThingFish::TestConstants::TEST_UUID => {
+				:title => ThingFish::TestConstants::TEST_TITLE,
+				:bitrate => '160',
+				:namespace => 'devlibrary',
+			},
+
+			ThingFish::TestConstants::TEST_UUID2 => {
+				:title => ThingFish::TestConstants::TEST_TITLE,
+				:namespace => 'private',
+			},
+		}
+	end
+
+
+	it "can set and get a property belonging to a UUID" do
+		metastore.set_property( TEST_UUID, TEST_PROP, TEST_PROPVALUE )
+		metastore.get_property( TEST_UUID, TEST_PROP ).should == TEST_PROPVALUE
+	end
+
+	it "can set a property belonging to a UUID object" do
+		metastore.set_property( TEST_UUID_OBJ, TEST_PROP, TEST_PROPVALUE )
+		metastore.get_property( TEST_UUID, TEST_PROP ).should == TEST_PROPVALUE
+	end
+
+	it "can set multiple properties for a UUID" do
+		metastore.set_property( TEST_UUID, 'cake', 'is a lie' )
+		props = {
+			TEST_PROP  => TEST_PROPVALUE,
+			TEST_PROP2 => TEST_PROPVALUE2
+		}
+		metastore.set_properties( TEST_UUID, props )
+		metastore.get_property( TEST_UUID, TEST_PROP ).should  == TEST_PROPVALUE
+		metastore.get_property( TEST_UUID, TEST_PROP2 ).should == TEST_PROPVALUE2
+		metastore.get_property( TEST_UUID, 'cake' ).should be_nil()
+	end
+
+	it "can set update properties for a UUID" do
+		metastore.set_property( TEST_UUID, 'cake', 'is a lie' )
+		props = {
+			TEST_PROP  => TEST_PROPVALUE,
+			TEST_PROP2 => TEST_PROPVALUE2
+		}
+		metastore.update_properties( TEST_UUID, props )
+		metastore.get_property( TEST_UUID, TEST_PROP ).should  == TEST_PROPVALUE
+		metastore.get_property( TEST_UUID, TEST_PROP2 ).should == TEST_PROPVALUE2
+		metastore.get_property( TEST_UUID, 'cake' ).should == 'is a lie'
+	end
+
+	it "can get a property belonging to a UUID object" do
+		metastore.set_property( TEST_UUID, TEST_PROP, TEST_PROPVALUE )
+		metastore.get_property( TEST_UUID_OBJ, TEST_PROP ).should == TEST_PROPVALUE
+	end
+
+	it "can test whether or not a property exists for a UUID" do
+		metastore.has_property?( TEST_UUID, TEST_PROP ).should be_false
+		metastore.set_property( TEST_UUID, TEST_PROP, TEST_PROPVALUE )
+		metastore.has_property?( TEST_UUID, TEST_PROP ).should be_true
+	end
+
+	it "can test whether or not a UUID has any metadata stored" do
+		metastore.has_uuid?( TEST_UUID ).should be_false
+		metastore.set_property( TEST_UUID_OBJ, TEST_PROP, TEST_PROPVALUE )
+		metastore.has_uuid?( TEST_UUID ).should be_true
+	end
+
+	it "can test whether or not a UUID (as an object) has any metadata stored" do
+		metastore.has_uuid?( TEST_UUID_OBJ ).should be_false
+		metastore.set_property( TEST_UUID, TEST_PROP, TEST_PROPVALUE )
+		metastore.has_uuid?( TEST_UUID_OBJ ).should be_true
+	end
+
+	it "can test whether or not a property exists for a UUID object" do
+		metastore.has_property?( TEST_UUID_OBJ, TEST_PROP ).should be_false
+		metastore.set_property( TEST_UUID, TEST_PROP, TEST_PROPVALUE )
+		metastore.has_property?( TEST_UUID_OBJ, TEST_PROP ).should be_true
+	end
+
+	it "can return the entire set of properties for a UUID" do
+		metastore.get_properties( TEST_UUID ).should be_empty
+		metastore.set_property( TEST_UUID, TEST_PROP, TEST_PROPVALUE )
+		metastore.set_property( TEST_UUID, TEST_PROP2, TEST_PROPVALUE2 )
+		metastore.get_properties( TEST_UUID ).should have_key( TEST_PROP.to_sym )
+		metastore.get_properties( TEST_UUID ).should have_key( TEST_PROP2.to_sym )
+	end
+
+	it "can get the entire set of properties for a UUID object" do
+		metastore.get_properties( TEST_UUID_OBJ ).should be_empty
+		metastore.set_property( TEST_UUID, TEST_PROP, TEST_PROPVALUE )
+		metastore.set_property( TEST_UUID, TEST_PROP2, TEST_PROPVALUE2 )
+		metastore.get_properties( TEST_UUID_OBJ ).should have_key( TEST_PROP.to_sym )
+		metastore.get_properties( TEST_UUID_OBJ ).should have_key( TEST_PROP2.to_sym )
+	end
+
+	it "can set the entire set of properties for a UUID object" do
+		metastore.get_properties( TEST_UUID ).should be_empty
+		metastore.set_property( TEST_UUID_OBJ, TEST_PROP, TEST_PROPVALUE )
+		metastore.set_property( TEST_UUID_OBJ, TEST_PROP2, TEST_PROPVALUE2 )
+		metastore.get_properties( TEST_UUID ).should have_key( TEST_PROP.to_sym )
+		metastore.get_properties( TEST_UUID ).should have_key( TEST_PROP2.to_sym )
+	end
+
+	it "can remove a property for a UUID" do
+		metastore.set_property( TEST_UUID, TEST_PROP, TEST_PROPVALUE )
+		metastore.set_property( TEST_UUID, TEST_PROP2, TEST_PROPVALUE2 )
+		metastore.delete_property( TEST_UUID, TEST_PROP )
+		metastore.has_property?( TEST_UUID, TEST_PROP ).should be_false
+		metastore.has_property?( TEST_UUID, TEST_PROP2 ).should be_true
+	end
+
+	it "can remove a property for a UUID object" do
+		metastore.set_property( TEST_UUID, TEST_PROP, TEST_PROPVALUE )
+		metastore.set_property( TEST_UUID, TEST_PROP2, TEST_PROPVALUE2 )
+		metastore.delete_property( TEST_UUID_OBJ, TEST_PROP )
+		metastore.has_property?( TEST_UUID, TEST_PROP ).should be_false
+		metastore.has_property?( TEST_UUID, TEST_PROP2 ).should be_true
+	end
+
+	it "can remove all properties for a UUID" do
+		metastore.set_property( TEST_UUID_OBJ, TEST_PROP, TEST_PROPVALUE )
+		metastore.set_property( TEST_UUID_OBJ, TEST_PROP2, TEST_PROPVALUE2 )
+		metastore.delete_resource( TEST_UUID )
+		metastore.has_property?( TEST_UUID_OBJ, TEST_PROP ).should be_false
+		metastore.has_property?( TEST_UUID_OBJ, TEST_PROP2 ).should be_false
+	end
+
+	it "can remove a set of properties for a UUID" do
+		metastore.set_property( TEST_UUID_OBJ, TEST_PROP, TEST_PROPVALUE )
+		metastore.set_property( TEST_UUID_OBJ, TEST_PROP2, TEST_PROPVALUE2 )
+		metastore.delete_properties( TEST_UUID, TEST_PROP )
+		metastore.has_property?( TEST_UUID_OBJ, TEST_PROP ).should be_false
+		metastore.has_property?( TEST_UUID_OBJ, TEST_PROP2 ).should be_true
+	end
+
+	it "can remove a set of properties for a UUID object" do
+		metastore.set_property( TEST_UUID, TEST_PROP, TEST_PROPVALUE )
+		metastore.set_property( TEST_UUID, TEST_PROP2, TEST_PROPVALUE2 )
+		metastore.delete_properties( TEST_UUID_OBJ, TEST_PROP2 )
+		metastore.has_property?( TEST_UUID, TEST_PROP ).should be_true
+		metastore.has_property?( TEST_UUID, TEST_PROP2 ).should be_false
+	end
+
+	it "can remove all properties for a UUID object" do
+		metastore.set_property( TEST_UUID, TEST_PROP, TEST_PROPVALUE )
+		metastore.set_property( TEST_UUID, TEST_PROP2, TEST_PROPVALUE2 )
+		metastore.delete_resource( TEST_UUID_OBJ )
+		metastore.has_property?( TEST_UUID, TEST_PROP ).should be_false
+		metastore.has_property?( TEST_UUID, TEST_PROP2 ).should be_false
+	end
+
+	it "can fetch all keys for all properties in the store" do
+		metastore.set_property( TEST_UUID, TEST_PROP, TEST_PROPVALUE )
+		metastore.set_property( TEST_UUID, TEST_PROP2, TEST_PROPVALUE2 )
+		metastore.get_all_property_keys.should include( TEST_PROP.to_sym )
+		metastore.get_all_property_keys.should include( TEST_PROP2.to_sym )
+	end
+
+
+	it "can fetch all values for a given key in the store" do
+		metastore.set_property( TEST_UUID, TEST_PROP, TEST_PROPVALUE )
+		metastore.set_property( TEST_UUID2, TEST_PROP, TEST_PROPVALUE2 )
+		metastore.set_property( TEST_UUID3, TEST_PROP, TEST_PROPVALUE2 )
+		metastore.set_property( TEST_UUID, TEST_PROP2, TEST_PROPVALUE2 )
+		metastore.get_all_property_values( TEST_PROP ).should include( TEST_PROPVALUE )
+		metastore.get_all_property_values( TEST_PROP ).should include( TEST_PROPVALUE2 )
+	end
+
+
+	# Searching APIs
+
+	it "can find tuples by single-property exact match" do
+		metastore.set_property( TEST_UUID, :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID2, :title, 'Squonk the Sea-Ranger' )
+
+		found = metastore.find_by_exact_properties( 'title' => TEST_TITLE )
+
+		found.should have(1).members
+		uuid, properties = found.first
+
+		uuid.should == TEST_UUID
+		properties[:title].should == TEST_TITLE
+	end
+
+
+	it "can find tuples by single-property case insensitive exact match" do
+		metastore.set_property( TEST_UUID, :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID2, :title, 'Squonk the Sea-Ranger' )
+
+		found = metastore.find_by_exact_properties( 'title' => TEST_TITLE.downcase )
+
+		found.should have(1).members
+		uuid, properties = found.first
+
+		uuid.should == TEST_UUID
+		properties[:title].should == TEST_TITLE
+	end
+
+
+	it "can find ordered tuples by single-property exact match" do
+		metastore.set_property( TEST_UUID2, :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID2, :namespace, 'private' )
+		metastore.set_property( TEST_UUID2, :description, 'Another description.' )
+
+		metastore.set_property( TEST_UUID,  :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID,  :namespace, 'devlibrary' )
+		metastore.set_property( TEST_UUID,  :description, 'This is a description.' )
+
+		found = metastore.find_by_exact_properties( {'title' => TEST_TITLE}, ['description'] )
+
+		found.should have(2).members
+		found.first[0] == TEST_UUID2
+		found.last[0]  == TEST_UUID
+	end
+
+
+	it "returns tuples in UUID order from a single-property exact match with no other order attributes" do
+		metastore.set_property( TEST_UUID2, :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID2, :namespace, 'private' )
+		metastore.set_property( TEST_UUID2, :description, 'Another description.' )
+
+		metastore.set_property( TEST_UUID,  :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID,  :namespace, 'devlibrary' )
+		metastore.set_property( TEST_UUID,  :description, 'This is a description.' )
+
+		found = metastore.find_by_exact_properties( {'title' => TEST_TITLE}, [] )
+
+		found.should have(2).members
+		found.map {|tuple| tuple.first }.should == [ TEST_UUID, TEST_UUID2 ].sort
+	end
+
+
+	it "can return a subset of found exact-match tuples" do
+		metastore.set_property( TEST_UUID,  :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID,  :namespace, 'devlibrary' )
+
+		metastore.set_property( TEST_UUID2, :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID2, :namespace, 'private' )
+
+		found = metastore.find_by_exact_properties( {'title' => TEST_TITLE}, [:namespace], 1 )
+
+		found.should have(1).members
+		uuid, properties = found.first
+
+		uuid.should == TEST_UUID
+		properties[:title].should == TEST_TITLE
+		properties[:namespace].should == 'devlibrary'
+	end
+
+
+	it "can return an offset subset of found exact-match tuples" do
+		uuids = []
+		20.times do |i|
+			uuid = UUIDTools::UUID.timestamp_create
+			uuids << uuid
+			title = TEST_TITLE + " (Episode %02d)" % [ i + 1 ]
+			metastore.set_property( uuid, :title, title )
+			metastore.set_property( uuid, :namespace, 'devlibrary' )
+		end
+
+		0.step( 15, 5 ) do |offset|
+			found = metastore.find_by_exact_properties( {'namespace' => 'devlibrary'}, [:title], 5, offset )
+
+			found.should have( 5 ).members
+			uuid, props = *found.first
+			uuid.should == uuids[ offset ].to_s
+			props[:title].should == TEST_TITLE + " (Episode %02d)" % [ offset + 1 ]
+		end
+	end
+
+
+	it "returns an empty array for offsets that exceed the number of results" do
+		metastore.set_property( TEST_UUID,  :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID,  :namespace, 'devlibrary' )
+
+		metastore.set_property( TEST_UUID2, :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID2, :namespace, 'private' )
+
+		found = metastore.find_by_exact_properties( {'namespace' => 'private'}, [], 10, 1 )
+
+		found.should be_empty()
+	end
+
+
+	it "can find tuples by multi-property exact match" do
+		metastore.set_property( TEST_UUID,  :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID,  :namespace, 'devlibrary' )
+
+		metastore.set_property( TEST_UUID2, :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID2, :namespace, 'private' )
+
+		found = metastore.find_by_exact_properties(
+		 	'title'    => TEST_TITLE,
+			'namespace' => 'devlibrary'
+		  )
+
+		found.should have(1).members
+		uuid, properties = found.first
+
+		uuid.should == TEST_UUID
+		properties[:title].should == TEST_TITLE
+		properties[:namespace].should == 'devlibrary'
+	end
+
+
+	it "can find tuples by single-property wildcard match" do
+		metastore.set_property( TEST_UUID, :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID2, :title, 'Squonk the Sea-Ranger' )
+
+		found = metastore.find_by_matching_properties( 'title' => 'Muffin*' )
+
+		found.should have(1).members
+		uuid, properties = found.first
+
+		uuid.should == TEST_UUID
+		properties[:title].should == TEST_TITLE
+	end
+
+
+	it "can find tuples by single-property case insensitive wildcard match" do
+		metastore.set_property( TEST_UUID, :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID2, :title, 'Squonk the Sea-Ranger' )
+
+		found = metastore.find_by_matching_properties( 'title' => 'muffin*' )
+
+		found.should have(1).members
+		uuid, properties = found.first
+
+		uuid.should == TEST_UUID
+		properties[:title].should == TEST_TITLE
+	end
+
+
+	it "can find tuples by multi-property wildcard match" do
+		metastore.set_property( TEST_UUID,  :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID,  :bitrate, '160' )
+		metastore.set_property( TEST_UUID,  :namespace, 'devlibrary' )
+
+		metastore.set_property( TEST_UUID2, :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID2, :namespace, 'private' )
+
+		found = metastore.find_by_matching_properties(
+		 	'title'     => '*panda*',
+			'namespace' => 'dev*',
+			'bitrate'   => 160
+		  )
+
+		found.should have(1).members
+		uuid, properties = found.first
+
+		uuid.should == TEST_UUID
+		properties[:title].should == TEST_TITLE
+		properties[:namespace].should == 'devlibrary'
+		properties[:bitrate].should == '160'
+	end
+
+
+	it "can find tuples by multi-property wildcard match with multiple values for a single key" do
+		metastore.set_property( TEST_UUID,  :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID,  :bitrate, '160' )
+		metastore.set_property( TEST_UUID,  :namespace, 'devlibrary' )
+
+		metastore.set_property( TEST_UUID2, :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID2, :namespace, 'private' )
+
+		found = metastore.find_by_matching_properties(
+		 	'title'     => '*panda*',
+			'namespace' => ['dev*', '*y'],
+			'bitrate'   => 160
+		  )
+
+		found.should have(1).members
+		uuid, properties = found.first
+
+		uuid.should == TEST_UUID
+		properties[:title].should == TEST_TITLE
+		properties[:namespace].should == 'devlibrary'
+		properties[:bitrate].should == '160'
+	end
+
+
+	it "can iterate over all of its resources, yielding them as uuid/prophash pairs" do
+		metastore.set_property( TEST_UUID,  :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID,  :bitrate, '160' )
+		metastore.set_property( TEST_UUID,  :namespace, 'devlibrary' )
+
+		metastore.set_property( TEST_UUID2, :title, TEST_TITLE )
+		metastore.set_property( TEST_UUID2, :namespace, 'private' )
+
+		results = {}
+		metastore.each_resource do |uuid, properties|
+			results[ uuid ] = properties
+		end
+
+		results.should have(2).members
+		results.keys.should include( TEST_UUID, TEST_UUID2 )
+		results[ TEST_UUID ].keys.should have_at_least(3).members
+		results[ TEST_UUID ][:title].should == TEST_TITLE
+		results[ TEST_UUID ][:bitrate].should == '160'
+		results[ TEST_UUID ][:namespace].should == 'devlibrary'
+
+		results[ TEST_UUID2 ].keys.should have_at_least(2).members
+		results[ TEST_UUID2 ][:title].should == TEST_TITLE
+		results[ TEST_UUID2 ][:namespace].should == 'private'
+	end
+
+	describe "import/export/migration API" do
+
+		it "can dump the contents of the store as a Hash" do
+			metastore.set_property( TEST_UUID,  :title, TEST_TITLE )
+			metastore.set_property( TEST_UUID,  :bitrate, '160' )
+			metastore.set_property( TEST_UUID,  :namespace, 'devlibrary' )
+
+			metastore.set_property( TEST_UUID2, :title, TEST_TITLE )
+			metastore.set_property( TEST_UUID2, :namespace, 'private' )
+
+			dumpstruct = metastore.transaction { metastore.dump_store }
+
+			dumpstruct.should be_an_instance_of( Hash )
+			# dumpstruct.keys.should == [ TEST_UUID, TEST_UUID2 ] # For debugging only
+			dumpstruct.keys.should have(2).members
+			dumpstruct.keys.should include( TEST_UUID, TEST_UUID2 )
+
+			dumpstruct[ TEST_UUID ].should be_an_instance_of( Hash )
+			dumpstruct[ TEST_UUID ].keys.should have_at_least(3).members
+			dumpstruct[ TEST_UUID ].keys.should include( :title, :bitrate, :namespace )
+			dumpstruct[ TEST_UUID ].values.should include( TEST_TITLE, '160', 'devlibrary' )
+
+			dumpstruct[ TEST_UUID2 ].should be_an_instance_of( Hash )
+			dumpstruct[ TEST_UUID2 ].keys.should have_at_least(2).members
+			dumpstruct[ TEST_UUID2 ].keys.should include( :title, :namespace )
+			dumpstruct[ TEST_UUID2 ].values.should include( TEST_TITLE, 'private' )
+		end
+
+		it "clears any current metadata when loading a metastore dump" do
+			metastore.set_property( TEST_UUID,  :title, TEST_TITLE )
+			metastore.set_property( TEST_UUID,  :bitrate, '160' )
+			metastore.set_property( TEST_UUID,  :namespace, 'devlibrary' )
+
+			metastore.set_property( TEST_UUID2, :title, TEST_TITLE )
+			metastore.set_property( TEST_UUID2, :namespace, 'private' )
+
+			metastore.transaction { metastore.load_store({}) }
+
+			metastore.should_not have_uuid( TEST_UUID )
+			metastore.should_not have_uuid( TEST_UUID2 )
+		end
+
+		it "can load the store from a Hash" do
+			metastore.transaction { metastore.load_store(TEST_DUMPSTRUCT) }
+
+			metastore.get_property( TEST_UUID,  :title ).should == TEST_TITLE
+			metastore.get_property( TEST_UUID,  :bitrate ).should == '160'
+			metastore.get_property( TEST_UUID,  :namespace ).should == 'devlibrary'
+
+			metastore.get_property( TEST_UUID2, :title ).should == TEST_TITLE
+			metastore.get_property( TEST_UUID2, :namespace ).should == 'private'
+		end
+
+
+		it "can migrate directly from another metastore" do
+			other_store = mock( "other metastore mock" )
+			expectation = other_store.should_receive( :migrate )
+			TEST_DUMPSTRUCT.each do |uuid, properties|
+				expectation.and_yield( uuid, properties )
+			end
+
+			metastore.migrate_from( other_store )
+
+			metastore.get_property( TEST_UUID,  :title ).should == TEST_TITLE
+			metastore.get_property( TEST_UUID,  :bitrate ).should == '160'
+			metastore.get_property( TEST_UUID,  :namespace ).should == 'devlibrary'
+
+			metastore.get_property( TEST_UUID2, :title ).should == TEST_TITLE
+			metastore.get_property( TEST_UUID2, :namespace ).should == 'private'
+		end
+
+		it "helps another metastore migrate its data" do
+			metastore.set_property( TEST_UUID,  :title, TEST_TITLE )
+			metastore.set_property( TEST_UUID,  :bitrate, '160' )
+			metastore.set_property( TEST_UUID,  :namespace, 'devlibrary' )
+
+			metastore.set_property( TEST_UUID2, :title, TEST_TITLE )
+			metastore.set_property( TEST_UUID2, :namespace, 'private' )
+
+			results = {}
+			metastore.migrate do |uuid, properties|
+				results[ uuid ] = properties
+			end
+
+			results.should have_at_least(2).members
+			results.keys.should include( TEST_UUID, TEST_UUID2 )
+			results[ TEST_UUID ].keys.should have_at_least(3).members
+			results[ TEST_UUID ][:title].should == TEST_TITLE
+			results[ TEST_UUID ][:bitrate].should == '160'
+			results[ TEST_UUID ][:namespace].should == 'devlibrary'
+
+			results[ TEST_UUID2 ].keys.should have_at_least(2).members
+			results[ TEST_UUID2 ][:title].should == TEST_TITLE
+			results[ TEST_UUID2 ][:namespace].should == 'private'
+		end
+
+	end
+
+
+	describe " (safety methods)" do
+
+		it "eliminates system attributes from properties passed through #set_safe_properties" do
+			metastore.set_properties( TEST_UUID, TEST_PROPSET )
+
+			metastore.set_safe_properties( TEST_UUID, 
+				:extent => "0",
+				:checksum => '',
+				TEST_PROP => 'something else'
+			  )
+
+			metastore.get_property( TEST_UUID, 'extent' ).should == TEST_PROPSET['extent']
+			metastore.get_property( TEST_UUID, 'checksum' ).should == TEST_PROPSET['checksum']
+			metastore.get_property( TEST_UUID, TEST_PROP ).should == 'something else'
+			metastore.get_property( TEST_UUID, TEST_PROP2 ).should be_nil()
+		end
+
+
+		it "eliminates system attributes from properties passed through #update_safe_properties" do
+			metastore.set_properties( TEST_UUID, TEST_PROPSET )
+
+			metastore.update_safe_properties( TEST_UUID, 
+				:extent => "0",
+				:checksum => '',
+				TEST_PROP => 'something else'
+			  )
+
+			metastore.get_property( TEST_UUID, 'extent' ).should == TEST_PROPSET['extent']
+			metastore.get_property( TEST_UUID, 'checksum' ).should == TEST_PROPSET['checksum']
+			metastore.get_property( TEST_UUID, TEST_PROP ).should == 'something else'
+		end
+
+
+		it "eliminates system attributes from properties passed through #delete_safe_properties" do
+			metastore.set_properties( TEST_UUID, TEST_PROPSET )
+
+			metastore.delete_safe_properties( TEST_UUID, :extent, :checksum, TEST_PROP )
+
+			metastore.should have_property( TEST_UUID, 'extent' )
+			metastore.should have_property( TEST_UUID, 'checksum' )
+			metastore.should_not have_property( TEST_UUID, TEST_PROP )
+		end
+
+
+		it "sets properties safely via #set_safe_property" do
+			metastore.set_safe_property( TEST_UUID, TEST_PROP, TEST_PROPVALUE )
+			metastore.get_property( TEST_UUID, TEST_PROP ).should == TEST_PROPVALUE			
+		end
+
+
+		it "refuses to update system properties via #set_safe_property" do
+			lambda {
+				metastore.set_safe_property( TEST_UUID, 'extent', 0 )
+			}.should raise_error( ThingFish::MetaStoreError, /used by the system/ )
+		end
+
+
+		it "deletes properties safely via #set_safe_property" do
+			metastore.set_property( TEST_UUID, TEST_PROP, TEST_PROPVALUE )
+			metastore.delete_safe_property( TEST_UUID, TEST_PROP )
+			metastore.has_property?( TEST_UUID, TEST_PROP ).should be_false			
+		end
+
+
+		it "refuses to delete system properties via #delete_safe_property" do
+			lambda {
+				metastore.delete_safe_property( TEST_UUID, 'extent' )
+			}.should raise_error( ThingFish::MetaStoreError, /used by the system/ )
+		end
+	end
+
+
+	# Transaction API
+
+	it "can execute a block in the context of a transaction" do
+		ran_block = false
+		metastore.transaction do
+			ran_block = true
+		end
+
+		ran_block.should be_true()
+	end
+
+
+end
+

lib/thingfish/client.rb

 	USER_AGENT_HEADER = "%s/%s.%s" % [
 		self.name.downcase.gsub(/\W+/, '-'),
 		ThingFish::VERSION,
-		VCSRev.match( /: (\w+)/ )[1] || 0
+		VCSRev[ /: (\w+)/, 1 ] || 0
 	]
 
 	# Maximum size of a resource response that's kept in-memory. Anything larger

lib/thingfish/filter/html.rb

 
 
 require 'thingfish'
+
 require 'thingfish/mixins'
 require 'thingfish/constants'
 require 'thingfish/acceptparam'
+require 'thingfish/filter'
 
 
 ### An HTML-conversion filter for ThingFish. It converts Ruby objects in the body
 		# Find the handlers that can make html
 		processor = response.handlers.last or raise "Oops! No processor for %s?!?" % [ request.uri.path ]
 		content = nil
-		
+
 		if processor.respond_to?( :make_html_content )
 			content = processor.make_html_content( response.body, request, response )
 		else
 		return {
 			'version'   => [1,0],
 			'supports'  => [],
-			'rev'       => VCSRev.match( /: (\w+)/ )[1] || 0,
+			'rev'       => VCSRev[ /: (\w+)/, 1 ] || 0,
 			'accepts'   => [],
 			'generates' => [CONFIGURED_HTML_MIMETYPE],
 		  }

lib/thingfish/filter/ruby.rb

 #!/usr/bin/env ruby
+
+
+require 'thingfish'
+require 'thingfish/mixins'
+require 'thingfish/constants'
+require 'thingfish/acceptparam'
+require 'thingfish/filter'
+
+
 #
-# A Marshalled Ruby object filter for ThingFish
+# A Ruby marshalled-object filter for ThingFish. It marshals and unmarshals Ruby objects in the
+# body of responses if the client sends a request with an `Accept:` header that includes
+# ThingFish::RUBY_MARSHALLED_MIMETYPE.
 #
 # == Synopsis
 #
 # * Michael Granger <ged@FaerieMUD.org>
 # * Mahlon E. Smith <mahlon@martini.nu>
 #
-# :include: LICENSE
-#
-#---
-#
-# Please see the file LICENSE in the top-level directory for licensing details.
-
-#
-
-
-require 'thingfish'
-require 'thingfish/mixins'
-require 'thingfish/constants'
-require 'thingfish/acceptparam'
-
-
-### A Ruby marshalled-object filter for ThingFish. It marshals and unmarshals Ruby objects in the
-### body of responses if the client sends a request with an `Accept:` header that includes
-### ThingFish::RUBY_MARSHALLED_MIMETYPE.
 class ThingFish::RubyFilter < ThingFish::Filter
 	include ThingFish::Loggable,
 		ThingFish::Constants
 		return {
 			'version'   => ruby_version,
 			'supports'  => supported_marshal_version,
-			'rev'       => VCSRev.match( /: (\w+)/ )[1] || 0,
+			'rev'       => VCSRev[ /: (\w+)/, 1 ] || 0,
 			'accepts'   => mimetypes,
 			'generates' => mimetypes,
 		  }

lib/thingfish/filter/yaml.rb

 #!/usr/bin/env ruby
-#
-# A YAML conversion filter for ThingFish
+
+require 'yaml'
+
+require 'thingfish'
+require 'thingfish/mixins'
+require 'thingfish/constants'
+require 'thingfish/acceptparam'
+require 'thingfish/filter'
+
+
+# A YAML-conversion filter for ThingFish. It converts Ruby objects in the body of responses
+# to YAML if the client accepts 'text/x-yaml'.
 #
 # == Synopsis
 #
 #---
 #
 # Please see the file LICENSE in the top-level directory for licensing details.
-
 #
-
-require 'yaml'
-
-require 'thingfish'
-require 'thingfish/mixins'
-require 'thingfish/constants'
-require 'thingfish/acceptparam'
-
-
-### A YAML-conversion filter for ThingFish. It converts Ruby objects in the body of responses
-### to YAML if the client accepts 'text/x-yaml'.
 class ThingFish::YAMLFilter < ThingFish::Filter
 	include ThingFish::Loggable,
-		ThingFish::Constants
+	        ThingFish::Constants
 
 	# VCS Revision
 	VCSRev = %q$Rev$
 		return {
 			'version'  => yaml_rb_version,
 			'supports' => supported_yaml_version,
-			'rev'       => VCSRev.match( /: (\w+)/ )[1] || 0,
+			'rev'       => VCSRev[ /: (\w+)/, 1 ] || 0,
 			'accepts'   => [YAML_MIMETYPE],
 			'generates' => [YAML_MIMETYPE],
 		}

lib/thingfish/handler/simplesearch.rb

 #!/usr/bin/env ruby
-#
-# A search handler for the thingfish daemon. This handler provides a REST
+
+require 'thingfish'
+require 'thingfish/constants'
+require 'thingfish/handler'
+require 'thingfish/metastore/simple'
+require 'thingfish/mixins'
+
+
+# A search handler for the ThingFish daemon. This handler provides a REST
 # interface to searching for resources which match criteria concerning their
 # associated metadata in a ThingFish::SimpleMetastore.
 #
 # Please see the file LICENSE in the top-level directory for licensing details.
 #
 #
-
-require 'thingfish'
-require 'thingfish/constants'
-require 'thingfish/handler'
-require 'thingfish/metastore/simple'
-require 'thingfish/mixins'
-
-
-### The search handler for the thingfish daemon when it's using a
-### ThingFish::SimpleMetaStore.
 class ThingFish::SimpleSearchHandler < ThingFish::Handler
 
 	include ThingFish::Constants,

lib/thingfish/handler/staticcontent.rb

 #!/usr/bin/env ruby
-#
+
+require 'pp'
+require 'thingfish'
+require 'thingfish/constants'
+require 'thingfish/handler'
+require 'thingfish/mixins'
+
+
 # The static resources handler -- serves static content from a directory. This is the
 # handler that's installed by the ThingFish::StaticResourcesHandler mixin ahead of
 # the including class in the URIspace.
 #---
 #
 # Please see the file LICENSE in the top-level directory for licensing details.
-
 #
-
-require 'pp'
-require 'thingfish'
-require 'thingfish/constants'
-require 'thingfish/handler'
-require 'thingfish/mixins'
-
-
-### The default top-level handler for the thingfish daemon
 class ThingFish::StaticContentHandler < ThingFish::Handler
 
 	include ThingFish::Constants,

lib/thingfish/metastore.rb

 	include Enumerable,
 	        PluginFactory,
 	        ThingFish::Loggable,
+	        ThingFish::Constants,
 	        ThingFish::AbstractClass,
 	        ThingFish::HtmlInspectableObject
 

lib/thingfish/mixins.rb

 #!/usr/bin/env ruby
-#
-# A collection of mixins shared between ThingFish classes
-#
-# == Version
-#
-#  $Id$
-#
-# == Authors
-#
-# * Michael Granger <ged@FaerieMUD.org>
-# * Mahlon E. Smith <mahlon@martini.nu>
-#
-# :include: LICENSE
-#
-#---
-#
-# Please see the file LICENSE in the top-level directory for licensing details.
-
-#
 
 require 'rbconfig'
 require 'erb'
 
 require 'thingfish'
 
+#---
+# A collection of mixins shared between ThingFish classes
+#
 
-module ThingFish # :nodoc:
+module ThingFish
 
 
 	### Adds a #log method to the including class which can be used to access the global
 				sym = :debug if @force_debug
 				ThingFish.logger.add( LEVEL[sym], msg, @classname, &block )
 			end
-			
+
 		end # ClassNameProxy
 
 		#########
 			### NotImplementedErrors when called via a concrete subclass.
 			def virtual( *syms )
 				syms.each do |sym|
-					define_method( sym ) {|*args|
+					define_method( sym ) do |*args|
 						raise ::NotImplementedError,
 							"%p does not provide an implementation of #%s" % [ self.class, sym ],
 							caller(1)
-					}
+					end
 				end
 			end
 

lib/thingfish/testconstants.rb

+#!/usr/bin/env ruby
+# coding: utf-8
+
+require 'thingfish'
+require 'thingfish/constants'
+require 'digest/md5'
+require 'ipaddr'
+require 'net/http'
+require 'net/protocol'
+
+module ThingFish::TestConstants
+	include ThingFish::Constants
+
+	unless defined?( TEST_CONTENT )
+		TEST_CONTENT = <<-'EOF'.gsub(/^\s*/, '')
+		<html>
+		<head>
+			<title>Test Index</title>
+		</head>
+		<body>
+			<h1>This is the test index content.</h1>
+		</body>
+		</html>
+		EOF
+
+		TEST_SERVER    		  = 'thingfish.laika.com'
+		TEST_SERVER_URI		  = 'http://thingfish.laika.com:5000/'
+		TEST_USERNAME		  = 'glimerrfeln'
+		TEST_PASSWORD		  = 'holz$dg21b,'
+		TEST_IP				  = '127.0.0.1'
+		TEST_PORT			  = 3443
+		TEST_RESOURCE_CONTENT = 'porn'
+		TEST_UUID			  = '60acc01e-cd82-11db-84d1-7ff059e49450'
+		TEST_UUID2			  = '06363890-c67f-11db-84a0-b7f0d178e52d'
+		TEST_UUID3			  = '26A4EBA6-1264-4269-AC0A-82A80C0FDB4D'
+		TEST_PROP			  = 'format'
+		TEST_PROPVALUE		  = 'application/json'
+		TEST_PROP2			  = 'turn_ons'
+		TEST_PROPVALUE2		  = 'long walks on the beach'
+		TEST_CHECKSUM		  = Digest::MD5.hexdigest( TEST_CONTENT )
+		TEST_CONTENT_TYPE	  = CONFIGURED_HTML_MIMETYPE
+		TEST_UUID_OBJ	      = UUIDTools::UUID.parse( '60acc01e-cd82-11db-84d1-7ff059e49450' )
+		TEST_ACCEPT_HEADER    = 'application/x-yaml, application/json; q=0.2, text/xml; q=0.75'
+		TEST_TITLE            = 'Muffin the Panda Goes To School'
+		TEST_PROPSET 		  = {
+			TEST_PROP  => TEST_PROPVALUE,
+			TEST_PROP2 => TEST_PROPVALUE2,
+			'extent'   => "213404",
+			'checksum' => '231c9a4500f2448e3bdec11c8baedc53',
+		}
+		TEST_RUBY_OBJECT = [
+			{:ip_address => IPAddr.new( '127.0.0.1' )},
+			{:pine_cone  => 'sandwiches'},
+			{:olive_oil  => 'pudding'},
+		]
+
+		TESTING_GET_REQUEST = (<<-END_OF_REQUEST).gsub!( /^\t\t/, '' ).gsub( /\n/, "\r\n" )
+		GET / HTTP/1.1
+		Host: localhost:3474
+		User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US;
+			rv:1.9.0.1) Gecko/2008070206 Firefox/3.0.1
+		Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+		Accept-Language: en-us,en;q=0.5
+		Accept-Encoding: gzip,deflate
+		Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
+		Keep-Alive: 300
+		Connection: close
+
+		END_OF_REQUEST
+
+		TESTING_DELETE_REQUEST = (<<-END_OF_REQUEST).gsub!( /^\t\t/, '' ).gsub( /\n/, "\r\n" )
+		DELETE /#{TEST_UUID} HTTP/1.1
+		Host: localhost:3474
+		User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US;
+			rv:1.9.0.1) Gecko/2008070206 Firefox/3.0.1
+		Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+		Accept-Language: en-us,en;q=0.5
+		Accept-Encoding: gzip,deflate
+		Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
+		Keep-Alive: 300
+		Connection: close
+
+		END_OF_REQUEST
+
+		TESTING_POST_REQUEST = (<<-END_OF_REQUEST).gsub!( /^\t\t/, '' ).gsub( /\n/, "\r\n" ) + TEST_CONTENT
+		POST / HTTP/1.1
+		Host: localhost:3474
+		User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US;
+			rv:1.9.0.1) Gecko/2008070206 Firefox/3.0.1
+		Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
+		Accept-Language: en-us,en;q=0.5
+		Accept-Encoding: gzip,deflate
+		Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
+		Keep-Alive: 300
+		Content-Type: text/plain
+		Content-Length: #{TEST_CONTENT.length}
+		Connection: close
+
+		END_OF_REQUEST
+
+		# Fixtured HTTP responses
+		TEST_OK_HTTP_RESPONSE = <<-'EOF'.gsub(/^\s*/, '')
+		HTTP/1.0 200 OK
+		Connection: close
+		Expires: Tue, 13 May 2008 23:19:49 GMT
+		Etag: "5f77bb4205ddd0482a834ab65a9cdbe4"
+		Content-Type: image/jpeg
+		Date: Mon, 14 May 2007 17:19:49 GMT
+		Server: ThingFish/0.0.1 (Rev: 185 )
+		Content-Length: 14620
+		EOF
+
+		SPECDIR = Pathname.new( __FILE__ ).dirname
+		DATADIR = SPECDIR + 'data'
+
+		# Testing patterns
+		VALID_HTTPDATE = /\w{3}, \d\d \w{3} \d{4} \d\d:\d\d:\d\d \w{3}/
+
+		# Freeze all constants so one test's constants stomping on 
+		# others are detected earlier.
+		constants.each {|const| const_get( const ).freeze }
+
+	end
+end
+
+# vim: set nosta noet ts=4 sw=4:

lib/thingfish/urimap.rb

 	def handlers
 		return @map.values.flatten
 	end
-	
+
 
 	### Return the Array of handlers that are registered for the specified +path+.
 	def handlers_for( path )

plugins/thingfish-filestore-filesystem/spec/thingfish/filestore/filesystem_spec.rb

 	$LOAD_PATH.unshift( pluglibdir ) unless $LOAD_PATH.include?( pluglibdir )
 }
 
-require 'spec'
-require 'spec/lib/constants'
+require 'rspec'
+
 require 'spec/lib/helpers'
 require 'spec/lib/filestore_behavior'
 
 		end
 
 		it "ensures any previously cached size files are destroyed" do
-			scache = mock( "A cached size file", :null_object => true )
+			scache = mock( "A cached size file" ).as_null_object
 			Pathname.should_receive( :glob ).
 				with( "#{@tmpdir}/*/.size" ).
 				once.

plugins/thingfish-filter-basicauth/lib/thingfish/filter/basicauth.rb

 		return {
 			'version'   => [1,0],
 			'supports'  => [],
-			'rev'       => VCSRev.match( /: (\w+)/ )[1] || 0,
+			'rev'       => VCSRev[ /: (\w+)/, 1 ] || 0,
 			'accepts'   => accepts,
 			'generates' => [],
 		  }

plugins/thingfish-filter-basicauth/spec/thingfish/filter/basicauth_spec.rb

 	$LOAD_PATH.unshift( libdir ) unless $LOAD_PATH.include?( libdir )
 }
 
-require 'spec'
-require 'spec/lib/constants'
+require 'rspec'
+
 require 'spec/lib/helpers'
 require 'spec/lib/filter_behavior'
 
 include ThingFish::Constants
 
 describe ThingFish::BasicAuthFilter do
-	include ThingFish::SpecHelpers
 
 	AUTHENTICATE_HEADER = ThingFish::BasicAuthFilter::AUTHENTICATE_HEADER
 	AUTHORIZATION_HEADER = ThingFish::BasicAuthFilter::AUTHORIZATION_HEADER
 
 
 	### Shared behaviors
-	it_should_behave_like "A Filter"
+	it_should_behave_like "a filter"
 
 
 	### Implementation-specific Examples

plugins/thingfish-filter-exif/lib/thingfish/filter/exif.rb

 		return {
 			'version'   => [1,0],
 			'supports'  => [],
-			'rev'       => VCSRev.match( /: (\w+)/ )[1] || 0,
+			'rev'       => VCSRev[ /: (\w+)/, 1 ] || 0,
 			'accepts'   => accepts,
 			'generates' => [],
 		  }

plugins/thingfish-filter-exif/spec/thingfish/filter/exif_spec.rb

 
 require 'pathname'
 require 'tmpdir'
-require 'spec'
-require 'spec/lib/constants'
+require 'rspec'
+
 require 'spec/lib/helpers'
 require 'spec/lib/filter_behavior'
 require 'thingfish/constants'
 require 'thingfish/filter/exif'
 
 
-include ThingFish::TestConstants
-include ThingFish::Constants
 
 #####################################################################
 ###	C O N T E X T S
 
 
 	### Shared behaviors
-	it_should_behave_like "A Filter"
+	it_should_behave_like "a filter"
 
 
 	### Filter-specific tests

plugins/thingfish-filter-image/lib/thingfish/filter/image.rb

 		return {
 			'version'   => [1,0],
 			'supports'  => supports,
-			'rev'       => VCSRev.match( /: (\w+)/ )[1] || 0,
+			'rev'       => VCSRev[ /: (\w+)/, 1 ] || 0,
 			'accepts'   => accepts,
 			'generates' => generates,
 		  }

plugins/thingfish-filter-image/spec/thingfish/filter/image_spec.rb

 	$LOAD_PATH.unshift( pluglibdir ) unless $LOAD_PATH.include?( pluglibdir )
 }
 
-require 'spec'
-require 'spec/lib/constants'
-require 'spec/lib/filter_behavior'
+require 'rspec'
+require 'spec/lib/helpers'
 
 require 'rbconfig'
 
 require 'thingfish'
 require 'thingfish/filter'
+require 'thingfish/behavior/filter'
 
 begin
 	require 'thingfish/filter/image'
 	$have_imagefilter = false
 end
 
-include ThingFish::TestConstants
-include ThingFish::Constants
 
 
 #####################################################################
 		setup_logging( :fatal )
 	end
 
+	let( :filter ) do
+		Magick.stub( :formats ).and_return({ 'PNG' => '*rw-', 'GIF' => '*rw+', 'JPG' => '*rw-' })
+		ThingFish::Filter.create( 'image', {} )
+	end
+
+	let( :testio ) do
+		stub( "image upload IO object", :read => :imagedata, :path => '/tmp/spooled_image' )
+	end
+
+
 	before( :each ) do
 		pending "image filter not loading correctly" unless $have_imagefilter
 
-		# Stub out some formats
-		Magick.stub!( :formats ).and_return({ 'PNG' => '*rw-', 'GIF' => '*rw+', 'JPG' => '*rw-' })
-
-		@filter = ThingFish::Filter.create( 'image', {} )
-
-		@io = stub( "image upload IO object", :read => :imagedata, :path => '/tmp/spooled_image' )
-
 		@request = mock( "request object" )
 		@response = mock( "response object" )
 		@response_headers = mock( "response headers" )
 		@response.stub!( :headers ).and_return( @response_headers )
 
 		@request_metadata = { :format => 'image/png' }
-		@request.stub!( :each_body ).and_yield( @io, @request_metadata )
+		@request.stub!( :each_body ).and_yield( self.testio, @request_metadata )
 
 		@extracted_metadata = {
 			:'image:height'       => :rows,
 
 
 
-	it_should_behave_like "A Filter"
+	it_should_behave_like "a filter"
 
 
 	# Request (extraction) filtering
 		@request.should_receive( :http_method ).at_least( :once ).and_return( :GET )
 		@request.should_not_receive( :each_body )
 
-		@filter.handle_request( @request, @response )
+		filter.handle_request( @request, @response )
 	end
 
 	it "doesn't attempt extraction on a DELETE" do
 		@request.should_receive( :http_method ).at_least( :once ).and_return( :DELETE )
 		@request.should_not_receive( :each_body )
 
-		@filter.handle_request( @request, @response )
+		filter.handle_request( @request, @response )
 	end
 
 
 		Magick::Image.should_not_receive( :from_blob )
 		@request.should_not_receive( :metadata )
 
-		@filter.handle_request( @request, @response )
+		filter.handle_request( @request, @response )
 	end
 
 
 			expected_metadata[ magick_method ] = metadata_key.to_s
 		end
 
-		@request.should_receive( :append_metadata_for ).with( @io, expected_metadata )
+		@request.should_receive( :append_metadata_for ).with( self.testio, expected_metadata )
 
 		# Thumbnail
 		thumbnail = mock( "thumbnail image object" )
 		thumbnail.should_receive( :to_blob ).and_yield( thumb_blob ).and_return( "thumbnail_data" )
 		thumb_blob.should_receive( :format= ).with( 'JPG' )
 		StringIO.should_receive( :new ).with( "thumbnail_data" ).and_return( :thumbio )
-		@request.should_receive( :append_related_resource ).with( @io, :thumbio, thumb_metadata )
+		@request.should_receive( :append_related_resource ).with( self.testio, :thumbio, thumb_metadata )
 
 		# Run the request filter
-		@filter.handle_request( @request, @response )
+		filter.handle_request( @request, @response )
 	end
 
 
 		@request.should_receive( :append_related_resource ).with( io, :thumbio, thumb_metadata )
 
 		# Run the request filter
-		@filter.handle_request( @request, @response )
+		filter.handle_request( @request, @response )
 	end
 
 
 		@request.should_receive( :http_method ).at_least( :once ).and_return( :POST )
 		@response.should_not_receive( :body )
 
-		@filter.handle_response( @response, @request )
+		filter.handle_response( @response, @request )
 	end
 
 
 
 		@response.should_not_receive( :body )
 
-		@filter.handle_response( @response, @request )
+		filter.handle_response( @response, @request )
 	end
 
 
 
 		@response.should_not_receive( :body )
 
-		@filter.handle_response( @response, @request )
+		filter.handle_response( @response, @request )
 	end
 
 
 
 		@response.should_not_receive( :body )
 
-		@filter.handle_response( @response, @request )
+		filter.handle_response( @response, @request )
 	end
 
 	it "doesn't try to convert if the request doesn't explicitly accept any formats it knows about" do
 
 		@response.should_not_receive( :body )
 
-		@filter.handle_response( @response, @request )
+		filter.handle_response( @response, @request )
 	end
 
 
 		acceptparam.should_receive( :subtype ).and_return( 'png' )
 		acceptparam.should_receive( :mediatype ).at_least( :once ).and_return( 'image/png' )
 
-		image = mock( "image object", :null_object => true )
+		image = mock( "image object" ).as_null_object
 		image_filehandle = stub( "image filehandle", :read =&