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
This commit is contained in:
Marc Siegel 2021-05-10 12:26:41 -04:00
parent cb95b8d85d
commit 25114d0c1d
13 changed files with 343 additions and 256 deletions

View File

@ -27,3 +27,5 @@ jobs:
with: with:
name: ${{ matrix.ruby }} name: ${{ matrix.ruby }}
file: ./coverage/coverage.xml file: ./coverage/coverage.xml
- run: bundle exec rubocop
if: matrix.ruby == '3.0'

2
.rubocop.yml Normal file
View File

@ -0,0 +1,2 @@
inherit_gem:
panolint: rubocop.yml

19
Gemfile
View File

@ -1,9 +1,26 @@
# frozen_string_literal: true
source "https://rubygems.org" source "https://rubygems.org"
# CI-only dependencies go here # CI-only dependencies go here
if ENV["CI"] == "true" if ENV["CI"] == "true" # rubocop:disable Style/IfUnlessModifier
gem "simplecov-cobertura", require: false, group: "test" gem "simplecov-cobertura", require: false, group: "test"
end end
# Specify gem's dependencies in docile.gemspec # Specify gem's dependencies in docile.gemspec
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

View File

@ -1,9 +1,11 @@
# frozen_string_literal: true
require "rake/clean" require "rake/clean"
require "bundler/gem_tasks" require "bundler/gem_tasks"
require "rspec/core/rake_task" require "rspec/core/rake_task"
# Default task for `rake` is to run rspec # Default task for `rake` is to run rspec
task :default => [:spec] task default: [:spec]
# Use default rspec rake task # Use default rspec rake task
RSpec::Core::RakeTask.new RSpec::Core::RakeTask.new

View File

@ -1,5 +1,6 @@
$:.push File.expand_path("../lib", __FILE__) # frozen_string_literal: true
require "docile/version"
require_relative "lib/docile/version"
Gem::Specification.new do |s| Gem::Specification.new do |s|
s.name = "docile" s.name = "docile"
@ -17,15 +18,12 @@ Gem::Specification.new do |s|
"semver.org." "semver.org."
s.license = "MIT" s.license = "MIT"
# Specify oldest supported Ruby version (2.5 to support JRuby 9.2.17.0)
s.required_ruby_version = ">= 2.5.0"
# Files included in the gem # Files included in the gem
s.files = `git ls-files -z`.split("\x0").reject do |f| s.files = `git ls-files -z`.split("\x0").reject do |f|
f.match(%r{^(test|spec|features)/}) f.match(%r{^(test|spec|features)/})
end end
s.require_paths = ["lib"] 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"
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require "docile/version" require "docile/version"
require "docile/execution" require "docile/execution"
require "docile/fallback_context_proxy" require "docile/fallback_context_proxy"
@ -86,7 +88,9 @@ module Docile
exec_in_proxy_context(dsl, FallbackContextProxy, *args, &block) exec_in_proxy_context(dsl, FallbackContextProxy, *args, &block)
end 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 module_function :dsl_eval_with_block_return
# Execute a block in the context of an immutable object whose methods, # Execute a block in the context of an immutable object whose methods,

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Docile module Docile
# @api private # @api private
# #
@ -7,15 +9,15 @@ module Docile
# If {NoMethodError} is caught then the exception object will be extended # If {NoMethodError} is caught then the exception object will be extended
# by this module to add filter functionalities. # by this module to add filter functionalities.
module BacktraceFilter module BacktraceFilter
FILTER_PATTERN = /lib\/docile/ FILTER_PATTERN = %r{/lib/docile/}.freeze
def backtrace def backtrace
super.select { |trace| trace !~ FILTER_PATTERN } super.reject { |trace| trace =~ FILTER_PATTERN }
end end
if ::Exception.public_method_defined?(:backtrace_locations) if ::Exception.public_method_defined?(:backtrace_locations)
def backtrace_locations def backtrace_locations
super.select { |location| location.absolute_path !~ FILTER_PATTERN } super.reject { |location| location.absolute_path =~ FILTER_PATTERN }
end end
end end
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require "docile/fallback_context_proxy" require "docile/fallback_context_proxy"
module Docile module Docile
@ -10,6 +12,8 @@ module Docile
# objects. # objects.
# #
# @see Docile.dsl_eval_immutable # @see Docile.dsl_eval_immutable
#
# rubocop:disable Style/MissingRespondToMissing
class ChainingFallbackContextProxy < FallbackContextProxy class ChainingFallbackContextProxy < FallbackContextProxy
# Proxy methods as in {FallbackContextProxy#method_missing}, replacing # Proxy methods as in {FallbackContextProxy#method_missing}, replacing
# `receiver` with the returned value. # `receiver` with the returned value.
@ -19,4 +23,5 @@ module Docile
ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true) ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
end end
# rubocop:enable Style/MissingRespondToMissing
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Docile module Docile
# @api private # @api private
# #
@ -15,7 +17,7 @@ module Docile
# @param block [Proc] the block of DSL commands to be executed # @param block [Proc] the block of DSL commands to be executed
# @return [Object] the return value of the block # @return [Object] the return value of the block
def exec_in_proxy_context(dsl, proxy_type, *args, &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 # Use #equal? to test strict object identity (assuming that this dictum
# from the Ruby docs holds: "[u]nlike ==, the equal? method should never # 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| block_context.instance_variables.each do |ivar|
next unless proxy_context.instance_variables.include?(ivar) next unless proxy_context.instance_variables.include?(ivar)
value_from_dsl_proxy = proxy_context.instance_variable_get(ivar) value_from_dsl_proxy = proxy_context.instance_variable_get(ivar)
block_context.instance_variable_set(ivar, value_from_dsl_proxy) block_context.instance_variable_set(ivar, value_from_dsl_proxy)
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
require "set" require "set"
module Docile module Docile
@ -13,11 +15,13 @@ module Docile
# This is useful for implementing DSL evaluation in the context of an object. # This is useful for implementing DSL evaluation in the context of an object.
# #
# @see Docile.dsl_eval # @see Docile.dsl_eval
#
# rubocop:disable Style/MissingRespondToMissing
class FallbackContextProxy class FallbackContextProxy
# The set of methods which will **not** be proxied, but instead answered # The set of methods which will **not** be proxied, but instead answered
# by this object directly. # by this object directly.
NON_PROXIED_METHODS = Set[:__send__, :object_id, :__id__, :==, :equal?, NON_PROXIED_METHODS = Set[:__send__, :object_id, :__id__, :==, :equal?,
:"!", :"!=", :instance_exec, :instance_variables, :!, :!=, :instance_exec, :instance_variables,
:instance_variable_get, :instance_variable_set, :instance_variable_get, :instance_variable_set,
:remove_instance_variable] :remove_instance_variable]
@ -54,14 +58,18 @@ module Docile
singleton_class. singleton_class.
send(:define_method, :method_missing) do |method, *args, &block| send(:define_method, :method_missing) do |method, *args, &block|
m = method.to_sym 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) receiver.__send__(method.to_sym, *args, &block)
else else
super(method, *args, &block) super(method, *args, &block)
end end
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 # instrument a helper method to remove the above instrumentation
singleton_class. singleton_class.
@ -74,12 +82,8 @@ module Docile
# @return [Array<Symbol>] Instance variable names, excluding # @return [Array<Symbol>] Instance variable names, excluding
# {NON_PROXIED_INSTANCE_VARIABLES} # {NON_PROXIED_INSTANCE_VARIABLES}
#
# @note on Ruby 1.8.x, the instance variable names are actually of
# type `String`.
def instance_variables def instance_variables
# Ruby 1.8.x returns string names, convert to symbols for compatibility super.reject { |v| NON_PROXIED_INSTANCE_VARIABLES.include?(v) }
super.select { |v| !NON_PROXIED_INSTANCE_VARIABLES.include?(v.to_sym) }
end end
# Proxy all methods, excluding {NON_PROXIED_METHODS}, first to `receiver` # 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) ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
end end
# rubocop:enable Style/MissingRespondToMissing
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
module Docile module Docile
# The current version of this library # The current version of this library
VERSION = "1.3.5" VERSION = "1.3.5"

View File

@ -1,9 +1,64 @@
# frozen_string_literal: true
require "spec_helper" require "spec_helper"
require "singleton"
# TODO: Factor single spec file into multiple specs
# rubocop:disable RSpec/MultipleDescribes
describe Docile do describe Docile do
describe ".dsl_eval" 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 context "when DSL context object is an Array" do
let(:array) { [] } let(:array) { [] }
@ -27,23 +82,15 @@ describe Docile do
end end
it "doesn't proxy #__id__" do it "doesn't proxy #__id__" do
Docile.dsl_eval(array) { expect(__id__).not_to eq(array.__id__) } described_class.dsl_eval(array) do
end expect(__id__).not_to eq(array.__id__)
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
end end
Pizza = Struct.new(:cheese, :pepperoni, :bacon, :sauce) it "raises NoMethodError if DSL object doesn't implement the method" do
expect do
class PizzaBuilder described_class.dsl_eval(array) { no_such_method }
def cheese(v=true); @cheese = v; end end.to raise_error(NoMethodError)
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)
end end
end end
@ -56,7 +103,7 @@ describe Docile do
Docile.dsl_eval(builder) do Docile.dsl_eval(builder) do
bacon bacon
cheese cheese
sauce @sauce sauce @sauce # rubocop:disable RSpec/InstanceVariable
end.build end.build
end end
@ -65,31 +112,6 @@ describe Docile do
end end
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 context "when given parameters for the DSL block" do
def parameterized(*args, &block) def parameterized(*args, &block)
Docile.dsl_eval(OuterDSL.new, *args, &block) Docile.dsl_eval(OuterDSL.new, *args, &block)
@ -121,7 +143,16 @@ describe Docile do
end end
context "when block's context has helper methods which call DSL methods" do context "when block's context has helper methods which call DSL methods" do
class BlockContextWithHelperMethods subject { context.method(:factorial_as_dsl_against_array) }
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) def initialize(array_as_dsl)
@array_as_dsl = array_as_dsl @array_as_dsl = array_as_dsl
end end
@ -130,32 +161,28 @@ describe Docile do
# as a DSL to implement it, via helper methods {#calculate_factorials} # as a DSL to implement it, via helper methods {#calculate_factorials}
# and {#save_factorials} which are defined in this class, so therefore # and {#save_factorials} which are defined in this class, so therefore
# outside the block. # outside the block.
def factorial_as_dsl_against_array(n) def factorial_as_dsl_against_array(num)
Docile.dsl_eval(@array_as_dsl) { calculate_factorials(n) }.last Docile.dsl_eval(@array_as_dsl) { calculate_factorials(num) }.last
end end
# Uses the helper method {#save_factorials} below. # Uses the helper method {#save_factorials} below.
def calculate_factorials(n) def calculate_factorials(num)
(2..n).each { |i| save_factorial(i) } (2..num).each { |i| save_factorial(i) }
end end
# Uses the methods {Array#push} and {Array#at} as a DSL from a helper # Uses the methods {Array#push} and {Array#at} as a DSL from a helper
# method defined in the block's context. Successfully calling this # method defined in the block's context. Successfully calling this
# proves that we can find helper methods from outside the block, and # proves that we can find helper methods from outside the block, and
# then find DSL methods from inside those helper methods. # then find DSL methods from inside those helper methods.
def save_factorial(i) def save_factorial(num)
push(i * at(i - 1)) push(num * at(num - 1))
end
end end
end end
subject { context.method(:factorial_as_dsl_against_array) }
let(:context) { BlockContextWithHelperMethods.new(array_as_dsl) }
let(:array_as_dsl) { [1, 1] }
it "finds DSL methods within helper method defined in block's context" do it "finds DSL methods within helper method defined in block's context" do
# see https://en.wikipedia.org/wiki/Factorial # see https://en.wikipedia.org/wiki/Factorial
# rubocop:disable Layout/ExtraSpacing
[ [
[0, 1], [0, 1],
[1, 1], [1, 1],
@ -177,6 +204,7 @@ describe Docile do
array_as_dsl.replace([1, 1]) array_as_dsl.replace([1, 1])
expect(subject.call(n)).to eq expected_factorial expect(subject.call(n)).to eq expected_factorial
end end
# rubocop:enable Layout/ExtraSpacing
end end
it "removes fallback instrumentation from the DSL object after block" do it "removes fallback instrumentation from the DSL object after block" do
@ -200,7 +228,7 @@ describe Docile do
end end
it "removes fallback instrumentation from the DSL object after block" do 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) }. not_to change { context.respond_to?(:method_missing) }.
from(false) from(false)
end end
@ -208,28 +236,31 @@ describe Docile do
end end
context "when DSL have NoMethod error inside" do context "when DSL have NoMethod error inside" do
class DSLWithNoMethod let(:class_dsl_with_no_method) do
def initialize(b); @b = b; end Class.new do
attr_accessor :b
def push_element def push_element
@b.push 1 nil.push(1)
end
end end
end end
it "raise NoMethodError error from nil" do 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 }. 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 end
end end
context "when DSL blocks are nested" do context "when DSL blocks are nested" do
describe "method lookup" do
context "method lookup" do # rubocop:disable Style/SingleLineMethods
it "finds method of outer dsl in outer dsl scope" do it "finds method of outer dsl in outer dsl scope" do
outer { expect(a).to eq("a") } 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 end
it "finds method of inner dsl in inner dsl scope" do 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 { expect(a).to eq("a") }
outer { inner { expect(a).to eq("a") } } outer { inner { expect(a).to eq("a") } }
end end
# rubocop:enable Style/SingleLineMethods
end end
context "local variable lookup" do describe "local variable lookup" do
it "finds local variable from block context in outer dsl scope" do it "finds local variable from block context in outer dsl scope" do
foo = "foo" foo = "foo"
outer { expect(foo).to eq("foo") } outer { expect(foo).to eq("foo") }
@ -270,25 +302,35 @@ describe Docile do
end end
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 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 end
it "proxies instance variable assignments in block in outer dsl scope back into block's context" do it "proxies instance variable assignments in block in outer dsl scope "\
@iv1 = "foo"; outer { @iv1 = "bar" }; expect(@iv1).to eq("bar") "back into block's context" do
@iv1 = "foo"
outer { @iv1 = "bar" }
expect(@iv1).to eq("bar")
end end
it "finds instance variable from block definition in inner dsl scope" do 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 end
it "proxies instance variable assignments in block in inner dsl scope back into block's context" do it "proxies instance variable assignments in block in inner dsl scope "\
@iv2 = "foo"; outer { inner { @iv2 = "bar" } }; expect(@iv2).to eq("bar") "back into block's context" do
@iv2 = "foo"
outer { inner { @iv2 = "bar" } }
expect(@iv2).to eq("bar")
end end
# rubocop:enable RSpec/InstanceVariable
end 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 # see https://github.com/ms-ati/docile/issues/31
subject do subject do
identified_selves = {} identified_selves = {}
@ -322,36 +364,44 @@ describe Docile do
end end
context "when DSL context object is a Dispatch pattern" do context "when DSL context object is a Dispatch pattern" do
class DispatchScope let(:class_message_dispatcher) do
def params Class.new do
{ :a => 1, :b => 2, :c => 3 }
end
end
class MessageDispatch
include Singleton
def initialize def initialize
@responders = {} @responders = {}
end end
def add_responder path, &block def add_responder(path, &block)
@responders[path] = block @responders[path] = block
end end
def dispatch path, request def dispatch(path, request)
Docile.dsl_eval(DispatchScope.new, request, &@responders[path]) Docile.
dsl_eval(class_dispatch_scope.new, request, &@responders[path])
end end
def class_dispatch_scope
Class.new do
def params
{ a: 1, b: 2, c: 3 }
end
end
end
end
end
let(:message_dispatcher_instance) do
class_message_dispatcher.new
end end
def respond(path, &block) def respond(path, &block)
MessageDispatch.instance.add_responder(path, &block) message_dispatcher_instance.add_responder(path, &block)
end end
def send_request(path, request) def send_request(path, request)
MessageDispatch.instance.dispatch(path, request) message_dispatcher_instance.dispatch(path, request)
end end
# rubocop:disable RSpec/InstanceVariable
it "dispatches correctly" do it "dispatches correctly" do
@first = @second = nil @first = @second = nil
@ -363,12 +413,16 @@ describe Docile do
@second = "Got a new #{bike}" @second = "Got a new #{bike}"
end end
def x(y) ; "Got a #{y}"; end def third(val)
respond "/third" do |third| "Got a #{val}"
expect(x(third)).to eq("Got a third thing") end
respond "/third" do |arg|
expect(third(arg)).to eq("Got a third thing")
end end
fourth = nil fourth = nil
respond "/params" do |arg| respond "/params" do |arg|
fourth = params[arg] fourth = params[arg]
end end
@ -382,18 +436,19 @@ describe Docile do
expect(@second).to eq("Got a new ten speed") expect(@second).to eq("Got a new ten speed")
expect(fourth).to eq(2) expect(fourth).to eq(2)
end end
# rubocop:enable RSpec/InstanceVariable
end end
context "when DSL context object is the same as the block's context object" do context "when DSL context object is same as the block's context object" do
class DSLContextSameAsBlockContext let(:class_context_same_as_block_context) do
def foo(v = nil) Class.new do
@foo = v if v def foo(val = nil)
@foo = val if val
@foo @foo
end end
def bar(v = nil) def bar(val = nil)
@bar = v if v @bar = val if val
@bar @bar
end end
@ -402,67 +457,63 @@ describe Docile do
end end
def dsl_eval_string(string) def dsl_eval_string(string)
block = binding.eval("proc { #{string} }") block = binding.eval("proc { #{string} }") # rubocop:disable all
dsl_eval(block) dsl_eval(block)
end end
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 it "calls DSL methods and sets state on the DSL context object" do
dsl.dsl_eval_string('foo 0; bar 1') dsl.dsl_eval_string("foo 0; bar 1")
expect(dsl.foo).to eq(0) expect(dsl.foo).to eq(0)
expect(dsl.bar).to eq(1) expect(dsl.bar).to eq(1)
end end
context "when the DSL object is frozen" do context "when the DSL object is frozen" do
it "can call non-mutative code without raising an exception" 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 end
end end
context "when NoMethodError is raised" do context "when NoMethodError is raised" do
specify "#backtrace does not include path to Docile's source file" do specify "#backtrace doesn't include path to Docile's sources" do
begin described_class.dsl_eval(Object.new) { foo }
Docile.dsl_eval(Object.new) { foo }
rescue NoMethodError => e rescue NoMethodError => e
expect(e.backtrace).not_to include(match(/lib\/docile/)) expect(e.backtrace).not_to include(match(%r{/lib/docile/}))
end
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
end 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/))
end
end
end
end
if RUBY_VERSION >= "2.0.0"
context "when a DSL method has a keyword argument" do context "when a DSL method has a keyword argument" do
class DSLMethodWithKeywordArgument let(:class_with_method_with_keyword_arg) do
Class.new do
attr_reader :v0, :v1, :v2 attr_reader :v0, :v1, :v2
class_eval(<<-METHOD)
def set(v0, v1:, v2:) def set(arg, kw1:, kw2:)
@v0 = v0 @v0 = arg
@v1 = v1 @v1 = kw1
@v2 = v2 @v2 = kw2
end
end end
METHOD
end end
let(:dsl) do let(:dsl) { class_with_method_with_keyword_arg.new }
DSLMethodWithKeywordArgument.new
end
it "calls such DSL methods with no stderr output" do it "calls such DSL methods with no stderr output" do
# This is to check warnings related to keyword argument is not output. # 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/ # 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) } }. expect do
described_class.dsl_eval(dsl) { set(0, kw2: 2, kw1: 1) }
end.
not_to output.to_stderr not_to output.to_stderr
expect(dsl.v0).to eq 0 expect(dsl.v0).to eq 0
@ -470,49 +521,41 @@ describe Docile do
expect(dsl.v2).to eq 2 expect(dsl.v2).to eq 2
end end
end end
end
if RUBY_VERSION >= "2.0.0"
context "when a DSL method has a double splat" do context "when a DSL method has a double splat" do
class DSLMethodWithDoubleSplat let(:class_with_method_with_double_splat) do
Class.new do
attr_reader :arguments, :options 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) def configure(*arguments, **options)
@arguments = arguments.dup @arguments = arguments.dup
@options = options.dup @options = options.dup
end end
METHOD end
end end
let(:dsl) { DSLMethodWithDoubleSplat.new } let(:dsl) { class_with_method_with_double_splat.new }
it "correctly passes keyword arguments" do it "correctly passes keyword arguments" do
Docile.dsl_eval(dsl) { configure(1, a: 1) } described_class.dsl_eval(dsl) { configure(1, a: 1) }
expect(dsl.arguments).to eq [1] expect(dsl.arguments).to eq [1]
expect(dsl.options).to eq({ a: 1 }) expect(dsl.options).to eq({ a: 1 })
end end
it "correctly passes hash arguments" do
described_class.dsl_eval(dsl) { configure(1, { a: 1 }) }
if RUBY_VERSION >= "3.0.0" 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.arguments).to eq [1, { a: 1 }]
expect(dsl.options).to eq({}) expect(dsl.options).to eq({})
end else
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.arguments).to eq [1]
expect(dsl.options).to eq({ a: 1 }) expect(dsl.options).to eq({ a: 1 })
end end
end end
end end
end end
end
describe ".dsl_eval_with_block_return" do describe ".dsl_eval_with_block_return" do
let(:array) { [] } let(:array) { [] }
@ -538,9 +581,8 @@ describe Docile do
end end
describe ".dsl_eval_immutable" do describe ".dsl_eval_immutable" do
context "when DSL context object is a frozen String" 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 } let!(:result) { execute_non_mutating_dsl_against_string }
def execute_non_mutating_dsl_against_string def execute_non_mutating_dsl_against_string
@ -575,16 +617,17 @@ describe Docile do
end end
end end
end end
end end
describe Docile::FallbackContextProxy do describe Docile::FallbackContextProxy do
describe "#instance_variables" do describe "#instance_variables" do
subject { create_fcp_and_set_one_instance_variable.instance_variables } subject { create_fcp_and_set_one_instance_variable.instance_variables }
let(:expected_type_of_names) { type_of_ivar_names_on_this_ruby } let(:expected_type_of_names) { type_of_ivar_names_on_this_ruby }
let(:actual_type_of_names) { subject.first.class } 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 def create_fcp_and_set_one_instance_variable
fcp = Docile::FallbackContextProxy.new(nil, nil) 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) expect(actual_type_of_names).to eq(expected_type_of_names)
end end
end end
end end

View File

@ -1,27 +1,30 @@
# frozen_string_literal: true
# Code coverage (via SimpleCov) # Code coverage (via SimpleCov)
begin
require "simplecov" require "simplecov"
SimpleCov.start do SimpleCov.start do
add_filter "/spec/" # exclude test code add_filter "/spec/" # exclude test code
add_filter "/vendor/" # exclude gems which are cached in CI add_filter "/vendor/" # exclude gems which are cached in CI
end end
# On CI we publish coverage to codecov.io # On CI we publish coverage to codecov.io
# To use codecov-action, we need to generate XML based covarage report # To use the codecov action, we need to generate XML based coverage report
if ENV["CI"] == "true" if ENV["CI"] == "true"
begin
require "simplecov-cobertura" require "simplecov-cobertura"
SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter 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 rescue LoadError
warn "warning: simplecov or codecov gems not found; skipping coverage" warn "simplecov-cobertura gem not found - not generating XML for codecov.io"
end
end end
lib_dir = File.join(File.dirname(File.dirname(__FILE__)), "lib") # Due to circular dependency (SimpleCov depends on Docile), remove docile and
$LOAD_PATH.unshift lib_dir unless $LOAD_PATH.include? lib_dir # 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 # 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" require "docile"