Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update project's best execution in a contest when execution is finished #1162

Merged
merged 7 commits into from
Sep 5, 2022
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need bestScore as separate field on lnkContestProject now? it can be replaced to bestExecution?.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
Fixed Show fixed Hide fixed
})
}

@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
Fixed Show fixed Hide fixed
})
}

@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]"
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed

companion object {
/**
* Create a stub for testing. Since all fields are mutable, only required ones can be set after calling this method.
Expand Down