Skip to content

Commit

Permalink
Assign the matcher subclass to a human readable const.
Browse files Browse the repository at this point in the history
This helps provides useful `#inspect` output.
  • Loading branch information
myronmarston committed Oct 4, 2013
1 parent 1d5c7f4 commit 28d715e
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 7 deletions.
2 changes: 1 addition & 1 deletion features/custom_matchers/access_running_example.feature
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,5 @@ Feature: access running example
When I run `rspec ./example_spec.rb`
Then the output should contain "1 example, 1 failure"
And the output should match /undefined.*method/
And the output should contain "RSpec::Matchers::DSL::Matcher"
And the output should contain "RSpec::Matchers::Custom::Bar"
And the output should not contain "ExampleGroup"
26 changes: 21 additions & 5 deletions lib/rspec/matchers/matcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -219,12 +219,10 @@ def initialize(*expected)
@expected = expected
end

# Needed so the matcher is easily identifier in error messages.
# Without this, ruby's default instance-of-an-anonymous-class
# #inspect would be used (which looks like
# `#<#<Class:0x007fca1505c4e0>:0x007fca1505c468>`)
# Adds the expected value so we can identify an instance of
# the matcher in error messages (e.g. for `NoMethodError`)
def inspect
"#<RSpec::Matchers::DSL::Matcher (#{name})>"
"#<#{self.class.name} expected=#{expected}>"
end

# Indicates that this matcher responds to messages
Expand Down Expand Up @@ -266,9 +264,27 @@ def self.subclass(name, &declarations)
Class.new(self) do
define_method(:name) { name }
@declarations = declarations
end.tap do |klass|
const_name = ('_' + name.to_s).gsub(/_+([[:alpha:]])/) { $1.upcase }

# Constants must start with [A-Z]
const_name.gsub!(/\A_+/, '') # Remove any leading underscores
const_name.gsub!(/\A([^A-Z])/, 'A\1') # Prepend a valid first letter if needed

# Add a trailing number if needed to disambiguate from an existing constant.
if Custom.const_defined?(const_name)
const_name << "2"
const_name.next! while Custom.const_defined?(const_name)
end

Custom.const_set(const_name, klass)
end
end
end
end

# Namespace module that holds custom matcher classes.
module Custom
end
end
end
46 changes: 45 additions & 1 deletion spec/rspec/matchers/matcher_spec.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# encoding: utf-8
require 'spec_helper'

class UnexpectedError < StandardError; end
Expand Down Expand Up @@ -41,6 +42,49 @@ def new_matcher(name, *expected, &block)
expect(m2.matches?(2)).to be_truthy
end

describe "constant assignment" do
it 'gets assigned to a human readable constant' do
matcher = new_matcher(:be_a_something) { }
expect(matcher).to be_a(RSpec::Matchers::Custom::BeASomething)
end

it 'assigns a constant even when there are extra leading underscores' do
matcher = new_matcher(:__foo__bar__) { }
expect(matcher).to be_a(RSpec::Matchers::Custom::FooBar__)
end

it 'differentiates redefinitions by appending a number' do
m1 = new_matcher(:foo_redef) { }
m2 = new_matcher(:foo_redef) { }
m3 = new_matcher(:foo_redef) { }

expect(m1).to be_a(RSpec::Matchers::Custom::FooRedef)
expect(m2).to be_a(RSpec::Matchers::Custom::FooRedef2)
expect(m3).to be_a(RSpec::Matchers::Custom::FooRedef3)
end

it 'supports non-ascii characters', :if => (RUBY_VERSION.to_f > 1.8) do
# We have to eval this because 1.8.7 can't parse it at file load time
# if we don't eval it. eval delays when it is parsed until the example
# is run, which does not happen on 1.8.7
eval <<-EOS
RSpec::Matchers.define(:们) { }
expect(们).to be_a(RSpec::Matchers::Custom::A们)
RSpec::Matchers.define(:be_们) { }
expect(be_们).to be_a(RSpec::Matchers::Custom::Be们)
EOS
end

it 'handles the case where the matcher name starts with a capitol letter' do
matcher = new_matcher(:Capitol_letter) { }
expect(matcher).to be_a(RSpec::Matchers::Custom::CapitolLetter)

matcher = new_matcher(:__Capitol_prefixed_by_underscores) { }
expect(matcher.inspect).to include('CapitolPrefixedByUnderscores')
end
end

context "with an included module" do
let(:matcher) do
new_matcher(:be_a_greeting) do
Expand Down Expand Up @@ -595,7 +639,7 @@ def a_method_in_the_example

expect {
expect(example).to __raise_no_method_error
}.to raise_error(NoMethodError, /RSpec::Matchers::DSL::Matcher/)
}.to raise_error(NoMethodError, /RSpec::Matchers::Custom::RaiseNoMethodError/)
end
end

Expand Down

0 comments on commit 28d715e

Please sign in to comment.