copycopter_client 1.0.2 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,12 +2,12 @@ require 'spec_helper'
2
2
 
3
3
  describe CopycopterClient::RequestSync do
4
4
 
5
- let(:sync) { {} }
5
+ let(:cache) { {} }
6
6
  let(:response) { 'response' }
7
7
  let(:env) { 'env' }
8
8
  let(:app) { stub('app', :call => response) }
9
- before { sync.stubs(:flush => nil, :download => nil) }
10
- subject { CopycopterClient::RequestSync.new(app, :sync => sync) }
9
+ before { cache.stubs(:flush => nil, :download => nil) }
10
+ subject { CopycopterClient::RequestSync.new(app, :cache => cache) }
11
11
 
12
12
  it "invokes the upstream app" do
13
13
  result = subject.call(env)
@@ -17,11 +17,11 @@ describe CopycopterClient::RequestSync do
17
17
 
18
18
  it "flushes defaults" do
19
19
  subject.call(env)
20
- sync.should have_received(:flush)
20
+ cache.should have_received(:flush)
21
21
  end
22
22
 
23
23
  it "downloads new copy" do
24
24
  subject.call(env)
25
- sync.should have_received(:download)
25
+ cache.should have_received(:download)
26
26
  end
27
27
  end
@@ -7,7 +7,7 @@ class FakeClient
7
7
  end
8
8
 
9
9
  attr_reader :uploaded, :uploads, :downloads
10
- attr_accessor :delay
10
+ attr_accessor :delay, :error
11
11
 
12
12
  def []=(key, value)
13
13
  @data[key] = value
@@ -15,6 +15,7 @@ class FakeClient
15
15
 
16
16
  def download
17
17
  wait_for_delay
18
+ raise_error_if_present
18
19
  @downloads += 1
19
20
  yield @data.dup
20
21
  nil
@@ -22,6 +23,7 @@ class FakeClient
22
23
 
23
24
  def upload(data)
24
25
  wait_for_delay
26
+ raise_error_if_present
25
27
  @uploaded.update(data)
26
28
  @uploads += 1
27
29
  end
@@ -39,5 +41,9 @@ class FakeClient
39
41
  def wait_for_delay
40
42
  sleep(delay) if delay
41
43
  end
44
+
45
+ def raise_error_if_present
46
+ raise error if error
47
+ end
42
48
  end
43
49
 
@@ -1,11 +1,18 @@
1
1
  class FakeResqueJob
2
- attr_accessor :sync
3
- def initialize(hash)
4
- @key = hash[:key]
5
- @value = hash[:value]
2
+ def initialize(&action)
3
+ @action = action || lambda {}
6
4
  end
5
+
6
+ def fork_and_perform
7
+ fork do
8
+ perform
9
+ exit!
10
+ end
11
+ Process.wait
12
+ end
13
+
7
14
  def perform
8
- sync[@key] = @value
15
+ @action.call
9
16
  true
10
17
  end
11
18
  end
@@ -0,0 +1,17 @@
1
+ class WritingCache
2
+ def flush
3
+ File.open(path, "w") do |file|
4
+ file.write(object_id.to_s)
5
+ end
6
+ end
7
+
8
+ def written?
9
+ IO.read(path) == object_id.to_s
10
+ end
11
+
12
+ private
13
+
14
+ def path
15
+ File.join(PROJECT_ROOT, 'tmp', 'written_cache')
16
+ end
17
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: copycopter_client
3
3
  version: !ruby/object:Gem::Version
4
- hash: 19
4
+ hash: 17
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
8
  - 0
9
- - 2
10
- version: 1.0.2
9
+ - 3
10
+ version: 1.0.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - thoughtbot
@@ -15,11 +15,12 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-03-10 00:00:00 -05:00
18
+ date: 2011-06-21 00:00:00 -04:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
- type: :runtime
22
+ name: i18n
23
+ prerelease: false
23
24
  requirement: &id001 !ruby/object:Gem::Requirement
24
25
  none: false
25
26
  requirements:
@@ -31,11 +32,11 @@ dependencies:
31
32
  - 5
32
33
  - 0
33
34
  version: 0.5.0
35
+ type: :runtime
34
36
  version_requirements: *id001
35
- name: i18n
36
- prerelease: false
37
37
  - !ruby/object:Gem::Dependency
38
- type: :runtime
38
+ name: json
39
+ prerelease: false
39
40
  requirement: &id002 !ruby/object:Gem::Requirement
40
41
  none: false
41
42
  requirements:
@@ -45,9 +46,8 @@ dependencies:
45
46
  segments:
46
47
  - 0
47
48
  version: "0"
49
+ type: :runtime
48
50
  version_requirements: *id002
49
- name: json
50
- prerelease: false
51
51
  description:
52
52
  email: support@thoughtbot.com
53
53
  executables: []
@@ -62,24 +62,28 @@ files:
62
62
  - Rakefile
63
63
  - init.rb
64
64
  - AddTrustExternalCARoot.crt
65
+ - lib/copycopter_client/cache.rb
65
66
  - lib/copycopter_client/client.rb
66
67
  - lib/copycopter_client/configuration.rb
67
68
  - lib/copycopter_client/errors.rb
68
69
  - lib/copycopter_client/i18n_backend.rb
70
+ - lib/copycopter_client/poller.rb
69
71
  - lib/copycopter_client/prefixed_logger.rb
72
+ - lib/copycopter_client/process_guard.rb
70
73
  - lib/copycopter_client/rails.rb
71
74
  - lib/copycopter_client/railtie.rb
72
75
  - lib/copycopter_client/request_sync.rb
73
- - lib/copycopter_client/sync.rb
74
76
  - lib/copycopter_client/version.rb
75
77
  - lib/copycopter_client.rb
76
78
  - lib/tasks/copycopter_client_tasks.rake
79
+ - spec/copycopter_client/cache_spec.rb
77
80
  - spec/copycopter_client/client_spec.rb
78
81
  - spec/copycopter_client/configuration_spec.rb
79
82
  - spec/copycopter_client/i18n_backend_spec.rb
83
+ - spec/copycopter_client/poller_spec.rb
80
84
  - spec/copycopter_client/prefixed_logger_spec.rb
85
+ - spec/copycopter_client/process_guard_spec.rb
81
86
  - spec/copycopter_client/request_sync_spec.rb
82
- - spec/copycopter_client/sync_spec.rb
83
87
  - spec/spec_helper.rb
84
88
  - spec/support/client_spec_helpers.rb
85
89
  - spec/support/defines_constants.rb
@@ -91,6 +95,7 @@ files:
91
95
  - spec/support/fake_resque_job.rb
92
96
  - spec/support/fake_unicorn.rb
93
97
  - spec/support/middleware_stack.rb
98
+ - spec/support/writing_cache.rb
94
99
  - features/rails.feature
95
100
  - features/step_definitions/copycopter_server_steps.rb
96
101
  - features/step_definitions/rails_steps.rb
@@ -126,7 +131,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
131
  requirements: []
127
132
 
128
133
  rubyforge_project: copycopter_client
129
- rubygems_version: 1.6.1
134
+ rubygems_version: 1.4.1
130
135
  signing_key:
131
136
  specification_version: 3
132
137
  summary: Client for the Copycopter content management service
@@ -1,182 +0,0 @@
1
- require 'thread'
2
- require 'copycopter_client/client'
3
-
4
- module CopycopterClient
5
- # Manages synchronization of copy between {I18nBackend} and {Client}. Acts
6
- # like a Hash. Applications using the client will not need to interact with
7
- # this class directly.
8
- #
9
- # Responsible for:
10
- # * Starting and running the background polling thread
11
- # * Locking down access to data used by both threads
12
- class Sync
13
- # Usually instantiated when {Configuration#apply} is invoked.
14
- # @param client [Client] the client used to fetch and upload data
15
- # @param options [Hash]
16
- # @option options [Fixnum] :polling_delay the number of seconds in between each synchronization with the server
17
- # @option options [Logger] :logger where errors should be logged
18
- def initialize(client, options)
19
- @client = client
20
- @blurbs = {}
21
- @polling_delay = options[:polling_delay]
22
- @stop = false
23
- @queued = {}
24
- @mutex = Mutex.new
25
- @logger = options[:logger]
26
- @pending = false
27
- end
28
-
29
- # Starts the polling thread. The polling thread doesn't run in test environments.
30
- #
31
- # If this sync was created from a master spawner (as in the case for
32
- # phusion passenger), it will instead register after fork hooks so that the
33
- # poller starts in each spawned process.
34
- def start
35
- if spawner?
36
- register_spawn_hooks
37
- else
38
- register_job_hooks
39
- logger.info("Starting poller")
40
- @pending = true
41
- at_exit { sync }
42
- unless Thread.new { poll }
43
- logger.error("Couldn't start poller thread")
44
- end
45
- end
46
- end
47
-
48
- # Stops the polling thread after the next run.
49
- def stop
50
- @stop = true
51
- end
52
-
53
- # Returns content for the given blurb.
54
- # @param key [String] the key of the desired blurb
55
- # @return [String] the contents of the blurb
56
- def [](key)
57
- lock { @blurbs[key] }
58
- end
59
-
60
- # Sets content for the given blurb. The content will be pushed to the
61
- # server on the next poll.
62
- # @param key [String] the key of the blurb to update
63
- # @param value [String] the new contents of the blurb
64
- def []=(key, value)
65
- lock { @queued[key] = value }
66
- end
67
-
68
- # Keys for all blurbs stored on the server.
69
- # @return [Array<String>] keys
70
- def keys
71
- lock { @blurbs.keys }
72
- end
73
-
74
- # Waits until the first download has finished.
75
- def wait_for_download
76
- if @pending
77
- logger.info("Waiting for first sync")
78
- logger.flush if logger.respond_to?(:flush)
79
- while @pending
80
- sleep(0.1)
81
- end
82
- end
83
- end
84
-
85
- def flush
86
- with_queued_changes do |queued|
87
- client.upload(queued)
88
- end
89
- rescue ConnectionError => error
90
- logger.error(error.message)
91
- end
92
-
93
- def download
94
- client.download do |downloaded_blurbs|
95
- downloaded_blurbs.reject! { |key, value| value == "" }
96
- lock { @blurbs = downloaded_blurbs }
97
- end
98
- rescue ConnectionError => error
99
- logger.error(error.message)
100
- end
101
-
102
- private
103
-
104
- attr_reader :client, :polling_delay, :logger
105
-
106
- def poll
107
- until @stop
108
- sync
109
- logger.flush if logger.respond_to?(:flush)
110
- sleep(polling_delay)
111
- end
112
- rescue InvalidApiKey => error
113
- logger.error(error.message)
114
- end
115
-
116
- def sync
117
- download
118
- flush
119
- ensure
120
- @pending = false
121
- end
122
-
123
- def with_queued_changes
124
- changes_to_push = nil
125
- lock do
126
- unless @queued.empty?
127
- changes_to_push = @queued
128
- @queued = {}
129
- end
130
- end
131
- yield(changes_to_push) if changes_to_push
132
- end
133
-
134
- def lock(&block)
135
- @mutex.synchronize(&block)
136
- end
137
-
138
- def spawner?
139
- passenger_spawner? || unicorn_spawner?
140
- end
141
-
142
- def passenger_spawner?
143
- $0.include?("ApplicationSpawner")
144
- end
145
-
146
- def unicorn_spawner?
147
- $0.include?("unicorn") && !caller.any? { |line| line.include?("worker_loop") }
148
- end
149
-
150
- def register_job_hooks
151
- if defined?(Resque)
152
- logger.info("Registered Resque after_perform hook")
153
- Resque::Job.class_eval do
154
- alias_method :perform_without_copycopter, :perform
155
- def perform
156
- job_was_performed = perform_without_copycopter
157
- CopycopterClient.flush
158
- job_was_performed
159
- end
160
- end
161
- end
162
- end
163
-
164
- def register_spawn_hooks
165
- if defined?(PhusionPassenger)
166
- logger.info("Registered Phusion Passenger fork hook")
167
- PhusionPassenger.on_event(:starting_worker_process) do |forked|
168
- start
169
- end
170
- elsif defined?(Unicorn::HttpServer)
171
- logger.info("Registered Unicorn fork hook")
172
- Unicorn::HttpServer.class_eval do
173
- alias_method :worker_loop_without_copycopter, :worker_loop
174
- def worker_loop(worker)
175
- CopycopterClient.start_sync
176
- worker_loop_without_copycopter(worker)
177
- end
178
- end
179
- end
180
- end
181
- end
182
- end
@@ -1,415 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe CopycopterClient::Sync do
4
- include DefinesConstants
5
-
6
- let(:client) { FakeClient.new }
7
-
8
- def build_sync(config = {})
9
- config[:logger] ||= FakeLogger.new
10
- default_config = CopycopterClient::Configuration.new.to_hash
11
- sync = CopycopterClient::Sync.new(client, default_config.update(config))
12
- @syncs << sync
13
- sync
14
- end
15
-
16
- before do
17
- @syncs = []
18
- @original_process_name = $0
19
- end
20
-
21
- after do
22
- $0 = @original_process_name
23
- @syncs.each { |sync| sync.stop }
24
- end
25
-
26
- it "syncs when the process terminates" do
27
- api_key = "12345"
28
- FakeCopycopterApp.add_project api_key
29
- pid = fork do
30
- config = { :logger => FakeLogger.new, :polling_delay => 86400, :api_key => api_key }
31
- default_config = CopycopterClient::Configuration.new.to_hash.update(config)
32
- real_client = CopycopterClient::Client.new(default_config)
33
- sync = CopycopterClient::Sync.new(real_client, default_config)
34
- sync.start
35
- sleep 2
36
- sync['test.key'] = 'value'
37
- Signal.trap("INT") { exit }
38
- sleep
39
- end
40
- sleep 3
41
- Process.kill("INT", pid)
42
- Process.wait
43
- project = FakeCopycopterApp.project(api_key)
44
- project.draft['test.key'].should == 'value'
45
- end
46
-
47
- it "provides access to downloaded data" do
48
- client['en.test.key'] = 'expected'
49
- client['en.test.other_key'] = 'expected'
50
-
51
- sync = build_sync
52
-
53
- sync.start
54
-
55
- sync['en.test.key'].should == 'expected'
56
- sync.keys.should =~ %w(en.test.key en.test.other_key)
57
- end
58
-
59
- it "it polls after being started" do
60
- sync = build_sync(:polling_delay => 1)
61
- sync.start
62
-
63
- sync['test.key'].should be_nil
64
-
65
- client['test.key'] = 'value'
66
- sleep(2)
67
-
68
- sync['test.key'].should == 'value'
69
- end
70
-
71
- it "stops polling when stopped" do
72
- sync = build_sync(:polling_delay => 1)
73
- sync.start
74
-
75
- sync['test.key'].should be_nil
76
-
77
- sync.stop
78
-
79
- client['test.key'] = 'value'
80
- sleep(2)
81
-
82
- sync['test.key'].should be_nil
83
- end
84
-
85
- it "doesn't upload without changes" do
86
- sync = build_sync(:polling_delay => 1)
87
- sync.start
88
- sleep(2)
89
- client.should_not be_uploaded
90
- end
91
-
92
- it "uploads changes when polling" do
93
- sync = build_sync(:polling_delay => 1)
94
- sync.start
95
-
96
- sync['test.key'] = 'test value'
97
- sleep(2)
98
-
99
- client.uploaded.should == { 'test.key' => 'test value' }
100
- end
101
-
102
- it "uploads changes when flushed" do
103
- sync = build_sync
104
- sync['test.key'] = 'test value'
105
-
106
- sync.flush
107
-
108
- client.uploaded.should == { 'test.key' => 'test value' }
109
- end
110
-
111
- it "downloads changes" do
112
- client['test.key'] = 'test value'
113
- sync = build_sync
114
-
115
- sync.download
116
-
117
- sync['test.key'].should == 'test value'
118
- end
119
-
120
- it "handles connection errors when flushing" do
121
- failure = "server is napping"
122
- logger = FakeLogger.new
123
- client.stubs(:upload).raises(CopycopterClient::ConnectionError.new(failure))
124
- sync = build_sync(:logger => logger)
125
- sync['upload.key'] = 'upload'
126
-
127
- sync.flush
128
-
129
- logger.should have_entry(:error, failure)
130
- end
131
-
132
- it "handles connection errors when downloading" do
133
- failure = "server is napping"
134
- logger = FakeLogger.new
135
- client.stubs(:download).raises(CopycopterClient::ConnectionError.new(failure))
136
- sync = build_sync(:logger => logger)
137
-
138
- sync.download
139
-
140
- logger.should have_entry(:error, failure)
141
- end
142
-
143
- it "handles connection errors when polling" do
144
- failure = "server is napping"
145
- logger = FakeLogger.new
146
- client.stubs(:upload).raises(CopycopterClient::ConnectionError.new(failure))
147
- sync = build_sync(:polling_delay => 1, :logger => logger)
148
-
149
- sync['upload.key'] = 'upload'
150
- sync.start
151
- sleep(2)
152
-
153
- logger.should have_entry(:error, failure),
154
- logger.entries.inspect
155
-
156
- client['test.key'] = 'test value'
157
- sleep(2)
158
-
159
- sync['test.key'].should == 'test value'
160
- end
161
-
162
- it "handles an invalid api key" do
163
- failure = "server is napping"
164
- logger = FakeLogger.new
165
- client.stubs(:upload).raises(CopycopterClient::InvalidApiKey.new(failure))
166
- sync = build_sync(:polling_delay => 1, :logger => logger)
167
-
168
- sync['upload.key'] = 'upload'
169
- sync.start
170
- sleep(2)
171
-
172
- logger.should have_entry(:error, failure),
173
- logger.entries.inspect
174
-
175
- client['test.key'] = 'test value'
176
- sleep(2)
177
-
178
- sync['test.key'].should be_nil
179
- end
180
-
181
- it "starts after spawning when using passenger" do
182
- logger = FakeLogger.new
183
- passenger = define_constant('PhusionPassenger', FakePassenger.new)
184
- passenger.become_master
185
- sync = build_sync(:polling_delay => 1, :logger => logger)
186
-
187
- sync.start
188
- sleep(2)
189
-
190
- client.should_not be_downloaded
191
- logger.should have_entry(:info, "Registered Phusion Passenger fork hook")
192
-
193
- passenger.spawn
194
- sleep(2)
195
-
196
- client.should be_downloaded
197
- logger.should have_entry(:info, "Starting poller"),
198
- "Got entries: #{logger.entries.inspect}"
199
- end
200
-
201
- it "flushes after running a resque job" do
202
- define_constant('Resque', Module.new)
203
- job = define_constant('Resque::Job', FakeResqueJob).new(:key => 'test.key', :value => 'all your base')
204
-
205
- api_key = "12345"
206
- FakeCopycopterApp.add_project api_key
207
- logger = FakeLogger.new
208
-
209
- config = { :logger => logger, :polling_delay => 86400, :api_key => api_key }
210
- default_config = CopycopterClient::Configuration.new.to_hash.update(config)
211
- real_client = CopycopterClient::Client.new(default_config)
212
- sync = CopycopterClient::Sync.new(real_client, default_config)
213
- sync.stubs(:at_exit)
214
- CopycopterClient.sync = sync
215
- job.sync = sync
216
-
217
- sync.start
218
- sleep(2)
219
-
220
- logger.should have_entry(:info, "Registered Resque after_perform hook")
221
-
222
- if fork
223
- Process.wait
224
- else
225
- job.perform
226
- exit!
227
- end
228
- sleep(2)
229
-
230
- project = FakeCopycopterApp.project(api_key)
231
- project.draft['test.key'].should == 'all your base'
232
- end
233
-
234
- it "starts after spawning when using unicorn" do
235
- logger = FakeLogger.new
236
- define_constant('Unicorn', Module.new)
237
- unicorn = define_constant('Unicorn::HttpServer', FakeUnicornServer).new
238
- unicorn.become_master
239
- sync = build_sync(:polling_delay => 1, :logger => logger)
240
- CopycopterClient.sync = sync
241
-
242
- sync.start
243
- sleep(2)
244
-
245
- client.should_not be_downloaded
246
- logger.should have_entry(:info, "Registered Unicorn fork hook")
247
-
248
- unicorn.spawn
249
- sleep(2)
250
-
251
- client.should be_downloaded
252
- logger.should have_entry(:info, "Starting poller")
253
- end
254
-
255
- it "blocks until the first download is complete" do
256
- logger = FakeLogger.new
257
- logger.stubs(:flush)
258
- client.delay = 1
259
- sync = build_sync(:logger => logger)
260
-
261
- sync.start
262
-
263
- finished = false
264
- Thread.new do
265
- sync.wait_for_download
266
- finished = true
267
- end
268
-
269
- logger.should have_entry(:info, "Waiting for first sync")
270
- logger.should have_received(:flush)
271
- finished.should == false
272
-
273
- sleep(3)
274
-
275
- finished.should == true
276
- end
277
-
278
- it "doesn't block if the first download fails" do
279
- client.delay = 1
280
- client.stubs(:upload).raises(StandardError.new("Failure"))
281
- sync = build_sync
282
-
283
- sync['test.key'] = 'value'
284
- sync.start
285
-
286
- finished = false
287
- Thread.new do
288
- sync.wait_for_download
289
- finished = true
290
- end
291
-
292
- finished.should == false
293
-
294
- sleep(4)
295
-
296
- finished.should == true
297
- end
298
-
299
- it "doesn't block before starting" do
300
- logger = FakeLogger.new
301
- sync = build_sync(:polling_delay => 3, :logger => logger)
302
-
303
- finished = false
304
- Thread.new do
305
- sync.wait_for_download
306
- finished = true
307
- end
308
-
309
- sleep(1)
310
-
311
- finished.should == true
312
- logger.should_not have_entry(:info, "Waiting for first sync")
313
- end
314
-
315
- it "logs an error if the background thread can't start" do
316
- Thread.stubs(:new => nil)
317
- logger = FakeLogger.new
318
-
319
- build_sync(:logger => logger).start
320
-
321
- logger.should have_entry(:error, "Couldn't start poller thread")
322
- end
323
-
324
- it "flushes the log when polling" do
325
- logger = FakeLogger.new
326
- logger.stubs(:flush)
327
- sync = build_sync(:polling_delay => 1, :logger => logger)
328
-
329
- sync.start
330
- sleep(2)
331
-
332
- logger.should have_received(:flush).at_least_once
333
- end
334
-
335
- it "doesn't return blank copy" do
336
- client['en.test.key'] = ''
337
- sync = build_sync(:polling_delay => 1)
338
-
339
- sync.start
340
- sleep(2)
341
-
342
- sync['en.test.key'].should be_nil
343
- end
344
-
345
- describe "given locked mutex" do
346
- RSpec::Matchers.define :finish_after_unlocking do |mutex|
347
- match do |thread|
348
- sleep(0.1)
349
-
350
- if thread.status === false
351
- violated("finished before unlocking")
352
- else
353
- mutex.unlock
354
- sleep(0.1)
355
-
356
- if thread.status === false
357
- true
358
- else
359
- violated("still running after unlocking")
360
- end
361
- end
362
- end
363
-
364
- def violated(failure)
365
- @failure_message = failure
366
- false
367
- end
368
-
369
- failure_message_for_should do
370
- @failure_message
371
- end
372
- end
373
-
374
- let(:mutex) { Mutex.new }
375
- let(:sync) { build_sync(:polling_delay => 0.1) }
376
-
377
- before do
378
- mutex.lock
379
- Mutex.stubs(:new => mutex)
380
- end
381
-
382
- it "synchronizes read access to keys between threads" do
383
- Thread.new { sync['test.key'] }.should finish_after_unlocking(mutex)
384
- end
385
-
386
- it "synchronizes read access to the key list between threads" do
387
- Thread.new { sync.keys }.should finish_after_unlocking(mutex)
388
- end
389
-
390
- it "synchronizes write access to keys between threads" do
391
- Thread.new { sync['test.key'] = 'value' }.should finish_after_unlocking(mutex)
392
- end
393
- end
394
-
395
- it "starts from the top-level constant" do
396
- sync = build_sync
397
- CopycopterClient.sync = sync
398
- sync.stubs(:start)
399
-
400
- CopycopterClient.start_sync
401
-
402
- sync.should have_received(:start)
403
- end
404
-
405
- it "flushes from the top level" do
406
- sync = build_sync
407
- CopycopterClient.sync = sync
408
- sync.stubs(:flush)
409
-
410
- CopycopterClient.flush
411
-
412
- sync.should have_received(:flush)
413
- end
414
- end
415
-