From c6ef339564286999c953a457cb0de4974eca422d Mon Sep 17 00:00:00 2001 From: Peter Trifanov <peter.trifanov@mail.ru> Date: Mon, 5 Sep 2022 16:26:52 +0300 Subject: [PATCH 1/4] Update project's best execution in a contest when execution is finished --- .../LnkContestExecutionRepository.kt | 3 + .../save/backend/service/ExecutionService.kt | 7 ++ .../service/LnkContestExecutionService.kt | 2 + .../service/LnkContestProjectService.kt | 33 ++++++++ .../service/LnkContestProjectServiceTest.kt | 81 +++++++++++++++++++ .../com/saveourtool/save/entities/Project.kt | 2 + 6 files changed, 128 insertions(+) create mode 100644 save-backend/src/test/kotlin/com/saveourtool/save/backend/service/LnkContestProjectServiceTest.kt diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/repository/LnkContestExecutionRepository.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/repository/LnkContestExecutionRepository.kt index dab37b1589..d620f3bb05 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/repository/LnkContestExecutionRepository.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/repository/LnkContestExecutionRepository.kt @@ -53,4 +53,7 @@ interface LnkContestExecutionRepository : BaseEntityRepository<LnkContestExecuti * @return [LnkContestExecution] associated with [project] and [Contest] with name [contestName] */ fun findByExecutionProjectAndContestName(project: Project, contestName: String): LnkContestExecution? + + + fun findByExecution(execution: Execution): LnkContestExecution? } diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/ExecutionService.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/ExecutionService.kt index d564a19097..849a2b4541 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/ExecutionService.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/ExecutionService.kt @@ -34,6 +34,7 @@ class ExecutionService( private val testExecutionRepository: TestExecutionRepository, @Lazy private val testSuitesService: TestSuitesService, private val configProperties: ConfigProperties, + private val lnkContestProjectService: LnkContestProjectService, ) { private val log = LoggerFactory.getLogger(ExecutionService::class.java) @@ -65,6 +66,12 @@ class ExecutionService( if (updatedExecution.status == ExecutionStatus.FINISHED || updatedExecution.status == ExecutionStatus.ERROR) { // execution is completed, we can update end time updatedExecution.endTime = LocalDateTime.now() + + if (execution.type == TestingType.CONTEST_MODE) { + // maybe this execution is the new best execution under a certain contest + lnkContestProjectService.updateBestExecution(execution) + } + // if the tests are stuck in the READY_FOR_TESTING or RUNNING status testExecutionRepository.findByStatusListAndExecutionId(listOf(TestResultStatus.READY_FOR_TESTING, TestResultStatus.RUNNING), execution.requiredId()).map { testExec -> log.debug { diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestExecutionService.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestExecutionService.kt index cbf90068b4..f8f69f7531 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestExecutionService.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestExecutionService.kt @@ -42,4 +42,6 @@ class LnkContestExecutionService( fun getLatestExecutionByContestAndProjectIds(contest: Contest, projectIds: List<Long>) = lnkContestExecutionRepository .findByContestAndExecutionProjectIdInOrderByExecutionStartTimeDesc(contest, projectIds) .distinctBy { it.execution.project } + + fun findContestByExecution(execution: Execution) = lnkContestExecutionRepository.findByExecution(execution)?.contest } diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestProjectService.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestProjectService.kt index b977638197..bb835a3448 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestProjectService.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestProjectService.kt @@ -2,8 +2,11 @@ package com.saveourtool.save.backend.service import com.saveourtool.save.backend.repository.LnkContestProjectRepository import com.saveourtool.save.entities.* +import com.saveourtool.save.utils.debug +import com.saveourtool.save.utils.getLogger import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional /** * Service of [LnkContestProject] @@ -11,6 +14,7 @@ import org.springframework.stereotype.Service @Service class LnkContestProjectService( private val lnkContestProjectRepository: LnkContestProjectRepository, + private val lnkContestExecutionService: LnkContestExecutionService, ) { /** * @param contestName name of a [Contest] @@ -65,4 +69,33 @@ class LnkContestProjectService( lnkContestProjectRepository.save(LnkContestProject(project, contest, null, 0.0)) true } + + @Transactional + fun updateBestExecution(newExecution: Execution) { + val newScore = requireNotNull(newExecution.score) { + "Cannot update best score, because no score has been provided for execution id=${newExecution.id}" + } + val contest = lnkContestExecutionService.findContestByExecution(newExecution) + ?: error("Execution was performed not as a part of contest") + val project = newExecution.project + val lnkContestProject = lnkContestProjectRepository.findByContestAndProject(contest, project) + .orElseThrow { + IllegalStateException("Project ${project.shortToString()} is not bound to contest name=${contest.name}") + } + val oldBestScore = lnkContestProject.bestScore + if (oldBestScore == null || oldBestScore <= newScore) { + logger.debug { + "For project ${project.shortToString()} updating best_score from execution " + + "[id=${lnkContestProject.bestExecution?.id},score=$oldBestScore] to " + + "[id=${newExecution.id},score=$newScore]" + } + lnkContestProject.bestExecution = newExecution + lnkContestProject.bestScore = newExecution.score + lnkContestProjectRepository.save(lnkContestProject) + } + } + + companion object { + private val logger = getLogger<LnkContestProjectService>() + } } diff --git a/save-backend/src/test/kotlin/com/saveourtool/save/backend/service/LnkContestProjectServiceTest.kt b/save-backend/src/test/kotlin/com/saveourtool/save/backend/service/LnkContestProjectServiceTest.kt new file mode 100644 index 0000000000..fe6aa54314 --- /dev/null +++ b/save-backend/src/test/kotlin/com/saveourtool/save/backend/service/LnkContestProjectServiceTest.kt @@ -0,0 +1,81 @@ +package com.saveourtool.save.backend.service + +import com.saveourtool.save.backend.repository.LnkContestProjectRepository +import com.saveourtool.save.entities.Contest +import com.saveourtool.save.entities.Execution +import com.saveourtool.save.entities.LnkContestProject +import com.saveourtool.save.entities.Project +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.kotlin.* +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.context.annotation.Import +import org.springframework.test.context.junit.jupiter.SpringExtension +import java.util.Optional +import kotlin.math.abs + +@ExtendWith(SpringExtension::class) +@Import(LnkContestProjectService::class) +class LnkContestProjectServiceTest { + @Autowired private lateinit var lnkContestProjectService: LnkContestProjectService + @MockBean private lateinit var lnkContestProjectRepository: LnkContestProjectRepository + @MockBean private lateinit var lnkContestExecutionService: LnkContestExecutionService + + @Test + fun `should update best score if it is empty`() { + givenOldBestExecution(null) + + lnkContestProjectService.updateBestExecution(Execution.stub(Project.stub(99)).apply { score = 4.0 }) + + then(lnkContestProjectRepository) + .should(times(1)) + .save(argWhere { + abs(it.bestScore!! - 4.0) < 1e-4 + }) + } + + @Test + fun `should update best score if the new one is greater`() { + givenOldBestExecution( + Execution.stub(Project.stub(99)).apply { + id = 99 + score = 3.3 + } + ) + + lnkContestProjectService.updateBestExecution(Execution.stub(Project.stub(99)).apply { score = 4.0 }) + + then(lnkContestProjectRepository) + .should(times(1)) + .save(argWhere { + abs(it.bestScore!! - 4.0) < 1e-4 + }) + } + + @Test + fun `should not update best score if the new one is smaller`() { + givenOldBestExecution( + Execution.stub(Project.stub(99)).apply { + id = 99 + score = 5.0 + } + ) + + lnkContestProjectService.updateBestExecution(Execution.stub(Project.stub(99)).apply { score = 4.4 }) + + then(lnkContestProjectRepository) + .should(never()) + .save(any()) + } + + private fun givenOldBestExecution(oldBestExecution: Execution?) { + given(lnkContestExecutionService.findContestByExecution(any())) + .willReturn(Contest.stub(99)) + given(lnkContestProjectRepository.findByContestAndProject(any(), any())) + .willAnswer { + LnkContestProject(it.arguments[1] as Project, it.arguments[0] as Contest, oldBestExecution, oldBestExecution?.score) + .let { Optional.of(it) } + } + } +} \ No newline at end of file diff --git a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/Project.kt b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/Project.kt index d047b5c057..2ccafe2253 100644 --- a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/Project.kt +++ b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/Project.kt @@ -79,6 +79,8 @@ data class Project( email ?: "", ) + fun shortToString() = "[organization=${organization.name},name=$name]" + companion object { /** * Create a stub for testing. Since all fields are mutable, only required ones can be set after calling this method. From 07aef873ccab51b7b43a506a128aaa857f4ab083 Mon Sep 17 00:00:00 2001 From: Peter Trifanov <peter.trifanov@mail.ru> Date: Mon, 5 Sep 2022 16:39:08 +0300 Subject: [PATCH 2/4] Code style --- .../backend/repository/LnkContestExecutionRepository.kt | 5 ++++- .../save/backend/service/LnkContestExecutionService.kt | 5 +++++ .../save/backend/service/LnkContestProjectService.kt | 8 ++++++-- .../save/backend/service/LnkContestProjectServiceTest.kt | 3 ++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/repository/LnkContestExecutionRepository.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/repository/LnkContestExecutionRepository.kt index d620f3bb05..0046fcfb59 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/repository/LnkContestExecutionRepository.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/repository/LnkContestExecutionRepository.kt @@ -54,6 +54,9 @@ interface LnkContestExecutionRepository : BaseEntityRepository<LnkContestExecuti */ fun findByExecutionProjectAndContestName(project: Project, contestName: String): LnkContestExecution? - + /** + * @param execution + * @return [LnkContestExecution] if any is associated with [execution] + */ fun findByExecution(execution: Execution): LnkContestExecution? } diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestExecutionService.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestExecutionService.kt index f8f69f7531..6f13be4079 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestExecutionService.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestExecutionService.kt @@ -43,5 +43,10 @@ class LnkContestExecutionService( .findByContestAndExecutionProjectIdInOrderByExecutionStartTimeDesc(contest, projectIds) .distinctBy { it.execution.project } + /** + * @param execution + * @return a [Contest] under which [execution] has been performed, or `null` if [execution] is not associated + * with any [Contest] + */ fun findContestByExecution(execution: Execution) = lnkContestExecutionRepository.findByExecution(execution)?.contest } diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestProjectService.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestProjectService.kt index bb835a3448..079549f630 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestProjectService.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/LnkContestProjectService.kt @@ -70,6 +70,9 @@ class LnkContestProjectService( true } + /** + * @param newExecution + */ @Transactional fun updateBestExecution(newExecution: Execution) { val newScore = requireNotNull(newExecution.score) { @@ -86,8 +89,8 @@ class LnkContestProjectService( if (oldBestScore == null || oldBestScore <= newScore) { logger.debug { "For project ${project.shortToString()} updating best_score from execution " + - "[id=${lnkContestProject.bestExecution?.id},score=$oldBestScore] to " + - "[id=${newExecution.id},score=$newScore]" + "[id=${lnkContestProject.bestExecution?.id},score=$oldBestScore] to " + + "[id=${newExecution.id},score=$newScore]" } lnkContestProject.bestExecution = newExecution lnkContestProject.bestScore = newExecution.score @@ -96,6 +99,7 @@ class LnkContestProjectService( } companion object { + @Suppress("GENERIC_VARIABLE_WRONG_DECLARATION") private val logger = getLogger<LnkContestProjectService>() } } diff --git a/save-backend/src/test/kotlin/com/saveourtool/save/backend/service/LnkContestProjectServiceTest.kt b/save-backend/src/test/kotlin/com/saveourtool/save/backend/service/LnkContestProjectServiceTest.kt index fe6aa54314..5af7e05d94 100644 --- a/save-backend/src/test/kotlin/com/saveourtool/save/backend/service/LnkContestProjectServiceTest.kt +++ b/save-backend/src/test/kotlin/com/saveourtool/save/backend/service/LnkContestProjectServiceTest.kt @@ -17,6 +17,7 @@ import kotlin.math.abs @ExtendWith(SpringExtension::class) @Import(LnkContestProjectService::class) +@Suppress("UnsafeCallOnNullableType") class LnkContestProjectServiceTest { @Autowired private lateinit var lnkContestProjectService: LnkContestProjectService @MockBean private lateinit var lnkContestProjectRepository: LnkContestProjectRepository @@ -78,4 +79,4 @@ class LnkContestProjectServiceTest { .let { Optional.of(it) } } } -} \ No newline at end of file +} From a664ec752a75a7300c584004dfe204ef3e3f6bb5 Mon Sep 17 00:00:00 2001 From: Peter Trifanov <peter.trifanov@mail.ru> Date: Mon, 5 Sep 2022 16:52:37 +0300 Subject: [PATCH 3/4] Fix test context initialization; move some beans from `@Import` to `@MockBeans` --- .../controller/OrganizationControllerTest.kt | 56 +++++++------------ 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/save-backend/src/test/kotlin/com/saveourtool/save/backend/controller/OrganizationControllerTest.kt b/save-backend/src/test/kotlin/com/saveourtool/save/backend/controller/OrganizationControllerTest.kt index 859ffe8b4b..45b4832dc6 100644 --- a/save-backend/src/test/kotlin/com/saveourtool/save/backend/controller/OrganizationControllerTest.kt +++ b/save-backend/src/test/kotlin/com/saveourtool/save/backend/controller/OrganizationControllerTest.kt @@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.boot.test.mock.mockito.MockBeans import org.springframework.context.annotation.Import import org.springframework.security.test.context.support.WithMockUser import org.springframework.test.context.ActiveProfiles @@ -40,15 +41,29 @@ import java.util.* GitService::class, TestSuitesSourceService::class, TestSuitesService::class, - ExecutionService::class, - AgentStatusService::class, - AgentService::class, WebConfig::class, - ProjectService::class, ProjectPermissionEvaluator::class, LnkUserProjectService::class, UserDetailsService::class, ) +@MockBeans( + MockBean(ExecutionService::class), + MockBean(ProjectService::class), + MockBean(AgentService::class), + MockBean(AgentStatusService::class), + MockBean(TestSuitesSourceRepository::class), + MockBean(TestSuiteRepository::class), + MockBean(TestRepository::class), + MockBean(TestExecutionRepository::class), + MockBean(TestSuitesSourceSnapshotStorage::class), + MockBean(ExecutionRepository::class), + MockBean(AgentStatusRepository::class), + MockBean(AgentRepository::class), + MockBean(ProjectRepository::class), + MockBean(LnkUserProjectRepository::class), + MockBean(OriginalLoginRepository::class), + MockBean(LnkContestProjectService::class), +) @AutoConfigureWebTestClient @Suppress("UnsafeCallOnNullableType") class OrganizationControllerTest { @@ -86,39 +101,6 @@ class OrganizationControllerTest { @MockBean private lateinit var gitRepository: GitRepository - @MockBean - private lateinit var testSuitesSourceRepository: TestSuitesSourceRepository - - @MockBean - private lateinit var testSuiteRepository: TestSuiteRepository - - @MockBean - private lateinit var testRepository: TestRepository - - @MockBean - private lateinit var testExecutionRepository: TestExecutionRepository - - @MockBean - private lateinit var testSuitesSourceSnapshotStorage: TestSuitesSourceSnapshotStorage - - @MockBean - private lateinit var executionRepository: ExecutionRepository - - @MockBean - private lateinit var agentStatusRepository: AgentStatusRepository - - @MockBean - private lateinit var agentRepository: AgentRepository - - @MockBean - private lateinit var projectRepository: ProjectRepository - - @MockBean - private lateinit var lnkUserProjectRepository: LnkUserProjectRepository - - @MockBean - private lateinit var originalLoginRepository: OriginalLoginRepository - @BeforeEach internal fun setUp() { whenever(gitRepository.save(any())).then { From 2d4919476c716bb1d85d9a688211dfd99e766ba2 Mon Sep 17 00:00:00 2001 From: Peter Trifanov <peter.trifanov@mail.ru> Date: Mon, 5 Sep 2022 16:57:25 +0300 Subject: [PATCH 4/4] Code style --- .../commonMain/kotlin/com/saveourtool/save/entities/Project.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/Project.kt b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/Project.kt index 2ccafe2253..92b22fc6cf 100644 --- a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/Project.kt +++ b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/Project.kt @@ -79,6 +79,9 @@ data class Project( email ?: "", ) + /** + * Return the shortest unique representation of this [Project] as a string + */ fun shortToString() = "[organization=${organization.name},name=$name]" companion object {