From 25114d0c1d5ec243af572a972fa568838ec2c47e Mon Sep 17 00:00:00 2001 From: Marc Siegel Date: Mon, 10 May 2021 12:26:41 -0400 Subject: [PATCH] Add Rubocop and run checks in CI This should finally allow us to adopt code style lints, as well as others such as performance linting, and have them enforced by the Github Actions continuous integration (CI) jobs. For now, I'm choosing to depend on the 'panolint' gem from Panorama Education, which is my current workplace, as I'd like to adopt and stay consistent with the setting used there. Changes * Move development and test dependencies from gemspec to Gemfile * Add dependency on 'panolint' gem * Add .rubocop.yml * Fix all existing issues * Run rubocop in github actions CI --- .github/workflows/main.yml | 2 + .rubocop.yml | 2 + Gemfile | 19 +- Rakefile | 4 +- docile.gemspec | 18 +- lib/docile.rb | 6 +- lib/docile/backtrace_filter.rb | 8 +- lib/docile/chaining_fallback_context_proxy.rb | 7 +- lib/docile/execution.rb | 5 +- lib/docile/fallback_context_proxy.rb | 21 +- lib/docile/version.rb | 2 + spec/docile_spec.rb | 462 ++++++++++-------- spec/spec_helper.rb | 43 +- 13 files changed, 343 insertions(+), 256 deletions(-) create mode 100644 .rubocop.yml 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"