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..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 @@ -53,4 +53,10 @@ interface LnkContestExecutionRepository : BaseEntityRepository 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..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 @@ -42,4 +42,11 @@ class LnkContestExecutionService( fun getLatestExecutionByContestAndProjectIds(contest: Contest, projectIds: List) = lnkContestExecutionRepository .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 b977638197..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 @@ -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,37 @@ class LnkContestProjectService( lnkContestProjectRepository.save(LnkContestProject(project, contest, null, 0.0)) true } + + /** + * @param newExecution + */ + @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 { + @Suppress("GENERIC_VARIABLE_WRONG_DECLARATION") + private val logger = getLogger() + } } 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 { 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..5af7e05d94 --- /dev/null +++ b/save-backend/src/test/kotlin/com/saveourtool/save/backend/service/LnkContestProjectServiceTest.kt @@ -0,0 +1,82 @@ +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) +@Suppress("UnsafeCallOnNullableType") +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) } + } + } +} 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..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,11 @@ data class Project( email ?: "", ) + /** + * Return the shortest unique representation of this [Project] as a string + */ + 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.