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