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:
parent
cb95b8d85d
commit
25114d0c1d
|
@ -27,3 +27,5 @@ jobs:
|
|||
with:
|
||||
name: ${{ matrix.ruby }}
|
||||
file: ./coverage/coverage.xml
|
||||
- run: bundle exec rubocop
|
||||
if: matrix.ruby == '3.0'
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
inherit_gem:
|
||||
panolint: rubocop.yml
|
19
Gemfile
19
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
|
||||
|
|
4
Rakefile
4
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Symbol>] 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
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Docile
|
||||
# The current version of this library
|
||||
VERSION = "1.3.5"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue