copy_tuner_client 1.0.0 → 1.1.0
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.
- checksums.yaml +4 -4
- data/lib/copy_tuner_client/cache.rb +15 -1
- data/lib/copy_tuner_client/client.rb +4 -0
- data/lib/copy_tuner_client/configuration.rb +0 -3
- data/lib/copy_tuner_client/i18n_backend.rb +48 -12
- data/lib/copy_tuner_client/version.rb +1 -1
- data/spec/copy_tuner_client/cache_spec.rb +136 -28
- data/spec/copy_tuner_client/client_spec.rb +54 -20
- data/spec/copy_tuner_client/i18n_backend_spec.rb +235 -31
- data/spec/support/fake_client.rb +2 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 077fca3432ac4660b4b4211b4546d5fe76c0ad8bb77c2c675a95bcf2e24d7090
|
4
|
+
data.tar.gz: 07ca6d48a19d32179a9aecc1b92d4c1550cc489b06c893cc80a25557bfbb3b8e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5806d46e0b1eae66187806615b387b9254c6c667dcf43c38312cda68f8dc1a7865475ea40e01a5166ef4678b7474652f2395e14986b29ae2cf5babc10be9725d
|
7
|
+
data.tar.gz: 6fe660fbe9406975a1aef91b236b3c718201606f003da46ea96b3fc1b437e0e0e50ff1dc86cd7f527c5efcde52dac3a602c0fd15ed7d2e7bdc182669c06eac77
|
@@ -71,7 +71,21 @@ module CopyTunerClient
|
|
71
71
|
# Yaml representation of all blurbs
|
72
72
|
# @return [String] yaml
|
73
73
|
def export
|
74
|
-
|
74
|
+
tree_hash = to_tree_hash
|
75
|
+
tree_hash.present? ? tree_hash.to_yaml : nil
|
76
|
+
end
|
77
|
+
|
78
|
+
# ツリー構造のハッシュを返す(I18nBackend用)
|
79
|
+
# @return [Hash] ツリー構造に変換されたblurbs
|
80
|
+
def to_tree_hash
|
81
|
+
lock { @blurbs.present? ? DottedHash.to_h(@blurbs) : {} }
|
82
|
+
end
|
83
|
+
|
84
|
+
# キャッシュの更新バージョンを返す(ツリーキャッシュの無効化判定用)
|
85
|
+
# ETags を使用してサーバーサイドの更新を検知
|
86
|
+
# @return [String, nil] 現在のETag値
|
87
|
+
def version
|
88
|
+
client.etag
|
75
89
|
end
|
76
90
|
|
77
91
|
# Waits until the first download has finished.
|
@@ -18,6 +18,10 @@ module CopyTunerClient
|
|
18
18
|
|
19
19
|
USER_AGENT = "copy_tuner_client #{CopyTunerClient::VERSION}"
|
20
20
|
|
21
|
+
# ETags を外部から取得可能にする
|
22
|
+
# @return [String, nil] 現在のETag値
|
23
|
+
attr_reader :etag
|
24
|
+
|
21
25
|
# Usually instantiated from {Configuration#apply}. Copies options.
|
22
26
|
# @param options [Hash]
|
23
27
|
# @option options [String] :api_key API key of the project to connect to
|
@@ -113,9 +113,6 @@ module CopyTunerClient
|
|
113
113
|
|
114
114
|
attr_accessor :poller
|
115
115
|
|
116
|
-
# @return [Boolean] To enable inline-translation-mode, set true.
|
117
|
-
attr_accessor :inline_translation
|
118
|
-
|
119
116
|
# @return [Regexp] Regular expression to exclude keys.
|
120
117
|
attr_accessor :exclude_key_regexp
|
121
118
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'i18n'
|
2
2
|
require 'copy_tuner_client/configuration'
|
3
|
+
require 'active_support/core_ext/hash/keys'
|
3
4
|
|
4
5
|
module CopyTunerClient
|
5
6
|
# I18n implementation designed to synchronize with CopyTuner.
|
@@ -18,26 +19,27 @@ module CopyTunerClient
|
|
18
19
|
# @param cache [Cache] must act like a hash, returning and accept blurbs by key.
|
19
20
|
def initialize(cache)
|
20
21
|
@cache = cache
|
21
|
-
|
22
|
-
|
23
|
-
# Translates the given local and key. See the I18n API documentation for details.
|
22
|
+
@tree_cache = nil
|
23
|
+
@cache_version = nil
|
24
|
+
end # Translates the given local and key. See the I18n API documentation for details.
|
24
25
|
#
|
25
26
|
# @return [Object] the translated key (usually a String)
|
26
27
|
def translate(locale, key, options = {})
|
28
|
+
# I18nの標準処理に任せる(内部でlookupが呼ばれる)
|
27
29
|
content = super(locale, key, options)
|
28
|
-
if CopyTunerClient.configuration.inline_translation
|
29
|
-
content = (content.is_a?(Array) ? content : key.to_s)
|
30
|
-
end
|
31
30
|
|
32
|
-
if
|
31
|
+
return content if content.nil? || content.is_a?(Hash)
|
32
|
+
|
33
|
+
# HTML escapeの処理(ツリー構造のHashは除く)
|
34
|
+
if CopyTunerClient.configuration.html_escape
|
35
|
+
content
|
36
|
+
else
|
33
37
|
# Backward compatible
|
34
38
|
content.respond_to?(:html_safe) ? content.html_safe : content
|
35
|
-
else
|
36
|
-
content
|
37
39
|
end
|
38
40
|
end
|
39
41
|
|
40
|
-
# Returns locales
|
42
|
+
# Returns locales available for this CopyTuner project.
|
41
43
|
# @return [Array<String>] available locales
|
42
44
|
def available_locales
|
43
45
|
return @available_locales if defined?(@available_locales)
|
@@ -71,11 +73,45 @@ module CopyTunerClient
|
|
71
73
|
CopyTunerClient::configuration.ignored_key_handler.call(IgnoredKey.new("Ignored key: #{key_without_locale}"))
|
72
74
|
end
|
73
75
|
|
74
|
-
|
75
|
-
|
76
|
+
# NOTE: ハッシュ化した場合に削除されるキーに対応するため、最初に完全一致をチェック(旧クライアントの動作を維持)
|
77
|
+
# 例: `en.test.key` が `en.test.key.conflict` のように別のキーで上書きされている場合の対応
|
78
|
+
exact_match = cache[key_with_locale]
|
79
|
+
if exact_match
|
80
|
+
return exact_match
|
81
|
+
end
|
82
|
+
|
83
|
+
ensure_tree_cache_current
|
84
|
+
tree_result = lookup_in_tree_cache(parts)
|
85
|
+
return tree_result if tree_result
|
86
|
+
|
87
|
+
content = super
|
88
|
+
|
89
|
+
if content.nil?
|
90
|
+
cache[key_with_locale] = nil
|
91
|
+
end
|
92
|
+
|
76
93
|
content
|
77
94
|
end
|
78
95
|
|
96
|
+
def ensure_tree_cache_current
|
97
|
+
current_version = cache.version
|
98
|
+
# ETag が nil の場合(初回ダウンロード前)や変更があった場合のみ更新
|
99
|
+
# 初回は @cache_version が nil なので、必ず更新される
|
100
|
+
if @cache_version != current_version || @tree_cache.nil?
|
101
|
+
@tree_cache = cache.to_tree_hash.deep_symbolize_keys
|
102
|
+
@cache_version = current_version
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def lookup_in_tree_cache(keys)
|
107
|
+
return nil if @tree_cache.nil?
|
108
|
+
|
109
|
+
symbol_keys = keys.map(&:to_sym)
|
110
|
+
result = @tree_cache.dig(*symbol_keys)
|
111
|
+
|
112
|
+
result.is_a?(Hash) ? result : nil
|
113
|
+
end
|
114
|
+
|
79
115
|
def store_item(locale, data, scope = [])
|
80
116
|
if data.respond_to?(:to_hash)
|
81
117
|
data.to_hash.each do |key, value|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe CopyTunerClient::Cache do
|
3
|
+
describe 'CopyTunerClient::Cache' do
|
4
4
|
let(:client) { FakeClient.new }
|
5
5
|
|
6
6
|
def build_cache(ready: false, **config)
|
@@ -12,7 +12,7 @@ describe CopyTunerClient::Cache do
|
|
12
12
|
cache
|
13
13
|
end
|
14
14
|
|
15
|
-
it '
|
15
|
+
it 'ダウンロードしたデータにアクセスできること' do
|
16
16
|
client['en.test.key'] = 'expected'
|
17
17
|
client['en.test.other_key'] = 'expected'
|
18
18
|
|
@@ -24,7 +24,7 @@ describe CopyTunerClient::Cache do
|
|
24
24
|
expect(cache.keys).to match_array(%w[en.test.key en.test.other_key])
|
25
25
|
end
|
26
26
|
|
27
|
-
it '
|
27
|
+
it 'exclude_key_regexpが設定されている場合、該当データを除外すること' do
|
28
28
|
cache = build_cache(exclude_key_regexp: /^en\.test\.other_key$/)
|
29
29
|
cache['en.test.key'] = 'expected'
|
30
30
|
cache['en.test.other_key'] = 'expected'
|
@@ -34,13 +34,13 @@ describe CopyTunerClient::Cache do
|
|
34
34
|
expect(cache.queued.keys).to match_array(%w[en.test.key])
|
35
35
|
end
|
36
36
|
|
37
|
-
it
|
37
|
+
it '変更がない場合はアップロードしないこと' do
|
38
38
|
cache = build_cache
|
39
39
|
cache.flush
|
40
40
|
expect(client).not_to be_uploaded
|
41
41
|
end
|
42
42
|
|
43
|
-
it
|
43
|
+
it '不正なキーはアップロードしないこと' do
|
44
44
|
cache = build_cache
|
45
45
|
cache['ja'] = 'incorrect key'
|
46
46
|
|
@@ -48,7 +48,7 @@ describe CopyTunerClient::Cache do
|
|
48
48
|
expect(client).not_to be_uploaded
|
49
49
|
end
|
50
50
|
|
51
|
-
it '
|
51
|
+
it '変更があればflush時にアップロードすること' do
|
52
52
|
cache = build_cache
|
53
53
|
cache['test.key'] = 'test value'
|
54
54
|
|
@@ -57,7 +57,7 @@ describe CopyTunerClient::Cache do
|
|
57
57
|
expect(client.uploaded).to eq({ 'test.key' => 'test value' })
|
58
58
|
end
|
59
59
|
|
60
|
-
it '
|
60
|
+
it 'nilを代入した場合は空文字でアップロードすること' do
|
61
61
|
cache = build_cache
|
62
62
|
cache['test.key'] = nil
|
63
63
|
|
@@ -66,7 +66,7 @@ describe CopyTunerClient::Cache do
|
|
66
66
|
expect(client.uploaded).to eq({ 'test.key' => '' })
|
67
67
|
end
|
68
68
|
|
69
|
-
it '
|
69
|
+
it 'ロケールフィルタなしでアップロードできること' do
|
70
70
|
cache = build_cache
|
71
71
|
cache['en.test.key'] = 'uploaded en'
|
72
72
|
cache['ja.test.key'] = 'uploaded ja'
|
@@ -76,7 +76,7 @@ describe CopyTunerClient::Cache do
|
|
76
76
|
expect(client.uploaded).to eq({ 'en.test.key' => 'uploaded en', 'ja.test.key' => 'uploaded ja' })
|
77
77
|
end
|
78
78
|
|
79
|
-
it '
|
79
|
+
it 'ロケールフィルタありでアップロードできること' do
|
80
80
|
cache = build_cache(locales: %(en))
|
81
81
|
cache['en.test.key'] = 'uploaded'
|
82
82
|
cache['ja.test.key'] = 'not uploaded'
|
@@ -86,7 +86,7 @@ describe CopyTunerClient::Cache do
|
|
86
86
|
expect(client.uploaded).to eq({ 'en.test.key' => 'uploaded' })
|
87
87
|
end
|
88
88
|
|
89
|
-
it '
|
89
|
+
it 'ダウンロードで値を取得できること' do
|
90
90
|
client['test.key'] = 'test value'
|
91
91
|
cache = build_cache
|
92
92
|
|
@@ -95,7 +95,7 @@ describe CopyTunerClient::Cache do
|
|
95
95
|
expect(cache['test.key']).to eq('test value')
|
96
96
|
end
|
97
97
|
|
98
|
-
it '
|
98
|
+
it 'syncでダウンロードとアップロードが両方行われること' do
|
99
99
|
cache = build_cache
|
100
100
|
client['test.key'] = 'test value'
|
101
101
|
cache['other.key'] = 'other value'
|
@@ -106,7 +106,7 @@ describe CopyTunerClient::Cache do
|
|
106
106
|
expect(cache['test.key']).to eq('test value')
|
107
107
|
end
|
108
108
|
|
109
|
-
it '
|
109
|
+
it '空文字のキーはダウンロード時にnilになること' do
|
110
110
|
client['en.test.key'] = 'test value'
|
111
111
|
client['en.test.empty'] = ''
|
112
112
|
cache = build_cache
|
@@ -120,7 +120,7 @@ describe CopyTunerClient::Cache do
|
|
120
120
|
expect(cache.queued).to be_empty
|
121
121
|
end
|
122
122
|
|
123
|
-
it '
|
123
|
+
it 'ダウンロードしたキーはアップロード対象にならないこと' do
|
124
124
|
client['en.test.key'] = 'test value'
|
125
125
|
cache = build_cache
|
126
126
|
|
@@ -130,7 +130,7 @@ describe CopyTunerClient::Cache do
|
|
130
130
|
expect(cache.queued).to be_empty
|
131
131
|
end
|
132
132
|
|
133
|
-
it '
|
133
|
+
it 'flush時に接続エラーが発生した場合はエラーログを出力すること' do
|
134
134
|
failure = 'server is napping'
|
135
135
|
logger = FakeLogger.new
|
136
136
|
expect(client).to receive(:upload).and_raise(CopyTunerClient::ConnectionError.new(failure))
|
@@ -142,7 +142,7 @@ describe CopyTunerClient::Cache do
|
|
142
142
|
expect(logger).to have_entry(:error, failure)
|
143
143
|
end
|
144
144
|
|
145
|
-
it '
|
145
|
+
it 'download時に接続エラーが発生した場合はエラーログを出力すること' do
|
146
146
|
failure = 'server is napping'
|
147
147
|
logger = FakeLogger.new
|
148
148
|
expect(client).to receive(:download).and_raise(CopyTunerClient::ConnectionError.new(failure))
|
@@ -153,7 +153,7 @@ describe CopyTunerClient::Cache do
|
|
153
153
|
expect(logger).to have_entry(:error, failure)
|
154
154
|
end
|
155
155
|
|
156
|
-
it '
|
156
|
+
it '最初のダウンロードが完了するまでブロックすること' do
|
157
157
|
logger = FakeLogger.new
|
158
158
|
expect(logger).to receive(:flush)
|
159
159
|
client.delay = true
|
@@ -172,7 +172,7 @@ describe CopyTunerClient::Cache do
|
|
172
172
|
expect(t_wait.join(1)).not_to be_nil
|
173
173
|
end
|
174
174
|
|
175
|
-
it
|
175
|
+
it 'ダウンロード前はブロックしないこと' do
|
176
176
|
logger = FakeLogger.new
|
177
177
|
cache = build_cache(logger: logger)
|
178
178
|
|
@@ -188,7 +188,7 @@ describe CopyTunerClient::Cache do
|
|
188
188
|
expect(logger).not_to have_entry(:info, 'Waiting for first download')
|
189
189
|
end
|
190
190
|
|
191
|
-
it
|
191
|
+
it '空文字のコピーは返さないこと' do
|
192
192
|
client['en.test.key'] = ''
|
193
193
|
cache = build_cache
|
194
194
|
|
@@ -197,13 +197,13 @@ describe CopyTunerClient::Cache do
|
|
197
197
|
expect(cache['en.test.key']).to be_nil
|
198
198
|
end
|
199
199
|
|
200
|
-
describe '
|
200
|
+
describe 'ミューテックスがロックされている場合' do
|
201
201
|
RSpec::Matchers.define :finish_after_unlocking do |mutex|
|
202
202
|
match do |thread|
|
203
203
|
sleep(0.1)
|
204
204
|
|
205
205
|
if thread.status === false
|
206
|
-
violated('
|
206
|
+
violated('アンロック前に終了してしまった')
|
207
207
|
else
|
208
208
|
mutex.unlock
|
209
209
|
sleep(0.1)
|
@@ -211,7 +211,7 @@ describe CopyTunerClient::Cache do
|
|
211
211
|
if thread.status === false
|
212
212
|
true
|
213
213
|
else
|
214
|
-
violated('
|
214
|
+
violated('アンロック後もスレッドが終了しない')
|
215
215
|
end
|
216
216
|
end
|
217
217
|
end
|
@@ -234,20 +234,20 @@ describe CopyTunerClient::Cache do
|
|
234
234
|
allow(Mutex).to receive(:new).and_return(mutex)
|
235
235
|
end
|
236
236
|
|
237
|
-
it '
|
237
|
+
it 'スレッド間でキーの読み取りアクセスが同期されること' do
|
238
238
|
expect(Thread.new { cache['test.key'] }).to finish_after_unlocking(mutex)
|
239
239
|
end
|
240
240
|
|
241
|
-
it '
|
241
|
+
it 'スレッド間でキーリストの読み取りアクセスが同期されること' do
|
242
242
|
expect(Thread.new { cache.keys }).to finish_after_unlocking(mutex)
|
243
243
|
end
|
244
244
|
|
245
|
-
it '
|
245
|
+
it 'スレッド間でキーの書き込みアクセスが同期されること' do
|
246
246
|
expect(Thread.new { cache['test.key'] = 'value' }).to finish_after_unlocking(mutex)
|
247
247
|
end
|
248
248
|
end
|
249
249
|
|
250
|
-
it '
|
250
|
+
it 'トップレベルからflushできること' do
|
251
251
|
cache = build_cache
|
252
252
|
CopyTunerClient.configure do |config|
|
253
253
|
config.cache = cache
|
@@ -257,6 +257,114 @@ describe CopyTunerClient::Cache do
|
|
257
257
|
CopyTunerClient.flush
|
258
258
|
end
|
259
259
|
|
260
|
+
describe '#to_tree_hash' do
|
261
|
+
subject { cache.to_tree_hash }
|
262
|
+
|
263
|
+
let(:cache) do
|
264
|
+
cache = build_cache
|
265
|
+
cache.download
|
266
|
+
cache
|
267
|
+
end
|
268
|
+
|
269
|
+
it 'データがない場合は空ハッシュを返すこと' do
|
270
|
+
is_expected.to eq({})
|
271
|
+
end
|
272
|
+
|
273
|
+
context 'フラットなキーの場合' do
|
274
|
+
before do
|
275
|
+
client['ja.views.hoge'] = 'test'
|
276
|
+
client['ja.views.fuga'] = 'test2'
|
277
|
+
client['en.hello'] = 'world'
|
278
|
+
end
|
279
|
+
|
280
|
+
it 'ツリー構造に変換されること' do
|
281
|
+
is_expected.to eq({
|
282
|
+
'ja' => {
|
283
|
+
'views' => {
|
284
|
+
'hoge' => 'test',
|
285
|
+
'fuga' => 'test2'
|
286
|
+
}
|
287
|
+
},
|
288
|
+
'en' => {
|
289
|
+
'hello' => 'world'
|
290
|
+
}
|
291
|
+
})
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
context '複雑なネスト構造の場合' do
|
296
|
+
before do
|
297
|
+
client['ja.views.users.index'] = 'user index'
|
298
|
+
client['ja.views.users.show'] = 'user show'
|
299
|
+
client['ja.views.posts.index'] = 'post index'
|
300
|
+
client['en.common.buttons.save'] = 'Save'
|
301
|
+
end
|
302
|
+
|
303
|
+
it '正しいツリー構造になること' do
|
304
|
+
is_expected.to eq({
|
305
|
+
'ja' => {
|
306
|
+
'views' => {
|
307
|
+
'users' => {
|
308
|
+
'index' => 'user index',
|
309
|
+
'show' => 'user show'
|
310
|
+
},
|
311
|
+
'posts' => {
|
312
|
+
'index' => 'post index'
|
313
|
+
}
|
314
|
+
}
|
315
|
+
},
|
316
|
+
'en' => {
|
317
|
+
'common' => {
|
318
|
+
'buttons' => {
|
319
|
+
'save' => 'Save'
|
320
|
+
}
|
321
|
+
}
|
322
|
+
}
|
323
|
+
})
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
describe '#version' do
|
329
|
+
it 'クライアントのetagを返すこと(効率的なバージョンチェック)' do
|
330
|
+
cache = build_cache
|
331
|
+
client_instance = cache.send(:client)
|
332
|
+
|
333
|
+
# ETag が設定されている場合
|
334
|
+
client_instance.etag = '"abc123"'
|
335
|
+
expect(cache.version).to eq('"abc123"')
|
336
|
+
|
337
|
+
# ETag が変更された場合
|
338
|
+
client_instance.etag = '"def456"'
|
339
|
+
expect(cache.version).to eq('"def456"')
|
340
|
+
end
|
341
|
+
|
342
|
+
it 'etagがnilの場合も正常に動作すること' do
|
343
|
+
cache = build_cache
|
344
|
+
client_instance = cache.send(:client)
|
345
|
+
client_instance.etag = nil
|
346
|
+
|
347
|
+
expect(cache.version).to be_nil
|
348
|
+
end
|
349
|
+
|
350
|
+
it '大量のキャッシュでも高速にバージョン取得できること(keyのhashではなくetagを使うため)' do
|
351
|
+
cache = build_cache
|
352
|
+
|
353
|
+
# 大量のキーを追加
|
354
|
+
1000.times do |i|
|
355
|
+
cache.instance_variable_get(:@blurbs)["ja.category#{i % 10}.item#{i}"] = "value#{i}"
|
356
|
+
end
|
357
|
+
|
358
|
+
# version メソッドが etag を使用しているため高速(keys.hashだと低速)
|
359
|
+
start_time = Time.now
|
360
|
+
1000.times { cache.version }
|
361
|
+
end_time = Time.now
|
362
|
+
|
363
|
+
# 10ms 以下で完了することを確認
|
364
|
+
expect((end_time - start_time) * 1000).to be < 10
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
260
368
|
describe '#export' do
|
261
369
|
subject { cache.export }
|
262
370
|
|
@@ -266,7 +374,7 @@ describe CopyTunerClient::Cache do
|
|
266
374
|
cache
|
267
375
|
end
|
268
376
|
|
269
|
-
it '
|
377
|
+
it 'トップレベル定数から呼び出せること' do
|
270
378
|
CopyTunerClient.configure do |config|
|
271
379
|
config.cache = cache
|
272
380
|
end
|
@@ -274,11 +382,11 @@ describe CopyTunerClient::Cache do
|
|
274
382
|
CopyTunerClient.export
|
275
383
|
end
|
276
384
|
|
277
|
-
it '
|
385
|
+
it 'blurbキーがない場合はyamlを返さないこと' do
|
278
386
|
is_expected.to eq nil
|
279
387
|
end
|
280
388
|
|
281
|
-
context '
|
389
|
+
context '1階層のblurbキーがある場合' do
|
282
390
|
before do
|
283
391
|
client['key'] = 'test value'
|
284
392
|
client['other_key'] = 'other test value'
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe CopyTunerClient do
|
3
|
+
describe 'CopyTunerClient' do
|
4
4
|
let(:download_cache_dir) { Pathname.new(Dir.mktmpdir('copy_tuner_client')) }
|
5
5
|
|
6
6
|
after do
|
@@ -27,7 +27,7 @@ describe CopyTunerClient do
|
|
27
27
|
build_client(config)
|
28
28
|
end
|
29
29
|
|
30
|
-
describe '
|
30
|
+
describe 'コネクションのオープン' do
|
31
31
|
let(:config) { CopyTunerClient::Configuration.new }
|
32
32
|
let(:http) { Net::HTTP.new(config.host, config.port) }
|
33
33
|
|
@@ -35,21 +35,21 @@ describe CopyTunerClient do
|
|
35
35
|
allow(Net::HTTP).to receive(:new).and_return(http)
|
36
36
|
end
|
37
37
|
|
38
|
-
it '
|
38
|
+
it '接続時のタイムアウトが設定されていること' do
|
39
39
|
project = add_project
|
40
40
|
client = build_client(:api_key => project.api_key, :http_open_timeout => 4)
|
41
41
|
client.download { |ignore| }
|
42
42
|
expect(http.open_timeout).to eq(4)
|
43
43
|
end
|
44
44
|
|
45
|
-
it '
|
45
|
+
it '読み込み時のタイムアウトが設定されていること' do
|
46
46
|
project = add_project
|
47
47
|
client = build_client(:api_key => project.api_key, :http_read_timeout => 4)
|
48
48
|
client.download { |ignore| }
|
49
49
|
expect(http.read_timeout).to eq(4)
|
50
50
|
end
|
51
51
|
|
52
|
-
it '
|
52
|
+
it 'secureがtrueの場合はSSL検証付きで接続すること' do
|
53
53
|
project = add_project
|
54
54
|
client = build_client(:api_key => project.api_key, :secure => true)
|
55
55
|
client.download { |ignore| }
|
@@ -57,14 +57,14 @@ describe CopyTunerClient do
|
|
57
57
|
expect(http.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER)
|
58
58
|
end
|
59
59
|
|
60
|
-
it '
|
60
|
+
it 'secureがfalseの場合はSSLを使用しないこと' do
|
61
61
|
project = add_project
|
62
62
|
client = build_client(:api_key => project.api_key, :secure => false)
|
63
63
|
client.download { |ignore| }
|
64
64
|
expect(http.use_ssl?).to eq(false)
|
65
65
|
end
|
66
66
|
|
67
|
-
it '
|
67
|
+
it 'HTTPエラーをConnectionErrorでラップすること' do
|
68
68
|
errors = [
|
69
69
|
Timeout::Error.new,
|
70
70
|
Errno::EINVAL.new,
|
@@ -89,30 +89,30 @@ describe CopyTunerClient do
|
|
89
89
|
end
|
90
90
|
end
|
91
91
|
|
92
|
-
it '
|
92
|
+
it 'ダウンロード時に500エラーが発生した場合はConnectionErrorになること' do
|
93
93
|
client = build_client(:api_key => 'raise_error')
|
94
94
|
expect { client.download { |ignore| } }.
|
95
95
|
to raise_error(CopyTunerClient::ConnectionError)
|
96
96
|
end
|
97
97
|
|
98
|
-
it '
|
98
|
+
it 'アップロード時に500エラーが発生した場合はConnectionErrorになること' do
|
99
99
|
client = build_client(:api_key => 'raise_error')
|
100
100
|
expect { client.upload({}) }.to raise_error(CopyTunerClient::ConnectionError)
|
101
101
|
end
|
102
102
|
|
103
|
-
it '
|
103
|
+
it 'ダウンロード時に404エラーが発生した場合はInvalidApiKeyになること' do
|
104
104
|
client = build_client(:api_key => 'bogus')
|
105
105
|
expect { client.download { |ignore| } }.
|
106
106
|
to raise_error(CopyTunerClient::InvalidApiKey)
|
107
107
|
end
|
108
108
|
|
109
|
-
it '
|
109
|
+
it 'アップロード時に404エラーが発生した場合はInvalidApiKeyになること' do
|
110
110
|
client = build_client(:api_key => 'bogus')
|
111
111
|
expect { client.upload({}) }.to raise_error(CopyTunerClient::InvalidApiKey)
|
112
112
|
end
|
113
113
|
end
|
114
114
|
|
115
|
-
it '
|
115
|
+
it '既存プロジェクトのpublishedなblurbをダウンロードできること' do
|
116
116
|
project = add_project
|
117
117
|
project.update({
|
118
118
|
'draft' => {
|
@@ -135,14 +135,14 @@ describe CopyTunerClient do
|
|
135
135
|
})
|
136
136
|
end
|
137
137
|
|
138
|
-
it '
|
138
|
+
it 'ダウンロードを実行したことをログに出力すること' do
|
139
139
|
logger = FakeLogger.new
|
140
140
|
client = build_client_with_project(:logger => logger)
|
141
141
|
client.download { |ignore| }
|
142
142
|
expect(logger).to have_entry(:info, 'Downloaded translations')
|
143
143
|
end
|
144
144
|
|
145
|
-
it '
|
145
|
+
it '既存プロジェクトのdraftなblurbをダウンロードできること' do
|
146
146
|
project = add_project
|
147
147
|
project.update({
|
148
148
|
'draft' => {
|
@@ -165,7 +165,7 @@ describe CopyTunerClient do
|
|
165
165
|
})
|
166
166
|
end
|
167
167
|
|
168
|
-
it
|
168
|
+
it '304レスポンス時は2回目以降yieldされないこと' do
|
169
169
|
project = add_project
|
170
170
|
project.update('draft' => { 'key.one' => "expected one" })
|
171
171
|
logger = FakeLogger.new
|
@@ -182,7 +182,7 @@ describe CopyTunerClient do
|
|
182
182
|
expect(logger).to have_entry(:info, "No new translations")
|
183
183
|
end
|
184
184
|
|
185
|
-
it
|
185
|
+
it '既存プロジェクトに存在しないblurbはアップロードされること' do
|
186
186
|
project = add_project
|
187
187
|
|
188
188
|
blurbs = {
|
@@ -196,14 +196,14 @@ describe CopyTunerClient do
|
|
196
196
|
expect(project.reload.draft).to eq(blurbs)
|
197
197
|
end
|
198
198
|
|
199
|
-
it
|
199
|
+
it 'アップロードを実行したことをログに出力すること' do
|
200
200
|
logger = FakeLogger.new
|
201
201
|
client = build_client_with_project(:logger => logger)
|
202
202
|
client.upload({})
|
203
203
|
expect(logger).to have_entry(:info, "Uploaded missing translations")
|
204
204
|
end
|
205
205
|
|
206
|
-
it
|
206
|
+
it 'トップレベル定数からdeployできること' do
|
207
207
|
client = build_client
|
208
208
|
allow(client).to receive(:download)
|
209
209
|
CopyTunerClient.configure do |config|
|
@@ -214,7 +214,7 @@ describe CopyTunerClient do
|
|
214
214
|
CopyTunerClient.deploy
|
215
215
|
end
|
216
216
|
|
217
|
-
it
|
217
|
+
it 'deployが実行できること' do
|
218
218
|
project = add_project
|
219
219
|
project.update({
|
220
220
|
'draft' => {
|
@@ -238,7 +238,41 @@ describe CopyTunerClient do
|
|
238
238
|
expect(logger).to have_entry(:info, "Deployed")
|
239
239
|
end
|
240
240
|
|
241
|
-
it
|
241
|
+
it 'deploy時にエラーが発生した場合は例外が発生すること' do
|
242
242
|
expect { build_client.deploy }.to raise_error(CopyTunerClient::InvalidApiKey)
|
243
243
|
end
|
244
|
+
|
245
|
+
describe '#etag' do
|
246
|
+
it 'etagが読み取り可能な属性として公開されていること' do
|
247
|
+
client = build_client
|
248
|
+
expect(client).to respond_to(:etag)
|
249
|
+
end
|
250
|
+
|
251
|
+
it '初期状態ではetagがnilであること' do
|
252
|
+
client = build_client
|
253
|
+
expect(client.etag).to be_nil
|
254
|
+
end
|
255
|
+
|
256
|
+
it 'ダウンロード成功時にetagが更新されること' do
|
257
|
+
project = add_project
|
258
|
+
client = build_client(:api_key => project.api_key)
|
259
|
+
|
260
|
+
# モックでETagを設定
|
261
|
+
response = Net::HTTPSuccess.new('1.1', '200', 'OK')
|
262
|
+
allow(response).to receive(:body).and_return('{}')
|
263
|
+
allow(response).to receive(:[]).with('ETag').and_return('"abc123"')
|
264
|
+
|
265
|
+
http = double('http')
|
266
|
+
allow(Net::HTTP).to receive(:new).and_return(http)
|
267
|
+
allow(http).to receive(:open_timeout=)
|
268
|
+
allow(http).to receive(:read_timeout=)
|
269
|
+
allow(http).to receive(:use_ssl=)
|
270
|
+
allow(http).to receive(:verify_mode=)
|
271
|
+
allow(http).to receive(:ca_file=)
|
272
|
+
allow(http).to receive(:request).and_return(response)
|
273
|
+
|
274
|
+
client.download { |blurbs| }
|
275
|
+
expect(client.etag).to eq('"abc123"')
|
276
|
+
end
|
277
|
+
end
|
244
278
|
end
|
@@ -1,7 +1,35 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe CopyTunerClient::I18nBackend do
|
4
|
-
|
3
|
+
describe 'CopyTunerClient::I18nBackend' do
|
4
|
+
# テスト用のキャッシュクラス:既存のHashインターフェースを維持しつつ新機能をサポート
|
5
|
+
class TestCache < Hash
|
6
|
+
def initialize(initial_etag = 'test-etag-1')
|
7
|
+
super()
|
8
|
+
@test_etag = initial_etag
|
9
|
+
end
|
10
|
+
|
11
|
+
def version
|
12
|
+
@test_etag
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_tree_hash
|
16
|
+
CopyTunerClient::DottedHash.to_h(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
def wait_for_download
|
20
|
+
# テスト用のスタブメソッド
|
21
|
+
end
|
22
|
+
|
23
|
+
def etag=(value)
|
24
|
+
@test_etag = value
|
25
|
+
end
|
26
|
+
|
27
|
+
def etag
|
28
|
+
@test_etag
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
let(:cache) { TestCache.new }
|
5
33
|
|
6
34
|
def build_backend
|
7
35
|
backend = CopyTunerClient::I18nBackend.new(cache)
|
@@ -11,25 +39,24 @@ describe CopyTunerClient::I18nBackend do
|
|
11
39
|
|
12
40
|
before do
|
13
41
|
@default_backend = I18n.backend
|
14
|
-
allow(cache).to receive(:wait_for_download)
|
15
42
|
end
|
16
43
|
|
17
44
|
after { I18n.backend = @default_backend }
|
18
45
|
|
19
46
|
subject { build_backend }
|
20
47
|
|
21
|
-
it
|
48
|
+
it 'ロケールファイルをリロードし、ダウンロード完了まで待機すること' do
|
22
49
|
expect(I18n).to receive(:load_path).and_return([])
|
23
|
-
|
50
|
+
# wait_for_downloadはTestCacheクラス内で呼ばれる
|
24
51
|
subject.reload!
|
25
52
|
subject.translate('en', 'test.key', :default => 'something')
|
26
53
|
end
|
27
54
|
|
28
|
-
it
|
55
|
+
it 'i18nのBaseバックエンドを継承していること' do
|
29
56
|
is_expected.to be_kind_of(I18n::Backend::Base)
|
30
57
|
end
|
31
58
|
|
32
|
-
it
|
59
|
+
it 'キャッシュからキーを検索できること' do
|
33
60
|
value = 'hello'
|
34
61
|
cache['en.prefix.test.key'] = value
|
35
62
|
|
@@ -38,7 +65,7 @@ describe CopyTunerClient::I18nBackend do
|
|
38
65
|
expect(backend.translate('en', 'test.key', :scope => 'prefix')).to eq(value)
|
39
66
|
end
|
40
67
|
|
41
|
-
it
|
68
|
+
it 'ロケールファイルとキャッシュから利用可能なロケールを取得できること' do
|
42
69
|
allow(YAML).to receive(:unsafe_load_file).and_return({ 'es' => { 'key' => 'value' } })
|
43
70
|
allow(I18n).to receive(:load_path).and_return(["test.yml"])
|
44
71
|
|
@@ -48,7 +75,7 @@ describe CopyTunerClient::I18nBackend do
|
|
48
75
|
expect(subject.available_locales).to match_array([:en, :es, :fr])
|
49
76
|
end
|
50
77
|
|
51
|
-
it
|
78
|
+
it 'default付きで未登録キーをキューイングすること' do
|
52
79
|
default = 'default value'
|
53
80
|
|
54
81
|
expect(subject.translate('en', 'test.key', :default => default)).to eq(default)
|
@@ -56,7 +83,7 @@ describe CopyTunerClient::I18nBackend do
|
|
56
83
|
expect(cache['en.test.key']).to eq(default)
|
57
84
|
end
|
58
85
|
|
59
|
-
it
|
86
|
+
it 'defaultが配列(文字列1つ)の場合も未登録キーをキューイングすること' do
|
60
87
|
default = 'default value'
|
61
88
|
|
62
89
|
expect(subject.translate('en', 'test.key', :default => [default])).to eq(default)
|
@@ -64,7 +91,7 @@ describe CopyTunerClient::I18nBackend do
|
|
64
91
|
expect(cache['en.test.key']).to eq(default)
|
65
92
|
end
|
66
93
|
|
67
|
-
it
|
94
|
+
it 'defaultなしで未登録キーをキューイングすること' do
|
68
95
|
expect { subject.translate('en', 'test.key') }.
|
69
96
|
to throw_symbol(:exception)
|
70
97
|
|
@@ -72,7 +99,7 @@ describe CopyTunerClient::I18nBackend do
|
|
72
99
|
expect(cache['en.test.key']).to be_nil
|
73
100
|
end
|
74
101
|
|
75
|
-
it
|
102
|
+
it 'scope付きで未登録キーをキューイングすること' do
|
76
103
|
default = 'default value'
|
77
104
|
|
78
105
|
expect(subject.translate('en', 'key', :default => default, :scope => ['test'])).
|
@@ -81,7 +108,7 @@ describe CopyTunerClient::I18nBackend do
|
|
81
108
|
expect(cache['en.test.key']).to eq(default)
|
82
109
|
end
|
83
110
|
|
84
|
-
it
|
111
|
+
it 'defaultがシンボルの場合は未登録キーをキューイングしないこと' do
|
85
112
|
cache['en.key.one'] = "Expected"
|
86
113
|
|
87
114
|
expect(subject.translate('en', 'key.three', :default => :"key.one")).to eq 'Expected'
|
@@ -92,7 +119,7 @@ describe CopyTunerClient::I18nBackend do
|
|
92
119
|
expect(subject.translate('en', 'key.three', :default => :"key.one")).to eq 'Expected'
|
93
120
|
end
|
94
121
|
|
95
|
-
it
|
122
|
+
it 'defaultが配列(シンボル含む)の場合は未登録キーをキューイングしないこと' do
|
96
123
|
cache['en.key.one'] = "Expected"
|
97
124
|
|
98
125
|
expect(subject.translate('en', 'key.three', :default => [:"key.two", :"key.one"])).to eq 'Expected'
|
@@ -103,7 +130,7 @@ describe CopyTunerClient::I18nBackend do
|
|
103
130
|
expect(subject.translate('en', 'key.three', :default => [:"key.two", :"key.one"])).to eq 'Expected'
|
104
131
|
end
|
105
132
|
|
106
|
-
it
|
133
|
+
it '補間付きで未登録キーをキューイングすること' do
|
107
134
|
default = 'default %{interpolate}'
|
108
135
|
|
109
136
|
expect(subject.translate('en', 'test.key', :default => default, :interpolate => 'interpolated')).to eq 'default interpolated'
|
@@ -111,20 +138,20 @@ describe CopyTunerClient::I18nBackend do
|
|
111
138
|
expect(cache['en.test.key']).to eq 'default %{interpolate}'
|
112
139
|
end
|
113
140
|
|
114
|
-
it
|
141
|
+
it 'html safeを付与しないこと' do
|
115
142
|
cache['en.test.key'] = FakeHtmlSafeString.new("Hello")
|
116
143
|
backend = build_backend
|
117
144
|
expect(backend.translate('en', 'test.key')).to_not be_html_safe
|
118
145
|
end
|
119
146
|
|
120
|
-
it
|
147
|
+
it 'defaultが配列の場合に順に検索できること' do
|
121
148
|
cache['en.key.one'] = "Expected"
|
122
149
|
backend = build_backend
|
123
150
|
expect(backend.translate('en', 'key.three', :default => [:"key.two", :"key.one"])).
|
124
151
|
to eq('Expected')
|
125
152
|
end
|
126
153
|
|
127
|
-
context
|
154
|
+
context 'html_escapeオプションがtrueの場合' do
|
128
155
|
before do
|
129
156
|
CopyTunerClient.configure do |configuration|
|
130
157
|
configuration.html_escape = true
|
@@ -132,71 +159,71 @@ describe CopyTunerClient::I18nBackend do
|
|
132
159
|
end
|
133
160
|
end
|
134
161
|
|
135
|
-
it
|
162
|
+
it 'html safeを付与しないこと' do
|
136
163
|
cache['en.test.key'] = FakeHtmlSafeString.new("Hello")
|
137
164
|
backend = build_backend
|
138
165
|
expect(backend.translate('en', 'test.key')).not_to be_html_safe
|
139
166
|
end
|
140
167
|
end
|
141
168
|
|
142
|
-
context '
|
143
|
-
it '
|
169
|
+
context '非文字列キーの場合' do
|
170
|
+
it 'キャッシュに登録されないこと' do
|
144
171
|
expect { subject.translate('en', {}) }.to throw_symbol(:exception)
|
145
172
|
expect(cache).not_to have_key 'en.{}'
|
146
173
|
end
|
147
174
|
end
|
148
175
|
|
149
|
-
describe
|
176
|
+
describe 'store_translations利用時' do
|
150
177
|
subject { build_backend }
|
151
178
|
|
152
|
-
it
|
179
|
+
it 'store_translationsで登録した値をdefaultとして利用できること' do
|
153
180
|
subject.store_translations('en', 'test' => { 'key' => 'Expected' })
|
154
181
|
expect(subject.translate('en', 'test.key', :default => 'Unexpected')).
|
155
182
|
to include('Expected')
|
156
183
|
expect(cache['en.test.key']).to eq('Expected')
|
157
184
|
end
|
158
185
|
|
159
|
-
it
|
186
|
+
it '補間マーカーを保持したまま保存できること' do
|
160
187
|
subject.store_translations('en', 'test' => { 'key' => '%{interpolate}' })
|
161
188
|
expect(subject.translate('en', 'test.key', :interpolate => 'interpolated')).
|
162
189
|
to include('interpolated')
|
163
190
|
expect(cache['en.test.key']).to eq('%{interpolate}')
|
164
191
|
end
|
165
192
|
|
166
|
-
it
|
193
|
+
it 'store_translationsでキーがなければdefaultを利用すること' do
|
167
194
|
expect(subject.translate('en', 'test.key', :default => 'Expected')).
|
168
195
|
to include('Expected')
|
169
196
|
end
|
170
197
|
|
171
|
-
it
|
198
|
+
it 'キャッシュにキーがあればそちらを優先すること' do
|
172
199
|
subject.store_translations('en', 'test' => { 'key' => 'Unexpected' })
|
173
200
|
cache['en.test.key'] = 'Expected'
|
174
201
|
expect(subject.translate('en', 'test.key', :default => 'default')).
|
175
202
|
to include('Expected')
|
176
203
|
end
|
177
204
|
|
178
|
-
it
|
205
|
+
it 'ネストしたハッシュを保存できること' do
|
179
206
|
nested = { :nested => 'value' }
|
180
207
|
subject.store_translations('en', 'key' => nested)
|
181
208
|
expect(subject.translate('en', 'key', :default => 'Unexpected')).to eq(nested)
|
182
209
|
expect(cache['en.key.nested']).to eq('value')
|
183
210
|
end
|
184
211
|
|
185
|
-
it
|
212
|
+
it '配列はそのまま返しキャッシュしないこと' do
|
186
213
|
array = ['value']
|
187
214
|
subject.store_translations('en', 'key' => array)
|
188
215
|
expect(subject.translate('en', 'key', :default => 'Unexpected')).to eq(array)
|
189
216
|
expect(cache['en.key']).to be_nil
|
190
217
|
end
|
191
218
|
|
192
|
-
it
|
219
|
+
it 'defaultが配列の場合に順に検索できること' do
|
193
220
|
subject.store_translations('en', 'key' => { 'one' => 'Expected' })
|
194
221
|
expect(subject.translate('en', 'key.three', :default => [:"key.two", :"key.one"])).
|
195
222
|
to include('Expected')
|
196
223
|
end
|
197
224
|
end
|
198
225
|
|
199
|
-
describe
|
226
|
+
describe 'Fallbacks利用時' do
|
200
227
|
subject { build_backend }
|
201
228
|
|
202
229
|
before do
|
@@ -205,7 +232,7 @@ describe CopyTunerClient::I18nBackend do
|
|
205
232
|
end
|
206
233
|
end
|
207
234
|
|
208
|
-
it
|
235
|
+
it 'defaultとFallbacks併用時はキャッシュにデフォルト値を入れないこと' do
|
209
236
|
default = 'default value'
|
210
237
|
expect(subject.translate('en', 'test.key', :default => default)).to eq(default)
|
211
238
|
|
@@ -215,4 +242,181 @@ describe CopyTunerClient::I18nBackend do
|
|
215
242
|
expect(cache['en.test.key']).to be_nil
|
216
243
|
end
|
217
244
|
end
|
245
|
+
|
246
|
+
describe 'ツリー構造のlookup' do
|
247
|
+
subject { build_backend }
|
248
|
+
|
249
|
+
context '完全一致が存在する場合' do
|
250
|
+
it 'ツリー構造より完全一致を優先すること' do
|
251
|
+
cache['ja.views.hoge'] = 'exact_value'
|
252
|
+
cache['ja.views.hoge.sub'] = 'sub_value'
|
253
|
+
|
254
|
+
result = subject.translate('ja', 'views.hoge')
|
255
|
+
expect(result).to eq('exact_value')
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
context 'ツリー構造のみ存在する場合' do
|
260
|
+
it '部分キーでツリー構造を返すこと' do
|
261
|
+
cache['ja.views.hoge'] = 'test'
|
262
|
+
cache['ja.views.fuga'] = 'test2'
|
263
|
+
cache['ja.other.key'] = 'other'
|
264
|
+
|
265
|
+
result = subject.translate('ja', 'views')
|
266
|
+
expect(result).to eq({
|
267
|
+
:hoge => 'test',
|
268
|
+
:fuga => 'test2'
|
269
|
+
})
|
270
|
+
end
|
271
|
+
|
272
|
+
it '存在しない部分キーはnilを返すこと' do
|
273
|
+
cache['ja.views.hoge'] = 'test'
|
274
|
+
|
275
|
+
result = subject.translate('ja', 'nonexistent', default: nil)
|
276
|
+
expect(result).to be_nil
|
277
|
+
end
|
278
|
+
|
279
|
+
it 'ネストしたツリー構造を返すこと' do
|
280
|
+
cache['ja.views.users.index'] = 'user index'
|
281
|
+
cache['ja.views.users.show'] = 'user show'
|
282
|
+
cache['ja.views.posts.index'] = 'post index'
|
283
|
+
|
284
|
+
result = subject.translate('ja', 'views')
|
285
|
+
expect(result).to eq({
|
286
|
+
:users => {
|
287
|
+
:index => 'user index',
|
288
|
+
:show => 'user show'
|
289
|
+
},
|
290
|
+
:posts => {
|
291
|
+
:index => 'post index'
|
292
|
+
}
|
293
|
+
})
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
context '混在シナリオ' do
|
298
|
+
before do
|
299
|
+
cache['ja.views.hoge'] = 'exact_hoge'
|
300
|
+
cache['ja.views.hoge.sub'] = 'sub_value'
|
301
|
+
cache['ja.views.fuga.one'] = 'one'
|
302
|
+
cache['ja.views.fuga.two'] = 'two'
|
303
|
+
end
|
304
|
+
|
305
|
+
it '完全一致を正しく扱うこと' do
|
306
|
+
expect(subject.translate('ja', 'views.hoge')).to eq('exact_hoge')
|
307
|
+
end
|
308
|
+
|
309
|
+
it 'ツリー構造を正しく扱うこと' do
|
310
|
+
expect(subject.translate('ja', 'views.fuga')).to eq({
|
311
|
+
:one => 'one',
|
312
|
+
:two => 'two'
|
313
|
+
})
|
314
|
+
end
|
315
|
+
|
316
|
+
it 'より深い完全一致も正しく扱うこと' do
|
317
|
+
expect(subject.translate('ja', 'views.hoge.sub')).to eq('sub_value')
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
context 'ツリーキャッシュ管理' do
|
322
|
+
it '最初のlookupでツリーキャッシュを構築すること' do
|
323
|
+
cache['ja.views.hoge'] = 'test'
|
324
|
+
cache['ja.views.fuga'] = 'test2'
|
325
|
+
|
326
|
+
# 最初のlookupでツリーキャッシュが構築される
|
327
|
+
result = subject.translate('ja', 'views')
|
328
|
+
expect(result).to eq({
|
329
|
+
:hoge => 'test',
|
330
|
+
:fuga => 'test2'
|
331
|
+
})
|
332
|
+
end
|
333
|
+
|
334
|
+
it '2回目以降はツリーキャッシュを再利用すること' do
|
335
|
+
cache['ja.views.hoge'] = 'test'
|
336
|
+
|
337
|
+
# 1回目
|
338
|
+
subject.translate('ja', 'views')
|
339
|
+
|
340
|
+
# ツリーキャッシュの再構築が発生しないことを確認
|
341
|
+
expect(cache).not_to receive(:to_tree_hash)
|
342
|
+
|
343
|
+
# 2回目
|
344
|
+
subject.translate('ja', 'views')
|
345
|
+
end
|
346
|
+
|
347
|
+
it 'キャッシュバージョンが変わった場合はツリーキャッシュを再構築すること' do
|
348
|
+
cache['ja.views.hoge'] = 'test'
|
349
|
+
subject.translate('ja', 'views')
|
350
|
+
|
351
|
+
# ETag(バージョン)を変更してキャッシュを更新
|
352
|
+
cache.etag = '"new_etag"'
|
353
|
+
|
354
|
+
# 新しい値を追加
|
355
|
+
cache['ja.views.new'] = 'new value'
|
356
|
+
result = subject.translate('ja', 'views')
|
357
|
+
expect(result).to include(:new => 'new value')
|
358
|
+
end
|
359
|
+
|
360
|
+
it 'キャッシュバージョンがnilでも正常に動作すること' do
|
361
|
+
cache['ja.views.test'] = 'value'
|
362
|
+
cache.etag = nil
|
363
|
+
|
364
|
+
result = subject.translate('ja', 'views')
|
365
|
+
expect(result).to eq({ :test => 'value' })
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
context '大規模キャッシュ時のパフォーマンス' do
|
370
|
+
it 'etagバージョン管理でツリーキャッシュを効率的に扱うこと' do
|
371
|
+
# 大量のキャッシュエントリを追加
|
372
|
+
1000.times do |i|
|
373
|
+
cache["ja.category#{i % 10}.item#{i}"] = "value#{i}"
|
374
|
+
end
|
375
|
+
|
376
|
+
# 初回のツリーキャッシュ構築
|
377
|
+
subject.translate('ja', 'category1')
|
378
|
+
|
379
|
+
# ETag が変わらない限り、再構築されない
|
380
|
+
expect(cache).not_to receive(:to_tree_hash)
|
381
|
+
|
382
|
+
# 複数回の lookup が高速で実行される
|
383
|
+
start_time = Time.now
|
384
|
+
10.times { subject.translate('ja', 'category2') }
|
385
|
+
end_time = Time.now
|
386
|
+
|
387
|
+
# 10ms 以下で完了することを確認
|
388
|
+
expect((end_time - start_time) * 1000).to be < 10
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
context 'エッジケース' do
|
393
|
+
it '空キャッシュでも正常に動作すること' do
|
394
|
+
result = subject.translate('ja', 'views', default: nil)
|
395
|
+
expect(result).to be_nil
|
396
|
+
end
|
397
|
+
|
398
|
+
it '1階層のキーも正常に扱えること' do
|
399
|
+
cache['ja.simple'] = 'simple value'
|
400
|
+
|
401
|
+
result = subject.translate('ja', 'simple')
|
402
|
+
expect(result).to eq('simple value')
|
403
|
+
end
|
404
|
+
|
405
|
+
it 'ignored_keysの機能がツリーlookupでも維持されること' do
|
406
|
+
# ignored_keys 設定
|
407
|
+
allow(CopyTunerClient.configuration).to receive(:ignored_keys).and_return(['views.secret'])
|
408
|
+
handler = double('ignored_key_handler')
|
409
|
+
allow(CopyTunerClient.configuration).to receive(:ignored_key_handler).and_return(handler)
|
410
|
+
|
411
|
+
cache['ja.views.public'] = 'public'
|
412
|
+
cache['ja.views.secret'] = 'secret'
|
413
|
+
|
414
|
+
# ignored_key_handler が呼ばれることを確認
|
415
|
+
expect(handler).to receive(:call).with(instance_of(CopyTunerClient::IgnoredKey))
|
416
|
+
|
417
|
+
# ignored_keys が動作することを確認
|
418
|
+
subject.translate('ja', 'views.secret')
|
419
|
+
end
|
420
|
+
end
|
421
|
+
end
|
218
422
|
end
|
data/spec/support/fake_client.rb
CHANGED
@@ -7,10 +7,11 @@ class FakeClient
|
|
7
7
|
@mutex = Mutex.new
|
8
8
|
@cond = ConditionVariable.new
|
9
9
|
@go = false
|
10
|
+
@etag = nil
|
10
11
|
end
|
11
12
|
|
12
13
|
attr_reader :uploaded, :uploads, :downloads
|
13
|
-
attr_accessor :delay, :error
|
14
|
+
attr_accessor :delay, :error, :etag
|
14
15
|
|
15
16
|
def []=(key, value)
|
16
17
|
@data[key] = value
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: copy_tuner_client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- SonicGarden
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-07-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: i18n
|