copy_tuner_client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +9 -0
  4. data/Appraisals +15 -0
  5. data/Gemfile +3 -0
  6. data/Gemfile.lock +161 -0
  7. data/README.md +4 -0
  8. data/Rakefile +28 -0
  9. data/copy_tuner_client.gemspec +33 -0
  10. data/features/rails.feature +270 -0
  11. data/features/step_definitions/copycopter_server_steps.rb +64 -0
  12. data/features/step_definitions/rails_steps.rb +172 -0
  13. data/features/support/env.rb +11 -0
  14. data/features/support/rails_server.rb +124 -0
  15. data/gemfiles/2.3.gemfile +7 -0
  16. data/gemfiles/2.3.gemfile.lock +105 -0
  17. data/gemfiles/3.0.gemfile +7 -0
  18. data/gemfiles/3.0.gemfile.lock +147 -0
  19. data/gemfiles/3.1.gemfile +11 -0
  20. data/gemfiles/3.1.gemfile.lock +191 -0
  21. data/init.rb +1 -0
  22. data/lib/copy_tuner_client/cache.rb +144 -0
  23. data/lib/copy_tuner_client/client.rb +136 -0
  24. data/lib/copy_tuner_client/configuration.rb +224 -0
  25. data/lib/copy_tuner_client/errors.rb +12 -0
  26. data/lib/copy_tuner_client/i18n_backend.rb +92 -0
  27. data/lib/copy_tuner_client/poller.rb +44 -0
  28. data/lib/copy_tuner_client/prefixed_logger.rb +45 -0
  29. data/lib/copy_tuner_client/process_guard.rb +92 -0
  30. data/lib/copy_tuner_client/rails.rb +21 -0
  31. data/lib/copy_tuner_client/railtie.rb +12 -0
  32. data/lib/copy_tuner_client/request_sync.rb +39 -0
  33. data/lib/copy_tuner_client/version.rb +7 -0
  34. data/lib/copy_tuner_client.rb +75 -0
  35. data/lib/tasks/copy_tuner_client_tasks.rake +20 -0
  36. data/spec/copy_tuner_client/cache_spec.rb +273 -0
  37. data/spec/copy_tuner_client/client_spec.rb +236 -0
  38. data/spec/copy_tuner_client/configuration_spec.rb +305 -0
  39. data/spec/copy_tuner_client/i18n_backend_spec.rb +157 -0
  40. data/spec/copy_tuner_client/poller_spec.rb +108 -0
  41. data/spec/copy_tuner_client/prefixed_logger_spec.rb +37 -0
  42. data/spec/copy_tuner_client/process_guard_spec.rb +118 -0
  43. data/spec/copy_tuner_client/request_sync_spec.rb +47 -0
  44. data/spec/copy_tuner_client_spec.rb +19 -0
  45. data/spec/spec_helper.rb +29 -0
  46. data/spec/support/client_spec_helpers.rb +8 -0
  47. data/spec/support/defines_constants.rb +44 -0
  48. data/spec/support/fake_client.rb +53 -0
  49. data/spec/support/fake_copy_tuner_app.rb +175 -0
  50. data/spec/support/fake_html_safe_string.rb +20 -0
  51. data/spec/support/fake_logger.rb +68 -0
  52. data/spec/support/fake_passenger.rb +27 -0
  53. data/spec/support/fake_resque_job.rb +18 -0
  54. data/spec/support/fake_unicorn.rb +13 -0
  55. data/spec/support/middleware_stack.rb +13 -0
  56. data/spec/support/writing_cache.rb +17 -0
  57. data/tmp/projects.json +1 -0
  58. 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