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