# frozen_string_literal: true require 'spec_helper' RSpec.describe 'Enumerator#next patch fix' do describe 'Enumerator' do RSpec::Matchers.define :contain_unique_method_calls_in_order do |expected| attr_reader :actual match do |actual| @actual_err = actual regexps = expected.map { |method_name| { name: method_name, regexp: make_regexp(method_name) } } @actual = actual.backtrace.filter_map do |line| regexp = regexps.find { |r| r[:regexp].match? line } regexp[:name] if regexp end expected == @actual end diffable failure_message do "#{super()}\n\nFull error backtrace:\n #{@actual_err.backtrace.join("\n ")}" end private def make_regexp(method_name) Regexp.new("/spec/initializers/enumerator_next_patch_spec\\.rb:[0-9]+:in `#{method_name}'$") end end def have_been_raised_by_next_and_not_fixed_up contain_unique_method_calls_in_order %w(call_enum_method) end def have_been_raised_by_enum_object_and_fixed_up contain_unique_method_calls_in_order %w(make_error call_enum_method) end def have_been_raised_by_nested_next_and_fixed_up contain_unique_method_calls_in_order %w(call_nested_next call_enum_method) end methods = [ { name: 'next', expected_value: 'Test value' }, { name: 'next_values', expected_value: ['Test value'] }, { name: 'peek', expected_value: 'Test value' }, { name: 'peek_values', expected_value: ['Test value'] } ] methods.each do |method| describe "##{method[:name]}" do def call_enum_method enumerator.send(method_name) end let(:method_name) { method[:name] } subject { call_enum_method } describe 'normal yield' do let(:enumerator) { Enumerator.new { |yielder| yielder << 'Test value' } } it 'returns yielded value' do is_expected.to eq(method[:expected_value]) end end describe 'end of iteration' do let(:enumerator) { Enumerator.new { |_| } } it 'does not fix up StopIteration' do expect { subject }.to raise_error do |err| expect(err).to be_a(StopIteration) expect(err).to have_been_raised_by_next_and_not_fixed_up end end context 'nested enum object' do def call_nested_next nested_enumerator.next end let(:nested_enumerator) { Enumerator.new { |_| } } let(:enumerator) { Enumerator.new { |yielder| yielder << call_nested_next } } it 'fixes up StopIteration thrown by another instance of #next' do expect { subject }.to raise_error do |err| expect(err).to be_a(StopIteration) expect(err).to have_been_raised_by_nested_next_and_fixed_up end end end end describe 'arguments error' do def call_enum_method enumerator.send(method_name, 'extra argument') end let(:enumerator) { Enumerator.new { |_| } } it 'does not fix up ArgumentError' do expect { subject }.to raise_error do |err| expect(err).to be_a(ArgumentError) expect(err).to have_been_raised_by_next_and_not_fixed_up end end end describe 'error' do let(:enumerator) { Enumerator.new { |_| raise error } } let(:error) { make_error } it 'fixes up StopIteration' do def make_error StopIteration.new.tap { |err| err.set_backtrace(caller) } end expect { subject }.to raise_error do |err| expect(err).to be(error) expect(err).to have_been_raised_by_enum_object_and_fixed_up end end it 'fixes up ArgumentError' do def make_error ArgumentError.new.tap { |err| err.set_backtrace(caller) } end expect { subject }.to raise_error do |err| expect(err).to be(error) expect(err).to have_been_raised_by_enum_object_and_fixed_up end end it 'adds backtrace from other errors' do def make_error StandardError.new('This is a test').tap { |err| err.set_backtrace(caller) } end expect { subject }.to raise_error do |err| expect(err).to be(error) expect(err).to have_been_raised_by_enum_object_and_fixed_up expect(err.message).to eq('This is a test') end end end end end end end