Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite custom matcher DSL #332

Merged
merged 22 commits into from
Oct 9, 2013
Merged

Rewrite custom matcher DSL #332

merged 22 commits into from
Oct 9, 2013

Conversation

myronmarston
Copy link
Member

Rewrite custom matcher DSL.

Rather than evaling the define block in the
context of the matcher instance, eval the define
block in the context of the matcher instance's
singleton class.

This also adds a few misc improvements:

  • Each custom matcher gets its own class, which is assigned to a constant in the RSpec::Matchers::Custom namespace.
  • I split the large number of methods up into a few modules for greater clarity about what is what: compare Macros vs DefaultImplementations.
  • All user overrides created by the DSL methods now compile to an actual method, so that users can use early returns, such as:
RSpec::Matchers.define :foo_bar do |expected|
  match do |actual|
    return false if actual == :unexpected
    actual.compares_to?(expected)
  end
end
  • There were some places where things were underspecified; I added specs for them.
  • On 1.9+, respond_to_missing? is defined so that matcher.method(:some_example_method) works.
  • Improved the docs a bit.
  • chain blocks can now accept blocks.

There is one breaking change that we'll probably want to put warnings in 2.99 about. In a case like this:

RSpec::Matchers.define :foo_bar do |expected|
  extend HelperModuleThatDefinesFoo
  def self.bar; end

  match do |actual|
    bar == foo
  end
end

...it would work previously, because self in the define block was the same as self within match (and the other DSL blocks). Now, self in the define block is the singleton class, so it has the feel of defining a class, with the blocks having instance-level scope. That means that methods from extended modules or defined using self. syntax cannot be called from match as they could before.

I think this is 100% compatible with what we had before otherwise.

Feedback wanted!

/cc @dchelimsky @xaviershay @JonRowe

`do` and `end`, as English words, read funny in the middle
of the expectation expression.
- Use a helper method to create the matchers, rather than
  `Matcher.new` + `for_expected`. I plan to rewrite the
  implementation of `Matcher` in a way that will require it
  to be instantiated differently and I having this helper
  method gives us only one place to update.
- Prefer `let` to `before(:each)` ivars.
- Define the module inline as a local; this avoids the need
  for the awkward `m = mod`.
This will help with my upcoming refactoring.
In RSpec 3 the body of `define` will be a
class definition and the body of `match` (etc)
will be a method def, so using `extend` to make
a method available to the `match` block will
not work.
Rather than evaling the `define` block in the
context of the matcher instance, eval the `define`
block in the context of the matcher instance's
singleton class.

* Fixes #272.
  `include` in `define` has a different meaning (module inclusion)
  than `include` in the `match` block (using the `include` matcher to
  match).
* Better solution than #194
  for #188. There's now
  a `match` class method and a `match` instance method.
* Completely avoids issues we had to use hacks to solve before:
  #29,
  #38,
  fc4b66d
This helps provides useful `#inspect` output.
- This makes the "this macro method defines a
  method" abstraction less leaky.
- It allows constructs like an early `return`
  to be used from within the block.
- It puts all user refs into a common module.
JRuby and Rubinius apparently don't handle
non-ascii in const names.
@coveralls
Copy link

Coverage Status

Coverage decreased (-0.27%) when pulling ec61662 on refactor_matcher_dsl into c7e8424 on master.

@myronmarston
Copy link
Member Author

One other thing...in d3e5310 for_expected was added that attempted to re-use matcher objects. I removed this; it didn't fit as well with the new class/instance split. Plus, it has been the source of multiple bugs (see #112 and fc4b66d) and had sprouted some really hairy logic to ensure stuff wasn't shared that shouldn't have been.

@dchelimsky -- do you remember the original motivation for adding for_expected? Was it a noticable perf improvement? I'm trying to make sure there's nothing important lost here.

# Provides default implementations for the matcher protocol methods.
include DefaultImplementations

# So that expectation expressions can be used in the match block...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the ellipsis signifies.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authorial whimsy perhaps.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like that. It's a habit I have, for some reason...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Future generations of lit-majors scouring rspec searching for meaning in The Word Of Myron.

@xaviershay
Copy link
Member

I don't have any substantive feedback, this seems like a good change to me.

They claim you can't use `match` matcher within` match` block,
but this now works.
@coveralls
Copy link

Coverage Status

Coverage increased (+0.12%) when pulling 805fe7c on refactor_matcher_dsl into c7e8424 on master.

def match(expected)
BuiltIn::Match.new(expected)
end

alias_method :match_regex, :match
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you've removed the note is this just being included for backwards compatibility now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I considered removing it but there's no maintenance cost in keeping it (it's one line) so given how many other things we're changing in RSpec 3 I didn't see a reason to remove this as well.

Here's what I think I'll do, though....I'll document it as deprecated/"not recommended for use", and we can consider removing it in 4.0.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps it's worth issuing a deprecation from it...

@JonRowe
Copy link
Member

JonRowe commented Oct 5, 2013

Great work! Have only a few minor comments above!

Actually, it probably doesn't matter much here,
since we are doing other things there that
are likely to blow away the method cache, but
we might as well avoid one more runtime extension.
myronmarston added a commit that referenced this pull request Oct 9, 2013
@myronmarston myronmarston merged commit 42df8d3 into master Oct 9, 2013
@myronmarston myronmarston deleted the refactor_matcher_dsl branch October 9, 2013 07:30
@myronmarston
Copy link
Member Author

I went ahead and merged, now that I have #337 open for the 2.99 deprecations.

@myronmarston
Copy link
Member Author

Oh yeah, I wanted to benchmark matchers built using this compared to 2.14, and see if there is a change, either way. I may still do that.

@dchelimsky
Copy link
Contributor

I think that benchmark is very important, and recommend it is added before a release.

@myronmarston
Copy link
Member Author

I think that benchmark is very important, and recommend it is added before a release.

Yep, I opened #338 to not forget that work. I plan to work on that next.

@dchelimsky
Copy link
Contributor

Thx!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants