flipper 0.17.1 → 0.21.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +57 -0
- data/Changelog.md +114 -1
- data/Dockerfile +1 -1
- data/Gemfile +3 -6
- data/README.md +103 -47
- data/Rakefile +1 -4
- data/docs/Adapters.md +9 -9
- data/docs/Caveats.md +2 -2
- data/docs/DockerCompose.md +0 -1
- data/docs/Gates.md +74 -74
- data/docs/Optimization.md +70 -47
- data/docs/http/README.md +12 -11
- data/docs/images/banner.jpg +0 -0
- data/docs/read-only/README.md +8 -5
- data/examples/basic.rb +1 -12
- data/examples/configuring_default.rb +2 -5
- data/examples/dsl.rb +13 -24
- data/examples/enabled_for_actor.rb +8 -15
- data/examples/group.rb +3 -6
- data/examples/group_dynamic_lookup.rb +5 -19
- data/examples/group_with_members.rb +4 -14
- data/examples/importing.rb +1 -1
- data/examples/individual_actor.rb +2 -5
- data/examples/instrumentation.rb +1 -2
- data/examples/memoizing.rb +35 -0
- data/examples/percentage_of_actors.rb +6 -16
- data/examples/percentage_of_actors_enabled_check.rb +7 -10
- data/examples/percentage_of_actors_group.rb +5 -18
- data/examples/percentage_of_time.rb +3 -6
- data/flipper.gemspec +3 -4
- data/lib/flipper.rb +7 -3
- data/lib/flipper/adapters/dual_write.rb +67 -0
- data/lib/flipper/adapters/http.rb +32 -28
- data/lib/flipper/adapters/memory.rb +23 -94
- data/lib/flipper/adapters/operation_logger.rb +5 -0
- data/lib/flipper/adapters/pstore.rb +8 -1
- data/lib/flipper/adapters/sync.rb +7 -7
- data/lib/flipper/adapters/sync/interval_synchronizer.rb +1 -1
- data/lib/flipper/adapters/sync/synchronizer.rb +1 -0
- data/lib/flipper/configuration.rb +33 -7
- data/lib/flipper/dsl.rb +8 -0
- data/lib/flipper/errors.rb +2 -3
- data/lib/flipper/feature.rb +2 -2
- data/lib/flipper/identifier.rb +17 -0
- data/lib/flipper/middleware/memoizer.rb +30 -15
- data/lib/flipper/middleware/setup_env.rb +13 -3
- data/lib/flipper/railtie.rb +38 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +15 -0
- data/lib/flipper/test/shared_adapter_test.rb +16 -1
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/adapter_spec.rb +2 -2
- data/spec/flipper/adapters/dual_write_spec.rb +71 -0
- data/spec/flipper/adapters/http_spec.rb +74 -8
- data/spec/flipper/adapters/memory_spec.rb +21 -1
- data/spec/flipper/adapters/operation_logger_spec.rb +9 -0
- data/spec/flipper/adapters/sync_spec.rb +4 -4
- data/spec/flipper/configuration_spec.rb +20 -2
- data/spec/flipper/feature_spec.rb +5 -5
- data/spec/flipper/identifier_spec.rb +14 -0
- data/spec/flipper/middleware/memoizer_spec.rb +95 -35
- data/spec/flipper/middleware/setup_env_spec.rb +23 -3
- data/spec/flipper/railtie_spec.rb +69 -0
- data/spec/{integration_spec.rb → flipper_integration_spec.rb} +0 -0
- data/spec/flipper_spec.rb +26 -0
- data/spec/helper.rb +3 -3
- data/spec/support/descriptions.yml +1 -0
- data/spec/support/spec_helpers.rb +25 -0
- data/test/test_helper.rb +2 -1
- metadata +19 -10
- data/.rubocop.yml +0 -52
- data/.rubocop_todo.yml +0 -562
- data/examples/example_setup.rb +0 -8
@@ -19,7 +19,7 @@ module Flipper
|
|
19
19
|
# Public: Initializes a new interval synchronizer.
|
20
20
|
#
|
21
21
|
# synchronizer - The Synchronizer to call when the interval has passed.
|
22
|
-
# interval - The Integer number of
|
22
|
+
# interval - The Integer number of seconds between invocations of
|
23
23
|
# the wrapped synchronizer.
|
24
24
|
def initialize(synchronizer, interval: nil)
|
25
25
|
@synchronizer = synchronizer
|
@@ -15,6 +15,7 @@ module Flipper
|
|
15
15
|
# adapter should be brought in line with.
|
16
16
|
# options - The Hash of options.
|
17
17
|
# :instrumenter - The instrumenter used to instrument.
|
18
|
+
# :raise - Should errors be raised (default: true).
|
18
19
|
def initialize(local, remote, options = {})
|
19
20
|
@local = local
|
20
21
|
@remote = remote
|
@@ -1,7 +1,33 @@
|
|
1
1
|
module Flipper
|
2
2
|
class Configuration
|
3
|
-
def initialize
|
4
|
-
@default = -> {
|
3
|
+
def initialize(options = {})
|
4
|
+
@default = -> { Flipper.new(adapter) }
|
5
|
+
@adapter = -> { Flipper::Adapters::Memory.new }
|
6
|
+
end
|
7
|
+
|
8
|
+
# The default adapter to use.
|
9
|
+
#
|
10
|
+
# Pass a block to assign the adapter, and invoke without a block to
|
11
|
+
# return the configured adapter instance.
|
12
|
+
#
|
13
|
+
# Flipper.configure do |config|
|
14
|
+
# config.adapter # => instance of default Memory adapter
|
15
|
+
#
|
16
|
+
# # Configure it to use the ActiveRecord adapter
|
17
|
+
# config.adapter do
|
18
|
+
# require "flipper/adapters/active_record"
|
19
|
+
# Flipper::Adapters::ActiveRecord.new
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# config.adapter # => instance of ActiveRecord adapter
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
def adapter(&block)
|
26
|
+
if block_given?
|
27
|
+
@adapter = block
|
28
|
+
else
|
29
|
+
@adapter.call
|
30
|
+
end
|
5
31
|
end
|
6
32
|
|
7
33
|
# Controls the default instance for flipper. When used with a block it
|
@@ -9,15 +35,15 @@ module Flipper
|
|
9
35
|
# without a block, it performs a block invocation and returns the result.
|
10
36
|
#
|
11
37
|
# configuration = Flipper::Configuration.new
|
12
|
-
# configuration.default # =>
|
38
|
+
# configuration.default # => Flipper::DSL instance using Memory adapter
|
13
39
|
#
|
14
|
-
# # sets the default block to generate a new instance using
|
40
|
+
# # sets the default block to generate a new instance using ActiveRecord adapter
|
15
41
|
# configuration.default do
|
16
|
-
# require "flipper/adapters/
|
17
|
-
# Flipper.new(Flipper::Adapters::
|
42
|
+
# require "flipper/adapters/active_record"
|
43
|
+
# Flipper.new(Flipper::Adapters::ActiveRecord.new)
|
18
44
|
# end
|
19
45
|
#
|
20
|
-
# configuration.default # => Flipper::DSL instance using
|
46
|
+
# configuration.default # => Flipper::DSL instance using ActiveRecord adapter
|
21
47
|
#
|
22
48
|
# Returns result of default block invocation if called without block. If
|
23
49
|
# called with block, assigns the default block.
|
data/lib/flipper/dsl.rb
CHANGED
@@ -273,5 +273,13 @@ module Flipper
|
|
273
273
|
def import(flipper)
|
274
274
|
adapter.import(flipper.adapter)
|
275
275
|
end
|
276
|
+
|
277
|
+
# Cloud DSL method that does nothing for open source version.
|
278
|
+
def sync
|
279
|
+
end
|
280
|
+
|
281
|
+
# Cloud DSL method that does nothing for open source version.
|
282
|
+
def sync_secret
|
283
|
+
end
|
276
284
|
end
|
277
285
|
end
|
data/lib/flipper/errors.rb
CHANGED
@@ -16,9 +16,8 @@ module Flipper
|
|
16
16
|
# use it.
|
17
17
|
class DefaultNotSet < Flipper::Error
|
18
18
|
def initialize(message = nil)
|
19
|
-
|
20
|
-
|
21
|
-
super(message || default)
|
19
|
+
warn "Flipper::DefaultNotSet is deprecated and will be removed in 1.0"
|
20
|
+
super
|
22
21
|
end
|
23
22
|
end
|
24
23
|
|
data/lib/flipper/feature.rb
CHANGED
@@ -5,7 +5,7 @@ require 'flipper/feature_check_context'
|
|
5
5
|
require 'flipper/gate_values'
|
6
6
|
|
7
7
|
module Flipper
|
8
|
-
class Feature
|
8
|
+
class Feature
|
9
9
|
# Private: The name of feature instrumentation events.
|
10
10
|
InstrumentationName = "feature_operation.#{InstrumentationNamespace}".freeze
|
11
11
|
|
@@ -205,7 +205,7 @@ module Flipper
|
|
205
205
|
boolean = gate(:boolean)
|
206
206
|
non_boolean_gates = gates - [boolean]
|
207
207
|
|
208
|
-
if values.boolean || values.
|
208
|
+
if values.boolean || values.percentage_of_time == 100
|
209
209
|
:on
|
210
210
|
elsif non_boolean_gates.detect { |gate| gate.enabled?(values[gate.key]) }
|
211
211
|
:conditional
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Flipper
|
2
|
+
# A default implementation of `#flipper_id` for actors.
|
3
|
+
#
|
4
|
+
# class User < Struct.new(:id)
|
5
|
+
# include Flipper::Identifier
|
6
|
+
# end
|
7
|
+
#
|
8
|
+
# user = User.new(99)
|
9
|
+
# Flipper.enable :thing, user
|
10
|
+
# Flipper.enabled? :thing, user #=> true
|
11
|
+
#
|
12
|
+
module Identifier
|
13
|
+
def flipper_id
|
14
|
+
"#{self.class.name};#{id}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -8,8 +8,7 @@ module Flipper
|
|
8
8
|
#
|
9
9
|
# app - The app this middleware is included in.
|
10
10
|
# opts - The Hash of options.
|
11
|
-
# :
|
12
|
-
# :preload - Array of Symbol feature names to preload.
|
11
|
+
# :preload - Boolean to preload all features or Array of Symbol feature names to preload.
|
13
12
|
#
|
14
13
|
# Examples
|
15
14
|
#
|
@@ -23,7 +22,12 @@ module Flipper
|
|
23
22
|
#
|
24
23
|
def initialize(app, opts = {})
|
25
24
|
if opts.is_a?(Flipper::DSL) || opts.is_a?(Proc)
|
26
|
-
raise 'Flipper::Middleware::Memoizer no longer initializes with a flipper instance or block. Read more at: https://git.io/vSo31.'
|
25
|
+
raise 'Flipper::Middleware::Memoizer no longer initializes with a flipper instance or block. Read more at: https://git.io/vSo31.'
|
26
|
+
end
|
27
|
+
|
28
|
+
if opts[:preload_all]
|
29
|
+
warn "Flipper::Middleware::Memoizer: `preload_all` is deprecated, use `preload: true`"
|
30
|
+
opts[:preload] = true
|
27
31
|
end
|
28
32
|
|
29
33
|
@app = app
|
@@ -34,39 +38,50 @@ module Flipper
|
|
34
38
|
def call(env)
|
35
39
|
request = Rack::Request.new(env)
|
36
40
|
|
37
|
-
if
|
38
|
-
@app.call(env)
|
39
|
-
else
|
41
|
+
if memoize?(request)
|
40
42
|
memoized_call(env)
|
43
|
+
else
|
44
|
+
@app.call(env)
|
41
45
|
end
|
42
46
|
end
|
43
47
|
|
44
48
|
private
|
45
49
|
|
46
|
-
def
|
47
|
-
|
50
|
+
def memoize?(request)
|
51
|
+
if @opts[:if]
|
52
|
+
@opts[:if].call(request)
|
53
|
+
elsif @opts[:unless]
|
54
|
+
!@opts[:unless].call(request)
|
55
|
+
else
|
56
|
+
true
|
57
|
+
end
|
48
58
|
end
|
49
59
|
|
50
60
|
def memoized_call(env)
|
51
61
|
reset_on_body_close = false
|
52
62
|
flipper = env.fetch(@env_key) { Flipper }
|
53
|
-
original = flipper.memoizing?
|
54
|
-
flipper.memoize = true
|
55
63
|
|
56
|
-
|
64
|
+
# Already memoizing. This instance does not need to do anything.
|
65
|
+
if flipper.memoizing?
|
66
|
+
warn "Flipper::Middleware::Memoizer appears to be running twice. Read how to resolve this at https://github.com/jnunemaker/flipper/pull/523"
|
67
|
+
return @app.call(env)
|
68
|
+
end
|
69
|
+
|
70
|
+
flipper.memoize = true
|
57
71
|
|
58
|
-
|
59
|
-
|
72
|
+
case @opts[:preload]
|
73
|
+
when true then flipper.preload_all
|
74
|
+
when Array then flipper.preload(@opts[:preload])
|
60
75
|
end
|
61
76
|
|
62
77
|
response = @app.call(env)
|
63
78
|
response[2] = Rack::BodyProxy.new(response[2]) do
|
64
|
-
flipper.memoize =
|
79
|
+
flipper.memoize = false
|
65
80
|
end
|
66
81
|
reset_on_body_close = true
|
67
82
|
response
|
68
83
|
ensure
|
69
|
-
flipper.memoize =
|
84
|
+
flipper.memoize = false if flipper && !reset_on_body_close
|
70
85
|
end
|
71
86
|
end
|
72
87
|
end
|
@@ -7,7 +7,8 @@ module Flipper
|
|
7
7
|
#
|
8
8
|
# app - The app this middleware is included in.
|
9
9
|
# flipper_or_block - The Flipper::DSL instance or a block that yields a
|
10
|
-
# Flipper::DSL instance to use for all operations
|
10
|
+
# Flipper::DSL instance to use for all operations
|
11
|
+
# (optional, default: Flipper).
|
11
12
|
#
|
12
13
|
# Examples
|
13
14
|
#
|
@@ -19,18 +20,27 @@ module Flipper
|
|
19
20
|
# # using with a block that yields a flipper instance
|
20
21
|
# use Flipper::Middleware::SetupEnv, lambda { Flipper.new(...) }
|
21
22
|
#
|
22
|
-
|
23
|
+
# # using default configured Flipper instance
|
24
|
+
# Flipper.configure do |config|
|
25
|
+
# config.default { Flipper.new(...) }
|
26
|
+
# end
|
27
|
+
# use Flipper::Middleware::SetupEnv
|
28
|
+
def initialize(app, flipper_or_block = nil, options = {})
|
23
29
|
@app = app
|
24
30
|
@env_key = options.fetch(:env_key, 'flipper')
|
25
31
|
|
26
32
|
if flipper_or_block.respond_to?(:call)
|
27
33
|
@flipper_block = flipper_or_block
|
28
34
|
else
|
29
|
-
@flipper = flipper_or_block
|
35
|
+
@flipper = flipper_or_block || Flipper
|
30
36
|
end
|
31
37
|
end
|
32
38
|
|
33
39
|
def call(env)
|
40
|
+
dup.call!(env)
|
41
|
+
end
|
42
|
+
|
43
|
+
def call!(env)
|
34
44
|
env[@env_key] ||= flipper
|
35
45
|
@app.call(env)
|
36
46
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Flipper
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
config.before_configuration do
|
4
|
+
config.flipper = ActiveSupport::OrderedOptions.new.update(
|
5
|
+
env_key: "flipper",
|
6
|
+
memoize: true,
|
7
|
+
preload: true,
|
8
|
+
instrumenter: ActiveSupport::Notifications
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
initializer "flipper.default", before: :load_config_initializers do |app|
|
13
|
+
Flipper.configure do |config|
|
14
|
+
config.default do
|
15
|
+
Flipper.new(config.adapter, instrumenter: app.config.flipper.instrumenter)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
initializer "flipper.memoizer", after: :load_config_initializers do |app|
|
21
|
+
config = app.config.flipper
|
22
|
+
|
23
|
+
if config.memoize
|
24
|
+
app.middleware.use Flipper::Middleware::Memoizer, {
|
25
|
+
env_key: config.env_key,
|
26
|
+
preload: config.preload,
|
27
|
+
if: config.memoize.respond_to?(:call) ? config.memoize : nil
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
initializer "flipper.identifier" do
|
33
|
+
ActiveSupport.on_load(:active_record) do
|
34
|
+
ActiveRecord::Base.include Flipper::Identifier
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -289,4 +289,19 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
289
289
|
expect(subject.enable(feature, boolean_gate, flipper.boolean)).to eq(true)
|
290
290
|
expect(subject.enable(feature, boolean_gate, flipper.boolean)).to eq(true)
|
291
291
|
end
|
292
|
+
|
293
|
+
it 'can get_all features when there are none' do
|
294
|
+
expect(subject.features).to eq(Set.new)
|
295
|
+
expect(subject.get_all).to eq({})
|
296
|
+
end
|
297
|
+
|
298
|
+
it 'clears other gate values on enable' do
|
299
|
+
actor = Flipper::Actor.new('Flipper::Actor;22')
|
300
|
+
subject.enable(feature, actors_gate, flipper.actors(25))
|
301
|
+
subject.enable(feature, time_gate, flipper.time(25))
|
302
|
+
subject.enable(feature, group_gate, flipper.group(:admins))
|
303
|
+
subject.enable(feature, actor_gate, flipper.actor(actor))
|
304
|
+
subject.enable(feature, boolean_gate, flipper.boolean(true))
|
305
|
+
expect(subject.get(feature)).to eq(subject.default_config.merge(boolean: "true"))
|
306
|
+
end
|
292
307
|
end
|
@@ -1,4 +1,3 @@
|
|
1
|
-
# rubocop:disable Metrics/ModuleLength
|
2
1
|
module Flipper
|
3
2
|
module Test
|
4
3
|
module SharedAdapterTests
|
@@ -285,6 +284,22 @@ module Flipper
|
|
285
284
|
assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean)
|
286
285
|
assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean)
|
287
286
|
end
|
287
|
+
|
288
|
+
def test_can_get_all_features_when_there_are_none
|
289
|
+
expected = {}
|
290
|
+
assert_equal Set.new, @adapter.features
|
291
|
+
assert_equal expected, @adapter.get_all
|
292
|
+
end
|
293
|
+
|
294
|
+
def test_clears_other_gate_values_on_enable
|
295
|
+
actor = Flipper::Actor.new('Flipper::Actor;22')
|
296
|
+
assert_equal true, @adapter.enable(@feature, @actors_gate, @flipper.actors(25))
|
297
|
+
assert_equal true, @adapter.enable(@feature, @time_gate, @flipper.time(25))
|
298
|
+
assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:admins))
|
299
|
+
assert_equal true, @adapter.enable(@feature, @actor_gate, @flipper.actor(actor))
|
300
|
+
assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean(true))
|
301
|
+
assert_equal @adapter.default_config.merge(boolean: "true"), @adapter.get(@feature)
|
302
|
+
end
|
288
303
|
end
|
289
304
|
end
|
290
305
|
end
|
data/lib/flipper/version.rb
CHANGED
@@ -16,7 +16,7 @@ RSpec.describe Flipper::Adapter do
|
|
16
16
|
describe '.default_config' do
|
17
17
|
it 'returns default config' do
|
18
18
|
adapter_class = Class.new do
|
19
|
-
include Flipper::Adapter
|
19
|
+
include Flipper::Adapter
|
20
20
|
end
|
21
21
|
expect(adapter_class.default_config).to eq(default_config)
|
22
22
|
end
|
@@ -25,7 +25,7 @@ RSpec.describe Flipper::Adapter do
|
|
25
25
|
describe '#default_config' do
|
26
26
|
it 'returns default config' do
|
27
27
|
adapter_class = Class.new do
|
28
|
-
include Flipper::Adapter
|
28
|
+
include Flipper::Adapter
|
29
29
|
end
|
30
30
|
expect(adapter_class.new.default_config).to eq(default_config)
|
31
31
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'flipper/adapters/dual_write'
|
3
|
+
require 'flipper/adapters/operation_logger'
|
4
|
+
require 'flipper/spec/shared_adapter_specs'
|
5
|
+
require 'active_support/notifications'
|
6
|
+
|
7
|
+
RSpec.describe Flipper::Adapters::DualWrite do
|
8
|
+
let(:local_adapter) do
|
9
|
+
Flipper::Adapters::OperationLogger.new Flipper::Adapters::Memory.new
|
10
|
+
end
|
11
|
+
let(:remote_adapter) do
|
12
|
+
Flipper::Adapters::OperationLogger.new Flipper::Adapters::Memory.new
|
13
|
+
end
|
14
|
+
let(:local) { Flipper.new(local_adapter) }
|
15
|
+
let(:remote) { Flipper.new(remote_adapter) }
|
16
|
+
let(:sync) { Flipper.new(subject) }
|
17
|
+
|
18
|
+
subject do
|
19
|
+
described_class.new(local_adapter, remote_adapter)
|
20
|
+
end
|
21
|
+
|
22
|
+
it_should_behave_like 'a flipper adapter'
|
23
|
+
|
24
|
+
it 'only uses local for #features' do
|
25
|
+
subject.features
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'only uses local for #get' do
|
29
|
+
subject.get sync[:search]
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'only uses local for #get_multi' do
|
33
|
+
subject.get_multi [sync[:search]]
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'only uses local for #get_all' do
|
37
|
+
subject.get_all
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'updates remote and local for #add' do
|
41
|
+
subject.add sync[:search]
|
42
|
+
expect(remote_adapter.count(:add)).to be(1)
|
43
|
+
expect(local_adapter.count(:add)).to be(1)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'updates remote and local for #remove' do
|
47
|
+
subject.remove sync[:search]
|
48
|
+
expect(remote_adapter.count(:remove)).to be(1)
|
49
|
+
expect(local_adapter.count(:remove)).to be(1)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'updates remote and local for #clear' do
|
53
|
+
subject.clear sync[:search]
|
54
|
+
expect(remote_adapter.count(:clear)).to be(1)
|
55
|
+
expect(local_adapter.count(:clear)).to be(1)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'updates remote and local for #enable' do
|
59
|
+
feature = sync[:search]
|
60
|
+
subject.enable feature, feature.gate(:boolean), local.boolean
|
61
|
+
expect(remote_adapter.count(:enable)).to be(1)
|
62
|
+
expect(local_adapter.count(:enable)).to be(1)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'updates remote and local for #disable' do
|
66
|
+
feature = sync[:search]
|
67
|
+
subject.disable feature, feature.gate(:boolean), local.boolean(false)
|
68
|
+
expect(remote_adapter.count(:disable)).to be(1)
|
69
|
+
expect(local_adapter.count(:disable)).to be(1)
|
70
|
+
end
|
71
|
+
end
|