fluidfeatures 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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