From 4420f2a78e8c44edd9ee78f158afb39f62cf6f2e Mon Sep 17 00:00:00 2001 From: Rob Smith Date: Wed, 18 Sep 2024 23:26:01 -0400 Subject: [PATCH] Clean up ExampleConstants concern. --- .rubocop.yml | 3 + .../concerns/example_constants.rb | 181 +++++---- .../concerns/example_constants_spec.rb | 345 ++++++++++-------- spec/support/constants.rb | 5 + spec/support/constants/example_class.rb | 7 + spec/support/mock_example_group.rb | 34 +- 6 files changed, 332 insertions(+), 243 deletions(-) create mode 100644 spec/support/constants.rb create mode 100644 spec/support/constants/example_class.rb diff --git a/.rubocop.yml b/.rubocop.yml index 41903ef..3c79f4f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,6 +12,7 @@ AllCops: - rspec-sleeping_king_studios.gemspec - lib/rspec/sleeping_king_studios.rb - lib/rspec/sleeping_king_studios/concerns.rb + - lib/rspec/sleeping_king_studios/concerns/example_constants.rb - lib/rspec/sleeping_king_studios/concerns/memoized_helpers.rb - lib/rspec/sleeping_king_studios/configuration.rb - lib/rspec/sleeping_king_studios/deferred.rb @@ -22,6 +23,7 @@ AllCops: - spec/integration/concerns/**/* - spec/integration/deferred/**/* - spec/rspec/sleeping_king_studios_spec.rb + - spec/rspec/sleeping_king_studios/concerns/example_constants_spec.rb - spec/rspec/sleeping_king_studios/concerns/memoized_helpers_spec.rb - spec/rspec/sleeping_king_studios/configuration_spec.rb - spec/rspec/sleeping_king_studios/deferred/**/* @@ -30,6 +32,7 @@ AllCops: - spec/rspec/sleeping_king_studios/support/shared_examples/deferred_call_examples.rb - spec/spec_helper.rb - spec/support/integration/**/*.rb + - spec/support/mock_example_group.rb - '*.thor' Exclude: - 'tmp/**/*' diff --git a/lib/rspec/sleeping_king_studios/concerns/example_constants.rb b/lib/rspec/sleeping_king_studios/concerns/example_constants.rb index 4f7226a..c3dfe49 100644 --- a/lib/rspec/sleeping_king_studios/concerns/example_constants.rb +++ b/lib/rspec/sleeping_king_studios/concerns/example_constants.rb @@ -1,107 +1,140 @@ -# lib/rspec/sleeping_king_studios/concerns/example_constants.rb +# frozen_string_literal: true require 'rspec/sleeping_king_studios/concerns' module RSpec::SleepingKingStudios::Concerns - module ExampleConstants + # Methods for defining example-scoped classes and constants. + module ExampleConstants # rubocop:disable Metrics/ModuleLength DEFAULT_VALUE = Object.new.freeze private_constant :DEFAULT_VALUE - def self.assign_constant namespace, constant_name, constant_value - prior_value = DEFAULT_VALUE + class << self + # @api private + def define_class(class_name:, example:, base_class: nil, &block) + klass = Class.new(resolve_base_class(base_class)) - if namespace.const_defined?(constant_name) - prior_value = namespace.const_get(constant_name) - end # if + klass.define_singleton_method(:name) { class_name } + klass.singleton_class.send(:alias_method, :inspect, :name) + klass.singleton_class.send(:alias_method, :to_s, :name) - namespace.const_set(constant_name, constant_value) + example.instance_exec(klass, &block) if block_given? - yield - ensure - if prior_value == DEFAULT_VALUE - namespace.send :remove_const, constant_name - else - namespace.const_set(constant_name, prior_value) - end # if-else - end # class method assign_constant + klass + end - def self.guard_existing_constant! namespace, constant_name - return unless namespace.const_defined?(constant_name) + # @api private + def with_constant( # rubocop:disable Metrics/MethodLength + constant_name:, + constant_value:, + namespace:, + force: false + ) + guard_existing_constant!(namespace, constant_name) unless force - message = - "constant #{constant_name} is already defined with value "\ - "#{namespace.const_get(constant_name).inspect}" + prior_value = DEFAULT_VALUE - raise NameError, message - end # class method guard_existing_constant! + if namespace.const_defined?(constant_name) + prior_value = namespace.const_get(constant_name) + end - def self.resolve_base_class value - value = value.fetch(:base_class, nil) if value.is_a?(Hash) + namespace.const_set(constant_name, constant_value) - return Object if value.nil? + yield + ensure + if prior_value == DEFAULT_VALUE + namespace.send :remove_const, constant_name + else + namespace.const_set(constant_name, prior_value) + end + end - return Object.const_get(value) if value.is_a?(String) + # @api private + def with_namespace(module_names) # rubocop:disable Metrics/MethodLength + last_defined = nil - value - end + resolved = + module_names.reduce(Object) do |namespace, module_name| + if namespace.const_defined?(module_name) + next namespace.const_get(module_name) + end - def self.resolve_namespace module_names - last_defined = nil + last_defined ||= { namespace:, module_name: } - resolved = - module_names.reduce(Object) do |ns, module_name| - next ns.const_get(module_name) if ns.const_defined?(module_name) + namespace.const_set(module_name, Module.new) + end - last_defined ||= { :namespace => ns, :module_name => module_name } + yield resolved + ensure + if last_defined + last_defined[:namespace] + .send(:remove_const, last_defined[:module_name]) + end + end - ns.const_set(module_name, Module.new) - end # reduce + private - yield resolved - ensure - if last_defined - last_defined[:namespace].send(:remove_const, last_defined[:module_name]) - end # if - end # class method resolve_namespace + def guard_existing_constant!(namespace, constant_name) + return unless namespace.const_defined?(constant_name) - def example_class class_name, base_class = nil, &block - class_name = class_name.to_s if class_name.is_a?(Symbol) + message = + "constant #{constant_name} is already defined with value " \ + "#{namespace.const_get(constant_name).inspect}" - example_constant(class_name) do - klass = Class.new(ExampleConstants.resolve_base_class(base_class)) + raise NameError, message + end - klass.define_singleton_method(:name) { class_name } - klass.singleton_class.send(:alias_method, :inspect, :name) - klass.singleton_class.send(:alias_method, :to_s, :name) + def resolve_base_class(value) + value = value.fetch(:base_class, nil) if value.is_a?(Hash) - instance_exec(klass, &block) if block_given? + return Object if value.nil? - klass - end # example_constant - end # method example_class + return Object.const_get(value) if value.is_a?(String) - def example_constant qualified_name, constant_value = DEFAULT_VALUE, force: false, &block - around(:example) do |wrapped_example| - example = wrapped_example.example + value + end + end + + def example_class(class_name, base_class = nil, &block) + class_name = class_name.to_s if class_name.is_a?(Symbol) + + example_constant(class_name) do + ExampleConstants.define_class( + base_class:, + class_name:, + example: self, + &block + ) + end + end + def example_constant( # rubocop:disable Metrics/MethodLength + qualified_name, + constant_value = DEFAULT_VALUE, + force: false, + &block + ) + around(:example) do |wrapped_example| resolved_value = - if constant_value == DEFAULT_VALUE - block ? example.instance_exec(&block) : nil + if constant_value == DEFAULT_VALUE && block_given? + wrapped_example.example.instance_exec(&block) else constant_value - end # if - - module_names = qualified_name.to_s.split('::') - constant_name = module_names.pop - - ExampleConstants.resolve_namespace(module_names) do |namespace| - ExampleConstants.guard_existing_constant!(namespace, constant_name) unless force - - ExampleConstants.assign_constant(namespace, constant_name, resolved_value) do + end + + *module_names, constant_name = qualified_name.to_s.split('::') + + ExampleConstants.with_namespace(module_names) do |namespace| + ExampleConstants.with_constant( + constant_name:, + constant_value: resolved_value, + namespace:, + force: + ) \ + do wrapped_example.call - end # assign_constant - end # resolve_namespace - end # before example - end # method example_constant - end # module -end # module + end + end + end + end + end +end diff --git a/spec/rspec/sleeping_king_studios/concerns/example_constants_spec.rb b/spec/rspec/sleeping_king_studios/concerns/example_constants_spec.rb index 6515f06..bf23718 100644 --- a/spec/rspec/sleeping_king_studios/concerns/example_constants_spec.rb +++ b/spec/rspec/sleeping_king_studios/concerns/example_constants_spec.rb @@ -1,19 +1,11 @@ -# spec/rspec/sleeping_king_studios/concerns/example_constants_spec.rb - -require 'spec_helper' +# frozen_string_literal: true require 'rspec/sleeping_king_studios/concerns/example_constants' - require 'rspec/sleeping_king_studios/matchers/built_in/respond_to' +require 'support/constants/example_class' require 'support/mock_example_group' -module Spec - module Constants - class ExampleClass; end - end # module -end # module - RSpec.describe RSpec::SleepingKingStudios::Concerns::ExampleConstants do let(:described_class) do Class.new(Spec::Support::MockExampleGroup) do @@ -21,48 +13,64 @@ class ExampleClass; end def helper_method :helper - end # method helper_method - end # class - end # let + end + end + end describe '::example_class' do shared_examples 'should define the class' do |proc = nil| - it 'should define the class' do - if class_proc - described_class.example_class class_name, - *class_args, - &class_proc - else - described_class.example_class class_name, *class_args - end # if-else + def define_example_class + described_class.example_class(class_name, *class_args, &class_proc) + end - expect { Object.const_get class_name }.to raise_error NameError + context 'before the example is run' do # rubocop:disable RSpec/ContextWording + it 'should not define the class' do + define_example_class - expect(described_class.example).to receive(:call) do - klass = Object.const_get class_name + expect { Object.const_get(class_name) }.to raise_error NameError + end + end - expect(klass).to be_a Class - expect(klass.inspect).to be == class_name.to_s - expect(klass.name).to be == class_name.to_s - expect(klass.to_s).to be == class_name.to_s + context 'while the example is running' do # rubocop:disable RSpec/ContextWording + it 'should define the class', :aggregate_failures do # rubocop:disable RSpec/ExampleLength + defined_class = nil - instance_exec(klass, &proc) unless proc.nil? - end # receive + define_example_class + + allow(described_class.example).to receive(:call) do + defined_class = Object.const_get(class_name) + end + + described_class.run_example - described_class.run_example + expect(defined_class).to be_a Class + expect(defined_class.inspect).to be == class_name.to_s + expect(defined_class.name).to be == class_name.to_s + expect(defined_class.to_s).to be == class_name.to_s - expect { Object.const_get class_name }.to raise_error NameError - end # it - end # shared_examples + instance_exec(defined_class, &proc) unless proc.nil? + end + end + + context 'after the example has run' do # rubocop:disable RSpec/ContextWording + it 'should not define the class' do + define_example_class + + described_class.run_example + + expect { Object.const_get(class_name) }.to raise_error NameError + end + end + end let(:class_name) { 'AnswerClass' } let(:class_args) { [] } let(:class_proc) { nil } it 'should define the class method' do - expect(described_class). - to respond_to(:example_class). - with(1..2).arguments + expect(described_class) + .to respond_to(:example_class) + .with(1..2).arguments end describe 'with a class name as a String' do @@ -76,7 +84,7 @@ def helper_method end describe 'with a class name and a base class' do - let(:base_class) { Spec::Constants::ExampleClass } + let(:base_class) { Spec::Support::Constants::ExampleClass } let(:class_args) { base_class } include_examples 'should define the class', @@ -84,7 +92,7 @@ def helper_method end describe 'with a class name and a base class name' do - let(:base_class) { Spec::Constants::ExampleClass } + let(:base_class) { Spec::Support::Constants::ExampleClass } let(:class_args) { base_class.name } include_examples 'should define the class', @@ -92,15 +100,15 @@ def helper_method end describe 'with a class name and base_class: a class' do - let(:base_class) { Spec::Constants::ExampleClass } - let(:class_args) { [{ base_class: base_class }] } + let(:base_class) { Spec::Support::Constants::ExampleClass } + let(:class_args) { [{ base_class: }] } include_examples 'should define the class', ->(klass) { expect(klass.superclass).to be base_class } end describe 'with a class name and base_class: a class name' do - let(:base_class) { Spec::Constants::ExampleClass } + let(:base_class) { Spec::Support::Constants::ExampleClass } let(:class_args) { [{ base_class: base_class.name }] } include_examples 'should define the class', @@ -109,10 +117,8 @@ def helper_method describe 'with a class name and a block' do let(:class_proc) do - lambda do |klass| - klass.send(:define_method, :value) { 42 } - end # lambda - end # let + ->(klass) { klass.send(:define_method, :value) { 42 } } + end include_examples 'should define the class', ->(klass) { expect(klass.new.value).to be 42 } @@ -123,168 +129,203 @@ def helper_method helper_value = helper_method klass.send(:define_method, :helper_value) { helper_value } - end # lambda - end # let + end + end include_examples 'should define the class', ->(klass) { expect(klass.new.helper_value).to be :helper } - end # describe - end # describe - end # describe + end + end + end describe '::example_constant' do + shared_examples 'should define the constant' do + context 'before the example is run' do # rubocop:disable RSpec/ContextWording + it 'should not define the constant' do + define_example_constant + + expect { Object.const_get(class_name) }.to raise_error NameError + end + end + + context 'while the example is running' do # rubocop:disable RSpec/ContextWording + it 'should define the constant' do + defined_constant = nil + + define_example_constant + + allow(described_class.example).to receive(:call) do + defined_constant = Object.const_get(constant_name) + end + + described_class.run_example + + expect(defined_constant).to be constant_value + end + end + + context 'after the example has run' do # rubocop:disable RSpec/ContextWording + it 'should not define the constant' do + define_example_constant + + described_class.run_example + + expect { Object.const_get(constant_name) }.to raise_error NameError + end + end + end + let(:constant_name) { 'THE_ANSWER' } it 'should define the class method' do - expect(described_class). - to respond_to(:example_constant). - with(1..2).arguments. - and_keywords(:force) - end # it + expect(described_class) + .to respond_to(:example_constant) + .with(1..2).arguments + .and_keywords(:force) + end describe 'with a constant name and a value' do let(:constant_value) { 42 } - it 'should set the constant' do - described_class.example_constant constant_name, constant_value + def define_example_constant + described_class.example_constant(constant_name, constant_value) + end - expect { Object.const_get constant_name }.to raise_error NameError - - expect(described_class.example).to receive(:call) do - expect(Object.const_get constant_name).to be constant_value - end # receive - - described_class.run_example - - expect { Object.const_get constant_name }.to raise_error NameError - end # it + include_examples 'should define the constant' context 'when the constant is already defined' do let(:prior_value) { 'Forty-two' } + let(:error_message) do + "constant #{constant_name} is already defined with value " \ + "#{prior_value.inspect}" + end around(:example) do |example| - begin - Object.const_set(:THE_ANSWER, prior_value) + Object.const_set(:THE_ANSWER, prior_value) - example.call - ensure - Object.send(:remove_const, :THE_ANSWER) - end # begin-ensure - end # around example + example.call + ensure + Object.send(:remove_const, :THE_ANSWER) # rubocop:disable RSpec/RemoveConst + end it 'should raise an error' do - message = Regexp.new( - "constant #{constant_name} is already defined with value "\ - "#{prior_value.inspect}" - ) - - described_class.example_constant constant_name, constant_value + described_class.example_constant(constant_name, constant_value) - expect { described_class.run_example }. - to raise_error NameError, message - end # it + expect { described_class.run_example } + .to raise_error(NameError, error_message) + .and(output.to_stderr) + end - describe 'with :force => true' do - let(:captured_warnings) { StringIO.new } + describe 'with force: true' do let(:expected_warning) do - 'warning: already initialized constant THE_ANSWER' + /warning: already initialized constant THE_ANSWER/ + end + + def define_example_constant + described_class + .example_constant(constant_name, constant_value, force: true) end - around(:example) do |example| - begin - stderr = $stderr - $stderr = captured_warnings + # rubocop:disable RSpec/NestedGroups + context 'before the example is run' do # rubocop:disable RSpec/ContextWording + it 'should define the constant with the prior value' do + define_example_constant - example.call - ensure - $stderr = stderr + expect(Object.const_get(constant_name)).to be == prior_value end end - it 'should override the constant' do - described_class.example_constant constant_name, constant_value, :force => true + context 'while the example is running' do # rubocop:disable RSpec/ContextWording + it 'should define the constant', :aggregate_failures do + defined_constant = nil - expect(Object.const_get constant_name).to be == prior_value + define_example_constant - expect(described_class.example).to receive(:call) do - expect(Object.const_get constant_name).to be constant_value - end # receive + allow(described_class.example).to receive(:call) do + defined_constant = Object.const_get(constant_name) + end - described_class.run_example + expect { described_class.run_example } + .to output(expected_warning) + .to_stderr + + expect(defined_constant).to be constant_value + end + end - expect(Object.const_get constant_name).to be == prior_value + context 'after the example has run' do # rubocop:disable RSpec/ContextWording + # rubocop:disable Style/RedundantLineContinuation + it 'should define the constant with the prior value', + :aggregate_failures \ + do + define_example_constant - expect(captured_warnings.string).to include expected_warning - end # it - end # describe - end # context - end # describe + expect { described_class.run_example } + .to output + .to_stderr + + expect(Object.const_get(constant_name)).to be == prior_value + end + # rubocop:enable Style/RedundantLineContinuation + end + # rubocop:enable RSpec/NestedGroups + end + end + end describe 'with a constant name and a block' do let(:constant_value) { 42 } before(:example) do - described_class.send(:define_method, :answer) {} - - allow(described_class.example). - to receive(:answer). - and_return(constant_value) - end # before example - - it 'should set the constant' do - described_class.example_constant(constant_name) { answer } + value = constant_value - expect { Object.const_get constant_name }.to raise_error NameError + described_class.send(:define_method, :answer) { value } - expect(described_class.example).to receive(:call) do - expect(Object.const_get constant_name).to be constant_value - end # receive + allow(described_class.example) + .to receive(:answer) + .and_call_original + end - described_class.run_example + def define_example_constant + described_class.example_constant(constant_name) { answer } + end - expect { Object.const_get constant_name }.to raise_error NameError - end # it - end # describe + include_examples 'should define the constant' + end describe 'with a qualified constant name' do let(:constant_name) { 'Spec::Constants::THE_ANSWER' } let(:constant_value) { 42 } - it 'should set the constant' do - described_class.example_constant constant_name, constant_value - - expect { Object.const_get constant_name }.to raise_error NameError - - expect(described_class.example).to receive(:call) do - expect(Object.const_get constant_name).to be constant_value - end # receive + def define_example_constant + described_class.example_constant(constant_name, constant_value) + end - described_class.run_example - - expect { Object.const_get constant_name }.to raise_error NameError - end # it + include_examples 'should define the constant' context 'when the namespace is undefined' do let(:constant_name) { 'Examples::Constants::THE_ANSWER' } - it 'should set the constant' do - described_class.example_constant constant_name, constant_value - - expect { Object.const_get 'Examples' }.to raise_error NameError + include_examples 'should define the constant' - expect { Object.const_get constant_name }.to raise_error NameError + context 'before the example is run' do # rubocop:disable RSpec/ContextWording + it 'should not define the namespace' do + define_example_constant - expect(described_class.example).to receive(:call) do - expect(Object.const_get constant_name).to be constant_value - end # receive + expect { Object.const_get('Examples') }.to raise_error NameError + end + end - described_class.run_example + context 'after the example has run' do # rubocop:disable RSpec/ContextWording + it 'should not define the namespace' do + define_example_constant - expect { Object.const_get 'Examples' }.to raise_error NameError + described_class.run_example - expect { Object.const_get constant_name }.to raise_error NameError - end # it - end # context - end # describe - end # describe -end # describe + expect { Object.const_get('Examples') }.to raise_error NameError + end + end + end + end + end +end diff --git a/spec/support/constants.rb b/spec/support/constants.rb new file mode 100644 index 0000000..b78a04a --- /dev/null +++ b/spec/support/constants.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Spec::Support + module Constants; end +end diff --git a/spec/support/constants/example_class.rb b/spec/support/constants/example_class.rb new file mode 100644 index 0000000..982f4fd --- /dev/null +++ b/spec/support/constants/example_class.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'support/constants' + +module Spec::Support::Constants + class ExampleClass; end +end diff --git a/spec/support/mock_example_group.rb b/spec/support/mock_example_group.rb index 02e0b43..a972ac7 100644 --- a/spec/support/mock_example_group.rb +++ b/spec/support/mock_example_group.rb @@ -1,49 +1,49 @@ -# spec/support/mock_example_group.rb +# frozen_string_literal: true module Spec module Support class MockExampleGroup def self.around(_scope, &block) - hooks << ->(example) { + hooks << lambda { |example| example.instance_exec(example, &block) - } # end hook - end # class method around + } + end def self.before(scope, &block) around(scope) do |example| example.instance_exec(example, &block) example.call - end # around - end # class method before + end + end def self.example @example ||= new - end # class method example + end def self.hooks @hooks ||= [] - end # class method hooks + end def self.run_example wrapped = hooks.reverse.reduce(example) do |wrapped_example, hook| - ->() { hook.call(wrapped_example) } - end # hook + -> { hook.call(wrapped_example) } + end wrapped.call - end # class method run_example + end def call; end def example self - end # method example + end def inspect '#' - end # method inspect - alias_method :to_s, :inspect - end # class - end # module -end # module + end + alias to_s inspect + end + end +end