Skip to content

Commit

Permalink
Shared specs for Stoplight::Light::Runnable (#176)
Browse files Browse the repository at this point in the history
  • Loading branch information
bolshakov authored Dec 11, 2022
1 parent d4a730e commit 6615b81
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 258 deletions.
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
271 changes: 13 additions & 258 deletions spec/stoplight/light/runnable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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
4 changes: 4 additions & 0 deletions spec/support/light/runnable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

require_relative 'runnable/color'
require_relative 'runnable/run'
75 changes: 75 additions & 0 deletions spec/support/light/runnable/color.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 6615b81

Please sign in to comment.