Last active
May 12, 2016 00:02
-
-
Save puyo/a18d0feb147fd6bae6fc to your computer and use it in GitHub Desktop.
RSpec feels unnecessarily verbose to me. This is an attempt at expressing the DSL I'd like to have, with a very hacky implementation that maps back onto RSpec.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
gem 'rspec' | |
require 'rspec/core' | |
require 'rspec/expectations' | |
require 'rspec/autorun' | |
require 'pp' | |
RSpec.configure do |config| | |
config.color = true | |
config.formatter = 'doc' | |
config.expect_with :rspec do |c| | |
c.syntax = :should | |
end | |
end | |
module Check | |
class Scope | |
include ::RSpec::Matchers | |
attr_reader :local_vars, :code_vars, :all_vars, :all_code_vars, :rspec_group | |
def initialize(local_vars: {}, code_vars: {}, parent: nil, doc: nil, &block) | |
#p vars: local_vars, code: code_vars | |
@local_vars = local_vars | |
@code_vars = code_vars | |
@parent = parent | |
@block = block | |
@doc = doc | |
copy_parent_vars | |
setup_rspec_group | |
setup_vars | |
setup_code_vars | |
end | |
def with(args, &block) | |
scope = Scope.new(local_vars: args, parent: self, &block) | |
scope.evaluate | |
end | |
def with_eval(**args, &block) | |
vals = {} | |
args.each do |k,v| | |
vals[k] = eval(v) | |
end | |
scope = Scope.new(local_vars: vals, parent: self, doc: "when evaluating #{args}", &block) | |
scope.evaluate | |
end | |
def code(**args, &block) | |
scope = Scope.new(code_vars: args, parent: self, &block) | |
scope.evaluate | |
end | |
def method(code, &block) | |
scope = Scope.new(code_vars: {call: code}, parent: self, doc: "method #{code}", &block) | |
scope.evaluate | |
end | |
def it(*args, &block) | |
s = self | |
c = @all_vars[:call] | |
x = proc { s.instance_eval(&c) } | |
@rspec_group.it(*args) do | |
s.instance_eval(&block) | |
end | |
end | |
def result(&block) | |
example = Example.new(local_vars: {}, parent: self, &block) | |
subject = instance_eval(@all_code_vars[:call]) | |
example.add_to_rspec_group(subject, 'result', nil, @rspec_group) | |
end | |
def call(desc=nil, &block) | |
if block | |
call_context(desc, &block) | |
else | |
call_instance | |
end | |
end | |
def evaluate | |
instance_eval(&@block) | |
end | |
private | |
def call_context(desc, &block) | |
example = Example.new(local_vars: @all_vars, parent: self, &block) | |
example.add_to_rspec_group(call_instance, 'call', desc, @rspec_group) | |
end | |
def call_instance | |
scope = self | |
code = scope.all_code_vars[:call] | |
proc { scope.instance_eval(code) } | |
end | |
def setup_rspec_group | |
if @parent | |
@rspec_group = @parent.rspec_group.describe description, **rspec_group_args | |
else | |
@rspec_group = RSpec.describe description, **rspec_group_args | |
end | |
end | |
def rspec_group_args | |
local_vars.select{|k,v| k != :doc } | |
end | |
def description | |
return @doc if [email protected]? | |
if local_vars.any? | |
"with #{local_vars}" | |
else | |
"code #{code_vars}" | |
end | |
end | |
def description_from_vars | |
"with #{local_vars.select{|k,v| !v.is_a?(Proc) }.inspect}" | |
end | |
def copy_parent_vars | |
if @parent | |
@all_vars = @parent.all_vars.merge(local_vars) | |
@all_code_vars = @parent.all_code_vars.merge(code_vars) | |
else | |
@all_vars = local_vars | |
@all_code_vars = code_vars | |
end | |
end | |
def setup_vars | |
all_vars.each do |name, value| | |
if not special_methods.include?(name) | |
define_singleton_method(name) { value } | |
end | |
end | |
local_vars.each do |name, value| | |
if not special_methods.include?(name) | |
@rspec_group.let(name) { value } | |
end | |
end | |
end | |
def setup_code_vars | |
all_code_vars.each do |name, value| | |
if not special_methods.include?(name) | |
define_singleton_method(name) { eval(value) } | |
end | |
end | |
code_vars.each do |name, value| | |
if not special_methods.include?(name) | |
@rspec_group.let(name) { eval(value) } | |
end | |
end | |
end | |
def special_methods | |
[:call] | |
end | |
end | |
class Example < Scope | |
def add_to_rspec_group(sub, subject_name, desc, group) | |
block = @block | |
group.describe(subject_name) do | |
subject { sub } | |
it(desc, &block) | |
end | |
end | |
end | |
def self.with(vars, &block) | |
scope = Scope.new(local_vars: vars, &block) | |
scope.evaluate | |
end | |
def self.namespace(&block) | |
scope = Scope.new(doc: 'Check', &block) | |
scope.evaluate | |
end | |
def with(*args, &block) | |
self.class.with(*args, &block) | |
end | |
end | |
# ----- | |
class Sum | |
attr :times_summed | |
def initialize | |
@times_summed = 0 | |
end | |
def calc(a, b, c) | |
@times_summed += 1 | |
a + b + c | |
end | |
end | |
Check.namespace do | |
with_eval sum: 'Sum.new' do | |
method 'sum.calc(a, b, c)' do | |
with a: 1, b: 2, c: 3 do | |
result { should eq 6 } | |
call { should change(sum, :times_summed).by(1) } | |
end | |
with a: 2, b: 4, c: 5 do | |
result { should eq 11 } | |
end | |
it 'should add 10, 20, 30 to 60 (verbose and flexible version)' do | |
sum.calc(10,20,30).should eq 60 | |
end | |
end | |
end | |
end | |
# Output: | |
# | |
# Check | |
# when evaluating {:sum=>"Sum.new"} | |
# method sum.calc(a, b, c) | |
# should add 10, 20, 30 to 60 (verbose and flexible version) | |
# with {:a=>1, :b=>2, :c=>3} | |
# result | |
# should eq 6 | |
# call | |
# should change #times_summed by 1 | |
# with {:a=>2, :b=>4, :c=>5} | |
# result | |
# should eq 11 | |
# | |
# Finished in 0.0034 seconds (files took 0.09671 seconds to load) | |
# 4 examples, 0 failures |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# A version with procs instead of eval. | |
gem 'rspec' | |
require 'rspec/core' | |
require 'rspec/expectations' | |
require 'rspec/autorun' | |
require 'pp' | |
RSpec.configure do |config| | |
config.color = true | |
config.formatter = 'doc' | |
config.expect_with :rspec do |c| | |
c.syntax = :should | |
end | |
end | |
class NicePrintHash < Hash | |
def initialize(hash, &block) | |
for k, v in hash | |
self[k] = v | |
end | |
super(&block) | |
end | |
def to_s | |
result = [] | |
for k, v in self | |
result << "#{k}: #{v.inspect}" | |
end | |
result.join(', ') | |
end | |
end | |
class LiteralString < String | |
def initialize(str) | |
super(str) | |
end | |
def inspect | |
self | |
end | |
end | |
module Check | |
class Scope | |
include ::RSpec::Matchers | |
attr_reader :local_vars, :code_vars, :all_vars, :all_code_vars, :rspec_group | |
def initialize(local_vars: nil, code_vars: nil, parent: nil, doc: nil, &block) | |
#p vars: local_vars, code: code_vars | |
@local_vars = NicePrintHash.new(local_vars || {}) | |
@code_vars = NicePrintHash.new(code_vars || {}) | |
@parent = parent | |
@block = block | |
@doc = doc | |
copy_parent_vars | |
setup_rspec_group | |
setup_vars | |
setup_code_vars | |
end | |
def context(args, &block) | |
scope = Scope.new(local_vars: args, parent: self, &block) | |
scope.evaluate | |
end | |
def code(code_proc, &block) | |
scope = Scope.new(code_vars: {call: code_proc}, parent: self, &block) | |
scope.evaluate | |
end | |
def it(*args, &block) | |
s = self | |
c = @all_vars[:call] | |
x = proc { s.instance_eval(&c) } | |
@rspec_group.it(*args) do | |
s.instance_eval(&block) | |
end | |
end | |
def result(&block) | |
example = Example.new(local_vars: {}, parent: self, &block) | |
subject = call_instance.call | |
example.add_to_rspec_group(subject, 'result', nil, @rspec_group) | |
end | |
def call(desc=nil, &block) | |
if block | |
call_context(desc, &block) | |
else | |
call_instance | |
end | |
end | |
def evaluate | |
instance_eval(&@block) | |
end | |
private | |
def call_context(desc, &block) | |
example = Example.new(local_vars: @all_vars, parent: self, &block) | |
example.add_to_rspec_group(call_instance, 'call', desc, @rspec_group) | |
end | |
def call_instance | |
scope = self | |
code = scope.all_code_vars[:call] | |
proc { scope.instance_exec(&code) } | |
end | |
def setup_rspec_group | |
if @parent | |
@rspec_group = @parent.rspec_group.describe description, **rspec_group_args | |
else | |
@rspec_group = RSpec.describe description, **rspec_group_args | |
end | |
end | |
def rspec_group_args | |
local_vars.select{|k,v| k != :doc } | |
end | |
def description | |
return @doc if [email protected]? | |
if local_vars.any? | |
"context #{local_vars}" | |
else | |
if call = code_vars[:call] | |
path, line = call.source_location | |
line_of_code = File.read(path).lines[line - 1] | |
if m = line_of_code.match(/->\s*{\s*(.*?)\s*}\s*do/) | |
line_of_code = LiteralString.new(m.captures.first) | |
end | |
"code #{line_of_code}" | |
else | |
"#{code_vars}" | |
end | |
end | |
end | |
def description_from_vars | |
"with #{local_vars.select{|k,v| !v.is_a?(Proc) }.inspect}" | |
end | |
def copy_parent_vars | |
if @parent | |
@all_vars = @parent.all_vars.merge(local_vars) | |
@all_code_vars = @parent.all_code_vars.merge(code_vars) | |
else | |
@all_vars = local_vars | |
@all_code_vars = code_vars | |
end | |
end | |
def setup_vars | |
all_vars.each do |name, value| | |
if not special_methods.include?(name) | |
define_singleton_method(name) { value } | |
end | |
end | |
local_vars.each do |name, value| | |
if not special_methods.include?(name) | |
@rspec_group.let(name) { value } | |
end | |
end | |
end | |
def setup_code_vars | |
all_code_vars.each do |name, value| | |
if not special_methods.include?(name) | |
define_singleton_method(name) { eval(value) } | |
end | |
end | |
code_vars.each do |name, value| | |
if not special_methods.include?(name) | |
@rspec_group.let(name) { eval(value) } | |
end | |
end | |
end | |
def special_methods | |
[:call] | |
end | |
end | |
class Example < Scope | |
def add_to_rspec_group(sub, subject_name, desc, group) | |
block = @block | |
group.describe(subject_name) do | |
subject { sub } | |
it(desc, &block) | |
end | |
end | |
end | |
def self.with(vars, &block) | |
scope = Scope.new(local_vars: vars, &block) | |
scope.evaluate | |
end | |
def self.namespace(&block) | |
scope = Scope.new(doc: 'Check', &block) | |
scope.evaluate | |
end | |
def with(*args, &block) | |
self.class.with(*args, &block) | |
end | |
end | |
# ----- | |
class Sum | |
attr :times_summed | |
def initialize | |
@times_summed = 0 | |
end | |
def calc(a, b, c) | |
@times_summed += 1 | |
a + b + c | |
end | |
end | |
Check.namespace do | |
context sum: Sum.new do | |
code -> { sum.calc(a, b, c) } do | |
context a: 1, b: 2, c: 3 do | |
result { should eq 6 } | |
call { should change(sum, :times_summed).by(1) } | |
end | |
context a: 2, b: 4, c: 5 do | |
result { should eq 11 } | |
end | |
it 'should add 10, 20, 30 to 60 (verbose and flexible version)' do | |
sum.calc(10,20,30).should eq 60 | |
end | |
end | |
end | |
end | |
# Output: | |
# | |
# Check | |
# context sum: #<Sum:0x007f959c240750 @times_summed=0> | |
# code sum.calc(a, b, c) | |
# should add 10, 20, 30 to 60 (verbose and flexible version) | |
# context a: 1, b: 2, c: 3 | |
# result | |
# should eq 6 | |
# call | |
# should change #times_summed by 1 | |
# context a: 2, b: 4, c: 5 | |
# result | |
# should eq 11 | |
# | |
# Finished in 0.0034 seconds (files took 0.09671 seconds to load) | |
# 4 examples, 0 failures |
@JonRowe Eval is bad. Agree 100%. I was trying to emulate how I might do this in Lisp with macros, but in Ruby. But Ruby seems to lack the DSL grunt to pull it off.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I got pinged about this on Slack... but my response is simply "RSpec is designed to be expressive, this is the opposite." ok ok so it's not quite that, I also want to say "Eval is bad mmm kay"