Reviewed-on: #433 Co-authored-by: miteruzo <miteruzo@naver.com>
このコミットはプルリクエスト #433 でマージされました。
このコミットが含まれているのは:
@@ -0,0 +1,116 @@
|
||||
require 'rails_helper'
|
||||
require 'tempfile'
|
||||
|
||||
RSpec.describe CertifiedTagNameTranslations do
|
||||
let!(:language) { Language.create!(code: 'ain', name: 'ain') }
|
||||
let!(:script) { Script.create!(code: 'Kana', name: 'Kana') }
|
||||
let!(:tag) { create(:tag, primary_name: 'source') }
|
||||
let(:entries) { [{ 'ja' => 'source', 'ain-Kana' => 'レ_ハイタ' }] }
|
||||
let(:document) do
|
||||
{ 'format' => described_class::FORMAT,
|
||||
'source_locale' => 'ja', 'target_locale' => 'ain-Kana', 'entries' => entries }
|
||||
end
|
||||
let(:locale) do
|
||||
Locale.new(code: 'ain-Kana', language_code: 'ain', script_code: 'Kana',
|
||||
region_code: 392, variants: '', name: 'Ainu')
|
||||
end
|
||||
|
||||
around do |example|
|
||||
Tempfile.create(['certified-names', '.json']) do |file|
|
||||
@path = file.path
|
||||
example.run
|
||||
end
|
||||
end
|
||||
|
||||
def write_document
|
||||
File.write(@path, JSON.generate(document))
|
||||
locale.certified_tag_names_path = @path
|
||||
end
|
||||
|
||||
it 'registers certified names before automatic names and records their provenance' do
|
||||
other = create(:tag, primary_name: 'レ_ハイタ')
|
||||
write_document
|
||||
|
||||
locale.save!
|
||||
|
||||
expect(tag.reload.tag_name('ain')).to have_attributes(
|
||||
name: 'レ_ハイタ', primary_flg: true, auto_generated: false)
|
||||
expect(other.reload.tag_name('ain')).to have_attributes(
|
||||
name: "Tag_##{ other.id }", auto_generated: true)
|
||||
version = tag.tag_versions.order(:version_no).last
|
||||
expect(version.created_by_user_id).to be_nil
|
||||
expect(version.names.fetch('ain')).to include(
|
||||
'name' => 'レ_ハイタ', 'auto_generated' => false)
|
||||
end
|
||||
|
||||
it 'skips missing sources without rolling back the locale or matching entries' do
|
||||
entries.unshift({ 'ja' => 'missing', 'ain-Kana' => 'unused' })
|
||||
write_document
|
||||
|
||||
locale.save!
|
||||
|
||||
expect(locale).to be_persisted
|
||||
expect(tag.reload.name('ain')).to eq('レ_ハイタ')
|
||||
expect(TagName.exists?(language_code: 'ain', name: 'unused')).to be(false)
|
||||
end
|
||||
|
||||
it 'matches source names exactly rather than using a case-insensitive collation' do
|
||||
entries.first['ja'] = 'SOURCE'
|
||||
write_document
|
||||
|
||||
expect(described_class.load!(@path, locale:)).to eq({})
|
||||
end
|
||||
|
||||
it 'rejects ambiguous primary matches' do
|
||||
write_document
|
||||
relation = instance_double(ActiveRecord::Relation)
|
||||
allow(TagName).to receive(:where).with(
|
||||
language_code: 'ja', primary_flg: true, name: ['source']).and_return(relation)
|
||||
allow(relation).to receive(:pluck).with(:name, :tag_id)
|
||||
.and_return([['source', tag.id], ['source', tag.id + 1]])
|
||||
|
||||
expect { described_class.load!(@path, locale:) }
|
||||
.to raise_error(described_class::Error, /source.*exactly one tag/)
|
||||
end
|
||||
|
||||
it 'rejects duplicate sources and identifies the offending name' do
|
||||
entries.push({ 'ja' => 'source', 'ain-Kana' => 'another' })
|
||||
write_document
|
||||
|
||||
expect { locale.save! }.to raise_error(described_class::Error, /duplicate source.*source/)
|
||||
expect(Locale.exists?(code: 'ain-Kana')).to be(false)
|
||||
end
|
||||
|
||||
it 'rejects duplicate targets even when a source is missing' do
|
||||
entries.push({ 'ja' => 'missing', 'ain-Kana' => 'レ_ハイタ' })
|
||||
write_document
|
||||
|
||||
expect { locale.save! }.to raise_error(described_class::Error, /duplicate target.*レ_ハイタ/)
|
||||
expect(Locale.exists?(code: 'ain-Kana')).to be(false)
|
||||
end
|
||||
|
||||
it 'rolls back names and histories if a later certified name is invalid' do
|
||||
create(:tag, primary_name: 'second')
|
||||
entries.push({ 'ja' => 'second', 'ain-Kana' => 'nico:invalid' })
|
||||
write_document
|
||||
original_versions = TagVersion.order(:id).map(&:attributes)
|
||||
|
||||
expect { locale.save! }.to raise_error(described_class::Error, /nico:invalid/)
|
||||
|
||||
expect(Locale.exists?(code: 'ain-Kana')).to be(false)
|
||||
expect(TagName.where(language_code: 'ain')).to be_empty
|
||||
expect(TagVersion.order(:id).map(&:attributes)).to eq(original_versions)
|
||||
end
|
||||
|
||||
it 'does not add names or versions on repeated completion' do
|
||||
write_document
|
||||
locale.save!
|
||||
original_names = TagName.order(:id).map(&:attributes)
|
||||
original_versions = TagVersion.order(:id).map(&:attributes)
|
||||
|
||||
TagNameCompletion.for_locale!(locale)
|
||||
|
||||
expect(TagName.order(:id).map(&:attributes)).to eq(original_names)
|
||||
expect(TagVersion.order(:id).map(&:attributes)).to eq(original_versions)
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PostBulkCreator do
|
||||
include_context 'English locale'
|
||||
include_context 'Ainu locale'
|
||||
|
||||
it 'limits workers to two and keeps failures in their request slots' do
|
||||
locale = Locale.nipponese
|
||||
@@ -67,8 +67,7 @@ RSpec.describe PostBulkCreator do
|
||||
expect(results[3].dig(:post, :id)).to eq(4)
|
||||
end
|
||||
|
||||
it 'passes a future English locale to preflight and creation in every worker' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
it 'passes a future Ainu locale to preflight and creation in every worker' do
|
||||
locale
|
||||
actor = instance_double(User, id: 123)
|
||||
allow(User).to receive(:find).with(123).and_return(actor)
|
||||
|
||||
@@ -11,23 +11,22 @@ RSpec.describe PostCreatePlan do
|
||||
end
|
||||
|
||||
context 'with an explicit locale' do
|
||||
include_context 'English locale'
|
||||
include_context 'Ainu locale'
|
||||
|
||||
it 'resolves the matching language identity and its parents' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
english_name = create(:tag_name, name: 'shared_name',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
english = create(:tag, primary_tag_name: english_name, category: :character)
|
||||
ainu_name = create(:tag_name, name: 'shared_name',
|
||||
language_code: 'ain', script_code: 'Kana')
|
||||
ainu = create(:tag, primary_tag_name: ainu_name, category: :character)
|
||||
primary_tag_name_for(ainu, 'ja').update!(name: 'ainu_in_japanese', auto_generated: false)
|
||||
nipponese = create(:tag, primary_name: 'temporary_nipponese', category: :general)
|
||||
# The DB permits this identity; validation has its own contract spec.
|
||||
primary_tag_name_for(nipponese, 'ja').update_columns(name: 'shared_name')
|
||||
english_parent_name = create(:tag_name, name: 'english_parent',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
english_parent = create(:tag, primary_tag_name: english_parent_name,
|
||||
primary_tag_name_for(nipponese, 'ja').update!(name: 'shared_name')
|
||||
ainu_parent_name = create(:tag_name, name: 'ainu_parent',
|
||||
language_code: 'ain', script_code: 'Kana')
|
||||
ainu_parent = create(:tag, primary_tag_name: ainu_parent_name,
|
||||
category: :material)
|
||||
nipponese_parent = create(:tag, primary_name: 'nipponese_parent', category: :general)
|
||||
TagImplication.create!(tag: english, parent_tag: english_parent)
|
||||
TagImplication.create!(tag: ainu, parent_tag: ainu_parent)
|
||||
TagImplication.create!(tag: nipponese, parent_tag: nipponese_parent)
|
||||
|
||||
plan = described_class.new(attributes: { tags: 'shared_name' }).build!(locale)
|
||||
@@ -35,35 +34,34 @@ RSpec.describe PostCreatePlan do
|
||||
expect(plan[:direct_tag_specs]).to eq(
|
||||
[{ name: 'shared_name', category: :character }])
|
||||
expect(plan[:post_tag_specs]).to include(
|
||||
{ name: primary_tag_name_for(english_parent, 'en').name, category: :material })
|
||||
{ name: primary_tag_name_for(ainu_parent, 'ain').name, category: :material })
|
||||
expect(plan[:post_tag_specs].pluck(:name))
|
||||
.not_to include(primary_tag_name_for(nipponese_parent, 'ja').name)
|
||||
end
|
||||
|
||||
it 'resolves aliases within the requested language' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
nipponese = create(:tag, primary_name: 'nipponese_canonical')
|
||||
english_name = create(:tag_name, name: 'english_canonical',
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
english = create(:tag, primary_tag_name: english_name, category: :character)
|
||||
ainu_name = create(:tag_name, name: 'ainu_canonical',
|
||||
language_code: 'ain', script_code: 'Kana')
|
||||
ainu = create(:tag, primary_tag_name: ainu_name, category: :character)
|
||||
nipponese_alias = create(:tag_name, :alias, name: 'shared_alias',
|
||||
tag: nipponese)
|
||||
english_alias = create(:tag_name, :alias, name: 'shared_alias',
|
||||
tag: english,
|
||||
language_code: 'en', script_code: 'Latn')
|
||||
ainu_alias = create(:tag_name, :alias, name: 'shared_alias',
|
||||
tag: ainu,
|
||||
language_code: 'ain', script_code: 'Kana')
|
||||
|
||||
expect(nipponese_alias.reload).to have_attributes(
|
||||
tag_id: nipponese.id,
|
||||
primary_flg: false, language_code: 'ja')
|
||||
expect(english_alias.reload).to have_attributes(
|
||||
tag_id: english.id,
|
||||
primary_flg: false, language_code: 'en')
|
||||
expect(ainu_alias.reload).to have_attributes(
|
||||
tag_id: ainu.id,
|
||||
primary_flg: false, language_code: 'ain')
|
||||
|
||||
plan = described_class.new(attributes: { tags: 'shared_alias' }).build!(locale)
|
||||
|
||||
expect(plan[:direct_tag_specs]).to eq(
|
||||
[{ name: english_name.name, category: :character }])
|
||||
[{ name: ainu_name.name, category: :character }])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -77,11 +77,10 @@ RSpec.describe PostCreator do
|
||||
end
|
||||
|
||||
context 'with an explicit locale' do
|
||||
include_context 'English locale'
|
||||
include_context 'Ainu locale'
|
||||
|
||||
[false, true].each do |planned|
|
||||
it "passes locale to tag creation with planned attributes: #{ planned }" do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
locale
|
||||
attributes = {
|
||||
title: 'Locale propagation',
|
||||
@@ -96,9 +95,10 @@ RSpec.describe PostCreator do
|
||||
post = described_class.new(actor:, attributes:).create!(locale)
|
||||
|
||||
expect(Tag).to have_received(:find_or_create_by_tag_name!)
|
||||
.with(locale, 'locale_character', category: :character).at_least(:once)
|
||||
tag_name = TagName.find_by!(language_code: 'en', name: 'locale_character')
|
||||
expect(tag_name).to have_attributes(script_code: 'Latn', primary_flg: true)
|
||||
.with(locale, 'locale_character', category: :character,
|
||||
created_by_user: actor).at_least(:once)
|
||||
tag_name = TagName.find_by!(language_code: 'ain', name: 'locale_character')
|
||||
expect(tag_name).to have_attributes(script_code: 'Kana', primary_flg: true)
|
||||
expect(post.tags).to include(tag_name.tag)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -141,6 +141,40 @@ RSpec.describe 'Shared resource IDs across MySQL connections', type: :model do
|
||||
result(first)
|
||||
end
|
||||
|
||||
[false, true].each do |inner_rollback|
|
||||
[false, true].each do |outer_rollback|
|
||||
it "keeps savepoint=#{ inner_rollback } lock through outer rollback=#{ outer_rollback }" do
|
||||
connection = ApplicationRecord.connection
|
||||
database = connection.pool.db_config.database
|
||||
lock_name = "btrc:ids:#{ Digest::SHA256.hexdigest(database)[0, 16] }:tags"
|
||||
quoted_lock = connection.quote(lock_name)
|
||||
owner = connection.select_value('SELECT CONNECTION_ID()').to_i
|
||||
|
||||
connection.transaction do
|
||||
connection.transaction(requires_new: true) do
|
||||
create_resource(:external)
|
||||
raise ActiveRecord::Rollback if inner_rollback
|
||||
end
|
||||
|
||||
expect(connection.select_value("SELECT IS_USED_LOCK(#{ quoted_lock })").to_i)
|
||||
.to eq(owner)
|
||||
contender = worker do |other|
|
||||
other.select_value("SELECT GET_LOCK(#{ quoted_lock }, 0)").to_i
|
||||
end
|
||||
expect(result(contender)).to eq(0)
|
||||
|
||||
SharedResourceIdAllocator.lock_space!(ExternalTag, 'tags')
|
||||
raise ActiveRecord::Rollback if outer_rollback
|
||||
end
|
||||
|
||||
expect(connection.select_value("SELECT IS_FREE_LOCK(#{ quoted_lock })").to_i)
|
||||
.to eq(1)
|
||||
expect(ExternalTag.exists?(name: "#{ @prefix }_external"))
|
||||
.to eq(!inner_rollback && !outer_rollback)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'raises on a real lock acquisition failure without inserting a record' do
|
||||
first, (_id, _connection_id, lock_name) = hold_creation(:external, rollback: true)
|
||||
stub_const('SharedResourceIdAllocator::LOCK_TIMEOUT', 0)
|
||||
|
||||
@@ -101,7 +101,7 @@ RSpec.describe SharedResourceIdAllocator, 'lock lifecycle' do
|
||||
let(:record) { build(:external_tag) }
|
||||
let(:connection) { double('connection') }
|
||||
let(:transaction) { double('transaction', open?: true) }
|
||||
let(:callbacks) { { } }
|
||||
let(:callbacks) { [] }
|
||||
let(:sql) { [] }
|
||||
|
||||
before do
|
||||
@@ -128,19 +128,24 @@ RSpec.describe SharedResourceIdAllocator, 'lock lifecycle' do
|
||||
end
|
||||
end
|
||||
allow(ExternalTag).to receive(:connection).and_return(connection)
|
||||
allow(ExternalTag).to receive(:current_transaction).and_return(transaction)
|
||||
allow(connection).to receive(:current_transaction).and_return(transaction)
|
||||
allow(connection).to receive(:add_transaction_record) { callbacks << _1 }
|
||||
allow(connection).to receive(:disconnect!)
|
||||
allow(ExternalTag).to receive(:uncached).and_yield
|
||||
[:after_commit, :after_rollback].each do |event|
|
||||
allow(transaction).to receive(event) { |&callback| callbacks[event] = callback }
|
||||
end
|
||||
end
|
||||
|
||||
[:after_commit, :after_rollback].each do |event|
|
||||
def finish_transaction event
|
||||
allow(transaction).to receive(:open?).and_return(false)
|
||||
callbacks.shift.public_send(event)
|
||||
allow(transaction).to receive(:open?).and_return(true)
|
||||
end
|
||||
|
||||
[:committed!, :rolledback!].each do |event|
|
||||
it "holds the shared lock until #{ event }" do
|
||||
described_class.assign!(record)
|
||||
expect(record.id).to eq(9)
|
||||
expect(sql.grep(/RELEASE_LOCK/)).to be_empty
|
||||
callbacks.fetch(event).call
|
||||
finish_transaction(event)
|
||||
expect(sql.grep(/RELEASE_LOCK/).size).to eq(1)
|
||||
end
|
||||
end
|
||||
@@ -155,7 +160,7 @@ RSpec.describe SharedResourceIdAllocator, 'lock lifecycle' do
|
||||
it 'releases a successfully acquired lock when allocation fails and rolls back' do
|
||||
allow(connection).to receive(:select_all).and_raise('query failed')
|
||||
expect { described_class.assign!(record) }.to raise_error(RuntimeError, 'query failed')
|
||||
callbacks.fetch(:after_rollback).call
|
||||
finish_transaction(:rolledback!)
|
||||
expect(sql.grep(/RELEASE_LOCK/).size).to eq(1)
|
||||
end
|
||||
|
||||
@@ -164,18 +169,17 @@ RSpec.describe SharedResourceIdAllocator, 'lock lifecycle' do
|
||||
described_class.assign!(record)
|
||||
expect(record.id).to eq(50)
|
||||
expect(sql.grep(/FOR UPDATE/)).to be_empty
|
||||
callbacks.fetch(:after_commit).call
|
||||
finish_transaction(:committed!)
|
||||
end
|
||||
|
||||
it 'uses the same named lock within each space and different locks between spaces' do
|
||||
locks = [Tag, ExternalTag, TagDescription, TagGroup, TagDescriptionVersion, TagGroupVersion].map do |model|
|
||||
allow(model).to receive(:connection).and_return(connection)
|
||||
allow(model).to receive(:current_transaction).and_return(transaction)
|
||||
allow(model).to receive(:uncached).and_yield
|
||||
record = model.new(id: 50)
|
||||
described_class.assign!(record)
|
||||
statement = sql.grep(/GET_LOCK/).last
|
||||
callbacks.fetch(:after_commit).call
|
||||
finish_transaction(:committed!)
|
||||
statement
|
||||
end
|
||||
expect(locks[0]).to eq(locks[1])
|
||||
@@ -187,7 +191,21 @@ RSpec.describe SharedResourceIdAllocator, 'lock lifecycle' do
|
||||
it 'reports lock release failures' do
|
||||
described_class.assign!(record)
|
||||
allow(connection).to receive(:select_value).with(/RELEASE_LOCK/).and_return(nil)
|
||||
expect { callbacks.fetch(:after_commit).call }
|
||||
expect { finish_transaction(:committed!) }
|
||||
.to raise_error(ActiveRecord::ActiveRecordError, /lock release failed/)
|
||||
expect(connection).to have_received(:disconnect!)
|
||||
end
|
||||
|
||||
it 'keeps the lock and transfers cleanup when a savepoint rolls back' do
|
||||
described_class.assign!(record)
|
||||
callbacks.shift.rolledback!
|
||||
|
||||
expect(callbacks.size).to eq(1)
|
||||
expect(sql.grep(/RELEASE_LOCK/)).to be_empty
|
||||
described_class.assign!(record)
|
||||
expect(sql.grep(/GET_LOCK/).size).to eq(1)
|
||||
|
||||
finish_transaction(:committed!)
|
||||
expect(sql.grep(/RELEASE_LOCK/).size).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe TagNameCompletion do
|
||||
let(:author) { create(:user, :member) }
|
||||
let!(:language) { Language.create!(code: 'ain', name: 'ain') }
|
||||
let!(:script) { Script.create!(code: 'Kana', name: 'Kana') }
|
||||
let(:locale) do
|
||||
Locale.create!(code: 'ain-Kana', language_code: 'ain', script_code: 'Kana',
|
||||
region_code: 392, variants: '', name: 'ain-Kana')
|
||||
end
|
||||
|
||||
it 'generates a phonetic primary with the required identity and provenance' do
|
||||
tag = create(:tag, primary_name: '漢字あカ')
|
||||
|
||||
locale
|
||||
|
||||
expect(tag.reload.tag_name('ain')).to have_attributes(
|
||||
name: 'カンチ_ア_カ', language_code: 'ain', script_code: 'Kana',
|
||||
primary_flg: true, auto_generated: true, tag_id: tag.id)
|
||||
end
|
||||
|
||||
it 'uses the exact fallback when the input cannot be converted' do
|
||||
tag = create(:tag, primary_name: '😀')
|
||||
|
||||
locale
|
||||
|
||||
expect(tag.reload.name('ain')).to eq("Tag_##{ tag.id }")
|
||||
end
|
||||
|
||||
it 'uses the exact fallback when a generated name collides with a human name' do
|
||||
other = create(:tag, primary_name: 'other')
|
||||
create(:tag_name, tag: other, name: 'AbC', language_code: 'ain', script_code: 'Kana')
|
||||
tag = create(:tag, primary_name: 'AbC')
|
||||
|
||||
locale
|
||||
|
||||
expect(tag.reload.name('ain')).to eq("Tag_##{ tag.id }")
|
||||
expect(other.reload.name('ain')).to eq('AbC')
|
||||
end
|
||||
|
||||
it 'uses the exact fallback for a transcription that fails name validation' do
|
||||
tag = create(:tag, primary_name: 'base')
|
||||
source = TagName.new(name: 'nico:invalid', language_code: 'ja')
|
||||
|
||||
expect(TagName.generate_name(locale, tag, source)).to eq("Tag_##{ tag.id }")
|
||||
end
|
||||
|
||||
it 'preserves human confirmation and is idempotent across repeated completion' do
|
||||
locale
|
||||
tag = Tag.find_or_create_by_tag_name!(Locale.nipponese, 'あいぬ', category: :general)
|
||||
primary = tag.tag_name('ain')
|
||||
primary.update!(name: 'human_confirmed', auto_generated: false)
|
||||
snapshot = tag.tag_names.order(:id).map(&:attributes)
|
||||
|
||||
2.times { described_class.for_tag!(tag) }
|
||||
|
||||
expect(tag.tag_names.order(:id).map(&:attributes)).to eq(snapshot)
|
||||
end
|
||||
|
||||
it 'creates one primary per active language through the production creation path' do
|
||||
locale
|
||||
|
||||
tag = Tag.find_or_create_by_tag_name!(Locale.nipponese, 'アイヌ', category: :general)
|
||||
|
||||
expect(tag.tag_names.where(primary_flg: true).pluck(:language_code, :name))
|
||||
.to contain_exactly(['ja', 'アイヌ'], ['ain', 'アイヌ'])
|
||||
expect(Tag.find_or_create_by_tag_name!(locale, 'アイヌ', category: :general)).to eq(tag)
|
||||
end
|
||||
|
||||
['ja', 'ain'].each do |language|
|
||||
it "records one authored create with all names for a #{ language } origin" do
|
||||
ainu_locale = locale
|
||||
origin = language == 'ja' ? Locale.nipponese : ainu_locale
|
||||
tag = nil
|
||||
|
||||
expect {
|
||||
tag = Tag.find_or_create_by_tag_name!(origin, 'アイヌ', category: :general,
|
||||
created_by_user: author)
|
||||
}.to change(TagVersion, :count).by(1)
|
||||
|
||||
version = tag.tag_versions.sole
|
||||
expect(version).to have_attributes(
|
||||
event_type: 'create', version_no: 1, created_by_user_id: author.id)
|
||||
expect(tag.reload.version_no).to eq(1)
|
||||
expect(version.names).to eq(
|
||||
'ja' => { 'name' => 'アイヌ', 'script_code' => 'Jpan',
|
||||
'auto_generated' => language != 'ja', 'aliases' => [] },
|
||||
'ain' => { 'name' => 'アイヌ', 'script_code' => 'Kana',
|
||||
'auto_generated' => language != 'ain', 'aliases' => [] })
|
||||
|
||||
expect {
|
||||
described_class.for_tag!(tag, created_by_user: author)
|
||||
found = Tag.find_or_create_by_tag_name!(origin, 'アイヌ', category: :general,
|
||||
created_by_user: author)
|
||||
expect(found).to eq(tag)
|
||||
TagVersioning.record_tag_snapshot!(tag, created_by_user: author)
|
||||
}.not_to change(TagVersion, :count)
|
||||
expect(version.reload.created_by_user_id).to eq(author.id)
|
||||
end
|
||||
end
|
||||
|
||||
it 'records the author even when no additional language needs completion' do
|
||||
tag = build(:tag, primary_name: 'single_language_creation')
|
||||
|
||||
described_class.for_tag!(tag, created_by_user: author)
|
||||
|
||||
expect(tag).to be_persisted
|
||||
expect(tag.tag_versions.sole).to have_attributes(
|
||||
event_type: 'create', version_no: 1, created_by_user_id: author.id)
|
||||
expect(tag.tag_versions.sole.names.keys).to eq(['ja'])
|
||||
end
|
||||
|
||||
it 'rolls back a new tag and all names when its create history fails' do
|
||||
locale
|
||||
author
|
||||
before_counts = [Tag.count, TagName.count, TagVersion.count]
|
||||
allow(TagVersioning).to receive(:record!).and_raise('history failed')
|
||||
|
||||
expect {
|
||||
Tag.find_or_create_by_tag_name!(Locale.nipponese, 'atomic_new_history',
|
||||
category: :general, created_by_user: author)
|
||||
}.to raise_error('history failed')
|
||||
|
||||
expect([Tag.count, TagName.count, TagVersion.count]).to eq(before_counts)
|
||||
expect(TagName.where(name: 'atomic_new_history')).to be_empty
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe TagNameTranscription do
|
||||
{
|
||||
'AbCdXYZ' => 'AbCdXYZ',
|
||||
'あいぬ' => 'アイヌ',
|
||||
'アイヌ' => 'アイヌ',
|
||||
'日本語' => 'ニチホンコ',
|
||||
'漢字' => 'カンチ',
|
||||
'漢あカ' => 'カン_ア_カ',
|
||||
'漢字あいうカナ' => 'カンチ_アイウ_カナ',
|
||||
'Ab漢あカZ' => 'Abカン_ア_カZ',
|
||||
'りゃリュリョ' => 'リヤ_リユリヨ',
|
||||
'キャット' => 'キヤット',
|
||||
'コーヒー' => 'コヒ',
|
||||
'コウコオケイケエカア' => 'コウコオケイケエカア',
|
||||
'ガギグゲゴダヂヅデド' => 'カキクケコタチチュテト',
|
||||
'バビブベボパピプペポ' => 'パピプペポパピプペポ',
|
||||
'ザジズゼゾ' => 'チャチチュチェチョ',
|
||||
'ジャジュジョ' => 'チャチュチョ',
|
||||
'アㇰアㇱアッアンアㇺアㇷ゚' => 'アㇰアㇱアッアンアㇺアㇷ゚',
|
||||
'アㇻイㇼウㇽエㇾオㇿ' => 'アㇻイㇼウㇽエㇾオㇿ',
|
||||
'漢Aあ' => 'カンAア' }.each do |source, expected|
|
||||
it "transcribes #{ source } as #{ expected }" do
|
||||
expect(described_class.ainu(source)).to eq(expected)
|
||||
end
|
||||
end
|
||||
|
||||
{ '😀' => '', 'ヴ' => '', 'ティ' => 'テ', 'ㇵ' => '',
|
||||
'魑' => '', '𠮷' => '', 'ー' => '', '々' => '',
|
||||
'ア😀😀イ' => 'ア_イ', '😀アイ😀' => 'アイ',
|
||||
'_😀_' => '__', 'ア_😀_イ' => 'ア___イ', '漢魑字' => 'カン_チ' }.each do |source, expected|
|
||||
it "preserves convertible portions of #{ source }" do
|
||||
expect(described_class.ainu(source)).to eq(expected)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,7 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe TagVersionRecorder do
|
||||
include_context 'English locale'
|
||||
include_context 'Ainu locale'
|
||||
|
||||
let(:user) { create_member_user! }
|
||||
let!(:tag) { create(:tag, primary_name: '日本語名') }
|
||||
@@ -17,11 +17,11 @@ RSpec.describe TagVersionRecorder do
|
||||
{ 'name' => 'alias_z', 'script_code' => 'Jpan' }] }
|
||||
end
|
||||
|
||||
def prepare_english_names!
|
||||
def prepare_ainu_names!
|
||||
locale
|
||||
primary_tag_name_for(tag, 'en').update!(name: 'English name', auto_generated: false)
|
||||
create(:tag_name, :alias, tag:, language_code: 'en', script_code: 'Latn',
|
||||
name: 'English alias')
|
||||
primary_tag_name_for(tag, 'ain').update!(name: 'Ainu name', auto_generated: false)
|
||||
create(:tag_name, :alias, tag:, language_code: 'ain', script_code: 'Kana',
|
||||
name: 'Ainu alias')
|
||||
end
|
||||
|
||||
it 'snapshots the Nipponese primary and sorted aliases in grouped names' do
|
||||
@@ -62,40 +62,37 @@ RSpec.describe TagVersionRecorder do
|
||||
end
|
||||
|
||||
it 'snapshots future languages together without a request locale argument' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
prepare_english_names!
|
||||
prepare_ainu_names!
|
||||
|
||||
version = described_class.record!(tag:, event_type: :create, created_by_user: user)
|
||||
version = described_class.record!(tag:, event_type: :update, created_by_user: user)
|
||||
|
||||
expect(version.names).to eq(
|
||||
'ja' => nipponese_snapshot,
|
||||
'en' => { 'name' => 'English name', 'script_code' => 'Latn',
|
||||
'ain' => { 'name' => 'Ainu name', 'script_code' => 'Kana',
|
||||
'auto_generated' => false,
|
||||
'aliases' => [{ 'name' => 'English alias', 'script_code' => 'Latn' }] })
|
||||
'aliases' => [{ 'name' => 'Ainu alias', 'script_code' => 'Kana' }] })
|
||||
end
|
||||
|
||||
it 'creates a version for an English-only change and preserves prior names' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
prepare_english_names!
|
||||
original = described_class.record!(tag:, event_type: :create, created_by_user: user)
|
||||
primary_tag_name_for(tag, 'en').update!(name: 'Renamed')
|
||||
it 'creates a version for an Ainu-only change and preserves prior names' do
|
||||
prepare_ainu_names!
|
||||
original = described_class.record!(tag:, event_type: :update, created_by_user: user)
|
||||
primary_tag_name_for(tag, 'ain').update!(name: 'Renamed')
|
||||
|
||||
updated = described_class.record!(tag:, event_type: :update, created_by_user: user)
|
||||
|
||||
expect(updated.version_no).to eq(2)
|
||||
expect(original.reload.names.fetch('en')).to include('name' => 'English name')
|
||||
expect(updated.names.fetch('en')).to include('name' => 'Renamed')
|
||||
expect(updated.version_no).to eq(original.version_no + 1)
|
||||
expect(original.reload.names.fetch('ain')).to include('name' => 'Ainu name')
|
||||
expect(updated.names.fetch('ain')).to include('name' => 'Renamed')
|
||||
expect(updated.names.fetch('ja')).to eq(nipponese_snapshot)
|
||||
end
|
||||
|
||||
it 'does not create a version for an unchanged multilingual snapshot' do
|
||||
pending '日本語以外の Locale 有効化は別課題で実装する'
|
||||
prepare_english_names!
|
||||
described_class.record!(tag:, event_type: :create, created_by_user: user)
|
||||
prepare_ainu_names!
|
||||
original = described_class.record!(tag:, event_type: :update, created_by_user: user)
|
||||
|
||||
unchanged = described_class.record!(tag:, event_type: :update, created_by_user: user)
|
||||
|
||||
expect(unchanged.version_no).to eq(1)
|
||||
expect(tag.reload.tag_versions.count).to eq(1)
|
||||
expect(unchanged.version_no).to eq(original.version_no)
|
||||
expect(tag.reload.tag_versions.count).to eq(original.version_no)
|
||||
end
|
||||
end
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする