From 6615b81da95f10c91ab602a2ac8b65393e7dbc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=ABma=20Bolshakov?= Date: Sun, 11 Dec 2022 23:10:40 +0100 Subject: [PATCH] Shared specs for Stoplight::Light::Runnable (#176) --- spec/spec_helper.rb | 1 + spec/stoplight/light/runnable_spec.rb | 271 ++------------------------ spec/support/light/runnable.rb | 4 + spec/support/light/runnable/color.rb | 75 +++++++ spec/support/light/runnable/run.rb | 219 +++++++++++++++++++++ 5 files changed, 312 insertions(+), 258 deletions(-) create mode 100644 spec/support/light/runnable.rb create mode 100644 spec/support/light/runnable/color.rb create mode 100644 spec/support/light/runnable/run.rb diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index dc638e65..c0d845e6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,6 +5,7 @@ require 'stoplight' require 'timecop' require_relative 'support/data_store/base' +require_relative 'support/light/runnable' require_relative 'support/database_cleaner' Timecop.safe_mode = true diff --git a/spec/stoplight/light/runnable_spec.rb b/spec/stoplight/light/runnable_spec.rb index 96764fb9..75ab0e4e 100644 --- a/spec/stoplight/light/runnable_spec.rb +++ b/spec/stoplight/light/runnable_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' require 'stringio' -RSpec.describe Stoplight::Light::Runnable do - subject { Stoplight::Light.new(name, &code) } +RSpec.describe Stoplight::Light::Runnable, :redis do + subject(:light) { Stoplight::Light.new(name, &code) } let(:code) { -> { code_result } } let(:code_result) { random_string } @@ -24,266 +24,21 @@ def random_string ('a'..'z').to_a.sample(8).join end - describe '#color' do - it 'is initially green' do - expect(subject.color).to eql(Stoplight::Color::GREEN) - end - - it 'is green when locked green' do - subject.data_store.set_state(subject, Stoplight::State::LOCKED_GREEN) - expect(subject.color).to eql(Stoplight::Color::GREEN) - end - - it 'is red when locked red' do - subject.data_store.set_state(subject, Stoplight::State::LOCKED_RED) - expect(subject.color).to eql(Stoplight::Color::RED) - end - - it 'is red when there are many failures' do - subject.threshold.times do - subject.data_store.record_failure(subject, failure) - end - expect(subject.color).to eql(Stoplight::Color::RED) - end - - it 'is yellow when the most recent failure is old' do - (subject.threshold - 1).times do - subject.data_store.record_failure(subject, failure) - end - other = Stoplight::Failure.new( - error.class.name, error.message, Time.new - subject.cool_off_time - ) - subject.data_store.record_failure(subject, other) - expect(subject.color).to eql(Stoplight::Color::YELLOW) - end - - it 'is red when the least recent failure is old' do - other = Stoplight::Failure.new( - error.class.name, error.message, Time.new - subject.cool_off_time - ) - subject.data_store.record_failure(subject, other) - (subject.threshold - 1).times do - subject.data_store.record_failure(subject, failure) - end - expect(subject.color).to eql(Stoplight::Color::RED) - end + before do + light.with_data_store(data_store) end - describe '#run' do - let(:notifiers) { [notifier] } - let(:notifier) { Stoplight::Notifier::IO.new(io) } - let(:io) { StringIO.new } - - before { subject.with_notifiers(notifiers) } - - context 'when the light is green' do - before { subject.data_store.clear_failures(subject) } - - it 'runs the code' do - expect(subject.run).to eql(code_result) - end - - context 'with some failures' do - before { subject.data_store.record_failure(subject, failure) } - - it 'clears the failures' do - subject.run - expect(subject.data_store.get_failures(subject).size).to eql(0) - end - end - - context 'when the code is failing' do - let(:code_result) { raise error } - - it 're-raises the error' do - expect { subject.run }.to raise_error(error.class) - end - - it 'records the failure' do - expect(subject.data_store.get_failures(subject).size).to eql(0) - begin - subject.run - rescue error.class - nil - end - expect(subject.data_store.get_failures(subject).size).to eql(1) - end - - context 'when we did not send notifications yet' do - it 'notifies when transitioning to red' do - subject.threshold.times do - expect(io.string).to eql('') - begin - subject.run - rescue error.class - nil - end - end - expect(io.string).to_not eql('') - end - end - - context 'when we already sent notifications' do - before do - subject.data_store.with_notification_lock(subject, Stoplight::Color::GREEN, Stoplight::Color::RED) {} - end - - it 'does not send new notifications' do - subject.threshold.times do - expect(io.string).to eql('') - begin - subject.run - rescue error.class - nil - end - end - expect(io.string).to eql('') - end - end - - it 'notifies when transitioning to red' do - subject.threshold.times do - expect(io.string).to eql('') - begin - subject.run - rescue error.class - nil - end - end - expect(io.string).to_not eql('') - end - - context 'with an error handler' do - let(:result) do - subject.run - expect(false).to be(true) - rescue error.class - expect(true).to be(true) - end + context 'with memory data store' do + let(:data_store) { Stoplight::DataStore::Memory.new } - it 'records the failure when the handler does nothing' do - subject.with_error_handler { |_error, _handler| } - expect { result } - .to change { subject.data_store.get_failures(subject).size } - .by(1) - end - - it 'records the failure when the handler calls handle' do - subject.with_error_handler { |error, handle| handle.call(error) } - expect { result } - .to change { subject.data_store.get_failures(subject).size } - .by(1) - end - - it 'does not record the failure when the handler raises' do - subject.with_error_handler { |error, _handle| raise error } - expect { result } - .to_not change { subject.data_store.get_failures(subject).size } - end - end - - context 'with a fallback' do - before { subject.with_fallback(&fallback) } - - it 'runs the fallback' do - expect(subject.run).to eql(fallback_result) - end - - it 'passes the error to the fallback' do - subject.with_fallback do |e| - expect(e).to eql(error) - fallback_result - end - expect(subject.run).to eql(fallback_result) - end - end - end - - context 'when the data store is failing' do - let(:data_store) { Object.new } - let(:error_notifier) { ->(_) {} } - - before do - subject - .with_data_store(data_store) - .with_error_notifier(&error_notifier) - end - - it 'runs the code' do - expect(subject.run).to eql(code_result) - end - - it 'notifies about the error' do - has_notified = false - subject.with_error_notifier do |e| - has_notified = true - expect(e).to be_a(NoMethodError) - end - subject.run - expect(has_notified).to eql(true) - end - end - end - - context 'when the light is yellow' do - before do - (subject.threshold - 1).times do - subject.data_store.record_failure(subject, failure) - end - - other = Stoplight::Failure.new( - error.class.name, error.message, time - subject.cool_off_time - ) - subject.data_store.record_failure(subject, other) - end - - it 'runs the code' do - expect(subject.run).to eql(code_result) - end - - it 'notifies when transitioning to green' do - expect(io.string).to eql('') - subject.run - expect(io.string).to_not eql('') - end - end - - context 'when the light is red' do - before do - subject.threshold.times do - subject.data_store.record_failure(subject, failure) - end - end - - it 'raises an error' do - expect { subject.run }.to raise_error(Stoplight::Error::RedLight) - end - - it 'uses the name as the error message' do - e = - begin - subject.run - rescue Stoplight::Error::RedLight => e - e - end - expect(e.message).to eql(subject.name) - end - - context 'with a fallback' do - before { subject.with_fallback(&fallback) } + it_behaves_like 'Stoplight::Light::Runnable#color' + it_behaves_like 'Stoplight::Light::Runnable#run' + end - it 'runs the fallback' do - expect(subject.run).to eql(fallback_result) - end + context 'with redis data store', :redis do + let(:data_store) { Stoplight::DataStore::Redis.new(redis) } - it 'does not pass anything to the fallback' do - subject.with_fallback do |e| - expect(e).to eql(nil) - fallback_result - end - expect(subject.run).to eql(fallback_result) - end - end - end + it_behaves_like 'Stoplight::Light::Runnable#color' + it_behaves_like 'Stoplight::Light::Runnable#run' end end diff --git a/spec/support/light/runnable.rb b/spec/support/light/runnable.rb new file mode 100644 index 00000000..cc4f4ffa --- /dev/null +++ b/spec/support/light/runnable.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +require_relative 'runnable/color' +require_relative 'runnable/run' diff --git a/spec/support/light/runnable/color.rb b/spec/support/light/runnable/color.rb new file mode 100644 index 00000000..ac2ec4b2 --- /dev/null +++ b/spec/support/light/runnable/color.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'Stoplight::Light::Runnable#color' do + it 'is initially green' do + expect(light.color).to eql(Stoplight::Color::GREEN) + end + + context 'when its locked green' do + before do + light.data_store.set_state(light, Stoplight::State::LOCKED_GREEN) + end + + it 'is green' do + expect(light.color).to eql(Stoplight::Color::GREEN) + end + end + + context 'when its locked red' do + before do + light.data_store.set_state(light, Stoplight::State::LOCKED_RED) + end + + it 'is red' do + expect(light.color).to eql(Stoplight::Color::RED) + end + end + + context 'when there are many failures' do + it 'turns red' do + expect do + light.threshold.times do + light.data_store.record_failure(light, failure) + end + end.to change(light, :color).to be(Stoplight::Color::RED) + end + end + + context 'when the most recent failure is old' do + let(:other) do + Stoplight::Failure.new( + error.class.name, error.message, Time.new - light.cool_off_time + ) + end + + before do + (light.threshold - 1).times do + light.data_store.record_failure(light, failure) + end + end + + it 'turns yellow' do + expect do + light.data_store.record_failure(light, other) + end.to change(light, :color).to be(Stoplight::Color::YELLOW) + end + end + + context 'when the least recent failure is old' do + let(:other) do + Stoplight::Failure.new(error.class.name, error.message, Time.new - light.cool_off_time) + end + + before do + light.data_store.record_failure(light, other) + end + + it 'is red when the least recent failure is old' do + expect do + (light.threshold - 1).times do + light.data_store.record_failure(light, failure) + end + end.to change(light, :color).to be(Stoplight::Color::RED) + end + end +end diff --git a/spec/support/light/runnable/run.rb b/spec/support/light/runnable/run.rb new file mode 100644 index 00000000..bee7d13d --- /dev/null +++ b/spec/support/light/runnable/run.rb @@ -0,0 +1,219 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'Stoplight::Light::Runnable#run' do + let(:notifiers) { [notifier] } + let(:notifier) { Stoplight::Notifier::IO.new(io) } + let(:io) { StringIO.new } + + before { light.with_notifiers(notifiers) } + + context 'when the light is green' do + before { light.data_store.clear_failures(light) } + + it 'runs the code' do + expect(light.run).to eql(code_result) + end + + context 'with some failures' do + before { light.data_store.record_failure(light, failure) } + + it 'clears the failures' do + light.run + expect(light.data_store.get_failures(light).size).to eql(0) + end + end + + context 'when the code is failing' do + let(:code_result) { raise error } + + it 're-raises the error' do + expect { light.run }.to raise_error(error.class) + end + + it 'records the failure' do + expect(light.data_store.get_failures(light).size).to eql(0) + begin + light.run + rescue error.class + nil + end + expect(light.data_store.get_failures(light).size).to eql(1) + end + + context 'when we did not send notifications yet' do + it 'notifies when transitioning to red' do + light.threshold.times do + expect(io.string).to eql('') + begin + light.run + rescue error.class + nil + end + end + expect(io.string).to_not eql('') + end + end + + context 'when we already sent notifications' do + before do + light.data_store.with_notification_lock(light, Stoplight::Color::GREEN, Stoplight::Color::RED) {} + end + + it 'does not send new notifications' do + light.threshold.times do + expect(io.string).to eql('') + begin + light.run + rescue error.class + nil + end + end + expect(io.string).to eql('') + end + end + + it 'notifies when transitioning to red' do + light.threshold.times do + expect(io.string).to eql('') + begin + light.run + rescue error.class + nil + end + end + expect(io.string).to_not eql('') + end + + context 'with an error handler' do + let(:result) do + light.run + expect(false).to be(true) + rescue error.class + expect(true).to be(true) + end + + it 'records the failure when the handler does nothing' do + light.with_error_handler { |_error, _handler| } + expect { result } + .to change { light.data_store.get_failures(light).size } + .by(1) + end + + it 'records the failure when the handler calls handle' do + light.with_error_handler { |error, handle| handle.call(error) } + expect { result } + .to change { light.data_store.get_failures(light).size } + .by(1) + end + + it 'does not record the failure when the handler raises' do + light.with_error_handler { |error, _handle| raise error } + expect { result } + .to_not change { light.data_store.get_failures(light).size } + end + end + + context 'with a fallback' do + before { light.with_fallback(&fallback) } + + it 'runs the fallback' do + expect(light.run).to eql(fallback_result) + end + + it 'passes the error to the fallback' do + light.with_fallback do |e| + expect(e).to eql(error) + fallback_result + end + expect(light.run).to eql(fallback_result) + end + end + end + + context 'when the data store is failing' do + let(:error_notifier) { ->(_) {} } + let(:error) { StandardError.new('something went wrong') } + + before do + expect(data_store).to receive(:clear_failures) { raise error } + + light.with_error_notifier(&error_notifier) + end + + it 'runs the code' do + expect(light.run).to eql(code_result) + end + + it 'notifies about the error' do + has_notified = false + light.with_error_notifier do |e| + has_notified = true + expect(e).to eq(error) + end + light.run + expect(has_notified).to eql(true) + end + end + end + + context 'when the light is yellow' do + before do + (light.threshold - 1).times do + light.data_store.record_failure(light, failure) + end + + other = Stoplight::Failure.new( + error.class.name, error.message, time - light.cool_off_time + ) + light.data_store.record_failure(light, other) + end + + it 'runs the code' do + expect(light.run).to eql(code_result) + end + + it 'notifies when transitioning to green' do + expect(io.string).to eql('') + light.run + expect(io.string).to_not eql('') + end + end + + context 'when the light is red' do + before do + light.threshold.times do + light.data_store.record_failure(light, failure) + end + end + + it 'raises an error' do + expect { light.run }.to raise_error(Stoplight::Error::RedLight) + end + + it 'uses the name as the error message' do + e = + begin + light.run + rescue Stoplight::Error::RedLight => e + e + end + expect(e.message).to eql(light.name) + end + + context 'with a fallback' do + before { light.with_fallback(&fallback) } + + it 'runs the fallback' do + expect(light.run).to eql(fallback_result) + end + + it 'does not pass anything to the fallback' do + light.with_fallback do |e| + expect(e).to eql(nil) + fallback_result + end + expect(light.run).to eql(fallback_result) + end + end + end +end