From 4bbec2d8f4fd2a315f81415fdea3c69918de51d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A7=D0=B5=D1=80=D0=BD=D0=B5=D0=BD=D0=BA=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=AE=D1=80=D1=8C?= =?UTF-8?q?=D0=B5=D0=B2=D0=B8=D1=87?= Date: Thu, 24 Oct 2024 10:02:10 +0000 Subject: [PATCH] [DEX-2616] Add composition metrics --- .gitlab-ci.yml | 2 + Gemfile | 2 +- config/initializers/yabeda.rb | 28 ++- lib/sbmt/strangler/action.rb | 10 +- lib/sbmt/strangler/action/composition.rb | 15 -- .../action/composition/composable.rb | 77 --------- .../composition/errors/configuration_error.rb | 2 +- ...tion_level_error.rb => max_level_error.rb} | 4 +- .../strangler/action/composition/metrics.rb | 57 +++++++ lib/sbmt/strangler/action/composition/step.rb | 137 ++++++++++++--- lib/sbmt/strangler/configuration.rb | 1 + lib/sbmt/strangler/version.rb | 2 +- .../action/composition/composable_spec.rb | 81 --------- .../strangler/action/composition/step_spec.rb | 159 ++++++++++++++++++ spec/lib/sbmt/strangler/flipper_spec.rb | 25 +-- 15 files changed, 388 insertions(+), 214 deletions(-) delete mode 100644 lib/sbmt/strangler/action/composition.rb delete mode 100644 lib/sbmt/strangler/action/composition/composable.rb rename lib/sbmt/strangler/action/composition/errors/{max_composition_level_error.rb => max_level_error.rb} (62%) create mode 100644 lib/sbmt/strangler/action/composition/metrics.rb delete mode 100644 spec/lib/sbmt/strangler/action/composition/composable_spec.rb create mode 100644 spec/lib/sbmt/strangler/action/composition/step_spec.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c7d083d..04b8d2b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -10,6 +10,8 @@ tests: matrix: - RUBY_VERSION: ['3.1', '3.2', '3.3'] before_script: + - gem sources --remove https://rubygems.org/ + - gem sources --add https://nexus.sbmt.io/repository/rubygems/ - gem install bundler -v 2.5.7 - bin/setup script: diff --git a/Gemfile b/Gemfile index be173b2..29e56f1 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,5 @@ # frozen_string_literal: true -source "https://rubygems.org" +source "https://nexus.sbmt.io/repository/rubygems/" gemspec diff --git a/config/initializers/yabeda.rb b/config/initializers/yabeda.rb index 4846618..9aec8fa 100644 --- a/config/initializers/yabeda.rb +++ b/config/initializers/yabeda.rb @@ -4,7 +4,9 @@ module Sbmt module Strangler module Metrics module Yabeda - HTTP_BUCKETS = [0.01, 0.02, 0.04, 0.1, 0.2, 0.5, 0.8, 1, 1.5, 2, 5, 15, 30, 60].freeze + DEFAULT_BUCKETS = [0.01, 0.02, 0.04, 0.1, 0.2, 0.5, 0.8, 1, 1.5, 2, 5, 15, 30, 60].freeze + HTTP_BUCKETS = DEFAULT_BUCKETS + COMPOSITION_BUCKETS = DEFAULT_BUCKETS ::Yabeda.configure do group :sbmt_strangler do @@ -38,3 +40,27 @@ module Yabeda end end end + +# Declaring composition step duration metric in an `after_initialize` block +# allows user to customize buckets in his app-level configuration file: +# +# # config/initializers/strangler.rb +# Sbmt::Strangler.configure do |strangler| +# strangler.composition_step_duration_metric_buckets = [0.1, 0.2, 0.3] +# end +# +Rails.application.config.after_initialize do + ::Yabeda.configure do + group :sbmt_strangler do + composition_buckets = + ::Sbmt::Strangler.configuration.composition_step_duration_metric_buckets || + ::Sbmt::Strangler::Metrics::Yabeda::COMPOSITION_BUCKETS + + histogram :composition_step_duration, + tags: %i[step part type level parent controller action], + unit: :seconds, + buckets: composition_buckets, + comment: "Composition step duration" + end + end +end diff --git a/lib/sbmt/strangler/action.rb b/lib/sbmt/strangler/action.rb index 1ec4188..9685bbc 100644 --- a/lib/sbmt/strangler/action.rb +++ b/lib/sbmt/strangler/action.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative "action/composition" +require_relative "action/composition/step" module Sbmt module Strangler @@ -36,9 +36,11 @@ def http_client end def composition(&) - return @composition unless block_given? - - @composition = Sbmt::Strangler::Action::Composition.new(&) + if block_given? + @composition ||= Composition::Step.new(name: :root) + yield(@composition) + end + @composition end def composition? diff --git a/lib/sbmt/strangler/action/composition.rb b/lib/sbmt/strangler/action/composition.rb deleted file mode 100644 index 32c5227..0000000 --- a/lib/sbmt/strangler/action/composition.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -require_relative "composition/errors/max_composition_level_error" -require_relative "composition/composable" -require_relative "composition/step" - -module Sbmt - module Strangler - class Action - class Composition - include Composable - end - end - end -end diff --git a/lib/sbmt/strangler/action/composition/composable.rb b/lib/sbmt/strangler/action/composition/composable.rb deleted file mode 100644 index 38b6958..0000000 --- a/lib/sbmt/strangler/action/composition/composable.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -module Sbmt - module Strangler - class Action - class Composition - module Composable - MAX_COMPOSITION_LEVEL = 2 - - def initialize(composition_level: 0, &) - if composition_level > MAX_COMPOSITION_LEVEL - raise Sbmt::Strangler::Action::Composition::Errors::MaxCompositionLevelError - end - - @composition_level = composition_level - @sync_steps = {} - @async_steps = {} - - block_given? ? yield(self) : self - end - - def call(rails_controller, previous_responses: {}) - async_responses = async_steps.map do |name, step| - Concurrent::Promises.future do - Rails.application.executor.wrap do - result = step.call(rails_controller, previous_responses: previous_responses) - {name => result} - end - end - end - - sync_responses = sync_steps.reduce(previous_responses) do |result, (name, step)| - result.merge(name => step.call(rails_controller, previous_responses: result)) - end - - responses = async_responses.map(&:value).reduce(sync_responses) do |result, step_result| - result.merge(step_result) - end - - compose_block.call(responses, rails_controller) - rescue => error - Sbmt::Strangler.logger.error(error.message) - Sbmt::Strangler.error_tracker.error(error) - - {} - end - - def sync(name, &) - @sync_steps[name] = Sbmt::Strangler::Action::Composition::Step.new( - name: name, - composition_level: composition_level + 1, & - ) - end - - def async(name, &) - @async_steps[name] = Sbmt::Strangler::Action::Composition::Step.new( - name: name, - composition_level: composition_level + 1, & - ) - end - - def composable? - @sync_steps.any? || @async_steps.any? - end - - def compose(&block) - @compose_block = block - end - - private - - attr_reader :sync_steps, :async_steps, :composition_level, :compose_block - end - end - end - end -end diff --git a/lib/sbmt/strangler/action/composition/errors/configuration_error.rb b/lib/sbmt/strangler/action/composition/errors/configuration_error.rb index ae59d97..c57bcf9 100644 --- a/lib/sbmt/strangler/action/composition/errors/configuration_error.rb +++ b/lib/sbmt/strangler/action/composition/errors/configuration_error.rb @@ -3,7 +3,7 @@ module Sbmt module Strangler class Action - class Composition + module Composition module Errors class ConfigurationError < StandardError; end end diff --git a/lib/sbmt/strangler/action/composition/errors/max_composition_level_error.rb b/lib/sbmt/strangler/action/composition/errors/max_level_error.rb similarity index 62% rename from lib/sbmt/strangler/action/composition/errors/max_composition_level_error.rb rename to lib/sbmt/strangler/action/composition/errors/max_level_error.rb index a02c5e5..a84879e 100644 --- a/lib/sbmt/strangler/action/composition/errors/max_composition_level_error.rb +++ b/lib/sbmt/strangler/action/composition/errors/max_level_error.rb @@ -3,9 +3,9 @@ module Sbmt module Strangler class Action - class Composition + module Composition module Errors - class MaxCompositionLevelError < StandardError; end + class MaxLevelError < StandardError; end end end end diff --git a/lib/sbmt/strangler/action/composition/metrics.rb b/lib/sbmt/strangler/action/composition/metrics.rb new file mode 100644 index 0000000..545cb32 --- /dev/null +++ b/lib/sbmt/strangler/action/composition/metrics.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Sbmt + module Strangler + class Action + module Composition + module Metrics + private + + def with_metrics(rails_controller:, part: nil) + result = nil + with_yabeda_duration_measurement(rails_controller: rails_controller, part: part) do + with_open_telemetry_tracing(part: part) do + result = yield + end + end + result + end + + def with_yabeda_duration_measurement(rails_controller:, part: nil) + result = nil + yabeda_tags = { + step: name.to_s, + part: part&.to_s, + type: type.to_s, + level: level.to_s, + parent: parent&.name&.to_s, + controller: rails_controller.controller_path, + action: rails_controller.action_name + } + Yabeda.sbmt_strangler.composition_step_duration.measure(yabeda_tags) do + result = yield + end + result + end + + def with_open_telemetry_tracing(part: nil) + return yield unless Object.const_defined?(:OpenTelemetry) + + span_name = "Composition step: #{name}" + span_name += " (#{part})" unless part.nil? + + span_attrs = {type: type.to_s, level: level} + span_attrs[:parent] = parent.name.to_s unless parent.nil? + + result = nil + ::OpenTelemetry.tracer_provider.tracer("Sbmt::Strangler") + .in_span(span_name, attributes: span_attrs, kind: :internal) do |_span| + result = yield + end + result + end + end + end + end + end +end diff --git a/lib/sbmt/strangler/action/composition/step.rb b/lib/sbmt/strangler/action/composition/step.rb index 29c8cc8..0a54485 100644 --- a/lib/sbmt/strangler/action/composition/step.rb +++ b/lib/sbmt/strangler/action/composition/step.rb @@ -1,49 +1,146 @@ # frozen_string_literal: true +require_relative "metrics" +require_relative "errors/configuration_error" +require_relative "errors/max_level_error" + module Sbmt module Strangler class Action - class Composition + module Composition class Step - include Composable + include Metrics - def initialize(name:, composition_level: 0, &) - @name = name + TYPES = %i[sync async].freeze + MAX_LEVEL = 2 + + attr_reader :name, :type, :level, :parent + + def initialize(name:, type: :sync, level: 0, parent: nil) + if name.nil? || name.to_s == "" + raise Errors::ConfigurationError, "Composition step name must be a non-empty string or symbol" + end + + if TYPES.exclude?(type) + raise Errors::ConfigurationError, "Composition step type must be a symbol from #{TYPES}" + end + + if !parent.nil? && !parent.is_a?(self.class) + raise Errors::ConfigurationError, "Composition step parent must be either #{self.class} or nil" + end - super(composition_level: composition_level, &) + if !level.is_a?(Integer) && level >= 0 + raise Errors::ConfigurationError, "Composition step level must be a non-negative integer" + end + + if level > MAX_LEVEL + raise Errors::MaxLevelError, "Composition step is too deeply nested" + end + + @name = name + @type = type + @level = level + @parent = parent + @sync_steps = {} + @async_steps = {} end def call(rails_controller, previous_responses: {}) - result = begin - process_lambda.call(rails_controller, previous_responses) - rescue => error - Sbmt::Strangler.logger.error(error.message) - Sbmt::Strangler.error_tracker.error(error) + with_metrics(rails_controller: rails_controller) do + result = begin + with_metrics(part: :process, rails_controller: rails_controller) do + process_lambda.call(rails_controller, previous_responses) + end + rescue => error + handle_error(error) - {} - end + {} # TODO: Better error handling in composition. + end + + # process composition if there are nested steps + if composable? + result = call_composition(rails_controller, previous_responses: previous_responses.merge(name => result)) + end - # process composition if there are nested steps - if composable? - result = super(rails_controller, previous_responses: previous_responses.merge(name => result)) + result end + end + + def sync(name, &) + step = Sbmt::Strangler::Action::Composition::Step.new( + name: name, + type: :sync, + parent: self, + level: level + 1 + ) + yield(step) if block_given? + @sync_steps[name] = step + step + end - result + def async(name, &) + step = Sbmt::Strangler::Action::Composition::Step.new( + name: name, + type: :async, + parent: self, + level: level + 1 + ) + yield(step) if block_given? + @async_steps[name] = step + step end def process(&block) @process_lambda = block - self end - def with_composition(&) - yield(self) + def compose(&block) + @compose_lambda = block + self end + alias_method :with_composition, :tap + private - attr_reader :process_lambda, :name + attr_reader :sync_steps, :async_steps, :process_lambda, :compose_lambda + + def composable? + @sync_steps.any? || @async_steps.any? + end + + def call_composition(rails_controller, previous_responses: {}) + async_responses = async_steps.map do |name, step| + Concurrent::Promises.future do + Rails.application.executor.wrap do + result = step.call(rails_controller, previous_responses: previous_responses) + {name => result} + end + end + end + + sync_responses = sync_steps.reduce(previous_responses) do |result, (name, step)| + result.merge(name => step.call(rails_controller, previous_responses: result)) + end + + responses = async_responses.map(&:value).reduce(sync_responses) do |result, step_result| + result.merge(step_result) + end + + with_metrics(part: :compose, rails_controller: rails_controller) do + compose_lambda.call(responses, rails_controller) + end + rescue => error + handle_error(error) + + {} # TODO: Better error handling in composition. + end + + def handle_error(error) + Sbmt::Strangler.logger.error(error.message) + Sbmt::Strangler.error_tracker.error(error) + end end end end diff --git a/lib/sbmt/strangler/configuration.rb b/lib/sbmt/strangler/configuration.rb index 07a2bb3..29ea6e0 100644 --- a/lib/sbmt/strangler/configuration.rb +++ b/lib/sbmt/strangler/configuration.rb @@ -9,6 +9,7 @@ class Configuration option :action_controller_base_class, default: "ActionController::Base" option :error_tracker, default: "Sbmt::Strangler::ErrorTracker" option :flipper_actor, default: ->(_http_params, _headers) {} + option :composition_step_duration_metric_buckets, default: nil attr_reader :controllers, :http diff --git a/lib/sbmt/strangler/version.rb b/lib/sbmt/strangler/version.rb index 9870c5b..58819d6 100644 --- a/lib/sbmt/strangler/version.rb +++ b/lib/sbmt/strangler/version.rb @@ -2,6 +2,6 @@ module Sbmt module Strangler - VERSION = "0.11.0" + VERSION = "0.12.0" end end diff --git a/spec/lib/sbmt/strangler/action/composition/composable_spec.rb b/spec/lib/sbmt/strangler/action/composition/composable_spec.rb deleted file mode 100644 index 3400773..0000000 --- a/spec/lib/sbmt/strangler/action/composition/composable_spec.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -describe Sbmt::Strangler::Action::Composition::Composable do - let(:composition_klass) do - Class.new do - include Sbmt::Strangler::Action::Composition::Composable - end - end - - describe ".new" do - subject(:new) { composition_klass.new(composition_level: composition_level) } - - let(:composition_level) { 0 } - - it "initializes new class" do - expect(new).to be_present - end - - context "with composition_level higher than MAX_COMPOSITION_LEVEL" do - let(:composition_level) { Sbmt::Strangler::Action::Composition::Composable::MAX_COMPOSITION_LEVEL + 1 } - - it "raises MaxCompositionLevelError" do - expect { new }.to raise_error(Sbmt::Strangler::Action::Composition::Errors::MaxCompositionLevelError) - end - end - end - - describe "#sync" do - subject(:add_sync_step) { composition_instance.sync(name) } - - let(:composition_instance) do - composition_klass.new(composition_level: composition_level) - end - - let(:composition_level) { 0 } - let(:name) { :service_a } - - it "adds sync step" do - expect(Sbmt::Strangler::Action::Composition::Step).to receive(:new).with(name: :service_a, composition_level: 1) - step = add_sync_step - expect(composition_instance.instance_variable_get(:@sync_steps)[name]).to eq(step) - end - end - - describe "#async" do - subject(:add_async_step) { composition_instance.async(name) } - - let(:composition_instance) do - composition_klass.new(composition_level: composition_level) - end - - let(:composition_level) { 0 } - let(:name) { :service_a } - - it "adds async step" do - expect(Sbmt::Strangler::Action::Composition::Step).to receive(:new).with(name: :service_a, composition_level: 1) - step = add_async_step - expect(composition_instance.instance_variable_get(:@async_steps)[name]).to eq(step) - end - end - - describe "#composable?" do - subject(:composable?) { composition_instance.composable? } - - let(:composition_instance) do - composition_klass.new - end - - it "return false" do - expect(composable?).to be_falsey - end - - context "with included step" do - it "return false" do - composition_instance.sync(:service_a) - - expect(composable?).to be_truthy - end - end - end -end diff --git a/spec/lib/sbmt/strangler/action/composition/step_spec.rb b/spec/lib/sbmt/strangler/action/composition/step_spec.rb new file mode 100644 index 0000000..39ab538 --- /dev/null +++ b/spec/lib/sbmt/strangler/action/composition/step_spec.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +describe Sbmt::Strangler::Action::Composition::Step do + describe ".new" do + subject(:new) { described_class.new(name: :step_name, level: level) } + + let(:level) { 0 } + + it "initializes new class" do + expect(new).to be_present + end + + context "with level higher than MAX_LEVEL" do + let(:level) { Sbmt::Strangler::Action::Composition::Step::MAX_LEVEL + 1 } + + it "raises MaxLevelError" do + expect { new }.to raise_error(Sbmt::Strangler::Action::Composition::Errors::MaxLevelError) + end + end + end + + describe "#sync" do + subject(:add_sync_step) { composition_instance.sync(name) } + + let!(:composition_instance) do + described_class.new(name: :step_name, level: level) + end + + let(:level) { 0 } + let(:name) { :service_a } + + it "adds sync step" do + expect(described_class).to receive(:new).with(name: :service_a, type: :sync, parent: composition_instance, level: 1) + step = add_sync_step + expect(composition_instance.instance_variable_get(:@sync_steps)[name]).to eq(step) + end + end + + describe "#async" do + subject(:add_async_step) { composition_instance.async(name) } + + let!(:composition_instance) do + described_class.new(name: :step_name, level: level) + end + + let(:level) { 0 } + let(:name) { :service_a } + + it "adds async step" do + expect(described_class).to receive(:new).with(name: :service_a, type: :async, parent: composition_instance, level: 1) + step = add_async_step + expect(composition_instance.instance_variable_get(:@async_steps)[name]).to eq(step) + end + end + + describe "#composable?" do + subject(:composable?) { composition_instance.send(:composable?) } + + let!(:composition_instance) do + described_class.new(name: :step_name) + end + + it "return false" do + expect(composable?).to be_falsey + end + + context "with included step" do + it "return false" do + composition_instance.sync(:service_a) + + expect(composable?).to be_truthy + end + end + end + + describe "#call" do + subject(:composition_step) do + step = described_class.new(name: :root_step) + step.sync(:sync_step).process {} + step.async(:async_step).process {} + step.process {}.compose {} + end + + let(:rails_controller) do + instance_double(ActionController::Base, controller_path: "ctrl", action_name: "actn") + end + + let(:call) { composition_step.call(rails_controller) } + + it "measures root composition step duration" do + m = Yabeda.sbmt_strangler.composition_step_duration + t = {step: "root_step", type: "sync", level: "0", parent: nil, controller: "ctrl", action: "actn"} + expect { call } + .to measure_yabeda_histogram(m).with_tags(t.merge(part: nil)) + .and measure_yabeda_histogram(m).with_tags(t.merge(part: "process")) + .and measure_yabeda_histogram(m).with_tags(t.merge(part: "compose")) + end + + it "measures sync composition step duration" do + m = Yabeda.sbmt_strangler.composition_step_duration + t = {step: "sync_step", type: "sync", level: "1", parent: "root_step", controller: "ctrl", action: "actn"} + expect { call } + .to measure_yabeda_histogram(m).with_tags(t.merge(part: "process")) + .and measure_yabeda_histogram(m).with_tags(t.merge(part: nil)) + end + + it "measures async composition step duration" do + m = Yabeda.sbmt_strangler.composition_step_duration + t = {step: "async_step", type: "async", level: "1", parent: "root_step", controller: "ctrl", action: "actn"} + expect { call } + .to measure_yabeda_histogram(m).with_tags(t.merge(part: "process")) + .and measure_yabeda_histogram(m).with_tags(t.merge(part: nil)) + end + + context "when OpenTelemetry defined" do + let(:call) do + fail "OpenTelemetry was not expected to be defined by this test" if Object.const_defined?(:OpenTelemetry) + + Object.const_set(:OpenTelemetry, otel) + composition_step.call(rails_controller) + Object.send(:remove_const, :OpenTelemetry) # rubocop:disable RSpec/RemoveConst + end + + # rubocop:disable RSpec/VerifiedDoubles + # FIXME: Rewrite test using verified doubles? + let(:otel) { double(:otel, tracer_provider: tracer_provider) } + let(:tracer_provider) { double(:tracer_provider, tracer: tracer) } + let(:tracer) { double(:tracer) } + # rubocop:enable RSpec/VerifiedDoubles + + before do + allow(tracer).to receive(:in_span).and_yield(nil) + end + + it "traces all composition steps" do + call + + expect(tracer).to have_received(:in_span).exactly(7) + + expect(tracer).to have_received(:in_span) + .with("Composition step: root_step", attributes: {type: "sync", level: 0}, kind: :internal) + expect(tracer).to have_received(:in_span) + .with("Composition step: root_step (process)", attributes: {type: "sync", level: 0}, kind: :internal) + expect(tracer).to have_received(:in_span) + .with("Composition step: root_step (compose)", attributes: {type: "sync", level: 0}, kind: :internal) + + expect(tracer).to have_received(:in_span) + .with("Composition step: sync_step", attributes: {type: "sync", parent: "root_step", level: 1}, kind: :internal) + expect(tracer).to have_received(:in_span) + .with("Composition step: sync_step (process)", attributes: {type: "sync", parent: "root_step", level: 1}, kind: :internal) + + expect(tracer).to have_received(:in_span) + .with("Composition step: async_step", attributes: {type: "async", parent: "root_step", level: 1}, kind: :internal) + expect(tracer).to have_received(:in_span) + .with("Composition step: async_step (process)", attributes: {type: "async", parent: "root_step", level: 1}, kind: :internal) + end + end + end +end diff --git a/spec/lib/sbmt/strangler/flipper_spec.rb b/spec/lib/sbmt/strangler/flipper_spec.rb index 9fd0867..5a91739 100644 --- a/spec/lib/sbmt/strangler/flipper_spec.rb +++ b/spec/lib/sbmt/strangler/flipper_spec.rb @@ -96,7 +96,7 @@ let(:feature_name) { "feature_name" } context "when feature enabled for current hour" do - around { |ex| travel_to(time_now, &ex) } + around { |ex| travel_to(time_to_travel_to, &ex) } before do hours_range = "ONTIME:#{start_hour}-#{end_hour}" @@ -108,26 +108,29 @@ let(:start_hour) { "18" } let(:end_hour) { "23" } + let(:now) { DateTime.now.in_time_zone } + let(:time_to_travel_to) { now } + context "when feature enabled for another hours range" do - let(:time_now) { DateTime.now.change(hour: 7) } + let(:time_to_travel_to) { now.change(hour: 7) } it("returns false") { expect(result).to be(false) } end context "when feature enabled for correct hours range" do - let(:time_now) { DateTime.now.change(hour: 20) } + let(:time_to_travel_to) { now.change(hour: 20) } it("returns true") { expect(result).to be(true) } end context "when feature enabled for end_hour" do - let(:time_now) { DateTime.now.change(hour: 23, minutes: 10) } + let(:time_to_travel_to) { now.change(hour: 23, minutes: 10) } it("returns false") { expect(result).to be(false) } end context "when feature enabled for start_hour" do - let(:time_now) { DateTime.now.change(hour: 18, minutes: 10) } + let(:time_to_travel_to) { now.change(hour: 18, minutes: 10) } it("returns true") { expect(result).to be(true) } end @@ -135,7 +138,7 @@ context "when start_hour eq end_hour" do let(:start_hour) { "02" } let(:end_hour) { "02" } - let(:time_now) { DateTime.now.change(hour: 12) } + let(:time_to_travel_to) { now.change(hour: 12) } it("returns true") { expect(result).to be(true) } end @@ -146,13 +149,13 @@ context "when result is false" do context "when time before start_hour" do - let(:time_now) { DateTime.now.change(hour: 16) } + let(:time_to_travel_to) { now.change(hour: 16) } it("returns false") { expect(result).to be(false) } end context "when time after end_hour" do - let(:time_now) { DateTime.now.change(hour: 7) } + let(:time_to_travel_to) { now.change(hour: 7) } it("returns false") { expect(result).to be(false) } end @@ -160,21 +163,21 @@ context "when start and end hours close" do let(:start_hour) { "03" } let(:end_hour) { "02" } - let(:time_now) { DateTime.now.change(hour: 2, minutes: 10) } + let(:time_to_travel_to) { now.change(hour: 2, minutes: 10) } it("returns false") { expect(result).to be(false) } end end context "when result is true" do - let(:time_now) { DateTime.now.change(hour: 20) } + let(:time_to_travel_to) { now.change(hour: 20) } it("returns true") { expect(result).to be(true) } context "when start and end hours close" do let(:start_hour) { "03" } let(:end_hour) { "02" } - let(:time_now) { DateTime.now.change(hour: 3, minutes: 10) } + let(:time_to_travel_to) { now.change(hour: 3, minutes: 10) } it("returns true") { expect(result).to be(true) } end