diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/RunExecutionController.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/RunExecutionController.kt index 7560b98e13..a03534fa86 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/RunExecutionController.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/RunExecutionController.kt @@ -18,6 +18,7 @@ import com.saveourtool.save.utils.DATABASE_DELIMITER import com.saveourtool.save.utils.debug import com.saveourtool.save.utils.getLogger import com.saveourtool.save.utils.switchIfEmptyToResponseException +import com.saveourtool.save.v1 import io.micrometer.core.instrument.MeterRegistry import org.slf4j.Logger import org.springframework.boot.web.reactive.function.client.WebClientCustomizer @@ -38,48 +39,25 @@ import reactor.core.publisher.Mono import reactor.core.scheduler.Schedulers @RestController -@RequestMapping("/api/run") +@RequestMapping("/api/$v1/run") class RunExecutionController( private val projectService: ProjectService, private val executionService: ExecutionService, private val executionInfoStorage: ExecutionInfoStorage, private val testService: TestService, private val testExecutionService: TestExecutionService, - private val contestService: ContestService, private val meterRegistry: MeterRegistry, configProperties: ConfigProperties, objectMapper: ObjectMapper, -// kotlinSerializationWebClientCustomizer: WebClientCustomizer, ) { private val webClientOrchestrator = WebClient.builder() .baseUrl(configProperties.orchestratorUrl) .codecs { it.defaultCodecs().multipartCodecs().encoder(Jackson2JsonEncoder(objectMapper)) } -// .apply(kotlinSerializationWebClientCustomizer::customize) .build() private val scheduler = Schedulers.boundedElastic() - - @PostMapping("/triggerByContest") - fun triggerByContestId( - @RequestBody originalRequest: ExecutionRunRequestByContest, - authentication: Authentication, - ): Mono = justOrNotFound(contestService.findById(originalRequest.contestId)) - .map { contest -> - ExecutionRunRequest( - projectCoordinates = originalRequest.projectCoordinates, - testSuiteIds = contest.testSuiteIds.split(DATABASE_DELIMITER).map { it.toLong() }, - files = originalRequest.files, - sdk = originalRequest.sdk, - execCmd = originalRequest.execCmd, - batchSizeForAnalyzer = originalRequest.batchSizeForAnalyzer, - ) - } - .flatMap { - trigger(it, authentication) - } - @PostMapping("/trigger") fun trigger( @RequestBody request: ExecutionRunRequest, diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/TestSuitesSourceController.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/TestSuitesSourceController.kt index a7031cb603..a81f2bf4ac 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/TestSuitesSourceController.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/TestSuitesSourceController.kt @@ -1,7 +1,9 @@ package com.saveourtool.save.backend.controllers import com.saveourtool.save.backend.ByteBufferFluxResponse +import com.saveourtool.save.backend.StringResponse import com.saveourtool.save.backend.configs.ApiSwaggerSupport +import com.saveourtool.save.backend.configs.ConfigProperties import com.saveourtool.save.backend.configs.RequiresAuthorizationSourceHeader import com.saveourtool.save.backend.service.* import com.saveourtool.save.backend.storage.TestSuitesSourceSnapshotStorage @@ -18,6 +20,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse import io.swagger.v3.oas.annotations.tags.Tag import io.swagger.v3.oas.annotations.tags.Tags import org.slf4j.Logger +import org.springframework.boot.web.reactive.function.client.WebClientCustomizer import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.http.ResponseEntity @@ -25,6 +28,8 @@ import org.springframework.http.codec.multipart.Part import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.core.Authentication import org.springframework.web.bind.annotation.* +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.toEntity import reactor.core.publisher.Mono import reactor.kotlin.core.publisher.toMono import reactor.kotlin.core.util.function.component1 @@ -48,7 +53,14 @@ class TestSuitesSourceController( private val organizationService: OrganizationService, private val gitService: GitService, private val executionService: ExecutionService, + configProperties: ConfigProperties, + jackson2WebClientCustomizer: WebClientCustomizer, ) { + private val preprocessorWebClient = WebClient.builder() + .apply(jackson2WebClientCustomizer::customize) + .baseUrl(configProperties.preprocessorUrl) + .build() + /** * @param organizationName * @return list of [TestSuitesSourceDto] found by provided values or empty response @@ -503,15 +515,23 @@ class TestSuitesSourceController( authentication: Authentication, ): Mono> = testSuitesSourceService.getOrganizationsWithPublicTestSuiteSources().toMono() + @PostMapping("/api/$v1/test-suites-sources/{organizationName}/{name}/fetch") fun triggerFetch( @PathVariable organizationName: String, @PathVariable name: String, authentication: Authentication, - ): ResponseEntity { - testSuitesSourceService.findByName(organizationName, name) - return ResponseEntity.accepted() - .body(Unit) - } + ): Mono = blockingToMono { testSuitesSourceService.findByName(organizationName, name) } + .flatMap { + preprocessorWebClient.post() + .uri("/test-suites-sources/fetch") + .bodyValue(it.toDto()) + .retrieve() + .toEntity() + } + .map { + ResponseEntity.ok() + .body("Trigger fetching new tests from $name in $organizationName") + } private fun TestSuitesSourceDto.downloadSnapshot( version: String 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 e0cba495ab..ec55e077e8 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 @@ -259,7 +259,8 @@ class ExecutionService( execCmd = execCmd, batchSizeForAnalyzer = batchSizeForAnalyzer, ) - log.info("Creating a new execution id=${execution.id} for project id=${project.id}") - return execution + val savedExecution = executionRepository.save(execution) + log.info("Created a new execution id=${savedExecution.id} for project id=${project.id}") + return savedExecution } } diff --git a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/ExecutionRunRequest.kt b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/ExecutionRunRequest.kt index ec6cf0052e..1acdee316d 100644 --- a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/ExecutionRunRequest.kt +++ b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/entities/ExecutionRunRequest.kt @@ -3,7 +3,9 @@ package com.saveourtool.save.entities import com.saveourtool.save.domain.FileInfo import com.saveourtool.save.domain.ProjectCoordinates import com.saveourtool.save.domain.Sdk +import kotlinx.serialization.Serializable +@Serializable data class ExecutionRunRequest( val projectCoordinates: ProjectCoordinates, diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/TestResourcesSelection.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/TestResourcesSelection.kt index cabc5d400b..230394a067 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/TestResourcesSelection.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/TestResourcesSelection.kt @@ -6,6 +6,7 @@ package com.saveourtool.save.frontend.components.basic +import com.saveourtool.save.entities.ContestDto import com.saveourtool.save.entities.GitDto import com.saveourtool.save.frontend.components.views.ProjectView import com.saveourtool.save.frontend.externals.fontawesome.faQuestionCircle @@ -49,6 +50,8 @@ external interface TestResourcesProps : PropsWithChildren { var projectName: String var organizationName: String var onContestEnrollerResponse: (String) -> Unit + var availableContests: List + var selectedContest: ContestDto // properties for CUSTOM_TESTS mode var availableGitCredentials: List @@ -130,6 +133,7 @@ fun testResourcesSelection( setExecCmd: (String) -> Unit, setBatchSize: (String) -> Unit, setSelectedLanguageForStandardTests: (String) -> Unit, + updateContestFromInputField: (ContestDto) -> Unit, ) = FC { props -> val (isContestEnrollerOpen, setIsContestEnrollerOpen) = useState(false) showContestEnrollerModal( @@ -336,11 +340,30 @@ fun testResourcesSelection( div { className = ClassName(cardStyleByTestingType(props, TestingType.CONTEST_MODE)) div { - className = ClassName("card-body control-label col-auto justify-content-between justify-content-center font-weight-bold text-danger mb-4 pl-0") - +"Stay turned! Soon you will be able to run your tool in contest mode!" + className = ClassName("input-group-prepend") + + select { + className = ClassName("form-control") + props.availableContests.forEach { + option { + +it.label() + } + } + required = true + value = props.selectedContest.label() + onChange = { event -> + val selectedContestLabel = event.target.value + val selectedContest = requireNotNull(props.availableContests.find { it.label() == selectedContestLabel }) { + "Invalid contest is selected $selectedContestLabel" + } + updateContestFromInputField(selectedContest) + } + } } } } +private fun ContestDto.label(): String = "$organizationName/$name" + private fun cardStyleByTestingType(props: TestResourcesProps, testingType: TestingType) = if (props.testingType == testingType) "card shadow mb-4 w-100" else "d-none" diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/ProjectView.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/ProjectView.kt index 7e226f66e8..7cb497ae73 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/ProjectView.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/views/ProjectView.kt @@ -62,10 +62,21 @@ external interface ProjectExecutionRouteProps : PropsWithChildren { var currentUserInfo: UserInfo? } +external interface StateWithRole : State { + /** + * Role of a user that is seeing this view + */ + var selfRole: Role +} + +external interface StandardTestsState : State { + +} + /** * [State] of project view component */ -external interface ProjectViewState : State { +external interface ProjectViewState : StateWithRole { /** * Currently loaded for display Project */ @@ -206,15 +217,21 @@ external interface ProjectViewState : State { */ var closeButtonLabel: String? - /** - * Role of a user that is seeing this view - */ - var selfRole: Role /** * File for delete */ var file: FileInfo + + /** + * + */ + var selectedContest: ContestDto + + /** + * + */ + var availableContests: List } /** @@ -258,6 +275,11 @@ class ProjectView : AbstractView(f setState { selectedLanguageForStandardTests = it } + }, + updateContestFromInputField = { + setState { + selectedContest = it + } } ) private val projectInfo = projectInfo( @@ -293,6 +315,8 @@ class ProjectView : AbstractView(f ) state.selectedGitCredential = GitDto("N/A") state.availableGitCredentials = emptyList() + state.selectedContest = ContestDto.empty + state.availableContests = emptyList() state.gitBranchOrCommitFromInputField = "" state.execCmd = "" state.batchSizeForAnalyzer = "" @@ -370,6 +394,13 @@ class ProjectView : AbstractView(f availableGitCredentials = gitCredentials gitCredentials.firstOrNull()?.let { selectedGitCredential = it } } + + val contests = getContests() + setState { + availableContests = contests + contests.firstOrNull()?.let { selectedContest = it } + } + fetchLatestExecutionId() } } @@ -378,6 +409,7 @@ class ProjectView : AbstractView(f private fun submitExecutionRequest() { when (state.testingType) { TestingType.CUSTOM_TESTS -> submitExecutionRequestWithCustomTests() + TestingType.CONTEST_MODE -> submitExecutionRequestByContest() else -> { if (selectedStandardSuites.isEmpty()) { setState { @@ -430,6 +462,19 @@ class ProjectView : AbstractView(f submitRequest("/submitExecutionRequest", Headers(), formData) } + private fun submitExecutionRequestByContest() { + val selectedSdk = "${state.selectedSdk}:${state.selectedSdkVersion}".toSdk() + val executionRequest = ExecutionRunRequest( + projectCoordinates = ProjectCoordinates(state.project.organization.name, state.project.name), + testSuiteIds = state.selectedContest.testSuiteIds, + files = state.files.toList(), + sdk = selectedSdk, + execCmd = state.execCmd, + batchSizeForAnalyzer = state.batchSizeForAnalyzer, + ) + submitRequest("/run/trigger", jsonHeaders, Json.encodeToString(executionRequest)) + } + private fun submitRequest(url: String, headers: Headers, body: dynamic) { scope.launch { val response = post( @@ -618,6 +663,7 @@ class ProjectView : AbstractView(f testingType = state.testingType isSubmitButtonPressed = state.isSubmitButtonPressed gitDto = gitDto + // properties for CONTEST_TESTS mode projectName = props.name organizationName = props.owner onContestEnrollerResponse = { @@ -627,6 +673,8 @@ class ProjectView : AbstractView(f errorLabel = "Contest enrollment" } } + selectedContest = state.selectedContest + availableContests = state.availableContests // properties for CUSTOM_TESTS mode testRootPath = state.testRootPath selectedGitCredential = state.selectedGitCredential @@ -968,6 +1016,15 @@ class ProjectView : AbstractView(f it.decodeFromJsonString>() } + private suspend fun getContests() = get( + "$apiUrl/contests/active", + Headers(), + loadingHandler = ::noopLoadingHandler, + ) + .unsafeMap { + it.decodeFromJsonString>() + } + companion object : RStatics>(ProjectView::class) { const val TEST_ROOT_DIR_HINT = """