refactor(i18n): 공급자 식별자를 enum으로 통일한다
This commit is contained in:
+5
-4
@@ -17,6 +17,7 @@ import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||
import kr.co.vividnext.sodalive.i18n.translation.DetectedLanguage
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageDetectionProvider
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderProperties
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
|
||||
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
|
||||
@@ -730,7 +731,7 @@ class ControlledLanguageDetectionCacheService(
|
||||
) : LanguageDetectionCacheService(
|
||||
languageDetectionResultRepository,
|
||||
listOf(object : LanguageDetectionProvider {
|
||||
override val providerName = "papago"
|
||||
override val providerId = LanguageProviderId.PAPAGO
|
||||
override fun detect(query: String): String? = null
|
||||
}),
|
||||
LanguageProviderProperties()
|
||||
@@ -759,15 +760,15 @@ class ControlledLanguageDetectionCacheService(
|
||||
queries.add(query)
|
||||
val concurrentDetection = concurrentDetections[query]
|
||||
if (concurrentDetection != null) {
|
||||
return DetectedLanguage(concurrentDetection.detect(), "papago")
|
||||
return DetectedLanguage(concurrentDetection.detect(), LanguageProviderId.PAPAGO)
|
||||
}
|
||||
val blockingDetection = blockingDetections[query]
|
||||
if (blockingDetection != null) {
|
||||
blockingDetection.started.countDown()
|
||||
check(blockingDetection.release.await(5, TimeUnit.SECONDS))
|
||||
return DetectedLanguage(blockingDetection.detectedLanguage, "papago")
|
||||
return DetectedLanguage(blockingDetection.detectedLanguage, LanguageProviderId.PAPAGO)
|
||||
}
|
||||
return detections[query]?.let { DetectedLanguage(it, "papago") }
|
||||
return detections[query]?.let { DetectedLanguage(it, LanguageProviderId.PAPAGO) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.content
|
||||
|
||||
import kr.co.vividnext.sodalive.i18n.translation.DetectedLanguage
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageDetectionProvider
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderProperties
|
||||
import kr.co.vividnext.sodalive.i18n.translation.SourceTextNormalizer
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -36,7 +37,7 @@ class LanguageDetectionCacheServiceTest {
|
||||
|
||||
val detected = service.detectWithCache("Hello world")
|
||||
|
||||
assertEquals(DetectedLanguage("en", "papago"), detected)
|
||||
assertEquals(DetectedLanguage("en", LanguageProviderId.PAPAGO), detected)
|
||||
assertEquals(0, providerCalls)
|
||||
Mockito.verify(repository).findBySourceHashAndProviderAndNormalizationVersion(
|
||||
sourceHash = sourceHash,
|
||||
@@ -80,7 +81,7 @@ class LanguageDetectionCacheServiceTest {
|
||||
|
||||
val detected = service.detectWithCache("Hello world")
|
||||
|
||||
assertEquals(DetectedLanguage("ko", "papago"), detected)
|
||||
assertEquals(DetectedLanguage("ko", LanguageProviderId.PAPAGO), detected)
|
||||
Mockito.verify(repository).findBySourceHashAndProviderAndNormalizationVersion(
|
||||
sourceHash = sourceHash,
|
||||
provider = "papago",
|
||||
@@ -104,7 +105,7 @@ class LanguageDetectionCacheServiceTest {
|
||||
|
||||
private fun service(repository: LanguageDetectionResultRepository, detect: () -> String?): LanguageDetectionCacheService {
|
||||
val provider = object : LanguageDetectionProvider {
|
||||
override val providerName = "papago"
|
||||
override val providerId = LanguageProviderId.PAPAGO
|
||||
override fun detect(query: String) = detect()
|
||||
}
|
||||
return LanguageDetectionCacheService(repository, listOf(provider), LanguageProviderProperties())
|
||||
|
||||
+28
-24
@@ -3,6 +3,8 @@ package kr.co.vividnext.sodalive.i18n.translation
|
||||
import kr.co.vividnext.sodalive.content.LanguageDetectionCacheService
|
||||
import kr.co.vividnext.sodalive.content.LanguageDetectionResult
|
||||
import kr.co.vividnext.sodalive.content.LanguageDetectionResultRepository
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.AWS
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.PAPAGO
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertSame
|
||||
@@ -21,39 +23,39 @@ class LanguageDetectionProviderTest {
|
||||
fun shouldFallbackAndReturnCanonicalWinnerWithActualProvider() {
|
||||
val failures: List<() -> String?> = listOf({ throw IllegalStateException("sensitive body") }, { null }, { " " })
|
||||
failures.forEach { failure ->
|
||||
val first = RecordingDetectionProvider("first", failure)
|
||||
val second = RecordingDetectionProvider("second") { "EN" }
|
||||
Mockito.`when`(repository.findDetectedLanguageForUpdate(hash, "second", "v1")).thenReturn("ko")
|
||||
val first = RecordingDetectionProvider(PAPAGO, failure)
|
||||
val second = RecordingDetectionProvider(AWS) { "EN" }
|
||||
Mockito.`when`(repository.findDetectedLanguageForUpdate(hash, "aws", "v1")).thenReturn("ko")
|
||||
|
||||
assertEquals(DetectedLanguage("ko", "second"), service(listOf(first, second)).detectWithCache(query))
|
||||
assertEquals(DetectedLanguage("ko", AWS), service(listOf(first, second)).detectWithCache(query))
|
||||
assertEquals(listOf(query), first.queries)
|
||||
assertEquals(listOf(query), second.queries)
|
||||
}
|
||||
Mockito.verify(repository, Mockito.times(3)).insertIfAbsent(hash, "original source", "en", "second", null, "v1")
|
||||
Mockito.verify(repository, Mockito.times(3)).insertIfAbsent(hash, "original source", "en", "aws", null, "v1")
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("보조 공급자의 유효 캐시가 있으면 첫 공급자도 호출하지 않는다")
|
||||
fun shouldCheckAllConfiguredCachesBeforeCallingAnyProvider() {
|
||||
val first = RecordingDetectionProvider("first") { "ko" }
|
||||
val second = RecordingDetectionProvider("second") { "en" }
|
||||
Mockito.`when`(repository.findBySourceHashAndProviderAndNormalizationVersion(hash, "second", "v1"))
|
||||
.thenReturn(LanguageDetectionResult(hash, "sample", "ja", "second", null, "v1"))
|
||||
val first = RecordingDetectionProvider(PAPAGO) { "ko" }
|
||||
val second = RecordingDetectionProvider(AWS) { "en" }
|
||||
Mockito.`when`(repository.findBySourceHashAndProviderAndNormalizationVersion(hash, "aws", "v1"))
|
||||
.thenReturn(LanguageDetectionResult(hash, "sample", "ja", "aws", null, "v1"))
|
||||
|
||||
assertEquals(DetectedLanguage("ja", "second"), service(listOf(first, second)).detectWithCache(query))
|
||||
assertEquals(DetectedLanguage("ja", AWS), service(listOf(first, second)).detectWithCache(query))
|
||||
assertEquals(emptyList<String>(), first.queries + second.queries)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("정상 미지원 언어 감지는 성공이며 후속 공급자를 호출하지 않는다")
|
||||
fun shouldKeepUnsupportedDetectionAsSuccessAndHonorOrder() {
|
||||
val first = RecordingDetectionProvider("first") { "ko" }
|
||||
val second = RecordingDetectionProvider("second") { "FR" }
|
||||
Mockito.`when`(repository.findDetectedLanguageForUpdate(hash, "second", "v1")).thenReturn("fr")
|
||||
val first = RecordingDetectionProvider(PAPAGO) { "ko" }
|
||||
val second = RecordingDetectionProvider(AWS) { "FR" }
|
||||
Mockito.`when`(repository.findDetectedLanguageForUpdate(hash, "aws", "v1")).thenReturn("fr")
|
||||
|
||||
assertEquals(
|
||||
DetectedLanguage("fr", "second"),
|
||||
service(listOf(first, second), listOf("second", "first")).detectWithCache(query)
|
||||
DetectedLanguage("fr", AWS),
|
||||
service(listOf(first, second), listOf(AWS, PAPAGO)).detectWithCache(query)
|
||||
)
|
||||
assertEquals(emptyList<String>(), first.queries)
|
||||
assertEquals(listOf(query), second.queries)
|
||||
@@ -62,8 +64,8 @@ class LanguageDetectionProviderTest {
|
||||
@Test
|
||||
@DisplayName("감지 전체 실패와 공백 입력은 null이고 공백은 저장소와 공급자를 호출하지 않는다")
|
||||
fun shouldReturnNullAfterAllFailuresAndSkipBlankInput() {
|
||||
val first = RecordingDetectionProvider("first") { null }
|
||||
val second = RecordingDetectionProvider("second") { null }
|
||||
val first = RecordingDetectionProvider(PAPAGO) { null }
|
||||
val second = RecordingDetectionProvider(AWS) { null }
|
||||
val service = service(listOf(first, second))
|
||||
assertNull(service.detectWithCache(" \n"))
|
||||
Mockito.verifyNoInteractions(repository)
|
||||
@@ -75,28 +77,30 @@ class LanguageDetectionProviderTest {
|
||||
@Test
|
||||
@DisplayName("감지 저장소 조회와 저장 오류는 다음 공급자로 전환하지 않고 전파한다")
|
||||
fun shouldPropagateRepositoryFailuresWithoutFallback() {
|
||||
val first = RecordingDetectionProvider("first") { "ko" }
|
||||
val second = RecordingDetectionProvider("second") { "en" }
|
||||
val first = RecordingDetectionProvider(PAPAGO) { "ko" }
|
||||
val second = RecordingDetectionProvider(AWS) { "en" }
|
||||
val service = service(listOf(first, second))
|
||||
val failure = IllegalStateException("database unavailable")
|
||||
Mockito.`when`(repository.findBySourceHashAndProviderAndNormalizationVersion(hash, "first", "v1"))
|
||||
Mockito.`when`(repository.findBySourceHashAndProviderAndNormalizationVersion(hash, "papago", "v1"))
|
||||
.thenThrow(failure)
|
||||
assertSame(failure, assertThrows(IllegalStateException::class.java) { service.detectWithCache(query) })
|
||||
assertEquals(emptyList<String>(), first.queries + second.queries)
|
||||
|
||||
Mockito.reset(repository)
|
||||
Mockito.`when`(repository.insertIfAbsent(hash, "original source", "ko", "first", null, "v1")).thenThrow(failure)
|
||||
Mockito.`when`(repository.insertIfAbsent(hash, "original source", "ko", "papago", null, "v1")).thenThrow(failure)
|
||||
assertSame(failure, assertThrows(IllegalStateException::class.java) { service.detectWithCache(query) })
|
||||
assertEquals(listOf(query), first.queries)
|
||||
assertEquals(emptyList<String>(), second.queries)
|
||||
}
|
||||
|
||||
private fun service(providers: List<LanguageDetectionProvider>, order: List<String> = providers.map { it.providerName }) =
|
||||
LanguageDetectionCacheService(repository, providers, LanguageProviderProperties(order, listOf("papago")))
|
||||
private fun service(
|
||||
providers: List<LanguageDetectionProvider>,
|
||||
order: List<LanguageProviderId> = providers.map { it.providerId }
|
||||
) = LanguageDetectionCacheService(repository, providers, LanguageProviderProperties(order, listOf(PAPAGO)))
|
||||
}
|
||||
|
||||
class RecordingDetectionProvider(
|
||||
override val providerName: String,
|
||||
override val providerId: LanguageProviderId,
|
||||
private val response: () -> String?
|
||||
) : LanguageDetectionProvider {
|
||||
val queries = mutableListOf<String>()
|
||||
|
||||
+18
-16
@@ -6,6 +6,8 @@ import kr.co.vividnext.sodalive.content.LanguageDetectionResultRepository
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityRepository
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.AWS
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.PAPAGO
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
@@ -86,16 +88,16 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
|
||||
@DisplayName("보조 공급자의 감지를 H2에 귀속 저장하고 외부 호출 없이 캐시를 재사용한다")
|
||||
fun shouldPersistSecondaryDetectionAndReuseCache() {
|
||||
val source = "provider source ${UUID.randomUUID()}"
|
||||
assertEquals(DetectedLanguage("ko", "secondary"), detector.detectWithCache(source))
|
||||
assertEquals(DetectedLanguage("ko", "secondary"), detector.detectWithCache(source))
|
||||
assertEquals(DetectedLanguage("ko", AWS), detector.detectWithCache(source))
|
||||
assertEquals(DetectedLanguage("ko", AWS), detector.detectWithCache(source))
|
||||
val detection = transaction.execute {
|
||||
detectionRepository.findBySourceHashAndProviderAndNormalizationVersion(
|
||||
SourceTextNormalizer.hash(source),
|
||||
"secondary",
|
||||
"aws",
|
||||
"v1"
|
||||
)
|
||||
}!!
|
||||
assertEquals("secondary", detection.provider)
|
||||
assertEquals("aws", detection.provider)
|
||||
assertEquals("ko", detection.detectedLanguage)
|
||||
assertEquals(1, papagoDetection.queries.size)
|
||||
assertEquals(1, secondaryDetection.queries.size)
|
||||
@@ -113,19 +115,19 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
|
||||
SourceTextNormalizer.hash(source),
|
||||
"ko",
|
||||
"en",
|
||||
"secondary",
|
||||
"aws",
|
||||
"v1"
|
||||
)
|
||||
}
|
||||
assertNotNull(memory)
|
||||
assertEquals("secondary", memory!!.provider)
|
||||
assertEquals("secondary-v1", memory.providerVersion)
|
||||
assertEquals("secondary:0", memory.translatedText)
|
||||
assertEquals("aws", memory!!.provider)
|
||||
assertEquals("AWS-v1", memory.providerVersion)
|
||||
assertEquals("AWS:0", memory.translatedText)
|
||||
assertEquals(
|
||||
TranslationJobStatus.COMPLETED,
|
||||
transaction.execute { jobRepository.findAll().single { it.resourceId == postId }.status }
|
||||
)
|
||||
assertEquals("secondary:0", displayService.findDisplayContents(listOf(postId), "en")[postId]?.content)
|
||||
assertEquals("AWS:0", displayService.findDisplayContents(listOf(postId), "en")[postId]?.content)
|
||||
|
||||
transaction.executeWithoutResult {
|
||||
translations.deleteAll()
|
||||
@@ -137,7 +139,7 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
|
||||
assertEquals(1, papagoTranslation.requests.size)
|
||||
assertEquals(1, secondaryTranslation.requests.size)
|
||||
assertEquals(listOf(source), secondaryTranslation.requests.single().texts)
|
||||
assertEquals("secondary:0", displayService.findDisplayContents(listOf(postId), "en")[postId]?.content)
|
||||
assertEquals("AWS:0", displayService.findDisplayContents(listOf(postId), "en")[postId]?.content)
|
||||
}
|
||||
|
||||
private fun schedule(postId: Long, source: String) {
|
||||
@@ -162,20 +164,20 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
|
||||
class Providers {
|
||||
@Bean
|
||||
fun languageProviderProperties() = LanguageProviderProperties(
|
||||
listOf("papago", "secondary"),
|
||||
listOf("papago", "secondary")
|
||||
listOf(PAPAGO, AWS),
|
||||
listOf(PAPAGO, AWS)
|
||||
)
|
||||
|
||||
@Bean
|
||||
fun papagoDetection() = RecordingDetectionProvider("papago") { throw IllegalStateException("sensitive provider body") }
|
||||
fun papagoDetection() = RecordingDetectionProvider(PAPAGO) { throw IllegalStateException("sensitive provider body") }
|
||||
|
||||
@Bean
|
||||
fun secondaryDetection() = RecordingDetectionProvider("secondary") { "ko" }
|
||||
fun secondaryDetection() = RecordingDetectionProvider(AWS) { "ko" }
|
||||
|
||||
@Bean
|
||||
fun papagoTranslation() = RecordingTranslationProvider("papago") { TranslateResult(emptyList()) }
|
||||
fun papagoTranslation() = RecordingTranslationProvider(PAPAGO) { TranslateResult(emptyList()) }
|
||||
|
||||
@Bean
|
||||
fun secondaryTranslation() = RecordingTranslationProvider("secondary")
|
||||
fun secondaryTranslation() = RecordingTranslationProvider(AWS)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package kr.co.vividnext.sodalive.i18n.translation
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertAll
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.function.Executable
|
||||
|
||||
class LanguageProviderIdTest {
|
||||
@Test
|
||||
@DisplayName("공급자 인터페이스와 성공 결과의 런타임 식별자는 enum이다")
|
||||
fun shouldExposeEnumProviderIdentityOnInterfacesAndResults() {
|
||||
assertAll(
|
||||
*listOf(
|
||||
LanguageDetectionProvider::class.java,
|
||||
TranslationProvider::class.java,
|
||||
DetectedLanguage::class.java,
|
||||
ProviderTranslationResult::class.java
|
||||
).map { contract ->
|
||||
Executable {
|
||||
val getter = contract.methods.firstOrNull { it.name == "getProviderId" }
|
||||
assertEquals(
|
||||
"kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId",
|
||||
getter?.returnType?.name,
|
||||
"${contract.simpleName}.getProviderId must return LanguageProviderId"
|
||||
)
|
||||
assertTrue(getter?.returnType?.isEnum == true)
|
||||
assertFalse(contract.methods.any { it.name == "getProviderName" })
|
||||
}
|
||||
}.toTypedArray()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("공급자의 저장 code는 기존 소문자 값으로 고정한다")
|
||||
fun shouldKeepStablePersistenceCodes() {
|
||||
assertEquals("papago", LanguageProviderId.PAPAGO.code)
|
||||
assertEquals("aws", LanguageProviderId.AWS.code)
|
||||
assertEquals(listOf(LanguageProviderId.PAPAGO, LanguageProviderId.AWS), LanguageProviderId.values().toList())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("production 코드 목록과 실제 Papago adapter는 PAPAGO enum을 사용한다")
|
||||
fun shouldUsePapagoEnumForProductionDefaultsAndAdapters() {
|
||||
assertEquals(listOf(LanguageProviderId.PAPAGO), LanguageProviderOrder.detectionOrder)
|
||||
assertEquals(listOf(LanguageProviderId.PAPAGO), LanguageProviderOrder.translationOrder)
|
||||
assertEquals(listOf(LanguageProviderId.PAPAGO), LanguageProviderProperties().detectionOrder)
|
||||
assertEquals(listOf(LanguageProviderId.PAPAGO), LanguageProviderProperties().translationOrder)
|
||||
assertEquals(LanguageProviderId.PAPAGO, PapagoLanguageDetectionProvider("test-id", "test-secret").providerId)
|
||||
val translator = PapagoTranslationService("test-id", "test-secret")
|
||||
assertEquals(LanguageProviderId.PAPAGO, translator.providerId)
|
||||
assertEquals("nmt-v1", translator.providerVersion)
|
||||
}
|
||||
}
|
||||
+13
-11
@@ -1,5 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.i18n.translation
|
||||
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.AWS
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.PAPAGO
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
@@ -9,33 +11,33 @@ class LanguageProviderPropertiesTest {
|
||||
@Test
|
||||
@DisplayName("기능별 목록을 서로 독립된 설정 순서로 정렬한다")
|
||||
fun shouldOrderCapabilitiesIndependently() {
|
||||
val properties = LanguageProviderProperties(listOf("second", "first"), listOf("first", "second"))
|
||||
val properties = LanguageProviderProperties(listOf(AWS, PAPAGO), listOf(PAPAGO, AWS))
|
||||
assertEquals(
|
||||
listOf("second", "first"),
|
||||
properties.orderedProviders(properties.detectionOrder, listOf("first", "second")) { it }
|
||||
listOf(AWS, PAPAGO),
|
||||
properties.orderedProviders(properties.detectionOrder, listOf(PAPAGO, AWS)) { it }
|
||||
)
|
||||
assertEquals(
|
||||
listOf("first", "second"),
|
||||
properties.orderedProviders(properties.translationOrder, listOf("second", "first")) { it }
|
||||
listOf(PAPAGO, AWS),
|
||||
properties.orderedProviders(properties.translationOrder, listOf(AWS, PAPAGO)) { it }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("빈 순서와 빈 이름과 중복 순서와 미등록 공급자는 초기화에서 거부한다")
|
||||
@DisplayName("빈 순서와 중복 순서와 미등록 AWS 공급자는 초기화에서 거부한다")
|
||||
fun shouldRejectInvalidOrders() {
|
||||
val properties = LanguageProviderProperties()
|
||||
listOf(emptyList(), listOf(""), listOf("first", "first"), listOf("unknown")).forEach { order ->
|
||||
listOf(emptyList(), listOf(PAPAGO, PAPAGO), listOf(AWS)).forEach { order ->
|
||||
assertThrows(IllegalArgumentException::class.java) {
|
||||
properties.orderedProviders(order, listOf("first", "second")) { it }
|
||||
properties.orderedProviders(order, listOf(PAPAGO)) { it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("같은 기능 안에서 중복 등록된 공급자 이름은 초기화에서 거부한다")
|
||||
fun shouldRejectDuplicateRegisteredNamesEvenWhenNotSelected() {
|
||||
@DisplayName("같은 기능 안에서 선택되지 않은 공급자도 중복 등록을 거부한다")
|
||||
fun shouldRejectDuplicateRegisteredIdsEvenWhenNotSelected() {
|
||||
assertThrows(IllegalArgumentException::class.java) {
|
||||
LanguageProviderProperties().orderedProviders(listOf("papago"), listOf("papago", "other", "other")) { it }
|
||||
LanguageProviderProperties().orderedProviders(listOf(PAPAGO), listOf(PAPAGO, AWS, AWS)) { it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+41
-35
@@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.i18n.translation
|
||||
|
||||
import kr.co.vividnext.sodalive.content.LanguageDetectionCacheService
|
||||
import kr.co.vividnext.sodalive.content.LanguageDetectionResultRepository
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.AWS
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.PAPAGO
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
@@ -16,7 +18,8 @@ import org.springframework.core.env.SystemEnvironmentPropertySource
|
||||
import java.util.function.Supplier
|
||||
|
||||
class LanguageProviderSpringConfigurationTest {
|
||||
private val runner = ApplicationContextRunner().withUserConfiguration(Providers::class.java)
|
||||
private val papagoRunner = ApplicationContextRunner().withUserConfiguration(Providers::class.java)
|
||||
private val runner = papagoRunner.withUserConfiguration(AwsProviders::class.java)
|
||||
private val defaultRunner = runner.withUserConfiguration(LanguageProviderProperties::class.java)
|
||||
|
||||
@Test
|
||||
@@ -28,8 +31,8 @@ class LanguageProviderSpringConfigurationTest {
|
||||
).run { context ->
|
||||
assertNull(context.startupFailure)
|
||||
val properties = context.getBean(LanguageProviderProperties::class.java)
|
||||
assertEquals(listOf("papago"), properties.detectionOrder)
|
||||
assertEquals(listOf("papago"), properties.translationOrder)
|
||||
assertEquals(listOf(PAPAGO), properties.detectionOrder)
|
||||
assertEquals(listOf(PAPAGO), properties.translationOrder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +57,8 @@ class LanguageProviderSpringConfigurationTest {
|
||||
assertEquals("secondary,papago", context.environment.getProperty("sodalive.language-providers.detection-order"))
|
||||
assertEquals("secondary,papago", context.environment.getProperty("sodalive.language-providers.translation-order"))
|
||||
val properties = context.getBean(LanguageProviderProperties::class.java)
|
||||
assertEquals(listOf("papago"), properties.detectionOrder)
|
||||
assertEquals(listOf("papago"), properties.translationOrder)
|
||||
assertEquals(listOf(PAPAGO), properties.detectionOrder)
|
||||
assertEquals(listOf(PAPAGO), properties.translationOrder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,39 +66,39 @@ class LanguageProviderSpringConfigurationTest {
|
||||
@DisplayName("Spring 테스트 bean의 감지 목록만 바꾸어도 번역 순서는 유지한다")
|
||||
fun shouldInjectIndependentCapabilityOrders() {
|
||||
listOf(
|
||||
listOf("papago", "secondary") to "papago",
|
||||
listOf("secondary", "papago") to "secondary"
|
||||
listOf(PAPAGO, AWS) to PAPAGO,
|
||||
listOf(AWS, PAPAGO) to AWS
|
||||
).forEach { (order, expected) ->
|
||||
runner.withBean(
|
||||
LanguageProviderProperties::class.java,
|
||||
Supplier { LanguageProviderProperties(order, listOf("papago", "secondary")) }
|
||||
Supplier { LanguageProviderProperties(order, listOf(PAPAGO, AWS)) }
|
||||
).run { context ->
|
||||
assertNull(context.startupFailure)
|
||||
val detector = context.getBean(LanguageDetectionCacheService::class.java)
|
||||
assertEquals(expected, detector.detectWithCache("source")?.providerName)
|
||||
assertEquals(expected, detector.detectWithCache("source")?.providerId)
|
||||
assertEquals(
|
||||
"papago",
|
||||
PAPAGO,
|
||||
context.getBean(TranslationCoordinator::class.java)
|
||||
.translate(TranslateRequest(listOf("source"), "ko", "en")).providerName
|
||||
.translate(TranslateRequest(listOf("source"), "ko", "en")).providerId
|
||||
)
|
||||
val secondary = context.getBean("secondaryDetection", RecordingDetectionProvider::class.java)
|
||||
assertEquals(if (expected == "secondary") 1 else 0, secondary.queries.size)
|
||||
assertEquals(if (expected == AWS) 1 else 0, secondary.queries.size)
|
||||
assertEquals(0, context.getBean("secondaryTranslation", RecordingTranslationProvider::class.java).requests.size)
|
||||
}
|
||||
}
|
||||
runner.withBean(
|
||||
LanguageProviderProperties::class.java,
|
||||
Supplier { LanguageProviderProperties(listOf("papago", "secondary"), listOf("secondary", "papago")) }
|
||||
Supplier { LanguageProviderProperties(listOf(PAPAGO, AWS), listOf(AWS, PAPAGO)) }
|
||||
).run { context ->
|
||||
assertNull(context.startupFailure)
|
||||
assertEquals(
|
||||
"papago",
|
||||
context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerName
|
||||
PAPAGO,
|
||||
context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerId
|
||||
)
|
||||
assertEquals(
|
||||
"secondary",
|
||||
AWS,
|
||||
context.getBean(TranslationCoordinator::class.java)
|
||||
.translate(TranslateRequest(listOf("source"), "ko", "en")).providerName
|
||||
.translate(TranslateRequest(listOf("source"), "ko", "en")).providerId
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -106,29 +109,29 @@ class LanguageProviderSpringConfigurationTest {
|
||||
defaultRunner.run { context ->
|
||||
assertNull(context.startupFailure)
|
||||
val properties = context.getBean(LanguageProviderProperties::class.java)
|
||||
assertEquals(listOf("papago"), properties.detectionOrder)
|
||||
assertEquals(listOf("papago"), properties.translationOrder)
|
||||
assertEquals(listOf(PAPAGO), properties.detectionOrder)
|
||||
assertEquals(listOf(PAPAGO), properties.translationOrder)
|
||||
assertEquals(
|
||||
"papago",
|
||||
context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerName
|
||||
PAPAGO,
|
||||
context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerId
|
||||
)
|
||||
assertEquals(
|
||||
"papago",
|
||||
PAPAGO,
|
||||
context.getBean(TranslationCoordinator::class.java)
|
||||
.translate(TranslateRequest(listOf("source"), "ko", "en")).providerName
|
||||
.translate(TranslateRequest(listOf("source"), "ko", "en")).providerId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Spring 테스트 bean의 빈 목록과 빈 이름과 중복 순서와 미등록 이름은 시작 시 실패한다")
|
||||
@DisplayName("Spring 테스트 bean의 빈 목록과 중복 순서와 해당 기능의 AWS 구현체 부재는 시작 시 실패한다")
|
||||
fun shouldRejectInvalidOrdersAtStartup() {
|
||||
listOf(emptyList(), listOf(""), listOf("papago", "papago"), listOf("unknown")).forEach { order ->
|
||||
listOf(emptyList(), listOf(PAPAGO, PAPAGO), listOf(AWS)).forEach { order ->
|
||||
listOf(
|
||||
LanguageProviderProperties(order, listOf("papago")),
|
||||
LanguageProviderProperties(listOf("papago"), order)
|
||||
LanguageProviderProperties(order, listOf(PAPAGO)),
|
||||
LanguageProviderProperties(listOf(PAPAGO), order)
|
||||
).forEach { properties ->
|
||||
runner.withBean(LanguageProviderProperties::class.java, Supplier { properties }).run { context ->
|
||||
papagoRunner.withBean(LanguageProviderProperties::class.java, Supplier { properties }).run { context ->
|
||||
assertNotNull(context.startupFailure)
|
||||
}
|
||||
}
|
||||
@@ -144,7 +147,7 @@ class LanguageProviderSpringConfigurationTest {
|
||||
@Bean
|
||||
fun detectionRepository(): LanguageDetectionResultRepository {
|
||||
val repository = Mockito.mock(LanguageDetectionResultRepository::class.java)
|
||||
listOf("papago", "secondary").forEach { provider ->
|
||||
listOf("papago", "aws").forEach { provider ->
|
||||
Mockito.`when`(repository.findDetectedLanguageForUpdate(SourceTextNormalizer.hash("source"), provider, "v1"))
|
||||
.thenReturn("ko")
|
||||
}
|
||||
@@ -152,15 +155,18 @@ class LanguageProviderSpringConfigurationTest {
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun papagoDetection() = RecordingDetectionProvider("papago") { "ko" }
|
||||
fun papagoDetection() = RecordingDetectionProvider(PAPAGO) { "ko" }
|
||||
|
||||
@Bean
|
||||
fun secondaryDetection() = RecordingDetectionProvider("secondary") { "ko" }
|
||||
fun papagoTranslation() = RecordingTranslationProvider(PAPAGO)
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
class AwsProviders {
|
||||
@Bean
|
||||
fun secondaryDetection() = RecordingDetectionProvider(AWS) { "ko" }
|
||||
|
||||
@Bean
|
||||
fun papagoTranslation() = RecordingTranslationProvider("papago")
|
||||
|
||||
@Bean
|
||||
fun secondaryTranslation() = RecordingTranslationProvider("secondary")
|
||||
fun secondaryTranslation() = RecordingTranslationProvider(AWS)
|
||||
}
|
||||
}
|
||||
|
||||
+28
-26
@@ -1,5 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.i18n.translation
|
||||
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.AWS
|
||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageProviderId.PAPAGO
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertSame
|
||||
@@ -15,15 +17,15 @@ class TranslationCoordinatorTest {
|
||||
@Test
|
||||
@DisplayName("설정 순서의 첫 성공 공급자와 버전을 반환하고 후속 호출을 멈춘다")
|
||||
fun shouldUseConfiguredOrderAndStopAfterSuccess() {
|
||||
val first = RecordingTranslationProvider("first")
|
||||
val second = RecordingTranslationProvider("second")
|
||||
val coordinator = coordinator(listOf(first, second), listOf("second", "first"))
|
||||
val first = RecordingTranslationProvider(PAPAGO)
|
||||
val second = RecordingTranslationProvider(AWS)
|
||||
val coordinator = coordinator(listOf(first, second), listOf(AWS, PAPAGO))
|
||||
|
||||
val result = coordinator.translate(request)
|
||||
|
||||
assertEquals("second", result.providerName)
|
||||
assertEquals("second-v1", result.providerVersion)
|
||||
assertEquals(listOf("second:0", "second:1"), result.result.translatedText)
|
||||
assertEquals(AWS, result.providerId)
|
||||
assertEquals("AWS-v1", result.providerVersion)
|
||||
assertEquals(listOf("AWS:0", "AWS:1"), result.result.translatedText)
|
||||
assertEquals(listOf(request), second.requests)
|
||||
assertEquals(emptyList<TranslateRequest>(), first.requests)
|
||||
}
|
||||
@@ -37,11 +39,11 @@ class TranslationCoordinatorTest {
|
||||
{ TranslateResult(listOf("translated", " ")) }
|
||||
)
|
||||
failures.forEach { failure ->
|
||||
val first = RecordingTranslationProvider("first", failure)
|
||||
val second = RecordingTranslationProvider("second")
|
||||
val first = RecordingTranslationProvider(PAPAGO, failure)
|
||||
val second = RecordingTranslationProvider(AWS)
|
||||
val coordinator = coordinator(listOf(first, second))
|
||||
|
||||
assertEquals("second", coordinator.translate(request).providerName)
|
||||
assertEquals(AWS, coordinator.translate(request).providerId)
|
||||
assertEquals(listOf(request), first.requests)
|
||||
assertEquals(listOf(request), second.requests)
|
||||
}
|
||||
@@ -50,8 +52,8 @@ class TranslationCoordinatorTest {
|
||||
@Test
|
||||
@DisplayName("전체 공급자 실패는 원문이나 원시 예외 원인 없이 고정 메시지를 반환한다")
|
||||
fun shouldSanitizeAllProviderFailures() {
|
||||
val first = RecordingTranslationProvider("first") { throw IllegalStateException("sensitive response body") }
|
||||
val second = RecordingTranslationProvider("second") { TranslateResult(emptyList()) }
|
||||
val first = RecordingTranslationProvider(PAPAGO) { throw IllegalStateException("sensitive response body") }
|
||||
val second = RecordingTranslationProvider(AWS) { TranslateResult(emptyList()) }
|
||||
|
||||
val error = assertThrows(IllegalStateException::class.java) {
|
||||
coordinator(listOf(first, second)).translate(request)
|
||||
@@ -66,7 +68,7 @@ class TranslationCoordinatorTest {
|
||||
@Test
|
||||
@DisplayName("빈 목록과 공백과 동일 언어와 미지원 언어는 공급자 호출 전에 거부한다")
|
||||
fun shouldRejectInvalidRequestsBeforeCallingProviders() {
|
||||
val provider = RecordingTranslationProvider("first")
|
||||
val provider = RecordingTranslationProvider(PAPAGO)
|
||||
val coordinator = coordinator(listOf(provider))
|
||||
listOf(
|
||||
request.copy(texts = emptyList()),
|
||||
@@ -85,17 +87,17 @@ class TranslationCoordinatorTest {
|
||||
@Test
|
||||
@DisplayName("모든 설정 공급자 캐시를 호출보다 먼저 조회하고 순서상 첫 유효 캐시를 선택한다")
|
||||
fun shouldFindSecondaryCacheAndPreferConfiguredFirstValidCache() {
|
||||
val first = RecordingTranslationProvider("first")
|
||||
val second = RecordingTranslationProvider("second")
|
||||
val first = RecordingTranslationProvider(PAPAGO)
|
||||
val second = RecordingTranslationProvider(AWS)
|
||||
val coordinator = coordinator(listOf(first, second))
|
||||
val secondary = memory("second", "cached secondary")
|
||||
givenMemory("second", secondary)
|
||||
val secondary = memory("aws", "cached secondary")
|
||||
givenMemory("aws", secondary)
|
||||
|
||||
assertSame(secondary, coordinator.findCached("hash", "ko", "en"))
|
||||
givenMemory("first", memory("first", " "))
|
||||
givenMemory("papago", memory("papago", " "))
|
||||
assertSame(secondary, coordinator.findCached("hash", "ko", "en"))
|
||||
val primary = memory("first", "cached primary")
|
||||
givenMemory("first", primary)
|
||||
val primary = memory("papago", "cached primary")
|
||||
givenMemory("papago", primary)
|
||||
assertSame(primary, coordinator.findCached("hash", "ko", "en"))
|
||||
assertEquals(emptyList<TranslateRequest>(), first.requests + second.requests)
|
||||
}
|
||||
@@ -103,14 +105,14 @@ class TranslationCoordinatorTest {
|
||||
@Test
|
||||
@DisplayName("캐시 저장소 오류는 공급자 실패로 숨기지 않는다")
|
||||
fun shouldPropagateCacheRepositoryFailure() {
|
||||
val provider = RecordingTranslationProvider("first")
|
||||
val provider = RecordingTranslationProvider(PAPAGO)
|
||||
val failure = IllegalStateException("database unavailable")
|
||||
Mockito.`when`(
|
||||
repository.findBySourceHashAndSourceLanguageAndTargetLanguageAndProviderAndNormalizationVersion(
|
||||
"hash",
|
||||
"ko",
|
||||
"en",
|
||||
"first",
|
||||
"papago",
|
||||
"v1"
|
||||
)
|
||||
).thenThrow(failure)
|
||||
@@ -124,8 +126,8 @@ class TranslationCoordinatorTest {
|
||||
|
||||
private fun coordinator(
|
||||
providers: List<TranslationProvider>,
|
||||
order: List<String> = providers.map { it.providerName }
|
||||
) = TranslationCoordinator(providers, LanguageProviderProperties(listOf("papago"), order), repository)
|
||||
order: List<LanguageProviderId> = providers.map { it.providerId }
|
||||
) = TranslationCoordinator(providers, LanguageProviderProperties(listOf(PAPAGO), order), repository)
|
||||
|
||||
private fun memory(provider: String, text: String) = TranslationMemory(
|
||||
"hash",
|
||||
@@ -151,12 +153,12 @@ class TranslationCoordinatorTest {
|
||||
}
|
||||
|
||||
class RecordingTranslationProvider(
|
||||
override val providerName: String,
|
||||
override val providerId: LanguageProviderId,
|
||||
private val response: (TranslateRequest) -> TranslateResult = { request ->
|
||||
TranslateResult(request.texts.indices.map { "$providerName:$it" })
|
||||
TranslateResult(request.texts.indices.map { "$providerId:$it" })
|
||||
}
|
||||
) : TranslationProvider {
|
||||
override val providerVersion = "$providerName-v1"
|
||||
override val providerVersion = "$providerId-v1"
|
||||
val requests = mutableListOf<TranslateRequest>()
|
||||
|
||||
override fun translate(request: TranslateRequest): TranslateResult {
|
||||
|
||||
+2
-2
@@ -236,7 +236,7 @@ class TranslationJobWorkerTest {
|
||||
|
||||
private fun successfulProvider(): TranslationProvider {
|
||||
return object : TranslationProvider {
|
||||
override val providerName: String = "papago"
|
||||
override val providerId: LanguageProviderId = LanguageProviderId.PAPAGO
|
||||
override val providerVersion: String = "nmt-v1"
|
||||
|
||||
override fun translate(request: TranslateRequest): TranslateResult {
|
||||
@@ -247,7 +247,7 @@ class TranslationJobWorkerTest {
|
||||
|
||||
private fun failingProvider(): TranslationProvider {
|
||||
return object : TranslationProvider {
|
||||
override val providerName: String = "papago"
|
||||
override val providerId: LanguageProviderId = LanguageProviderId.PAPAGO
|
||||
override val providerVersion: String = "nmt-v1"
|
||||
|
||||
override fun translate(request: TranslateRequest): TranslateResult {
|
||||
|
||||
Reference in New Issue
Block a user