copy_tuner_client 0.0.1
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/.gitignore +18 -0
- data/.rspec +2 -0
- data/.travis.yml +9 -0
- data/Appraisals +15 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +161 -0
- data/README.md +4 -0
- data/Rakefile +28 -0
- data/copy_tuner_client.gemspec +33 -0
- data/features/rails.feature +270 -0
- data/features/step_definitions/copycopter_server_steps.rb +64 -0
- data/features/step_definitions/rails_steps.rb +172 -0
- data/features/support/env.rb +11 -0
- data/features/support/rails_server.rb +124 -0
- data/gemfiles/2.3.gemfile +7 -0
- data/gemfiles/2.3.gemfile.lock +105 -0
- data/gemfiles/3.0.gemfile +7 -0
- data/gemfiles/3.0.gemfile.lock +147 -0
- data/gemfiles/3.1.gemfile +11 -0
- data/gemfiles/3.1.gemfile.lock +191 -0
- data/init.rb +1 -0
- data/lib/copy_tuner_client/cache.rb +144 -0
- data/lib/copy_tuner_client/client.rb +136 -0
- data/lib/copy_tuner_client/configuration.rb +224 -0
- data/lib/copy_tuner_client/errors.rb +12 -0
- data/lib/copy_tuner_client/i18n_backend.rb +92 -0
- data/lib/copy_tuner_client/poller.rb +44 -0
- data/lib/copy_tuner_client/prefixed_logger.rb +45 -0
- data/lib/copy_tuner_client/process_guard.rb +92 -0
- data/lib/copy_tuner_client/rails.rb +21 -0
- data/lib/copy_tuner_client/railtie.rb +12 -0
- data/lib/copy_tuner_client/request_sync.rb +39 -0
- data/lib/copy_tuner_client/version.rb +7 -0
- data/lib/copy_tuner_client.rb +75 -0
- data/lib/tasks/copy_tuner_client_tasks.rake +20 -0
- data/spec/copy_tuner_client/cache_spec.rb +273 -0
- data/spec/copy_tuner_client/client_spec.rb +236 -0
- data/spec/copy_tuner_client/configuration_spec.rb +305 -0
- data/spec/copy_tuner_client/i18n_backend_spec.rb +157 -0
- data/spec/copy_tuner_client/poller_spec.rb +108 -0
- data/spec/copy_tuner_client/prefixed_logger_spec.rb +37 -0
- data/spec/copy_tuner_client/process_guard_spec.rb +118 -0
- data/spec/copy_tuner_client/request_sync_spec.rb +47 -0
- data/spec/copy_tuner_client_spec.rb +19 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/client_spec_helpers.rb +8 -0
- data/spec/support/defines_constants.rb +44 -0
- data/spec/support/fake_client.rb +53 -0
- data/spec/support/fake_copy_tuner_app.rb +175 -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_resque_job.rb +18 -0
- data/spec/support/fake_unicorn.rb +13 -0
- data/spec/support/middleware_stack.rb +13 -0
- data/spec/support/writing_cache.rb +17 -0
- data/tmp/projects.json +1 -0
- metadata +389 -0
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CopyTunerClient::I18nBackend do
|
4
|
+
let(:cache) { {} }
|
5
|
+
|
6
|
+
def build_backend
|
7
|
+
backend = CopyTunerClient::I18nBackend.new(cache)
|
8
|
+
I18n.backend = backend
|
9
|
+
backend
|
10
|
+
end
|
11
|
+
|
12
|
+
before do
|
13
|
+
@default_backend = I18n.backend
|
14
|
+
cache.stubs(:wait_for_download)
|
15
|
+
end
|
16
|
+
|
17
|
+
after { I18n.backend = @default_backend }
|
18
|
+
|
19
|
+
subject { build_backend }
|
20
|
+
|
21
|
+
it "reloads locale files and waits for the download to complete" do
|
22
|
+
I18n.stubs(:load_path => [])
|
23
|
+
subject.reload!
|
24
|
+
subject.translate('en', 'test.key', :default => 'something')
|
25
|
+
|
26
|
+
cache.should have_received(:wait_for_download)
|
27
|
+
I18n.should have_received(:load_path)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "includes the base i18n backend" do
|
31
|
+
should be_kind_of(I18n::Backend::Base)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "looks up a key in cache" do
|
35
|
+
value = 'hello'
|
36
|
+
cache['en.prefix.test.key'] = value
|
37
|
+
|
38
|
+
backend = build_backend
|
39
|
+
|
40
|
+
backend.translate('en', 'test.key', :scope => 'prefix').should == value
|
41
|
+
end
|
42
|
+
|
43
|
+
it "finds available locales from locale files and cache" do
|
44
|
+
YAML.stubs(:load_file => { 'es' => { 'key' => 'value' } })
|
45
|
+
I18n.stubs(:load_path => ["test.yml"])
|
46
|
+
|
47
|
+
cache['en.key'] = ''
|
48
|
+
cache['fr.key'] = ''
|
49
|
+
|
50
|
+
subject.available_locales.should =~ [:en, :es, :fr]
|
51
|
+
end
|
52
|
+
|
53
|
+
it "queues missing keys with default" do
|
54
|
+
default = 'default value'
|
55
|
+
|
56
|
+
subject.translate('en', 'test.key', :default => default).should == default
|
57
|
+
|
58
|
+
cache['en.test.key'].should == default
|
59
|
+
end
|
60
|
+
|
61
|
+
it "queues missing keys without default" do
|
62
|
+
expect { subject.translate('en', 'test.key') }.
|
63
|
+
to throw_symbol(:exception)
|
64
|
+
|
65
|
+
cache['en.test.key'].should == ""
|
66
|
+
end
|
67
|
+
|
68
|
+
it "queues missing keys with scope" do
|
69
|
+
default = 'default value'
|
70
|
+
|
71
|
+
subject.translate('en', 'key', :default => default, :scope => ['test']).
|
72
|
+
should == default
|
73
|
+
|
74
|
+
cache['en.test.key'].should == default
|
75
|
+
end
|
76
|
+
|
77
|
+
it "marks strings as html safe" do
|
78
|
+
cache['en.test.key'] = FakeHtmlSafeString.new("Hello")
|
79
|
+
backend = build_backend
|
80
|
+
backend.translate('en', 'test.key').should be_html_safe
|
81
|
+
end
|
82
|
+
|
83
|
+
it "looks up an array of defaults" do
|
84
|
+
cache['en.key.one'] = "Expected"
|
85
|
+
backend = build_backend
|
86
|
+
backend.translate('en', 'key.three', :default => [:"key.two", :"key.one"]).
|
87
|
+
should == 'Expected'
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "with stored translations" do
|
91
|
+
subject { build_backend }
|
92
|
+
|
93
|
+
it "uses stored translations as a default" do
|
94
|
+
subject.store_translations('en', 'test' => { 'key' => 'Expected' })
|
95
|
+
subject.translate('en', 'test.key', :default => 'Unexpected').
|
96
|
+
should include('Expected')
|
97
|
+
cache['en.test.key'].should == 'Expected'
|
98
|
+
end
|
99
|
+
|
100
|
+
it "preserves interpolation markers in the stored translation" do
|
101
|
+
subject.store_translations('en', 'test' => { 'key' => '%{interpolate}' })
|
102
|
+
subject.translate('en', 'test.key', :interpolate => 'interpolated').
|
103
|
+
should include('interpolated')
|
104
|
+
cache['en.test.key'].should == '%{interpolate}'
|
105
|
+
end
|
106
|
+
|
107
|
+
it "uses the default if the stored translations don't have the key" do
|
108
|
+
subject.translate('en', 'test.key', :default => 'Expected').
|
109
|
+
should include('Expected')
|
110
|
+
end
|
111
|
+
|
112
|
+
it "uses the cached key when present" do
|
113
|
+
subject.store_translations('en', 'test' => { 'key' => 'Unexpected' })
|
114
|
+
cache['en.test.key'] = 'Expected'
|
115
|
+
subject.translate('en', 'test.key', :default => 'default').
|
116
|
+
should include('Expected')
|
117
|
+
end
|
118
|
+
|
119
|
+
it "stores a nested hash" do
|
120
|
+
nested = { :nested => 'value' }
|
121
|
+
subject.store_translations('en', 'key' => nested)
|
122
|
+
subject.translate('en', 'key', :default => 'Unexpected').should == nested
|
123
|
+
cache['en.key.nested'].should == 'value'
|
124
|
+
end
|
125
|
+
|
126
|
+
it "returns an array directly without storing" do
|
127
|
+
array = ['value']
|
128
|
+
subject.store_translations('en', 'key' => array)
|
129
|
+
subject.translate('en', 'key', :default => 'Unexpected').should == array
|
130
|
+
cache['en.key'].should be_nil
|
131
|
+
end
|
132
|
+
|
133
|
+
it "looks up an array of defaults" do
|
134
|
+
subject.store_translations('en', 'key' => { 'one' => 'Expected' })
|
135
|
+
subject.translate('en', 'key.three', :default => [:"key.two", :"key.one"]).
|
136
|
+
should include('Expected')
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
describe "with a backend using fallbacks" do
|
141
|
+
subject { build_backend }
|
142
|
+
|
143
|
+
before do
|
144
|
+
CopyTunerClient::I18nBackend.class_eval do
|
145
|
+
include I18n::Backend::Fallbacks
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
it "queues missing keys with default" do
|
150
|
+
default = 'default value'
|
151
|
+
|
152
|
+
subject.translate('en', 'test.key', :default => default).should == default
|
153
|
+
|
154
|
+
cache['en.test.key'].should == default
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CopyTunerClient::Poller do
|
4
|
+
POLLING_DELAY = 0.5
|
5
|
+
|
6
|
+
let(:client) { FakeClient.new }
|
7
|
+
let(:cache) { CopyTunerClient::Cache.new(client, :logger => FakeLogger.new) }
|
8
|
+
|
9
|
+
def build_poller(config = {})
|
10
|
+
config[:logger] ||= FakeLogger.new
|
11
|
+
config[:polling_delay] = POLLING_DELAY
|
12
|
+
default_config = CopyTunerClient::Configuration.new.to_hash
|
13
|
+
poller = CopyTunerClient::Poller.new(cache, default_config.update(config))
|
14
|
+
@pollers << poller
|
15
|
+
poller
|
16
|
+
end
|
17
|
+
|
18
|
+
def wait_for_next_sync
|
19
|
+
sleep(POLLING_DELAY * 3)
|
20
|
+
end
|
21
|
+
|
22
|
+
before do
|
23
|
+
@pollers = []
|
24
|
+
end
|
25
|
+
|
26
|
+
after do
|
27
|
+
@pollers.each { |poller| poller.stop }
|
28
|
+
end
|
29
|
+
|
30
|
+
it "it polls after being started" do
|
31
|
+
poller = build_poller
|
32
|
+
poller.start
|
33
|
+
|
34
|
+
client['test.key'] = 'value'
|
35
|
+
wait_for_next_sync
|
36
|
+
|
37
|
+
cache['test.key'].should == 'value'
|
38
|
+
end
|
39
|
+
|
40
|
+
it "it doesn't poll before being started" do
|
41
|
+
poller = build_poller
|
42
|
+
client['test.key'] = 'value'
|
43
|
+
|
44
|
+
wait_for_next_sync
|
45
|
+
|
46
|
+
cache['test.key'].should be_nil
|
47
|
+
end
|
48
|
+
|
49
|
+
it "stops polling when stopped" do
|
50
|
+
poller = build_poller
|
51
|
+
|
52
|
+
poller.start
|
53
|
+
poller.stop
|
54
|
+
|
55
|
+
client['test.key'] = 'value'
|
56
|
+
wait_for_next_sync
|
57
|
+
|
58
|
+
cache['test.key'].should be_nil
|
59
|
+
end
|
60
|
+
|
61
|
+
it "stops polling with an invalid api key" do
|
62
|
+
failure = "server is napping"
|
63
|
+
logger = FakeLogger.new
|
64
|
+
cache.stubs(:download).raises(CopyTunerClient::InvalidApiKey.new(failure))
|
65
|
+
poller = build_poller(:logger => logger)
|
66
|
+
|
67
|
+
cache['upload.key'] = 'upload'
|
68
|
+
poller.start
|
69
|
+
wait_for_next_sync
|
70
|
+
|
71
|
+
logger.should have_entry(:error, failure)
|
72
|
+
|
73
|
+
client['test.key'] = 'test value'
|
74
|
+
wait_for_next_sync
|
75
|
+
|
76
|
+
cache['test.key'].should be_nil
|
77
|
+
end
|
78
|
+
|
79
|
+
it "logs an error if the background thread can't start" do
|
80
|
+
Thread.stubs(:new => nil)
|
81
|
+
logger = FakeLogger.new
|
82
|
+
|
83
|
+
build_poller(:logger => logger).start
|
84
|
+
|
85
|
+
logger.should have_entry(:error, "Couldn't start poller thread")
|
86
|
+
end
|
87
|
+
|
88
|
+
it "flushes the log when polling" do
|
89
|
+
logger = FakeLogger.new
|
90
|
+
logger.stubs(:flush)
|
91
|
+
|
92
|
+
build_poller(:logger => logger).start
|
93
|
+
|
94
|
+
wait_for_next_sync
|
95
|
+
|
96
|
+
logger.should have_received(:flush).at_least_once
|
97
|
+
end
|
98
|
+
|
99
|
+
it "starts from the top-level constant" do
|
100
|
+
poller = build_poller
|
101
|
+
CopyTunerClient.poller = poller
|
102
|
+
poller.stubs(:start)
|
103
|
+
|
104
|
+
CopyTunerClient.start_poller
|
105
|
+
|
106
|
+
poller.should have_received(:start)
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CopyTunerClient::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 { CopyTunerClient::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
|
+
|
26
|
+
it "calls flush for a logger that responds to flush" do
|
27
|
+
output_logger.stubs(:flush)
|
28
|
+
|
29
|
+
subject.flush
|
30
|
+
|
31
|
+
output_logger.should have_received(:flush)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "doesn't call flush for a logger that doesn't respond to flush" do
|
35
|
+
lambda { subject.flush }.should_not raise_error
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CopyTunerClient::ProcessGuard do
|
4
|
+
include DefinesConstants
|
5
|
+
|
6
|
+
before do
|
7
|
+
@original_process_name = $0
|
8
|
+
end
|
9
|
+
|
10
|
+
after do
|
11
|
+
$0 = @original_process_name
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:cache) { stub('cache', :flush => nil) }
|
15
|
+
let(:poller) { stub('poller', :start => nil) }
|
16
|
+
|
17
|
+
def build_process_guard(options = {})
|
18
|
+
options[:logger] ||= FakeLogger.new
|
19
|
+
options[:cache] ||= cache
|
20
|
+
CopyTunerClient::ProcessGuard.new(options[:cache], poller, options)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "starts polling from a worker process" do
|
24
|
+
process_guard = build_process_guard
|
25
|
+
|
26
|
+
process_guard.start
|
27
|
+
|
28
|
+
poller.should have_received(:start)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "registers passenger hooks from the passenger master" do
|
32
|
+
logger = FakeLogger.new
|
33
|
+
passenger = define_constant('PhusionPassenger', FakePassenger.new)
|
34
|
+
passenger.become_master
|
35
|
+
|
36
|
+
process_guard = build_process_guard(:logger => logger)
|
37
|
+
process_guard.start
|
38
|
+
|
39
|
+
logger.should have_entry(:info, "Registered Phusion Passenger fork hook")
|
40
|
+
poller.should have_received(:start).never
|
41
|
+
end
|
42
|
+
|
43
|
+
it "starts polling from a passenger worker" do
|
44
|
+
logger = FakeLogger.new
|
45
|
+
passenger = define_constant('PhusionPassenger', FakePassenger.new)
|
46
|
+
passenger.become_master
|
47
|
+
process_guard = build_process_guard(:logger => logger)
|
48
|
+
|
49
|
+
process_guard.start
|
50
|
+
passenger.spawn
|
51
|
+
|
52
|
+
poller.should have_received(:start)
|
53
|
+
end
|
54
|
+
|
55
|
+
it "registers unicorn hooks from the unicorn master" do
|
56
|
+
logger = FakeLogger.new
|
57
|
+
define_constant('Unicorn', Module.new)
|
58
|
+
http_server = Class.new(FakeUnicornServer)
|
59
|
+
unicorn = define_constant('Unicorn::HttpServer', http_server).new
|
60
|
+
unicorn.become_master
|
61
|
+
|
62
|
+
process_guard = build_process_guard(:logger => logger)
|
63
|
+
process_guard.start
|
64
|
+
|
65
|
+
logger.should have_entry(:info, "Registered Unicorn fork hook")
|
66
|
+
poller.should have_received(:start).never
|
67
|
+
end
|
68
|
+
|
69
|
+
it "starts polling from a unicorn worker" do
|
70
|
+
logger = FakeLogger.new
|
71
|
+
define_constant('Unicorn', Module.new)
|
72
|
+
http_server = Class.new(FakeUnicornServer)
|
73
|
+
unicorn = define_constant('Unicorn::HttpServer', http_server).new
|
74
|
+
unicorn.become_master
|
75
|
+
process_guard = build_process_guard(:logger => logger)
|
76
|
+
|
77
|
+
process_guard.start
|
78
|
+
unicorn.spawn
|
79
|
+
|
80
|
+
poller.should have_received(:start)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "flushes when the process terminates" do
|
84
|
+
cache = WritingCache.new
|
85
|
+
pid = fork do
|
86
|
+
process_guard = build_process_guard(:cache => cache)
|
87
|
+
process_guard.start
|
88
|
+
exit
|
89
|
+
end
|
90
|
+
Process.wait
|
91
|
+
|
92
|
+
cache.should be_written
|
93
|
+
end
|
94
|
+
|
95
|
+
it "flushes after running a resque job" do
|
96
|
+
logger = FakeLogger.new
|
97
|
+
cache = WritingCache.new
|
98
|
+
define_constant('Resque', Module.new)
|
99
|
+
job_class = define_constant('Resque::Job', FakeResqueJob)
|
100
|
+
job = job_class.new
|
101
|
+
process_guard = build_process_guard(:cache => cache, :logger => logger)
|
102
|
+
|
103
|
+
process_guard.start
|
104
|
+
job.fork_and_perform
|
105
|
+
|
106
|
+
cache.should be_written
|
107
|
+
logger.should have_entry(:info, "Registered Resque after_perform hook")
|
108
|
+
end
|
109
|
+
|
110
|
+
it "doesn't fail if only Resque is defined and not Resque::Job" do
|
111
|
+
logger = FakeLogger.new
|
112
|
+
cache = WritingCache.new
|
113
|
+
define_constant('Resque', Module.new)
|
114
|
+
process_guard = build_process_guard(:cache => cache, :logger => logger)
|
115
|
+
|
116
|
+
process_guard.start
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CopyTunerClient::RequestSync do
|
4
|
+
|
5
|
+
let(:cache) { {} }
|
6
|
+
let(:response) { 'response' }
|
7
|
+
let(:env) { 'env' }
|
8
|
+
let(:app) { stub('app', :call => response) }
|
9
|
+
before { cache.stubs(:flush => nil, :download => nil) }
|
10
|
+
subject { CopyTunerClient::RequestSync.new(app, :cache => cache) }
|
11
|
+
|
12
|
+
it "invokes the upstream app" do
|
13
|
+
result = subject.call(env)
|
14
|
+
app.should have_received(:call).with(env)
|
15
|
+
result.should == response
|
16
|
+
end
|
17
|
+
|
18
|
+
it "flushes defaults" do
|
19
|
+
subject.call(env)
|
20
|
+
cache.should have_received(:flush)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "downloads new copy" do
|
24
|
+
subject.call(env)
|
25
|
+
cache.should have_received(:download)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe CopyTunerClient::RequestSync, 'serving assets' do
|
30
|
+
let(:env) do
|
31
|
+
{ "PATH_INFO" => '/assets/choper.png' }
|
32
|
+
end
|
33
|
+
let(:cache) { {} }
|
34
|
+
let(:response) { 'response' }
|
35
|
+
let(:app) { stub('app', :call => response) }
|
36
|
+
before { cache.stubs(:flush => nil, :download => nil) }
|
37
|
+
subject { CopyTunerClient::RequestSync.new(app, :cache => cache) }
|
38
|
+
|
39
|
+
it "does not flush defaults" do
|
40
|
+
subject.call(env)
|
41
|
+
cache.should_not have_received(:flush)
|
42
|
+
end
|
43
|
+
it "does not download new copy" do
|
44
|
+
subject.call(env)
|
45
|
+
cache.should_not have_received(:download)
|
46
|
+
end
|
47
|
+
end
|