From 212994409eb897bff8f33583195c7a8716a21141 Mon Sep 17 00:00:00 2001 From: Alexander Frolov Date: Fri, 26 Aug 2022 14:12:45 +0300 Subject: [PATCH] Smarter test suites display (#1099) * Smarter test suites display ### What's done: * Fixed automatic fetch when test suite source is created * Added plugins column to test_suite * Implemented filtering out empty test suites * Added contest filtering (ONLY WarnPlugin) * Added plugin label for TestSuitesDisplayer * Added useTooltip hook (#1076) Co-authored-by: Peter Trifanov --- db/test-data/test-suite-insert.xml | 11 + db/v-2/tables/test_suite.xml | 6 + save-backend/backend-api-docs.json | 316 +++++++++++++++++- .../backend/controllers/ContestController.kt | 3 +- .../controllers/TestSuitesController.kt | 207 +++++++++--- .../backend/repository/TestSuiteRepository.kt | 6 + .../save/backend/service/TestSuitesService.kt | 41 ++- .../com/saveourtool/save/domain/PluginType.kt | 38 +++ .../com/saveourtool/save/test/TestDto.kt | 10 + .../save/testsuite/TestSuiteDto.kt | 5 +- .../save/testsuite/TestSuiteFilters.kt | 20 +- .../com/saveourtool/save/utils/Constants.kt | 2 +- .../saveourtool/save/entities/TestSuite.kt | 34 +- .../frontend/components/basic/FileUploader.kt | 8 +- .../components/basic/ManageUserRoleCard.kt | 2 +- .../basic/TestSuiteSourceCreationComponent.kt | 39 +-- .../components/basic/TestSuitesDisplayer.kt | 18 +- .../contests/ContestCreationComponent.kt | 6 +- .../organizations/ManageGitCredentialsCard.kt | 2 +- .../organizations/OrganizationSettingsMenu.kt | 4 +- .../organizations/OrganizationTestsMenu.kt | 4 +- .../basic/projects/ProjectSettingsMenu.kt | 4 +- .../testsuiteselector/TestSuiteSelector.kt | 67 +++- .../TestSuiteSelectorBrowserMode.kt | 98 +++--- .../TestSuiteSelectorManagerMode.kt | 10 +- .../TestSuiteSelectorSearchMode.kt | 21 +- .../inputform/InputTextFormOptional.kt | 26 +- .../save/frontend/utils/ReactUtils.kt | 64 +++- .../service/TestDiscoveringService.kt | 65 +++- .../service/TestDiscoveringServiceTest.kt | 25 +- 30 files changed, 910 insertions(+), 252 deletions(-) create mode 100644 save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/domain/PluginType.kt diff --git a/db/test-data/test-suite-insert.xml b/db/test-data/test-suite-insert.xml index e9687da7a2..10b95fb125 100644 --- a/db/test-data/test-suite-insert.xml +++ b/db/test-data/test-suite-insert.xml @@ -13,6 +13,7 @@ + @@ -23,6 +24,7 @@ + @@ -33,6 +35,7 @@ + @@ -45,6 +48,7 @@ + @@ -56,6 +60,7 @@ + @@ -67,6 +72,7 @@ + @@ -78,6 +84,7 @@ + @@ -90,6 +97,7 @@ + @@ -101,6 +109,7 @@ + @@ -112,6 +121,7 @@ + @@ -124,6 +134,7 @@ + diff --git a/db/v-2/tables/test_suite.xml b/db/v-2/tables/test_suite.xml index 9047626ee1..1be0a1e771 100644 --- a/db/v-2/tables/test_suite.xml +++ b/db/v-2/tables/test_suite.xml @@ -36,4 +36,10 @@ + + + + + + \ No newline at end of file diff --git a/save-backend/backend-api-docs.json b/save-backend/backend-api-docs.json index 90dfccc0e7..d1f488a610 100644 --- a/save-backend/backend-api-docs.json +++ b/save-backend/backend-api-docs.json @@ -14,12 +14,26 @@ "/api/v1/updateStandardTestSuites": { "post": { "tags": [ - "test-suites-controller" + "internal", + "superadmins", + "test-suites" ], + "summary": "Update standard test suites.", + "description": "Trigger update of standard test suites. Can be called only by super admins externally.", "operationId": "updateStandardTestSuites", "responses": { "200": { - "description": "OK", + "description": "Successfully updated standard test suites.", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/Unit" + } + } + } + }, + "401": { + "description": "Unauthorized", "content": { "*/*": { "schema": { @@ -28,7 +42,12 @@ } } } - } + }, + "security": [ + { + "basic": [] + } + ] } }, "/api/v1/test-suites-sources/{organizationName}/{sourceName}/upload-snapshot": { @@ -1033,9 +1052,23 @@ "/api/v1/test-suites/get-by-ids": { "post": { "tags": [ - "test-suites-controller" + "test-suites" ], + "summary": "Get test suites by ids.", + "description": "Get list of available test suites by their ids.", "operationId": "getTestSuitesByIds", + "parameters": [ + { + "name": "isContest", + "in": "query", + "description": "is given request sent for browsing test suites for contest, default is false", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], "requestBody": { "content": { "application/json": { @@ -1052,7 +1085,20 @@ }, "responses": { "200": { - "description": "OK", + "description": "Successfully fetched test suites by ids.", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestSuiteDto" + } + } + } + } + }, + "401": { + "description": "Unauthorized", "content": { "*/*": { "schema": { @@ -1064,7 +1110,12 @@ } } } - } + }, + "security": [ + { + "basic": [] + } + ] } }, "/api/v1/test-suites-sources/{organizationName}/{sourceName}/fetch": { @@ -3091,12 +3142,27 @@ "/api/v1/allStandardTestSuites": { "get": { "tags": [ - "test-suites-controller" + "test-suites" ], + "summary": "Get standard test suites.", + "description": "Get list of standard TestSuiteDtos.", "operationId": "getAllStandardTestSuites", "responses": { "200": { - "description": "OK", + "description": "Successfully fetched standard test suites.", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestSuiteDto" + } + } + } + } + }, + "401": { + "description": "Unauthorized", "content": { "*/*": { "schema": { @@ -3108,7 +3174,13 @@ } } } - } + }, + "deprecated": true, + "security": [ + { + "basic": [] + } + ] } }, "/api/v1/organizations/{organizationName}/list-git": { @@ -3790,16 +3862,138 @@ ] } }, + "/api/v1/test-suites/get-standard": { + "get": { + "tags": [ + "test-suites" + ], + "summary": "Get standard test suites.", + "description": "Get list of standard test suites.", + "operationId": "getStandardTestSuites", + "parameters": [ + { + "name": "isContest", + "in": "query", + "description": "is given request sent for browsing test suites for contest, default is false", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Successfully fetched standard test suites.", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestSuiteDto" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestSuiteDto" + } + } + } + } + } + }, + "security": [ + { + "basic": [] + } + ] + } + }, + "/api/v1/test-suites/get-by-organization": { + "get": { + "tags": [ + "test-suites" + ], + "summary": "Get test suites by organization.", + "description": "Get list of available test suites posted by given organization.", + "operationId": "getTestSuitesByOrganizationName", + "parameters": [ + { + "name": "organizationName", + "in": "query", + "description": "name of an organization", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "isContest", + "in": "query", + "description": "is given request sent for browsing test suites for contest, default is false", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Successfully fetched test suites by organization name.", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestSuiteDto" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestSuiteDto" + } + } + } + } + } + }, + "security": [ + { + "basic": [] + } + ] + } + }, "/api/v1/test-suites/filtered": { "get": { "tags": [ - "test-suites-controller" + "test-suites" ], + "summary": "Get test suites with filters.", + "description": "Get test suites with filters.", "operationId": "getFilteredTestSuites", "parameters": [ { "name": "tags", "in": "query", + "description": "test suite tags substring for filtering, default is empty", "required": false, "schema": { "type": "string", @@ -3809,6 +4003,7 @@ { "name": "name", "in": "query", + "description": "test suite name substring for filtering, default is empty", "required": false, "schema": { "type": "string", @@ -3818,16 +4013,40 @@ { "name": "language", "in": "query", + "description": "test suite language substring for filtering, default is empty", "required": false, "schema": { "type": "string", "default": "" } + }, + { + "name": "isContest", + "in": "query", + "description": "is given request sent for browsing test suites for contest, default is false", + "required": false, + "schema": { + "type": "boolean", + "default": false + } } ], "responses": { "200": { - "description": "OK", + "description": "Successfully fetched filtered test suites.", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestSuiteDto" + } + } + } + } + }, + "401": { + "description": "Unauthorized", "content": { "*/*": { "schema": { @@ -3839,7 +4058,67 @@ } } } - } + }, + "security": [ + { + "basic": [] + } + ] + } + }, + "/api/v1/test-suites/available": { + "get": { + "tags": [ + "test-suites" + ], + "summary": "Get public test suites.", + "description": "Get list of available test suites.", + "operationId": "getPublicTestSuites", + "parameters": [ + { + "name": "isContest", + "in": "query", + "description": "is given request sent for browsing test suites for contest, default is false", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Successfully fetched public test suites.", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestSuiteDto" + } + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestSuiteDto" + } + } + } + } + } + }, + "security": [ + { + "basic": [] + } + ] } }, "/api/v1/test-suites-sources/{organizationName}/{sourceName}/get-test-suites": { @@ -6855,6 +7134,7 @@ "TestSuiteDto": { "required": [ "name", + "plugins", "source", "version" ], @@ -6884,6 +7164,18 @@ "id": { "type": "integer", "format": "int64" + }, + "plugins": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "FIX", + "GENERAL", + "WARN", + "FIX AND WARN" + ] + } } } }, diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/ContestController.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/ContestController.kt index aa91045e4e..9543721c1b 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/ContestController.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/ContestController.kt @@ -34,6 +34,7 @@ import org.springframework.security.core.Authentication import org.springframework.web.bind.annotation.* import reactor.core.publisher.Flux import reactor.core.publisher.Mono +import reactor.kotlin.core.publisher.toFlux import reactor.kotlin.core.publisher.toMono import reactor.kotlin.core.util.function.component1 import reactor.kotlin.core.util.function.component2 @@ -207,7 +208,7 @@ internal class ContestController( it.getTestSuiteIds() } .flatMapMany { testSuiteIds -> - testSuitesService.findTestSuitesByIds(testSuiteIds) + testSuitesService.findTestSuitesByIds(testSuiteIds).toFlux() } .map { it.toDto(it.requiredId()) diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/TestSuitesController.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/TestSuitesController.kt index 531c9236ca..a3ce5e2b29 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/TestSuitesController.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/controllers/TestSuitesController.kt @@ -1,12 +1,22 @@ package com.saveourtool.save.backend.controllers +import com.saveourtool.save.backend.configs.ApiSwaggerSupport import com.saveourtool.save.backend.scheduling.UpdateJob import com.saveourtool.save.backend.security.TestSuitePermissionEvaluator +import com.saveourtool.save.backend.service.TestSuiteDtoList import com.saveourtool.save.backend.service.TestSuitesService +import com.saveourtool.save.domain.isAllowedForContests import com.saveourtool.save.entities.TestSuite import com.saveourtool.save.testsuite.TestSuiteDto import com.saveourtool.save.testsuite.TestSuiteFilters import com.saveourtool.save.v1 +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.Parameters +import io.swagger.v3.oas.annotations.enums.ParameterIn +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.quartz.Scheduler import org.springframework.http.HttpStatus @@ -17,104 +27,211 @@ import org.springframework.transaction.annotation.Transactional import org.springframework.web.bind.annotation.* import reactor.core.publisher.Flux import reactor.core.publisher.Mono +import reactor.kotlin.core.publisher.toFlux typealias ResponseListTestSuites = ResponseEntity> /** * Controller for test suites */ +@ApiSwaggerSupport +@Tags( + Tag(name = "test-suites"), +) @RestController class TestSuitesController( private val testSuitesService: TestSuitesService, private val quartzScheduler: Scheduler, private val testSuitePermissionEvaluator: TestSuitePermissionEvaluator, ) { - /** - * Save new test suites into DB - * - * @param testSuiteDtos - * @return mono list of *all* TestSuite - */ @PostMapping("/internal/saveTestSuites") + @PreAuthorize("permitAll()") + @Operation( + method = "POST", + summary = "Save test suites.", + description = "Save new test suites into DB.", + ) + @Tag(name = "internal") + @ApiResponse(responseCode = "200", description = "Successfully saved test suites.") fun saveTestSuite(@RequestBody testSuiteDtos: List): Mono> = Mono.just(testSuitesService.saveTestSuite(testSuiteDtos)) - /** - * @return response with list of test suite dtos - */ @GetMapping(path = ["/api/$v1/allStandardTestSuites", "/internal/allStandardTestSuites"]) + @PreAuthorize("permitAll()") + @Operation( + method = "GET", + summary = "Get standard test suites.", + description = "Get list of standard TestSuiteDtos.", + deprecated = true, + ) + @ApiResponse(responseCode = "200", description = "Successfully fetched standard test suites.") fun getAllStandardTestSuites(): Mono = testSuitesService.getStandardTestSuites().map { ResponseEntity.status(HttpStatus.OK).body(it) } - /** - * @param testSuiteIds - * @param authentication - * @return [Flux] of [TestSuiteDto]s - */ @PostMapping("/api/$v1/test-suites/get-by-ids") + @PreAuthorize("permitAll()") + @Operation( + method = "POST", + summary = "Get test suites by ids.", + description = "Get list of available test suites by their ids.", + ) + @Parameters( + Parameter(name = "isContest", `in` = ParameterIn.QUERY, description = "is given request sent for browsing test suites for contest, default is false", required = false), + ) + @ApiResponse(responseCode = "200", description = "Successfully fetched test suites by ids.") fun getTestSuitesByIds( @RequestBody testSuiteIds: List, + @RequestParam(required = false, defaultValue = "false") isContest: Boolean, authentication: Authentication, ): Flux = testSuitesService.findTestSuitesByIds(testSuiteIds) + .toFlux() + .mapToDtoFiltered(authentication, isContest) + + @GetMapping("/api/$v1/test-suites/get-by-organization") + @PreAuthorize("permitAll()") + @Operation( + method = "GET", + summary = "Get test suites by organization.", + description = "Get list of available test suites posted by given organization.", + ) + @Parameters( + Parameter(name = "organizationName", `in` = ParameterIn.QUERY, description = "name of an organization", required = true), + Parameter(name = "isContest", `in` = ParameterIn.QUERY, description = "is given request sent for browsing test suites for contest, default is false", required = false), + ) + @ApiResponse(responseCode = "200", description = "Successfully fetched test suites by organization name.") + fun getTestSuitesByOrganizationName( + @RequestParam organizationName: String, + @RequestParam(required = false, defaultValue = "false") isContest: Boolean, + authentication: Authentication, + ): Flux = testSuitesService.findTestSuitesByOrganizationName(organizationName) + .toFlux() + .mapToDtoFiltered(authentication, isContest) + + @GetMapping("/api/$v1/test-suites/get-standard") + @PreAuthorize("permitAll()") + @Operation( + method = "GET", + summary = "Get standard test suites.", + description = "Get list of standard test suites.", + ) + @Parameters( + Parameter(name = "isContest", `in` = ParameterIn.QUERY, description = "is given request sent for browsing test suites for contest, default is false", required = false), + ) + @ApiResponse(responseCode = "200", description = "Successfully fetched standard test suites.") + fun getStandardTestSuites( + @RequestParam(required = false, defaultValue = "false") isContest: Boolean, + authentication: Authentication, + ): Mono = testSuitesService.getStandardTestSuites() + .map { testSuites -> + testSuites.filter { + if (isContest) { + it.plugins.isAllowedForContests() + } else { + it.plugins.isNotEmpty() + } + } + } + + @GetMapping("/api/$v1/test-suites/available") + @PreAuthorize("permitAll()") + @Operation( + method = "GET", + summary = "Get public test suites.", + description = "Get list of available test suites.", + ) + @Parameters( + Parameter(name = "isContest", `in` = ParameterIn.QUERY, description = "is given request sent for browsing test suites for contest, default is false", required = false), + ) + @ApiResponse(responseCode = "200", description = "Successfully fetched public test suites.") + fun getPublicTestSuites( + @RequestParam(required = false, defaultValue = "false") isContest: Boolean, + authentication: Authentication, + ): Flux = testSuitesService.findAllTestSuites().toFlux() + .mapToDtoFiltered(authentication, isContest) + + private fun Flux.mapToDtoFiltered(authentication: Authentication, isContest: Boolean): Flux = filter { testSuite -> + testSuitePermissionEvaluator.canAccessTestSuite(testSuite, authentication) + } .filter { testSuite -> - testSuitePermissionEvaluator.canAccessTestSuite(testSuite, authentication) + if (isContest) { + testSuite.pluginsAsListOfPluginType().isAllowedForContests() + } else { + testSuite.pluginsAsListOfPluginType().isNotEmpty() + } } .map { testSuite -> testSuite.toDto(testSuite.requiredId()) } - /** - * @param tags - * @param name - * @param language - * @param authentication - * @return [Flux] of [TestSuiteDto]s - */ @GetMapping("/api/$v1/test-suites/filtered") + @PreAuthorize("permitAll()") + @Operation( + method = "GET", + summary = "Get test suites with filters.", + description = "Get test suites with filters.", + ) + @Parameters( + Parameter(name = "tags", `in` = ParameterIn.QUERY, description = "test suite tags substring for filtering, default is empty", required = false), + Parameter(name = "name", `in` = ParameterIn.QUERY, description = "test suite name substring for filtering, default is empty", required = false), + Parameter(name = "language", `in` = ParameterIn.QUERY, description = "test suite language substring for filtering, default is empty", required = false), + Parameter(name = "isContest", `in` = ParameterIn.QUERY, description = "is given request sent for browsing test suites for contest, default is false", required = false), + ) + @ApiResponse(responseCode = "200", description = "Successfully fetched filtered test suites.") fun getFilteredTestSuites( @RequestParam(required = false, defaultValue = "") tags: String, @RequestParam(required = false, defaultValue = "") name: String, @RequestParam(required = false, defaultValue = "") language: String, + @RequestParam(required = false, defaultValue = "false") isContest: Boolean, authentication: Authentication, ): Flux = Mono.just(TestSuiteFilters(name, language, tags)) .flatMapMany { - testSuitesService.findTestSuitesMatchingFilters(it) - } - .filter { - testSuitePermissionEvaluator.canAccessTestSuite(it, authentication) - } - .map { - it.toDto(it.requiredId()) + testSuitesService.findTestSuitesMatchingFilters(it).toFlux() } + .mapToDtoFiltered(authentication, isContest) - /** - * @param id id of the test suite - * @return response with test suite with provided id - */ @GetMapping("/internal/testSuite/{id}") - fun getTestSuiteById(@PathVariable id: Long) = + @PreAuthorize("permitAll()") + @Operation( + method = "GET", + summary = "Get test suite by id.", + description = "Get test suite by id.", + ) + @Tag(name = "internal") + @Parameters( + Parameter(name = "id", `in` = ParameterIn.PATH, description = "id of test suite", required = true), + ) + @ApiResponse(responseCode = "200", description = "Successfully fetched filtered test suites.") + fun getTestSuiteById(@PathVariable id: Long): ResponseEntity = ResponseEntity.status(HttpStatus.OK).body(testSuitesService.findTestSuiteById(id)) - /** - * Trigger update of standard test suites. Can be called only by superadmins externally. - * - * @return response entity - */ @PostMapping(path = ["/api/$v1/updateStandardTestSuites", "/internal/updateStandardTestSuites"]) @PreAuthorize("hasRole('ROLE_SUPER_ADMIN')") - fun updateStandardTestSuites() = Mono.just(quartzScheduler) + @Tags( + Tag(name = "superadmins"), + Tag(name = "internal"), + ) + @Operation( + method = "POST", + summary = "Update standard test suites.", + description = "Trigger update of standard test suites. Can be called only by super admins externally.", + ) + @ApiResponse(responseCode = "200", description = "Successfully updated standard test suites.") + fun updateStandardTestSuites(): Mono = Mono.just(quartzScheduler) .map { it.triggerJob( UpdateJob.jobKey ) } - /** - * @param testSuiteDtos suites, which need to be deleted - * @return response entity - */ @PostMapping("/internal/deleteTestSuite") @Transactional - fun deleteTestSuite(@RequestBody testSuiteDtos: List) = + @Tag(name = "internal") + @Operation( + method = "POST", + summary = "Delete test suites.", + description = "Delete test suites.", + ) + @ApiResponse(responseCode = "200", description = "Successfully deleted test suites.") + fun deleteTestSuite(@RequestBody testSuiteDtos: List): ResponseEntity = ResponseEntity.status(HttpStatus.OK).body(testSuitesService.deleteTestSuiteDto(testSuiteDtos)) } diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/repository/TestSuiteRepository.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/repository/TestSuiteRepository.kt index 5507a9d3fd..0f5287ac00 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/repository/TestSuiteRepository.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/repository/TestSuiteRepository.kt @@ -41,4 +41,10 @@ interface TestSuiteRepository : BaseEntityRepository, QueryByExampleE fun findAllBySource( source: TestSuitesSource, ): List + + /** + * @param organizationName + * @return List of [TestSuite]s + */ + fun findBySourceOrganizationName(organizationName: String): List } diff --git a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/TestSuitesService.kt b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/TestSuitesService.kt index 3440ac60eb..11063ac567 100644 --- a/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/TestSuitesService.kt +++ b/save-backend/src/main/kotlin/com/saveourtool/save/backend/service/TestSuitesService.kt @@ -17,9 +17,7 @@ import org.springframework.data.domain.ExampleMatcher import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.web.server.ResponseStatusException -import reactor.core.publisher.Flux import reactor.core.publisher.Mono -import reactor.kotlin.core.publisher.toFlux import reactor.kotlin.extra.math.max import java.time.LocalDateTime @@ -57,15 +55,16 @@ class TestSuitesService( // We allow description of existing test suites to be changed. it.copy(description = null) } - .map { + .map { dto -> TestSuite( - name = it.name, - description = it.description, - source = testSuitesSourceService.getByName(it.source.organizationName, it.source.name), - version = it.version, + name = dto.name, + description = dto.description, + source = testSuitesSourceService.getByName(dto.source.organizationName, dto.source.name), + version = dto.version, dateAdded = null, - language = it.language, - tags = it.tags?.let(TestSuite::tagsFromList), + language = dto.language, + tags = dto.tags?.let(TestSuite::tagsFromList), + plugins = TestSuite.pluginsByTypes(dto.plugins) ) } .map { testSuite -> @@ -113,23 +112,32 @@ class TestSuitesService( * @param ids * @return List of [TestSuite] by [ids] */ - fun findTestSuitesByIds(ids: List): Flux = blockingToFlux { - ids.mapNotNull { id -> - testSuiteRepository.findByIdOrNull(id) - } + fun findTestSuitesByIds(ids: List): List = ids.mapNotNull { id -> + testSuiteRepository.findByIdOrNull(id) } + /** + * @param organizationName + * @return [List] of [TestSuite]s by [organizationName] + */ + fun findTestSuitesByOrganizationName(organizationName: String): List = testSuiteRepository.findBySourceOrganizationName(organizationName) + + /** + * @return [List] of ALL [TestSuite]s + */ + fun findAllTestSuites(): List = testSuiteRepository.findAll() + /** * @param filters - * @return [Flux] of [TestSuite] that match [filters] + * @return [List] of [TestSuite] that match [filters] */ @Suppress("TOO_MANY_LINES_IN_LAMBDA") - fun findTestSuitesMatchingFilters(filters: TestSuiteFilters): Flux = + fun findTestSuitesMatchingFilters(filters: TestSuiteFilters): List = ExampleMatcher.matchingAll() .withMatcher("name", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase()) .withMatcher("language", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase()) .withMatcher("tags", ExampleMatcher.GenericPropertyMatchers.contains().ignoreCase()) - .withIgnorePaths("description", "source", "version", "dateAdded") + .withIgnorePaths("description", "source", "version", "dateAdded", "plugins") .let { Example.of( TestSuite( @@ -145,7 +153,6 @@ class TestSuitesService( ) } .let { testSuiteRepository.findAll(it) } - .toFlux() /** * @param id diff --git a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/domain/PluginType.kt b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/domain/PluginType.kt new file mode 100644 index 0000000000..e88f67a949 --- /dev/null +++ b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/domain/PluginType.kt @@ -0,0 +1,38 @@ +@file:Suppress("HEADER_MISSING_IN_NON_SINGLE_CLASS_FILE") + +package com.saveourtool.save.domain + +import com.saveourtool.save.core.config.TestConfigSections + +private val contestAllowedPlugins = listOf(TestConfigSections.WARN) + +// todo: Probably should fix name in save-cli +typealias PluginType = TestConfigSections + +/** + * @return true if given TestSuite can be chosen for contest, false otherwise + */ +fun List.isAllowedForContests() = this == contestAllowedPlugins + +/** + * @return [PluginType] from [String] + */ +fun String.toPluginType(): PluginType = when (this) { + "WarnPlugin" -> PluginType.WARN + "FixPlugin" -> PluginType.FIX + "FixAndWarnPlugin" -> PluginType.`FIX AND WARN` + "" -> PluginType.GENERAL + else -> throw IllegalArgumentException("No such plugin.") +} + +/** + * fixme: Will need to support pluginName in save-cli + * + * @return Pretty name from [PluginType] + */ +fun PluginType.pluginName() = when (this) { + PluginType.WARN -> "WarnPlugin" + PluginType.FIX -> "FixPlugin" + PluginType.`FIX AND WARN` -> "FixAndWarnPlugin" + else -> "UNKNOWN" +} diff --git a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/test/TestDto.kt b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/test/TestDto.kt index a979f93e64..7ed324806c 100644 --- a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/test/TestDto.kt +++ b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/test/TestDto.kt @@ -4,6 +4,8 @@ package com.saveourtool.save.test +import com.saveourtool.save.domain.PluginType +import com.saveourtool.save.domain.toPluginType import com.saveourtool.save.testsuite.TestSuitesSourceDto import com.saveourtool.save.utils.DATABASE_DELIMITER import kotlinx.serialization.Serializable @@ -46,3 +48,11 @@ data class TestFilesRequest( val version: String, ) + +/** + * @return [List] of plugin names + */ +fun List.collectPluginNames() = map { it.pluginName } + .distinct() + .map { it.toPluginType() } + .filter { it != PluginType.GENERAL } diff --git a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/testsuite/TestSuiteDto.kt b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/testsuite/TestSuiteDto.kt index 2da58314b4..e8404fb729 100644 --- a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/testsuite/TestSuiteDto.kt +++ b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/testsuite/TestSuiteDto.kt @@ -1,5 +1,6 @@ package com.saveourtool.save.testsuite +import com.saveourtool.save.domain.PluginType import kotlinx.serialization.Serializable /** @@ -10,6 +11,7 @@ import kotlinx.serialization.Serializable * @property language [com.saveourtool.save.entities.TestSuite.language] * @property tags [com.saveourtool.save.entities.TestSuite.tags] * @property id + * @property plugins */ @Serializable data class TestSuiteDto( @@ -19,7 +21,8 @@ data class TestSuiteDto( val version: String, val language: String? = null, val tags: List? = null, - var id: Long? = null, + val id: Long? = null, + val plugins: List = emptyList(), ) { /** * @return non-nullable [id] diff --git a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/testsuite/TestSuiteFilters.kt b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/testsuite/TestSuiteFilters.kt index a87d17c81d..8bbe953874 100644 --- a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/testsuite/TestSuiteFilters.kt +++ b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/testsuite/TestSuiteFilters.kt @@ -21,16 +21,24 @@ data class TestSuiteFilters( fun tagsAsList() = tags.split(DATABASE_DELIMITER).distinct() /** + * @param additionalParams some extra parameters that should be in query * @return [TestSuiteFilters] as query params for request */ - fun toQueryParams() = listOf("tags" to tags, "name" to name, "language" to language) + fun toQueryParams(vararg additionalParams: Pair) = listOf( + "tags" to tags, + "name" to name, + "language" to language + ) .filter { it.second.isNotBlank() } - .joinToString("&") { "${it.first}=${it.second}" } - .let { - if (it.isNotBlank()) { - "?$it" + .plus(additionalParams) + .joinToString("&") { + "${it.first}=${it.second}" + } + .let { query -> + if (query.isNotBlank()) { + "?$query" } else { - it + query } } diff --git a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/utils/Constants.kt b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/utils/Constants.kt index 8dbec807e4..fdb1a98ec9 100644 --- a/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/utils/Constants.kt +++ b/save-cloud-common/src/commonMain/kotlin/com/saveourtool/save/utils/Constants.kt @@ -17,4 +17,4 @@ const val URL_PATH_DELIMITER = "/" /** * Period in ms for debounce on frontend */ -const val DEFAULT_DEBOUNCE_PERIOD = 500 +const val DEFAULT_DEBOUNCE_PERIOD = 750 diff --git a/save-cloud-common/src/jvmMain/kotlin/com/saveourtool/save/entities/TestSuite.kt b/save-cloud-common/src/jvmMain/kotlin/com/saveourtool/save/entities/TestSuite.kt index a63e3b5877..cfa09a7c02 100644 --- a/save-cloud-common/src/jvmMain/kotlin/com/saveourtool/save/entities/TestSuite.kt +++ b/save-cloud-common/src/jvmMain/kotlin/com/saveourtool/save/entities/TestSuite.kt @@ -1,5 +1,8 @@ package com.saveourtool.save.entities +import com.saveourtool.save.domain.PluginType +import com.saveourtool.save.domain.pluginName +import com.saveourtool.save.domain.toPluginType import com.saveourtool.save.testsuite.TestSuiteDto import com.saveourtool.save.utils.DATABASE_DELIMITER @@ -16,6 +19,7 @@ import javax.persistence.ManyToOne * @property dateAdded date and time, when this test suite was added to the project * @property language * @property tags + * @property plugins */ @Suppress("LongParameterList") @Entity @@ -35,7 +39,18 @@ class TestSuite( var language: String? = null, var tags: String? = null, + + var plugins: String = "" ) : BaseEntity() { + /** + * @return [plugins] as a list of string + */ + fun pluginsAsListOfPluginType() = plugins.split(DATABASE_DELIMITER) + .map { pluginName -> + pluginName.toPluginType() + } + .filter { it != PluginType.GENERAL } + /** * @return [tags] as a list of strings */ @@ -53,7 +68,8 @@ class TestSuite( this.version, this.language, this.tagsAsList(), - id + id, + this.pluginsAsListOfPluginType(), ) companion object { @@ -64,5 +80,21 @@ class TestSuite( * @return representation of [tags] as a single string understood by [TestSuite.tagsAsList] */ fun tagsFromList(tags: List) = tags.joinToString(separator = DATABASE_DELIMITER) + + /** + * [plugins] by list of strings + * + * @param pluginNamesAsList list of string names of plugins + * @return [String] of plugins separated by [DATABASE_DELIMITER] from [List] of [String]s + */ + fun pluginsByNames(pluginNamesAsList: List) = pluginNamesAsList.joinToString(DATABASE_DELIMITER) + + /** + * Update [plugins] by list of strings + * + * @param pluginTypesAsList list of [PluginType] + * @return [String] of plugins separated by [DATABASE_DELIMITER] from [List] of [PluginType]s + */ + fun pluginsByTypes(pluginTypesAsList: List) = pluginsByNames(pluginTypesAsList.map { it.pluginName() }) } } diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/FileUploader.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/FileUploader.kt index c9027513b5..d4d913ed86 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/FileUploader.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/FileUploader.kt @@ -10,6 +10,7 @@ import com.saveourtool.save.domain.FileInfo import com.saveourtool.save.domain.ProjectCoordinates import com.saveourtool.save.frontend.externals.fontawesome.* import com.saveourtool.save.frontend.utils.toPrettyString +import com.saveourtool.save.frontend.utils.useTooltip import com.saveourtool.save.v1 import csstype.ClassName @@ -274,12 +275,7 @@ private fun fileUploader() = FC { props -> } } - useEffect { - val jquery = kotlinext.js.require("jquery") - kotlinext.js.require("popper.js") - kotlinext.js.require("bootstrap") - jquery("[data-toggle=\"tooltip\"]").tooltip() - } + useTooltip() } private fun getHref( diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/ManageUserRoleCard.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/ManageUserRoleCard.kt index 46e6a4a5e1..4f87c1db5c 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/ManageUserRoleCard.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/ManageUserRoleCard.kt @@ -202,7 +202,7 @@ fun manageUserRoleCardComponent() = FC { props -> setSelfRole(getHighestRole(role, props.selfUserInfo.globalRole)) } - runOnlyOnFirstRender { + useOnce { getUsersFromGroup() } diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/TestSuiteSourceCreationComponent.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/TestSuiteSourceCreationComponent.kt index 90392c793e..9d4a084ff3 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/TestSuiteSourceCreationComponent.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/TestSuiteSourceCreationComponent.kt @@ -49,7 +49,7 @@ external interface TestSuiteSourceCreationProps : Props { /** * Callback invoked on successful save */ - var onSuccess: () -> Unit + var onSuccess: (TestSuitesSourceDto) -> Unit } /** @@ -62,7 +62,7 @@ external interface TestSuiteSourceCreationProps : Props { fun ChildrenBuilder.showTestSuiteSourceCreationModal( isOpen: Boolean, organizationName: String, - onSuccess: () -> Unit, + onSuccess: (TestSuitesSourceDto) -> Unit, onClose: () -> Unit, ) { modal { props -> @@ -107,28 +107,21 @@ fun ChildrenBuilder.showTestSuiteSourceCreationModal( private fun testSuiteSourceCreationComponent() = FC { props -> val (testSuiteSource, setTestSuiteSource) = useState(TestSuitesSourceDto.empty.copy(organizationName = props.organizationName)) val (saveStatus, setSaveStatus) = useState(null) - val fetchTestSuiteSource = useDeferredRequest { - post( - url = "$apiUrl/test-suites-sources/${testSuiteSource.organizationName}/${testSuiteSource.name}/fetch", - headers = jsonHeaders, - body = undefined, - loadingHandler = ::noopLoadingHandler, - responseHandler = ::noopResponseHandler, - ) - } + @Suppress("TOO_MANY_LINES_IN_LAMBDA") val onSubmitButtonPressed = useDeferredRequest { - val response = post( - url = "/api/$v1/test-suites-sources/create", - headers = jsonHeaders, - body = Json.encodeToString(testSuiteSource), - loadingHandler = ::loadingHandler, - responseHandler = ::responseHandlerWithValidation, - ) - if (response.ok) { - fetchTestSuiteSource() - props.onSuccess() - } else if (response.isConflict()) { - setSaveStatus(response.decodeFromJsonString()) + testSuiteSource.let { + val response = post( + url = "/api/$v1/test-suites-sources/create", + headers = jsonHeaders, + body = Json.encodeToString(it), + loadingHandler = ::loadingHandler, + responseHandler = ::responseHandlerWithValidation, + ) + if (response.ok) { + props.onSuccess(it) + } else if (response.isConflict()) { + setSaveStatus(response.decodeFromJsonString()) + } } } diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/TestSuitesDisplayer.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/TestSuitesDisplayer.kt index 410201c33d..8dc722103f 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/TestSuitesDisplayer.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/TestSuitesDisplayer.kt @@ -7,6 +7,7 @@ package com.saveourtool.save.frontend.components.basic +import com.saveourtool.save.domain.pluginName import com.saveourtool.save.testsuite.TestSuiteDto import csstype.ClassName import react.ChildrenBuilder @@ -52,8 +53,21 @@ fun ChildrenBuilder.showAvaliableTestSuites( p { +(testSuite.description ?: "") } - small { - +(testSuite.tags?.joinToString(", ") ?: "") + div { + className = ClassName("d-flex justify-content-between") + small { + asDynamic()["data-toggle"] = "tooltip" + asDynamic()["data-placement"] = "bottom" + title = "Test suite tags" + +(testSuite.tags?.joinToString(", ") ?: "") + } + + small { + asDynamic()["data-toggle"] = "tooltip" + asDynamic()["data-placement"] = "bottom" + title = "Plugin type" + +(testSuite.plugins.joinToString(", ") { it.pluginName() }) + } } } } diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/contests/ContestCreationComponent.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/contests/ContestCreationComponent.kt index c3f9de96a0..d9fe89882e 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/contests/ContestCreationComponent.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/contests/ContestCreationComponent.kt @@ -4,7 +4,7 @@ package com.saveourtool.save.frontend.components.basic.contests import com.saveourtool.save.entities.ContestDto import com.saveourtool.save.frontend.components.basic.* -import com.saveourtool.save.frontend.components.basic.testsuiteselector.showGeneralTestSuitesSelectorModal +import com.saveourtool.save.frontend.components.basic.testsuiteselector.showContestTestSuitesSelectorModal import com.saveourtool.save.frontend.components.inputform.* import com.saveourtool.save.frontend.components.inputform.inputTextDisabled import com.saveourtool.save.frontend.components.inputform.inputTextFormOptionalWrapperConst @@ -154,7 +154,7 @@ private fun contestCreationComponent() = FC { pro div { className = ClassName("card") contestCreationCard { - showGeneralTestSuitesSelectorModal( + showContestTestSuitesSelectorModal( contestDto.testSuiteIds, testSuitesSelectorWindowOpenness, useState(emptyList()), @@ -214,7 +214,7 @@ private fun contestCreationComponent() = FC { pro className = ClassName("mt-2") inputTextFormRequired( InputTypes.CONTEST_TEST_SUITE_IDS, - contestDto.testSuiteIds.joinToString(", "), + contestDto.testSuiteIds.sorted().joinToString(", "), true, "col-12 pl-2 pr-2 text-center", "Test Suites:", diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/organizations/ManageGitCredentialsCard.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/organizations/ManageGitCredentialsCard.kt index 191628bd23..e34fe7b2fb 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/organizations/ManageGitCredentialsCard.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/organizations/ManageGitCredentialsCard.kt @@ -186,7 +186,7 @@ fun manageGitCredentialsCardComponent() = FC { pr } } - runOnlyOnFirstRender { + useOnce { fetchGitCredentialsRequest() } } diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/organizations/OrganizationSettingsMenu.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/organizations/OrganizationSettingsMenu.kt index 886b438bd1..a353d0e386 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/organizations/OrganizationSettingsMenu.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/organizations/OrganizationSettingsMenu.kt @@ -5,7 +5,7 @@ package com.saveourtool.save.frontend.components.basic.organizations import com.saveourtool.save.domain.Role import com.saveourtool.save.entities.Organization import com.saveourtool.save.frontend.components.basic.manageUserRoleCardComponent -import com.saveourtool.save.frontend.utils.createGlobalRoleWarningCallback +import com.saveourtool.save.frontend.utils.useGlobalRoleWarningCallback import com.saveourtool.save.info.UserInfo import csstype.ClassName @@ -82,7 +82,7 @@ external interface OrganizationSettingsMenuProps : Props { private fun organizationSettingsMenu() = FC { props -> @Suppress("LOCAL_VARIABLE_EARLY_DECLARATION") val organizationPath = props.organizationName - val (wasConfirmationModalShown, showGlobalRoleWarning) = createGlobalRoleWarningCallback(props.updateNotificationMessage) + val (wasConfirmationModalShown, showGlobalRoleWarning) = useGlobalRoleWarningCallback(props.updateNotificationMessage) div { className = ClassName("row justify-content-center mb-2") // ===================== LEFT COLUMN ======================================================================= diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/organizations/OrganizationTestsMenu.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/organizations/OrganizationTestsMenu.kt index a9cf1745da..b9bfd2b4ed 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/organizations/OrganizationTestsMenu.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/organizations/OrganizationTestsMenu.kt @@ -119,8 +119,10 @@ private fun organizationTestsMenu() = FC { props -> showTestSuiteSourceCreationModal( isTestSuiteSourceCreationModalOpen, props.organizationName, - { + { source -> setIsTestSuitesSourceCreationModalOpen(false) + setTestSuiteSourceToFetch(source) + triggerFetchTestSuiteSource() setIsSourceCreated { !it } }, ) { diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/projects/ProjectSettingsMenu.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/projects/ProjectSettingsMenu.kt index eb7e4e62e8..a4fca1eaf3 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/projects/ProjectSettingsMenu.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/projects/ProjectSettingsMenu.kt @@ -7,7 +7,7 @@ import com.saveourtool.save.entities.Project import com.saveourtool.save.frontend.components.basic.manageUserRoleCardComponent import com.saveourtool.save.frontend.components.inputform.InputTypes import com.saveourtool.save.frontend.components.inputform.inputTextFormOptionalWrapperConst -import com.saveourtool.save.frontend.utils.createGlobalRoleWarningCallback +import com.saveourtool.save.frontend.utils.useGlobalRoleWarningCallback import com.saveourtool.save.info.UserInfo import csstype.ClassName @@ -93,7 +93,7 @@ private fun projectSettingsMenu() = FC { props -> val projectPath = props.project.let { "${it.organization.name}/${it.name}" } - val (wasConfirmationModalShown, showGlobalRoleWarning) = createGlobalRoleWarningCallback(props.updateNotificationMessage) + val (wasConfirmationModalShown, showGlobalRoleWarning) = useGlobalRoleWarningCallback(props.updateNotificationMessage) div { className = ClassName("row justify-content-center mb-2") diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/testsuiteselector/TestSuiteSelector.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/testsuiteselector/TestSuiteSelector.kt index fc34dd4035..6ced4b89b0 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/testsuiteselector/TestSuiteSelector.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/testsuiteselector/TestSuiteSelector.kt @@ -9,6 +9,7 @@ package com.saveourtool.save.frontend.components.basic.testsuiteselector import com.saveourtool.save.frontend.externals.fontawesome.* import com.saveourtool.save.frontend.externals.modal.* import com.saveourtool.save.frontend.utils.WindowOpenness +import com.saveourtool.save.frontend.utils.useTooltip import csstype.ClassName import react.* @@ -41,9 +42,9 @@ external interface TestSuiteSelectorProps : Props { var specificOrganizationName: String? /** - * If this flag is true public tests will be shown + * Mode that defines what kind of test suites will be shown */ - var isStandardMode: Boolean + var selectorPurpose: TestSuiteSelectorPurpose } /** @@ -57,7 +58,22 @@ enum class TestSuiteSelectorMode { } /** - * Browse standard test suites + * Enum that defines what type of test suites should be shown + */ +enum class TestSuiteSelectorPurpose { + CONTEST, + PRIVATE, + PUBLIC, + + /** + * Will later be merged with PUBLIC + */ + STANDARD, + ; +} + +/** + * Browse standard test suites. * * @param initTestSuiteIds initial value * @param windowOpenness state to control openness of window @@ -70,7 +86,7 @@ fun ChildrenBuilder.showPublicTestSuitesSelectorModal( testSuiteIdsInSelectorState: StateInstance>, setSelectedTestSuiteIds: (List) -> Unit, ) { - showTestSuitesSelectorModal(null, true, initTestSuiteIds, windowOpenness, testSuiteIdsInSelectorState, setSelectedTestSuiteIds) + showTestSuitesSelectorModal(null, TestSuiteSelectorPurpose.STANDARD, initTestSuiteIds, windowOpenness, testSuiteIdsInSelectorState, setSelectedTestSuiteIds) } /** @@ -87,11 +103,11 @@ fun ChildrenBuilder.showGeneralTestSuitesSelectorModal( testSuiteIdsInSelectorState: StateInstance>, setSelectedTestSuiteIds: (List) -> Unit, ) { - showTestSuitesSelectorModal(null, false, initTestSuiteIds, windowOpenness, testSuiteIdsInSelectorState, setSelectedTestSuiteIds) + showTestSuitesSelectorModal(null, TestSuiteSelectorPurpose.PUBLIC, initTestSuiteIds, windowOpenness, testSuiteIdsInSelectorState, setSelectedTestSuiteIds) } /** - * Browse test suites of a given organization + * Browse test suites of a given organization. * * @param organizationName * @param initTestSuiteIds initial value @@ -106,13 +122,30 @@ fun ChildrenBuilder.showPrivateTestSuitesSelectorModal( testSuiteIdsInSelectorState: StateInstance>, setSelectedTestSuiteIds: (List) -> Unit, ) { - showTestSuitesSelectorModal(organizationName, false, initTestSuiteIds, windowOpenness, testSuiteIdsInSelectorState, setSelectedTestSuiteIds) + showTestSuitesSelectorModal(organizationName, TestSuiteSelectorPurpose.PRIVATE, initTestSuiteIds, windowOpenness, testSuiteIdsInSelectorState, setSelectedTestSuiteIds) +} + +/** + * Browse test suites for a contest. + * + * @param initTestSuiteIds initial value + * @param windowOpenness state to control openness of window + * @param testSuiteIdsInSelectorState state for intermediate result in selector + * @param setSelectedTestSuiteIds consumer for result + */ +fun ChildrenBuilder.showContestTestSuitesSelectorModal( + initTestSuiteIds: List, + windowOpenness: WindowOpenness, + testSuiteIdsInSelectorState: StateInstance>, + setSelectedTestSuiteIds: (List) -> Unit, +) { + showTestSuitesSelectorModal(null, TestSuiteSelectorPurpose.CONTEST, initTestSuiteIds, windowOpenness, testSuiteIdsInSelectorState, setSelectedTestSuiteIds) } @Suppress("TOO_MANY_PARAMETERS", "LongParameterList") private fun ChildrenBuilder.showTestSuitesSelectorModal( specificOrganizationName: String?, - isStandardMode: Boolean, + selectorPurpose: TestSuiteSelectorPurpose, initTestSuiteIds: List, windowOpenness: WindowOpenness, testSuiteIdsInSelectorState: StateInstance>, @@ -130,7 +163,7 @@ private fun ChildrenBuilder.showTestSuitesSelectorModal( currentlySelectedTestSuiteIds = initTestSuiteIds windowOpenness.closeWindow() } - showTestSuitesSelectorModal(windowOpenness.isOpen(), specificOrganizationName, isStandardMode, initTestSuiteIds, onSubmit, onTestSuiteIdUpdate, onCancel) + showTestSuitesSelectorModal(windowOpenness.isOpen(), specificOrganizationName, selectorPurpose, initTestSuiteIds, onSubmit, onTestSuiteIdUpdate, onCancel) } @Suppress( @@ -142,7 +175,7 @@ private fun ChildrenBuilder.showTestSuitesSelectorModal( private fun ChildrenBuilder.showTestSuitesSelectorModal( isOpen: Boolean, specificOrganizationName: String?, - isStandardMode: Boolean, + selectorPurpose: TestSuiteSelectorPurpose, preselectedTestSuiteIds: List, onSubmit: () -> Unit, onTestSuiteIdUpdate: (List) -> Unit, @@ -179,7 +212,7 @@ private fun ChildrenBuilder.showTestSuitesSelectorModal( this.onTestSuiteIdUpdate = onTestSuiteIdUpdate this.preselectedTestSuiteIds = preselectedTestSuiteIds this.specificOrganizationName = specificOrganizationName - this.isStandardMode = isStandardMode + this.selectorPurpose = selectorPurpose } } @@ -232,13 +265,8 @@ private fun ChildrenBuilder.buildButton( onClick = { onClickFun() } - - val jquery = kotlinext.js.require("jquery") - kotlinext.js.require("popper.js") - kotlinext.js.require("bootstrap") asDynamic()["data-toggle"] = "tooltip" asDynamic()["data-placement"] = "bottom" - jquery("[data-toggle=\"tooltip\"]").tooltip() } } @@ -254,20 +282,25 @@ private fun testSuiteSelector() = FC { props -> buildButton(faPlus, currentMode == TestSuiteSelectorMode.BROWSER, "Browse public test suites") { setCurrentMode(TestSuiteSelectorMode.BROWSER) } buildButton(faSearch, currentMode == TestSuiteSelectorMode.SEARCH, "Search by name or tag") { setCurrentMode(TestSuiteSelectorMode.SEARCH) } } + + useTooltip() + when (currentMode) { TestSuiteSelectorMode.MANAGER -> testSuiteSelectorManagerMode { this.onTestSuiteIdsUpdate = props.onTestSuiteIdUpdate this.preselectedTestSuiteIds = props.preselectedTestSuiteIds + this.selectorPurpose = props.selectorPurpose } TestSuiteSelectorMode.BROWSER -> testSuiteSelectorBrowserMode { this.onTestSuiteIdsUpdate = props.onTestSuiteIdUpdate this.preselectedTestSuiteIds = props.preselectedTestSuiteIds this.specificOrganizationName = props.specificOrganizationName - this.isStandardMode = props.isStandardMode + this.selectorPurpose = props.selectorPurpose } TestSuiteSelectorMode.SEARCH -> testSuiteSelectorSearchMode { this.onTestSuiteIdsUpdate = props.onTestSuiteIdUpdate this.preselectedTestSuiteIds = props.preselectedTestSuiteIds + this.selectorPurpose = props.selectorPurpose } } } diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/testsuiteselector/TestSuiteSelectorBrowserMode.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/testsuiteselector/TestSuiteSelectorBrowserMode.kt index 4d01113f13..abc3b8667c 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/testsuiteselector/TestSuiteSelectorBrowserMode.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/testsuiteselector/TestSuiteSelectorBrowserMode.kt @@ -12,12 +12,8 @@ import com.saveourtool.save.frontend.externals.fontawesome.fontAwesomeIcon import com.saveourtool.save.frontend.utils.* import com.saveourtool.save.frontend.utils.noopResponseHandler import com.saveourtool.save.testsuite.TestSuiteDto -import com.saveourtool.save.testsuite.TestSuitesSourceDtoList -import com.saveourtool.save.testsuite.TestSuitesSourceSnapshotKeyList import csstype.ClassName -import react.ChildrenBuilder -import react.FC -import react.Props +import react.* import react.dom.aria.AriaRole import react.dom.aria.ariaLabel import react.dom.html.ReactHTML.a @@ -28,7 +24,6 @@ import react.dom.html.ReactHTML.li import react.dom.html.ReactHTML.nav import react.dom.html.ReactHTML.ol import react.dom.html.ReactHTML.ul -import react.useState val testSuiteSelectorBrowserMode = testSuiteSelectorBrowserMode() @@ -53,9 +48,9 @@ external interface TestSuiteSelectorBrowserModeProps : Props { var specificOrganizationName: String? /** - * If this flag is true public tests will be shown + * Mode that defines what kind of test suites will be shown */ - var isStandardMode: Boolean + var selectorPurpose: TestSuiteSelectorPurpose } @Suppress( @@ -124,9 +119,9 @@ private fun ChildrenBuilder.showBreadcrumb( if (shouldDisplayVersion) { selectedTestSuiteVersion?.let { li { + className = ClassName("breadcrumb-item active") a { role = "button".unsafeCast() - className = ClassName("breadcrumb-item active") +selectedTestSuiteVersion } } @@ -156,6 +151,7 @@ private fun ChildrenBuilder.showAvaliableOptions( @Suppress("TOO_LONG_FUNCTION", "LongMethod", "ComplexMethod") private fun testSuiteSelectorBrowserMode() = FC { props -> + useTooltip() val (selectedOrganization, setSelectedOrganization) = useState(null) val (selectedTestSuiteSource, setSelectedTestSuiteSource) = useState(null) val (selectedTestSuiteVersion, setSelectedTestSuiteVersion) = useState(null) @@ -163,12 +159,15 @@ private fun testSuiteSelectorBrowserMode() = FC>(emptyList()) val (availableTestSuiteSources, setAvailableTestSuiteSources) = useState>(emptyList()) - + val (availableTestSuitesVersions, setAvailableTestSuitesVersions) = useState>(emptyList()) + val (availableTestSuites, setAvailableTestSuites) = useState>(emptyList()) + val (fetchedTestSuites, setFetchedTestSuites) = useState>(emptyList()) useRequest { - val url = when { - props.isStandardMode -> "$apiUrl/test-suites-sources/public-list" - props.specificOrganizationName == null -> "$apiUrl/test-suites-sources/avaliable" - else -> "$apiUrl/test-suites-sources/${props.specificOrganizationName}/list" + val url = when (props.selectorPurpose) { + TestSuiteSelectorPurpose.PUBLIC -> "$apiUrl/test-suites/available" + TestSuiteSelectorPurpose.PRIVATE -> "$apiUrl/test-suites/get-by-organization?organizationName=${props.specificOrganizationName}" + TestSuiteSelectorPurpose.STANDARD -> "$apiUrl/test-suites/get-standard" + TestSuiteSelectorPurpose.CONTEST -> "$apiUrl/test-suites/available?isContest=true" } val response = get( url = url, @@ -177,52 +176,40 @@ private fun testSuiteSelectorBrowserMode() = FC = response.decodeFromJsonString() + setFetchedTestSuites(testSuites) + setAvailableOrganizations(testSuites.map { it.source.organizationName }.distinct()) } - val (availableTestSuitesVersions, setAvailableTestSuitesVersions) = useState>(emptyList()) - useRequest(dependencies = arrayOf(selectedTestSuiteSource)) { + useEffect(selectedOrganization) { + selectedOrganization?.let { selectedOrganization -> + setAvailableTestSuiteSources( + fetchedTestSuites.map { it.source } + .filter { it.organizationName == selectedOrganization } + .map { it.name } + .distinct() + ) + } ?: setAvailableTestSuiteSources(emptyList()) + } + + useEffect(selectedTestSuiteSource) { selectedTestSuiteSource?.let { selectedTestSuiteSource -> - val testSuiteSourcesVersions: List = get( - url = "$apiUrl/test-suites-sources/$selectedOrganization/${encodeURIComponent(selectedTestSuiteSource)}/list-snapshot", - headers = jsonHeaders, - loadingHandler = ::noopLoadingHandler, - responseHandler = ::noopResponseHandler, + setAvailableTestSuitesVersions( + fetchedTestSuites.filter { it.source.name == selectedTestSuiteSource } + .map { it.version } + .distinct() ) - .decodeFromJsonString() - .map { it.version } - setAvailableTestSuitesVersions(testSuiteSourcesVersions) - setSelectedTestSuiteVersion(testSuiteSourcesVersions.singleOrNull()) - } + } ?: setAvailableTestSuitesVersions(emptyList()) } - val (availableTestSuites, setAvailableTestSuites) = useState>(emptyList()) - useRequest(dependencies = arrayOf(selectedTestSuiteVersion)) { + useEffect(selectedTestSuiteVersion) { selectedTestSuiteVersion?.let { selectedTestSuiteVersion -> - selectedTestSuiteSource?.let { selectedTestSuiteSource -> - val testSuites: List = get( - url = "$apiUrl/test-suites-sources/$selectedOrganization/${ - encodeURIComponent( - selectedTestSuiteSource - ) - }" + - "/get-test-suites?version=${encodeURIComponent(selectedTestSuiteVersion)}", - headers = jsonHeaders, - loadingHandler = ::noopLoadingHandler, - responseHandler = ::noopResponseHandler, - ) - .decodeFromJsonString() - setAvailableTestSuites(testSuites) - testSuites.filter { - it.id in props.preselectedTestSuiteIds + setAvailableTestSuites( + fetchedTestSuites.filter { + it.source.name == selectedTestSuiteSource && it.version == selectedTestSuiteVersion } - .let { - setSelectedTestSuites(it) - } - } - } + ) + } ?: setAvailableTestSuites(emptyList()) } val (namePrefix, setNamePrefix) = useState("") @@ -234,13 +221,13 @@ private fun testSuiteSelectorBrowserMode() = FC 1, - { + onOrganizationsClick = { setSelectedOrganization(null) setSelectedTestSuiteSource(null) setSelectedTestSuiteVersion(null) setNamePrefix("") }, - { + onSelectedOrganizationClick = { setSelectedTestSuiteSource(null) setSelectedTestSuiteVersion(null) setNamePrefix("") @@ -274,6 +261,9 @@ private fun testSuiteSelectorBrowserMode() = FC if (selectedTestSuites.containsAll(availableTestSuites)) { diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/testsuiteselector/TestSuiteSelectorManagerMode.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/testsuiteselector/TestSuiteSelectorManagerMode.kt index 7b6e252092..ea31734610 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/testsuiteselector/TestSuiteSelectorManagerMode.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/testsuiteselector/TestSuiteSelectorManagerMode.kt @@ -35,6 +35,11 @@ external interface TestSuiteSelectorManagerModeProps : Props { * Callback invoked when test suite is being removed */ var onTestSuiteIdsUpdate: (List) -> Unit + + /** + * Mode that defines what kind of test suites will be shown + */ + var selectorPurpose: TestSuiteSelectorPurpose } @Suppress("TOO_LONG_FUNCTION", "LongMethod", "ComplexMethod") @@ -60,7 +65,10 @@ private fun testSuiteSelectorManagerMode() = FC + showAvaliableTestSuites( + preselectedTestSuites, + selectedTestSuites, + ) { testSuite -> setSelectedTestSuites { selectedTestSuites -> selectedTestSuites.toMutableList() .apply { diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/testsuiteselector/TestSuiteSelectorSearchMode.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/testsuiteselector/TestSuiteSelectorSearchMode.kt index b7a935def0..f95b3b6059 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/testsuiteselector/TestSuiteSelectorSearchMode.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/basic/testsuiteselector/TestSuiteSelectorSearchMode.kt @@ -7,6 +7,7 @@ package com.saveourtool.save.frontend.components.basic.testsuiteselector import com.saveourtool.save.frontend.components.basic.showAvaliableTestSuites +import com.saveourtool.save.frontend.components.basic.testsuiteselector.TestSuiteSelectorPurpose.CONTEST import com.saveourtool.save.frontend.externals.lodash.debounce import com.saveourtool.save.frontend.utils.* import com.saveourtool.save.frontend.utils.noopResponseHandler @@ -39,6 +40,11 @@ external interface TestSuiteSelectorSearchModeProps : Props { * Callback invoked when test suite is being removed */ var onTestSuiteIdsUpdate: (List) -> Unit + + /** + * Mode that defines what kind of test suites will be shown + */ + var selectorPurpose: TestSuiteSelectorPurpose } private fun ChildrenBuilder.buildInput( @@ -60,8 +66,13 @@ private fun testSuiteSelectorSearchMode() = FC val (selectedTestSuites, setSelectedTestSuites) = useState>(emptyList()) val (filteredTestSuites, setFilteredTestSuites) = useState>(emptyList()) useRequest { + val contestFlag = if (props.selectorPurpose == CONTEST) { + "?isContest=true" + } else { + "" + } val testSuitesFromBackend: List = post( - url = "$apiUrl/test-suites/get-by-ids", + url = "$apiUrl/test-suites/get-by-ids$contestFlag", headers = jsonHeaders, body = Json.encodeToString(props.preselectedTestSuiteIds), loadingHandler = ::loadingHandler, @@ -77,7 +88,8 @@ private fun testSuiteSelectorSearchMode() = FC if (filters.isNotEmpty()) { val testSuitesFromBackend: List = get( url = "$apiUrl/test-suites/filtered${ - filters.copy(language = encodeURIComponent(filters.language)).toQueryParams() + filters.copy(language = encodeURIComponent(filters.language)) + .toQueryParams("isContest" to "${props.selectorPurpose == CONTEST}") }", headers = jsonHeaders, loadingHandler = ::noopLoadingHandler, @@ -111,7 +123,10 @@ private fun testSuiteSelectorSearchMode() = FC } } - showAvaliableTestSuites(filteredTestSuites, selectedTestSuites) { testSuite -> + showAvaliableTestSuites( + filteredTestSuites, + selectedTestSuites, + ) { testSuite -> setSelectedTestSuites { selectedTestSuites -> selectedTestSuites.toMutableList() .apply { diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/inputform/InputTextFormOptional.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/inputform/InputTextFormOptional.kt index a3f1a00feb..f4594df2b8 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/inputform/InputTextFormOptional.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/components/inputform/InputTextFormOptional.kt @@ -8,6 +8,7 @@ package com.saveourtool.save.frontend.components.inputform import com.saveourtool.save.frontend.externals.fontawesome.faQuestionCircle import com.saveourtool.save.frontend.externals.fontawesome.fontAwesomeIcon +import com.saveourtool.save.frontend.utils.useTooltipAndPopover import csstype.ClassName import org.w3c.dom.HTMLInputElement import react.ChildrenBuilder @@ -20,26 +21,6 @@ import react.dom.html.ReactHTML.div import react.dom.html.ReactHTML.input import react.dom.html.ReactHTML.label import react.dom.html.ReactHTML.sup -import react.useEffect - -// language=JS -private const val ENABLE_TOOLTIP_AND_POPOVER_JS: String = """ - var jQuery = require("jquery") - require("popper.js") - require("bootstrap") - jQuery('.form-popover').each(function() { - jQuery(this).popover({ - placement: jQuery(this).attr("popover-placement"), - title: jQuery(this).attr("popover-title"), - content: jQuery(this).attr("popover-content"), - html: true - }).on('show.bs.popover', function() { - jQuery(this).tooltip('hide') - }).on('hide.bs.popover', function() { - jQuery(this).tooltip('show') - }) - }) -""" /** * constant FC to avoid re-creation @@ -177,8 +158,5 @@ fun inputTextFormOptionalWrapper() = FC { props -> props.onChangeFun ) - useEffect { - js(ENABLE_TOOLTIP_AND_POPOVER_JS) - return@useEffect - } + useTooltipAndPopover() } diff --git a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/utils/ReactUtils.kt b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/utils/ReactUtils.kt index da0385875d..d87d0b49e0 100644 --- a/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/utils/ReactUtils.kt +++ b/save-frontend/src/main/kotlin/com/saveourtool/save/frontend/utils/ReactUtils.kt @@ -4,7 +4,7 @@ package com.saveourtool.save.frontend.utils -import react.ChildrenBuilder +import react.useEffect import react.useState /** @@ -12,8 +12,7 @@ import react.useState * * @param action */ -@Suppress("unused") -fun ChildrenBuilder.runOnlyOnFirstRender(action: () -> Unit) { +fun useOnce(action: () -> Unit) { val (isFirstRender, setFirstRender) = useState(true) if (isFirstRender) { action() @@ -25,7 +24,7 @@ fun ChildrenBuilder.runOnlyOnFirstRender(action: () -> Unit) { * @param updateNotificationMessage callback to show notification message * @return current value and callback for showGlobalRoleWarning */ -fun createGlobalRoleWarningCallback(updateNotificationMessage: (String, String) -> Unit): Pair Unit> { +fun useGlobalRoleWarningCallback(updateNotificationMessage: (String, String) -> Unit): Pair Unit> { val (wasConfirmationModalShown, setWasConfirmationModalShown) = useState(false) val showGlobalRoleWarning = { updateNotificationMessage( @@ -36,3 +35,60 @@ fun createGlobalRoleWarningCallback(updateNotificationMessage: (String, String) } return wasConfirmationModalShown to showGlobalRoleWarning } + +/** + * Custom hook to enable tooltips. + */ +fun useTooltip() { + useEffect { + enableTooltip() + return@useEffect + } +} + +/** + * Custom hook to enable tooltips and popovers. + */ +fun useTooltipAndPopover() { + useEffect { + enableTooltipAndPopover() + return@useEffect + } +} + +/** + * JS code lines to enable tooltip. + * + * @return dynamic + */ +// language=js +fun enableTooltip() = js(""" + var jQuery = require("jquery") + require("popper.js") + require("bootstrap") + jQuery('[data-toggle="tooltip"]').tooltip() +""") + +/** + * JS code lines to enable tooltip and popover. + * + * @return dynamic + */ +// language=JS +fun enableTooltipAndPopover() = js(""" + var jQuery = require("jquery") + require("popper.js") + require("bootstrap") + jQuery('.popover').each(function() { + jQuery(this).popover({ + placement: jQuery(this).attr("popover-placement"), + title: jQuery(this).attr("popover-title"), + content: jQuery(this).attr("popover-content"), + html: true + }).on('show.bs.popover', function() { + jQuery(this).tooltip('hide') + }).on('hide.bs.popover', function() { + jQuery(this).tooltip('show') + }) + }) +""") diff --git a/save-preprocessor/src/main/kotlin/com/saveourtool/save/preprocessor/service/TestDiscoveringService.kt b/save-preprocessor/src/main/kotlin/com/saveourtool/save/preprocessor/service/TestDiscoveringService.kt index 97dc9c3977..1c6785fcbf 100644 --- a/save-preprocessor/src/main/kotlin/com/saveourtool/save/preprocessor/service/TestDiscoveringService.kt +++ b/save-preprocessor/src/main/kotlin/com/saveourtool/save/preprocessor/service/TestDiscoveringService.kt @@ -10,8 +10,10 @@ import com.saveourtool.save.plugins.fix.FixPlugin import com.saveourtool.save.preprocessor.EmptyResponse import com.saveourtool.save.preprocessor.utils.toHash import com.saveourtool.save.test.TestDto +import com.saveourtool.save.test.collectPluginNames import com.saveourtool.save.testsuite.TestSuiteDto import com.saveourtool.save.testsuite.TestSuitesSourceDto +import com.saveourtool.save.utils.debug import com.saveourtool.save.utils.info import okio.FileSystem import okio.Path @@ -21,6 +23,7 @@ import org.springframework.stereotype.Service import reactor.core.publisher.Flux import reactor.core.publisher.Mono import reactor.kotlin.core.publisher.toFlux +import reactor.kotlin.core.publisher.toMono import reactor.kotlin.core.util.function.component1 import reactor.kotlin.core.util.function.component2 import kotlin.io.path.absolutePathString @@ -49,19 +52,18 @@ class TestDiscoveringService( .map { getRootTestConfig(it.absolutePathString()) } .zipWhen { rootTestConfig -> log.info { "Starting to discover test suites for root test config ${rootTestConfig.location}" } - discoverAndSaveAllTestSuites( + discoverAllTestSuites( rootTestConfig, testSuitesSourceDto, version - ) + ).toMono() } - .flatMap { (rootTestConfig, testSuites) -> + .map { (rootTestConfig, testSuites) -> log.info { "Test suites size = ${testSuites.size}" } log.info { "Starting to save new tests for config test root $repositoryPath" } - discoverAndSaveAllTests(rootTestConfig, testSuites) - .collectList() - .map { testSuites } + discoverAllTestsIntoMap(rootTestConfig, testSuites) } + .saveTestSuitesAndTests() } /** @@ -122,11 +124,11 @@ class TestDiscoveringService( * @throws IllegalArgumentException when provided path doesn't point to a valid config file */ @Suppress("UnsafeCallOnNullableType") - fun discoverAndSaveAllTestSuites( + fun discoverAllTestSuites( rootTestConfig: TestConfig, source: TestSuitesSourceDto, version: String, - ) = getAllTestSuites(rootTestConfig, source, version).save() + ) = getAllTestSuites(rootTestConfig, source, version) private fun Path.getRelativePath(rootTestConfig: TestConfig) = this.toFile() .relativeTo(rootTestConfig.directory.toFile()) @@ -141,7 +143,7 @@ class TestDiscoveringService( * @throws PluginException if configs use unknown plugin */ @Suppress("UnsafeCallOnNullableType", "TOO_MANY_LINES_IN_LAMBDA") - fun getAllTests(rootTestConfig: TestConfig, testSuites: List) = rootTestConfig + fun getAllTests(rootTestConfig: TestConfig, testSuites: List) = rootTestConfig .getAllTestConfigs() .asSequence() .flatMap { testConfig -> @@ -163,18 +165,18 @@ class TestDiscoveringService( listOf(it.test) to emptyList() } val testRelativePath = it.test.getRelativePath(rootTestConfig) - TestDto( + testSuite to TestDto( testRelativePath, plugin::class.simpleName!!, - testSuite.id!!, + 0, allFiles.toHash(), additionalFiles, ) } } } - .onEach { - log.debug("Discovered the following test: $it") + .onEach { (testSuite, tests) -> + log.debug("For test suite ${testSuite.name} discovered the following tests: $tests") } /** @@ -186,10 +188,45 @@ class TestDiscoveringService( * @throws PluginException if configs use unknown plugin */ @Suppress("UnsafeCallOnNullableType") - fun discoverAndSaveAllTests(rootTestConfig: TestConfig, testSuites: List) = getAllTests(rootTestConfig, testSuites) + fun discoverAllTestsIntoMap( + rootTestConfig: TestConfig, + testSuites: List, + ) = getAllTests(rootTestConfig, testSuites).convertToMap().updatePluginNames() + + @Suppress("TYPE_ALIAS") + private fun Mono>>.saveTestSuitesAndTests() = flatMap { testsMap -> + testsMap.run { + saveTestSuites().also { + saveTests() + } + } + } + + @Suppress("TYPE_ALIAS") + private fun Map>.saveTestSuites() = keys.toList().save() + + @Suppress("TYPE_ALIAS") + private fun Map>.saveTests() = values.flatten() .toFlux() .save() + @Suppress("TYPE_ALIAS") + private fun Sequence>.convertToMap() = groupBy({ (testSuite, _) -> + testSuite + }) { (_, test) -> + test + } + + @Suppress("TYPE_ALIAS") + private fun Map>.updatePluginNames() = map { (testSuite, tests) -> + val collectedPluginNames = tests.collectPluginNames() + log.debug { + "Test suite ${testSuite.name} has [$collectedPluginNames] plugins." + } + testSuite.copy(plugins = collectedPluginNames) to tests + } + .toMap() + private fun TestConfig.getGeneralConfigOrNull() = pluginConfigs.filterIsInstance().singleOrNull() /** diff --git a/save-preprocessor/src/test/kotlin/com/saveourtool/save/preprocessor/service/TestDiscoveringServiceTest.kt b/save-preprocessor/src/test/kotlin/com/saveourtool/save/preprocessor/service/TestDiscoveringServiceTest.kt index 623b92a57b..47be48174c 100644 --- a/save-preprocessor/src/test/kotlin/com/saveourtool/save/preprocessor/service/TestDiscoveringServiceTest.kt +++ b/save-preprocessor/src/test/kotlin/com/saveourtool/save/preprocessor/service/TestDiscoveringServiceTest.kt @@ -3,6 +3,7 @@ package com.saveourtool.save.preprocessor.service import com.saveourtool.save.core.config.TestConfig import com.saveourtool.save.entities.* import com.saveourtool.save.preprocessor.config.ConfigProperties +import com.saveourtool.save.testsuite.TestSuiteDto import com.saveourtool.save.testsuite.TestSuitesSourceDto import org.eclipse.jgit.api.Git import org.junit.jupiter.api.AfterAll @@ -105,15 +106,20 @@ class TestDiscoveringServiceTest { val testDtos = testDiscoveringService.getAllTests( rootTestConfig, listOf( - createTestSuiteStub("Autofix: Smoke Tests", 1), - createTestSuiteStub("DocsCheck", 2), - createTestSuiteStub("Only Warnings: General", 3), - createTestSuiteStub("Autofix and Warn", 4), - createTestSuiteStub("Directory: Chapter 1", 5), - createTestSuiteStub("Directory: Chapter2", 6), - createTestSuiteStub("Directory: Chapter3", 7), + createTestSuiteStub("Autofix: Smoke Tests"), + createTestSuiteStub("DocsCheck"), + createTestSuiteStub("Only Warnings: General"), + createTestSuiteStub("Autofix and Warn"), + createTestSuiteStub("Directory: Chapter 1"), + createTestSuiteStub("Directory: Chapter2"), + createTestSuiteStub("Directory: Chapter3"), ) - ).toList() + ) + .map { + it.second + } + .toList() + logger.debug("Discovered the following tests: $testDtos") Assertions.assertEquals(16, testDtos.size) @@ -125,8 +131,7 @@ class TestDiscoveringServiceTest { } } - private fun createTestSuiteStub(name: String, id: Long) = mock().also { - whenever(it.id).thenReturn(id) + private fun createTestSuiteStub(name: String) = mock().also { whenever(it.name).thenReturn(name) } }