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 {