From 295349e78b585c6610463289e55c64538ca6caf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brage=20Skj=C3=B8nborg?= Date: Sat, 3 Jan 2026 08:55:45 +0100 Subject: [PATCH] Version + more tests --- apps/converter/build.gradle.kts | 3 +- .../converter/ConverterEnvironment.kt | 10 + .../converter/ExportAdapter.kt | 14 + .../mediaprocessing/converter/Exporter.kt | 11 + .../converter/convert/Converter.kt | 18 ++ .../converter/convert/Converter2.kt | 47 ++-- .../listeners/ConvertTaskListener.kt | 54 +++- .../converter/MockConverter.kt | 38 +++ .../converter/MockConverterEnvironment.kt | 19 ++ .../converter/convert/Converter2Test.kt | 249 ++++++++++++++++++ .../listeners/ConvertTaskListenerTest.kt | 240 ++++++++++++++++- apps/coordinator/build.gradle.kts | 2 +- .../MigrateContentToStoreTaskListener.kt | 7 +- .../mediaprocessing/MockFileSystemService.kt | 3 + .../MigrateContentToStoreTaskListenerTest.kt | 134 ++++++++++ apps/processer/build.gradle.kts | 2 +- shared/common/build.gradle.kts | 2 +- 17 files changed, 806 insertions(+), 47 deletions(-) create mode 100644 apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/ConverterEnvironment.kt create mode 100644 apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/ExportAdapter.kt create mode 100644 apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/Exporter.kt create mode 100644 apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/convert/Converter.kt create mode 100644 apps/converter/src/test/kotlin/no/iktdev/mediaprocessing/converter/MockConverter.kt create mode 100644 apps/converter/src/test/kotlin/no/iktdev/mediaprocessing/converter/MockConverterEnvironment.kt create mode 100644 apps/converter/src/test/kotlin/no/iktdev/mediaprocessing/converter/convert/Converter2Test.kt diff --git a/apps/converter/build.gradle.kts b/apps/converter/build.gradle.kts index 2c03b8aa..8ab939c7 100644 --- a/apps/converter/build.gradle.kts +++ b/apps/converter/build.gradle.kts @@ -39,7 +39,7 @@ dependencies { implementation("no.iktdev:exfl:0.0.16-SNAPSHOT") implementation("no.iktdev.library:subtitle:1.8.1-SNAPSHOT") - implementation("no.iktdev:eventi:1.0-rc15") + implementation("no.iktdev:eventi:1.0-rc17") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") @@ -53,6 +53,7 @@ dependencies { testImplementation("io.mockk:mockk:1.12.0") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation(project(":shared:common", configuration = "testArtifacts")) + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2") } diff --git a/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/ConverterEnvironment.kt b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/ConverterEnvironment.kt new file mode 100644 index 00000000..99f7dc06 --- /dev/null +++ b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/ConverterEnvironment.kt @@ -0,0 +1,10 @@ +package no.iktdev.mediaprocessing.converter + +import no.iktdev.library.subtitle.reader.BaseReader +import java.io.File + +interface ConverterEnvironment { + fun canRead(file: File): Boolean + fun getReader(file: File): BaseReader? + fun createExporter(input: File, outputDir: File, name: String): Exporter +} diff --git a/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/ExportAdapter.kt b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/ExportAdapter.kt new file mode 100644 index 00000000..2e745708 --- /dev/null +++ b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/ExportAdapter.kt @@ -0,0 +1,14 @@ +package no.iktdev.mediaprocessing.converter + +import no.iktdev.library.subtitle.classes.Dialog +import no.iktdev.library.subtitle.export.Export + +class ExportAdapter( + private val export: Export +) : Exporter { + + override fun write(dialogs: List) = export.write(dialogs) + override fun writeSrt(dialogs: List) = export.writeSrt(dialogs) + override fun writeSmi(dialogs: List) = export.writeSmi(dialogs) + override fun writeVtt(dialogs: List) = export.writeVtt(dialogs) +} diff --git a/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/Exporter.kt b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/Exporter.kt new file mode 100644 index 00000000..872bec3e --- /dev/null +++ b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/Exporter.kt @@ -0,0 +1,11 @@ +package no.iktdev.mediaprocessing.converter + +import no.iktdev.library.subtitle.classes.Dialog +import java.io.File + +interface Exporter { + fun write(dialogs: List): MutableList + fun writeSrt(dialogs: List): File + fun writeSmi(dialogs: List): File + fun writeVtt(dialogs: List): File +} diff --git a/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/convert/Converter.kt b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/convert/Converter.kt new file mode 100644 index 00000000..ad8b271a --- /dev/null +++ b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/convert/Converter.kt @@ -0,0 +1,18 @@ +package no.iktdev.mediaprocessing.converter.convert + +import no.iktdev.library.subtitle.reader.BaseReader +import no.iktdev.mediaprocessing.converter.ConverterEnvironment +import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.ConvertTask +import java.io.File + +abstract class Converter(val env: ConverterEnvironment, val listener: ConvertListener) { + var writtenUris: List? = null + + abstract fun getSubtitleReader(useFile: File): BaseReader? + abstract suspend fun convert(data: ConvertTask.Data) + + class FileIsNullOrEmpty(override val message: String? = "File read is null or empty"): RuntimeException() + class FileUnavailableException(override val message: String): RuntimeException() + + abstract fun getResult(): List +} \ No newline at end of file diff --git a/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/convert/Converter2.kt b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/convert/Converter2.kt index e6a98610..9e2cec4a 100644 --- a/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/convert/Converter2.kt +++ b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/convert/Converter2.kt @@ -4,51 +4,43 @@ import no.iktdev.library.subtitle.Configuration import no.iktdev.library.subtitle.Syncro import no.iktdev.library.subtitle.classes.Dialog import no.iktdev.library.subtitle.classes.DialogType -import no.iktdev.library.subtitle.export.Export import no.iktdev.library.subtitle.reader.BaseReader -import no.iktdev.library.subtitle.reader.Reader import no.iktdev.mediaprocessing.converter.ConverterEnv +import no.iktdev.mediaprocessing.converter.ConverterEnvironment import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.ConvertTask import no.iktdev.mediaprocessing.shared.common.model.SubtitleFormat import java.io.File -class Converter2(val data: ConvertTask.Data, - private val listener: ConvertListener) { +class Converter2( + env: ConverterEnvironment, + listener: ConvertListener +): Converter(env = env, listener = listener) { @Throws(FileUnavailableException::class) - private fun getReader(): BaseReader? { - val file = File(data.inputFile) - if (!file.canRead()) + override fun getSubtitleReader(useFile: File): BaseReader? { + if (!env.canRead(useFile)) { throw FileUnavailableException("Can't open file for reading..") - return Reader(file).getSubtitleReader() + } + return env.getReader(useFile) } private fun syncDialogs(input: List): List { return if (ConverterEnv.syncDialogs) Syncro().sync(input) else input } - fun canRead(): Boolean { - try { - val reader = getReader() - return reader != null - } catch (e: FileUnavailableException) { - return false - } - } - @Throws(FileUnavailableException::class, FileIsNullOrEmpty::class) - fun execute() { + override suspend fun convert(data: ConvertTask.Data) { val file = File(data.inputFile) listener.onStarted(file.absolutePath) try { Configuration.exportJson = true - val read = getReader()?.read() ?: throw FileIsNullOrEmpty() + val read = getSubtitleReader(file)?.read() ?: throw FileIsNullOrEmpty() if (read.isEmpty()) throw FileIsNullOrEmpty() val filtered = read.filter { !it.ignore && it.type !in listOf(DialogType.SIGN_SONG, DialogType.CAPTION) } val syncOrNotSync = syncDialogs(filtered) - val exporter = Export(file, File(data.outputDirectory), data.outputFileName) + val exporter = env.createExporter(file, File(data.outputDirectory), data.outputFileName) val outFiles = if (data.formats.isEmpty()) { exporter.write(syncOrNotSync) @@ -65,22 +57,17 @@ class Converter2(val data: ConvertTask.Data, } exported } - result = outFiles.map { it.absolutePath } - listener.onCompleted(file.absolutePath, result!!) + writtenUris = outFiles.map { it.absolutePath } + listener.onCompleted(file.absolutePath, writtenUris!!) } catch (e: Exception) { listener.onError(file.absolutePath, e.message ?: e.localizedMessage) } } - private var result: List? = null - fun getResult(): List { - if (result == null) { + override fun getResult(): List { + if (writtenUris == null) { throw IllegalStateException("Execute must be called before getting result") } - return result!! + return writtenUris!! } - - - class FileIsNullOrEmpty(override val message: String? = "File read is null or empty"): RuntimeException() - class FileUnavailableException(override val message: String): RuntimeException() } \ No newline at end of file diff --git a/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/listeners/ConvertTaskListener.kt b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/listeners/ConvertTaskListener.kt index ebd9babd..b4e040b2 100644 --- a/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/listeners/ConvertTaskListener.kt +++ b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/listeners/ConvertTaskListener.kt @@ -1,19 +1,29 @@ package no.iktdev.mediaprocessing.converter.listeners +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import no.iktdev.eventi.models.Event import no.iktdev.eventi.models.Task import no.iktdev.eventi.models.store.TaskStatus import no.iktdev.eventi.tasks.TaskListener import no.iktdev.eventi.tasks.TaskType +import no.iktdev.library.subtitle.export.Export +import no.iktdev.library.subtitle.reader.BaseReader +import no.iktdev.library.subtitle.reader.Reader +import no.iktdev.mediaprocessing.converter.ConverterEnvironment +import no.iktdev.mediaprocessing.converter.ExportAdapter +import no.iktdev.mediaprocessing.converter.Exporter import no.iktdev.mediaprocessing.converter.convert.ConvertListener +import no.iktdev.mediaprocessing.converter.convert.Converter import no.iktdev.mediaprocessing.converter.convert.Converter2 import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ConvertTaskResultEvent import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.ConvertTask import org.springframework.stereotype.Component +import java.io.File import java.util.* @Component -class ConvertTaskListener: TaskListener(TaskType.CPU_INTENSIVE) { +open class ConvertTaskListener(): TaskListener(TaskType.CPU_INTENSIVE) { override fun getWorkerId(): String { return "${this::class.java.simpleName}-${TaskType.CPU_INTENSIVE}-${UUID.randomUUID()}" @@ -27,18 +37,15 @@ class ConvertTaskListener: TaskListener(TaskType.CPU_INTENSIVE) { if (task !is ConvertTask) { throw IllegalArgumentException("Invalid task type: ${task::class.java.name}") } - val converter = Converter2(task.data, object: ConvertListener { - override fun onStarted(inputFile: String) { - } - override fun onCompleted(inputFile: String, outputFiles: List) { - } - }) + val converter = getConverter() withHeartbeatRunner { reporter?.updateLastSeen(task.taskId) } - converter.execute() + withContext(Dispatchers.Unconfined) { + converter.convert(task.data) + } return try { val result = converter.getResult() @@ -59,9 +66,38 @@ class ConvertTaskListener: TaskListener(TaskType.CPU_INTENSIVE) { ).producedFrom(task) newEvent } + } + open fun getConverter(): Converter { + return Converter2(getConverterEnvironment(), getListener()) + } + + open fun getListener(): ConvertListener { + return DefaultConvertListener() + } + + class DefaultConvertListener: ConvertListener { + override fun onStarted(inputFile: String) { + } + + override fun onCompleted(inputFile: String, outputFiles: List) { + } + } + + open fun getConverterEnvironment(): ConverterEnvironment { + return DefaultConverterEnvironment() + } + + class DefaultConverterEnvironment : ConverterEnvironment { + override fun canRead(file: File) = file.canRead() + + override fun getReader(file: File): BaseReader? = + Reader(file).getSubtitleReader() + + override fun createExporter(input: File, outputDir: File, name: String): Exporter { + return ExportAdapter(Export(input, outputDir, name)) + } } - } \ No newline at end of file diff --git a/apps/converter/src/test/kotlin/no/iktdev/mediaprocessing/converter/MockConverter.kt b/apps/converter/src/test/kotlin/no/iktdev/mediaprocessing/converter/MockConverter.kt new file mode 100644 index 00000000..885c20f4 --- /dev/null +++ b/apps/converter/src/test/kotlin/no/iktdev/mediaprocessing/converter/MockConverter.kt @@ -0,0 +1,38 @@ +package no.iktdev.mediaprocessing.converter + +import kotlinx.coroutines.delay +import no.iktdev.eventi.models.store.TaskStatus +import no.iktdev.library.subtitle.reader.BaseReader +import no.iktdev.mediaprocessing.converter.convert.ConvertListener +import no.iktdev.mediaprocessing.converter.convert.Converter +import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.ConvertTask +import java.io.File + +class MockConverter( + val delayMillis: Long = 0, + private val simulatedResult: List? = null, + private val taskResultStatus: TaskStatus = TaskStatus.Completed, + private val throwException: Boolean = false, + val mockEnv: ConverterEnvironment = MockConverterEnvironment(), + listener: ConvertListener +) : Converter(env = mockEnv, listener = listener) { + + override fun getSubtitleReader(useFile: File): BaseReader? { + TODO("Not yet implemented") + } + + override suspend fun convert(data: ConvertTask.Data) { + if (delayMillis > 0) delay(delayMillis) + if (throwException) throw RuntimeException("Simulated convert failure") + + if (taskResultStatus == TaskStatus.Failed) { + listener.onError(data.inputFile, "Failed state desired") + } else { + listener.onCompleted(data.inputFile, simulatedResult!!) + } + } + + override fun getResult(): List { + return simulatedResult!! + } +} diff --git a/apps/converter/src/test/kotlin/no/iktdev/mediaprocessing/converter/MockConverterEnvironment.kt b/apps/converter/src/test/kotlin/no/iktdev/mediaprocessing/converter/MockConverterEnvironment.kt new file mode 100644 index 00000000..39eaa431 --- /dev/null +++ b/apps/converter/src/test/kotlin/no/iktdev/mediaprocessing/converter/MockConverterEnvironment.kt @@ -0,0 +1,19 @@ +package no.iktdev.mediaprocessing.converter + +import no.iktdev.library.subtitle.reader.BaseReader +import java.io.File + +class MockConverterEnvironment( + var canReadValue: Boolean = true, + var reader: BaseReader? = null, + var exporter: Exporter? = null +) : ConverterEnvironment { + + override fun canRead(file: File): Boolean = canReadValue + + override fun getReader(file: File): BaseReader? = reader + + override fun createExporter(input: File, outputDir: File, name: String): Exporter { + return exporter ?: error("FakeEnv.exporter must be set before calling createExporter") + } +} \ No newline at end of file diff --git a/apps/converter/src/test/kotlin/no/iktdev/mediaprocessing/converter/convert/Converter2Test.kt b/apps/converter/src/test/kotlin/no/iktdev/mediaprocessing/converter/convert/Converter2Test.kt new file mode 100644 index 00000000..aba6e78a --- /dev/null +++ b/apps/converter/src/test/kotlin/no/iktdev/mediaprocessing/converter/convert/Converter2Test.kt @@ -0,0 +1,249 @@ +package no.iktdev.mediaprocessing.converter.convert + +import kotlinx.coroutines.test.runTest +import no.iktdev.library.subtitle.classes.Dialog +import no.iktdev.library.subtitle.classes.DialogType +import no.iktdev.library.subtitle.classes.Time +import no.iktdev.library.subtitle.reader.BaseReader +import no.iktdev.mediaprocessing.converter.ConverterEnvironment +import no.iktdev.mediaprocessing.converter.Exporter +import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.ConvertTask +import no.iktdev.mediaprocessing.shared.common.model.SubtitleFormat +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.io.File + +class Converter2Test { + + // ------------------------------------------------------------------------- + // Fake implementations + // ------------------------------------------------------------------------- + + class FakeReader( + private val dialogs: List, + private val shouldThrow: Boolean = false + ) : BaseReader() { + override fun read(): List { + if (shouldThrow) throw RuntimeException("reader failed") + return dialogs + } + } + + class FakeExporter( + private val srtFile: File? = null, + private val smiFile: File? = null, + private val vttFile: File? = null, + private val filesForWrite: List = emptyList(), + private val shouldThrow: Boolean = false + ) : Exporter { + + override fun write(dialogs: List): MutableList { + if (shouldThrow) throw RuntimeException("export failed") + return filesForWrite.toMutableList() + } + + override fun writeSrt(dialogs: List): File = + srtFile ?: error("srtFile not set in FakeExporter") + + override fun writeSmi(dialogs: List): File = + smiFile ?: error("smiFile not set in FakeExporter") + + override fun writeVtt(dialogs: List): File = + vttFile ?: error("vttFile not set in FakeExporter") + } + + + class FakeListener : ConvertListener { + var started = 0 + var completed = 0 + var errors = 0 + var lastError: String? = null + + override fun onStarted(inputFile: String) { + started++ + } + + override fun onCompleted(inputFile: String, outputFiles: List) { + completed++ + } + + override fun onError(inputFile: String, message: String) { + errors++ + lastError = message + } + } + + class FakeEnv( + var canReadValue: Boolean = true, + var reader: BaseReader? = null, + var exporter: Exporter? = null + ) : ConverterEnvironment { + + override fun canRead(file: File): Boolean = canReadValue + + override fun getReader(file: File): BaseReader? = reader + + override fun createExporter(input: File, outputDir: File, name: String): Exporter { + return exporter ?: error("FakeEnv.exporter must be set before calling createExporter") + } + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private fun makeTaskData( + input: String = "input.srt", + language: String = "no", + outDir: String = "out", + outName: String = "name", + formats: List = emptyList(), + allowOverwrite: Boolean = true + ) = ConvertTask.Data( + inputFile = input, + language = language, + outputDirectory = outDir, + outputFileName = outName, + formats = formats, + allowOverwrite = allowOverwrite + ) + + // ------------------------------------------------------------------------- + // Tests + // ------------------------------------------------------------------------- + + + @Test + @DisplayName( + """ + Når execute() kjøres + Hvis reader returnerer tom liste + Så kalles onError + """ + ) + fun execute_emptyFile() = runTest { + val env = FakeEnv( + canReadValue = true, + reader = FakeReader(emptyList()) + ) + val listener = FakeListener() + + val converter = Converter2( + env = env, + listener = listener + ) + + converter.convert(makeTaskData()) + + assertEquals(1, listener.errors) + assertEquals(0, listener.completed) + } + + @Test + @DisplayName( + """ + Når execute() kjøres + Hvis reader returnerer dialoger + Og exporter.write lykkes + Så kalles onCompleted + """ + ) + fun execute_success() = runTest { + val dialogs = listOf( + Dialog("0", Time(0, 0, 0, 0), Time(0, 0, 1000, 0), "Hello", DialogType.DIALOG_NORMAL) + ) + + val env = FakeEnv( + canReadValue = true, + reader = FakeReader(dialogs), + exporter = FakeExporter(srtFile = File("/fake/out.srt")) + ) + + val listener = FakeListener() + + val converter = Converter2( + env = env, + listener = listener + ) + + converter.convert( + makeTaskData( + formats = listOf(SubtitleFormat.SRT) + ) + ) + + assertEquals(1, listener.completed) + assertEquals(listOf("/fake/out.srt"), converter.getResult()) + } + + @Test + @DisplayName( + """ + Når execute() kjøres + Hvis exporter.write kaster exception + Så kalles onError + """ + ) + fun execute_exportFails() = runTest { + val dialogs = listOf( + Dialog("0", Time(0, 0, 0, 0), Time(0, 0, 1000, 0), "Hello", DialogType.DIALOG_NORMAL) + ) + + val env = FakeEnv( + canReadValue = true, + reader = FakeReader(dialogs), + exporter = FakeExporter(srtFile = File("/fake/out.srt"), shouldThrow = true) + ) + + val listener = FakeListener() + + val converter = Converter2( + env = env, + listener = listener + ) + + converter.convert(makeTaskData()) + + assertEquals(1, listener.errors) + assertEquals(0, listener.completed) + } + + @Test + @DisplayName( + """ + Når execute() kjøres + Hvis formats inneholder SRT og VTT + Så brukes writeSrt og writeVtt + """ + ) + fun execute_multipleFormats() = runTest { + val dialogs = listOf( + Dialog("0", Time(0, 0, 0, 0), Time(0, 0, 1000, 0), "Hello", DialogType.DIALOG_NORMAL) + ) + + + val env = FakeEnv( + canReadValue = true, + reader = FakeReader(dialogs), + exporter = FakeExporter(srtFile = File("/fake/out.srt"), vttFile = File("/fake/out.vtt")) + ) + + val listener = FakeListener() + + val converter = Converter2( + env = env, + listener = listener + ) + + converter.convert( + makeTaskData( + formats = listOf(SubtitleFormat.SRT, SubtitleFormat.VTT) + ) + ) + + assertEquals(1, listener.completed) + assertEquals(listOf("/fake/out.srt", "/fake/out.vtt"), converter.getResult()) + } +} + diff --git a/apps/converter/src/test/kotlin/no/iktdev/mediaprocessing/converter/listeners/ConvertTaskListenerTest.kt b/apps/converter/src/test/kotlin/no/iktdev/mediaprocessing/converter/listeners/ConvertTaskListenerTest.kt index 65e8fe67..96a622c7 100644 --- a/apps/converter/src/test/kotlin/no/iktdev/mediaprocessing/converter/listeners/ConvertTaskListenerTest.kt +++ b/apps/converter/src/test/kotlin/no/iktdev/mediaprocessing/converter/listeners/ConvertTaskListenerTest.kt @@ -1,7 +1,245 @@ package no.iktdev.mediaprocessing.converter.listeners +import kotlinx.coroutines.test.runTest +import no.iktdev.eventi.models.Event +import no.iktdev.eventi.models.Task +import no.iktdev.eventi.models.store.TaskStatus +import no.iktdev.eventi.tasks.TaskReporter +import no.iktdev.library.subtitle.classes.Dialog +import no.iktdev.library.subtitle.classes.DialogType +import no.iktdev.library.subtitle.classes.Time +import no.iktdev.library.subtitle.reader.BaseReader +import no.iktdev.mediaprocessing.converter.ConverterEnvironment +import no.iktdev.mediaprocessing.converter.Exporter +import no.iktdev.mediaprocessing.converter.MockConverter +import no.iktdev.mediaprocessing.converter.MockConverterEnvironment +import no.iktdev.mediaprocessing.converter.convert.ConvertListener +import no.iktdev.mediaprocessing.converter.convert.Converter +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ConvertTaskResultEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.ConvertTask +import no.iktdev.mediaprocessing.shared.common.model.SubtitleFormat +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.io.File +import java.util.* +import kotlin.system.measureTimeMillis + class ConvertTaskListenerTest { + class ConvertTaskListenerTestImplementation : ConvertTaskListener() { + fun getJob() = currentJob -} \ No newline at end of file + var overrideEnv: ConverterEnvironment = DefaultConverterEnvironment() + override fun getConverterEnvironment(): ConverterEnvironment { + return overrideEnv + } + + var overrideListener: ConvertListener? = null + override fun getListener(): ConvertListener { + if (overrideListener != null) + return overrideListener!! + else + return super.getListener() + } + + var overrideConverter: Converter? = null + override fun getConverter(): Converter { + return if (overrideConverter != null) + overrideConverter!! + else + super.getConverter() + + } + } + + val listener = ConvertTaskListenerTestImplementation() + + // --------------------------------------------------------------------- + // Fake environment + fake converter + // --------------------------------------------------------------------- + + class FakeListener : ConvertListener { + var started = 0 + var completed = 0 + var errors = 0 + + override fun onStarted(inputFile: String) { + started++ + } + + override fun onCompleted(inputFile: String, outputFiles: List) { + completed++ + } + + override fun onError(inputFile: String, message: String) { + errors++ + } + } + + class FakeExporter( + private val files: List, + private val shouldThrow: Boolean = false + ) : Exporter { + + override fun write(dialogs: List): MutableList { + if (shouldThrow) throw RuntimeException("export failed") + return files.toMutableList() + } + + override fun writeSrt(dialogs: List) = files[0] + override fun writeSmi(dialogs: List) = files[0] + override fun writeVtt(dialogs: List) = files[0] + } + + class FakeReader( + private val dialogs: List, + private val shouldThrow: Boolean = false + ) : BaseReader() { + override fun read(): List { + if (shouldThrow) throw RuntimeException("reader failed") + return dialogs + } + } + + + val overrideReporter = object : TaskReporter { + override fun markClaimed(taskId: UUID, workerId: String) {} + override fun updateLastSeen(taskId: UUID) {} + override fun markConsumed(taskId: UUID) {} + override fun updateProgress(taskId: UUID, progress: Int) {} + override fun log(taskId: UUID, message: String) {} + override fun publishEvent(event: Event) { + + } + } + + // --------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------- + + private fun makeTask( + formats: List = emptyList() + ): ConvertTask { + return ConvertTask( + ConvertTask.Data( + inputFile = "input.srt", + language = "no", + outputDirectory = "out", + outputFileName = "name", + formats = formats, + allowOverwrite = true + ) + ).apply { newReferenceId() } + } + + // --------------------------------------------------------------------- + // Tests + // --------------------------------------------------------------------- + + @Test + @DisplayName(""" + Når onTask kjøres og converterer + Hvis den bruker lengre tid + Så: + Skal koden vente til den er ferdig + """) + fun onTask_validate_delay() = runTest { + val delay = 1000L + val converter = MockConverter( + delay, + listOf("file:///potato.srt"), + listener = FakeListener() + ) + listener.apply { + overrideConverter = converter + } + val task = makeTask() + val event = listener.onTask(task) + + val time = measureTimeMillis { + val accepted = listener.accept(task, overrideReporter) + assertTrue(accepted, "Task listener did not accept the task.") + listener.getJob()?.join() + assertTrue(event is ConvertTaskResultEvent) + assertEquals(TaskStatus.Completed, (event as ConvertTaskResultEvent).status) + } + } + + + @Test + @DisplayName( + """ + Når onTask kjøres + Hvis converter lykkes + Så returneres Completed-event + """ + ) + fun onTask_success() = runTest { + val dialogs = listOf( + Dialog("0", Time(0, 0, 0, 0), Time(0, 0, 1000, 0), "Hello", DialogType.DIALOG_NORMAL) + ) + + val env = MockConverterEnvironment( + canReadValue = true, + reader = FakeReader(dialogs), + exporter = FakeExporter(listOf(File("/fake/out.srt"))) + ) + + listener.apply { + overrideListener = FakeListener() + overrideEnv = env + } + + val task = makeTask() + + val event = listener.onTask(task) as ConvertTaskResultEvent + + assertEquals(TaskStatus.Completed, event.status) + assertEquals(listOf("/fake/out.srt"), event.data!!.outputFiles) + } + + @Test + @DisplayName( + """ + Når onTask kjøres + Hvis converter feiler + Så returneres Failed-event + """ + ) + fun onTask_failure() = runTest { + val env = MockConverterEnvironment( + canReadValue = true, + reader = FakeReader(emptyList()) // triggers FileIsNullOrEmpty + ) + + listener.apply { + overrideListener = FakeListener() + overrideEnv = env + } + + val task = makeTask() + + val event = listener.onTask(task) as ConvertTaskResultEvent + + assertEquals(TaskStatus.Failed, event.status) + assertNull(event.data) + } + + @Test + @DisplayName( + """ + Når supports() kalles + Så returnerer den true for ConvertTask og false for andre typer + """ + ) + fun supports_test() { + val listener = ConvertTaskListener() + + assertTrue(listener.supports(makeTask())) + assertFalse(listener.supports(DummyTask())) + } + + class DummyTask : Task() +} diff --git a/apps/coordinator/build.gradle.kts b/apps/coordinator/build.gradle.kts index d5529f86..9e54d2ad 100644 --- a/apps/coordinator/build.gradle.kts +++ b/apps/coordinator/build.gradle.kts @@ -39,7 +39,7 @@ dependencies { implementation("no.iktdev:exfl:0.0.16-SNAPSHOT") implementation("no.iktdev.streamit.library:streamit-library-db:1.0.0-alpha14") - implementation("no.iktdev:eventi:1.0-rc16") + implementation("no.iktdev:eventi:1.0-rc17") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/tasks/MigrateContentToStoreTaskListener.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/tasks/MigrateContentToStoreTaskListener.kt index dab59ed7..603220df 100644 --- a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/tasks/MigrateContentToStoreTaskListener.kt +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/tasks/MigrateContentToStoreTaskListener.kt @@ -9,6 +9,7 @@ import no.iktdev.mediaprocessing.coordinator.util.FileSystemService import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MigrateContentToStoreTaskResultEvent import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.MigrateToContentStoreTask import no.iktdev.mediaprocessing.shared.common.model.MigrateStatus +import no.iktdev.mediaprocessing.shared.common.silentTry import org.jetbrains.annotations.VisibleForTesting import org.springframework.stereotype.Component import java.io.File @@ -40,13 +41,13 @@ class MigrateContentToStoreTaskListener: TaskListener(TaskType.IO_INTENSIVE) { coverStatus.none { it.status == MigrateStatus.Failed }) { pickedTask.data.videoContent?.cachedUri?.let { File(it) }?.let { - fs.delete(it) + silentTry { fs.delete(it) } } pickedTask.data.subtitleContent?.map { File(it.cachedUri) }?.forEach { - fs.delete(it) + silentTry { fs.delete(it) } } pickedTask.data.coverContent?.map { File(it.cachedUri) }?.forEach { - fs.delete(it) + silentTry { fs.delete(it) } } } else { status = TaskStatus.Failed diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/MockFileSystemService.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/MockFileSystemService.kt index 65fb637d..4fbaa3a4 100644 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/MockFileSystemService.kt +++ b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/MockFileSystemService.kt @@ -5,6 +5,8 @@ import java.io.File class MockFileSystemService : FileSystemService { var copyShouldFail = false + var deleteShouldFail = false + var identical = true val copied = mutableListOf>() val deleted = mutableListOf() @@ -19,6 +21,7 @@ class MockFileSystemService : FileSystemService { } override fun delete(file: File) { + if (deleteShouldFail) throw RuntimeException("delete failed") deleted += file } } \ No newline at end of file diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/tasks/MigrateContentToStoreTaskListenerTest.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/tasks/MigrateContentToStoreTaskListenerTest.kt index 8cc05e13..d33b2923 100644 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/tasks/MigrateContentToStoreTaskListenerTest.kt +++ b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/tasks/MigrateContentToStoreTaskListenerTest.kt @@ -102,6 +102,25 @@ class MigrateContentToStoreTaskListenerTest { assertEquals(MigrateStatus.Failed, result.status) } + @Test + @DisplayName( + """ + Når migrateVideo kjøres + Hvis content er null + Så: + returneres NotPresent + """ + ) + fun migrateVideo_null() { + val fs = MockFileSystemService().also { + listener.fs = it + } + + val result = listener.migrateVideo(fs, null) + + assertEquals(MigrateStatus.NotPresent, result.status) + } + // ------------------------------------------------------------------------- // migrateSubtitle // ------------------------------------------------------------------------- @@ -151,6 +170,36 @@ class MigrateContentToStoreTaskListenerTest { assertEquals(MigrateStatus.Completed, result.first().status) } + @Test + @DisplayName( + """ + Når migrateSubtitle kjøres + Hvis én lykkes og én feiler + Så: + returneres både Completed og Failed + """ + ) + fun migrateSubtitle_mixed() { + val fs = MockFileSystemService().apply { + // first OK, second fails + copyShouldFail = false + }.also { listener.fs = it } + + val subs = listOf( + MigrateToContentStoreTask.Data.SingleSubtitle("en", "/tmp/a", "/tmp/b"), + MigrateToContentStoreTask.Data.SingleSubtitle("no", "/tmp/c", "/tmp/d") + ) + + // simulate second failing + fs.copyShouldFail = false + val result1 = listener.migrateSubtitle(fs, listOf(subs[0])) + fs.copyShouldFail = true + val result2 = listener.migrateSubtitle(fs, listOf(subs[1])) + + assertEquals(MigrateStatus.Completed, result1.first().status) + assertEquals(MigrateStatus.Failed, result2.first().status) + } + // ------------------------------------------------------------------------- // migrateCover // ------------------------------------------------------------------------- @@ -179,6 +228,34 @@ class MigrateContentToStoreTaskListenerTest { assertEquals(MigrateStatus.Completed, result.first().status) } + @Test + @DisplayName( + """ + Når migrateCover kjøres + Hvis flere covers og én feiler + Så: + returneres både Completed og Failed + """ + ) + fun migrateCover_mixed() { + val fs = MockFileSystemService().also { listener.fs = it } + + val covers = listOf( + MigrateToContentStoreTask.Data.SingleContent("/tmp/a", "/tmp/b"), + MigrateToContentStoreTask.Data.SingleContent("/tmp/c", "/tmp/d") + ) + + // first OK, second mismatch + fs.identical = true + val ok = listener.migrateCover(fs, listOf(covers[0])) + + fs.identical = false + val fail = listener.migrateCover(fs, listOf(covers[1])) + + assertEquals(MigrateStatus.Completed, ok.first().status) + assertEquals(MigrateStatus.Failed, fail.first().status) + } + // ------------------------------------------------------------------------- // onTask // ------------------------------------------------------------------------- @@ -244,4 +321,61 @@ class MigrateContentToStoreTaskListenerTest { assertEquals(TaskStatus.Failed, event.status) assertEquals(0, fs.deleted.size) } + + @Test + @DisplayName( + """ + Når onTask kjøres + Hvis video feiler + Så: + returneres Failed og ingenting slettes + """ + ) + fun onTask_videoFails() = runTest { + val fs = MockFileSystemService().apply { copyShouldFail = true } + .also { listener.fs = it } + + val task = MigrateToContentStoreTask( + MigrateToContentStoreTask.Data( + collection = "col", + videoContent = MigrateToContentStoreTask.Data.SingleContent("/tmp/v", "/tmp/v2"), + subtitleContent = emptyList(), + coverContent = emptyList() + ) + ).newReferenceId() + + val event = listener.onTask(task) as MigrateContentToStoreTaskResultEvent + + assertEquals(TaskStatus.Failed, event.status) + assertEquals(0, fs.deleted.size) + } + + @Test + @DisplayName( + """ + Når onTask kjøres + Hvis sletting feiler + Så: + returneres fortsatt Completed + """ + ) + fun onTask_deleteFails() = runTest { + val fs = MockFileSystemService().apply { deleteShouldFail = true } + .also { listener.fs = it } + + + val task = MigrateToContentStoreTask( + MigrateToContentStoreTask.Data( + collection = "col", + videoContent = MigrateToContentStoreTask.Data.SingleContent("/tmp/v", "/tmp/v2"), + subtitleContent = emptyList(), + coverContent = emptyList() + ) + ).newReferenceId() + + val event = listener.onTask(task) as MigrateContentToStoreTaskResultEvent + + assertEquals(TaskStatus.Completed, event.status) + } } + diff --git a/apps/processer/build.gradle.kts b/apps/processer/build.gradle.kts index 63b85529..214af726 100644 --- a/apps/processer/build.gradle.kts +++ b/apps/processer/build.gradle.kts @@ -35,7 +35,7 @@ dependencies { implementation("org.json:json:20210307") implementation("no.iktdev:exfl:0.0.16-SNAPSHOT") - implementation("no.iktdev:eventi:1.0-rc15") + implementation("no.iktdev:eventi:1.0-rc17") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") diff --git a/shared/common/build.gradle.kts b/shared/common/build.gradle.kts index cac2a14e..d74291a3 100644 --- a/shared/common/build.gradle.kts +++ b/shared/common/build.gradle.kts @@ -61,7 +61,7 @@ dependencies { implementation("com.zaxxer:HikariCP:7.0.2") implementation(project(":shared:ffmpeg")) - implementation("no.iktdev:eventi:1.0-rc16") + implementation("no.iktdev:eventi:1.0-rc17") testImplementation(kotlin("test")) testImplementation(platform("org.junit:junit-bom:5.10.0"))