copycopter_client 1.0.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|
+
|