copycopter_client 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.textile +71 -0
- data/Rakefile +38 -0
- data/features/rails.feature +267 -0
- data/features/step_definitions/copycopter_server_steps.rb +65 -0
- data/features/step_definitions/rails_steps.rb +134 -0
- data/features/support/env.rb +8 -0
- data/features/support/rails_server.rb +118 -0
- data/init.rb +2 -0
- data/lib/copycopter_client/client.rb +117 -0
- data/lib/copycopter_client/configuration.rb +197 -0
- data/lib/copycopter_client/errors.rb +13 -0
- data/lib/copycopter_client/helper.rb +40 -0
- data/lib/copycopter_client/i18n_backend.rb +100 -0
- data/lib/copycopter_client/prefixed_logger.rb +41 -0
- data/lib/copycopter_client/rails.rb +31 -0
- data/lib/copycopter_client/railtie.rb +13 -0
- data/lib/copycopter_client/sync.rb +145 -0
- data/lib/copycopter_client/version.rb +8 -0
- data/lib/copycopter_client.rb +58 -0
- data/lib/tasks/copycopter_client_tasks.rake +6 -0
- data/spec/copycopter_client/client_spec.rb +208 -0
- data/spec/copycopter_client/configuration_spec.rb +252 -0
- data/spec/copycopter_client/helper_spec.rb +86 -0
- data/spec/copycopter_client/i18n_backend_spec.rb +133 -0
- data/spec/copycopter_client/prefixed_logger_spec.rb +25 -0
- data/spec/copycopter_client/sync_spec.rb +295 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/support/client_spec_helpers.rb +9 -0
- data/spec/support/defines_constants.rb +38 -0
- data/spec/support/fake_client.rb +42 -0
- data/spec/support/fake_copycopter_app.rb +136 -0
- data/spec/support/fake_html_safe_string.rb +20 -0
- data/spec/support/fake_logger.rb +68 -0
- data/spec/support/fake_passenger.rb +27 -0
- data/spec/support/fake_unicorn.rb +14 -0
- metadata +121 -0
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'copycopter_client/helper'
|
3
|
+
|
4
|
+
describe CopycopterClient::Helper do
|
5
|
+
subject { Object.new }
|
6
|
+
|
7
|
+
before do
|
8
|
+
class << subject
|
9
|
+
include CopycopterClient::Helper
|
10
|
+
def warn(*args); end # these are annoying in test output
|
11
|
+
end
|
12
|
+
I18n.stubs(:translate)
|
13
|
+
end
|
14
|
+
|
15
|
+
Spec::Matchers.define :have_translated do |key, default|
|
16
|
+
match do |ignored_subject|
|
17
|
+
extend Mocha::API
|
18
|
+
I18n.should have_received(:translate).with(key, :default => default)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it "translates keys on CopycopterClient.s" do
|
23
|
+
CopycopterClient.s('test.key', 'default')
|
24
|
+
should have_translated("test.key", 'default')
|
25
|
+
end
|
26
|
+
|
27
|
+
it "translates keys on CopycopterClient.copy_for" do
|
28
|
+
CopycopterClient.copy_for('test.key', 'default')
|
29
|
+
should have_translated("test.key", 'default')
|
30
|
+
end
|
31
|
+
|
32
|
+
it "uses existing scope by partial key when present" do
|
33
|
+
subject.stubs(:scope_key_by_partial => "controller.action.key")
|
34
|
+
class << subject
|
35
|
+
private :scope_key_by_partial
|
36
|
+
end
|
37
|
+
|
38
|
+
subject.s(".key")
|
39
|
+
|
40
|
+
subject.should have_received(:scope_key_by_partial).with(".key")
|
41
|
+
should have_translated("controller.action.key", nil)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should prepend current partial when key starts with . and inside a view" do
|
45
|
+
template = stub(:path_without_format_and_extension => "controller/action")
|
46
|
+
subject.stubs(:template => template)
|
47
|
+
|
48
|
+
subject.s(".key")
|
49
|
+
|
50
|
+
should have_translated("controller.action.key", nil)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should prepend controller and action when key starts with . and inside a controller" do
|
54
|
+
subject.stubs(:controller_name => "controller", :action_name => "action")
|
55
|
+
|
56
|
+
subject.s(".key")
|
57
|
+
|
58
|
+
should have_translated("controller.action.key", nil)
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "default assignment" do
|
62
|
+
before do
|
63
|
+
subject.stubs(:scope_copycopter_key_by_partial => '.key')
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should allow a hash with key default" do
|
67
|
+
subject.s(@key, :default => "Default string")
|
68
|
+
should have_translated('.key', "Default string")
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should not allow a hash with stringed key default" do
|
72
|
+
subject.s(@key, "default" => "Default string")
|
73
|
+
should have_translated('.key', nil)
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should not allow a hash with key other than default" do
|
77
|
+
subject.s(@key, :junk => "Default string")
|
78
|
+
should have_translated('.key', nil)
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should allow a string" do
|
82
|
+
subject.s(@key, "Default string")
|
83
|
+
should have_translated('.key', "Default string")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CopycopterClient::I18nBackend do
|
4
|
+
let(:sync) { {} }
|
5
|
+
|
6
|
+
def build_backend(config = {})
|
7
|
+
default_config = CopycopterClient::Configuration.new.to_hash
|
8
|
+
backend = CopycopterClient::I18nBackend.new(sync, default_config.update(config))
|
9
|
+
I18n.backend = backend
|
10
|
+
backend
|
11
|
+
end
|
12
|
+
|
13
|
+
before { @default_backend = I18n.backend }
|
14
|
+
after { I18n.backend = @default_backend }
|
15
|
+
|
16
|
+
subject { build_backend }
|
17
|
+
|
18
|
+
it "waits until the first download when reloaded" do
|
19
|
+
sync.stubs(:wait_for_download)
|
20
|
+
|
21
|
+
subject.reload!
|
22
|
+
|
23
|
+
sync.should have_received(:wait_for_download)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "includes the base i18n backend" do
|
27
|
+
should be_kind_of(I18n::Backend::Base)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "looks up a key in sync" do
|
31
|
+
value = 'hello'
|
32
|
+
sync['en.prefix.test.key'] = value
|
33
|
+
|
34
|
+
backend = build_backend
|
35
|
+
|
36
|
+
backend.translate('en', 'test.key', :scope => 'prefix').should == value
|
37
|
+
end
|
38
|
+
|
39
|
+
it "finds available locales" do
|
40
|
+
sync['en.key'] = ''
|
41
|
+
sync['fr.key'] = ''
|
42
|
+
|
43
|
+
subject.available_locales.should =~ %w(en fr)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "queues missing keys with default" do
|
47
|
+
default = 'default value'
|
48
|
+
|
49
|
+
subject.translate('en', 'test.key', :default => default).should == default
|
50
|
+
|
51
|
+
sync['en.test.key'].should == default
|
52
|
+
end
|
53
|
+
|
54
|
+
it "queues missing keys without default" do
|
55
|
+
expect { subject.translate('en', 'test.key') }.
|
56
|
+
to raise_error(I18n::MissingTranslationData)
|
57
|
+
|
58
|
+
sync['en.test.key'].should == ""
|
59
|
+
end
|
60
|
+
|
61
|
+
it "queues missing keys with scope" do
|
62
|
+
default = 'default value'
|
63
|
+
|
64
|
+
subject.translate('en', 'key', :default => default, :scope => ['test']).
|
65
|
+
should == default
|
66
|
+
|
67
|
+
sync['en.test.key'].should == default
|
68
|
+
end
|
69
|
+
|
70
|
+
it "marks strings as html safe" do
|
71
|
+
sync['en.test.key'] = FakeHtmlSafeString.new("Hello")
|
72
|
+
backend = build_backend
|
73
|
+
backend.translate('en', 'test.key').should be_html_safe
|
74
|
+
end
|
75
|
+
|
76
|
+
it "looks up an array of defaults" do
|
77
|
+
sync['en.key.one'] = "Expected"
|
78
|
+
backend = build_backend
|
79
|
+
backend.translate('en', 'key.three', :default => [:"key.two", :"key.one"]).
|
80
|
+
should == 'Expected'
|
81
|
+
end
|
82
|
+
|
83
|
+
describe "with a fallback" do
|
84
|
+
let(:fallback) { I18n::Backend::Simple.new }
|
85
|
+
subject { build_backend(:fallback_backend => fallback) }
|
86
|
+
|
87
|
+
it "uses the fallback as a default" do
|
88
|
+
fallback.store_translations('en', 'test' => { 'key' => 'Expected' })
|
89
|
+
subject.translate('en', 'test.key', :default => 'Unexpected').
|
90
|
+
should include('Expected')
|
91
|
+
sync['en.test.key'].should == 'Expected'
|
92
|
+
end
|
93
|
+
|
94
|
+
it "preserves interpolation markers in the fallback" do
|
95
|
+
fallback.store_translations('en', 'test' => { 'key' => '%{interpolate}' })
|
96
|
+
subject.translate('en', 'test.key', :interpolate => 'interpolated').
|
97
|
+
should include('interpolated')
|
98
|
+
sync['en.test.key'].should == '%{interpolate}'
|
99
|
+
end
|
100
|
+
|
101
|
+
it "uses the default if the fallback doesn't have the key" do
|
102
|
+
subject.translate('en', 'test.key', :default => 'Expected').
|
103
|
+
should include('Expected')
|
104
|
+
end
|
105
|
+
|
106
|
+
it "uses the syncd key when present" do
|
107
|
+
fallback.store_translations('en', 'test' => { 'key' => 'Unexpected' })
|
108
|
+
sync['en.test.key'] = 'Expected'
|
109
|
+
subject.translate('en', 'test.key', :default => 'default').
|
110
|
+
should include('Expected')
|
111
|
+
end
|
112
|
+
|
113
|
+
it "returns a hash directly without storing" do
|
114
|
+
nested = { :nested => 'value' }
|
115
|
+
fallback.store_translations('en', 'key' => nested)
|
116
|
+
subject.translate('en', 'key', :default => 'Unexpected').should == nested
|
117
|
+
sync['en.key'].should be_nil
|
118
|
+
end
|
119
|
+
|
120
|
+
it "returns an array directly without storing" do
|
121
|
+
array = ['value']
|
122
|
+
fallback.store_translations('en', 'key' => array)
|
123
|
+
subject.translate('en', 'key', :default => 'Unexpected').should == array
|
124
|
+
sync['en.key'].should be_nil
|
125
|
+
end
|
126
|
+
|
127
|
+
it "looks up an array of defaults" do
|
128
|
+
fallback.store_translations('en', 'key' => { 'one' => 'Expected' })
|
129
|
+
subject.translate('en', 'key.three', :default => [:"key.two", :"key.one"]).
|
130
|
+
should include('Expected')
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CopycopterClient::PrefixedLogger do
|
4
|
+
let(:output_logger) { FakeLogger.new }
|
5
|
+
let(:prefix) { "** NOTICE:" }
|
6
|
+
let(:thread_info) { "[P:#{Process.pid}] [T:#{Thread.current.object_id}]" }
|
7
|
+
subject { CopycopterClient::PrefixedLogger.new(prefix, output_logger) }
|
8
|
+
|
9
|
+
it "provides the prefix" do
|
10
|
+
subject.prefix.should == prefix
|
11
|
+
end
|
12
|
+
|
13
|
+
it "provides the logger" do
|
14
|
+
subject.original_logger.should == output_logger
|
15
|
+
end
|
16
|
+
|
17
|
+
[:debug, :info, :warn, :error, :fatal].each do |level|
|
18
|
+
it "prefixes #{level} log messages" do
|
19
|
+
message = 'hello'
|
20
|
+
subject.send(level, message)
|
21
|
+
|
22
|
+
output_logger.should have_entry(level, "#{prefix} #{thread_info} #{message}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,295 @@
|
|
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
|
+
end
|
19
|
+
|
20
|
+
after { @syncs.each { |sync| sync.stop } }
|
21
|
+
|
22
|
+
it "syncs when the process terminates" do
|
23
|
+
api_key = "12345"
|
24
|
+
FakeCopycopterApp.add_project api_key
|
25
|
+
pid = fork do
|
26
|
+
config = { :logger => FakeLogger.new, :polling_delay => 86400, :api_key => api_key }
|
27
|
+
default_config = CopycopterClient::Configuration.new.to_hash.update(config)
|
28
|
+
real_client = CopycopterClient::Client.new(default_config)
|
29
|
+
sync = CopycopterClient::Sync.new(real_client, default_config)
|
30
|
+
sync.start
|
31
|
+
sleep 2
|
32
|
+
sync['test.key'] = 'value'
|
33
|
+
Signal.trap("INT") { exit }
|
34
|
+
sleep
|
35
|
+
end
|
36
|
+
sleep 3
|
37
|
+
Process.kill("INT", pid)
|
38
|
+
Process.wait
|
39
|
+
project = FakeCopycopterApp.project(api_key)
|
40
|
+
project.draft['test.key'].should == 'value'
|
41
|
+
end
|
42
|
+
|
43
|
+
it "provides access to downloaded data" do
|
44
|
+
client['en.test.key'] = 'expected'
|
45
|
+
client['en.test.other_key'] = 'expected'
|
46
|
+
|
47
|
+
sync = build_sync
|
48
|
+
|
49
|
+
sync.start
|
50
|
+
|
51
|
+
sync['en.test.key'].should == 'expected'
|
52
|
+
sync.keys.should =~ %w(en.test.key en.test.other_key)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "it polls after being started" do
|
56
|
+
sync = build_sync(:polling_delay => 1)
|
57
|
+
sync.start
|
58
|
+
|
59
|
+
sync['test.key'].should be_nil
|
60
|
+
|
61
|
+
client['test.key'] = 'value'
|
62
|
+
sleep(2)
|
63
|
+
|
64
|
+
sync['test.key'].should == 'value'
|
65
|
+
end
|
66
|
+
|
67
|
+
it "stops polling when stopped" do
|
68
|
+
sync = build_sync(:polling_delay => 1)
|
69
|
+
sync.start
|
70
|
+
|
71
|
+
sync['test.key'].should be_nil
|
72
|
+
|
73
|
+
sync.stop
|
74
|
+
|
75
|
+
client['test.key'] = 'value'
|
76
|
+
sleep(2)
|
77
|
+
|
78
|
+
sync['test.key'].should be_nil
|
79
|
+
end
|
80
|
+
|
81
|
+
it "doesn't upload without changes" do
|
82
|
+
sync = build_sync(:polling_delay => 1)
|
83
|
+
sync.start
|
84
|
+
sleep(2)
|
85
|
+
client.should_not be_uploaded
|
86
|
+
end
|
87
|
+
|
88
|
+
it "uploads changes when polling" do
|
89
|
+
sync = build_sync(:polling_delay => 1)
|
90
|
+
sync.start
|
91
|
+
|
92
|
+
sync['test.key'] = 'test value'
|
93
|
+
sleep(2)
|
94
|
+
|
95
|
+
client.uploaded.should == { 'test.key' => 'test value' }
|
96
|
+
end
|
97
|
+
|
98
|
+
it "handles connection errors when polling" do
|
99
|
+
failure = "server is napping"
|
100
|
+
logger = FakeLogger.new
|
101
|
+
client.stubs(:upload).raises(CopycopterClient::ConnectionError.new(failure))
|
102
|
+
sync = build_sync(:polling_delay => 1, :logger => logger)
|
103
|
+
|
104
|
+
sync['upload.key'] = 'upload'
|
105
|
+
sync.start
|
106
|
+
sleep(2)
|
107
|
+
|
108
|
+
logger.should have_entry(:error, failure),
|
109
|
+
logger.entries.inspect
|
110
|
+
|
111
|
+
client['test.key'] = 'test value'
|
112
|
+
sleep(2)
|
113
|
+
|
114
|
+
sync['test.key'].should == 'test value'
|
115
|
+
end
|
116
|
+
|
117
|
+
it "handles an invalid api key" do
|
118
|
+
failure = "server is napping"
|
119
|
+
logger = FakeLogger.new
|
120
|
+
client.stubs(:upload).raises(CopycopterClient::InvalidApiKey.new(failure))
|
121
|
+
sync = build_sync(:polling_delay => 1, :logger => logger)
|
122
|
+
|
123
|
+
sync['upload.key'] = 'upload'
|
124
|
+
sync.start
|
125
|
+
sleep(2)
|
126
|
+
|
127
|
+
logger.should have_entry(:error, failure),
|
128
|
+
logger.entries.inspect
|
129
|
+
|
130
|
+
client['test.key'] = 'test value'
|
131
|
+
sleep(2)
|
132
|
+
|
133
|
+
sync['test.key'].should be_nil
|
134
|
+
end
|
135
|
+
|
136
|
+
it "starts after spawning when using passenger" do
|
137
|
+
logger = FakeLogger.new
|
138
|
+
passenger = define_constant('PhusionPassenger', FakePassenger.new)
|
139
|
+
passenger.become_master
|
140
|
+
sync = build_sync(:polling_delay => 1, :logger => logger)
|
141
|
+
|
142
|
+
sync.start
|
143
|
+
sleep(2)
|
144
|
+
|
145
|
+
client.should_not be_downloaded
|
146
|
+
logger.should have_entry(:info, "Registered Phusion Passenger fork hook")
|
147
|
+
|
148
|
+
passenger.spawn
|
149
|
+
sleep(2)
|
150
|
+
|
151
|
+
client.should be_downloaded
|
152
|
+
logger.should have_entry(:info, "Starting poller"),
|
153
|
+
"Got entries: #{logger.entries.inspect}"
|
154
|
+
end
|
155
|
+
|
156
|
+
it "starts after spawning when using unicorn" do
|
157
|
+
logger = FakeLogger.new
|
158
|
+
define_constant('Unicorn', Module.new)
|
159
|
+
unicorn = define_constant('Unicorn::HttpServer', FakeUnicornServer).new
|
160
|
+
unicorn.become_master
|
161
|
+
sync = build_sync(:polling_delay => 1, :logger => logger)
|
162
|
+
CopycopterClient.sync = sync
|
163
|
+
|
164
|
+
sync.start
|
165
|
+
sleep(2)
|
166
|
+
|
167
|
+
client.should_not be_downloaded
|
168
|
+
logger.should have_entry(:info, "Registered Unicorn fork hook")
|
169
|
+
|
170
|
+
unicorn.spawn
|
171
|
+
sleep(2)
|
172
|
+
|
173
|
+
client.should be_downloaded
|
174
|
+
logger.should have_entry(:info, "Starting poller")
|
175
|
+
end
|
176
|
+
|
177
|
+
it "blocks until the first download is complete" do
|
178
|
+
logger = FakeLogger.new
|
179
|
+
client.delay = 1
|
180
|
+
sync = build_sync(:logger => logger)
|
181
|
+
|
182
|
+
sync.start
|
183
|
+
|
184
|
+
finished = false
|
185
|
+
Thread.new do
|
186
|
+
sync.wait_for_download
|
187
|
+
finished = true
|
188
|
+
end
|
189
|
+
|
190
|
+
logger.should have_entry(:info, "Waiting for first sync")
|
191
|
+
finished.should == false
|
192
|
+
|
193
|
+
sleep(3)
|
194
|
+
|
195
|
+
finished.should == true
|
196
|
+
end
|
197
|
+
|
198
|
+
it "doesn't block if the first download fails" do
|
199
|
+
client.delay = 1
|
200
|
+
client.stubs(:upload).raises(StandardError.new("Failure"))
|
201
|
+
sync = build_sync
|
202
|
+
|
203
|
+
sync['test.key'] = 'value'
|
204
|
+
sync.start
|
205
|
+
|
206
|
+
finished = false
|
207
|
+
Thread.new do
|
208
|
+
sync.wait_for_download
|
209
|
+
finished = true
|
210
|
+
end
|
211
|
+
|
212
|
+
finished.should == false
|
213
|
+
|
214
|
+
sleep(4)
|
215
|
+
|
216
|
+
finished.should == true
|
217
|
+
end
|
218
|
+
|
219
|
+
it "doesn't block before starting" do
|
220
|
+
logger = FakeLogger.new
|
221
|
+
sync = build_sync(:polling_delay => 3, :logger => logger)
|
222
|
+
|
223
|
+
finished = false
|
224
|
+
Thread.new do
|
225
|
+
sync.wait_for_download
|
226
|
+
finished = true
|
227
|
+
end
|
228
|
+
|
229
|
+
sleep(1)
|
230
|
+
|
231
|
+
finished.should == true
|
232
|
+
logger.should_not have_entry(:info, "Waiting for first sync")
|
233
|
+
end
|
234
|
+
|
235
|
+
describe "given locked mutex" do
|
236
|
+
Spec::Matchers.define :finish_after_unlocking do |mutex|
|
237
|
+
match do |thread|
|
238
|
+
sleep(0.1)
|
239
|
+
|
240
|
+
if thread.status === false
|
241
|
+
violated("finished before unlocking")
|
242
|
+
else
|
243
|
+
mutex.unlock
|
244
|
+
sleep(0.1)
|
245
|
+
|
246
|
+
if thread.status === false
|
247
|
+
true
|
248
|
+
else
|
249
|
+
violated("still running after unlocking")
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def violated(failure)
|
255
|
+
@failure_message = failure
|
256
|
+
false
|
257
|
+
end
|
258
|
+
|
259
|
+
failure_message_for_should do
|
260
|
+
@failure_message
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
let(:mutex) { Mutex.new }
|
265
|
+
let(:sync) { build_sync(:polling_delay => 0.1) }
|
266
|
+
|
267
|
+
before do
|
268
|
+
mutex.lock
|
269
|
+
Mutex.stubs(:new => mutex)
|
270
|
+
end
|
271
|
+
|
272
|
+
it "synchronizes read access to keys between threads" do
|
273
|
+
Thread.new { sync['test.key'] }.should finish_after_unlocking(mutex)
|
274
|
+
end
|
275
|
+
|
276
|
+
it "synchronizes read access to the key list between threads" do
|
277
|
+
Thread.new { sync.keys }.should finish_after_unlocking(mutex)
|
278
|
+
end
|
279
|
+
|
280
|
+
it "synchronizes write access to keys between threads" do
|
281
|
+
Thread.new { sync['test.key'] = 'value' }.should finish_after_unlocking(mutex)
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
it "starts from the top-level constant" do
|
286
|
+
sync = build_sync
|
287
|
+
CopycopterClient.sync = sync
|
288
|
+
sync.stubs(:start)
|
289
|
+
|
290
|
+
CopycopterClient.start_sync
|
291
|
+
|
292
|
+
sync.should have_received(:start)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec'
|
3
|
+
require 'spec/autorun'
|
4
|
+
require 'bourne'
|
5
|
+
require 'sham_rack'
|
6
|
+
require 'webmock/rspec'
|
7
|
+
|
8
|
+
PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
9
|
+
|
10
|
+
$LOAD_PATH << File.join(PROJECT_ROOT, "lib")
|
11
|
+
|
12
|
+
require "copycopter_client"
|
13
|
+
|
14
|
+
Dir.glob(File.join(PROJECT_ROOT, "spec", "support", "**", "*.rb")).each do |file|
|
15
|
+
require(file)
|
16
|
+
end
|
17
|
+
|
18
|
+
WebMock.disable_net_connect!
|
19
|
+
ShamRack.mount(FakeCopycopterApp.new, "copycopter.com")
|
20
|
+
|
21
|
+
Spec::Runner.configure do |config|
|
22
|
+
config.include ClientSpecHelpers
|
23
|
+
config.include WebMock
|
24
|
+
config.mock_with :mocha
|
25
|
+
config.before do
|
26
|
+
FakeCopycopterApp.reset
|
27
|
+
reset_config
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
share_as :DefinesConstants do
|
2
|
+
def define_class(class_name, base = Object, &block)
|
3
|
+
class_name = class_name.to_s.camelize
|
4
|
+
klass = Class.new(base)
|
5
|
+
define_constant(class_name, klass)
|
6
|
+
klass.class_eval(&block) if block_given?
|
7
|
+
klass
|
8
|
+
end
|
9
|
+
|
10
|
+
def define_constant(path, value)
|
11
|
+
parse_constant(path) do |parent, name|
|
12
|
+
parent.const_set(name, value)
|
13
|
+
end
|
14
|
+
|
15
|
+
@defined_constants << path
|
16
|
+
value
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse_constant(path)
|
20
|
+
parent_names = path.split('::')
|
21
|
+
name = parent_names.pop
|
22
|
+
parent = parent_names.inject(Object) do |ref, child_name|
|
23
|
+
ref.const_get(child_name)
|
24
|
+
end
|
25
|
+
yield(parent, name)
|
26
|
+
end
|
27
|
+
|
28
|
+
before { @defined_constants = [] }
|
29
|
+
|
30
|
+
after do
|
31
|
+
@defined_constants.reverse.each do |path|
|
32
|
+
parse_constant(path) do |parent, name|
|
33
|
+
parent.send(:remove_const, name)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class FakeClient
|
2
|
+
def initialize
|
3
|
+
@data = {}
|
4
|
+
@uploaded = {}
|
5
|
+
@uploads = 0
|
6
|
+
@downloads = 0
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :uploaded, :uploads, :downloads
|
10
|
+
attr_accessor :delay
|
11
|
+
|
12
|
+
def []=(key, value)
|
13
|
+
@data[key] = value
|
14
|
+
end
|
15
|
+
|
16
|
+
def download
|
17
|
+
wait_for_delay
|
18
|
+
@downloads += 1
|
19
|
+
@data.dup
|
20
|
+
end
|
21
|
+
|
22
|
+
def upload(data)
|
23
|
+
wait_for_delay
|
24
|
+
@uploaded.update(data)
|
25
|
+
@uploads += 1
|
26
|
+
end
|
27
|
+
|
28
|
+
def uploaded?
|
29
|
+
@uploads > 0
|
30
|
+
end
|
31
|
+
|
32
|
+
def downloaded?
|
33
|
+
@downloads > 0
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def wait_for_delay
|
39
|
+
sleep(delay) if delay
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|