AWS 번역 공급자 추가 #469
@@ -13,7 +13,7 @@ class LanguageDetectionCacheService(
|
|||||||
providers: List<LanguageDetectionProvider>,
|
providers: List<LanguageDetectionProvider>,
|
||||||
properties: LanguageProviderProperties
|
properties: LanguageProviderProperties
|
||||||
) {
|
) {
|
||||||
private val providers = properties.orderedProviders(properties.detectionOrder, providers) { it.providerName }
|
private val providers = properties.orderedProviders(properties.detectionOrder, providers) { it.providerId }
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun detectWithCache(query: String): DetectedLanguage? {
|
fun detectWithCache(query: String): DetectedLanguage? {
|
||||||
@@ -24,11 +24,11 @@ class LanguageDetectionCacheService(
|
|||||||
providers.forEach { provider ->
|
providers.forEach { provider ->
|
||||||
val cached = languageDetectionResultRepository.findBySourceHashAndProviderAndNormalizationVersion(
|
val cached = languageDetectionResultRepository.findBySourceHashAndProviderAndNormalizationVersion(
|
||||||
sourceHash = sourceHash,
|
sourceHash = sourceHash,
|
||||||
provider = provider.providerName,
|
provider = provider.providerId.code,
|
||||||
normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION
|
normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION
|
||||||
)
|
)
|
||||||
if (cached != null && cached.detectedLanguage.isNotBlank()) {
|
if (cached != null && cached.detectedLanguage.isNotBlank()) {
|
||||||
return DetectedLanguage(cached.detectedLanguage, provider.providerName)
|
return DetectedLanguage(cached.detectedLanguage, provider.providerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,18 +42,18 @@ class LanguageDetectionCacheService(
|
|||||||
sourceHash = sourceHash,
|
sourceHash = sourceHash,
|
||||||
sourceTextSample = normalizedQuery.take(MAX_SAMPLE_LENGTH),
|
sourceTextSample = normalizedQuery.take(MAX_SAMPLE_LENGTH),
|
||||||
detectedLanguage = detectedLanguage,
|
detectedLanguage = detectedLanguage,
|
||||||
provider = provider.providerName,
|
provider = provider.providerId.code,
|
||||||
confidence = null,
|
confidence = null,
|
||||||
normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION
|
normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION
|
||||||
)
|
)
|
||||||
val winner = requireNotNull(
|
val winner = requireNotNull(
|
||||||
languageDetectionResultRepository.findDetectedLanguageForUpdate(
|
languageDetectionResultRepository.findDetectedLanguageForUpdate(
|
||||||
sourceHash = sourceHash,
|
sourceHash = sourceHash,
|
||||||
provider = provider.providerName,
|
provider = provider.providerId.code,
|
||||||
normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION
|
normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return DetectedLanguage(winner, provider.providerName)
|
return DetectedLanguage(winner, provider.providerId)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package kr.co.vividnext.sodalive.i18n.translation
|
package kr.co.vividnext.sodalive.i18n.translation
|
||||||
|
|
||||||
data class DetectedLanguage(val detectedLanguage: String, val providerName: String)
|
data class DetectedLanguage(val detectedLanguage: String, val providerId: LanguageProviderId)
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
package kr.co.vividnext.sodalive.i18n.translation
|
package kr.co.vividnext.sodalive.i18n.translation
|
||||||
|
|
||||||
interface LanguageDetectionProvider {
|
interface LanguageDetectionProvider {
|
||||||
val providerName: String
|
val providerId: LanguageProviderId
|
||||||
|
|
||||||
fun detect(query: String): String?
|
fun detect(query: String): String?
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package kr.co.vividnext.sodalive.i18n.translation
|
||||||
|
|
||||||
|
enum class LanguageProviderId(val code: String) {
|
||||||
|
PAPAGO("papago"),
|
||||||
|
AWS("aws")
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package kr.co.vividnext.sodalive.i18n.translation
|
package kr.co.vividnext.sodalive.i18n.translation
|
||||||
|
|
||||||
object LanguageProviderOrder {
|
object LanguageProviderOrder {
|
||||||
val detectionOrder = listOf("papago")
|
val detectionOrder = listOf(LanguageProviderId.PAPAGO)
|
||||||
val translationOrder = listOf("papago")
|
val translationOrder = listOf(LanguageProviderId.PAPAGO)
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-8
@@ -5,18 +5,18 @@ import org.springframework.stereotype.Component
|
|||||||
|
|
||||||
@Component
|
@Component
|
||||||
class LanguageProviderProperties(
|
class LanguageProviderProperties(
|
||||||
val detectionOrder: List<String>,
|
val detectionOrder: List<LanguageProviderId>,
|
||||||
val translationOrder: List<String>
|
val translationOrder: List<LanguageProviderId>
|
||||||
) {
|
) {
|
||||||
@Autowired
|
@Autowired
|
||||||
constructor() : this(LanguageProviderOrder.detectionOrder, LanguageProviderOrder.translationOrder)
|
constructor() : this(LanguageProviderOrder.detectionOrder, LanguageProviderOrder.translationOrder)
|
||||||
|
|
||||||
fun <T> orderedProviders(order: List<String>, providers: List<T>, name: (T) -> String): List<T> {
|
fun <T> orderedProviders(order: List<LanguageProviderId>, providers: List<T>, id: (T) -> LanguageProviderId): List<T> {
|
||||||
require(order.isNotEmpty() && order.none { it.isBlank() }) { "language provider order must not be empty" }
|
require(order.isNotEmpty()) { "language provider order must not be empty" }
|
||||||
require(order.distinct().size == order.size) { "duplicate language provider order" }
|
require(order.distinct().size == order.size) { "duplicate language provider order" }
|
||||||
val byName = providers.associateBy(name)
|
val byId = providers.associateBy(id)
|
||||||
require(byName.size == providers.size) { "duplicate registered language provider name" }
|
require(byId.size == providers.size) { "duplicate registered language provider name" }
|
||||||
require(order.all { it in byName }) { "unknown language provider in order" }
|
require(order.all { it in byId }) { "unknown language provider in order" }
|
||||||
return order.map { byName.getValue(it) }
|
return order.map { byId.getValue(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ class PapagoLanguageDetectionProvider(
|
|||||||
private val papagoClientSecret: String
|
private val papagoClientSecret: String
|
||||||
) : LanguageDetectionProvider {
|
) : LanguageDetectionProvider {
|
||||||
private val restTemplate = RestTemplate()
|
private val restTemplate = RestTemplate()
|
||||||
override val providerName = "papago"
|
override val providerId = LanguageProviderId.PAPAGO
|
||||||
|
|
||||||
override fun detect(query: String): String? {
|
override fun detect(query: String): String? {
|
||||||
val headers = HttpHeaders().apply {
|
val headers = HttpHeaders().apply {
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ class PapagoTranslationService(
|
|||||||
|
|
||||||
private val papagoTranslateUrl = "https://papago.apigw.ntruss.com/nmt/v1/translation"
|
private val papagoTranslateUrl = "https://papago.apigw.ntruss.com/nmt/v1/translation"
|
||||||
|
|
||||||
override val providerName: String = "papago"
|
override val providerId: LanguageProviderId = LanguageProviderId.PAPAGO
|
||||||
|
|
||||||
override val providerVersion: String = "nmt-v1"
|
override val providerVersion: String = "nmt-v1"
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,6 +2,6 @@ package kr.co.vividnext.sodalive.i18n.translation
|
|||||||
|
|
||||||
data class ProviderTranslationResult(
|
data class ProviderTranslationResult(
|
||||||
val result: TranslateResult,
|
val result: TranslateResult,
|
||||||
val providerName: String,
|
val providerId: LanguageProviderId,
|
||||||
val providerVersion: String
|
val providerVersion: String
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class TranslationCoordinator(
|
|||||||
properties: LanguageProviderProperties,
|
properties: LanguageProviderProperties,
|
||||||
private val translationMemoryRepository: TranslationMemoryRepository
|
private val translationMemoryRepository: TranslationMemoryRepository
|
||||||
) {
|
) {
|
||||||
private val providers = properties.orderedProviders(properties.translationOrder, providers) { it.providerName }
|
private val providers = properties.orderedProviders(properties.translationOrder, providers) { it.providerId }
|
||||||
|
|
||||||
fun findCached(sourceHash: String, sourceLanguage: String, targetLanguage: String): TranslationMemory? {
|
fun findCached(sourceHash: String, sourceLanguage: String, targetLanguage: String): TranslationMemory? {
|
||||||
providers.forEach { provider ->
|
providers.forEach { provider ->
|
||||||
@@ -17,7 +17,7 @@ class TranslationCoordinator(
|
|||||||
sourceHash,
|
sourceHash,
|
||||||
sourceLanguage,
|
sourceLanguage,
|
||||||
targetLanguage,
|
targetLanguage,
|
||||||
provider.providerName,
|
provider.providerId.code,
|
||||||
SourceTextNormalizer.NORMALIZATION_VERSION
|
SourceTextNormalizer.NORMALIZATION_VERSION
|
||||||
)
|
)
|
||||||
if (memory != null && memory.translatedText.isNotBlank()) return memory
|
if (memory != null && memory.translatedText.isNotBlank()) return memory
|
||||||
@@ -42,7 +42,7 @@ class TranslationCoordinator(
|
|||||||
result != null && result.translatedText.size == request.texts.size &&
|
result != null && result.translatedText.size == request.texts.size &&
|
||||||
result.translatedText.none { it.isBlank() }
|
result.translatedText.none { it.isBlank() }
|
||||||
) {
|
) {
|
||||||
return ProviderTranslationResult(result, provider.providerName, provider.providerVersion)
|
return ProviderTranslationResult(result, provider.providerId, provider.providerVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw IllegalStateException("all translation providers failed")
|
throw IllegalStateException("all translation providers failed")
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class TranslationJobWorker(
|
|||||||
sourceHash = job.sourceHash,
|
sourceHash = job.sourceHash,
|
||||||
sourceLanguage = job.sourceLanguage,
|
sourceLanguage = job.sourceLanguage,
|
||||||
targetLanguage = job.targetLanguage,
|
targetLanguage = job.targetLanguage,
|
||||||
provider = response.providerName,
|
provider = response.providerId.code,
|
||||||
normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION
|
normalizationVersion = SourceTextNormalizer.NORMALIZATION_VERSION
|
||||||
)
|
)
|
||||||
if (memory == null) {
|
if (memory == null) {
|
||||||
@@ -98,7 +98,7 @@ class TranslationJobWorker(
|
|||||||
sourceLanguage = job.sourceLanguage,
|
sourceLanguage = job.sourceLanguage,
|
||||||
targetLanguage = job.targetLanguage,
|
targetLanguage = job.targetLanguage,
|
||||||
translatedText = translated,
|
translatedText = translated,
|
||||||
provider = response.providerName,
|
provider = response.providerId.code,
|
||||||
providerVersion = response.providerVersion
|
providerVersion = response.providerVersion
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package kr.co.vividnext.sodalive.i18n.translation
|
package kr.co.vividnext.sodalive.i18n.translation
|
||||||
|
|
||||||
interface TranslationProvider {
|
interface TranslationProvider {
|
||||||
val providerName: String
|
val providerId: LanguageProviderId
|
||||||
val providerVersion: String
|
val providerVersion: String
|
||||||
|
|
||||||
fun translate(request: TranslateRequest): TranslateResult
|
fun translate(request: TranslateRequest): TranslateResult
|
||||||
|
|||||||
+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.SodaMessageSource
|
||||||
import kr.co.vividnext.sodalive.i18n.translation.DetectedLanguage
|
import kr.co.vividnext.sodalive.i18n.translation.DetectedLanguage
|
||||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageDetectionProvider
|
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.LanguageProviderProperties
|
||||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
|
import kr.co.vividnext.sodalive.i18n.translation.LanguageTranslationTargetType
|
||||||
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
|
import kr.co.vividnext.sodalive.i18n.translation.PapagoTranslationService
|
||||||
@@ -730,7 +731,7 @@ class ControlledLanguageDetectionCacheService(
|
|||||||
) : LanguageDetectionCacheService(
|
) : LanguageDetectionCacheService(
|
||||||
languageDetectionResultRepository,
|
languageDetectionResultRepository,
|
||||||
listOf(object : LanguageDetectionProvider {
|
listOf(object : LanguageDetectionProvider {
|
||||||
override val providerName = "papago"
|
override val providerId = LanguageProviderId.PAPAGO
|
||||||
override fun detect(query: String): String? = null
|
override fun detect(query: String): String? = null
|
||||||
}),
|
}),
|
||||||
LanguageProviderProperties()
|
LanguageProviderProperties()
|
||||||
@@ -759,15 +760,15 @@ class ControlledLanguageDetectionCacheService(
|
|||||||
queries.add(query)
|
queries.add(query)
|
||||||
val concurrentDetection = concurrentDetections[query]
|
val concurrentDetection = concurrentDetections[query]
|
||||||
if (concurrentDetection != null) {
|
if (concurrentDetection != null) {
|
||||||
return DetectedLanguage(concurrentDetection.detect(), "papago")
|
return DetectedLanguage(concurrentDetection.detect(), LanguageProviderId.PAPAGO)
|
||||||
}
|
}
|
||||||
val blockingDetection = blockingDetections[query]
|
val blockingDetection = blockingDetections[query]
|
||||||
if (blockingDetection != null) {
|
if (blockingDetection != null) {
|
||||||
blockingDetection.started.countDown()
|
blockingDetection.started.countDown()
|
||||||
check(blockingDetection.release.await(5, TimeUnit.SECONDS))
|
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.DetectedLanguage
|
||||||
import kr.co.vividnext.sodalive.i18n.translation.LanguageDetectionProvider
|
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.LanguageProviderProperties
|
||||||
import kr.co.vividnext.sodalive.i18n.translation.SourceTextNormalizer
|
import kr.co.vividnext.sodalive.i18n.translation.SourceTextNormalizer
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
@@ -36,7 +37,7 @@ class LanguageDetectionCacheServiceTest {
|
|||||||
|
|
||||||
val detected = service.detectWithCache("Hello world")
|
val detected = service.detectWithCache("Hello world")
|
||||||
|
|
||||||
assertEquals(DetectedLanguage("en", "papago"), detected)
|
assertEquals(DetectedLanguage("en", LanguageProviderId.PAPAGO), detected)
|
||||||
assertEquals(0, providerCalls)
|
assertEquals(0, providerCalls)
|
||||||
Mockito.verify(repository).findBySourceHashAndProviderAndNormalizationVersion(
|
Mockito.verify(repository).findBySourceHashAndProviderAndNormalizationVersion(
|
||||||
sourceHash = sourceHash,
|
sourceHash = sourceHash,
|
||||||
@@ -80,7 +81,7 @@ class LanguageDetectionCacheServiceTest {
|
|||||||
|
|
||||||
val detected = service.detectWithCache("Hello world")
|
val detected = service.detectWithCache("Hello world")
|
||||||
|
|
||||||
assertEquals(DetectedLanguage("ko", "papago"), detected)
|
assertEquals(DetectedLanguage("ko", LanguageProviderId.PAPAGO), detected)
|
||||||
Mockito.verify(repository).findBySourceHashAndProviderAndNormalizationVersion(
|
Mockito.verify(repository).findBySourceHashAndProviderAndNormalizationVersion(
|
||||||
sourceHash = sourceHash,
|
sourceHash = sourceHash,
|
||||||
provider = "papago",
|
provider = "papago",
|
||||||
@@ -104,7 +105,7 @@ class LanguageDetectionCacheServiceTest {
|
|||||||
|
|
||||||
private fun service(repository: LanguageDetectionResultRepository, detect: () -> String?): LanguageDetectionCacheService {
|
private fun service(repository: LanguageDetectionResultRepository, detect: () -> String?): LanguageDetectionCacheService {
|
||||||
val provider = object : LanguageDetectionProvider {
|
val provider = object : LanguageDetectionProvider {
|
||||||
override val providerName = "papago"
|
override val providerId = LanguageProviderId.PAPAGO
|
||||||
override fun detect(query: String) = detect()
|
override fun detect(query: String) = detect()
|
||||||
}
|
}
|
||||||
return LanguageDetectionCacheService(repository, listOf(provider), LanguageProviderProperties())
|
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.LanguageDetectionCacheService
|
||||||
import kr.co.vividnext.sodalive.content.LanguageDetectionResult
|
import kr.co.vividnext.sodalive.content.LanguageDetectionResult
|
||||||
import kr.co.vividnext.sodalive.content.LanguageDetectionResultRepository
|
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.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertNull
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
import org.junit.jupiter.api.Assertions.assertSame
|
import org.junit.jupiter.api.Assertions.assertSame
|
||||||
@@ -21,39 +23,39 @@ class LanguageDetectionProviderTest {
|
|||||||
fun shouldFallbackAndReturnCanonicalWinnerWithActualProvider() {
|
fun shouldFallbackAndReturnCanonicalWinnerWithActualProvider() {
|
||||||
val failures: List<() -> String?> = listOf({ throw IllegalStateException("sensitive body") }, { null }, { " " })
|
val failures: List<() -> String?> = listOf({ throw IllegalStateException("sensitive body") }, { null }, { " " })
|
||||||
failures.forEach { failure ->
|
failures.forEach { failure ->
|
||||||
val first = RecordingDetectionProvider("first", failure)
|
val first = RecordingDetectionProvider(PAPAGO, failure)
|
||||||
val second = RecordingDetectionProvider("second") { "EN" }
|
val second = RecordingDetectionProvider(AWS) { "EN" }
|
||||||
Mockito.`when`(repository.findDetectedLanguageForUpdate(hash, "second", "v1")).thenReturn("ko")
|
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), first.queries)
|
||||||
assertEquals(listOf(query), second.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
|
@Test
|
||||||
@DisplayName("보조 공급자의 유효 캐시가 있으면 첫 공급자도 호출하지 않는다")
|
@DisplayName("보조 공급자의 유효 캐시가 있으면 첫 공급자도 호출하지 않는다")
|
||||||
fun shouldCheckAllConfiguredCachesBeforeCallingAnyProvider() {
|
fun shouldCheckAllConfiguredCachesBeforeCallingAnyProvider() {
|
||||||
val first = RecordingDetectionProvider("first") { "ko" }
|
val first = RecordingDetectionProvider(PAPAGO) { "ko" }
|
||||||
val second = RecordingDetectionProvider("second") { "en" }
|
val second = RecordingDetectionProvider(AWS) { "en" }
|
||||||
Mockito.`when`(repository.findBySourceHashAndProviderAndNormalizationVersion(hash, "second", "v1"))
|
Mockito.`when`(repository.findBySourceHashAndProviderAndNormalizationVersion(hash, "aws", "v1"))
|
||||||
.thenReturn(LanguageDetectionResult(hash, "sample", "ja", "second", null, "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)
|
assertEquals(emptyList<String>(), first.queries + second.queries)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("정상 미지원 언어 감지는 성공이며 후속 공급자를 호출하지 않는다")
|
@DisplayName("정상 미지원 언어 감지는 성공이며 후속 공급자를 호출하지 않는다")
|
||||||
fun shouldKeepUnsupportedDetectionAsSuccessAndHonorOrder() {
|
fun shouldKeepUnsupportedDetectionAsSuccessAndHonorOrder() {
|
||||||
val first = RecordingDetectionProvider("first") { "ko" }
|
val first = RecordingDetectionProvider(PAPAGO) { "ko" }
|
||||||
val second = RecordingDetectionProvider("second") { "FR" }
|
val second = RecordingDetectionProvider(AWS) { "FR" }
|
||||||
Mockito.`when`(repository.findDetectedLanguageForUpdate(hash, "second", "v1")).thenReturn("fr")
|
Mockito.`when`(repository.findDetectedLanguageForUpdate(hash, "aws", "v1")).thenReturn("fr")
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
DetectedLanguage("fr", "second"),
|
DetectedLanguage("fr", AWS),
|
||||||
service(listOf(first, second), listOf("second", "first")).detectWithCache(query)
|
service(listOf(first, second), listOf(AWS, PAPAGO)).detectWithCache(query)
|
||||||
)
|
)
|
||||||
assertEquals(emptyList<String>(), first.queries)
|
assertEquals(emptyList<String>(), first.queries)
|
||||||
assertEquals(listOf(query), second.queries)
|
assertEquals(listOf(query), second.queries)
|
||||||
@@ -62,8 +64,8 @@ class LanguageDetectionProviderTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("감지 전체 실패와 공백 입력은 null이고 공백은 저장소와 공급자를 호출하지 않는다")
|
@DisplayName("감지 전체 실패와 공백 입력은 null이고 공백은 저장소와 공급자를 호출하지 않는다")
|
||||||
fun shouldReturnNullAfterAllFailuresAndSkipBlankInput() {
|
fun shouldReturnNullAfterAllFailuresAndSkipBlankInput() {
|
||||||
val first = RecordingDetectionProvider("first") { null }
|
val first = RecordingDetectionProvider(PAPAGO) { null }
|
||||||
val second = RecordingDetectionProvider("second") { null }
|
val second = RecordingDetectionProvider(AWS) { null }
|
||||||
val service = service(listOf(first, second))
|
val service = service(listOf(first, second))
|
||||||
assertNull(service.detectWithCache(" \n"))
|
assertNull(service.detectWithCache(" \n"))
|
||||||
Mockito.verifyNoInteractions(repository)
|
Mockito.verifyNoInteractions(repository)
|
||||||
@@ -75,28 +77,30 @@ class LanguageDetectionProviderTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("감지 저장소 조회와 저장 오류는 다음 공급자로 전환하지 않고 전파한다")
|
@DisplayName("감지 저장소 조회와 저장 오류는 다음 공급자로 전환하지 않고 전파한다")
|
||||||
fun shouldPropagateRepositoryFailuresWithoutFallback() {
|
fun shouldPropagateRepositoryFailuresWithoutFallback() {
|
||||||
val first = RecordingDetectionProvider("first") { "ko" }
|
val first = RecordingDetectionProvider(PAPAGO) { "ko" }
|
||||||
val second = RecordingDetectionProvider("second") { "en" }
|
val second = RecordingDetectionProvider(AWS) { "en" }
|
||||||
val service = service(listOf(first, second))
|
val service = service(listOf(first, second))
|
||||||
val failure = IllegalStateException("database unavailable")
|
val failure = IllegalStateException("database unavailable")
|
||||||
Mockito.`when`(repository.findBySourceHashAndProviderAndNormalizationVersion(hash, "first", "v1"))
|
Mockito.`when`(repository.findBySourceHashAndProviderAndNormalizationVersion(hash, "papago", "v1"))
|
||||||
.thenThrow(failure)
|
.thenThrow(failure)
|
||||||
assertSame(failure, assertThrows(IllegalStateException::class.java) { service.detectWithCache(query) })
|
assertSame(failure, assertThrows(IllegalStateException::class.java) { service.detectWithCache(query) })
|
||||||
assertEquals(emptyList<String>(), first.queries + second.queries)
|
assertEquals(emptyList<String>(), first.queries + second.queries)
|
||||||
|
|
||||||
Mockito.reset(repository)
|
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) })
|
assertSame(failure, assertThrows(IllegalStateException::class.java) { service.detectWithCache(query) })
|
||||||
assertEquals(listOf(query), first.queries)
|
assertEquals(listOf(query), first.queries)
|
||||||
assertEquals(emptyList<String>(), second.queries)
|
assertEquals(emptyList<String>(), second.queries)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun service(providers: List<LanguageDetectionProvider>, order: List<String> = providers.map { it.providerName }) =
|
private fun service(
|
||||||
LanguageDetectionCacheService(repository, providers, LanguageProviderProperties(order, listOf("papago")))
|
providers: List<LanguageDetectionProvider>,
|
||||||
|
order: List<LanguageProviderId> = providers.map { it.providerId }
|
||||||
|
) = LanguageDetectionCacheService(repository, providers, LanguageProviderProperties(order, listOf(PAPAGO)))
|
||||||
}
|
}
|
||||||
|
|
||||||
class RecordingDetectionProvider(
|
class RecordingDetectionProvider(
|
||||||
override val providerName: String,
|
override val providerId: LanguageProviderId,
|
||||||
private val response: () -> String?
|
private val response: () -> String?
|
||||||
) : LanguageDetectionProvider {
|
) : LanguageDetectionProvider {
|
||||||
val queries = mutableListOf<String>()
|
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.content.theme.AudioContentThemeQueryRepository
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
|
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
|
||||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityRepository
|
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.Member
|
||||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||||
import kr.co.vividnext.sodalive.member.MemberRole
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
@@ -86,16 +88,16 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
|
|||||||
@DisplayName("보조 공급자의 감지를 H2에 귀속 저장하고 외부 호출 없이 캐시를 재사용한다")
|
@DisplayName("보조 공급자의 감지를 H2에 귀속 저장하고 외부 호출 없이 캐시를 재사용한다")
|
||||||
fun shouldPersistSecondaryDetectionAndReuseCache() {
|
fun shouldPersistSecondaryDetectionAndReuseCache() {
|
||||||
val source = "provider source ${UUID.randomUUID()}"
|
val source = "provider source ${UUID.randomUUID()}"
|
||||||
assertEquals(DetectedLanguage("ko", "secondary"), detector.detectWithCache(source))
|
assertEquals(DetectedLanguage("ko", AWS), detector.detectWithCache(source))
|
||||||
assertEquals(DetectedLanguage("ko", "secondary"), detector.detectWithCache(source))
|
assertEquals(DetectedLanguage("ko", AWS), detector.detectWithCache(source))
|
||||||
val detection = transaction.execute {
|
val detection = transaction.execute {
|
||||||
detectionRepository.findBySourceHashAndProviderAndNormalizationVersion(
|
detectionRepository.findBySourceHashAndProviderAndNormalizationVersion(
|
||||||
SourceTextNormalizer.hash(source),
|
SourceTextNormalizer.hash(source),
|
||||||
"secondary",
|
"aws",
|
||||||
"v1"
|
"v1"
|
||||||
)
|
)
|
||||||
}!!
|
}!!
|
||||||
assertEquals("secondary", detection.provider)
|
assertEquals("aws", detection.provider)
|
||||||
assertEquals("ko", detection.detectedLanguage)
|
assertEquals("ko", detection.detectedLanguage)
|
||||||
assertEquals(1, papagoDetection.queries.size)
|
assertEquals(1, papagoDetection.queries.size)
|
||||||
assertEquals(1, secondaryDetection.queries.size)
|
assertEquals(1, secondaryDetection.queries.size)
|
||||||
@@ -113,19 +115,19 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
|
|||||||
SourceTextNormalizer.hash(source),
|
SourceTextNormalizer.hash(source),
|
||||||
"ko",
|
"ko",
|
||||||
"en",
|
"en",
|
||||||
"secondary",
|
"aws",
|
||||||
"v1"
|
"v1"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
assertNotNull(memory)
|
assertNotNull(memory)
|
||||||
assertEquals("secondary", memory!!.provider)
|
assertEquals("aws", memory!!.provider)
|
||||||
assertEquals("secondary-v1", memory.providerVersion)
|
assertEquals("AWS-v1", memory.providerVersion)
|
||||||
assertEquals("secondary:0", memory.translatedText)
|
assertEquals("AWS:0", memory.translatedText)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
TranslationJobStatus.COMPLETED,
|
TranslationJobStatus.COMPLETED,
|
||||||
transaction.execute { jobRepository.findAll().single { it.resourceId == postId }.status }
|
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 {
|
transaction.executeWithoutResult {
|
||||||
translations.deleteAll()
|
translations.deleteAll()
|
||||||
@@ -137,7 +139,7 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
|
|||||||
assertEquals(1, papagoTranslation.requests.size)
|
assertEquals(1, papagoTranslation.requests.size)
|
||||||
assertEquals(1, secondaryTranslation.requests.size)
|
assertEquals(1, secondaryTranslation.requests.size)
|
||||||
assertEquals(listOf(source), secondaryTranslation.requests.single().texts)
|
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) {
|
private fun schedule(postId: Long, source: String) {
|
||||||
@@ -162,20 +164,20 @@ class LanguageProviderCacheEndToEndTest @Autowired constructor(
|
|||||||
class Providers {
|
class Providers {
|
||||||
@Bean
|
@Bean
|
||||||
fun languageProviderProperties() = LanguageProviderProperties(
|
fun languageProviderProperties() = LanguageProviderProperties(
|
||||||
listOf("papago", "secondary"),
|
listOf(PAPAGO, AWS),
|
||||||
listOf("papago", "secondary")
|
listOf(PAPAGO, AWS)
|
||||||
)
|
)
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun papagoDetection() = RecordingDetectionProvider("papago") { throw IllegalStateException("sensitive provider body") }
|
fun papagoDetection() = RecordingDetectionProvider(PAPAGO) { throw IllegalStateException("sensitive provider body") }
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun secondaryDetection() = RecordingDetectionProvider("secondary") { "ko" }
|
fun secondaryDetection() = RecordingDetectionProvider(AWS) { "ko" }
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun papagoTranslation() = RecordingTranslationProvider("papago") { TranslateResult(emptyList()) }
|
fun papagoTranslation() = RecordingTranslationProvider(PAPAGO) { TranslateResult(emptyList()) }
|
||||||
|
|
||||||
@Bean
|
@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
|
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.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertThrows
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
@@ -9,33 +11,33 @@ class LanguageProviderPropertiesTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("기능별 목록을 서로 독립된 설정 순서로 정렬한다")
|
@DisplayName("기능별 목록을 서로 독립된 설정 순서로 정렬한다")
|
||||||
fun shouldOrderCapabilitiesIndependently() {
|
fun shouldOrderCapabilitiesIndependently() {
|
||||||
val properties = LanguageProviderProperties(listOf("second", "first"), listOf("first", "second"))
|
val properties = LanguageProviderProperties(listOf(AWS, PAPAGO), listOf(PAPAGO, AWS))
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("second", "first"),
|
listOf(AWS, PAPAGO),
|
||||||
properties.orderedProviders(properties.detectionOrder, listOf("first", "second")) { it }
|
properties.orderedProviders(properties.detectionOrder, listOf(PAPAGO, AWS)) { it }
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("first", "second"),
|
listOf(PAPAGO, AWS),
|
||||||
properties.orderedProviders(properties.translationOrder, listOf("second", "first")) { it }
|
properties.orderedProviders(properties.translationOrder, listOf(AWS, PAPAGO)) { it }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("빈 순서와 빈 이름과 중복 순서와 미등록 공급자는 초기화에서 거부한다")
|
@DisplayName("빈 순서와 중복 순서와 미등록 AWS 공급자는 초기화에서 거부한다")
|
||||||
fun shouldRejectInvalidOrders() {
|
fun shouldRejectInvalidOrders() {
|
||||||
val properties = LanguageProviderProperties()
|
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) {
|
assertThrows(IllegalArgumentException::class.java) {
|
||||||
properties.orderedProviders(order, listOf("first", "second")) { it }
|
properties.orderedProviders(order, listOf(PAPAGO)) { it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("같은 기능 안에서 중복 등록된 공급자 이름은 초기화에서 거부한다")
|
@DisplayName("같은 기능 안에서 선택되지 않은 공급자도 중복 등록을 거부한다")
|
||||||
fun shouldRejectDuplicateRegisteredNamesEvenWhenNotSelected() {
|
fun shouldRejectDuplicateRegisteredIdsEvenWhenNotSelected() {
|
||||||
assertThrows(IllegalArgumentException::class.java) {
|
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.LanguageDetectionCacheService
|
||||||
import kr.co.vividnext.sodalive.content.LanguageDetectionResultRepository
|
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.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||||
import org.junit.jupiter.api.Assertions.assertNull
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
@@ -16,7 +18,8 @@ import org.springframework.core.env.SystemEnvironmentPropertySource
|
|||||||
import java.util.function.Supplier
|
import java.util.function.Supplier
|
||||||
|
|
||||||
class LanguageProviderSpringConfigurationTest {
|
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)
|
private val defaultRunner = runner.withUserConfiguration(LanguageProviderProperties::class.java)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -28,8 +31,8 @@ class LanguageProviderSpringConfigurationTest {
|
|||||||
).run { context ->
|
).run { context ->
|
||||||
assertNull(context.startupFailure)
|
assertNull(context.startupFailure)
|
||||||
val properties = context.getBean(LanguageProviderProperties::class.java)
|
val properties = context.getBean(LanguageProviderProperties::class.java)
|
||||||
assertEquals(listOf("papago"), properties.detectionOrder)
|
assertEquals(listOf(PAPAGO), properties.detectionOrder)
|
||||||
assertEquals(listOf("papago"), properties.translationOrder)
|
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.detection-order"))
|
||||||
assertEquals("secondary,papago", context.environment.getProperty("sodalive.language-providers.translation-order"))
|
assertEquals("secondary,papago", context.environment.getProperty("sodalive.language-providers.translation-order"))
|
||||||
val properties = context.getBean(LanguageProviderProperties::class.java)
|
val properties = context.getBean(LanguageProviderProperties::class.java)
|
||||||
assertEquals(listOf("papago"), properties.detectionOrder)
|
assertEquals(listOf(PAPAGO), properties.detectionOrder)
|
||||||
assertEquals(listOf("papago"), properties.translationOrder)
|
assertEquals(listOf(PAPAGO), properties.translationOrder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,39 +66,39 @@ class LanguageProviderSpringConfigurationTest {
|
|||||||
@DisplayName("Spring 테스트 bean의 감지 목록만 바꾸어도 번역 순서는 유지한다")
|
@DisplayName("Spring 테스트 bean의 감지 목록만 바꾸어도 번역 순서는 유지한다")
|
||||||
fun shouldInjectIndependentCapabilityOrders() {
|
fun shouldInjectIndependentCapabilityOrders() {
|
||||||
listOf(
|
listOf(
|
||||||
listOf("papago", "secondary") to "papago",
|
listOf(PAPAGO, AWS) to PAPAGO,
|
||||||
listOf("secondary", "papago") to "secondary"
|
listOf(AWS, PAPAGO) to AWS
|
||||||
).forEach { (order, expected) ->
|
).forEach { (order, expected) ->
|
||||||
runner.withBean(
|
runner.withBean(
|
||||||
LanguageProviderProperties::class.java,
|
LanguageProviderProperties::class.java,
|
||||||
Supplier { LanguageProviderProperties(order, listOf("papago", "secondary")) }
|
Supplier { LanguageProviderProperties(order, listOf(PAPAGO, AWS)) }
|
||||||
).run { context ->
|
).run { context ->
|
||||||
assertNull(context.startupFailure)
|
assertNull(context.startupFailure)
|
||||||
val detector = context.getBean(LanguageDetectionCacheService::class.java)
|
val detector = context.getBean(LanguageDetectionCacheService::class.java)
|
||||||
assertEquals(expected, detector.detectWithCache("source")?.providerName)
|
assertEquals(expected, detector.detectWithCache("source")?.providerId)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"papago",
|
PAPAGO,
|
||||||
context.getBean(TranslationCoordinator::class.java)
|
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)
|
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)
|
assertEquals(0, context.getBean("secondaryTranslation", RecordingTranslationProvider::class.java).requests.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
runner.withBean(
|
runner.withBean(
|
||||||
LanguageProviderProperties::class.java,
|
LanguageProviderProperties::class.java,
|
||||||
Supplier { LanguageProviderProperties(listOf("papago", "secondary"), listOf("secondary", "papago")) }
|
Supplier { LanguageProviderProperties(listOf(PAPAGO, AWS), listOf(AWS, PAPAGO)) }
|
||||||
).run { context ->
|
).run { context ->
|
||||||
assertNull(context.startupFailure)
|
assertNull(context.startupFailure)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"papago",
|
PAPAGO,
|
||||||
context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerName
|
context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerId
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"secondary",
|
AWS,
|
||||||
context.getBean(TranslationCoordinator::class.java)
|
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 ->
|
defaultRunner.run { context ->
|
||||||
assertNull(context.startupFailure)
|
assertNull(context.startupFailure)
|
||||||
val properties = context.getBean(LanguageProviderProperties::class.java)
|
val properties = context.getBean(LanguageProviderProperties::class.java)
|
||||||
assertEquals(listOf("papago"), properties.detectionOrder)
|
assertEquals(listOf(PAPAGO), properties.detectionOrder)
|
||||||
assertEquals(listOf("papago"), properties.translationOrder)
|
assertEquals(listOf(PAPAGO), properties.translationOrder)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"papago",
|
PAPAGO,
|
||||||
context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerName
|
context.getBean(LanguageDetectionCacheService::class.java).detectWithCache("source")?.providerId
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"papago",
|
PAPAGO,
|
||||||
context.getBean(TranslationCoordinator::class.java)
|
context.getBean(TranslationCoordinator::class.java)
|
||||||
.translate(TranslateRequest(listOf("source"), "ko", "en")).providerName
|
.translate(TranslateRequest(listOf("source"), "ko", "en")).providerId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Spring 테스트 bean의 빈 목록과 빈 이름과 중복 순서와 미등록 이름은 시작 시 실패한다")
|
@DisplayName("Spring 테스트 bean의 빈 목록과 중복 순서와 해당 기능의 AWS 구현체 부재는 시작 시 실패한다")
|
||||||
fun shouldRejectInvalidOrdersAtStartup() {
|
fun shouldRejectInvalidOrdersAtStartup() {
|
||||||
listOf(emptyList(), listOf(""), listOf("papago", "papago"), listOf("unknown")).forEach { order ->
|
listOf(emptyList(), listOf(PAPAGO, PAPAGO), listOf(AWS)).forEach { order ->
|
||||||
listOf(
|
listOf(
|
||||||
LanguageProviderProperties(order, listOf("papago")),
|
LanguageProviderProperties(order, listOf(PAPAGO)),
|
||||||
LanguageProviderProperties(listOf("papago"), order)
|
LanguageProviderProperties(listOf(PAPAGO), order)
|
||||||
).forEach { properties ->
|
).forEach { properties ->
|
||||||
runner.withBean(LanguageProviderProperties::class.java, Supplier { properties }).run { context ->
|
papagoRunner.withBean(LanguageProviderProperties::class.java, Supplier { properties }).run { context ->
|
||||||
assertNotNull(context.startupFailure)
|
assertNotNull(context.startupFailure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,7 +147,7 @@ class LanguageProviderSpringConfigurationTest {
|
|||||||
@Bean
|
@Bean
|
||||||
fun detectionRepository(): LanguageDetectionResultRepository {
|
fun detectionRepository(): LanguageDetectionResultRepository {
|
||||||
val repository = Mockito.mock(LanguageDetectionResultRepository::class.java)
|
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"))
|
Mockito.`when`(repository.findDetectedLanguageForUpdate(SourceTextNormalizer.hash("source"), provider, "v1"))
|
||||||
.thenReturn("ko")
|
.thenReturn("ko")
|
||||||
}
|
}
|
||||||
@@ -152,15 +155,18 @@ class LanguageProviderSpringConfigurationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun papagoDetection() = RecordingDetectionProvider("papago") { "ko" }
|
fun papagoDetection() = RecordingDetectionProvider(PAPAGO) { "ko" }
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun secondaryDetection() = RecordingDetectionProvider("secondary") { "ko" }
|
fun papagoTranslation() = RecordingTranslationProvider(PAPAGO)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestConfiguration
|
||||||
|
class AwsProviders {
|
||||||
|
@Bean
|
||||||
|
fun secondaryDetection() = RecordingDetectionProvider(AWS) { "ko" }
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun papagoTranslation() = RecordingTranslationProvider("papago")
|
fun secondaryTranslation() = RecordingTranslationProvider(AWS)
|
||||||
|
|
||||||
@Bean
|
|
||||||
fun secondaryTranslation() = RecordingTranslationProvider("secondary")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-26
@@ -1,5 +1,7 @@
|
|||||||
package kr.co.vividnext.sodalive.i18n.translation
|
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.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertNull
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
import org.junit.jupiter.api.Assertions.assertSame
|
import org.junit.jupiter.api.Assertions.assertSame
|
||||||
@@ -15,15 +17,15 @@ class TranslationCoordinatorTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("설정 순서의 첫 성공 공급자와 버전을 반환하고 후속 호출을 멈춘다")
|
@DisplayName("설정 순서의 첫 성공 공급자와 버전을 반환하고 후속 호출을 멈춘다")
|
||||||
fun shouldUseConfiguredOrderAndStopAfterSuccess() {
|
fun shouldUseConfiguredOrderAndStopAfterSuccess() {
|
||||||
val first = RecordingTranslationProvider("first")
|
val first = RecordingTranslationProvider(PAPAGO)
|
||||||
val second = RecordingTranslationProvider("second")
|
val second = RecordingTranslationProvider(AWS)
|
||||||
val coordinator = coordinator(listOf(first, second), listOf("second", "first"))
|
val coordinator = coordinator(listOf(first, second), listOf(AWS, PAPAGO))
|
||||||
|
|
||||||
val result = coordinator.translate(request)
|
val result = coordinator.translate(request)
|
||||||
|
|
||||||
assertEquals("second", result.providerName)
|
assertEquals(AWS, result.providerId)
|
||||||
assertEquals("second-v1", result.providerVersion)
|
assertEquals("AWS-v1", result.providerVersion)
|
||||||
assertEquals(listOf("second:0", "second:1"), result.result.translatedText)
|
assertEquals(listOf("AWS:0", "AWS:1"), result.result.translatedText)
|
||||||
assertEquals(listOf(request), second.requests)
|
assertEquals(listOf(request), second.requests)
|
||||||
assertEquals(emptyList<TranslateRequest>(), first.requests)
|
assertEquals(emptyList<TranslateRequest>(), first.requests)
|
||||||
}
|
}
|
||||||
@@ -37,11 +39,11 @@ class TranslationCoordinatorTest {
|
|||||||
{ TranslateResult(listOf("translated", " ")) }
|
{ TranslateResult(listOf("translated", " ")) }
|
||||||
)
|
)
|
||||||
failures.forEach { failure ->
|
failures.forEach { failure ->
|
||||||
val first = RecordingTranslationProvider("first", failure)
|
val first = RecordingTranslationProvider(PAPAGO, failure)
|
||||||
val second = RecordingTranslationProvider("second")
|
val second = RecordingTranslationProvider(AWS)
|
||||||
val coordinator = coordinator(listOf(first, second))
|
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), first.requests)
|
||||||
assertEquals(listOf(request), second.requests)
|
assertEquals(listOf(request), second.requests)
|
||||||
}
|
}
|
||||||
@@ -50,8 +52,8 @@ class TranslationCoordinatorTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("전체 공급자 실패는 원문이나 원시 예외 원인 없이 고정 메시지를 반환한다")
|
@DisplayName("전체 공급자 실패는 원문이나 원시 예외 원인 없이 고정 메시지를 반환한다")
|
||||||
fun shouldSanitizeAllProviderFailures() {
|
fun shouldSanitizeAllProviderFailures() {
|
||||||
val first = RecordingTranslationProvider("first") { throw IllegalStateException("sensitive response body") }
|
val first = RecordingTranslationProvider(PAPAGO) { throw IllegalStateException("sensitive response body") }
|
||||||
val second = RecordingTranslationProvider("second") { TranslateResult(emptyList()) }
|
val second = RecordingTranslationProvider(AWS) { TranslateResult(emptyList()) }
|
||||||
|
|
||||||
val error = assertThrows(IllegalStateException::class.java) {
|
val error = assertThrows(IllegalStateException::class.java) {
|
||||||
coordinator(listOf(first, second)).translate(request)
|
coordinator(listOf(first, second)).translate(request)
|
||||||
@@ -66,7 +68,7 @@ class TranslationCoordinatorTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("빈 목록과 공백과 동일 언어와 미지원 언어는 공급자 호출 전에 거부한다")
|
@DisplayName("빈 목록과 공백과 동일 언어와 미지원 언어는 공급자 호출 전에 거부한다")
|
||||||
fun shouldRejectInvalidRequestsBeforeCallingProviders() {
|
fun shouldRejectInvalidRequestsBeforeCallingProviders() {
|
||||||
val provider = RecordingTranslationProvider("first")
|
val provider = RecordingTranslationProvider(PAPAGO)
|
||||||
val coordinator = coordinator(listOf(provider))
|
val coordinator = coordinator(listOf(provider))
|
||||||
listOf(
|
listOf(
|
||||||
request.copy(texts = emptyList()),
|
request.copy(texts = emptyList()),
|
||||||
@@ -85,17 +87,17 @@ class TranslationCoordinatorTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("모든 설정 공급자 캐시를 호출보다 먼저 조회하고 순서상 첫 유효 캐시를 선택한다")
|
@DisplayName("모든 설정 공급자 캐시를 호출보다 먼저 조회하고 순서상 첫 유효 캐시를 선택한다")
|
||||||
fun shouldFindSecondaryCacheAndPreferConfiguredFirstValidCache() {
|
fun shouldFindSecondaryCacheAndPreferConfiguredFirstValidCache() {
|
||||||
val first = RecordingTranslationProvider("first")
|
val first = RecordingTranslationProvider(PAPAGO)
|
||||||
val second = RecordingTranslationProvider("second")
|
val second = RecordingTranslationProvider(AWS)
|
||||||
val coordinator = coordinator(listOf(first, second))
|
val coordinator = coordinator(listOf(first, second))
|
||||||
val secondary = memory("second", "cached secondary")
|
val secondary = memory("aws", "cached secondary")
|
||||||
givenMemory("second", secondary)
|
givenMemory("aws", secondary)
|
||||||
|
|
||||||
assertSame(secondary, coordinator.findCached("hash", "ko", "en"))
|
assertSame(secondary, coordinator.findCached("hash", "ko", "en"))
|
||||||
givenMemory("first", memory("first", " "))
|
givenMemory("papago", memory("papago", " "))
|
||||||
assertSame(secondary, coordinator.findCached("hash", "ko", "en"))
|
assertSame(secondary, coordinator.findCached("hash", "ko", "en"))
|
||||||
val primary = memory("first", "cached primary")
|
val primary = memory("papago", "cached primary")
|
||||||
givenMemory("first", primary)
|
givenMemory("papago", primary)
|
||||||
assertSame(primary, coordinator.findCached("hash", "ko", "en"))
|
assertSame(primary, coordinator.findCached("hash", "ko", "en"))
|
||||||
assertEquals(emptyList<TranslateRequest>(), first.requests + second.requests)
|
assertEquals(emptyList<TranslateRequest>(), first.requests + second.requests)
|
||||||
}
|
}
|
||||||
@@ -103,14 +105,14 @@ class TranslationCoordinatorTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("캐시 저장소 오류는 공급자 실패로 숨기지 않는다")
|
@DisplayName("캐시 저장소 오류는 공급자 실패로 숨기지 않는다")
|
||||||
fun shouldPropagateCacheRepositoryFailure() {
|
fun shouldPropagateCacheRepositoryFailure() {
|
||||||
val provider = RecordingTranslationProvider("first")
|
val provider = RecordingTranslationProvider(PAPAGO)
|
||||||
val failure = IllegalStateException("database unavailable")
|
val failure = IllegalStateException("database unavailable")
|
||||||
Mockito.`when`(
|
Mockito.`when`(
|
||||||
repository.findBySourceHashAndSourceLanguageAndTargetLanguageAndProviderAndNormalizationVersion(
|
repository.findBySourceHashAndSourceLanguageAndTargetLanguageAndProviderAndNormalizationVersion(
|
||||||
"hash",
|
"hash",
|
||||||
"ko",
|
"ko",
|
||||||
"en",
|
"en",
|
||||||
"first",
|
"papago",
|
||||||
"v1"
|
"v1"
|
||||||
)
|
)
|
||||||
).thenThrow(failure)
|
).thenThrow(failure)
|
||||||
@@ -124,8 +126,8 @@ class TranslationCoordinatorTest {
|
|||||||
|
|
||||||
private fun coordinator(
|
private fun coordinator(
|
||||||
providers: List<TranslationProvider>,
|
providers: List<TranslationProvider>,
|
||||||
order: List<String> = providers.map { it.providerName }
|
order: List<LanguageProviderId> = providers.map { it.providerId }
|
||||||
) = TranslationCoordinator(providers, LanguageProviderProperties(listOf("papago"), order), repository)
|
) = TranslationCoordinator(providers, LanguageProviderProperties(listOf(PAPAGO), order), repository)
|
||||||
|
|
||||||
private fun memory(provider: String, text: String) = TranslationMemory(
|
private fun memory(provider: String, text: String) = TranslationMemory(
|
||||||
"hash",
|
"hash",
|
||||||
@@ -151,12 +153,12 @@ class TranslationCoordinatorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class RecordingTranslationProvider(
|
class RecordingTranslationProvider(
|
||||||
override val providerName: String,
|
override val providerId: LanguageProviderId,
|
||||||
private val response: (TranslateRequest) -> TranslateResult = { request ->
|
private val response: (TranslateRequest) -> TranslateResult = { request ->
|
||||||
TranslateResult(request.texts.indices.map { "$providerName:$it" })
|
TranslateResult(request.texts.indices.map { "$providerId:$it" })
|
||||||
}
|
}
|
||||||
) : TranslationProvider {
|
) : TranslationProvider {
|
||||||
override val providerVersion = "$providerName-v1"
|
override val providerVersion = "$providerId-v1"
|
||||||
val requests = mutableListOf<TranslateRequest>()
|
val requests = mutableListOf<TranslateRequest>()
|
||||||
|
|
||||||
override fun translate(request: TranslateRequest): TranslateResult {
|
override fun translate(request: TranslateRequest): TranslateResult {
|
||||||
|
|||||||
+2
-2
@@ -236,7 +236,7 @@ class TranslationJobWorkerTest {
|
|||||||
|
|
||||||
private fun successfulProvider(): TranslationProvider {
|
private fun successfulProvider(): TranslationProvider {
|
||||||
return object : TranslationProvider {
|
return object : TranslationProvider {
|
||||||
override val providerName: String = "papago"
|
override val providerId: LanguageProviderId = LanguageProviderId.PAPAGO
|
||||||
override val providerVersion: String = "nmt-v1"
|
override val providerVersion: String = "nmt-v1"
|
||||||
|
|
||||||
override fun translate(request: TranslateRequest): TranslateResult {
|
override fun translate(request: TranslateRequest): TranslateResult {
|
||||||
@@ -247,7 +247,7 @@ class TranslationJobWorkerTest {
|
|||||||
|
|
||||||
private fun failingProvider(): TranslationProvider {
|
private fun failingProvider(): TranslationProvider {
|
||||||
return object : TranslationProvider {
|
return object : TranslationProvider {
|
||||||
override val providerName: String = "papago"
|
override val providerId: LanguageProviderId = LanguageProviderId.PAPAGO
|
||||||
override val providerVersion: String = "nmt-v1"
|
override val providerVersion: String = "nmt-v1"
|
||||||
|
|
||||||
override fun translate(request: TranslateRequest): TranslateResult {
|
override fun translate(request: TranslateRequest): TranslateResult {
|
||||||
|
|||||||
Reference in New Issue
Block a user