diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6f6faf2..621d0af 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,3 +27,5 @@ jobs: with: name: ${{ matrix.ruby }} file: ./coverage/coverage.xml + - run: bundle exec rubocop + if: matrix.ruby == '3.0' diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..d75a94f --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,2 @@ +inherit_gem: + panolint: rubocop.yml diff --git a/Gemfile b/Gemfile index 9a983be..4ebdeca 100644 --- a/Gemfile +++ b/Gemfile @@ -1,9 +1,26 @@ +# frozen_string_literal: true + source "https://rubygems.org" # CI-only dependencies go here -if ENV["CI"] == "true" +if ENV["CI"] == "true" # rubocop:disable Style/IfUnlessModifier gem "simplecov-cobertura", require: false, group: "test" end # Specify gem's dependencies in docile.gemspec gemspec + +group :test do + gem "rspec", "~> 3.10" + gem "simplecov", require: false +end + +# Excluded from CI except on latest MRI Ruby, to reduce compatibility burden +group :checks do + gem "panolint", github: "panorama-ed/panolint", branch: "main" +end + +# Optional, only used locally to release to rubygems.org +group :release, optional: true do + gem "rake" +end diff --git a/Rakefile b/Rakefile index e4315ee..83e2b67 100644 --- a/Rakefile +++ b/Rakefile @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require "rake/clean" require "bundler/gem_tasks" require "rspec/core/rake_task" # Default task for `rake` is to run rspec -task :default => [:spec] +task default: [:spec] # Use default rspec rake task RSpec::Core::RakeTask.new diff --git a/docile.gemspec b/docile.gemspec index 7a7ef86..7ac3c35 100644 --- a/docile.gemspec +++ b/docile.gemspec @@ -1,5 +1,6 @@ -$:.push File.expand_path("../lib", __FILE__) -require "docile/version" +# frozen_string_literal: true + +require_relative "lib/docile/version" Gem::Specification.new do |s| s.name = "docile" @@ -17,15 +18,12 @@ Gem::Specification.new do |s| "semver.org." s.license = "MIT" - # Files included in the gem - s.files = `git ls-files -z`.split("\x0").reject do |f| - f.match(%r{^(test|spec|features)/}) - end - s.require_paths = ["lib"] - # Specify oldest supported Ruby version (2.5 to support JRuby 9.2.17.0) s.required_ruby_version = ">= 2.5.0" - s.add_development_dependency "rake", "~> 12.3.3" - s.add_development_dependency "rspec", "~> 3.9" + # Files included in the gem + s.files = `git ls-files -z`.split("\x0").reject do |f| + f.match(%r{^(test|spec|features)/}) + end + s.require_paths = ["lib"] end diff --git a/lib/docile.rb b/lib/docile.rb index f91e811..90e6e84 100644 --- a/lib/docile.rb +++ b/lib/docile.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "docile/version" require "docile/execution" require "docile/fallback_context_proxy" @@ -86,7 +88,9 @@ module Docile exec_in_proxy_context(dsl, FallbackContextProxy, *args, &block) end - ruby2_keywords :dsl_eval_with_block_return if respond_to?(:ruby2_keywords, true) + if respond_to?(:ruby2_keywords, true) + ruby2_keywords :dsl_eval_with_block_return + end module_function :dsl_eval_with_block_return # Execute a block in the context of an immutable object whose methods, diff --git a/lib/docile/backtrace_filter.rb b/lib/docile/backtrace_filter.rb index fd8f1f0..3d7a0d8 100644 --- a/lib/docile/backtrace_filter.rb +++ b/lib/docile/backtrace_filter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Docile # @api private # @@ -7,15 +9,15 @@ module Docile # If {NoMethodError} is caught then the exception object will be extended # by this module to add filter functionalities. module BacktraceFilter - FILTER_PATTERN = /lib\/docile/ + FILTER_PATTERN = %r{/lib/docile/}.freeze def backtrace - super.select { |trace| trace !~ FILTER_PATTERN } + super.reject { |trace| trace =~ FILTER_PATTERN } end if ::Exception.public_method_defined?(:backtrace_locations) def backtrace_locations - super.select { |location| location.absolute_path !~ FILTER_PATTERN } + super.reject { |location| location.absolute_path =~ FILTER_PATTERN } end end end diff --git a/lib/docile/chaining_fallback_context_proxy.rb b/lib/docile/chaining_fallback_context_proxy.rb index 3f7875a..4fea047 100644 --- a/lib/docile/chaining_fallback_context_proxy.rb +++ b/lib/docile/chaining_fallback_context_proxy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "docile/fallback_context_proxy" module Docile @@ -10,13 +12,16 @@ module Docile # objects. # # @see Docile.dsl_eval_immutable + # + # rubocop:disable Style/MissingRespondToMissing class ChainingFallbackContextProxy < FallbackContextProxy # Proxy methods as in {FallbackContextProxy#method_missing}, replacing # `receiver` with the returned value. def method_missing(method, *args, &block) @__receiver__ = super(method, *args, &block) end - + ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true) end + # rubocop:enable Style/MissingRespondToMissing end diff --git a/lib/docile/execution.rb b/lib/docile/execution.rb index 253227c..e8a6408 100644 --- a/lib/docile/execution.rb +++ b/lib/docile/execution.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Docile # @api private # @@ -15,7 +17,7 @@ module Docile # @param block [Proc] the block of DSL commands to be executed # @return [Object] the return value of the block def exec_in_proxy_context(dsl, proxy_type, *args, &block) - block_context = eval("self", block.binding) + block_context = eval("self", block.binding) # rubocop:disable Style/EvalWithLocation # Use #equal? to test strict object identity (assuming that this dictum # from the Ruby docs holds: "[u]nlike ==, the equal? method should never @@ -38,6 +40,7 @@ module Docile block_context.instance_variables.each do |ivar| next unless proxy_context.instance_variables.include?(ivar) + value_from_dsl_proxy = proxy_context.instance_variable_get(ivar) block_context.instance_variable_set(ivar, value_from_dsl_proxy) end diff --git a/lib/docile/fallback_context_proxy.rb b/lib/docile/fallback_context_proxy.rb index 64637d5..9eeeb9b 100644 --- a/lib/docile/fallback_context_proxy.rb +++ b/lib/docile/fallback_context_proxy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "set" module Docile @@ -13,11 +15,13 @@ module Docile # This is useful for implementing DSL evaluation in the context of an object. # # @see Docile.dsl_eval + # + # rubocop:disable Style/MissingRespondToMissing class FallbackContextProxy # The set of methods which will **not** be proxied, but instead answered # by this object directly. NON_PROXIED_METHODS = Set[:__send__, :object_id, :__id__, :==, :equal?, - :"!", :"!=", :instance_exec, :instance_variables, + :!, :!=, :instance_exec, :instance_variables, :instance_variable_get, :instance_variable_set, :remove_instance_variable] @@ -54,14 +58,18 @@ module Docile singleton_class. send(:define_method, :method_missing) do |method, *args, &block| m = method.to_sym - if !NON_FALLBACK_METHODS.include?(m) && !fallback.respond_to?(m) && receiver.respond_to?(m) + if !NON_FALLBACK_METHODS.member?(m) && + !fallback.respond_to?(m) && + receiver.respond_to?(m) receiver.__send__(method.to_sym, *args, &block) else super(method, *args, &block) end end - singleton_class.send(:ruby2_keywords, :method_missing) if singleton_class.respond_to?(:ruby2_keywords, true) + if singleton_class.respond_to?(:ruby2_keywords, true) + singleton_class.send(:ruby2_keywords, :method_missing) + end # instrument a helper method to remove the above instrumentation singleton_class. @@ -74,12 +82,8 @@ module Docile # @return [Array] Instance variable names, excluding # {NON_PROXIED_INSTANCE_VARIABLES} - # - # @note on Ruby 1.8.x, the instance variable names are actually of - # type `String`. def instance_variables - # Ruby 1.8.x returns string names, convert to symbols for compatibility - super.select { |v| !NON_PROXIED_INSTANCE_VARIABLES.include?(v.to_sym) } + super.reject { |v| NON_PROXIED_INSTANCE_VARIABLES.include?(v) } end # Proxy all methods, excluding {NON_PROXIED_METHODS}, first to `receiver` @@ -99,4 +103,5 @@ module Docile ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true) end + # rubocop:enable Style/MissingRespondToMissing end diff --git a/lib/docile/version.rb b/lib/docile/version.rb index 9a6f0a7..cbb72ff 100644 --- a/lib/docile/version.rb +++ b/lib/docile/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Docile # The current version of this library VERSION = "1.3.5" diff --git a/spec/docile_spec.rb b/spec/docile_spec.rb index d118426..d1bea49 100644 --- a/spec/docile_spec.rb +++ b/spec/docile_spec.rb @@ -1,9 +1,64 @@ +# frozen_string_literal: true + require "spec_helper" -require "singleton" +# TODO: Factor single spec file into multiple specs +# rubocop:disable RSpec/MultipleDescribes describe Docile do - describe ".dsl_eval" do + before :each do + stub_const("Pizza", + Struct.new(:cheese, :pepperoni, :bacon, :sauce)) + + stub_const("PizzaBuilder", Class.new do + # rubocop:disable all + def cheese(v=true); @cheese = v; end + def pepperoni(v=true); @pepperoni = v; end + def bacon(v=true); @bacon = v; end + def sauce(v=nil); @sauce = v; end + # rubocop:enable all + + def build + Pizza.new(!!@cheese, !!@pepperoni, !!@bacon, @sauce) + end + end) + + stub_const("InnerDSL", Class.new do + def initialize + @b = "b" + end + + attr_accessor :b + + def c + "inner c" + end + end) + + stub_const("OuterDSL", Class.new do + def initialize + @a = "a" + end + + attr_accessor :a + + def c + "outer c" + end + + def inner(&block) + Docile.dsl_eval(InnerDSL.new, &block) + end + + def inner_with_params(param, &block) + Docile.dsl_eval(InnerDSL.new, param, :foo, &block) + end + end) + end + + def outer(&block) + Docile.dsl_eval(OuterDSL.new, &block) + end context "when DSL context object is an Array" do let(:array) { [] } @@ -27,23 +82,15 @@ describe Docile do end it "doesn't proxy #__id__" do - Docile.dsl_eval(array) { expect(__id__).not_to eq(array.__id__) } + described_class.dsl_eval(array) do + expect(__id__).not_to eq(array.__id__) + end end - it "raises NoMethodError if the DSL object doesn't implement the method" do - expect { Docile.dsl_eval(array) { no_such_method } }.to raise_error(NoMethodError) - end - end - - Pizza = Struct.new(:cheese, :pepperoni, :bacon, :sauce) - - class PizzaBuilder - def cheese(v=true); @cheese = v; end - def pepperoni(v=true); @pepperoni = v; end - def bacon(v=true); @bacon = v; end - def sauce(v=nil); @sauce = v; end - def build - Pizza.new(!!@cheese, !!@pepperoni, !!@bacon, @sauce) + it "raises NoMethodError if DSL object doesn't implement the method" do + expect do + described_class.dsl_eval(array) { no_such_method } + end.to raise_error(NoMethodError) end end @@ -56,7 +103,7 @@ describe Docile do Docile.dsl_eval(builder) do bacon cheese - sauce @sauce + sauce @sauce # rubocop:disable RSpec/InstanceVariable end.build end @@ -65,38 +112,13 @@ describe Docile do end end - class InnerDSL - def initialize; @b = "b"; end - attr_accessor :b - def c; "inner c"; end - end - - class OuterDSL - def initialize; @a = "a"; end - attr_accessor :a - - def c; "outer c"; end - - def inner(&block) - Docile.dsl_eval(InnerDSL.new, &block) - end - - def inner_with_params(param, &block) - Docile.dsl_eval(InnerDSL.new, param, :foo, &block) - end - end - - def outer(&block) - Docile.dsl_eval(OuterDSL.new, &block) - end - context "when given parameters for the DSL block" do def parameterized(*args, &block) Docile.dsl_eval(OuterDSL.new, *args, &block) end it "passes parameters to the block" do - parameterized(1,2,3) do |x,y,z| + parameterized(1, 2, 3) do |x, y, z| expect(x).to eq(1) expect(y).to eq(2) expect(z).to eq(3) @@ -108,8 +130,8 @@ describe Docile do end it "find outer dsl parameters in inner dsl scope" do - parameterized(1,2,3) do |a,b,c| - inner_with_params(c) do |d,e| + parameterized(1, 2, 3) do |a, b, c| + inner_with_params(c) do |d, e| expect(a).to eq(1) expect(b).to eq(2) expect(c).to eq(3) @@ -121,41 +143,46 @@ describe Docile do end context "when block's context has helper methods which call DSL methods" do - class BlockContextWithHelperMethods - def initialize(array_as_dsl) - @array_as_dsl = array_as_dsl - end - - # Classic dynamic programming factorial, using the methods of {Array} - # as a DSL to implement it, via helper methods {#calculate_factorials} - # and {#save_factorials} which are defined in this class, so therefore - # outside the block. - def factorial_as_dsl_against_array(n) - Docile.dsl_eval(@array_as_dsl) { calculate_factorials(n) }.last - end - - # Uses the helper method {#save_factorials} below. - def calculate_factorials(n) - (2..n).each { |i| save_factorial(i) } - end - - # Uses the methods {Array#push} and {Array#at} as a DSL from a helper - # method defined in the block's context. Successfully calling this - # proves that we can find helper methods from outside the block, and - # then find DSL methods from inside those helper methods. - def save_factorial(i) - push(i * at(i - 1)) - end - end - subject { context.method(:factorial_as_dsl_against_array) } - let(:context) { BlockContextWithHelperMethods.new(array_as_dsl) } + let(:context) do + class_block_context_with_helper_methods.new(array_as_dsl) + end let(:array_as_dsl) { [1, 1] } + let(:class_block_context_with_helper_methods) do + Class.new do + def initialize(array_as_dsl) + @array_as_dsl = array_as_dsl + end + + # Classic dynamic programming factorial, using the methods of {Array} + # as a DSL to implement it, via helper methods {#calculate_factorials} + # and {#save_factorials} which are defined in this class, so therefore + # outside the block. + def factorial_as_dsl_against_array(num) + Docile.dsl_eval(@array_as_dsl) { calculate_factorials(num) }.last + end + + # Uses the helper method {#save_factorials} below. + def calculate_factorials(num) + (2..num).each { |i| save_factorial(i) } + end + + # Uses the methods {Array#push} and {Array#at} as a DSL from a helper + # method defined in the block's context. Successfully calling this + # proves that we can find helper methods from outside the block, and + # then find DSL methods from inside those helper methods. + def save_factorial(num) + push(num * at(num - 1)) + end + end + end + it "finds DSL methods within helper method defined in block's context" do # see https://en.wikipedia.org/wiki/Factorial + # rubocop:disable Layout/ExtraSpacing [ [0, 1], [1, 1], @@ -177,6 +204,7 @@ describe Docile do array_as_dsl.replace([1, 1]) expect(subject.call(n)).to eq expected_factorial end + # rubocop:enable Layout/ExtraSpacing end it "removes fallback instrumentation from the DSL object after block" do @@ -200,7 +228,7 @@ describe Docile do end it "removes fallback instrumentation from the DSL object after block" do - expect { subject.call(5) rescue nil }. + expect { subject.call(5) rescue nil }. # rubocop:disable Style/RescueModifier not_to change { context.respond_to?(:method_missing) }. from(false) end @@ -208,28 +236,31 @@ describe Docile do end context "when DSL have NoMethod error inside" do - class DSLWithNoMethod - def initialize(b); @b = b; end - attr_accessor :b - def push_element - @b.push 1 + let(:class_dsl_with_no_method) do + Class.new do + def push_element + nil.push(1) + end end end it "raise NoMethodError error from nil" do - Docile.dsl_eval(DSLWithNoMethod.new(nil)) do + described_class.dsl_eval(class_dsl_with_no_method.new) do expect { push_element }. - to raise_error(NoMethodError, /undefined method `push' (for|on) nil:NilClass/) + to raise_error( + NoMethodError, + /undefined method `push' (for|on) nil:NilClass/ + ) end end end context "when DSL blocks are nested" do - - context "method lookup" do + describe "method lookup" do + # rubocop:disable Style/SingleLineMethods it "finds method of outer dsl in outer dsl scope" do outer { expect(a).to eq("a") } - outer { inner {}; expect(c).to eq("outer c") } + outer { inner {}; expect(c).to eq("outer c") } # rubocop:disable Style/Semicolon end it "finds method of inner dsl in inner dsl scope" do @@ -256,9 +287,10 @@ describe Docile do outer { expect(a).to eq("a") } outer { inner { expect(a).to eq("a") } } end + # rubocop:enable Style/SingleLineMethods end - context "local variable lookup" do + describe "local variable lookup" do it "finds local variable from block context in outer dsl scope" do foo = "foo" outer { expect(foo).to eq("foo") } @@ -270,25 +302,35 @@ describe Docile do end end - context "instance variable lookup" do + describe "instance variable lookup" do + # rubocop:disable RSpec/InstanceVariable it "finds instance variable from block definition in outer dsl scope" do - @iv1 = "iv1"; outer { expect(@iv1).to eq("iv1") } + @iv1 = "iv1" + outer { expect(@iv1).to eq("iv1") } end - it "proxies instance variable assignments in block in outer dsl scope back into block's context" do - @iv1 = "foo"; outer { @iv1 = "bar" }; expect(@iv1).to eq("bar") + it "proxies instance variable assignments in block in outer dsl scope "\ + "back into block's context" do + @iv1 = "foo" + outer { @iv1 = "bar" } + expect(@iv1).to eq("bar") end it "finds instance variable from block definition in inner dsl scope" do - @iv2 = "iv2"; outer { inner { expect(@iv2).to eq("iv2") } } + @iv2 = "iv2" + outer { inner { expect(@iv2).to eq("iv2") } } end - it "proxies instance variable assignments in block in inner dsl scope back into block's context" do - @iv2 = "foo"; outer { inner { @iv2 = "bar" } }; expect(@iv2).to eq("bar") + it "proxies instance variable assignments in block in inner dsl scope "\ + "back into block's context" do + @iv2 = "foo" + outer { inner { @iv2 = "bar" } } + expect(@iv2).to eq("bar") end + # rubocop:enable RSpec/InstanceVariable end - context "identity of 'self' inside nested dsl blocks" do + describe "identity of 'self' inside nested dsl blocks" do # see https://github.com/ms-ati/docile/issues/31 subject do identified_selves = {} @@ -322,36 +364,44 @@ describe Docile do end context "when DSL context object is a Dispatch pattern" do - class DispatchScope - def params - { :a => 1, :b => 2, :c => 3 } + let(:class_message_dispatcher) do + Class.new do + def initialize + @responders = {} + end + + def add_responder(path, &block) + @responders[path] = block + end + + def dispatch(path, request) + Docile. + dsl_eval(class_dispatch_scope.new, request, &@responders[path]) + end + + def class_dispatch_scope + Class.new do + def params + { a: 1, b: 2, c: 3 } + end + end + end end end - class MessageDispatch - include Singleton - - def initialize - @responders = {} - end - - def add_responder path, &block - @responders[path] = block - end - - def dispatch path, request - Docile.dsl_eval(DispatchScope.new, request, &@responders[path]) - end + let(:message_dispatcher_instance) do + class_message_dispatcher.new end def respond(path, &block) - MessageDispatch.instance.add_responder(path, &block) + message_dispatcher_instance.add_responder(path, &block) end def send_request(path, request) - MessageDispatch.instance.dispatch(path, request) + message_dispatcher_instance.dispatch(path, request) end + # rubocop:disable RSpec/InstanceVariable it "dispatches correctly" do @first = @second = nil @@ -363,12 +413,16 @@ describe Docile do @second = "Got a new #{bike}" end - def x(y) ; "Got a #{y}"; end - respond "/third" do |third| - expect(x(third)).to eq("Got a third thing") + def third(val) + "Got a #{val}" + end + + respond "/third" do |arg| + expect(third(arg)).to eq("Got a third thing") end fourth = nil + respond "/params" do |arg| fourth = params[arg] end @@ -382,133 +436,122 @@ describe Docile do expect(@second).to eq("Got a new ten speed") expect(fourth).to eq(2) end - + # rubocop:enable RSpec/InstanceVariable end - context "when DSL context object is the same as the block's context object" do - class DSLContextSameAsBlockContext - def foo(v = nil) - @foo = v if v - @foo - end + context "when DSL context object is same as the block's context object" do + let(:class_context_same_as_block_context) do + Class.new do + def foo(val = nil) + @foo = val if val + @foo + end - def bar(v = nil) - @bar = v if v - @bar - end + def bar(val = nil) + @bar = val if val + @bar + end - def dsl_eval(block) - Docile.dsl_eval(self, &block) - end + def dsl_eval(block) + Docile.dsl_eval(self, &block) + end - def dsl_eval_string(string) - block = binding.eval("proc { #{string} }") - dsl_eval(block) + def dsl_eval_string(string) + block = binding.eval("proc { #{string} }") # rubocop:disable all + dsl_eval(block) + end end end - let(:dsl) { DSLContextSameAsBlockContext.new } + let(:dsl) { class_context_same_as_block_context.new } - it "calls DSL methods and sets instance variables on the DSL context object" do - dsl.dsl_eval_string('foo 0; bar 1') + it "calls DSL methods and sets state on the DSL context object" do + dsl.dsl_eval_string("foo 0; bar 1") expect(dsl.foo).to eq(0) expect(dsl.bar).to eq(1) end context "when the DSL object is frozen" do it "can call non-mutative code without raising an exception" do - expect { dsl.freeze.dsl_eval_string('1 + 2') }.not_to raise_error + expect { dsl.freeze.dsl_eval_string("1 + 2") }.not_to raise_error end end end context "when NoMethodError is raised" do - specify "#backtrace does not include path to Docile's source file" do - begin - Docile.dsl_eval(Object.new) { foo } - rescue NoMethodError => e - expect(e.backtrace).not_to include(match(/lib\/docile/)) - end + specify "#backtrace doesn't include path to Docile's sources" do + described_class.dsl_eval(Object.new) { foo } + rescue NoMethodError => e + expect(e.backtrace).not_to include(match(%r{/lib/docile/})) end - if ::Exception.public_method_defined?(:backtrace_locations) - specify "#backtrace_locations also does not include path to Docile's source file" do - begin - Docile.dsl_eval(Object.new) { foo } - rescue NoMethodError => e - expect(e.backtrace_locations.map(&:absolute_path)).not_to include(match(/lib\/docile/)) + specify "#backtrace_locations doesn't include path to Docile's sources" do + described_class.dsl_eval(Object.new) { foo } + rescue NoMethodError => e + expect(e.backtrace_locations.map(&:absolute_path)). + not_to include(match(%r{/lib/docile/})) + end + end + + context "when a DSL method has a keyword argument" do + let(:class_with_method_with_keyword_arg) do + Class.new do + attr_reader :v0, :v1, :v2 + + def set(arg, kw1:, kw2:) + @v0 = arg + @v1 = kw1 + @v2 = kw2 end end end - end - if RUBY_VERSION >= "2.0.0" - context "when a DSL method has a keyword argument" do - class DSLMethodWithKeywordArgument - attr_reader :v0, :v1, :v2 - class_eval(<<-METHOD) - def set(v0, v1:, v2:) - @v0 = v0 - @v1 = v1 - @v2 = v2 - end - METHOD - end + let(:dsl) { class_with_method_with_keyword_arg.new } - let(:dsl) do - DSLMethodWithKeywordArgument.new - end + it "calls such DSL methods with no stderr output" do + # This is to check warnings related to keyword argument is not output. + # See: https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/ + expect do + described_class.dsl_eval(dsl) { set(0, kw2: 2, kw1: 1) } + end. + not_to output.to_stderr - it "calls such DSL methods with no stderr output" do - # This is to check warnings related to keyword argument is not output. - # See: https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/ - expect { Docile.dsl_eval(dsl) { set(0, v2: 2, v1: 1) } }. - not_to output.to_stderr - - expect(dsl.v0).to eq 0 - expect(dsl.v1).to eq 1 - expect(dsl.v2).to eq 2 - end + expect(dsl.v0).to eq 0 + expect(dsl.v1).to eq 1 + expect(dsl.v2).to eq 2 end end - if RUBY_VERSION >= "2.0.0" - context "when a DSL method has a double splat" do - class DSLMethodWithDoubleSplat + context "when a DSL method has a double splat" do + let(:class_with_method_with_double_splat) do + Class.new do attr_reader :arguments, :options - # Use class_eval because Ruby 1.x does not support double splat - class_eval <<-METHOD, __FILE__, __LINE__ + 1 - def configure(*arguments, **options) - @arguments = arguments.dup - @options = options.dup - end - METHOD + def configure(*arguments, **options) + @arguments = arguments.dup + @options = options.dup + end end + end - let(:dsl) { DSLMethodWithDoubleSplat.new } + let(:dsl) { class_with_method_with_double_splat.new } - it "correctly passes keyword arguments" do - Docile.dsl_eval(dsl) { configure(1, a: 1) } + it "correctly passes keyword arguments" do + described_class.dsl_eval(dsl) { configure(1, a: 1) } - expect(dsl.arguments).to eq [1] - expect(dsl.options).to eq({ a: 1 }) - end + expect(dsl.arguments).to eq [1] + expect(dsl.options).to eq({ a: 1 }) + end + + it "correctly passes hash arguments" do + described_class.dsl_eval(dsl) { configure(1, { a: 1 }) } if RUBY_VERSION >= "3.0.0" - it "correctly passes hash arguments on Ruby 3+" do - Docile.dsl_eval(dsl) { configure(1, { a: 1 }) } - - expect(dsl.arguments).to eq [1, { a: 1 }] - expect(dsl.options).to eq({}) - end - elsif RUBY_VERSION >= "2.0.0" - it "correctly passes hash arguments on Ruby 2" do - Docile.dsl_eval(dsl) { configure(1, { a: 1 }) } - - expect(dsl.arguments).to eq [1] - expect(dsl.options).to eq({ a: 1 }) - end + expect(dsl.arguments).to eq [1, { a: 1 }] + expect(dsl.options).to eq({}) + else + expect(dsl.arguments).to eq [1] + expect(dsl.options).to eq({ a: 1 }) end end end @@ -538,9 +581,8 @@ describe Docile do end describe ".dsl_eval_immutable" do - context "when DSL context object is a frozen String" do - let(:original) { "I'm immutable!".freeze } + let(:original) { "I'm immutable!".freeze } # rubocop:disable Style/RedundantFreeze let!(:result) { execute_non_mutating_dsl_against_string } def execute_non_mutating_dsl_against_string @@ -575,16 +617,17 @@ describe Docile do end end end - end describe Docile::FallbackContextProxy do - describe "#instance_variables" do subject { create_fcp_and_set_one_instance_variable.instance_variables } + let(:expected_type_of_names) { type_of_ivar_names_on_this_ruby } let(:actual_type_of_names) { subject.first.class } - let(:excluded) { Docile::FallbackContextProxy::NON_PROXIED_INSTANCE_VARIABLES } + let(:excluded) do + Docile::FallbackContextProxy::NON_PROXIED_INSTANCE_VARIABLES + end def create_fcp_and_set_one_instance_variable fcp = Docile::FallbackContextProxy.new(nil, nil) @@ -609,5 +652,4 @@ describe Docile::FallbackContextProxy do expect(actual_type_of_names).to eq(expected_type_of_names) end end - end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e073a25..37fdd99 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,27 +1,30 @@ +# frozen_string_literal: true + # Code coverage (via SimpleCov) -begin - require "simplecov" - SimpleCov.start do - add_filter "/spec/" # exclude test code - add_filter "/vendor/" # exclude gems which are cached in CI - end +require "simplecov" - # On CI we publish coverage to codecov.io - # To use codecov-action, we need to generate XML based covarage report - if ENV["CI"] == "true" - require "simplecov-cobertura" - SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter - end - - # Due to circular dependency (simplecov depends on docile), remove docile and require again below - Object.send(:remove_const, :Docile) - $LOADED_FEATURES.reject! { |f| f =~ /\/lib\/docile/ } -rescue LoadError - warn "warning: simplecov or codecov gems not found; skipping coverage" +SimpleCov.start do + add_filter "/spec/" # exclude test code + add_filter "/vendor/" # exclude gems which are cached in CI end -lib_dir = File.join(File.dirname(File.dirname(__FILE__)), "lib") -$LOAD_PATH.unshift lib_dir unless $LOAD_PATH.include? lib_dir +# On CI we publish coverage to codecov.io +# To use the codecov action, we need to generate XML based coverage report +if ENV["CI"] == "true" + begin + require "simplecov-cobertura" + SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter + rescue LoadError + warn "simplecov-cobertura gem not found - not generating XML for codecov.io" + end +end + +# Due to circular dependency (SimpleCov depends on Docile), remove docile and +# then require the docile gem again below. +Object.send(:remove_const, :Docile) +$LOADED_FEATURES.reject! { |f| f.include?("/lib/docile") } # Require Docile again, now with coverage enabled +lib_dir = File.join(File.dirname(File.dirname(__FILE__)), "lib") +$LOAD_PATH.unshift lib_dir unless $LOAD_PATH.include? lib_dir require "docile"