diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/SaveApplication.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/SaveApplication.kt index 9e1e496721..9299c41a7d 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/SaveApplication.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/SaveApplication.kt @@ -14,7 +14,6 @@ typealias StringResponse = ResponseEntity typealias EmptyResponse = ResponseEntity typealias ByteBufferFluxResponse = ResponseEntity> typealias ResourceResponse = ResponseEntity -typealias IdResponse = ResponseEntity /** * An entrypoint for spring for save-backend 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 6536d9dc74..eb594fef65 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 @@ -1,12 +1,13 @@ package com.saveourtool.save.backend.controllers import com.fasterxml.jackson.databind.ObjectMapper -import com.saveourtool.save.backend.IdResponse +import com.saveourtool.save.backend.StringResponse import com.saveourtool.save.backend.configs.ConfigProperties import com.saveourtool.save.backend.service.* import com.saveourtool.save.backend.storage.ExecutionInfoStorage import com.saveourtool.save.backend.utils.blockingToMono import com.saveourtool.save.backend.utils.username +import com.saveourtool.save.domain.ProjectCoordinates import com.saveourtool.save.entities.Execution import com.saveourtool.save.entities.ExecutionRunRequest import com.saveourtool.save.execution.ExecutionStatus @@ -14,6 +15,7 @@ import com.saveourtool.save.execution.ExecutionUpdateDto import com.saveourtool.save.permission.Permission import com.saveourtool.save.utils.debug import com.saveourtool.save.utils.getLogger +import com.saveourtool.save.utils.switchIfEmptyToNotFound import com.saveourtool.save.utils.switchIfEmptyToResponseException import com.saveourtool.save.v1 import io.micrometer.core.instrument.MeterRegistry @@ -27,6 +29,7 @@ import org.springframework.security.core.Authentication import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import org.springframework.web.reactive.function.BodyInserters import org.springframework.web.reactive.function.client.WebClient @@ -58,17 +61,11 @@ class RunExecutionController( fun trigger( @RequestBody request: ExecutionRunRequest, authentication: Authentication, - ): Mono = with(request.projectCoordinates) { - // Project cannot be taken from executionRequest directly for permission evaluation: - // it can be fudged by user, who submits it. We should get project from DB based on name/owner combination. - projectService.findWithPermissionByNameAndOrganization(authentication, projectName, organizationName, Permission.WRITE) - } - .switchIfEmptyToResponseException(HttpStatus.FORBIDDEN) { - "User ${authentication.username()} doesn't have access to ${request.projectCoordinates}" - } - .map { project -> + ): Mono = Mono.just(request.projectCoordinates) + .validateAccess(authentication) { it } + .map { executionService.createNew( - project = project, + projectCoordinates = request.projectCoordinates, testSuiteIds = request.testSuiteIds, files = request.files, username = authentication.username(), @@ -79,44 +76,87 @@ class RunExecutionController( } .subscribeOn(Schedulers.boundedElastic()) .flatMap { execution -> - val executionId = execution.requiredId() - Mono.just(ResponseEntity.accepted().body(executionId)) + Mono.just(execution.toAcceptedResponse()) .doOnSuccess { - blockingToMono { - val tests = request.testSuiteIds.flatMap { testService.findTestsByTestSuiteId(it) } - log.debug { "Received the following test ids for saving test execution under executionId=$executionId: ${tests.map { it.requiredId() }}" } - meterRegistry.timer("save.backend.saveTestExecution").record { - testExecutionService.saveTestExecutions(execution, tests) - } - } - .flatMap { - initializeAgents(execution) - .map { - log.debug { "Initialized agents for execution $executionId" } - } - } - .onErrorResume { ex -> - val failReason = "Error during preprocessing. Reason: ${ex.message}" - log.error( - "$failReason, will mark execution.id=$executionId as failed; error: ", - ex - ) - val executionUpdateDto = ExecutionUpdateDto( - executionId, - ExecutionStatus.ERROR, - failReason - ) - blockingToMono { - executionService.updateExecutionStatus(execution, executionUpdateDto.status) - }.flatMap { - executionInfoStorage.upsertIfRequired(executionUpdateDto) - } - } - .subscribeOn(scheduler) - .subscribe() + asyncTrigger(execution) + } + } + + @PostMapping("/reTrigger") + fun reTrigger( + @RequestParam executionId: Long, + authentication: Authentication, + ): Mono = blockingToMono { executionService.findExecution(executionId) } + .switchIfEmptyToNotFound { "Not found execution id = $executionId" } + .validateAccess(authentication) { execution -> + ProjectCoordinates( + execution.project.organization.name, + execution.project.name + ) + } + .map { executionService.createNewCopy(it, authentication.username()) } + .flatMap { execution -> + Mono.just(execution.toAcceptedResponse()) + .doOnSuccess { + asyncTrigger(execution) } } + private fun Mono.validateAccess( + authentication: Authentication, + projectCoordinatesGetter: (T) -> ProjectCoordinates, + ): Mono = + flatMap { value -> + val projectCoordinates = projectCoordinatesGetter(value) + with(projectCoordinates) { + projectService.findWithPermissionByNameAndOrganization( + authentication, + projectName, + organizationName, + Permission.WRITE + ) + }.switchIfEmptyToResponseException(HttpStatus.FORBIDDEN) { + "User ${authentication.username()} doesn't have access to $projectCoordinates" + }.map { value } + } + + private fun asyncTrigger(execution: Execution) { + val executionId = execution.requiredId() + blockingToMono { + val tests = execution.parseAndGetTestSuiteIds() + .orEmpty() + .flatMap { testService.findTestsByTestSuiteId(it) } + log.debug { "Received the following test ids for saving test execution under executionId=$executionId: ${tests.map { it.requiredId() }}" } + meterRegistry.timer("save.backend.saveTestExecution").record { + testExecutionService.saveTestExecutions(execution, tests) + } + } + .flatMap { + initializeAgents(execution) + .map { + log.debug { "Initialized agents for execution $executionId" } + } + } + .onErrorResume { ex -> + val failReason = "Error during preprocessing. Reason: ${ex.message}" + log.error( + "$failReason, will mark execution.id=$executionId as failed; error: ", + ex + ) + val executionUpdateDto = ExecutionUpdateDto( + executionId, + ExecutionStatus.ERROR, + failReason + ) + blockingToMono { + executionService.updateExecutionStatus(execution, executionUpdateDto.status) + }.flatMap { + executionInfoStorage.upsertIfRequired(executionUpdateDto) + } + } + .subscribeOn(scheduler) + .subscribe() + } /** * POST request to orchestrator to initiate its work @@ -135,6 +175,9 @@ class RunExecutionController( .toEntity() } + private fun Execution.toAcceptedResponse(): StringResponse = + ResponseEntity.accepted().body("Clone pending, execution id is ${requiredId()}") + companion object { private val log: Logger = getLogger() } diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/internal/TestController.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/internal/TestController.kt index ae5d90c716..ff223c0f23 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/internal/TestController.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/internal/TestController.kt @@ -1,9 +1,7 @@ package com.saveourtool.save.backend.controllers.internal -import com.saveourtool.save.backend.service.TestExecutionService import com.saveourtool.save.backend.service.TestService import com.saveourtool.save.backend.storage.TestSuitesSourceSnapshotStorage -import com.saveourtool.save.entities.Test import com.saveourtool.save.test.TestDto import com.saveourtool.save.test.TestFilesContent import com.saveourtool.save.test.TestFilesRequest @@ -28,7 +26,6 @@ import reactor.core.publisher.Mono @RequestMapping("/internal") class TestController( private val testService: TestService, - private val testExecutionService: TestExecutionService, private val meterRegistry: MeterRegistry, private val testSuitesSourceSnapshotStorage: TestSuitesSourceSnapshotStorage, ) { @@ -44,26 +41,6 @@ class TestController( } } - /** - * @param executionId ID of the [Execution][com.saveourtool.save.entities.Execution], - * all tests are initialized for this execution will be executed (creating an instance of [TestExecution][com.saveourtool.save.entities.TestExecution]) - */ - @PostMapping("/executeTestsByExecutionId") - fun executeTestsByExecutionId(@RequestParam executionId: Long) { - val testIds = testService.findTestsByExecutionId(executionId).map { it.requiredId() } - log.debug { "Received the following test ids for saving test execution under executionId=$executionId: $testIds" } - meterRegistry.timer("save.backend.saveTestExecution").record { - testExecutionService.saveTestExecutionsByTestIds(executionId, testIds) - } - } - - /** - * @param testSuiteId ID of the [TestSuite], for which all corresponding tests will be returned - * @return list of tests - */ - @GetMapping("/getTestsByTestSuiteId") - fun getTestsByTestSuiteId(@RequestParam testSuiteId: Long): List = testService.findTestsByTestSuiteId(testSuiteId) - /** * @param agentId * @return test batches diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/scheduling/UpdateJob.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/scheduling/UpdateJob.kt index 4b4c5d8988..8dd45fa829 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/scheduling/UpdateJob.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/scheduling/UpdateJob.kt @@ -1,28 +1,37 @@ package com.saveourtool.save.backend.scheduling import com.saveourtool.save.backend.configs.ConfigProperties +import com.saveourtool.save.backend.service.TestSuitesSourceService import org.quartz.Job import org.quartz.JobExecutionContext import org.quartz.JobKey import org.slf4j.LoggerFactory import org.springframework.web.reactive.function.client.WebClient +import reactor.kotlin.core.publisher.toFlux import java.time.Duration /** * a [Job] that commands preprocessor to update standard test suites */ class UpdateJob( - configProperties: ConfigProperties + private val testSuitesSourceService: TestSuitesSourceService, + configProperties: ConfigProperties, ) : Job { private val preprocessorWebClient = WebClient.create(configProperties.preprocessorUrl) @Suppress("MagicNumber") override fun execute(context: JobExecutionContext?) { logger.info("Running job $jobKey") - preprocessorWebClient.post() - .uri("/uploadStandardTestSuite") - .retrieve() - .toBodilessEntity() + testSuitesSourceService.getStandardTestSuitesSources() + .toFlux() + .flatMap { testSuitesSource -> + preprocessorWebClient.post() + .uri("/test-suites-sources/fetch") + .bodyValue(testSuitesSource) + .retrieve() + .toBodilessEntity() + } + .collectList() .block(Duration.ofSeconds(10)) } 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 01eb6b15c1..70c13056a6 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 @@ -28,6 +28,7 @@ import java.util.Optional @Service class ExecutionService( private val executionRepository: ExecutionRepository, + private val projectService: ProjectService, private val userRepository: UserRepository, private val testRepository: TestRepository, private val testExecutionRepository: TestExecutionRepository, @@ -219,15 +220,66 @@ class ExecutionService( }) } + @Suppress("LongParameterList") @Transactional fun createNew( - project: Project, + projectCoordinates: ProjectCoordinates, testSuiteIds: List, - files: List, + files: List, username: String, sdk: Sdk, execCmd: String?, batchSizeForAnalyzer: String?, + ): Execution { + val project = with(projectCoordinates) { + projectService.findByNameAndOrganizationName(projectName, organizationName).orNotFound { + "Not found project $projectName in $organizationName" + } + } + return doCreateNew( + project = project, + formattedTestSuiteIds = Execution.formatTestSuiteIds(testSuiteIds), + version = testSuitesService.getSingleVersionByIds(testSuiteIds), + allTests = testSuiteIds.flatMap { testRepository.findAllByTestSuiteId(it) } + .count() + .toLong(), + additionalFiles = files.format(), + username = username, + sdk = sdk.toString(), + execCmd = execCmd, + batchSizeForAnalyzer = batchSizeForAnalyzer, + ) + } + + @Transactional + fun createNewCopy( + execution: Execution, + username: String, + ): Execution { + return doCreateNew( + project = execution.project, + formattedTestSuiteIds = execution.testSuiteIds, + version = execution.version, + allTests = execution.allTests, + additionalFiles = execution.additionalFiles, + username = username, + sdk = execution.sdk, + execCmd = execution.execCmd, + batchSizeForAnalyzer = execution.batchSizeForAnalyzer, + ) + } + + @Suppress("LongParameterList") + fun doCreateNew( + project: Project, + formattedTestSuiteIds: String?, + version: String?, + allTests: Long, + additionalFiles: String, + username: String, + sdk: String, + execCmd: String?, + batchSizeForAnalyzer: String?, ): Execution { val user = userRepository.findByName(username).orNotFound { "Not found user $username" @@ -237,14 +289,12 @@ class ExecutionService( startTime = LocalDateTime.now(), endTime = null, status = ExecutionStatus.PENDING, - testSuiteIds = Execution.formatTestSuiteIds(testSuiteIds), + testSuiteIds = formattedTestSuiteIds, batchSize = configProperties.initialBatchSize, // FIXME: remove this type type = ExecutionType.GIT, - version = testSuitesService.getSingleVersionByIds(testSuiteIds), - allTests = testSuiteIds.flatMap { testRepository.findAllByTestSuiteId(it) } - .count() - .toLong(), + version = version, + allTests = allTests, runningTests = 0, passedTests = 0, failedTests = 0, @@ -253,8 +303,8 @@ class ExecutionService( matchedChecks = 0, expectedChecks = 0, unexpectedChecks = 0, - sdk = sdk.toString(), - additionalFiles = files.map { it.toStorageKey() }.format(), + sdk = sdk, + additionalFiles = additionalFiles, user = user, execCmd = execCmd, batchSizeForAnalyzer = batchSizeForAnalyzer, diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/UserDetailsService.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/UserDetailsService.kt index 7476b077da..46540ebb3b 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/UserDetailsService.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/UserDetailsService.kt @@ -4,6 +4,7 @@ import com.saveourtool.save.backend.repository.UserRepository import com.saveourtool.save.domain.Role import com.saveourtool.save.entities.User import com.saveourtool.save.utils.IdentitySourceAwareUserDetails +import com.saveourtool.save.utils.orNotFound import org.slf4j.LoggerFactory import org.springframework.security.core.Authentication import org.springframework.security.core.userdetails.ReactiveUserDetailsService @@ -45,9 +46,11 @@ class UserDetailsService( * @throws NoSuchElementException */ fun saveAvatar(name: String, relativePath: String) { - val user = userRepository.findByName(name)!!.apply { - avatar = relativePath - } + val user = userRepository.findByName(name) + .orNotFound() + .apply { + avatar = relativePath + } user.let { userRepository.save(it) } } diff --git a/save-preprocessor/src/main/kotlin/com/saveourtool/save/preprocessor/controllers/DownloadProjectController.kt b/save-preprocessor/src/main/kotlin/com/saveourtool/save/preprocessor/controllers/DownloadProjectController.kt deleted file mode 100644 index 0df1ab6d41..0000000000 --- a/save-preprocessor/src/main/kotlin/com/saveourtool/save/preprocessor/controllers/DownloadProjectController.kt +++ /dev/null @@ -1,354 +0,0 @@ -package com.saveourtool.save.preprocessor.controllers - -import com.saveourtool.save.entities.* -import com.saveourtool.save.execution.ExecutionInitializationDto -import com.saveourtool.save.execution.ExecutionStatus -import com.saveourtool.save.execution.ExecutionUpdateDto -import com.saveourtool.save.preprocessor.StatusResponse -import com.saveourtool.save.preprocessor.TextResponse -import com.saveourtool.save.preprocessor.config.ConfigProperties -import com.saveourtool.save.preprocessor.service.TestsPreprocessorToBackendBridge -import com.saveourtool.save.preprocessor.utils.* -import com.saveourtool.save.testsuite.TestSuitesSourceDto -import com.saveourtool.save.testsuite.TestSuitesSourceSnapshotKey -import com.saveourtool.save.utils.info - -import com.fasterxml.jackson.databind.ObjectMapper -import org.slf4j.LoggerFactory -import org.springframework.boot.web.reactive.function.client.WebClientCustomizer -import org.springframework.http.HttpStatus -import org.springframework.http.MediaType -import org.springframework.http.ReactiveHttpOutputMessage -import org.springframework.http.ResponseEntity -import org.springframework.http.client.MultipartBodyBuilder -import org.springframework.http.codec.json.Jackson2JsonEncoder -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController -import org.springframework.web.reactive.function.BodyInserter -import org.springframework.web.reactive.function.BodyInserters -import org.springframework.web.reactive.function.client.WebClient -import org.springframework.web.reactive.function.client.bodyToMono -import org.springframework.web.reactive.function.client.toEntity -import org.springframework.web.server.ResponseStatusException -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono -import reactor.core.scheduler.Schedulers -import reactor.netty.http.client.HttpClientRequest - -import java.time.Duration - -import kotlin.io.path.ExperimentalPathApi - -typealias Status = Mono> - -/** - * A Spring controller for git project downloading - * - * @property configProperties config properties - */ -@OptIn(ExperimentalPathApi::class) -@RestController -class DownloadProjectController( - private val configProperties: ConfigProperties, - private val objectMapper: ObjectMapper, - kotlinSerializationWebClientCustomizer: WebClientCustomizer, - private val testSuitesPreprocessorController: TestSuitesPreprocessorController, - private val testsPreprocessorToBackendBridge: TestsPreprocessorToBackendBridge, -) { - private val log = LoggerFactory.getLogger(DownloadProjectController::class.java) - private val webClientBackend = WebClient.builder() - .baseUrl(configProperties.backend) - .apply(kotlinSerializationWebClientCustomizer::customize) - .build() - private val webClientOrchestrator = WebClient.builder() - .baseUrl(configProperties.orchestrator) - .codecs { - it.defaultCodecs().multipartCodecs().encoder(Jackson2JsonEncoder(objectMapper)) - } - .apply(kotlinSerializationWebClientCustomizer::customize) - .build() - private val scheduler = Schedulers.boundedElastic() - - /** - * @param executionRequest Dto of repo information to clone and project info - * @return response entity with text - */ - @Suppress("TOO_LONG_FUNCTION") - @PostMapping("/upload") - fun upload( - @RequestBody executionRequest: ExecutionRequest, - ): Mono = executionRequest.toAcceptedResponseMono() - .doOnSuccess { - Mono.fromCallable { - val (selectedBranch, selectedVersion) = with(executionRequest.branchOrCommit) { - if (isNullOrBlank()) { - null to null - } else if (startsWith("origin/")) { - replaceFirst("origin/", "") to null - } else { - null to this - } - } - val branch = selectedBranch ?: executionRequest.gitDto.detectDefaultBranchName() - val version = selectedVersion ?: executionRequest.gitDto.detectLatestSha1(branch) - branch to version - } - .flatMapMany { (branch, version) -> - // search or create new test suites source by content - testsPreprocessorToBackendBridge.getOrCreateTestSuitesSource( - executionRequest.project.organization.name, - executionRequest.gitDto.url, - executionRequest.testRootPath, - branch, - ).mapToTestSuites(version) - } - .saveOnExecutionAndExecute(executionRequest) - .subscribeOn(scheduler) - .subscribe() - } - - /** - * @param executionRequestForStandardSuites Dto of binary file, test suites names and project info - * @return response entity with text - */ - @PostMapping(value = ["/uploadBin"]) - fun uploadBin( - @RequestBody executionRequestForStandardSuites: ExecutionRequestForStandardSuites, - ) = executionRequestForStandardSuites.toAcceptedResponseMono() - .doOnSuccess { - testsPreprocessorToBackendBridge.getStandardTestSuitesSources() - .flatMapMany { Flux.fromIterable(it) } - .flatMap { testSuitesSource -> - testsPreprocessorToBackendBridge.listTestSuitesSourceVersions(testSuitesSource) - .map { keys -> - keys.maxByOrNull(TestSuitesSourceSnapshotKey::creationTimeInMills)?.version - ?: throw IllegalStateException("Failed to detect latest version for $testSuitesSource") - } - .flatMapMany { testSuitesSource.getTestSuites(it) } - } - .saveOnExecutionAndExecute(executionRequestForStandardSuites) - .subscribeOn(scheduler) - .subscribe() - } - - private fun Mono.mapToTestSuites( - version: String, - ): Flux = flatMapMany { it.fetchAndGetTestSuites(version) } - - private fun TestSuitesSourceDto.fetchAndGetTestSuites( - version: String, - ): Flux = testSuitesPreprocessorController.fetch(this, version) - .flatMapMany { getTestSuites(version) } - - private fun TestSuitesSourceDto.getTestSuites( - version: String, - ): Flux = testsPreprocessorToBackendBridge.getTestSuites( - organizationName, - name, - version - ).flatMapMany { Flux.fromIterable(it) } - - private fun Flux.saveOnExecutionAndExecute( - requestBase: ExecutionRequestBase, - ) = this - .filter(requestBase.getTestSuiteFilter()) - .collectList() - .flatMap { testSuites -> - require(testSuites.isNotEmpty()) { - "No test suite is selected" - } - testSuites.map { it.source } - .distinctBy { it.requiredId() } - .also { sources -> - require(sources.size == 1) { - "Only a single test suites source is allowed for a run, but got: $sources" - } - } - val version = testSuites.map { it.version } - .distinct() - .also { versions -> - require(versions.size == 1) { - "Only a single version is supported, but got: $versions" - } - } - .single() - updateExecution( - requestBase, - version, - testSuites.map { it.requiredId() }, - ) - } - .flatMap { it.executeTests() } - - /** - * Accept execution rerun request - * - * @param executionId ID of [Execution] - * @return status 202 - */ - @Suppress("UnsafeCallOnNullableType", "TOO_LONG_FUNCTION") - @PostMapping("/rerunExecution") - fun rerunExecution(@RequestParam("id") executionId: Long) = Mono.fromCallable { - ResponseEntity("Clone pending", HttpStatus.ACCEPTED) - } - .doOnSuccess { - updateExecutionStatus(executionId, ExecutionStatus.PENDING) - .flatMap { cleanupInOrchestrator(executionId) } - .flatMap { getExecution(executionId) } - .doOnNext { - log.info { "Skip initializing tests for execution.id = ${it.id}: it's rerun" } - } - .flatMap { it.executeTests() } - .subscribeOn(scheduler) - .subscribe() - } - - /** - * Controller to download standard test suites - * - * @return Empty response entity - */ - @Suppress("TOO_LONG_FUNCTION", "TYPE_ALIAS") - @PostMapping("/uploadStandardTestSuite") - fun uploadStandardTestSuite() = - Mono.just(ResponseEntity("Upload standard test suites pending...\n", HttpStatus.ACCEPTED)) - .doOnSuccess { - testsPreprocessorToBackendBridge.getStandardTestSuitesSources() - .flatMapMany { Flux.fromIterable(it) } - .flatMap { testSuitesSourceDto -> - testSuitesPreprocessorController.fetch(testSuitesSourceDto) - }.doOnError { - log.error("Error to update standard test suite sources") - } - .subscribeOn(scheduler) - .subscribe() - } - - /** - * Execute tests by execution id: - * - Post request to backend to find all tests by test suite id which are set in execution and create TestExecutions for them - * - Send a request to orchestrator to initialize agents and start tests execution - */ - @Suppress( - "LongParameterList", - "TOO_MANY_PARAMETERS", - "UnsafeCallOnNullableType" - ) - private fun Execution.executeTests(): Mono = webClientBackend.post() - .uri("/executeTestsByExecutionId?executionId=$id") - .retrieve() - .toBodilessEntity() - .then(initializeAgents(this)) - .onErrorResume { ex -> - val failReason = "Error during preprocessing. Reason: ${ex.message}" - log.error( - "$failReason, will mark execution.id=$id as failed; error: ", - ex - ) - updateExecutionStatus(id!!, ExecutionStatus.ERROR, failReason) - } - - private fun getExecution(executionId: Long) = webClientBackend.get() - .uri("/execution?id=$executionId") - .retrieve() - .bodyToMono() - - @Suppress("TOO_MANY_PARAMETERS", "LongParameterList") - private fun updateExecution( - requestBase: ExecutionRequestBase, - executionVersion: String, - testSuiteIds: List, - ): Mono { - val executionUpdate = ExecutionInitializationDto(requestBase.project, testSuiteIds, executionVersion, requestBase.execCmd, requestBase.batchSizeForAnalyzer) - return webClientBackend.makeRequest(BodyInserters.fromValue(executionUpdate), "/updateNewExecution") { spec -> - spec.onStatus({ status -> status != HttpStatus.OK }) { clientResponse -> - log.error("Error when making update to execution fro project id = ${requestBase.project.id} ${clientResponse.statusCode()}") - throw ResponseStatusException( - HttpStatus.INTERNAL_SERVER_ERROR, - "Execution not found" - ) - } - spec.bodyToMono() - } - } - - @Suppress("MagicNumber") - private fun cleanupInOrchestrator(executionId: Long) = - webClientOrchestrator.post() - .uri("/cleanup?executionId=$executionId") - .httpRequest { - // increased timeout, because orchestrator should finish cleaning up first - it.getNativeRequest() - .responseTimeout(Duration.ofSeconds(10)) - } - .retrieve() - .toBodilessEntity() - - /** - * POST request to orchestrator to initiate its work - */ - private fun initializeAgents(execution: Execution): Status { - val bodyBuilder = MultipartBodyBuilder().apply { - part("execution", execution, MediaType.APPLICATION_JSON) - } - - return webClientOrchestrator - .post() - .uri("/initializeAgents") - .contentType(MediaType.MULTIPART_FORM_DATA) - .body(BodyInserters.fromMultipartData(bodyBuilder.build())) - .retrieve() - .toEntity() - } - - private fun WebClient.makeRequest( - body: BodyInserter, - uri: String, - toBody: (WebClient.ResponseSpec) -> Mono - ): Mono { - val responseSpec = this - .post() - .uri(uri) - .body(body) - .retrieve() - .onStatus({status -> status != HttpStatus.OK }) { clientResponse -> - log.error("Error when making request to $uri: ${clientResponse.statusCode()}") - throw ResponseStatusException( - HttpStatus.INTERNAL_SERVER_ERROR, - "Upstream request error" - ) - } - return toBody(responseSpec) - } - - private fun updateExecutionStatus(executionId: Long, executionStatus: ExecutionStatus, failReason: String? = null) = - webClientBackend.makeRequest( - BodyInserters.fromValue(ExecutionUpdateDto(executionId, executionStatus, failReason)), - "/updateExecutionByDto" - ) { it.toEntity() } - .doOnSubscribe { - log.info("Making request to set execution status for id=$executionId to $executionStatus") - } - - @Suppress("NO_BRACES_IN_CONDITIONALS_AND_LOOPS") - private fun ExecutionRequestBase.getTestSuiteFilter(): (TestSuite) -> Boolean = when (this) { - is ExecutionRequest -> { - { true } - } - is ExecutionRequestForStandardSuites -> { - { it.name in this.testSuites } - } - } - - private fun ExecutionRequestBase.toAcceptedResponseMono() = - Mono.just(ResponseEntity(executionResponseBody(executionId), HttpStatus.ACCEPTED)) -} - -/** - * @param executionId - * @return response body for execution submission request - */ -@Suppress("UnsafeCallOnNullableType") -fun executionResponseBody(executionId: Long?): String = "Clone pending, execution id is ${executionId!!}"