Skip to content

Commit

Permalink
Update project's best execution in a contest when execution is finish…
Browse files Browse the repository at this point in the history
…ed (#1162)

* Update project's best execution in a contest when execution is finished
* Move some beans from `@Import` to `@MockBeans`

Part of #1115
  • Loading branch information
petertrr authored Sep 5, 2022
1 parent 46af018 commit 2fd153f
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,10 @@ interface LnkContestExecutionRepository : BaseEntityRepository<LnkContestExecuti
* @return [LnkContestExecution] associated with [project] and [Contest] with name [contestName]
*/
fun findByExecutionProjectAndContestName(project: Project, contestName: String): LnkContestExecution?

/**
* @param execution
* @return [LnkContestExecution] if any is associated with [execution]
*/
fun findByExecution(execution: Execution): LnkContestExecution?
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,11 @@ class LnkContestExecutionService(
fun getLatestExecutionByContestAndProjectIds(contest: Contest, projectIds: List<Long>) = 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
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ 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]
*/
@Service
class LnkContestProjectService(
private val lnkContestProjectRepository: LnkContestProjectRepository,
private val lnkContestExecutionService: LnkContestExecutionService,
) {
/**
* @param contestName name of a [Contest]
Expand Down Expand Up @@ -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<LnkContestProjectService>()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 2fd153f

Please sign in to comment.