From cf68c08db6d676a1af066e6f9b7e44f2877c9204 Mon Sep 17 00:00:00 2001 From: Roman Makeev <57789105+makeevrserg@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:52:56 +0300 Subject: [PATCH] Refactor and fix orders (#18) * fix empty content * refactor and fix orders * fix wrong signal inheritance * add simple test for files * fix failed signals * fix sort by signal count * up version 0.8.0 --- .github/workflows/pr.yml | 6 +- gradle.properties | 2 +- .../ifrmvp/backend/core/logging/Loggable.kt | 10 + .../backend/core/logging/Slf4jLoggable.kt | 21 + .../backend/db/signal/table/SignalTable.kt | 4 + .../kenerator/configuration/build.gradle.kts | 2 - modules/kenerator/sql/build.gradle.kts | 1 + .../parser/presentation/FillerController.kt | 21 +- .../ifrmvp/backend/model/SignalModel.kt | 2 + web-api/build.gradle.kts | 2 + .../signal/data/IncludedFilesRepository.kt | 144 +++++++ .../data/InstantCategoryConfigRepository.kt | 18 - .../route/signal/data/OrderRepository.kt | 39 ++ .../route/signal/data/SignalRepository.kt | 129 ++++++ .../route/signal/data/model/IncludedFile.kt | 6 + .../presentation/SignalRouteRegistry.kt | 368 ++++-------------- .../data/IncludedFilesRepositoryTest.kt | 119 ++++++ 17 files changed, 568 insertions(+), 326 deletions(-) create mode 100644 modules/core/src/main/kotlin/com/flipperdevices/ifrmvp/backend/core/logging/Loggable.kt create mode 100644 modules/core/src/main/kotlin/com/flipperdevices/ifrmvp/backend/core/logging/Slf4jLoggable.kt create mode 100644 web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/IncludedFilesRepository.kt delete mode 100644 web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/InstantCategoryConfigRepository.kt create mode 100644 web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/OrderRepository.kt create mode 100644 web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/SignalRepository.kt create mode 100644 web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/model/IncludedFile.kt create mode 100644 web-api/src/test/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/IncludedFilesRepositoryTest.kt diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 4de3b34..cd547c1 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -67,4 +67,8 @@ jobs: - name: Run SQL kenerator uses: gradle/gradle-build-action@ac2d340dc04d9e1113182899e983b5400c17cda1 # v3 with: - arguments: :modules:kenerator:sql:run \ No newline at end of file + arguments: :modules:kenerator:sql:run + - name: Run test + uses: gradle/gradle-build-action@ac2d340dc04d9e1113182899e983b5400c17cda1 # v3 + with: + arguments: :web-api:test \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index c77d1b3..2fa8e5d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ org.gradle.parallel=true makeevrserg.project.name=IRDBBackend makeevrserg.project.url=https://github.com/flipperdevices/IRDB-Backend makeevrserg.project.group=com.flipperdevices.ifrmvp.backend -makeevrserg.project.version.string=0.7.2 +makeevrserg.project.version.string=0.8.0 makeevrserg.project.description=Api for IfrSample makeevrserg.project.developers=makeevrserg|Makeev Roman|makeevrserg@gmail.com # Java diff --git a/modules/core/src/main/kotlin/com/flipperdevices/ifrmvp/backend/core/logging/Loggable.kt b/modules/core/src/main/kotlin/com/flipperdevices/ifrmvp/backend/core/logging/Loggable.kt new file mode 100644 index 0000000..3584587 --- /dev/null +++ b/modules/core/src/main/kotlin/com/flipperdevices/ifrmvp/backend/core/logging/Loggable.kt @@ -0,0 +1,10 @@ +package com.flipperdevices.ifrmvp.backend.core.logging + +interface Loggable { + fun info(msg: () -> String) + fun debug(msg: () -> String) + fun error(throwable: Throwable? = null, msg: () -> String) + + class Default(tag: String) : Loggable by Slf4jLoggable(tag) +} + diff --git a/modules/core/src/main/kotlin/com/flipperdevices/ifrmvp/backend/core/logging/Slf4jLoggable.kt b/modules/core/src/main/kotlin/com/flipperdevices/ifrmvp/backend/core/logging/Slf4jLoggable.kt new file mode 100644 index 0000000..480163a --- /dev/null +++ b/modules/core/src/main/kotlin/com/flipperdevices/ifrmvp/backend/core/logging/Slf4jLoggable.kt @@ -0,0 +1,21 @@ +package com.flipperdevices.ifrmvp.backend.core.logging + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +class Slf4jLoggable(tag: String) : Loggable { + private val logger: Logger = LoggerFactory.getLogger(tag) + + override fun info(msg: () -> String) { + logger.debug(msg.invoke()) + } + + override fun debug(msg: () -> String) { + logger.debug(msg.invoke()) + } + + override fun error(throwable: Throwable?, msg: () -> String) { + if (throwable == null) logger.error(msg.invoke()) + else logger.error(msg.invoke(), throwable) + } +} \ No newline at end of file diff --git a/modules/database/src/main/kotlin/com/flipperdevices/ifrmvp/backend/db/signal/table/SignalTable.kt b/modules/database/src/main/kotlin/com/flipperdevices/ifrmvp/backend/db/signal/table/SignalTable.kt index 51b121d..5565112 100644 --- a/modules/database/src/main/kotlin/com/flipperdevices/ifrmvp/backend/db/signal/table/SignalTable.kt +++ b/modules/database/src/main/kotlin/com/flipperdevices/ifrmvp/backend/db/signal/table/SignalTable.kt @@ -1,5 +1,6 @@ package com.flipperdevices.ifrmvp.backend.db.signal.table +import com.flipperdevices.ifrmvp.backend.model.DeviceKey import org.jetbrains.exposed.dao.id.LongIdTable /** @@ -20,9 +21,11 @@ object SignalTable : LongIdTable("SIGNAL_TABLE") { val dutyCycle = text("duty_cycle").nullable() val data = text("data").nullable() val hash = text("hash") + val deviceKey = enumeration("device_key").nullable() init { uniqueIndex( + deviceKey, brandId, type, frequency, @@ -30,6 +33,7 @@ object SignalTable : LongIdTable("SIGNAL_TABLE") { data ) uniqueIndex( + deviceKey, brandId, type, protocol, diff --git a/modules/kenerator/configuration/build.gradle.kts b/modules/kenerator/configuration/build.gradle.kts index cb8ed05..529748c 100644 --- a/modules/kenerator/configuration/build.gradle.kts +++ b/modules/kenerator/configuration/build.gradle.kts @@ -19,9 +19,7 @@ dependencies { implementation(projects.modules.database) implementation(projects.modules.core) implementation(projects.modules.model) - implementation(projects.modules.database) implementation(projects.modules.infrared) - implementation(projects.modules.kenerator.sql) implementation(projects.modules.kenerator.paths) } diff --git a/modules/kenerator/sql/build.gradle.kts b/modules/kenerator/sql/build.gradle.kts index e20a780..c518151 100644 --- a/modules/kenerator/sql/build.gradle.kts +++ b/modules/kenerator/sql/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(projects.modules.database) implementation(projects.modules.infrared) implementation(projects.modules.kenerator.paths) + implementation(projects.modules.kenerator.configuration) } application { diff --git a/modules/kenerator/sql/src/main/kotlin/com/flipperdevices/ifrmvp/parser/presentation/FillerController.kt b/modules/kenerator/sql/src/main/kotlin/com/flipperdevices/ifrmvp/parser/presentation/FillerController.kt index 8c63100..6968b6d 100644 --- a/modules/kenerator/sql/src/main/kotlin/com/flipperdevices/ifrmvp/parser/presentation/FillerController.kt +++ b/modules/kenerator/sql/src/main/kotlin/com/flipperdevices/ifrmvp/parser/presentation/FillerController.kt @@ -11,6 +11,8 @@ import com.flipperdevices.ifrmvp.backend.db.signal.table.SignalKeyTable import com.flipperdevices.ifrmvp.backend.db.signal.table.SignalNameAliasTable import com.flipperdevices.ifrmvp.backend.db.signal.table.SignalTable import com.flipperdevices.ifrmvp.backend.db.signal.table.UiPresetTable +import com.flipperdevices.ifrmvp.generator.config.device.api.DeviceKeyNamesProvider.Companion.getKey +import com.flipperdevices.ifrmvp.generator.config.device.api.any.AnyDeviceKeyNamesProvider import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier import com.flipperdevices.ifrmvp.parser.util.ParserPathResolver import com.flipperdevices.infrared.editor.encoding.InfraredRemoteEncoder.identifier @@ -24,9 +26,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.JoinType +import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.batchInsert import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.or import org.jetbrains.exposed.sql.transactions.transaction internal class FillerController(private val database: Database) : CoroutineScope by IoCoroutineScope() { @@ -115,6 +119,7 @@ internal class FillerController(private val database: Database) : CoroutineScope this[SignalTable.dutyCycle] = rawRemote?.dutyCycle this[SignalTable.data] = rawRemote?.data this[SignalTable.hash] = remote.identifier.hash + this[SignalTable.deviceKey] = AnyDeviceKeyNamesProvider.getKey(remote.identifier.name) } // ManyToMany file to signal references val irFileId = InfraredFileTable @@ -124,25 +129,25 @@ internal class FillerController(private val database: Database) : CoroutineScope .map { it[InfraredFileTable.id] } .first() - val signalIds = signals.map { - val parsedRemote = it as? InfraredRemote.Parsed - val rawRemote = it as? InfraredRemote.Raw + val signalIds = signals.map { remote -> + val parsedRemote = remote as? InfraredRemote.Parsed + val rawRemote = remote as? InfraredRemote.Raw SignalTable .select(SignalTable.id) .where { SignalTable.brandId eq brandId } -// .andWhere { SignalTable.name eq it.name } - .andWhere { SignalTable.type eq it.type } + .andWhere { SignalTable.type eq remote.type } .andWhere { SignalTable.protocol eq parsedRemote?.protocol } .andWhere { SignalTable.address eq parsedRemote?.address } .andWhere { SignalTable.command eq parsedRemote?.command } .andWhere { SignalTable.frequency eq rawRemote?.frequency } .andWhere { SignalTable.dutyCycle eq rawRemote?.dutyCycle } .andWhere { SignalTable.data eq rawRemote?.data } + .andWhere { SignalTable.deviceKey eq AnyDeviceKeyNamesProvider.getKey(remote.name) } .map { it[SignalTable.id] } .firstOrNull() ?: error( """ - The list is emoty for brand: ${brand.name} category: ${categoryFolder} file: ${irFile.name}; - name: ${it.name} + The list is empty for brand: ${brand.name} category: ${categoryFolder} file: ${irFile.name}; + name: ${remote.name} """.trimIndent() ) } @@ -201,6 +206,8 @@ internal class FillerController(private val database: Database) : CoroutineScope .select(SignalTable.id) .where { SignalTable.brandId eq brandId } .andWhere { InfraredFileToSignalTable.infraredFileId eq irFileId } + .andWhere { SignalTable.deviceKey.eq(baseKey).or(SignalTable.deviceKey.isNull()) } + .orderBy(SignalTable.deviceKey to SortOrder.ASC_NULLS_LAST) .apply { when (keyIdentifier) { IfrKeyIdentifier.Empty -> error("Identifying is not possible!") diff --git a/modules/model/src/commonMain/kotlin/com/flipperdevices/ifrmvp/backend/model/SignalModel.kt b/modules/model/src/commonMain/kotlin/com/flipperdevices/ifrmvp/backend/model/SignalModel.kt index 8ec2834..4f9addc 100644 --- a/modules/model/src/commonMain/kotlin/com/flipperdevices/ifrmvp/backend/model/SignalModel.kt +++ b/modules/model/src/commonMain/kotlin/com/flipperdevices/ifrmvp/backend/model/SignalModel.kt @@ -9,6 +9,8 @@ data class SignalModel( val id: Long, @SerialName("remote") val remote: FlipperRemote, + @SerialName("device_key") + val deviceKey: DeviceKey? = null ) { @Serializable data class FlipperRemote( diff --git a/web-api/build.gradle.kts b/web-api/build.gradle.kts index 7514b0b..4a0010d 100644 --- a/web-api/build.gradle.kts +++ b/web-api/build.gradle.kts @@ -37,6 +37,8 @@ dependencies { // Exposed implementation(libs.exposed.core) implementation(libs.exposed.dao) + // test + testImplementation(kotlin("test")) // Services implementation(projects.modules.buildKonfig) implementation(projects.modules.model) diff --git a/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/IncludedFilesRepository.kt b/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/IncludedFilesRepository.kt new file mode 100644 index 0000000..e77d0dd --- /dev/null +++ b/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/IncludedFilesRepository.kt @@ -0,0 +1,144 @@ +package com.flipperdevices.ifrmvp.backend.route.signal.data + +import com.flipperdevices.ifrmvp.backend.core.logging.Loggable +import com.flipperdevices.ifrmvp.backend.db.signal.table.InfraredFileTable +import com.flipperdevices.ifrmvp.backend.db.signal.table.InfraredFileToSignalTable +import com.flipperdevices.ifrmvp.backend.model.SignalRequestModel +import com.flipperdevices.ifrmvp.backend.route.signal.data.model.IncludedFile +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.Join +import org.jetbrains.exposed.sql.JoinType +import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.alias +import org.jetbrains.exposed.sql.andWhere +import org.jetbrains.exposed.sql.transactions.transaction + +class IncludedFilesRepository( + private val database: Database +) : Loggable by Loggable.Default("IncludedFilesRepository") { + + /** + * This is a list of files, which contains every instance of [successSignalIds] + * aka list().containsAll(successSignal1,successSignal2) + */ + private fun getWhiteListedFileIds(successSignalIds: List): List { + var join: Join? = null + successSignalIds.forEachIndexed { i, id -> + join = (join ?: InfraredFileToSignalTable) + .join( + otherTable = InfraredFileToSignalTable.alias("F$i"), + onColumn = InfraredFileToSignalTable.infraredFileId, + otherColumn = InfraredFileToSignalTable.alias("F$i")[InfraredFileToSignalTable.infraredFileId], + joinType = JoinType.INNER + ) + } + var query = join + ?.select(InfraredFileToSignalTable.infraredFileId) + ?.withDistinct(true) + successSignalIds.forEachIndexed { index, id -> + query = if (index == 0) { + query?.where { InfraredFileToSignalTable.signalId eq id } + } else { + query?.andWhere { + InfraredFileToSignalTable.alias("F$index")[InfraredFileToSignalTable.signalId] eq id + } + } + } + return when { + successSignalIds.isEmpty() -> emptyList() + else -> transaction(database) { + query?.map { it[InfraredFileToSignalTable.infraredFileId].value }.orEmpty() + } + } + } + + /** + * If any signal is failed then we skip this file + */ + private fun getBlackListedFileIds(failedSignalIds: List): List { + return when { + failedSignalIds.isEmpty() -> emptyList() + else -> transaction(database) { + InfraredFileToSignalTable.select(InfraredFileToSignalTable.infraredFileId) + .withDistinct(true) + .where { InfraredFileToSignalTable.signalId inList failedSignalIds } + .map { it[InfraredFileToSignalTable.infraredFileId].value } + } + } + } + + /** + * Keep only those files, in which signals have been successfully passed + * + * SELECT INFRARED_FILE."id", INFRARED_FILE."signal_count" + * FROM INFRARED_FILE JOIN INFRARED_FILE_TO_SIGNAL on INFRARED_FILE_TO_SIGNAL."infrared_file_id" = INFRARED_FILE."id" + * group by INFRARED_FILE."id" + * order by INFRARED_FILE."signal_count" desc + */ + suspend fun findIncludedFiles(signalRequestModel: SignalRequestModel): List { + info { "#findIncludedFiles invoked" } + + val excludedFileIds = getBlackListedFileIds( + failedSignalIds = signalRequestModel.failedResults + .map(SignalRequestModel.SignalResultData::signalId) + ) + info { "#findIncludedFiles excludedFileIds: $excludedFileIds" } + val includedFileIds = getWhiteListedFileIds( + successSignalIds = signalRequestModel.successResults + .map(SignalRequestModel.SignalResultData::signalId) + ) + info { "#findIncludedFiles includedFileIds: $includedFileIds" } + return transaction(database) { + InfraredFileTable + // Main query + .select(InfraredFileTable.id, InfraredFileTable.signalCount) + .groupBy(InfraredFileTable.id, InfraredFileTable.signalCount) + .orderBy(InfraredFileTable.signalCount to SortOrder.DESC) + .where { InfraredFileTable.brandId eq signalRequestModel.brandId } + .let { nextQuery -> + if (includedFileIds.isEmpty()) nextQuery + else nextQuery.andWhere { InfraredFileTable.id inList includedFileIds } + } + //[3273, 3282, 3283, 3286] + .let { nextQuery -> + if (excludedFileIds.isEmpty()) nextQuery + else nextQuery.andWhere { InfraredFileTable.id notInList excludedFileIds } + } + .map { + val file = IncludedFile( + fileId = it[InfraredFileTable.id].value, + signalCount = it[InfraredFileTable.signalCount] + ) + file + }.also { + + debug { "#got files: ${it.map { it.fileId }}" } + } + } + } + + suspend fun findFallbackFile(signalRequestModel: SignalRequestModel): IncludedFile? { + // Getting last available file + return findIncludedFiles( + signalRequestModel = when { + signalRequestModel.skippedResults.isNotEmpty() -> { + signalRequestModel.copy( + skippedResults = signalRequestModel.skippedResults.dropLast(1) + ) + } + + signalRequestModel.failedResults.isNotEmpty() -> { + signalRequestModel.copy( + failedResults = signalRequestModel.failedResults.dropLast(1) + ) + } + + else -> { + signalRequestModel.copy( + successResults = signalRequestModel.successResults.dropLast(1) + ) + } + }, + ).firstOrNull() + } +} \ No newline at end of file diff --git a/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/InstantCategoryConfigRepository.kt b/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/InstantCategoryConfigRepository.kt deleted file mode 100644 index 8d61192..0000000 --- a/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/InstantCategoryConfigRepository.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.flipperdevices.ifrmvp.backend.route.signal.data - -import com.flipperdevices.ifrmvp.backend.model.CategoryConfiguration -import com.flipperdevices.ifrmvp.backend.model.CategoryType -import com.flipperdevices.ifrmvp.parser.util.ParserPathResolver - -interface CategoryConfigRepository { - fun getOrNull(categoryType: CategoryType, index: Int): CategoryConfiguration.OrderModel? -} - -object IRDBCategoryConfigRepository : CategoryConfigRepository { - override fun getOrNull(categoryType: CategoryType, index: Int): CategoryConfiguration.OrderModel? { - return ParserPathResolver - .categoryConfiguration(categoryType.folderName) - .orders - .getOrNull(index) - } -} diff --git a/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/OrderRepository.kt b/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/OrderRepository.kt new file mode 100644 index 0000000..bb78ea2 --- /dev/null +++ b/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/OrderRepository.kt @@ -0,0 +1,39 @@ +package com.flipperdevices.ifrmvp.backend.route.signal.data + +import com.flipperdevices.ifrmvp.backend.db.signal.table.SignalNameAliasTable +import com.flipperdevices.ifrmvp.backend.model.CategoryConfiguration +import com.flipperdevices.ifrmvp.backend.model.CategoryType +import com.flipperdevices.ifrmvp.backend.model.SignalRequestModel +import com.flipperdevices.ifrmvp.generator.config.device.api.DeviceKeyNamesProvider.Companion.getKey +import com.flipperdevices.ifrmvp.generator.config.device.api.any.AnyDeviceKeyNamesProvider +import com.flipperdevices.ifrmvp.parser.util.ParserPathResolver +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.orWhere +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction + +class OrderRepository(private val database: Database) { + fun findOrder( + signalRequestModel: SignalRequestModel, + categoryType: CategoryType, + recursionLevel: Int + ): CategoryConfiguration.OrderModel? { + val skippedKeys = transaction(database) { + SignalNameAliasTable + .selectAll() + .where { SignalNameAliasTable.id inList signalRequestModel.skippedResults.map(SignalRequestModel.SignalResultData::signalId) } + .orWhere { SignalNameAliasTable.id inList signalRequestModel.successResults.map(SignalRequestModel.SignalResultData::signalId) } +// .orWhere { SignalNameAliasTable.id inList signalRequestModel.failedResults.map(SignalRequestModel.SignalResultData::signalId) } + .mapNotNull { + val keyName = it[SignalNameAliasTable.signalName] + AnyDeviceKeyNamesProvider.getKey(keyName) + }.distinct() + } + + return ParserPathResolver + .categoryConfiguration(categoryType.folderName) + .orders + .filter { !skippedKeys.contains(it.key) } + .getOrNull(recursionLevel) + } +} diff --git a/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/SignalRepository.kt b/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/SignalRepository.kt new file mode 100644 index 0000000..8ef129d --- /dev/null +++ b/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/SignalRepository.kt @@ -0,0 +1,129 @@ +package com.flipperdevices.ifrmvp.backend.route.signal.data + +import com.flipperdevices.ifrmvp.backend.db.signal.table.InfraredFileTable +import com.flipperdevices.ifrmvp.backend.db.signal.table.InfraredFileToSignalTable +import com.flipperdevices.ifrmvp.backend.db.signal.table.SignalKeyTable +import com.flipperdevices.ifrmvp.backend.db.signal.table.SignalTable +import com.flipperdevices.ifrmvp.backend.model.CategoryConfiguration +import com.flipperdevices.ifrmvp.backend.model.SignalModel +import com.flipperdevices.ifrmvp.backend.model.SignalRequestModel +import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier +import com.flipperdevices.ifrmvp.model.buttondata.SingleKeyButtonData +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.JoinType +import org.jetbrains.exposed.sql.SortOrder +import org.jetbrains.exposed.sql.andWhere +import org.jetbrains.exposed.sql.count +import org.jetbrains.exposed.sql.or +import org.jetbrains.exposed.sql.selectAll +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.wrapAsExpression + +class SignalRepository(private val database: Database) { + suspend fun getSignalModel( + signalRequestModel: SignalRequestModel, + order: CategoryConfiguration.OrderModel, + includedFiles: List + ): SignalModel? { + return transaction(database) { + SignalTable + .join( + otherTable = InfraredFileToSignalTable, + joinType = JoinType.LEFT, + onColumn = SignalTable.id, + otherColumn = InfraredFileToSignalTable.signalId + ) + .join( + otherTable = InfraredFileTable, + joinType = JoinType.LEFT, + onColumn = InfraredFileToSignalTable.infraredFileId, + otherColumn = InfraredFileTable.id + ) + .join( + otherTable = SignalKeyTable, + joinType = JoinType.LEFT, + onColumn = SignalTable.id, + otherColumn = SignalKeyTable.signalId + ) + .selectAll() + .where { SignalTable.brandId eq signalRequestModel.brandId } + .let { query -> + val singleButtonData = order.data as? SingleKeyButtonData + when (val identifier = singleButtonData?.keyIdentifier) { + is IfrKeyIdentifier.Sha256 -> { + query.andWhere { + SignalKeyTable.deviceKey.eq(order.key) + .or { SignalKeyTable.hash.eq(identifier.hash) } + } + } + + is IfrKeyIdentifier.Name -> { + query.andWhere { + SignalKeyTable.remoteKeyName.eq(identifier.name) + } + } + + else -> { + query.andWhere { SignalKeyTable.deviceKey.eq(order.key) } + } + } + } + .andWhere { SignalKeyTable.deviceKey eq order.key } + .andWhere { SignalKeyTable.signalId eq SignalTable.id } + .let { query -> + if (includedFiles.isEmpty()) query + else query.andWhere { InfraredFileToSignalTable.infraredFileId inList includedFiles } + + } + .let { query -> + val failedSignalIds = signalRequestModel.failedResults + .map(SignalRequestModel.SignalResultData::signalId) + if (failedSignalIds.isEmpty()) { + query + } else { + query.andWhere { SignalTable.id.notInList(failedSignalIds) } + } + } + .let { query -> + val successfulSignalIds = signalRequestModel.successResults + .map(SignalRequestModel.SignalResultData::signalId) + if (successfulSignalIds.isEmpty()) { + query + } else { + query.andWhere { SignalTable.id.notInList(successfulSignalIds) } + } + } + .let { query -> + val successfulSignalIds = signalRequestModel.successResults + .map(SignalRequestModel.SignalResultData::signalId) + if (successfulSignalIds.isEmpty()) { + query + } else { + query.andWhere { + InfraredFileToSignalTable.infraredFileId inSubQuery InfraredFileToSignalTable + .select(InfraredFileToSignalTable.infraredFileId) + .where { InfraredFileToSignalTable.signalId inList successfulSignalIds } + } + } + } + .orderBy(InfraredFileTable.signalCount to SortOrder.DESC) + .limit(1) + .map { + SignalModel( + id = it[SignalTable.id].value, + remote = SignalModel.FlipperRemote( + name = "empty", + type = it[SignalTable.type], + protocol = it[SignalTable.protocol], + address = it[SignalTable.address], + command = it[SignalTable.command], + frequency = it[SignalTable.frequency], + dutyCycle = it[SignalTable.dutyCycle], + data = it[SignalTable.data], + ) + ) + } + .firstOrNull() + } + } +} \ No newline at end of file diff --git a/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/model/IncludedFile.kt b/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/model/IncludedFile.kt new file mode 100644 index 0000000..70a70a1 --- /dev/null +++ b/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/model/IncludedFile.kt @@ -0,0 +1,6 @@ +package com.flipperdevices.ifrmvp.backend.route.signal.data.model + +data class IncludedFile( + val fileId: Long, + val signalCount: Int +) \ No newline at end of file diff --git a/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/presentation/SignalRouteRegistry.kt b/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/presentation/SignalRouteRegistry.kt index 9ab55ff..96c5776 100644 --- a/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/presentation/SignalRouteRegistry.kt +++ b/web-api/src/main/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/presentation/SignalRouteRegistry.kt @@ -1,283 +1,76 @@ package com.flipperdevices.ifrmvp.backend.route.signal.presentation +import com.flipperdevices.ifrmvp.backend.core.logging.Loggable import com.flipperdevices.ifrmvp.backend.core.route.RouteRegistry import com.flipperdevices.ifrmvp.backend.db.signal.dao.TableDao import com.flipperdevices.ifrmvp.backend.db.signal.exception.TableDaoException -import com.flipperdevices.ifrmvp.backend.db.signal.table.InfraredFileTable -import com.flipperdevices.ifrmvp.backend.db.signal.table.InfraredFileToSignalTable -import com.flipperdevices.ifrmvp.backend.db.signal.table.SignalKeyTable -import com.flipperdevices.ifrmvp.backend.db.signal.table.SignalNameAliasTable -import com.flipperdevices.ifrmvp.backend.db.signal.table.SignalTable -import com.flipperdevices.ifrmvp.backend.model.BrandModel -import com.flipperdevices.ifrmvp.backend.model.CategoryConfiguration import com.flipperdevices.ifrmvp.backend.model.CategoryType import com.flipperdevices.ifrmvp.backend.model.DeviceCategory -import com.flipperdevices.ifrmvp.backend.model.SignalModel import com.flipperdevices.ifrmvp.backend.model.SignalRequestModel import com.flipperdevices.ifrmvp.backend.model.SignalResponse import com.flipperdevices.ifrmvp.backend.model.SignalResponseModel -import com.flipperdevices.ifrmvp.backend.route.signal.data.CategoryConfigRepository -import com.flipperdevices.ifrmvp.backend.route.signal.data.IRDBCategoryConfigRepository -import com.flipperdevices.ifrmvp.generator.config.device.api.DeviceKeyNamesProvider.Companion.getKey -import com.flipperdevices.ifrmvp.generator.config.device.api.any.AnyDeviceKeyNamesProvider -import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier -import com.flipperdevices.ifrmvp.model.buttondata.SingleKeyButtonData +import com.flipperdevices.ifrmvp.backend.route.signal.data.IncludedFilesRepository +import com.flipperdevices.ifrmvp.backend.route.signal.data.OrderRepository +import com.flipperdevices.ifrmvp.backend.route.signal.data.SignalRepository +import com.flipperdevices.ifrmvp.backend.route.signal.data.model.IncludedFile import io.github.smiley4.ktorswaggerui.dsl.routing.post import io.ktor.http.HttpStatusCode import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.Routing import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.JoinType -import org.jetbrains.exposed.sql.Query -import org.jetbrains.exposed.sql.SortOrder -import org.jetbrains.exposed.sql.andWhere -import org.jetbrains.exposed.sql.count -import org.jetbrains.exposed.sql.or -import org.jetbrains.exposed.sql.orWhere -import org.jetbrains.exposed.sql.selectAll -import org.jetbrains.exposed.sql.transactions.transaction -import org.jetbrains.exposed.sql.wrapAsExpression @Suppress("UnusedPrivateProperty") internal class SignalRouteRegistry( private val database: Database, private val tableDao: TableDao, - private val categoryConfigRepository: CategoryConfigRepository = IRDBCategoryConfigRepository, -) : RouteRegistry { +) : RouteRegistry, Loggable by Loggable.Default("SignalRouteRegistry") { + private val includedFilesRepository = IncludedFilesRepository(database) + private val signalRepository = SignalRepository(database) + private val orderRepository = OrderRepository(database) - /** - * Keep only those files, in which signals have been successfully passed - * - * SELECT INFRARED_FILE."id", INFRARED_FILE."signal_count" - * FROM INFRARED_FILE JOIN INFRARED_FILE_TO_SIGNAL on INFRARED_FILE_TO_SIGNAL."infrared_file_id" = INFRARED_FILE."id" - * group by INFRARED_FILE."id" - * order by INFRARED_FILE."signal_count" desc - */ - private fun getIncludedFileIds(signalRequestModel: SignalRequestModel, brandId: Long): Query { - val excludedFileIds = transaction(database) { - InfraredFileToSignalTable.select(InfraredFileToSignalTable.infraredFileId) - .where { - InfraredFileToSignalTable.signalId inList - signalRequestModel.failedResults - .map(SignalRequestModel.SignalResultData::signalId) - }.map { it[InfraredFileToSignalTable.infraredFileId].value } - } - return transaction(database) { - InfraredFileTable - .join( - otherTable = InfraredFileToSignalTable, - onColumn = InfraredFileTable.id, - otherColumn = InfraredFileToSignalTable.infraredFileId, - joinType = JoinType.LEFT - ) - .select(InfraredFileTable.id, InfraredFileTable.signalCount) - .groupBy(InfraredFileTable.id) - .orderBy(InfraredFileTable.signalCount to SortOrder.DESC) - .where { InfraredFileTable.brandId eq brandId } - .apply { - val successSignalIds = signalRequestModel - .successResults - .map(SignalRequestModel.SignalResultData::signalId) - var nextQuery = this - nextQuery = if (successSignalIds.isNotEmpty()) { - nextQuery.andWhere { - InfraredFileToSignalTable - .signalId - .inList(successSignalIds) - } - } else { - nextQuery - } - val failedSignalIds = signalRequestModel - .failedResults - .map(SignalRequestModel.SignalResultData::signalId) - nextQuery = if (failedSignalIds.isNotEmpty()) { - nextQuery.andWhere { - InfraredFileToSignalTable - .infraredFileId - .notInList(excludedFileIds) - } - } else { - nextQuery - } - nextQuery - } - } - } - - private suspend fun getSignalModel( - signalRequestModel: SignalRequestModel, - order: CategoryConfiguration.OrderModel, - brand: BrandModel - ): SignalModel? { - return transaction(database) { - SignalTable - .join( - otherTable = InfraredFileToSignalTable, - joinType = JoinType.LEFT, - onColumn = SignalTable.id, - otherColumn = InfraredFileToSignalTable.signalId - ) - .join( - otherTable = InfraredFileTable, - joinType = JoinType.LEFT, - onColumn = InfraredFileToSignalTable.infraredFileId, - otherColumn = InfraredFileTable.id - ) - .join( - otherTable = SignalKeyTable, - joinType = JoinType.LEFT, - onColumn = SignalTable.id, - otherColumn = SignalKeyTable.signalId - ) - .selectAll() - .where { SignalTable.brandId eq brand.id } - .apply { - val singleButtonData = order.data as? SingleKeyButtonData - when (val identifier = singleButtonData?.keyIdentifier) { - is IfrKeyIdentifier.Sha256 -> { - andWhere { - SignalKeyTable.deviceKey.eq(order.key) - .or { SignalKeyTable.hash.eq(identifier.hash) } - } - } - - is IfrKeyIdentifier.Name -> { - andWhere { - SignalKeyTable.remoteKeyName.eq(identifier.name) - } - } - - else -> andWhere { SignalKeyTable.deviceKey.eq(order.key) } - } - } - .andWhere { SignalKeyTable.deviceKey eq order.key } - .andWhere { SignalKeyTable.signalId eq SignalTable.id } - .apply { - val failedSignalIds = signalRequestModel.failedResults - .map(SignalRequestModel.SignalResultData::signalId) - if (failedSignalIds.isEmpty()) { - this - } else { - andWhere { SignalTable.id.notInList(failedSignalIds) } - } - } - .apply { - val successfulSignalIds = signalRequestModel.successResults - .map(SignalRequestModel.SignalResultData::signalId) - if (successfulSignalIds.isEmpty()) { - this - } else { - andWhere { SignalTable.id.notInList(successfulSignalIds) } - } - } - .apply { - val successfulSignalIds = signalRequestModel.successResults - .map(SignalRequestModel.SignalResultData::signalId) - if (successfulSignalIds.isEmpty()) { - this - } else { - andWhere { - InfraredFileToSignalTable.infraredFileId inSubQuery InfraredFileToSignalTable - .select(InfraredFileToSignalTable.infraredFileId) - .where { InfraredFileToSignalTable.signalId inList successfulSignalIds } - } - } - } - .orderBy( - wrapAsExpression( - InfraredFileToSignalTable - .select(InfraredFileToSignalTable.signalId.count()) - .where { InfraredFileToSignalTable.signalId eq SignalTable.id } - ) to SortOrder.DESC - ) - .limit(1) - .map { - SignalModel( - id = it[SignalTable.id].value, - remote = SignalModel.FlipperRemote( - name = "empty", - type = it[SignalTable.type], - protocol = it[SignalTable.protocol], - address = it[SignalTable.address], - command = it[SignalTable.command], - frequency = it[SignalTable.frequency], - dutyCycle = it[SignalTable.dutyCycle], - data = it[SignalTable.data], - ) - ) - } - .firstOrNull() - } - } - private suspend fun findSignal( + private suspend fun getNextSignal( signalRequestModel: SignalRequestModel, - includedFileIds: Query, - brand: BrandModel, - // Index of successful results - index: Int = signalRequestModel.successResults.size - .plus(signalRequestModel.skippedResults.size) - .plus(signalRequestModel.failedResults.size), categoryType: CategoryType, - category: DeviceCategory - ): SignalResponseModel { - val order = categoryConfigRepository.getOrNull( + includedFiles: List, + category: DeviceCategory, + recursionLevel: Int = 0 + ): SignalResponseModel? { + val orderModel = orderRepository.findOrder( + signalRequestModel = signalRequestModel, categoryType = categoryType, - index = index - ) - println("Getting order: $order index: $index") - // todo When orders is empty we can't define the next key. Need to think how to bypass it or may be just log - if (order == null) { - val infraredFileId = transaction(database) { - includedFileIds - .map { it[InfraredFileTable.id] } - .first() - .value - } - val response = SignalResponseModel(ifrFileModel = tableDao.ifrFileById(infraredFileId)) - return response - } - - val skippedKeys = transaction(database) { - SignalNameAliasTable - .selectAll() - .where { SignalNameAliasTable.id inList signalRequestModel.skippedResults.map(SignalRequestModel.SignalResultData::signalId) } - .orWhere { SignalNameAliasTable.id inList signalRequestModel.successResults.map(SignalRequestModel.SignalResultData::signalId) } - .orWhere { SignalNameAliasTable.id inList signalRequestModel.failedResults.map(SignalRequestModel.SignalResultData::signalId) } - .mapNotNull { - val keyName = it[SignalNameAliasTable.signalName] - AnyDeviceKeyNamesProvider.getKey(keyName) - }.distinct() + recursionLevel = recursionLevel + ) ?: run { + debug { "Order is null for recursionLevel: $recursionLevel $signalRequestModel" } + return null } - val signalModel = getSignalModel( + val signalModel = signalRepository.getSignalModel( signalRequestModel = signalRequestModel, - order = order, - brand = brand + order = orderModel, + includedFiles = includedFiles.map(IncludedFile::fileId) ) - - if (signalModel == null || skippedKeys.contains(order.key)) { - return findSignal( + // For some orders signal may be null + // But it may be not null for next orders + if (signalModel == null) { + debug { "Signal model is null for $orderModel" } + return getNextSignal( signalRequestModel = signalRequestModel, - includedFileIds = includedFileIds, - brand = brand, - index = index + 1, categoryType = categoryType, - category = category + includedFiles = includedFiles, + category = category, + recursionLevel = recursionLevel + 1 ) } - - val response = SignalResponseModel( + return SignalResponseModel( signalResponse = SignalResponse( signalModel = signalModel, - message = order.message, + message = orderModel.message, categoryName = category.meta.manifest.singularDisplayName, - data = order.data + data = orderModel.data ) ) - return response } private fun Routing.statusRoute() { @@ -287,7 +80,6 @@ internal class SignalRouteRegistry( body = { @Suppress("UnusedPrivateProperty") val signalRequestModel = context.receive() - println("signalRequestModel: ${signalRequestModel}") val brand = tableDao.getBrandById(signalRequestModel.brandId) val category = tableDao.getCategoryById(brand.categoryId) @@ -295,75 +87,57 @@ internal class SignalRouteRegistry( .entries .firstOrNull { it.folderName == category.folderName } ?: throw TableDaoException.CategoryNotFound(category.id) + debug { "category: $category categoryType: $categoryType signalRequestModel: ${signalRequestModel}" } - val includedFileIds = getIncludedFileIds(signalRequestModel, brand.id) - val includedInfraredFilesCount = transaction(database) { includedFileIds.count() } - println("#root includedInfraredFilesCount=$includedInfraredFilesCount") - when (includedInfraredFilesCount) { - 0L -> { - if (signalRequestModel.failedResults.isEmpty() - .and(signalRequestModel.successResults.isEmpty()) - .and(signalRequestModel.skippedResults.isEmpty()) - ) { - println("#root everything is empty") - context.respond(HttpStatusCode.NoContent) - } else { - val fallbackIncludedFileId = transaction(database) { - getIncludedFileIds( - signalRequestModel = when { - signalRequestModel.skippedResults.isNotEmpty() -> { - signalRequestModel.copy( - skippedResults = signalRequestModel.skippedResults.dropLast(1) - ) - } - signalRequestModel.failedResults.isNotEmpty() -> { - signalRequestModel.copy( - failedResults = signalRequestModel.failedResults.dropLast(1) - ) - } + val includedFiles = includedFilesRepository.findIncludedFiles(signalRequestModel) - else -> { - signalRequestModel.copy( - successResults = signalRequestModel.successResults.dropLast(1) - ) - } - }, - brandId = brand.id - ).map { it[InfraredFileTable.id] }.firstOrNull()?.value - } - println("#root fallbackIncludedFileId is $fallbackIncludedFileId") - if (fallbackIncludedFileId == null) { - context.respond(HttpStatusCode.NoContent) - } else { - val response = - SignalResponseModel(ifrFileModel = tableDao.ifrFileById(fallbackIncludedFileId)) - context.respond(response) - } + when { + includedFiles.isEmpty() -> { + debug { "#root includedInfraredFilesCount is empty!" } + val irFileModel = includedFilesRepository + .findFallbackFile(signalRequestModel) + ?.fileId + ?.let { id -> tableDao.ifrFileById(id) } + if (irFileModel == null) { + context.respond(HttpStatusCode.NoContent) + } else { + context.respond(SignalResponseModel(ifrFileModel = irFileModel)) } - return@post } - 1L -> { - val infraredFileId = transaction(database) { - includedFileIds - .map { it[InfraredFileTable.id] } - .first() - .value - } - val response = SignalResponseModel(ifrFileModel = tableDao.ifrFileById(infraredFileId)) + includedFiles.size == 1 -> { + debug { "#root found exact one infrared file" } + val irFileModel = tableDao.ifrFileById(includedFiles.first().fileId) + val response = SignalResponseModel(ifrFileModel = irFileModel) context.respond(response) } else -> { - val response = findSignal( + val signal = getNextSignal( signalRequestModel = signalRequestModel, - includedFileIds = includedFileIds, - brand = brand, categoryType = categoryType, + includedFiles = includedFiles, category = category ) - context.respond(response) + + if (signal != null) { + debug { "#root found multiple infrared files: ${includedFiles.size}" } + context.respond(signal) + } else { + debug { "#root could not find signal model. Giving fallback file" } + + val irFileModel = includedFilesRepository + .findFallbackFile(signalRequestModel) + ?.fileId + ?.let { id -> tableDao.ifrFileById(id) } + + if (irFileModel == null) { + context.respond(HttpStatusCode.NoContent) + } else { + context.respond(SignalResponseModel(ifrFileModel = irFileModel)) + } + } } } } diff --git a/web-api/src/test/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/IncludedFilesRepositoryTest.kt b/web-api/src/test/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/IncludedFilesRepositoryTest.kt new file mode 100644 index 0000000..3c1820f --- /dev/null +++ b/web-api/src/test/kotlin/com/flipperdevices/ifrmvp/backend/route/signal/data/IncludedFilesRepositoryTest.kt @@ -0,0 +1,119 @@ +package com.flipperdevices.ifrmvp.backend.route.signal.data + +import com.flipperdevices.ifrmvp.backend.db.signal.di.SignalApiModule +import com.flipperdevices.ifrmvp.backend.db.signal.table.BrandTable +import com.flipperdevices.ifrmvp.backend.db.signal.table.CategoryTable +import com.flipperdevices.ifrmvp.backend.envkonfig.EnvKonfig +import com.flipperdevices.ifrmvp.backend.model.CategoryConfiguration +import com.flipperdevices.ifrmvp.backend.model.CategoryType +import com.flipperdevices.ifrmvp.backend.model.DeviceKey +import com.flipperdevices.ifrmvp.backend.model.SignalRequestModel +import com.flipperdevices.ifrmvp.backend.route.signal.data.model.IncludedFile +import com.flipperdevices.ifrmvp.model.buttondata.UnknownButtonData +import kotlinx.coroutines.runBlocking +import org.jetbrains.exposed.sql.andWhere +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.jetbrains.exposed.sql.transactions.transaction +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class IncludedFilesRepositoryTest { + private var signalApiModule: SignalApiModule? = null + private val requireSignalApiModule: SignalApiModule + get() = signalApiModule ?: error("Forget to register module") + + @BeforeTest + fun setup() { + signalApiModule = SignalApiModule.Default( + signalDbConnection = EnvKonfig.signalDatabaseConnection + ) + } + + @AfterTest + fun tearDown() { + signalApiModule?.database?.run(TransactionManager::closeAndUnregister) + signalApiModule = null + } + + fun stubOrderModel(key: DeviceKey) = CategoryConfiguration.OrderModel( + message = "stub", + index = key.ordinal, + key = key, + data = UnknownButtonData + ) + + @Test + fun `test included files are correct`(): Unit = runBlocking { + + val categoryId = transaction(requireSignalApiModule.database) { + CategoryTable.select(CategoryTable.id) + .where { CategoryTable.folderName eq CategoryType.TVS.folderName } + .limit(1) + .map { it[CategoryTable.id] } + .first() + .value + } + val brandId = transaction(requireSignalApiModule.database) { + BrandTable.select(BrandTable.id) + .where { BrandTable.categoryId eq categoryId } + .andWhere { BrandTable.folderName eq "Skyworth" } + .limit(1) + .map { it[BrandTable.id] } + .first() + .value + } + val includedFilesRepository = IncludedFilesRepository(requireSignalApiModule.database) + val signalRepository = SignalRepository(requireSignalApiModule.database) + + SignalRequestModel(brandId = brandId).let { request1 -> + val includedFiles1 = includedFilesRepository.findIncludedFiles(signalRequestModel = request1) + assertEquals(31, includedFiles1.size) + val signal1 = signalRepository.getSignalModel( + signalRequestModel = request1, + order = stubOrderModel(DeviceKey.PWR), + includedFiles = includedFiles1.map(IncludedFile::fileId) + ) + assertNotNull(signal1) + assertEquals("parsed", signal1.remote.type) + assertEquals("00 00 00 00", signal1.remote.address) + assertEquals("0C 00 00 00", signal1.remote.command) + assertEquals("RC5", signal1.remote.protocol) + SignalRequestModel( + brandId = brandId, + successResults = listOf(SignalRequestModel.SignalResultData(signal1.id)) + ).let { request2 -> + val includedFiles2 = includedFilesRepository.findIncludedFiles(signalRequestModel = request2) + assertEquals(4, includedFiles2.size) + val signal2 = signalRepository.getSignalModel( + signalRequestModel = request1, + order = stubOrderModel(DeviceKey.VOL_UP), + includedFiles = includedFiles1.map(IncludedFile::fileId) + ) + assertNotNull(signal2) + assertEquals("parsed", signal2.remote.type) + assertEquals("00 00 00 00", signal2.remote.address) + assertEquals("10 00 00 00", signal2.remote.command) + assertEquals("RC5", signal2.remote.protocol) + SignalRequestModel( + brandId = brandId, + successResults = listOf( + SignalRequestModel.SignalResultData(signal1.id), + SignalRequestModel.SignalResultData(signal2.id) + ) + ).let { request3 -> + val includedFiles3 = includedFilesRepository.findIncludedFiles(signalRequestModel = request3) + assertEquals(3, includedFiles3.size) + val signal3 = signalRepository.getSignalModel( + signalRequestModel = request1, + order = stubOrderModel(DeviceKey.VOL_DOWN), + includedFiles = includedFiles1.map(IncludedFile::fileId) + ) + assertNotNull(signal3) + } + } + } + } +} \ No newline at end of file