diff options
Diffstat (limited to 'spec/ruby/core/module')
-rw-r--r-- | spec/ruby/core/module/fixtures/refine.rb | 13 | ||||
-rw-r--r-- | spec/ruby/core/module/fixtures/using.rb | 10 | ||||
-rw-r--r-- | spec/ruby/core/module/refine_spec.rb | 616 | ||||
-rw-r--r-- | spec/ruby/core/module/using_spec.rb | 276 |
4 files changed, 915 insertions, 0 deletions
diff --git a/spec/ruby/core/module/fixtures/refine.rb b/spec/ruby/core/module/fixtures/refine.rb new file mode 100644 index 0000000000..46975361dd --- /dev/null +++ b/spec/ruby/core/module/fixtures/refine.rb @@ -0,0 +1,13 @@ +module ModuleSpecs + class ClassWithFoo + def foo; "foo" end + end + + module PrependedModule + def foo; "foo from prepended module"; end + end + + module IncludedModule + def foo; "foo from included module"; end + end +end diff --git a/spec/ruby/core/module/fixtures/using.rb b/spec/ruby/core/module/fixtures/using.rb new file mode 100644 index 0000000000..0ed9355af1 --- /dev/null +++ b/spec/ruby/core/module/fixtures/using.rb @@ -0,0 +1,10 @@ +module ModuleSpecs + module EmptyRefinement + end + + module RefinementForStringToS + refine String do + def to_s; "hello from refinement"; end + end + end +end diff --git a/spec/ruby/core/module/refine_spec.rb b/spec/ruby/core/module/refine_spec.rb new file mode 100644 index 0000000000..03e70915e2 --- /dev/null +++ b/spec/ruby/core/module/refine_spec.rb @@ -0,0 +1,616 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/refine', __FILE__) + +describe "Module#refine" do + it "runs its block in an anonymous module" do + inner_self = nil + mod = Module.new do + refine String do + inner_self = self + end + end + + mod.should_not == inner_self + inner_self.should be_kind_of(Module) + inner_self.name.should == nil + end + + it "uses the same anonymous module for future refines of the same class" do + selves = [] + mod = Module.new do + refine String do + selves << self + end + end + + mod.module_eval do + refine String do + selves << self + end + end + + selves[0].should == selves[1] + end + + it "adds methods defined in its block to the anonymous module's public instance methods" do + inner_self = nil + mod = Module.new do + refine String do + def blah + "blah" + end + inner_self = self + end + end + + inner_self.public_instance_methods.should include(:blah) + end + + it "returns created anonymous module" do + inner_self = nil + result = nil + mod = Module.new do + result = refine String do + inner_self = self + end + end + + result.should == inner_self + end + + it "raises ArgumentError if not passed an argument" do + lambda do + Module.new do + refine {} + end + end.should raise_error(ArgumentError) + end + + it "raises TypeError if not passed a class" do + lambda do + Module.new do + refine("foo") {} + end + end.should raise_error(TypeError) + end + + ruby_version_is "" ... "2.4" do + it "raises TypeError if passed a module" do + lambda do + Module.new do + refine(Enumerable) {} + end + end.should raise_error(TypeError) + end + end + + ruby_version_is "2.4" do + it "accepts a module as argument" do + inner_self = nil + Module.new do + refine(Enumerable) do + def blah + end + inner_self = self + end + end + + inner_self.public_instance_methods.should include(:blah) + end + end + + it "raises ArgumentError if not given a block" do + lambda do + Module.new do + refine String + end + end.should raise_error(ArgumentError) + end + + it "applies refinements to calls in the refine block" do + result = nil + Module.new do + refine(String) do + def foo; "foo"; end + result = "hello".foo + end + end + result.should == "foo" + end + + it "doesn't apply refinements outside the refine block" do + Module.new do + refine(String) {def foo; "foo"; end} + -> () { + "hello".foo + }.should raise_error(NoMethodError) + end + end + + it "does not apply refinements to external scopes not using the module" do + Module.new do + refine(String) {def foo; 'foo'; end} + end + + lambda {"hello".foo}.should raise_error(NoMethodError) + end + + # When defining multiple refinements in the same module, + # inside a refine block all refinements from the same + # module are active when a refined method is called + it "makes available all refinements from the same module" do + refinement = Module.new do + refine Integer do + def to_json_format + to_s + end + end + + refine Array do + def to_json_format + "[" + map { |i| i.to_json_format }.join(", ") + "]" + end + end + + refine Hash do + def to_json_format + "{" + map { |k, v| k.to_s.dump + ": " + v.to_json_format }.join(", ") + "}" + end + end + end + + result = nil + + Module.new do + using refinement + + result = [{1 => 2}, {3 => 4}].to_json_format + end + + result.should == '[{"1": 2}, {"3": 4}]' + end + + it "does not make available methods from another refinement module" do + refinery_integer = Module.new do + refine Integer do + def to_json_format + to_s + end + end + end + + refinery_array = Module.new do + refine Array do + def to_json_format + "[" + map { |i| i.to_json_format }.join(",") + "]" + end + end + end + + result = nil + + -> () { + Module.new do + using refinery_integer + using refinery_array + + [1, 2].to_json_format + end + }.should raise_error(NoMethodError) + end + + # method lookup: + # * The prepended modules from the refinement for C + # * The refinement for C + # * The included modules from the refinement for C + # * The prepended modules of C + # * C + # * The included modules of C + describe "method lookup" do + it "looks in the object singleton class first" do + refinement = Module.new do + refine ModuleSpecs::ClassWithFoo do + def foo; "foo from refinement"; end + end + end + + result = nil + Module.new do + using refinement + + obj = ModuleSpecs::ClassWithFoo.new + class << obj + def foo; "foo from singleton class"; end + end + result = obj.foo + end + + result.should == "foo from singleton class" + end + + it "looks in prepended modules from the refinement first" do + refinement = Module.new do + refine ModuleSpecs::ClassWithFoo do + include ModuleSpecs::IncludedModule + prepend ModuleSpecs::PrependedModule + + def foo; "foo from refinement"; end + end + end + + result = nil + Module.new do + using refinement + result = ModuleSpecs::ClassWithFoo.new.foo + end + + result.should == "foo from prepended module" + end + + it "looks in refinement then" do + refinement = Module.new do + refine(ModuleSpecs::ClassWithFoo) do + include ModuleSpecs::IncludedModule + + def foo; "foo from refinement"; end + end + end + + result = nil + Module.new do + using refinement + result = ModuleSpecs::ClassWithFoo.new.foo + end + + result.should == "foo from refinement" + end + + it "looks in included modules from the refinement then" do + refinement = Module.new do + refine ModuleSpecs::ClassWithFoo do + include ModuleSpecs::IncludedModule + end + end + + result = nil + Module.new do + using refinement + result = ModuleSpecs::ClassWithFoo.new.foo + end + + result.should == "foo from included module" + end + + it "looks in the class then" do + refinement = Module.new do + refine(ModuleSpecs::ClassWithFoo) { } + end + + result = nil + Module.new do + using refinement + result = ModuleSpecs::ClassWithFoo.new.foo + end + + result.should == "foo" + end + end + + + # methods in a subclass have priority over refinements in a superclass + it "does not override methods in subclasses" do + subclass = Class.new(ModuleSpecs::ClassWithFoo) do + def foo; "foo from subclass"; end + end + + refinement = Module.new do + refine ModuleSpecs::ClassWithFoo do + def foo; "foo from refinement"; end + end + end + + result = nil + Module.new do + using refinement + result = subclass.new.foo + end + + result.should == "foo from subclass" + end + + context "for methods accesses indirectly" do + ruby_version_is "" ... "2.4" do + it "is not honored by Kernel#send" do + refinement = Module.new do + refine ModuleSpecs::ClassWithFoo do + def foo; "foo from refinement"; end + end + end + + result = nil + Module.new do + using refinement + result = ModuleSpecs::ClassWithFoo.new.send :foo + end + + result.should == "foo" + end + + it "is not honored by BasicObject#__send__" do + refinement = Module.new do + refine ModuleSpecs::ClassWithFoo do + def foo; "foo from refinement"; end + end + end + + result = nil + Module.new do + using refinement + result = ModuleSpecs::ClassWithFoo.new.__send__ :foo + end + + result.should == "foo" + end + + it "is not honored by Symbol#to_proc" do + refinement = Module.new do + refine Integer do + def to_s + "(#{super})" + end + end + end + + result = nil + Module.new do + using refinement + result = [1, 2, 3].map(&:to_s) + end + + result.should == ["1", "2", "3"] + end + end + + ruby_version_is "2.4" do + it "is honored by Kernel#send" do + refinement = Module.new do + refine ModuleSpecs::ClassWithFoo do + def foo; "foo from refinement"; end + end + end + + result = nil + Module.new do + using refinement + result = ModuleSpecs::ClassWithFoo.new.send :foo + end + + result.should == "foo from refinement" + end + + it "is honored by BasicObject#__send__" do + refinement = Module.new do + refine ModuleSpecs::ClassWithFoo do + def foo; "foo from refinement"; end + end + end + + result = nil + Module.new do + using refinement + result = ModuleSpecs::ClassWithFoo.new.__send__ :foo + end + + result.should == "foo from refinement" + end + + it "is honored by Symbol#to_proc" do + refinement = Module.new do + refine Integer do + def to_s + "(#{super})" + end + end + end + + result = nil + Module.new do + using refinement + result = [1, 2, 3].map(&:to_s) + end + + result.should == ["(1)", "(2)", "(3)"] + end + end + + it "is honored by Kernel#binding" do + refinement = Module.new do + refine String do + def to_s + "hello from refinement" + end + end + end + + klass = Class.new do + using refinement + + def foo + "foo".to_s + end + + def get_binding + binding + end + end + + result = Kernel.eval("self.foo()", klass.new.get_binding) + result.should == "hello from refinement" + end + + it "is not honored by Kernel#method" do + klass = Class.new + refinement = Module.new do + refine klass do + def foo; end + end + end + + -> { + Module.new do + using refinement + klass.new.method(:foo) + end + }.should raise_error(NameError, /undefined method `foo'/) + end + + it "is not honored by Kernel#respond_to?" do + klass = Class.new + refinement = Module.new do + refine klass do + def foo; end + end + end + + result = nil + Module.new do + using refinement + result = klass.new.respond_to?(:foo) + end + + result.should == false + end + end + + context "when super is called in a refinement" do + it "looks in the included to refinery module" do + refinement = Module.new do + refine ModuleSpecs::ClassWithFoo do + include ModuleSpecs::IncludedModule + + def foo + super + end + end + end + + result = nil + Module.new do + using refinement + result = ModuleSpecs::ClassWithFoo.new.foo + end + + result.should == "foo from included module" + end + + it "looks in the refined class" do + refinement = Module.new do + refine ModuleSpecs::ClassWithFoo do + def foo + super + end + end + end + + result = nil + Module.new do + using refinement + result = ModuleSpecs::ClassWithFoo.new.foo + end + + result.should == "foo" + end + + # super in a method of a refinement invokes the method in the refined + # class even if there is another refinement which has been activated + # in the same context. + it "looks in the refined class even if there is another active refinement" do + refinement = Module.new do + refine ModuleSpecs::ClassWithFoo do + def foo + "foo from refinement" + end + end + end + + refinement_with_super = Module.new do + refine ModuleSpecs::ClassWithFoo do + def foo + super + end + end + end + + result = nil + Module.new do + using refinement + using refinement_with_super + result = ModuleSpecs::ClassWithFoo.new.foo + end + + result.should == "foo" + end + end + + # Refinements are inherited by module inclusion. + # That is, using activates all refinements in the ancestors of the specified module. + # Refinements in a descendant have priority over refinements in an ancestor. + context "module inclusion" do + it "activates all refinements from all ancestors" do + refinement_included = Module.new do + refine Integer do + def to_json_format + to_s + end + end + end + + refinement = Module.new do + include refinement_included + + refine Array do + def to_json_format + "[" + map { |i| i.to_s }.join(", ") + "]" + end + end + end + + result = nil + Module.new do + using refinement + result = [5.to_json_format, [1, 2, 3].to_json_format] + end + + result.should == ["5", "[1, 2, 3]"] + end + + it "overrides methods of ancestors by methods in descendants" do + refinement_included = Module.new do + refine Integer do + def to_json_format + to_s + end + end + end + + refinement = Module.new do + include refinement_included + + refine Integer do + def to_json_format + "hello from refinement" + end + end + end + + result = nil + Module.new do + using refinement + result = 5.to_json_format + end + + result.should == "hello from refinement" + end + end +end + diff --git a/spec/ruby/core/module/using_spec.rb b/spec/ruby/core/module/using_spec.rb new file mode 100644 index 0000000000..32e8f8f96b --- /dev/null +++ b/spec/ruby/core/module/using_spec.rb @@ -0,0 +1,276 @@ +require File.expand_path('../../../spec_helper', __FILE__) +require File.expand_path('../fixtures/using', __FILE__) + +describe "Module#using" do + it "imports class refinements from module into the current class/module" do + refinement = Module.new do + refine Integer do + def foo; "foo"; end + end + end + + result = nil + Module.new do + using refinement + result = 1.foo + end + + result.should == "foo" + end + + it "accepts module as argument" do + refinement = Module.new do + refine Integer do + def foo; "foo"; end + end + end + + -> () { + Module.new do + using refinement + end + }.should_not raise_error + end + + it "accepts module without refinements" do + mod = Module.new + + -> () { + Module.new do + using mod + end + }.should_not raise_error + end + + it "does not accept class" do + klass = Class.new + + -> () { + Module.new do + using klass + end + }.should raise_error(TypeError) + end + + it "raises TypeError if passed something other than module" do + -> () { + Module.new do + using "foo" + end + }.should raise_error(TypeError) + end + + it "returns self" do + refinement = Module.new + + result = nil + mod = Module.new do + result = using refinement + end + + result.should equal(mod) + end + + it "works in classes too" do + refinement = Module.new do + refine Integer do + def foo; "foo"; end + end + end + + result = nil + Class.new do + using refinement + result = 1.foo + end + + result.should == "foo" + end + + it "raises error in method scope" do + mod = Module.new do + def self.foo + using ModuleSpecs::EmptyRefinement + end + end + + -> () { + mod.foo + }.should raise_error(RuntimeError, /Module#using is not permitted in methods/) + end + + it "activates refinement even for existed objects" do + result = nil + + Module.new do + klass = Class.new do + def foo; "foo"; end + end + + refinement = Module.new do + refine klass do + def foo; "foo from refinement"; end + end + end + + obj = klass.new + using refinement + result = obj.foo + end + + result.should == "foo from refinement" + end + + it "activates updates when refinement reopens later" do + result = nil + + Module.new do + klass = Class.new do + def foo; "foo"; end + end + + refinement = Module.new do + refine klass do + def foo; "foo from refinement"; end + end + end + + using refinement + + refinement.class_eval do + refine klass do + def foo; "foo from reopened refinement"; end + end + end + + obj = klass.new + result = obj.foo + end + + result.should == "foo from reopened refinement" + end + + describe "scope of refinement" do + it "is active until the end of current class/module" do + ScratchPad.record [] + + Module.new do + Class.new do + using ModuleSpecs::RefinementForStringToS + ScratchPad << "1".to_s + end + + ScratchPad << "1".to_s + end + + ScratchPad.recorded.should == ["hello from refinement", "1"] + end + + # Refinements are lexical in scope. + # Refinements are only active within a scope after the call to using. + # Any code before the using statement will not have the refinement activated. + it "is not active before the `using` call" do + ScratchPad.record [] + + Module.new do + Class.new do + ScratchPad << "1".to_s + using ModuleSpecs::RefinementForStringToS + ScratchPad << "1".to_s + end + end + + ScratchPad.recorded.should == ["1", "hello from refinement"] + end + + # If you call a method that is defined outside the current scope + # the refinement will be deactivated + it "is not active for code defined outside the current scope" do + result = nil + + Module.new do + klass = Class.new do + def foo; "foo"; end + end + + refinement = Module.new do + refine klass do + def foo; "foo from refinement"; end + end + end + + def self.call_foo(c) + c.foo + end + + using refinement + + result = call_foo(klass.new) + end + + result.should == "foo" + end + + # If a method is defined in a scope where a refinement is active + # the refinement will be active when the method is called. + it "is active for method defined in a scope wherever it's called" do + klass = Class.new do + def foo; "foo"; end + end + + mod = Module.new do + refinement = Module.new do + refine klass do + def foo; "foo from refinement"; end + end + end + + using refinement + + def self.call_foo(c) + c.foo + end + end + + c = klass.new + mod.call_foo(c).should == "foo from refinement" + end + + it "is not active if `using` call is not evaluated" do + result = nil + + Module.new do + if false + using ModuleSpecs::RefinementForStringToS + end + result = "1".to_s + end + + result.should == "1" + end + + # The refinements in module are not activated automatically + # if the class is reopened later + it "is not active when class/module reopens" do + refinement = Module.new do + refine String do + def to_s + "hello from refinement" + end + end + end + + result = [] + klass = Class.new do + using refinement + result << "1".to_s + end + + klass.class_eval do + result << "1".to_s + end + + result.should == ["hello from refinement", "1"] + end + end +end |