fluidfeatures 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.travis.yml CHANGED
@@ -4,4 +4,3 @@ rvm:
4
4
  gemfile:
5
5
  - Gemfile
6
6
  script: bundle exec rake spec
7
- env: FLUIDFEATURES_APPID=1vu33ki6emqe3 FLUIDFEATURES_SECRET=secret FLUIDFEATURES_BASEURI=https://www.fluidfeatures.com/service
data/README.md CHANGED
@@ -35,8 +35,8 @@ Call `app` on the `FluidFeatures` module to instantiate this object.
35
35
  require 'fluidfeatures'
36
36
 
37
37
  config = {
38
- "baseuri" => "https://www.fluidfeatures.com/service"
39
- "appid" => "1vu33ki6emqe3"
38
+ "base_uri" => "https://www.fluidfeatures.com/service"
39
+ "app_id" => "1vu33ki6emqe3"
40
40
  "secret" = "sssseeecrrreeetttt"
41
41
  }
42
42
 
@@ -27,13 +27,26 @@ module FluidFeatures
27
27
 
28
28
  def initialize(app)
29
29
  raise "app invalid : #{app}" unless app.is_a? ::FluidFeatures::App
30
+ @sending = false
30
31
  configure(app)
31
- run_loop
32
32
  at_exit do
33
33
  buckets_storage.append(@buckets)
34
34
  end
35
35
  end
36
36
 
37
+ def start_sending
38
+ return if @sending
39
+ @sending = true
40
+ run_loop
41
+ end
42
+
43
+ def stop_sending(wait=false)
44
+ @sending = false
45
+ if wait
46
+ @loop_thread.join if @loop_thread and @loop_thread.alive?
47
+ end
48
+ end
49
+
37
50
  def buckets_storage
38
51
  @buckets_storage ||= FluidFeatures::Persistence::Buckets.create(FluidFeatures.config["cache"])
39
52
  end
@@ -96,48 +109,66 @@ module FluidFeatures
96
109
  features_storage.replace_unknown(@unknown_features)
97
110
  end
98
111
 
112
+ start_sending unless @sending
99
113
  end
100
114
 
101
115
  def run_loop
102
- Thread.new do
103
- while true
104
- begin
105
116
 
106
- unless transactions_queued?
107
- sleep WAIT_BETWEEN_QUEUE_EMTPY_CHECKS
108
- next
109
- end
117
+ return unless @sending
118
+ return if @loop_thread and @loop_thread.alive?
119
+
120
+ @loop_thread = Thread.new do
121
+ while @sending
122
+ run_loop_iteration(
123
+ WAIT_BETWEEN_QUEUE_EMTPY_CHECKS,
124
+ WAIT_BETWEEN_SEND_SUCCESS_NONE_WAITING,
125
+ WAIT_BETWEEN_SEND_SUCCESS_NEXT_WAITING,
126
+ WAIT_BETWEEN_SEND_FAILURES
127
+ )
128
+ end
129
+ end
130
+ end
110
131
 
111
- success = send_transactions
112
-
113
- if success
114
- # Unless we have a full bucket waiting do not make
115
- # more than N requests per second.
116
- if bucket_count <= 1
117
- sleep WAIT_BETWEEN_SEND_SUCCESS_NONE_WAITING
118
- else
119
- sleep WAIT_BETWEEN_SEND_SUCCESS_NEXT_WAITING
120
- end
121
- else
122
- # If service is down, then slow our requests
123
- # within this thread
124
- sleep WAIT_BETWEEN_SEND_FAILURES
125
- end
132
+ def run_loop_iteration(
133
+ wait_between_queue_emtpy_checks,
134
+ wait_between_send_success_none_waiting,
135
+ wait_between_send_success_next_waiting,
136
+ wait_between_send_failures)
137
+ begin
138
+
139
+ unless transactions_queued?
140
+ sleep wait_between_queue_emtpy_checks
141
+ return
142
+ end
126
143
 
127
- rescue Exception => err
128
- # catch errors, so that we do not affect the rest of the application
129
- app.logger.error "[FF] send_transactions failed : #{err.message}\n#{err.backtrace.join("\n")}"
130
- # hold off for a little while and try again
131
- sleep WAIT_BETWEEN_SEND_FAILURES
144
+ success = send_transactions
145
+
146
+ if success
147
+ # Unless we have a full bucket waiting do not make
148
+ # more than N requests per second.
149
+ if bucket_count <= 1
150
+ sleep wait_between_send_success_none_waiting
151
+ else
152
+ sleep wait_between_send_success_next_waiting
132
153
  end
154
+ else
155
+ # If service is down, then slow our requests
156
+ # within this thread
157
+ sleep wait_between_send_failures
133
158
  end
159
+
160
+ rescue Exception => err
161
+ # catch errors, so that we do not affect the rest of the application
162
+ app.logger.error "[FF] send_transactions failed : #{err.message}\n#{err.backtrace.join("\n")}"
163
+ # hold off for a little while and try again
164
+ sleep wait_between_send_failures
134
165
  end
135
166
  end
136
167
 
137
168
  @private
138
169
  def transactions_queued?
139
170
  have_transactions = false
140
- @buckets_lock.synchronize do
171
+ buckets_lock_synchronize do
141
172
  if @buckets.size == 1
142
173
  @current_bucket_lock.synchronize do
143
174
  if @current_bucket.size > 0
@@ -163,7 +194,7 @@ module FluidFeatures
163
194
  end
164
195
 
165
196
  remaining_buckets_stats = nil
166
- @buckets_lock.synchronize do
197
+ buckets_lock_synchronize do
167
198
  remaining_buckets_stats = @buckets.map { |b| b.size }
168
199
  end
169
200
 
@@ -205,7 +236,7 @@ module FluidFeatures
205
236
  @private
206
237
  def bucket_count
207
238
  num_buckets = 0
208
- @buckets_lock.synchronize do
239
+ buckets_lock_synchronize do
209
240
  num_buckets = @buckets.size
210
241
  end
211
242
  num_buckets
@@ -214,7 +245,7 @@ module FluidFeatures
214
245
  @private
215
246
  def new_bucket
216
247
  bucket = []
217
- @buckets_lock.synchronize do
248
+ buckets_lock_synchronize do
218
249
  @buckets << bucket
219
250
  if @buckets.size > MAX_BUCKETS
220
251
  #offload to storage
@@ -229,7 +260,7 @@ module FluidFeatures
229
260
  @private
230
261
  def remove_bucket
231
262
  removed_bucket = nil
232
- @buckets_lock.synchronize do
263
+ buckets_lock_synchronize do
233
264
  #try to get buckets from storage first
234
265
  if @buckets.empty? && !buckets_storage.empty?
235
266
  @buckets = buckets_storage.fetch(MAX_BUCKETS)
@@ -251,7 +282,7 @@ module FluidFeatures
251
282
  @private
252
283
  def unremove_bucket(bucket)
253
284
  success = false
254
- @buckets_lock.synchronize do
285
+ buckets_lock_synchronize do
255
286
  if @buckets.size <= MAX_BUCKETS
256
287
  @buckets.unshift bucket
257
288
  success = true
@@ -292,5 +323,12 @@ module FluidFeatures
292
323
  end
293
324
  end
294
325
 
326
+ @private
327
+ def buckets_lock_synchronize
328
+ @buckets_lock.synchronize do
329
+ yield
330
+ end
331
+ end
332
+
295
333
  end
296
334
  end
@@ -4,6 +4,7 @@ require "thread"
4
4
 
5
5
  require "fluidfeatures/const"
6
6
  require "fluidfeatures/app/transaction"
7
+ require "fluidfeatures/exceptions"
7
8
 
8
9
  module FluidFeatures
9
10
  class AppState
@@ -29,70 +30,121 @@ module FluidFeatures
29
30
 
30
31
  def initialize(app)
31
32
  raise "app invalid : #{app}" unless app.is_a? ::FluidFeatures::App
33
+ @receiving = false
32
34
  configure(app)
33
- run_loop
34
35
  end
35
36
 
36
37
  def configure(app)
37
38
  @app = app
38
- @features = features_storage.list
39
+ @features = nil
39
40
  @features_lock = ::Mutex.new
40
41
  end
41
42
 
43
+ def start_receiving
44
+ return if @receiving
45
+ @receiving = true
46
+ run_loop
47
+ end
48
+
49
+ def stop_receiving(wait=false)
50
+ @receiving = false
51
+ if wait
52
+ @loop_thread.join if @loop_thread and @loop_thread.alive?
53
+ end
54
+ end
55
+
42
56
  def features_storage
43
57
  @features_storage ||= FluidFeatures::Persistence::Features.create(FluidFeatures.config["cache"])
44
58
  end
45
59
 
46
60
  def features
47
61
  f = nil
48
- @features_lock.synchronize do
49
- f = @features
62
+ if @receiving
63
+ # use features loaded in background
64
+ features_lock_synchronize do
65
+ f = @features
66
+ end
67
+ end
68
+ unless f
69
+ # we have not loaded features yet.
70
+ # load in foreground but do not use caching (etags)
71
+ success, state = load_state(use_cache=false)
72
+ if success
73
+ unless state
74
+ # Since we did not use etag caching, state should never
75
+ # be nil if success was true.
76
+ raise FFeaturesAppStateLoadFailure.new("Unexpected nil state returned from successful load_state(use_cache=false).")
77
+ end
78
+ self.features = f = state
79
+ else
80
+ # fluidfeatures API must be down.
81
+ # load persisted features from disk.
82
+ self.features = f = features_storage.list
83
+ end
84
+ end
85
+ # we should never return nil
86
+ unless f
87
+ # If we still could not load state then croak
88
+ raise FFeaturesAppStateLoadFailure.new("Could not load features state from API: #{state}")
89
+ end
90
+ unless @receiving
91
+ # start background receiver loop
92
+ start_receiving
50
93
  end
51
94
  f
52
95
  end
53
96
 
54
97
  def features= f
55
98
  return unless f.is_a? Hash
56
- @features_lock.synchronize do
57
- features_storage.replace(f) unless @features == f
99
+ features_lock_synchronize do
100
+ features_storage.replace(f)
58
101
  @features = f
59
102
  end
103
+ f
60
104
  end
61
105
 
62
106
  def run_loop
63
- Thread.new do
64
- while true
65
- begin
66
-
67
- success, state = load_state
68
-
69
- # Note, success could be true, but state might be nil.
70
- # This occurs with 304 (no change)
71
- if success and state
72
- # switch out current state with new one
73
- self.features = state
74
- elsif not success
75
- # If service is down, then slow our requests
76
- # within this thread
77
- sleep WAIT_BETWEEN_FETCH_FAILURES
78
- end
79
107
 
80
- # What ever happens never make more than N requests
81
- # per second
82
- sleep WAIT_BETWEEN_FETCH_SUCCESS
108
+ return unless @receiving
109
+ return if @loop_thread and @loop_thread.alive?
83
110
 
84
- rescue Exception => err
85
- # catch errors, so that we do not affect the rest of the application
86
- app.logger.error "load_state failed : #{err.message}\n#{err.backtrace.join("\n")}"
87
- # hold off for a little while and try again
88
- sleep WAIT_BETWEEN_FETCH_FAILURES
89
- end
111
+ @loop_thread = Thread.new do
112
+ while @receiving
113
+ run_loop_iteration(WAIT_BETWEEN_FETCH_SUCCESS, WAIT_BETWEEN_FETCH_FAILURES)
114
+ end
115
+ end
116
+ end
117
+
118
+ def run_loop_iteration(wait_between_fetch_success, wait_between_fetch_failures)
119
+ begin
120
+
121
+ success, state = load_state
122
+
123
+ # Note, success could be true, but state might be nil.
124
+ # This occurs with 304 (no change)
125
+ if success and state
126
+ # switch out current state with new one
127
+ self.features = state
128
+ elsif not success
129
+ # If service is down, then slow our requests
130
+ # within this thread
131
+ sleep wait_between_fetch_failures
90
132
  end
133
+
134
+ # What ever happens never make more than N requests
135
+ # per second
136
+ sleep wait_between_fetch_success
137
+
138
+ rescue Exception => err
139
+ # catch errors, so that we do not affect the rest of the application
140
+ app.logger.error "load_state failed : #{err.message}\n#{err.backtrace.join("\n")}"
141
+ # hold off for a little while and try again
142
+ sleep wait_between_fetch_failures
91
143
  end
92
144
  end
93
145
 
94
- def load_state
95
- success, state = app.get("/features", { :verbose => true, :etag_wait => ETAG_WAIT }, true)
146
+ def load_state(use_cache=true)
147
+ success, state = app.get("/features", { :verbose => true, :etag_wait => ETAG_WAIT }, use_cache)
96
148
  if success and state
97
149
  state.each_pair do |feature_name, feature|
98
150
  feature["versions"].each_pair do |version_name, version|
@@ -109,8 +161,6 @@ module FluidFeatures
109
161
  version_name ||= ::FluidFeatures::DEFAULT_VERSION_NAME
110
162
  raise "version_name invalid : #{version_name}" unless version_name.is_a? String
111
163
 
112
- #assert(isinstance(user_id, basestring))
113
-
114
164
  user_attributes ||= {}
115
165
  user_attributes["user"] = user_id.to_s
116
166
  if user_id.is_a? Integer
@@ -123,7 +173,10 @@ module FluidFeatures
123
173
  enabled = false
124
174
 
125
175
  feature = features[feature_name]
176
+ return false unless feature
126
177
  version = feature["versions"][version_name]
178
+ return false unless version
179
+
127
180
  modulus = user_id_hash % feature["num_parts"]
128
181
  enabled = version["parts"].include? modulus
129
182
 
@@ -150,5 +203,12 @@ module FluidFeatures
150
203
  enabled
151
204
  end
152
205
 
206
+ @private
207
+ def features_lock_synchronize
208
+ @features_lock.synchronize do
209
+ yield
210
+ end
211
+ end
212
+
153
213
  end
154
214
  end
@@ -11,7 +11,7 @@ module FluidFeatures
11
11
  @user = user
12
12
  @url = url
13
13
 
14
- # take a snap-shot of the features end at
14
+ # take a snap-shot of the features enabled state at
15
15
  # the beginning of the transaction
16
16
  @features = user.features
17
17
 
@@ -7,6 +7,7 @@ require "uuid"
7
7
  require "json"
8
8
 
9
9
  require "fluidfeatures/app"
10
+ require 'fluidfeatures/exceptions'
10
11
 
11
12
  module FluidFeatures
12
13
  class Client
@@ -20,6 +21,9 @@ module FluidFeatures
20
21
 
21
22
  def initialize(base_uri, logger)
22
23
 
24
+ raise FFeaturesBadParam.new("base_uri is not String, is #{base_uri.class}") \
25
+ unless base_uri.is_a? String
26
+
23
27
  @uuid = UUID.new.generate
24
28
  @logger = logger
25
29
  @base_uri = base_uri
@@ -1,24 +1,57 @@
1
1
 
2
2
  require 'yaml'
3
- require 'erb'
3
+ require 'fluidfeatures/exceptions'
4
4
 
5
5
  module FluidFeatures
6
6
  class Config
7
+
7
8
  attr_accessor :vars
8
- def initialize(path, environment, replacements = {})
9
- self.vars = YAML.load(ERB.new(File.read(path)).result)
10
- self.vars = vars["common"].update(vars[environment])
11
- self.vars = vars.update(replacements)
12
- self.vars["cache"]["limit"] = self.class.parse_file_size(vars["cache"]["limit"]) if vars["cache"]
9
+
10
+ def initialize(source, environment=nil)
11
+ if source.is_a? String
12
+ init_from_file(source, environment)
13
+ elsif source.is_a? Hash
14
+ init_from_hash(source)
15
+ else
16
+ raise FFeaturesConfigInvalid.new(
17
+ "Invalid 'source' given. Expected file path String or Hash. Got #{source.class}"
18
+ )
19
+ end
20
+ if @vars["cache"] and @vars["cache"]["limit"]
21
+ @vars["cache"]["limit"] = self.class.parse_file_size(vars["cache"]["limit"])
22
+ end
13
23
  end
14
24
 
15
25
  def [](name)
16
26
  @vars[name.to_s]
17
27
  end
18
28
 
29
+ private
30
+
31
+ def init_from_file path, environment
32
+ unless File.exists? path
33
+ raise FFeaturesConfigFileNotExists.new("File not found : #{path}")
34
+ end
35
+ environments = YAML.load_file path
36
+ unless environments.is_a? Hash
37
+ raise FFeaturesConfigInvalid.new("Config is invalid : #{path}")
38
+ end
39
+ @vars = (environments["common"] || {}).clone
40
+ @vars.update environments[environment] if environments[environment]
41
+ end
42
+
43
+ def init_from_hash hash
44
+ @vars = hash.clone
45
+ end
46
+
19
47
  def self.parse_file_size(size)
20
- return size if !size || size.is_a?(Numeric) || !/(\d+)\s*(kb|mb|gb)$/.match(size.downcase)
21
- $1.to_i * 1024 ** (%w{kb mb gb}.index($2) + 1)
48
+ return size if size.is_a? Numeric
49
+ return size.to_i if size.is_a? String and size.match /^\d+$/
50
+ unless size.is_a? String and (/^(\d+)\s*(k|m|g)b$/i).match(size)
51
+ raise FFeaturesConfigInvalid.new("Invalid file size string in config : '#{size}'")
52
+ end
53
+ $1.to_i * 1024 ** ("kmg".index($2.downcase) + 1)
22
54
  end
55
+
23
56
  end
24
57
  end
@@ -0,0 +1,16 @@
1
+
2
+ class FFeaturesException < Exception
3
+ end
4
+
5
+ class FFeaturesConfigInvalid < FFeaturesException
6
+ end
7
+
8
+ class FFeaturesConfigFileNotExists < FFeaturesConfigInvalid
9
+ end
10
+
11
+ class FFeaturesBadParam < FFeaturesException
12
+ end
13
+
14
+ class FFeaturesAppStateLoadFailure < FFeaturesException
15
+ end
16
+
@@ -3,14 +3,14 @@ module FluidFeatures
3
3
  class Buckets < Storage
4
4
  attr_accessor :limit
5
5
 
6
- def self.create(config)
6
+ def self.create(config, logger=nil)
7
7
  return NullBuckets.new unless config && config["dir"] && config["enable"] && config["limit"] > 0
8
- new(config)
8
+ new(config, logger)
9
9
  end
10
10
 
11
- def initialize(config)
11
+ def initialize(config, logger=nil)
12
12
  self.limit = config["limit"]
13
- super config
13
+ super
14
14
  end
15
15
 
16
16
  def fetch(n = 1)
@@ -2,18 +2,17 @@ module FluidFeatures
2
2
  module Persistence
3
3
  class Features < Storage
4
4
 
5
- def self.create(config)
6
- return NullFeatures.new unless config && config["dir"] && config["enable"]
7
- new(config)
8
- end
9
-
10
- def initialize(config)
11
- super config
5
+ def self.create(config, logger=nil)
6
+ if config && config["dir"] && config["enable"]
7
+ new(config, logger)
8
+ else
9
+ NullFeatures.new
10
+ end
12
11
  end
13
12
 
14
13
  def list
15
14
  store.transaction(true) do
16
- return {} unless store && store["features"]
15
+ return nil unless store && store["features"]
17
16
  store["features"]
18
17
  end
19
18
  end
@@ -25,6 +24,7 @@ module FluidFeatures
25
24
  end
26
25
  end
27
26
 
27
+ # TODO: remove. we do not care about persisting these
28
28
  def list_unknown
29
29
  store.transaction(true) do
30
30
  return {} unless store && store["unknown_features"]
@@ -53,7 +53,7 @@ module FluidFeatures
53
53
  end
54
54
 
55
55
  class NullFeatures
56
- def list; {} end
56
+ def list; nil end
57
57
  def list_unknown; {} end
58
58
  def replace(*args); false end
59
59
  def replace_unknown(*args); false end
@@ -5,10 +5,10 @@ module FluidFeatures
5
5
  class Storage
6
6
  attr_accessor :dir, :file_name, :store, :logger
7
7
 
8
- def initialize(config)
9
- self.dir = config["dir"]
10
- self.logger = config["logger"] || Logger.new(STDERR)
11
- self.file_name = "#{self.class.to_s.split('::').last.downcase}.pstore"
8
+ def initialize(config, logger=nil)
9
+ @dir = config["dir"]
10
+ @logger = logger || Logger.new(STDERR)
11
+ @file_name = "#{self.class.to_s.split('::').last.downcase}.pstore"
12
12
  FileUtils.mkpath(dir) unless Dir.exists?(dir)
13
13
  end
14
14
 
@@ -21,11 +21,8 @@ module FluidFeatures
21
21
  end
22
22
 
23
23
  def file_size
24
- begin
25
- File.size(path)
26
- rescue Exception => _
27
- return 0
28
- end
24
+ # TODO: rescue should return nil here
25
+ File.size(path) rescue 0
29
26
  end
30
27
  end
31
28
  end
@@ -1,3 +1,3 @@
1
1
  module FluidFeatures
2
- VERSION = '0.5.0'
2
+ VERSION = '0.6.0'
3
3
  end
data/lib/fluidfeatures.rb CHANGED
@@ -15,10 +15,10 @@ module FluidFeatures
15
15
  attr_accessor :config
16
16
  end
17
17
 
18
- def self.app(config)
19
- config["logger"] ||= ::Logger.new(STDERR)
18
+ def self.app(config, logger=nil)
19
+ logger ||= ::Logger.new(STDERR)
20
20
  self.config = config
21
- client = ::FluidFeatures::Client.new(config["baseuri"], config["logger"])
22
- ::FluidFeatures::App.new(client, config["appid"], config["secret"], config["logger"])
21
+ client = ::FluidFeatures::Client.new(config["base_uri"], logger)
22
+ ::FluidFeatures::App.new(client, config["app_id"], config["secret"], logger)
23
23
  end
24
- end
24
+ end
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe FluidFeatures::AppReporter do
4
4
 
5
- it_should_behave_like "polling loop", :send_transactions do
5
+ it_should_behave_like "polling loop" do
6
6
  before(:each) { described_class.any_instance.stub(:transactions_queued?).and_return(true) }
7
7
  end
8
8
 
@@ -15,15 +15,15 @@ describe FluidFeatures::AppReporter do
15
15
  let(:transaction) { mock('transaction', url: 'http://example.com/source.html', duration: 999, user: user, unknown_features: [], features_hit: %w[feature], goals_hit: %w[goal]) }
16
16
 
17
17
  before(:each) do
18
- described_class.any_instance.stub(:run_loop)
19
18
  FluidFeatures.stub(:config).and_return(config)
20
19
  end
21
20
 
22
21
  describe "#features_storage" do
23
22
  before(:each) do
24
23
  described_class.any_instance.stub(:configure)
25
- FluidFeatures::Persistence::Features.should_receive(:create).with(config["cache"]).twice
26
- .and_return(FluidFeatures::Persistence::NullFeatures.new)
24
+ FluidFeatures::Persistence::Features
25
+ .should_receive(:create).with(config["cache"])
26
+ .and_return(FluidFeatures::Persistence::NullFeatures.new)
27
27
  end
28
28
 
29
29
  it "should create features storage" do
@@ -184,6 +184,17 @@ describe FluidFeatures::AppReporter do
184
184
  end
185
185
  end
186
186
 
187
+ describe "#start_sending" do
188
+ it "#start_sending should call send_transactions" do
189
+ reporter.should_receive(:transactions_queued?).and_return(true)
190
+ # send_transactions needs to return success=true to prevent throttling
191
+ reporter.should_receive(:send_transactions).and_return(true)
192
+ reporter.start_sending
193
+ sleep(0.001) # wait for thread to start
194
+ reporter.stop_sending(wait=true)
195
+ end
196
+ end
197
+
187
198
  describe "#send_transactions" do
188
199
  let(:unknown_features) { { "feature" => { "a" => true } } }
189
200
 
@@ -4,7 +4,7 @@ require "fluidfeatures/persistence/features"
4
4
 
5
5
  describe FluidFeatures::AppState do
6
6
 
7
- it_should_behave_like "polling loop", :load_state
7
+ it_should_behave_like "polling loop"
8
8
 
9
9
  context do
10
10
  let(:state) { described_class.new(app) }
@@ -16,10 +16,14 @@ describe FluidFeatures::AppState do
16
16
  before(:each) do
17
17
  app.stub!(:is_a?).and_return(false)
18
18
  app.stub!(:is_a?).with(FluidFeatures::App).and_return(true)
19
- described_class.any_instance.stub(:run_loop)
20
19
  FluidFeatures.stub(:config).and_return(config)
21
20
  end
22
21
 
22
+ after(:each) do
23
+ # ensure the loop is shutdown
24
+ state.stop_receiving(wait=true)
25
+ end
26
+
23
27
  describe "#features_storage" do
24
28
  before(:each) do
25
29
  described_class.any_instance.stub(:configure)
@@ -48,19 +52,26 @@ describe FluidFeatures::AppState do
48
52
  features = mock("features")
49
53
  features_storage.should_receive(:list).and_return(features)
50
54
  state.configure(app)
51
- state.instance_variable_get(:@features).should == features
55
+ state.should_receive(:run_loop)
56
+ state.should_receive(:load_state).with(false).and_return([false,nil])
57
+ state.features.should == features
52
58
  end
53
59
  end
54
60
 
55
61
  describe "#features=" do
56
- it "should replace features if there are changes" do
62
+ it "should replace features" do
57
63
  state.features_storage.should_receive(:replace).with({foo: "bar"})
58
64
  state.features = {foo: "bar"}
59
65
  end
66
+ end
60
67
 
61
- it "should not replace features if not amended" do
62
- state.features_storage.should_not_receive(:replace)
63
- state.features = {}
68
+ describe "#start_receiving" do
69
+ it "#start_receiving should call load_state" do
70
+ # load_state needs to return success=true to prevent throttling
71
+ state.should_receive(:load_state).and_return([true,{}])
72
+ state.start_receiving
73
+ sleep(0.001) # wait for thread to start
74
+ state.stop_receiving(wait=true)
64
75
  end
65
76
  end
66
77
 
data/spec/config_spec.rb CHANGED
@@ -1,18 +1,18 @@
1
1
  require "spec_helper"
2
+ require 'fluidfeatures/exceptions'
2
3
 
3
4
  describe FluidFeatures::Config do
4
- let(:path) { "#{File.dirname(__FILE__)}/fixtures/fluidfeatures.yml" }
5
+ let(:source) { "#{File.dirname(__FILE__)}/fixtures/fluidfeatures.yml" }
5
6
 
6
7
  let(:env) { "development" }
7
8
 
8
- let(:replacements) { {} }
9
-
10
- let(:config) { FluidFeatures::Config.new(path, env, replacements) }
9
+ let(:config) { FluidFeatures::Config.new(source, env) }
11
10
 
12
11
  context "parse file size" do
13
- [nil, 1024, "foo"].each do |input|
14
- it "should pass #{input.class.to_s} through" do
15
- described_class.parse_file_size(input).should == input
12
+ [nil, "foo", Object.new, Object, [], {} ].each do |input|
13
+ it "should raise exception for non-matching #{input.class.to_s} #{input||'nil'}" do
14
+ expect { described_class.parse_file_size(input) }
15
+ .to raise_error(FFeaturesConfigInvalid)
16
16
  end
17
17
  end
18
18
 
@@ -21,6 +21,12 @@ describe FluidFeatures::Config do
21
21
  described_class.parse_file_size(i).should == o
22
22
  end
23
23
  end
24
+
25
+ { "123" => 123, "456" => 456 }.each do |i, o|
26
+ it "should read '#{i}' as #{o}" do
27
+ described_class.parse_file_size(i).should == o
28
+ end
29
+ end
24
30
  end
25
31
 
26
32
  %w{test development production}.each do |env_name|
@@ -29,25 +35,25 @@ describe FluidFeatures::Config do
29
35
 
30
36
  it "should load environment configuration from yml file and merge common section" do
31
37
  config.vars.should == {
32
- "baseuri" => "baseuri",
38
+ "base_uri" => "base_uri",
33
39
  "cache" => { "enable" => false, "dir" => "cache_dir", "limit" => 2097152 },
34
- "appid" => "#{env_name}_appid",
40
+ "app_id" => "#{env_name}_app_id",
35
41
  "secret" => "#{env_name}_secret"
36
42
  }
37
43
  end
44
+ end
45
+ end
38
46
 
39
- context "and replacements" do
40
- let(:replacements) { { "baseuri" => "env_baseuri", "appid" => "env_appid", "secret" => "env_secret" } }
41
-
42
- it "should update variables with passed hash" do
43
- config.vars.should == {
44
- "baseuri" => "env_baseuri",
45
- "cache" => { "enable" => false, "dir" => "cache_dir", "limit" => 2097152 },
46
- "appid" => "env_appid",
47
- "secret" => "env_secret"
48
- }
49
- end
50
- end
47
+ context "with hash config" do
48
+ let(:source) { { "base_uri" => "env_base_uri", "app_id" => "env_app_id", "secret" => "env_secret" } }
49
+
50
+ it "should update variables with passed hash" do
51
+ config.vars.should == {
52
+ "base_uri" => "env_base_uri",
53
+ "app_id" => "env_app_id",
54
+ "secret" => "env_secret"
55
+ }
51
56
  end
52
57
  end
58
+
53
59
  end
@@ -1,18 +1,18 @@
1
1
  common:
2
- baseuri: baseuri
2
+ base_uri: base_uri
3
3
  cache:
4
4
  enable: false
5
5
  dir: cache_dir
6
6
  limit: 2mb
7
7
 
8
8
  development:
9
- appid: development_appid
9
+ app_id: development_app_id
10
10
  secret: development_secret
11
11
 
12
12
  test:
13
- appid: test_appid
13
+ app_id: test_app_id
14
14
  secret: test_secret
15
15
 
16
16
  production:
17
- appid: production_appid
17
+ app_id: production_app_id
18
18
  secret: production_secret
@@ -4,12 +4,12 @@ describe "FluidFeatures" do
4
4
 
5
5
  describe "App initialization" do
6
6
  it "should raise 'host not set' without valid base uri" do
7
- expect { FluidFeatures.app(config.remove("baseuri")) }.
8
- to raise_error(StandardError, /host not set/)
7
+ expect { FluidFeatures.app(config.remove("base_uri")) }.
8
+ to raise_error(FFeaturesBadParam, /base_uri/)
9
9
  end
10
10
 
11
11
  it "should raise 'app_id invalid' without valid app id" do
12
- expect { FluidFeatures.app(config.remove("appid")) }.
12
+ expect { FluidFeatures.app(config.remove("app_id")) }.
13
13
  to raise_error(StandardError, /app_id invalid/)
14
14
  end
15
15
 
@@ -19,7 +19,8 @@ describe "FluidFeatures" do
19
19
  end
20
20
 
21
21
  it "should set @config class variable to passed config" do
22
- FluidFeatures::Client.stub!(:new); FluidFeatures::App.stub!(:new)
22
+ FluidFeatures::Client.stub!(:new)
23
+ FluidFeatures::App.stub!(:new)
23
24
  config = mock("config", "[]" => nil, "[]=" => nil)
24
25
  FluidFeatures.app(config)
25
26
  FluidFeatures.config.should == config
@@ -32,12 +33,28 @@ describe "FluidFeatures" do
32
33
 
33
34
  let(:feature) { app.features.pop[feature_name] }
34
35
 
36
+ before(:each) do
37
+ app.state.should_receive(:run_loop).once do
38
+ # only iterate once and not in a thread
39
+ app.state.run_loop_iteration(0,0)
40
+ end
41
+ app.reporter.should_receive(:run_loop).once do
42
+ # only iterate once and not in a thread
43
+ app.reporter.run_loop_iteration(0,0,0,0)
44
+ end
45
+ end
46
+
47
+ after(:each) do
48
+ # ensure the loop is shutdown
49
+ app.state.stop_receiving(wait=true)
50
+ app.reporter.stop_sending(wait=true)
51
+ end
52
+
35
53
  specify "#feature_enabled? should create feature" do
36
54
  VCR.use_cassette('feature') do
37
55
  transaction.feature_enabled?(feature_name, "a", true)
38
56
  transaction.feature_enabled?(feature_name, "b", true)
39
57
  commit transaction
40
- sleep abit
41
58
  feature["name"].should == feature_name
42
59
  feature["versions"].size.should == 2
43
60
  end
@@ -51,5 +68,6 @@ describe "FluidFeatures" do
51
68
  sleep abit
52
69
  end
53
70
  end
71
+
54
72
  end
55
73
  end
@@ -26,7 +26,7 @@ describe FluidFeatures::Persistence::Features do
26
26
  context "null instance" do
27
27
  let(:storage) { described_class.create(nil) }
28
28
 
29
- specify { storage.list.should == {} }
29
+ specify { storage.list.should == nil }
30
30
 
31
31
  specify { storage.list_unknown.should == {} }
32
32
 
@@ -34,4 +34,4 @@ describe FluidFeatures::Persistence::Features do
34
34
 
35
35
  specify { storage.replace_unknown([["bucket0"], ["bucket1"]]).should == false }
36
36
  end
37
- end
37
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,9 +1,4 @@
1
1
 
2
- unless ENV["FLUIDFEATURES_APPID"]
3
- $stderr.puts "Expect ENV variabled FLUIDFEATURES_APPID FLUIDFEATURES_SECRET FLUIDFEATURES_BASEURI to run the tests"
4
- exit!
5
- end
6
-
7
2
  require 'vcr'
8
3
  require 'fluidfeatures'
9
4
 
@@ -3,9 +3,9 @@ module FluidFeatures
3
3
  def config
4
4
  {
5
5
  "cache" => { "enable" => true, "dir" => "spec/tmp", "limit" => 1024 ** 2 },
6
- "baseuri" => ENV["FLUIDFEATURES_BASEURI"],
7
- "appid" => ENV["FLUIDFEATURES_APPID"],
8
- "secret" => ENV["FLUIDFEATURES_SECRET"],
6
+ "base_uri" => "https://www.fluidfeatures.com/service",
7
+ "app_id" => "1vu33ki6emqe3",
8
+ "secret" => "secret",
9
9
  "logger" => Logger.new("/dev/null")
10
10
  }
11
11
  end
@@ -1,4 +1,4 @@
1
- shared_examples "polling loop" do |payload_method|
1
+ shared_examples "polling loop" do
2
2
  let(:app) { mock "FluidFeatures::App" }
3
3
 
4
4
  before(:each) do
@@ -22,18 +22,12 @@ shared_examples "polling loop" do |payload_method|
22
22
  described_class.new(app)
23
23
  end
24
24
 
25
- it "should call #run_loop" do
26
- described_class.any_instance.should_receive(:run_loop)
25
+ it "should not call #run_loop" do
26
+ described_class.any_instance.should_not_receive(:run_loop)
27
27
  described_class.new(app)
28
28
  end
29
29
  end
30
30
 
31
- it "#run_loop should call payload method" do
32
- described_class.any_instance.should_receive(payload_method)
33
- described_class.new(app)
34
- sleep 0.15
35
- end
36
-
37
31
  it "should initialize @app" do
38
32
  described_class.new(app).app.should == app
39
33
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluidfeatures
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-08 00:00:00.000000000 Z
12
+ date: 2013-01-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: persistent_http
@@ -165,6 +165,7 @@ files:
165
165
  - lib/fluidfeatures/client.rb
166
166
  - lib/fluidfeatures/config.rb
167
167
  - lib/fluidfeatures/const.rb
168
+ - lib/fluidfeatures/exceptions.rb
168
169
  - lib/fluidfeatures/persistence/buckets.rb
169
170
  - lib/fluidfeatures/persistence/features.rb
170
171
  - lib/fluidfeatures/persistence/storage.rb