copy_tuner_client 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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,44 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'copy_tuner_client/cache'
|
3
|
+
|
4
|
+
module CopyTunerClient
|
5
|
+
# Starts a background thread that continually resynchronizes with the remote
|
6
|
+
# server using the given {Cache} after a set delay.
|
7
|
+
class Poller
|
8
|
+
# @param options [Hash]
|
9
|
+
# @option options [Logger] :logger where errors should be logged
|
10
|
+
# @option options [Fixnum] :polling_delay how long to wait in between requests
|
11
|
+
def initialize(cache, options)
|
12
|
+
@cache = cache
|
13
|
+
@polling_delay = options[:polling_delay]
|
14
|
+
@logger = options[:logger]
|
15
|
+
@stop = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def start
|
19
|
+
Thread.new { poll } or logger.error("Couldn't start poller thread")
|
20
|
+
end
|
21
|
+
|
22
|
+
def stop
|
23
|
+
@stop = true
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :cache, :logger, :polling_delay
|
29
|
+
|
30
|
+
def poll
|
31
|
+
until @stop
|
32
|
+
cache.sync
|
33
|
+
logger.flush if logger.respond_to?(:flush)
|
34
|
+
delay
|
35
|
+
end
|
36
|
+
rescue InvalidApiKey => error
|
37
|
+
logger.error(error.message)
|
38
|
+
end
|
39
|
+
|
40
|
+
def delay
|
41
|
+
sleep(polling_delay)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module CopyTunerClient
|
2
|
+
class PrefixedLogger
|
3
|
+
attr_reader :prefix, :original_logger
|
4
|
+
|
5
|
+
def initialize(prefix, logger)
|
6
|
+
@prefix = prefix
|
7
|
+
@original_logger = logger
|
8
|
+
end
|
9
|
+
|
10
|
+
def info(message = nil, &block)
|
11
|
+
log(:info, message, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def debug(message = nil, &block)
|
15
|
+
log(:debug, message, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def warn(message = nil, &block)
|
19
|
+
log(:warn, message, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def error(message = nil, &block)
|
23
|
+
log(:error, message, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def fatal(message = nil, &block)
|
27
|
+
log(:fatal, message, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def flush
|
31
|
+
original_logger.flush if original_logger.respond_to?(:flush)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def log(severity, message, &block)
|
37
|
+
prefixed_message = "#{prefix} #{thread_info} #{message}"
|
38
|
+
original_logger.send(severity, prefixed_message, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def thread_info
|
42
|
+
"[P:#{Process.pid}] [T:#{Thread.current.object_id}]"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module CopyTunerClient
|
2
|
+
# Starts the poller from a worker process, or register hooks for a spawner
|
3
|
+
# process (such as in Unicorn or Passenger). Also registers hooks for exiting
|
4
|
+
# processes and completing background jobs. Applications using the client
|
5
|
+
# will not need to interact with this class directly.
|
6
|
+
class ProcessGuard
|
7
|
+
# @param options [Hash]
|
8
|
+
# @option options [Logger] :logger where errors should be logged
|
9
|
+
def initialize(cache, poller, options)
|
10
|
+
@cache = cache
|
11
|
+
@poller = poller
|
12
|
+
@logger = options[:logger]
|
13
|
+
end
|
14
|
+
|
15
|
+
# Starts the poller or registers hooks
|
16
|
+
def start
|
17
|
+
if spawner?
|
18
|
+
register_spawn_hooks
|
19
|
+
else
|
20
|
+
register_exit_hooks
|
21
|
+
register_job_hooks
|
22
|
+
start_polling
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def start_polling
|
29
|
+
@poller.start
|
30
|
+
end
|
31
|
+
|
32
|
+
def spawner?
|
33
|
+
passenger_spawner? || unicorn_spawner?
|
34
|
+
end
|
35
|
+
|
36
|
+
def passenger_spawner?
|
37
|
+
$0.include?("ApplicationSpawner")
|
38
|
+
end
|
39
|
+
|
40
|
+
def unicorn_spawner?
|
41
|
+
$0.include?("unicorn") && !caller.any? { |line| line.include?("worker_loop") }
|
42
|
+
end
|
43
|
+
|
44
|
+
def register_spawn_hooks
|
45
|
+
if defined?(PhusionPassenger)
|
46
|
+
register_passenger_hook
|
47
|
+
elsif defined?(Unicorn::HttpServer)
|
48
|
+
register_unicorn_hook
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def register_passenger_hook
|
53
|
+
@logger.info("Registered Phusion Passenger fork hook")
|
54
|
+
PhusionPassenger.on_event(:starting_worker_process) do |forked|
|
55
|
+
start_polling
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def register_unicorn_hook
|
60
|
+
@logger.info("Registered Unicorn fork hook")
|
61
|
+
poller = @poller
|
62
|
+
Unicorn::HttpServer.class_eval do
|
63
|
+
alias_method :worker_loop_without_copy_tuner, :worker_loop
|
64
|
+
define_method :worker_loop do |worker|
|
65
|
+
poller.start
|
66
|
+
worker_loop_without_copy_tuner(worker)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def register_exit_hooks
|
72
|
+
at_exit do
|
73
|
+
@cache.flush
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def register_job_hooks
|
78
|
+
if defined?(Resque::Job)
|
79
|
+
@logger.info("Registered Resque after_perform hook")
|
80
|
+
cache = @cache
|
81
|
+
Resque::Job.class_eval do
|
82
|
+
alias_method :perform_without_copy_tuner, :perform
|
83
|
+
define_method :perform do
|
84
|
+
job_was_performed = perform_without_copy_tuner
|
85
|
+
cache.flush
|
86
|
+
job_was_performed
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module CopyTunerClient
|
2
|
+
# Responsible for Rails initialization
|
3
|
+
module Rails
|
4
|
+
# Sets up the logger, environment, name, project root, and framework name
|
5
|
+
# for Rails applications. Must be called after framework initialization.
|
6
|
+
def self.initialize
|
7
|
+
CopyTunerClient.configure(false) do |config|
|
8
|
+
config.environment_name = ::Rails.env
|
9
|
+
config.logger = ::Rails.logger
|
10
|
+
config.framework = "Rails: #{::Rails::VERSION::STRING}"
|
11
|
+
config.middleware = ::Rails.configuration.middleware
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
if defined?(Rails::Railtie)
|
18
|
+
require 'copy_tuner_client/railtie'
|
19
|
+
else
|
20
|
+
CopyTunerClient::Rails.initialize
|
21
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module CopyTunerClient
|
2
|
+
# Connects to integration points for Rails 3 applications
|
3
|
+
class Railtie < ::Rails::Railtie
|
4
|
+
initializer :initialize_copy_tuner_rails, :after => :before_initialize do
|
5
|
+
CopyTunerClient::Rails.initialize
|
6
|
+
end
|
7
|
+
|
8
|
+
rake_tasks do
|
9
|
+
load "tasks/copy_tuner_client_tasks.rake"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module CopyTunerClient
|
2
|
+
# Rack middleware that synchronizes with CopyTuner during each request.
|
3
|
+
#
|
4
|
+
# This is injected into the Rails middleware stack in development environments.
|
5
|
+
class RequestSync
|
6
|
+
# @param app [Rack] the upstream app into whose responses to inject the editor
|
7
|
+
# @param options [Hash]
|
8
|
+
# @option options [Cache] :cache agent that should be flushed after each request
|
9
|
+
def initialize(app, options)
|
10
|
+
@app = app
|
11
|
+
@cache = options[:cache]
|
12
|
+
@interval = options[:interval] || 1.minutes
|
13
|
+
@last_synced = Time.now.utc
|
14
|
+
end
|
15
|
+
|
16
|
+
# Invokes the upstream Rack application and flushes the cache after each
|
17
|
+
# request.
|
18
|
+
def call(env)
|
19
|
+
@cache.download unless asset_request?(env) or in_interval?
|
20
|
+
response = @app.call(env)
|
21
|
+
@cache.flush unless asset_request?(env) or in_interval?
|
22
|
+
update_last_synced unless in_interval?
|
23
|
+
response
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def asset_request?(env)
|
28
|
+
env['PATH_INFO'] =~ /^\/assets/
|
29
|
+
end
|
30
|
+
|
31
|
+
def in_interval?
|
32
|
+
@last_synced + @interval > Time.now.utc
|
33
|
+
end
|
34
|
+
|
35
|
+
def update_last_synced
|
36
|
+
@last_synced = Time.now.utc
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'copy_tuner_client/version'
|
2
|
+
require 'copy_tuner_client/configuration'
|
3
|
+
|
4
|
+
# Top-level interface to the CopyTuner client.
|
5
|
+
#
|
6
|
+
# Most applications should only need to use the {.configure}
|
7
|
+
# method, which will setup all the pieces and begin synchronization when
|
8
|
+
# appropriate.
|
9
|
+
module CopyTunerClient
|
10
|
+
class << self
|
11
|
+
# @return [Configuration] current client configuration
|
12
|
+
# Must act like a hash and return sensible values for all CopyTuner
|
13
|
+
# configuration options. Usually set when {.configure} is called.
|
14
|
+
attr_accessor :configuration
|
15
|
+
|
16
|
+
# @return [Poller] instance used to poll for changes.
|
17
|
+
# This is set when {.configure} is called.
|
18
|
+
attr_accessor :poller
|
19
|
+
end
|
20
|
+
|
21
|
+
# Issues a new deploy, marking all draft blurbs as published.
|
22
|
+
# This is called when the copy_tuner:deploy rake task is invoked.
|
23
|
+
def self.deploy
|
24
|
+
client.deploy
|
25
|
+
end
|
26
|
+
|
27
|
+
# Issues a new export, returning yaml representation of blurb cache.
|
28
|
+
# This is called when the copy_tuner:export rake task is invoked.
|
29
|
+
def self.export
|
30
|
+
cache.export
|
31
|
+
end
|
32
|
+
|
33
|
+
# Starts the polling process.
|
34
|
+
def self.start_poller
|
35
|
+
poller.start
|
36
|
+
end
|
37
|
+
|
38
|
+
# Flush queued changed synchronously
|
39
|
+
def self.flush
|
40
|
+
cache.flush
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.cache
|
44
|
+
CopyTunerClient.configuration.cache
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.client
|
48
|
+
CopyTunerClient.configuration.client
|
49
|
+
end
|
50
|
+
|
51
|
+
# Call this method to modify defaults in your initializers.
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
# CopyTunerClient.configure do |config|
|
55
|
+
# config.api_key = '1234567890abcdef'
|
56
|
+
# config.host = 'your-copy-tuner-server.herokuapp.com'
|
57
|
+
# config.secure = true
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# @param apply [Boolean] (internal) whether the configuration should be applied yet.
|
61
|
+
#
|
62
|
+
# @yield [Configuration] the configuration to be modified
|
63
|
+
def self.configure(apply = true)
|
64
|
+
self.configuration ||= Configuration.new
|
65
|
+
yield configuration
|
66
|
+
|
67
|
+
if apply
|
68
|
+
configuration.apply
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
if defined? Rails
|
74
|
+
require 'copy_tuner_client/rails'
|
75
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
namespace :copy_tuner do
|
2
|
+
desc "Notify CopyTuner of a new deploy."
|
3
|
+
task :deploy => :environment do
|
4
|
+
CopyTunerClient.deploy
|
5
|
+
puts "Successfully marked all blurbs as published."
|
6
|
+
end
|
7
|
+
|
8
|
+
desc "Export CopyTuner blurbs to yaml."
|
9
|
+
task :export => :environment do
|
10
|
+
CopyTunerClient.cache.sync
|
11
|
+
|
12
|
+
if yml = CopyTunerClient.export
|
13
|
+
PATH = "config/locales/copy_tuner.yml"
|
14
|
+
File.new("#{Rails.root}/#{PATH}", 'w').write(yml)
|
15
|
+
puts "Successfully exported blurbs to #{PATH}."
|
16
|
+
else
|
17
|
+
puts "No blurbs have been cached."
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,273 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CopyTunerClient::Cache do
|
4
|
+
let(:client) { FakeClient.new }
|
5
|
+
|
6
|
+
def build_cache(config = {})
|
7
|
+
config[:logger] ||= FakeLogger.new
|
8
|
+
default_config = CopyTunerClient::Configuration.new.to_hash
|
9
|
+
CopyTunerClient::Cache.new(client, default_config.update(config))
|
10
|
+
end
|
11
|
+
|
12
|
+
it "provides access to downloaded data" do
|
13
|
+
client['en.test.key'] = 'expected'
|
14
|
+
client['en.test.other_key'] = 'expected'
|
15
|
+
|
16
|
+
cache = build_cache
|
17
|
+
|
18
|
+
cache.download
|
19
|
+
|
20
|
+
cache['en.test.key'].should == 'expected'
|
21
|
+
cache.keys.should =~ %w(en.test.key en.test.other_key)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "doesn't upload without changes" do
|
25
|
+
cache = build_cache
|
26
|
+
cache.flush
|
27
|
+
client.should_not be_uploaded
|
28
|
+
end
|
29
|
+
|
30
|
+
it "uploads changes when flushed" do
|
31
|
+
cache = build_cache
|
32
|
+
cache['test.key'] = 'test value'
|
33
|
+
|
34
|
+
cache.flush
|
35
|
+
|
36
|
+
client.uploaded.should == { 'test.key' => 'test value' }
|
37
|
+
end
|
38
|
+
|
39
|
+
it "downloads changes" do
|
40
|
+
client['test.key'] = 'test value'
|
41
|
+
cache = build_cache
|
42
|
+
|
43
|
+
cache.download
|
44
|
+
|
45
|
+
cache['test.key'].should == 'test value'
|
46
|
+
end
|
47
|
+
|
48
|
+
it "downloads and uploads when synced" do
|
49
|
+
cache = build_cache
|
50
|
+
client['test.key'] = 'test value'
|
51
|
+
cache['other.key'] = 'other value'
|
52
|
+
|
53
|
+
cache.sync
|
54
|
+
|
55
|
+
client.uploaded.should == { 'other.key' => 'other value' }
|
56
|
+
cache['test.key'].should == 'test value'
|
57
|
+
end
|
58
|
+
|
59
|
+
it "handles connection errors when flushing" do
|
60
|
+
failure = "server is napping"
|
61
|
+
logger = FakeLogger.new
|
62
|
+
client.stubs(:upload).raises(CopyTunerClient::ConnectionError.new(failure))
|
63
|
+
cache = build_cache(:logger => logger)
|
64
|
+
cache['upload.key'] = 'upload'
|
65
|
+
|
66
|
+
cache.flush
|
67
|
+
|
68
|
+
logger.should have_entry(:error, failure)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "handles connection errors when downloading" do
|
72
|
+
failure = "server is napping"
|
73
|
+
logger = FakeLogger.new
|
74
|
+
client.stubs(:download).raises(CopyTunerClient::ConnectionError.new(failure))
|
75
|
+
cache = build_cache(:logger => logger)
|
76
|
+
|
77
|
+
cache.download
|
78
|
+
|
79
|
+
logger.should have_entry(:error, failure)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "blocks until the first download is complete" do
|
83
|
+
logger = FakeLogger.new
|
84
|
+
logger.stubs(:flush)
|
85
|
+
client.delay = 0.5
|
86
|
+
cache = build_cache(:logger => logger)
|
87
|
+
|
88
|
+
Thread.new { cache.download }
|
89
|
+
|
90
|
+
finished = false
|
91
|
+
Thread.new do
|
92
|
+
cache.wait_for_download
|
93
|
+
finished = true
|
94
|
+
end
|
95
|
+
|
96
|
+
sleep(1)
|
97
|
+
|
98
|
+
finished.should == true
|
99
|
+
logger.should have_entry(:info, "Waiting for first download")
|
100
|
+
logger.should have_received(:flush)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "doesn't block if the first download fails" do
|
104
|
+
client.delay = 0.5
|
105
|
+
client.error = StandardError.new("Failure")
|
106
|
+
cache = build_cache
|
107
|
+
|
108
|
+
Thread.new { cache.download }
|
109
|
+
|
110
|
+
finished = false
|
111
|
+
Thread.new do
|
112
|
+
cache.wait_for_download
|
113
|
+
finished = true
|
114
|
+
end
|
115
|
+
|
116
|
+
sleep(1)
|
117
|
+
|
118
|
+
expect { cache.download }.to raise_error(StandardError, "Failure")
|
119
|
+
finished.should == true
|
120
|
+
end
|
121
|
+
|
122
|
+
it "doesn't block before downloading" do
|
123
|
+
logger = FakeLogger.new
|
124
|
+
cache = build_cache(:logger => logger)
|
125
|
+
|
126
|
+
finished = false
|
127
|
+
Thread.new do
|
128
|
+
cache.wait_for_download
|
129
|
+
finished = true
|
130
|
+
end
|
131
|
+
|
132
|
+
sleep(1)
|
133
|
+
|
134
|
+
finished.should == true
|
135
|
+
logger.should_not have_entry(:info, "Waiting for first download")
|
136
|
+
end
|
137
|
+
|
138
|
+
it "doesn't return blank copy" do
|
139
|
+
client['en.test.key'] = ''
|
140
|
+
cache = build_cache
|
141
|
+
|
142
|
+
cache.download
|
143
|
+
|
144
|
+
cache['en.test.key'].should be_nil
|
145
|
+
end
|
146
|
+
|
147
|
+
describe "given locked mutex" do
|
148
|
+
RSpec::Matchers.define :finish_after_unlocking do |mutex|
|
149
|
+
match do |thread|
|
150
|
+
sleep(0.1)
|
151
|
+
|
152
|
+
if thread.status === false
|
153
|
+
violated("finished before unlocking")
|
154
|
+
else
|
155
|
+
mutex.unlock
|
156
|
+
sleep(0.1)
|
157
|
+
|
158
|
+
if thread.status === false
|
159
|
+
true
|
160
|
+
else
|
161
|
+
violated("still running after unlocking")
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def violated(failure)
|
167
|
+
@failure_message = failure
|
168
|
+
false
|
169
|
+
end
|
170
|
+
|
171
|
+
failure_message_for_should do
|
172
|
+
@failure_message
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
let(:mutex) { Mutex.new }
|
177
|
+
let(:cache) { build_cache }
|
178
|
+
|
179
|
+
before do
|
180
|
+
mutex.lock
|
181
|
+
Mutex.stubs(:new => mutex)
|
182
|
+
end
|
183
|
+
|
184
|
+
it "synchronizes read access to keys between threads" do
|
185
|
+
Thread.new { cache['test.key'] }.should finish_after_unlocking(mutex)
|
186
|
+
end
|
187
|
+
|
188
|
+
it "synchronizes read access to the key list between threads" do
|
189
|
+
Thread.new { cache.keys }.should finish_after_unlocking(mutex)
|
190
|
+
end
|
191
|
+
|
192
|
+
it "synchronizes write access to keys between threads" do
|
193
|
+
Thread.new { cache['test.key'] = 'value' }.should finish_after_unlocking(mutex)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
it "flushes from the top level" do
|
198
|
+
cache = build_cache
|
199
|
+
CopyTunerClient.configure do |config|
|
200
|
+
config.cache = cache
|
201
|
+
end
|
202
|
+
cache.stubs(:flush)
|
203
|
+
|
204
|
+
CopyTunerClient.flush
|
205
|
+
|
206
|
+
cache.should have_received(:flush)
|
207
|
+
end
|
208
|
+
|
209
|
+
describe "#export" do
|
210
|
+
before do
|
211
|
+
save_blurbs
|
212
|
+
@cache = build_cache
|
213
|
+
@cache.download
|
214
|
+
end
|
215
|
+
|
216
|
+
let(:save_blurbs) {}
|
217
|
+
|
218
|
+
it "can be invoked from the top-level constant" do
|
219
|
+
CopyTunerClient.configure do |config|
|
220
|
+
config.cache = @cache
|
221
|
+
end
|
222
|
+
@cache.stubs(:export)
|
223
|
+
|
224
|
+
CopyTunerClient.export
|
225
|
+
|
226
|
+
@cache.should have_received(:export)
|
227
|
+
end
|
228
|
+
|
229
|
+
it "returns no yaml with no blurb keys" do
|
230
|
+
@cache.export.should == nil
|
231
|
+
end
|
232
|
+
|
233
|
+
context "with single-level blurb keys" do
|
234
|
+
let(:save_blurbs) do
|
235
|
+
client['key'] = 'test value'
|
236
|
+
client['other_key'] = 'other test value'
|
237
|
+
end
|
238
|
+
|
239
|
+
it "returns blurbs as yaml" do
|
240
|
+
exported = YAML.load(@cache.export)
|
241
|
+
exported['key'].should == 'test value'
|
242
|
+
exported['other_key'].should == 'other test value'
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
context "with multi-level blurb keys" do
|
247
|
+
let(:save_blurbs) do
|
248
|
+
client['en.test.key'] = 'en test value'
|
249
|
+
client['en.test.other_key'] = 'en other test value'
|
250
|
+
client['fr.test.key'] = 'fr test value'
|
251
|
+
end
|
252
|
+
|
253
|
+
it "returns blurbs as yaml" do
|
254
|
+
exported = YAML.load(@cache.export)
|
255
|
+
exported['en']['test']['key'].should == 'en test value'
|
256
|
+
exported['en']['test']['other_key'].should == 'en other test value'
|
257
|
+
exported['fr']['test']['key'].should == 'fr test value'
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
context "with conflicting blurb keys" do
|
262
|
+
let(:save_blurbs) do
|
263
|
+
client['en.test'] = 'test value'
|
264
|
+
client['en.test.key'] = 'other test value'
|
265
|
+
end
|
266
|
+
|
267
|
+
it "retains the new key" do
|
268
|
+
exported = YAML.load(@cache.export)
|
269
|
+
exported['en']['test']['key'].should == 'other test value'
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|