copycopter_client 1.0.2 → 1.0.3

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.
@@ -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
-