Fixed state reporting and producing

This commit is contained in:
Brage Skjønborg 2026-01-31 20:53:10 +01:00
parent 6dab106f2a
commit ed6bddb95f
22 changed files with 554 additions and 318 deletions

View File

@ -53,11 +53,11 @@ class DefaultTaskReporter() : TaskReporter {
} }
override fun markFailed(taskId: UUID) { override fun markFailed(referenceId: UUID, taskId: UUID) {
TaskStore.markConsumed(taskId, TaskStatus.Failed) TaskStore.markConsumed(taskId, TaskStatus.Failed)
} }
override fun markCancelled(taskId: UUID) { override fun markCancelled(referenceId: UUID, taskId: UUID) {
TaskStore.markConsumed(taskId, TaskStatus.Cancelled) TaskStore.markConsumed(taskId, TaskStatus.Cancelled)
} }

View File

@ -33,6 +33,7 @@ open class ConvertTaskListener(): TaskListener(TaskType.CPU_INTENSIVE) {
return task is ConvertTask return task is ConvertTask
} }
override suspend fun onTask(task: Task): Event? { override suspend fun onTask(task: Task): Event? {
if (task !is ConvertTask) { if (task !is ConvertTask) {
throw IllegalArgumentException("Invalid task type: ${task::class.java.name}") throw IllegalArgumentException("Invalid task type: ${task::class.java.name}")
@ -68,6 +69,19 @@ open class ConvertTaskListener(): TaskListener(TaskType.CPU_INTENSIVE) {
} }
} }
override fun createIncompleteStateTaskEvent(
task: Task,
status: TaskStatus,
exception: Exception?
): Event {
val message = when (status) {
TaskStatus.Failed -> exception?.message ?: "Unknown error, see log"
TaskStatus.Cancelled -> "Canceled"
else -> ""
}
return ConvertTaskResultEvent(null, status, error = message)
}
open fun getConverter(): Converter { open fun getConverter(): Converter {
return Converter2(getConverterEnvironment(), getListener()) return Converter2(getConverterEnvironment(), getListener())
} }

View File

@ -108,8 +108,8 @@ class ConvertTaskListenerTest {
override fun markClaimed(taskId: UUID, workerId: String) {} override fun markClaimed(taskId: UUID, workerId: String) {}
override fun updateLastSeen(taskId: UUID) {} override fun updateLastSeen(taskId: UUID) {}
override fun markCompleted(taskId: UUID) {} override fun markCompleted(taskId: UUID) {}
override fun markFailed(taskId: UUID) {} override fun markFailed(referenceId: UUID, taskId: UUID) {}
override fun markCancelled(taskId: UUID) {} override fun markCancelled(referenceId: UUID, taskId: UUID) {}
override fun updateProgress(taskId: UUID, progress: Int) {} override fun updateProgress(taskId: UUID, progress: Int) {}
override fun log(taskId: UUID, message: String) {} override fun log(taskId: UUID, message: String) {}
override fun publishEvent(event: Event) { override fun publishEvent(event: Event) {

View File

@ -52,11 +52,11 @@ class DefaultTaskReporter() : TaskReporter {
TaskStore.markConsumed(taskId, TaskStatus.Completed) TaskStore.markConsumed(taskId, TaskStatus.Completed)
} }
override fun markFailed(taskId: UUID) { override fun markFailed(referenceId: UUID, taskId: UUID) {
TaskStore.markConsumed(taskId, TaskStatus.Failed) TaskStore.markConsumed(taskId, TaskStatus.Failed)
} }
override fun markCancelled(taskId: UUID) { override fun markCancelled(referenceId: UUID, taskId: UUID) {
TaskStore.markConsumed(taskId, TaskStatus.Cancelled) TaskStore.markConsumed(taskId, TaskStatus.Cancelled)
} }

View File

@ -58,6 +58,19 @@ class DownloadCoverTaskListener(
} }
} }
override fun createIncompleteStateTaskEvent(
task: Task,
status: TaskStatus,
exception: Exception?
): Event {
val message = when (status) {
TaskStatus.Failed -> exception?.message ?: "Unknown error, see log"
TaskStatus.Cancelled -> "Canceled"
else -> ""
}
return CoverDownloadResultEvent(null, status, error = message)
}
open fun getDownloadClient(): DownloadClient { open fun getDownloadClient(): DownloadClient {
return DefaultDownloadClient(coordinatorEnv) return DefaultDownloadClient(coordinatorEnv)
} }

View File

@ -49,6 +49,19 @@ class MediaStreamReadTaskListener(
} }
} }
override fun createIncompleteStateTaskEvent(
task: Task,
status: TaskStatus,
exception: Exception?
): Event {
val message = when (status) {
TaskStatus.Failed -> exception?.message ?: "Unknown error, see log"
TaskStatus.Cancelled -> "Canceled"
else -> ""
}
return CoordinatorReadStreamsResultEvent(null, status, error = message)
}
override fun getFfprobe(): FFprobe { override fun getFfprobe(): FFprobe {
return JsonFfinfo(coordinatorEnv.ffprobe) return JsonFfinfo(coordinatorEnv.ffprobe)
} }

View File

@ -28,61 +28,89 @@ class MigrateContentToStoreTaskListener: TaskListener(TaskType.IO_INTENSIVE) {
override suspend fun onTask(task: Task): Event? { override suspend fun onTask(task: Task): Event? {
val pickedTask = task as? MigrateToContentStoreTask ?: return null val pickedTask = task as? MigrateToContentStoreTask ?: return null
val fs = getFileSystemService() val fs = getFileSystemService()
// Disse vil kaste exceptions hvis noe går galt
val videoStatus = migrateVideo(fs, pickedTask.data.videoContent) val videoStatus = migrateVideo(fs, pickedTask.data.videoContent)
val subtitleStatus = migrateSubtitle(fs, pickedTask.data.subtitleContent ?: emptyList()) val subtitleStatus = migrateSubtitle(fs, pickedTask.data.subtitleContent ?: emptyList())
val coverStatus = migrateCover(fs, pickedTask.data.coverContent ?: emptyList()) val coverStatus = migrateCover(fs, pickedTask.data.coverContent ?: emptyList())
var status = TaskStatus.Completed // Hvis vi kommer hit, har ingen migrering kastet exceptions → alt OK
if (videoStatus.status != MigrateStatus.Failed && deleteCache(fs, pickedTask)
subtitleStatus.none { it.status == MigrateStatus.Failed } &&
coverStatus.none { it.status == MigrateStatus.Failed })
{
pickedTask.data.videoContent?.cachedUri?.let { File(it) }?.let {
silentTry { fs.delete(it) }
}
pickedTask.data.subtitleContent?.map { File(it.cachedUri) }?.forEach {
silentTry { fs.delete(it) }
}
pickedTask.data.coverContent?.map { File(it.cachedUri) }?.forEach {
silentTry { fs.delete(it) }
}
} else {
status = TaskStatus.Failed
}
return MigrateContentToStoreTaskResultEvent(
val completedEvent = MigrateContentToStoreTaskResultEvent( status = TaskStatus.Completed,
status = status, migrateData = MigrateContentToStoreTaskResultEvent.MigrateData(
collection = pickedTask.data.collection, collection = pickedTask.data.collection,
videoMigrate = videoStatus, videoMigrate = videoStatus,
subtitleMigrate = subtitleStatus, subtitleMigrate = subtitleStatus,
coverMigrate = coverStatus coverMigrate = coverStatus
)
).producedFrom(task) ).producedFrom(task)
return completedEvent
} }
@VisibleForTesting
internal fun migrateVideo(fs: FileSystemService, videoContent: MigrateToContentStoreTask.Data.SingleContent?): MigrateContentToStoreTaskResultEvent.FileMigration { override fun createIncompleteStateTaskEvent(
if (videoContent == null) return MigrateContentToStoreTaskResultEvent.FileMigration(null, MigrateStatus.NotPresent) task: Task,
status: TaskStatus,
exception: Exception?
): Event {
val message = when (status) {
TaskStatus.Failed -> exception?.message ?: "Unknown error, see log"
TaskStatus.Cancelled -> "Canceled"
else -> ""
}
return MigrateContentToStoreTaskResultEvent(null, status, error = message)
}
private fun deleteCache(fs: FileSystemService, task: MigrateToContentStoreTask) {
task.data.videoContent?.cachedUri?.let { silentTry { fs.delete(File(it)) } }
task.data.subtitleContent?.forEach { silentTry { fs.delete(File(it.cachedUri)) } }
task.data.coverContent?.forEach { silentTry { fs.delete(File(it.cachedUri)) } }
}
internal fun migrateVideo(
fs: FileSystemService,
videoContent: MigrateToContentStoreTask.Data.SingleContent?
): MigrateContentToStoreTaskResultEvent.FileMigration {
if (videoContent == null) {
return MigrateContentToStoreTaskResultEvent.FileMigration(null, MigrateStatus.NotPresent)
}
val source = File(videoContent.cachedUri) val source = File(videoContent.cachedUri)
val destination = File(videoContent.storeUri) val destination = File(videoContent.storeUri)
return try {
if (!fs.copy(source, destination)) {
return MigrateContentToStoreTaskResultEvent.FileMigration(null, MigrateStatus.Failed)
}
if (!fs.areIdentical(source, destination)) { // 1. Hvis destinasjonen finnes, sjekk identitet
return MigrateContentToStoreTaskResultEvent.FileMigration(null, MigrateStatus.Failed) if (destination.exists()) {
if (fs.areIdentical(source, destination)) {
// Skip allerede migrert
return MigrateContentToStoreTaskResultEvent.FileMigration(
destination.absolutePath,
MigrateStatus.Completed
)
} else {
throw IllegalStateException(
"Destination file already exists but is not identical: $destination"
)
} }
MigrateContentToStoreTaskResultEvent.FileMigration(destination.absolutePath, MigrateStatus.Completed)
} catch (e: Exception) {
MigrateContentToStoreTaskResultEvent.FileMigration(null, MigrateStatus.Failed)
} }
// 2. Utfør kopiering
if (!fs.copy(source, destination)) {
throw IllegalStateException("File could not be copied to: $destination from $source")
}
// 3. Verifiser kopien (optional)
if (!fs.areIdentical(source, destination)) {
throw IllegalStateException("Copied file is not identical to source: $destination")
}
return MigrateContentToStoreTaskResultEvent.FileMigration(
destination.absolutePath,
MigrateStatus.Completed
)
} }
@VisibleForTesting @VisibleForTesting
@ -90,53 +118,116 @@ class MigrateContentToStoreTaskListener: TaskListener(TaskType.IO_INTENSIVE) {
fs: FileSystemService, fs: FileSystemService,
subtitleContents: List<MigrateToContentStoreTask.Data.SingleSubtitle> subtitleContents: List<MigrateToContentStoreTask.Data.SingleSubtitle>
): List<MigrateContentToStoreTaskResultEvent.SubtitleMigration> { ): List<MigrateContentToStoreTaskResultEvent.SubtitleMigration> {
if (subtitleContents.isEmpty()) return listOf(MigrateContentToStoreTaskResultEvent.SubtitleMigration(null, null, MigrateStatus.NotPresent))
val results = mutableListOf<MigrateContentToStoreTaskResultEvent.SubtitleMigration>() if (subtitleContents.isEmpty()) {
for (subtitle in subtitleContents) { return listOf(
MigrateContentToStoreTaskResultEvent.SubtitleMigration(
language = null,
storedUri = null,
status = MigrateStatus.NotPresent
)
)
}
return subtitleContents.map { subtitle ->
val source = File(subtitle.cachedUri) val source = File(subtitle.cachedUri)
val destination = File(subtitle.storeUri) val destination = File(subtitle.storeUri)
try {
if (!fs.copy(source, destination)) {
results.add(MigrateContentToStoreTaskResultEvent.SubtitleMigration(subtitle.language, destination.absolutePath, MigrateStatus.Failed))
continue
}
if (!fs.areIdentical(source, destination)) { // 1. Hvis destinasjonen finnes
results.add(MigrateContentToStoreTaskResultEvent.SubtitleMigration(subtitle.language, destination.absolutePath, MigrateStatus.Failed)) if (destination.exists()) {
if (fs.areIdentical(source, destination)) {
return@map MigrateContentToStoreTaskResultEvent.SubtitleMigration(
subtitle.language,
destination.absolutePath,
MigrateStatus.Completed
)
} else { } else {
results.add(MigrateContentToStoreTaskResultEvent.SubtitleMigration(subtitle.language,destination.absolutePath, MigrateStatus.Completed)) throw IllegalStateException(
"Destination subtitle exists but is not identical: ${destination.absolutePath}"
)
} }
} catch (e: Exception) {
results.add(MigrateContentToStoreTaskResultEvent.SubtitleMigration(subtitle.language,destination.absolutePath, MigrateStatus.Failed))
} }
// 2. Kopier
if (!fs.copy(source, destination)) {
throw IllegalStateException(
"Failed to copy subtitle ${subtitle.language} from $source to $destination"
)
}
// 3. Verifiser
if (!fs.areIdentical(source, destination)) {
throw IllegalStateException(
"Copied subtitle ${subtitle.language} is not identical: ${destination.absolutePath}"
)
}
// 4. OK
MigrateContentToStoreTaskResultEvent.SubtitleMigration(
subtitle.language,
destination.absolutePath,
MigrateStatus.Completed
)
} }
return results
} }
@VisibleForTesting @VisibleForTesting
internal fun migrateCover(fs: FileSystemService, coverContents: List<MigrateToContentStoreTask.Data.SingleContent>): List<MigrateContentToStoreTaskResultEvent.FileMigration> { internal fun migrateCover(
if (coverContents.isEmpty()) return listOf(MigrateContentToStoreTaskResultEvent.FileMigration(null, MigrateStatus.NotPresent)) fs: FileSystemService,
val results = mutableListOf<MigrateContentToStoreTaskResultEvent.FileMigration>() coverContents: List<MigrateToContentStoreTask.Data.SingleContent>
for (cover in coverContents) { ): List<MigrateContentToStoreTaskResultEvent.FileMigration> {
if (coverContents.isEmpty()) {
return listOf(
MigrateContentToStoreTaskResultEvent.FileMigration(
storedUri = null,
status = MigrateStatus.NotPresent
)
)
}
return coverContents.map { cover ->
val source = File(cover.cachedUri) val source = File(cover.cachedUri)
val destination = File(cover.storeUri) val destination = File(cover.storeUri)
try {
if (!fs.copy(source, destination)) { // 1. Hvis destinasjonen finnes
results.add(MigrateContentToStoreTaskResultEvent.FileMigration(destination.absolutePath, MigrateStatus.Failed)) if (destination.exists()) {
continue if (fs.areIdentical(source, destination)) {
} return@map MigrateContentToStoreTaskResultEvent.FileMigration(
if (!fs.areIdentical(source, destination)) { destination.absolutePath,
results.add(MigrateContentToStoreTaskResultEvent.FileMigration(destination.absolutePath, MigrateStatus.Failed)) MigrateStatus.Completed
)
} else { } else {
results.add(MigrateContentToStoreTaskResultEvent.FileMigration(destination.absolutePath, MigrateStatus.Completed)) throw IllegalStateException(
"Destination cover exists but is not identical: ${destination.absolutePath}"
)
} }
} catch (e: Exception) {
results.add(MigrateContentToStoreTaskResultEvent.FileMigration(destination.absolutePath, MigrateStatus.Failed))
} }
// 2. Kopier
if (!fs.copy(source, destination)) {
throw IllegalStateException(
"Failed to copy cover from $source to $destination"
)
}
// 3. Verifiser
if (!fs.areIdentical(source, destination)) {
throw IllegalStateException(
"Copied cover is not identical: ${destination.absolutePath}"
)
}
// 4. OK
MigrateContentToStoreTaskResultEvent.FileMigration(
destination.absolutePath,
MigrateStatus.Completed
)
} }
return results
} }
open fun getFileSystemService(): FileSystemService { open fun getFileSystemService(): FileSystemService {
return DefaultFileSystemService() return DefaultFileSystemService()
} }

View File

@ -6,7 +6,6 @@ import no.iktdev.eventi.models.store.TaskStatus
import no.iktdev.eventi.tasks.TaskListener import no.iktdev.eventi.tasks.TaskListener
import no.iktdev.eventi.tasks.TaskType import no.iktdev.eventi.tasks.TaskType
import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.StoreContentAndMetadataTaskResultEvent import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.StoreContentAndMetadataTaskResultEvent
import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.MigrateToContentStoreTask
import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.StoreContentAndMetadataTask import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.StoreContentAndMetadataTask
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpEntity import org.springframework.http.HttpEntity
@ -15,10 +14,11 @@ import org.springframework.http.HttpMethod
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.web.client.RestTemplate import org.springframework.web.client.RestTemplate
import java.util.UUID import java.util.*
@Component @Component
class StoreContentAndMetadataTaskListener: TaskListener(TaskType.MIXED) { class StoreContentAndMetadataTaskListener : TaskListener(TaskType.MIXED) {
@Autowired @Autowired
lateinit var streamitRestTemplate: RestTemplate lateinit var streamitRestTemplate: RestTemplate
@ -36,21 +36,38 @@ class StoreContentAndMetadataTaskListener: TaskListener(TaskType.MIXED) {
val headers = HttpHeaders().apply { contentType = MediaType.APPLICATION_JSON } val headers = HttpHeaders().apply { contentType = MediaType.APPLICATION_JSON }
val entity = HttpEntity(pickedTask.data, headers) val entity = HttpEntity(pickedTask.data, headers)
val response = try { // ❗ Ikke fang exceptions — la TaskListener håndtere dem
val res = streamitRestTemplate.exchange( val response = streamitRestTemplate.exchange(
"open/api/mediaprocesser/import", "/open/api/mediaprocesser/import",
HttpMethod.POST, HttpMethod.POST,
entity, entity,
Void::class.java, Void::class.java,
) )
res.statusCode.is2xxSuccessful
} catch (e: Exception) { if (!response.statusCode.is2xxSuccessful) {
false throw IllegalStateException("StreamIt returned ${response.statusCode}")
}
// Hvis vi kommer hit → alt OK
return StoreContentAndMetadataTaskResultEvent(
status = TaskStatus.Completed
).producedFrom(task)
}
override fun createIncompleteStateTaskEvent(
task: Task,
status: TaskStatus,
exception: Exception?
): Event {
val message = when (status) {
TaskStatus.Failed -> exception?.message ?: "Unknown error, see log"
TaskStatus.Cancelled -> "Canceled"
else -> ""
} }
return StoreContentAndMetadataTaskResultEvent( return StoreContentAndMetadataTaskResultEvent(
if (response) TaskStatus.Completed else TaskStatus.Failed status = status,
) error = message
).producedFrom(task)
} }
} }

View File

@ -279,25 +279,27 @@ class MigrateCreateStoreTaskListenerTest : TestBase() {
subtitleUris: List<String> subtitleUris: List<String>
) = MigrateContentToStoreTaskResultEvent( ) = MigrateContentToStoreTaskResultEvent(
status = TaskStatus.Completed, status = TaskStatus.Completed,
collection = collection, migrateData = MigrateContentToStoreTaskResultEvent.MigrateData(
videoMigrate = MigrateContentToStoreTaskResultEvent.FileMigration( collection = collection,
storedUri = videoUri, videoMigrate = MigrateContentToStoreTaskResultEvent.FileMigration(
status = if (videoUri != null) MigrateStatus.Completed else MigrateStatus.Failed storedUri = videoUri,
), status = if (videoUri != null) MigrateStatus.Completed else MigrateStatus.Failed
subtitleMigrate = subtitleUris.map { ),
MigrateContentToStoreTaskResultEvent.SubtitleMigration( subtitleMigrate = subtitleUris.map {
language = "en", MigrateContentToStoreTaskResultEvent.SubtitleMigration(
storedUri = it, language = "en",
status = MigrateStatus.Completed
)
},
coverMigrate = listOfNotNull(
coverUri?.let {
MigrateContentToStoreTaskResultEvent.FileMigration(
storedUri = it, storedUri = it,
status = MigrateStatus.Completed status = MigrateStatus.Completed
) )
} },
coverMigrate = listOfNotNull(
coverUri?.let {
MigrateContentToStoreTaskResultEvent.FileMigration(
storedUri = it,
status = MigrateStatus.Completed
)
}
)
) )
) )
} }

View File

@ -153,25 +153,27 @@ class StoreContentAndMetadataListenerTest : TestBase() {
): MigrateContentToStoreTaskResultEvent { ): MigrateContentToStoreTaskResultEvent {
return MigrateContentToStoreTaskResultEvent( return MigrateContentToStoreTaskResultEvent(
status = status, status = status,
collection = collection, migrateData = MigrateContentToStoreTaskResultEvent.MigrateData(
videoMigrate = MigrateContentToStoreTaskResultEvent.FileMigration( collection = collection,
storedUri = videoUri, videoMigrate = MigrateContentToStoreTaskResultEvent.FileMigration(
status = if (videoUri != null) MigrateStatus.Completed else MigrateStatus.Failed storedUri = videoUri,
), status = if (videoUri != null) MigrateStatus.Completed else MigrateStatus.Failed
subtitleMigrate = subtitleUris.map { ),
MigrateContentToStoreTaskResultEvent.SubtitleMigration( subtitleMigrate = subtitleUris.map {
language = "en", MigrateContentToStoreTaskResultEvent.SubtitleMigration(
storedUri = it, language = "en",
status = MigrateStatus.Completed
)
},
coverMigrate = listOfNotNull(
coverUri?.let {
MigrateContentToStoreTaskResultEvent.FileMigration(
storedUri = it, storedUri = it,
status = MigrateStatus.Completed status = MigrateStatus.Completed
) )
} },
coverMigrate = listOfNotNull(
coverUri?.let {
MigrateContentToStoreTaskResultEvent.FileMigration(
storedUri = it,
status = MigrateStatus.Completed
)
}
)
) )
) )
} }

View File

@ -39,8 +39,8 @@ class DownloadCoverTaskListenerTest: TestBase() {
override fun markClaimed(taskId: UUID, workerId: String) {} override fun markClaimed(taskId: UUID, workerId: String) {}
override fun updateLastSeen(taskId: UUID) {} override fun updateLastSeen(taskId: UUID) {}
override fun markCompleted(taskId: UUID) {} override fun markCompleted(taskId: UUID) {}
override fun markCancelled(taskId: UUID) {} override fun markCancelled(referenceId: UUID, taskId: UUID) {}
override fun markFailed(taskId: UUID) {} override fun markFailed(referenceId: UUID, taskId: UUID) {}
override fun updateProgress(taskId: UUID, progress: Int) {} override fun updateProgress(taskId: UUID, progress: Int) {}
override fun log(taskId: UUID, message: String) {} override fun log(taskId: UUID, message: String) {}
override fun publishEvent(event: Event) {} override fun publishEvent(event: Event) {}

View File

@ -45,8 +45,8 @@ class MediaStreamReadTaskListenerTest: TestBase() {
override fun markClaimed(taskId: UUID, workerId: String) {} override fun markClaimed(taskId: UUID, workerId: String) {}
override fun updateLastSeen(taskId: UUID) {} override fun updateLastSeen(taskId: UUID) {}
override fun markCompleted(taskId: UUID) {} override fun markCompleted(taskId: UUID) {}
override fun markFailed(taskId: UUID) {} override fun markFailed(referenceId: UUID, taskId: UUID) {}
override fun markCancelled(taskId: UUID) {} override fun markCancelled(referenceId: UUID, taskId: UUID) {}
override fun updateProgress(taskId: UUID, progress: Int) {} override fun updateProgress(taskId: UUID, progress: Int) {}
override fun log(taskId: UUID, message: String) {} override fun log(taskId: UUID, message: String) {}
override fun publishEvent(event: Event) { override fun publishEvent(event: Event) {

View File

@ -1,30 +1,58 @@
package no.iktdev.mediaprocessing.coordinator.listeners.tasks package no.iktdev.mediaprocessing.coordinator.listeners.tasks
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import no.iktdev.eventi.models.Event
import no.iktdev.eventi.models.store.TaskStatus import no.iktdev.eventi.models.store.TaskStatus
import no.iktdev.eventi.tasks.TaskReporter
import no.iktdev.mediaprocessing.MockFileSystemService import no.iktdev.mediaprocessing.MockFileSystemService
import no.iktdev.mediaprocessing.coordinator.util.FileSystemService 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.events.MigrateContentToStoreTaskResultEvent
import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.MigrateToContentStoreTask 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.model.MigrateStatus
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.util.*
class MigrateContentToStoreTaskListenerTest { class MigrateContentToStoreTaskListenerTest {
class MigrateContentToStoreTaskListenerTestImplementation: MigrateContentToStoreTaskListener() { // -------------------------------------------------------------------------
// Fake Reporter
// -------------------------------------------------------------------------
var fs: FileSystemService? = null class FakeTaskReporter : TaskReporter {
override fun getFileSystemService(): FileSystemService { val events = mutableListOf<Event>()
return fs!! var completed = false
var failed = false
override fun markClaimed(taskId: UUID, workerId: String) {}
override fun updateLastSeen(taskId: UUID) {}
override fun markCompleted(taskId: UUID) { completed = true }
override fun markFailed(referenceId: UUID, taskId: UUID) { failed = true }
override fun markCancelled(referenceId: UUID, taskId: UUID) {}
override fun updateProgress(taskId: UUID, progress: Int) {}
override fun log(taskId: UUID, message: String) {}
override fun publishEvent(event: Event) {
events.add(event)
} }
} }
val listener = MigrateContentToStoreTaskListenerTestImplementation() // -------------------------------------------------------------------------
// Listener with injectable FS
// -------------------------------------------------------------------------
class TestListener : MigrateContentToStoreTaskListener() {
var fs: FileSystemService? = null
override fun getFileSystemService(): FileSystemService = fs!!
}
private val listener = TestListener()
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// migrateVideo // migrateVideo (direct tests)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@Test @Test
@ -37,19 +65,14 @@ class MigrateContentToStoreTaskListenerTest {
""" """
) )
fun migrateVideo_success() { fun migrateVideo_success() {
val fs = MockFileSystemService().also { val fs = MockFileSystemService().also { listener.fs = it }
listener.fs = it
}
val content = MigrateToContentStoreTask.Data.SingleContent( val content = MigrateToContentStoreTask.Data.SingleContent("/tmp/source", "/tmp/dest")
cachedUri = "/tmp/source.mp4",
storeUri = "/tmp/dest.mp4"
)
val result = listener.migrateVideo(fs, content) val result = listener.migrateVideo(fs, content)
assertEquals(MigrateStatus.Completed, result.status) assertEquals(MigrateStatus.Completed, result.status)
assertEquals("/tmp/dest.mp4", result.storedUri) assertEquals("/tmp/dest", result.storedUri)
assertEquals(1, fs.copied.size) assertEquals(1, fs.copied.size)
} }
@ -59,47 +82,36 @@ class MigrateContentToStoreTaskListenerTest {
Når migrateVideo kjøres Når migrateVideo kjøres
Hvis copy feiler Hvis copy feiler
: :
returneres Failed kastes exception
""" """
) )
fun migrateVideo_copyFails() { fun migrateVideo_copyFails() {
val fs = MockFileSystemService().apply { copyShouldFail = true }.also { val fs = MockFileSystemService().apply { copyShouldFail = true }.also { listener.fs = it }
listener.fs = it
val content = MigrateToContentStoreTask.Data.SingleContent("/tmp/source", "/tmp/dest")
assertThrows<IllegalStateException> {
listener.migrateVideo(fs, content)
} }
val content = MigrateToContentStoreTask.Data.SingleContent(
cachedUri = "/tmp/source.mp4",
storeUri = "/tmp/dest.mp4"
)
val result = listener.migrateVideo(fs, content)
assertEquals(MigrateStatus.Failed, result.status)
} }
@Test @Test
@DisplayName( @DisplayName(
""" """
Når migrateVideo kjøres Når migrateVideo kjøres
Hvis copy lykkes men filene ikke er identiske Hvis filene ikke er identiske
: :
returneres Failed kastes exception
""" """
) )
fun migrateVideo_mismatch() { fun migrateVideo_mismatch() {
val fs = MockFileSystemService().apply { identical = false }.also { val fs = MockFileSystemService().apply { identical = false }.also { listener.fs = it }
listener.fs = it
val content = MigrateToContentStoreTask.Data.SingleContent("/tmp/source", "/tmp/dest")
assertThrows<IllegalStateException> {
listener.migrateVideo(fs, content)
} }
val content = MigrateToContentStoreTask.Data.SingleContent(
cachedUri = "/tmp/source.mp4",
storeUri = "/tmp/dest.mp4"
)
val result = listener.migrateVideo(fs, content)
assertEquals(MigrateStatus.Failed, result.status)
} }
@Test @Test
@ -112,9 +124,7 @@ class MigrateContentToStoreTaskListenerTest {
""" """
) )
fun migrateVideo_null() { fun migrateVideo_null() {
val fs = MockFileSystemService().also { val fs = MockFileSystemService().also { listener.fs = it }
listener.fs = it
}
val result = listener.migrateVideo(fs, null) val result = listener.migrateVideo(fs, null)
@ -122,7 +132,7 @@ class MigrateContentToStoreTaskListenerTest {
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// migrateSubtitle // migrateSubtitle (direct tests)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@Test @Test
@ -135,9 +145,7 @@ class MigrateContentToStoreTaskListenerTest {
""" """
) )
fun migrateSubtitle_empty() { fun migrateSubtitle_empty() {
val fs = MockFileSystemService().also { val fs = MockFileSystemService().also { listener.fs = it }
listener.fs = it
}
val result = listener.migrateSubtitle(fs, emptyList()) val result = listener.migrateSubtitle(fs, emptyList())
@ -155,15 +163,9 @@ class MigrateContentToStoreTaskListenerTest {
""" """
) )
fun migrateSubtitle_success() { fun migrateSubtitle_success() {
val fs = MockFileSystemService().also { val fs = MockFileSystemService().also { listener.fs = it }
listener.fs = it
}
val sub = MigrateToContentStoreTask.Data.SingleSubtitle( val sub = MigrateToContentStoreTask.Data.SingleSubtitle("en", "/tmp/a", "/tmp/b")
language = "en",
cachedUri = "/tmp/a.srt",
storeUri = "/tmp/b.srt"
)
val result = listener.migrateSubtitle(fs, listOf(sub)) val result = listener.migrateSubtitle(fs, listOf(sub))
@ -174,34 +176,42 @@ class MigrateContentToStoreTaskListenerTest {
@DisplayName( @DisplayName(
""" """
Når migrateSubtitle kjøres Når migrateSubtitle kjøres
Hvis én lykkes og én feiler Hvis filene ikke er identiske
: :
returneres både Completed og Failed kastes exception
""" """
) )
fun migrateSubtitle_mixed() { fun migrateSubtitle_mismatch() {
val fs = MockFileSystemService().apply { val fs = MockFileSystemService().apply { identical = false }.also { listener.fs = it }
// first OK, second fails
copyShouldFail = false
}.also { listener.fs = it }
val subs = listOf( val sub = MigrateToContentStoreTask.Data.SingleSubtitle("en", "/tmp/a", "/tmp/b")
MigrateToContentStoreTask.Data.SingleSubtitle("en", "/tmp/a", "/tmp/b"),
MigrateToContentStoreTask.Data.SingleSubtitle("no", "/tmp/c", "/tmp/d")
)
// simulate second failing assertThrows<IllegalStateException> {
fs.copyShouldFail = false listener.migrateSubtitle(fs, listOf(sub))
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) @Test
assertEquals(MigrateStatus.Failed, result2.first().status) @DisplayName(
"""
Når migrateSubtitle kjøres
Hvis copy feiler
:
kastes exception
"""
)
fun migrateSubtitle_copyFails() {
val fs = MockFileSystemService().apply { copyShouldFail = true }.also { listener.fs = it }
val sub = MigrateToContentStoreTask.Data.SingleSubtitle("en", "/tmp/a", "/tmp/b")
assertThrows<IllegalStateException> {
listener.migrateSubtitle(fs, listOf(sub))
}
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// migrateCover // migrateCover (direct tests)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@Test @Test
@ -214,14 +224,9 @@ class MigrateContentToStoreTaskListenerTest {
""" """
) )
fun migrateCover_success() { fun migrateCover_success() {
val fs = MockFileSystemService().also { val fs = MockFileSystemService().also { listener.fs = it }
listener.fs = it
}
val cover = MigrateToContentStoreTask.Data.SingleContent( val cover = MigrateToContentStoreTask.Data.SingleContent("/tmp/c", "/tmp/c2")
cachedUri = "/tmp/c.jpg",
storeUri = "/tmp/c2.jpg"
)
val result = listener.migrateCover(fs, listOf(cover)) val result = listener.migrateCover(fs, listOf(cover))
@ -232,51 +237,60 @@ class MigrateContentToStoreTaskListenerTest {
@DisplayName( @DisplayName(
""" """
Når migrateCover kjøres Når migrateCover kjøres
Hvis flere covers og én feiler Hvis filene ikke er identiske
: :
returneres både Completed og Failed kastes exception
""" """
) )
fun migrateCover_mixed() { fun migrateCover_mismatch() {
val fs = MockFileSystemService().also { listener.fs = it } val fs = MockFileSystemService().apply { identical = false }.also { listener.fs = it }
val covers = listOf( val cover = MigrateToContentStoreTask.Data.SingleContent("/tmp/c", "/tmp/c2")
MigrateToContentStoreTask.Data.SingleContent("/tmp/a", "/tmp/b"),
MigrateToContentStoreTask.Data.SingleContent("/tmp/c", "/tmp/d")
)
// first OK, second mismatch assertThrows<IllegalStateException> {
fs.identical = true listener.migrateCover(fs, listOf(cover))
val ok = listener.migrateCover(fs, listOf(covers[0])) }
}
fs.identical = false @Test
val fail = listener.migrateCover(fs, listOf(covers[1])) @DisplayName(
"""
Når migrateCover kjøres
Hvis copy feiler
:
kastes exception
"""
)
fun migrateCover_copyFails() {
val fs = MockFileSystemService().apply { copyShouldFail = true }.also { listener.fs = it }
assertEquals(MigrateStatus.Completed, ok.first().status) val cover = MigrateToContentStoreTask.Data.SingleContent("/tmp/c", "/tmp/c2")
assertEquals(MigrateStatus.Failed, fail.first().status)
assertThrows<IllegalStateException> {
listener.migrateCover(fs, listOf(cover))
}
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// onTask // accept() — full TaskListener flow
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@Test @Test
@DisplayName( @DisplayName(
""" """
Når onTask kjøres Når accept() kjøres
Hvis alle migreringer lykkes Hvis alle migreringer lykkes
: :
returneres Completed-event og cache slettes publiseres Completed-event og cache slettes
""" """
) )
fun onTask_success() = runTest { fun accept_success() = runTest {
val fs = MockFileSystemService().also { val fs = MockFileSystemService().also { listener.fs = it }
listener.fs = it val reporter = FakeTaskReporter()
}
val task = MigrateToContentStoreTask( val task = MigrateToContentStoreTask(
MigrateToContentStoreTask.Data( MigrateToContentStoreTask.Data(
collection = "col", "col",
videoContent = MigrateToContentStoreTask.Data.SingleContent("/tmp/v", "/tmp/v2"), videoContent = MigrateToContentStoreTask.Data.SingleContent("/tmp/v", "/tmp/v2"),
subtitleContent = listOf( subtitleContent = listOf(
MigrateToContentStoreTask.Data.SingleSubtitle("en", "/tmp/s", "/tmp/s2") MigrateToContentStoreTask.Data.SingleSubtitle("en", "/tmp/s", "/tmp/s2")
@ -287,8 +301,12 @@ class MigrateContentToStoreTaskListenerTest {
) )
).newReferenceId() ).newReferenceId()
val event = listener.onTask(task) as MigrateContentToStoreTaskResultEvent listener.accept(task, reporter)
listener.currentJob?.join()
val event = reporter.events.first() as MigrateContentToStoreTaskResultEvent
assertTrue(reporter.completed)
assertEquals(TaskStatus.Completed, event.status) assertEquals(TaskStatus.Completed, event.status)
assertEquals(3, fs.deleted.size) assertEquals(3, fs.deleted.size)
} }
@ -296,28 +314,31 @@ class MigrateContentToStoreTaskListenerTest {
@Test @Test
@DisplayName( @DisplayName(
""" """
Når onTask kjøres Når accept() kjøres
Hvis en migrering feiler Hvis migrateVideo kaster exception
: :
returneres Failed-event og ingenting slettes publiseres Failed-event og cache slettes ikke
""" """
) )
fun onTask_failure() = runTest { fun accept_failure() = runTest {
val fs = MockFileSystemService().apply { copyShouldFail = true }.also { val fs = MockFileSystemService().apply { copyShouldFail = true }.also { listener.fs = it }
listener.fs = it val reporter = FakeTaskReporter()
}
val task = MigrateToContentStoreTask( val task = MigrateToContentStoreTask(
MigrateToContentStoreTask.Data( MigrateToContentStoreTask.Data(
collection = "col", "col",
videoContent = MigrateToContentStoreTask.Data.SingleContent("/tmp/v", "/tmp/v2"), videoContent = MigrateToContentStoreTask.Data.SingleContent("/tmp/v", "/tmp/v2"),
subtitleContent = emptyList(), subtitleContent = emptyList(),
coverContent = emptyList() coverContent = emptyList()
) )
).newReferenceId() ).newReferenceId()
val event = listener.onTask(task) as MigrateContentToStoreTaskResultEvent listener.accept(task, reporter)
listener.currentJob?.join()
val event = reporter.events.first() as MigrateContentToStoreTaskResultEvent
assertTrue(reporter.failed)
assertEquals(TaskStatus.Failed, event.status) assertEquals(TaskStatus.Failed, event.status)
assertEquals(0, fs.deleted.size) assertEquals(0, fs.deleted.size)
} }
@ -325,57 +346,31 @@ class MigrateContentToStoreTaskListenerTest {
@Test @Test
@DisplayName( @DisplayName(
""" """
Når onTask kjøres Når accept() kjøres
Hvis video feiler
:
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 Hvis sletting feiler
: :
returneres fortsatt Completed publiseres fortsatt Completed-event
""" """
) )
fun onTask_deleteFails() = runTest { fun accept_deleteFails() = runTest {
val fs = MockFileSystemService().apply { deleteShouldFail = true } val fs = MockFileSystemService().apply { deleteShouldFail = true }.also { listener.fs = it }
.also { listener.fs = it } val reporter = FakeTaskReporter()
val task = MigrateToContentStoreTask( val task = MigrateToContentStoreTask(
MigrateToContentStoreTask.Data( MigrateToContentStoreTask.Data(
collection = "col", "col",
videoContent = MigrateToContentStoreTask.Data.SingleContent("/tmp/v", "/tmp/v2"), videoContent = MigrateToContentStoreTask.Data.SingleContent("/tmp/v", "/tmp/v2"),
subtitleContent = emptyList(), subtitleContent = emptyList(),
coverContent = emptyList() coverContent = emptyList()
) )
).newReferenceId() ).newReferenceId()
val event = listener.onTask(task) as MigrateContentToStoreTaskResultEvent listener.accept(task, reporter)
listener.currentJob?.join()
val event = reporter.events.first() as MigrateContentToStoreTaskResultEvent
assertTrue(reporter.completed)
assertEquals(TaskStatus.Completed, event.status) assertEquals(TaskStatus.Completed, event.status)
} }
} }

View File

@ -1,8 +1,10 @@
package no.iktdev.mediaprocessing.coordinator.listeners.tasks package no.iktdev.mediaprocessing.coordinator.listeners.tasks
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import no.iktdev.eventi.models.Event
import no.iktdev.eventi.models.Task import no.iktdev.eventi.models.Task
import no.iktdev.eventi.models.store.TaskStatus import no.iktdev.eventi.models.store.TaskStatus
import no.iktdev.eventi.tasks.TaskReporter
import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.StoreContentAndMetadataTaskResultEvent import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.StoreContentAndMetadataTaskResultEvent
import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.StoreContentAndMetadataTask import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.StoreContentAndMetadataTask
import no.iktdev.mediaprocessing.shared.common.model.ContentExport import no.iktdev.mediaprocessing.shared.common.model.ContentExport
@ -21,10 +23,37 @@ import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.client.RestTemplate import org.springframework.web.client.RestTemplate
import java.util.*
@ExtendWith(MockitoExtension::class) @ExtendWith(MockitoExtension::class)
class StoreContentAndMetadataTaskListenerTest { class StoreContentAndMetadataTaskListenerTest {
// -------------------------------------------------------------------------
// Fake Reporter
// -------------------------------------------------------------------------
class FakeTaskReporter : TaskReporter {
val events = mutableListOf<Event>()
var completed = false
var failed = false
override fun markClaimed(taskId: UUID, workerId: String) {}
override fun updateLastSeen(taskId: UUID) {}
override fun markCompleted(taskId: UUID) { completed = true }
override fun markFailed(referenceId: UUID, taskId: UUID) { failed = true }
override fun markCancelled(referenceId: UUID, taskId: UUID) {}
override fun updateProgress(taskId: UUID, progress: Int) {}
override fun log(taskId: UUID, message: String) {}
override fun publishEvent(event: Event) {
events.add(event)
}
}
// -------------------------------------------------------------------------
// Setup
// -------------------------------------------------------------------------
@Mock @Mock
lateinit var restTemplate: RestTemplate lateinit var restTemplate: RestTemplate
@ -39,10 +68,19 @@ class StoreContentAndMetadataTaskListenerTest {
private fun sampleContentExport(): ContentExport { private fun sampleContentExport(): ContentExport {
return ContentExport( return ContentExport(
collection = "series", collection = "series",
episodeInfo = ContentExport.EpisodeInfo(episodeNumber = 1, seasonNumber = 1, episodeTitle = "Pilot"), episodeInfo = ContentExport.EpisodeInfo(
episodeNumber = 1,
seasonNumber = 1,
episodeTitle = "Pilot"
),
media = ContentExport.MediaExport( media = ContentExport.MediaExport(
videoFile = "bb.s01e01.mkv", videoFile = "bb.s01e01.mkv",
subtitles = listOf(ContentExport.MediaExport.Subtitle(subtitleFile = "bb.en.srt", language = "en")) subtitles = listOf(
ContentExport.MediaExport.Subtitle(
subtitleFile = "bb.en.srt",
language = "en"
)
)
), ),
metadata = ContentExport.MetadataExport( metadata = ContentExport.MetadataExport(
title = "Breaking Bad", title = "Breaking Bad",
@ -55,6 +93,9 @@ class StoreContentAndMetadataTaskListenerTest {
) )
} }
// -------------------------------------------------------------------------
// supports()
// -------------------------------------------------------------------------
@Test @Test
@DisplayName( @DisplayName(
@ -63,7 +104,7 @@ class StoreContentAndMetadataTaskListenerTest {
Når supports() kalles Når supports() kalles
: :
Returnerer true Returnerer true
""" """
) )
fun supports_returnsTrueForCorrectTask() { fun supports_returnsTrueForCorrectTask() {
val task = StoreContentAndMetadataTask(data = sampleContentExport()) val task = StoreContentAndMetadataTask(data = sampleContentExport())
@ -80,7 +121,7 @@ class StoreContentAndMetadataTaskListenerTest {
Når supports() kalles Når supports() kalles
: :
Returnerer false Returnerer false
""" """
) )
fun supports_returnsFalseForWrongTask() { fun supports_returnsFalseForWrongTask() {
val task = object : Task() {} val task = object : Task() {}
@ -90,45 +131,53 @@ class StoreContentAndMetadataTaskListenerTest {
assertThat(result).isFalse() assertThat(result).isFalse()
} }
// -------------------------------------------------------------------------
// accept() — full TaskListener flow
// -------------------------------------------------------------------------
@Test @Test
@DisplayName( @DisplayName(
""" """
Gitt at RestTemplate returnerer 200 OK Gitt at RestTemplate returnerer 200 OK
Når onTask() kalles Når accept() kjøres
: :
Returnerer Completed-event Publiseres Completed-event
""" """
) )
fun onTask_returnsCompletedOnSuccess() = runTest { fun accept_returnsCompletedOnSuccess() = runTest {
val task = StoreContentAndMetadataTask(data = sampleContentExport()) val reporter = FakeTaskReporter()
val task = StoreContentAndMetadataTask(data = sampleContentExport()).newReferenceId()
whenever( whenever(
restTemplate.exchange( restTemplate.exchange(
eq("open/api/mediaprocesser/import"), eq("/open/api/mediaprocesser/import"),
eq(HttpMethod.POST), eq(HttpMethod.POST),
any(), any(),
eq(Void::class.java) eq(Void::class.java)
) )
).thenReturn(ResponseEntity(HttpStatus.OK)) ).thenReturn(ResponseEntity(HttpStatus.OK))
val event = listener.onTask(task) listener.accept(task, reporter)
listener.currentJob?.join()
assertThat(event).isInstanceOf(StoreContentAndMetadataTaskResultEvent::class.java) val event = reporter.events.first() as StoreContentAndMetadataTaskResultEvent
val result = event as StoreContentAndMetadataTaskResultEvent
assertThat(result.status).isEqualTo(TaskStatus.Completed) assertThat(reporter.completed).isTrue()
assertThat(event.status).isEqualTo(TaskStatus.Completed)
} }
@Test @Test
@DisplayName( @DisplayName(
""" """
Gitt at RestTemplate kaster exception Gitt at RestTemplate kaster exception
Når onTask() kalles Når accept() kjøres
: :
Returnerer Failed-event Publiseres Failed-event via createIncompleteStateTaskEvent()
""" """
) )
fun onTask_returnsFailedOnException() = runTest { fun accept_returnsFailedOnException() = runTest {
val task = StoreContentAndMetadataTask(data = sampleContentExport()) val reporter = FakeTaskReporter()
val task = StoreContentAndMetadataTask(data = sampleContentExport()).newReferenceId()
whenever( whenever(
restTemplate.exchange( restTemplate.exchange(
@ -139,13 +188,20 @@ class StoreContentAndMetadataTaskListenerTest {
) )
).thenThrow(RuntimeException("boom")) ).thenThrow(RuntimeException("boom"))
val event = listener.onTask(task) listener.accept(task, reporter)
listener.currentJob?.join()
assertThat(event).isInstanceOf(StoreContentAndMetadataTaskResultEvent::class.java) val event = reporter.events.first() as StoreContentAndMetadataTaskResultEvent
val result = event as StoreContentAndMetadataTaskResultEvent
assertThat(result.status).isEqualTo(TaskStatus.Failed) assertThat(reporter.failed).isTrue()
assertThat(event.status).isEqualTo(TaskStatus.Failed)
assertThat(event.error).contains("boom")
} }
// -------------------------------------------------------------------------
// workerId()
// -------------------------------------------------------------------------
@Test @Test
@DisplayName( @DisplayName(
""" """
@ -153,12 +209,12 @@ class StoreContentAndMetadataTaskListenerTest {
Når getWorkerId() kalles Når getWorkerId() kalles
: :
Returnerer en streng som inneholder klassenavn, tasktype og UUID Returnerer en streng som inneholder klassenavn, tasktype og UUID
""" """
) )
fun workerId_hasCorrectFormat() { fun workerId_hasCorrectFormat() {
val id = listener.getWorkerId() val id = listener.getWorkerId()
assertThat(id).contains("StoreContentAndMetadataTaskListener-MIXED-") assertThat(id).contains("StoreContentAndMetadataTaskListener-MIXED-")
assertThat(id.split("-").last().length).isGreaterThan(10) // UUID-ish assertThat(id.split("-").last().length).isGreaterThan(10)
} }
} }

View File

@ -57,12 +57,12 @@ class DefaultTaskReporter() : TaskReporter {
TaskStore.markConsumed(taskId, TaskStatus.Completed) TaskStore.markConsumed(taskId, TaskStatus.Completed)
} }
override fun markFailed(taskId: UUID) { override fun markFailed(referenceId: UUID, taskId: UUID) {
log.info { "Marking task $taskId as failed" } log.info { "Marking task $taskId as failed" }
TaskStore.markConsumed(taskId, TaskStatus.Failed) TaskStore.markConsumed(taskId, TaskStatus.Failed)
} }
override fun markCancelled(taskId: UUID) { override fun markCancelled(referenceId: UUID, taskId: UUID) {
log.info { "Margin task $taskId as cancelled"} log.info { "Margin task $taskId as cancelled"}
TaskStore.markConsumed(taskId, TaskStatus.Cancelled) TaskStore.markConsumed(taskId, TaskStatus.Cancelled)
} }

View File

@ -71,6 +71,19 @@ class SubtitleTaskListener: FfmpegTaskListener(TaskType.CPU_INTENSIVE) {
).producedFrom(task) ).producedFrom(task)
} }
override fun createIncompleteStateTaskEvent(
task: Task,
status: TaskStatus,
exception: Exception?
): Event {
val message = when (status) {
TaskStatus.Failed -> exception?.message ?: "Unknown error, see log"
TaskStatus.Cancelled -> "Canceled"
else -> ""
}
return ProcesserExtractResultEvent(null, status, error = message)
}
override fun getFfmpeg(): FFmpeg { override fun getFfmpeg(): FFmpeg {
return SubtitleFFmpeg() return SubtitleFFmpeg()
} }

View File

@ -70,6 +70,20 @@ class VideoTaskListener(private var coordinatorWebClient: CoordinatorClient): Ff
).producedFrom(task) ).producedFrom(task)
} }
override fun createIncompleteStateTaskEvent(
task: Task,
status: TaskStatus,
exception: Exception?
): Event {
val message = when (status) {
TaskStatus.Failed -> exception?.message ?: "Unknown error, see log"
TaskStatus.Cancelled -> "Canceled"
else -> ""
}
return ProcesserEncodeResultEvent(null, status, error = message)
}
override fun getFfmpeg(): FFmpeg { override fun getFfmpeg(): FFmpeg {
return VideoFFmpeg(object : FFmpeg.Listener { return VideoFFmpeg(object : FFmpeg.Listener {
var lastProgress: FfmpegDecodedProgress? = null var lastProgress: FfmpegDecodedProgress? = null

View File

@ -39,8 +39,8 @@ class SubtitleTaskListenerTest {
override fun markClaimed(taskId: UUID, workerId: String) {} override fun markClaimed(taskId: UUID, workerId: String) {}
override fun updateLastSeen(taskId: UUID) {} override fun updateLastSeen(taskId: UUID) {}
override fun markCompleted(taskId: UUID) {} override fun markCompleted(taskId: UUID) {}
override fun markFailed(taskId: UUID) {} override fun markFailed(referenceId: UUID, taskId: UUID) {}
override fun markCancelled(taskId: UUID) {} override fun markCancelled(referenceId: UUID, taskId: UUID) {}
override fun updateProgress(taskId: UUID, progress: Int) {} override fun updateProgress(taskId: UUID, progress: Int) {}
override fun log(taskId: UUID, message: String) {} override fun log(taskId: UUID, message: String) {}

View File

@ -41,8 +41,8 @@ class VideoTaskListenerTest {
override fun markClaimed(taskId: UUID, workerId: String) {} override fun markClaimed(taskId: UUID, workerId: String) {}
override fun updateLastSeen(taskId: UUID) {} override fun updateLastSeen(taskId: UUID) {}
override fun markCompleted(taskId: UUID) {} override fun markCompleted(taskId: UUID) {}
override fun markFailed(taskId: UUID) {} override fun markFailed(referenceId: UUID, taskId: UUID) {}
override fun markCancelled(taskId: UUID) {} override fun markCancelled(referenceId: UUID, taskId: UUID) {}
override fun updateProgress(taskId: UUID, progress: Int) {} override fun updateProgress(taskId: UUID, progress: Int) {}
override fun log(taskId: UUID, message: String) {} override fun log(taskId: UUID, message: String) {}
override fun publishEvent(event: Event) { override fun publishEvent(event: Event) {

View File

@ -1,5 +1,5 @@
[versions] [versions]
eventi = "1.0-rc36" eventi = "1.0-rc38"
exfl = "1.0-rc1" exfl = "1.0-rc1"
[libraries] [libraries]

View File

@ -5,13 +5,18 @@ import no.iktdev.mediaprocessing.shared.common.event_task_contract.TaskResultEve
import no.iktdev.mediaprocessing.shared.common.model.MigrateStatus import no.iktdev.mediaprocessing.shared.common.model.MigrateStatus
class MigrateContentToStoreTaskResultEvent( class MigrateContentToStoreTaskResultEvent(
val collection: String, val migrateData: MigrateData? = null,
val videoMigrate: FileMigration,
val subtitleMigrate: List<SubtitleMigration>,
val coverMigrate: List<FileMigration>,
status: TaskStatus, status: TaskStatus,
error: String? = null error: String? = null
) : TaskResultEvent(status, error) { ) : TaskResultEvent(status, error) {
data class MigrateData(
val collection: String,
val videoMigrate: FileMigration,
val subtitleMigrate: List<SubtitleMigration>,
val coverMigrate: List<FileMigration>,
)
data class FileMigration( data class FileMigration(
val storedUri: String?, val storedUri: String?,
val status: MigrateStatus val status: MigrateStatus

View File

@ -14,7 +14,8 @@ class StoreProjection(val events: List<Event>) {
val metadata = CollectProjection(events).metadata val metadata = CollectProjection(events).metadata
if (metadata != null) { if (metadata != null) {
val useCover = if (metadata.cover != null) { val useCover = if (metadata.cover != null) {
val migrated = events.filterIsInstance<MigrateContentToStoreTaskResultEvent>().lastOrNull { it.status == TaskStatus.Completed }?.coverMigrate ?: emptyList() val migrateData = events.filterIsInstance<MigrateContentToStoreTaskResultEvent>().lastOrNull { it.status == TaskStatus.Completed }?.migrateData
val migrated = migrateData?.coverMigrate ?: emptyList()
migrated.filter { it.status == MigrateStatus.Completed && it.storedUri != null } migrated.filter { it.status == MigrateStatus.Completed && it.storedUri != null }
.map { File(it.storedUri!!).name } .map { File(it.storedUri!!).name }
.find { it == metadata.cover.name } .find { it == metadata.cover.name }
@ -47,7 +48,7 @@ class StoreProjection(val events: List<Event>) {
} }
fun projectMediaFiles(): ContentExport.MediaExport? { fun projectMediaFiles(): ContentExport.MediaExport? {
val migrated = events.filterIsInstance<MigrateContentToStoreTaskResultEvent>().lastOrNull { it.status == TaskStatus.Completed } val migrated = events.filterIsInstance<MigrateContentToStoreTaskResultEvent>().lastOrNull { it.status == TaskStatus.Completed }?.migrateData
return ContentExport.MediaExport( return ContentExport.MediaExport(
videoFile = migrated?.videoMigrate?.let { video -> videoFile = migrated?.videoMigrate?.let { video ->
if (video.status == MigrateStatus.Completed) File(video.storedUri!!).name else null if (video.status == MigrateStatus.Completed) File(video.storedUri!!).name else null
@ -59,7 +60,7 @@ class StoreProjection(val events: List<Event>) {
fun getCollection(): String? { fun getCollection(): String? {
val migrated = events.filterIsInstance<MigrateContentToStoreTaskResultEvent>().lastOrNull { it.status == TaskStatus.Completed } ?: return null val migrated = events.filterIsInstance<MigrateContentToStoreTaskResultEvent>().lastOrNull { it.status == TaskStatus.Completed } ?: return null
return if (migrated.status == TaskStatus.Completed) migrated.collection else null return if (migrated.status == TaskStatus.Completed) migrated.migrateData?.collection else null
} }
} }