From 638d2f785b0dce29b1ca2483b48ee80a950c2e3d Mon Sep 17 00:00:00 2001 From: RomanDavlyatshin Date: Thu, 12 Feb 2026 18:41:26 +0400 Subject: [PATCH 1/2] fix: merge conflicts + add tests --- .../rawdata/route/RawDataWriterRoutes.kt | 37 ++++- .../route/payload/TestMetadataPayload.kt | 39 +++++- .../writer/rawdata/service/RawDataWriter.kt | 2 + .../service/impl/RawDataServiceImpl.kt | 29 ++++ .../writer/rawdata/TestMetadataApiTest.kt | 129 ++++++++++++++++++ 5 files changed, 229 insertions(+), 7 deletions(-) diff --git a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/route/RawDataWriterRoutes.kt b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/route/RawDataWriterRoutes.kt index 6de87f4ce..d02cf23f9 100644 --- a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/route/RawDataWriterRoutes.kt +++ b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/route/RawDataWriterRoutes.kt @@ -16,6 +16,7 @@ package com.epam.drill.admin.writer.rawdata.route import com.epam.drill.admin.common.principal.User +import com.epam.drill.admin.common.route.ok import com.epam.drill.admin.writer.rawdata.service.RawDataWriter import io.ktor.client.* import io.ktor.client.engine.apache.* @@ -23,14 +24,14 @@ import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* import io.ktor.http.* import io.ktor.resources.* -import io.ktor.server.resources.put -import io.ktor.server.resources.post -import io.ktor.server.resources.get -import io.ktor.server.resources.delete import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* +import io.ktor.server.auth.* import io.ktor.server.plugins.* import io.ktor.server.request.* +import io.ktor.server.resources.* +import io.ktor.server.resources.post +import io.ktor.server.resources.put import io.ktor.server.routing.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -42,8 +43,6 @@ import org.kodein.di.instance import org.kodein.di.ktor.closestDI import java.io.InputStream import java.util.zip.GZIPInputStream -import com.epam.drill.admin.common.route.ok -import io.ktor.server.auth.principal private val logger = KotlinLogging.logger {} @@ -65,6 +64,12 @@ class TestMetadataRoute() @Resource("sessions") class TestSessionRoute() +@Resource("test-definitions") +class TestDefinitionsRoute() + +@Resource("test-launches") +class TestLaunchesRoute() + @Resource("method-ignore-rules") class MethodIgnoreRulesRoute() { @Resource("/{id}") @@ -81,6 +86,8 @@ fun Route.dataIngestRoutes() { putMethods() postTestMetadata() putTestSessions() + postTestDefinitions() + postTestLaunches() postMethodIgnoreRules() getMethodIgnoreRules() deleteMethodIgnoreRule() @@ -142,6 +149,24 @@ fun Route.putTestSessions() { } } +fun Route.postTestDefinitions() { + val rawDataWriter by closestDI().instance() + + post { + rawDataWriter.saveTestDefinitions(call.decompressAndReceive()) + call.ok("Test definitions saved") + } +} + +fun Route.postTestLaunches() { + val rawDataWriter by closestDI().instance() + + post { + rawDataWriter.saveTestLaunches(call.decompressAndReceive()) + call.ok("Test launches saved") + } +} + fun Route.postMethodIgnoreRules() { val rawDataWriter by closestDI().instance() diff --git a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/route/payload/TestMetadataPayload.kt b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/route/payload/TestMetadataPayload.kt index 3ed284c5c..b124f5caa 100644 --- a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/route/payload/TestMetadataPayload.kt +++ b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/route/payload/TestMetadataPayload.kt @@ -19,7 +19,7 @@ import kotlinx.datetime.Instant import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement -// TODO rework alongside with Java Autotest Agent +// TODO update test agent @Serializable class AddTestsPayload( val groupId: String, @@ -70,4 +70,41 @@ class SessionPayload( val testTaskId: String, val startedAt: Instant, val builds: List = emptyList(), +) + +@Serializable +class AddTestLaunchesPayload( + val groupId: String, + val testSessionId: String, + val launches: List, +) + +@Serializable +class TestLaunchPayload ( + val id: String, + val testDefinitionId: String, + val result: String?, + val duration: Int? = null, +) + + +@Serializable +class AddTestDefinitionsPayload( + val groupId: String, + val definitions: List +) + +// TODO: update test agent +// Order of fields, and field definitions changed compared to original TestDefinition class: +// - name and runner are no longer nullable +// - type field is moved and became nullable +@Serializable +class TestDefinitionPayload( + val id: String, + val runner: String, + val name: String, + val type: String?, + val path: String?, + val tags: List = emptyList(), + val metadata: JsonElement? = null, ) \ No newline at end of file diff --git a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/service/RawDataWriter.kt b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/service/RawDataWriter.kt index 9a445fa4c..853f4ffcb 100644 --- a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/service/RawDataWriter.kt +++ b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/service/RawDataWriter.kt @@ -25,6 +25,8 @@ interface RawDataWriter { suspend fun saveMethods(methodsPayload: MethodsPayload) suspend fun saveCoverage(coveragePayload: CoveragePayload) suspend fun saveTestMetadata(testsPayload: AddTestsPayload) + suspend fun saveTestDefinitions(testDefinitionsPayload: AddTestDefinitionsPayload) + suspend fun saveTestLaunches(testLaunchesPayload: AddTestLaunchesPayload) suspend fun saveTestSession(sessionPayload: SessionPayload, user: User?) suspend fun saveMethodIgnoreRule(rulePayload: MethodIgnoreRulePayload) suspend fun getAllMethodIgnoreRules(): List diff --git a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/service/impl/RawDataServiceImpl.kt b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/service/impl/RawDataServiceImpl.kt index a9b658f13..8ec233129 100644 --- a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/service/impl/RawDataServiceImpl.kt +++ b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/service/impl/RawDataServiceImpl.kt @@ -177,6 +177,7 @@ class RawDataServiceImpl( } } + // WARNING: keep this method for backward compatibility with existing adoptions override suspend fun saveTestMetadata(testsPayload: AddTestsPayload) = transaction { testsPayload.tests.map { test -> TestLaunch( @@ -203,6 +204,34 @@ class RawDataServiceImpl( }.let { testDefinitionRepository.createMany(it) } } + override suspend fun saveTestDefinitions(testDefinitionsPayload: AddTestDefinitionsPayload) = transaction { + testDefinitionsPayload.definitions.map { definition -> + TestDefinition( + groupId = testDefinitionsPayload.groupId, + id = definition.id, + type = definition.type, + runner = definition.runner, + name = definition.name, + path = definition.path, + tags = definition.tags, + metadata = definition.metadata + ) + }.let { testDefinitionRepository.createMany(it) } + } + + override suspend fun saveTestLaunches(testLaunchesPayload: AddTestLaunchesPayload) = transaction { + testLaunchesPayload.launches.map { launch -> + TestLaunch( + groupId = testLaunchesPayload.groupId, + id = launch.id, + testDefinitionId = launch.testDefinitionId, + testSessionId = testLaunchesPayload.testSessionId, + result = launch.result.toString(), + duration = launch.duration + ) + }.let { testLaunchRepository.createMany(it) } + } + override suspend fun saveTestSession(sessionPayload: SessionPayload, user: User?) { val testSession = TestSession( id = sessionPayload.id, diff --git a/admin-writer/src/test/kotlin/com/epam/drill/admin/writer/rawdata/TestMetadataApiTest.kt b/admin-writer/src/test/kotlin/com/epam/drill/admin/writer/rawdata/TestMetadataApiTest.kt index 7a763b1ae..909dcd752 100644 --- a/admin-writer/src/test/kotlin/com/epam/drill/admin/writer/rawdata/TestMetadataApiTest.kt +++ b/admin-writer/src/test/kotlin/com/epam/drill/admin/writer/rawdata/TestMetadataApiTest.kt @@ -21,6 +21,8 @@ import com.epam.drill.admin.test.drillApplication import com.epam.drill.admin.test.withRollback import com.epam.drill.admin.writer.rawdata.config.RawDataWriterDatabaseConfig import com.epam.drill.admin.writer.rawdata.config.rawDataServicesDIModule +import com.epam.drill.admin.writer.rawdata.route.postTestDefinitions +import com.epam.drill.admin.writer.rawdata.route.postTestLaunches import com.epam.drill.admin.writer.rawdata.route.postTestMetadata import com.epam.drill.admin.writer.rawdata.table.TestDefinitionTable import com.epam.drill.admin.writer.rawdata.table.TestLaunchTable @@ -131,4 +133,131 @@ class TestMetadataApiTest : DatabaseTests({ RawDataWriterDatabaseConfig.init(it) assertTrue(it[TestDefinitionTable.createdAt] >= timeBeforeTest) } } + + @Test + fun `given test definitions payload, post test definitions should return OK and save definitions`() = withRollback { + val testGroup = "group-1" + val testDefinition1 = "def-1" + val testDefinition2 = "def-2" + val timeBeforeTest = LocalDateTime.now() + + val app = drillApplication(rawDataServicesDIModule) { + postTestDefinitions() + } + + app.client.post("/test-definitions") { + header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + setBody( + """ + { + "groupId": "$testGroup", + "definitions": [ + { + "id": "$testDefinition1", + "runner": "JUnit", + "name": "test-1", + "type": "UNIT", + "path": "com.example.Test1", + "tags": ["tag-1"], + "metadata": { "k1": "v1" } + }, + { + "id": "$testDefinition2", + "runner": "JUnit", + "name": "test-2", + "type": "UNIT", + "path": "com.example.Test2", + "tags": ["tag-2"], + "metadata": { "k2": "v2" } + } + ] + } + """.trimIndent() + ) + }.apply { + assertEquals(HttpStatusCode.OK, status) + assertJsonEquals( + """ + { + "message": "Test definitions saved" + } + """.trimIndent(), + bodyAsText() + ) + } + + val saved = TestDefinitionTable.selectAll() + .filter { it[TestDefinitionTable.groupId] == testGroup } + + assertEquals(2, saved.size) + saved.forEach { + assertNotNull(it[TestDefinitionTable.runner]) + assertNotNull(it[TestDefinitionTable.name]) + assertTrue(it[TestDefinitionTable.createdAt] >= timeBeforeTest) + } + } + + @Test + fun `given test launches payload, post test launches should return OK and save launches`() = withRollback { + val testGroup = "group-1" + val testSession = "session-1" + val testDefinition = "def-1" + val launch1 = "launch-1" + val launch2 = "launch-2" + val timeBeforeTest = LocalDateTime.now() + + val app = drillApplication(rawDataServicesDIModule) { + postTestLaunches() + } + + app.client.post("/test-launches") { + header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + setBody( + """ + { + "groupId": "$testGroup", + "testSessionId": "$testSession", + "launches": [ + { + "id": "$launch1", + "testDefinitionId": "$testDefinition", + "result": "PASSED", + "duration": 100 + }, + { + "id": "$launch2", + "testDefinitionId": "$testDefinition", + "result": "FAILED", + "duration": 200 + } + ] + } + """.trimIndent() + ) + }.apply { + assertEquals(HttpStatusCode.OK, status) + assertJsonEquals( + """ + { + "message": "Test launches saved" + } + """.trimIndent(), + bodyAsText() + ) + } + + val saved = TestLaunchTable.selectAll() + .filter { it[TestLaunchTable.groupId] == testGroup } + .filter { it[TestLaunchTable.testSessionId] == testSession } + .filter { it[TestLaunchTable.testDefinitionId] == testDefinition } + + assertEquals(2, saved.size) + saved.forEach { + assertNotNull(it[TestLaunchTable.result]) + assertNotNull(it[TestLaunchTable.duration]) + assertTrue(it[TestLaunchTable.createdAt] >= timeBeforeTest) + } + } + + } \ No newline at end of file From beaad216195134b23fe08ca4a58bdf7975bd751e Mon Sep 17 00:00:00 2001 From: RomanDavlyatshin Date: Mon, 16 Feb 2026 16:43:18 +0400 Subject: [PATCH 2/2] docs: update openapi.yml --- admin-app/src/main/resources/openapi.yml | 120 +++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/admin-app/src/main/resources/openapi.yml b/admin-app/src/main/resources/openapi.yml index a3f92b59c..ad57dda97 100644 --- a/admin-app/src/main/resources/openapi.yml +++ b/admin-app/src/main/resources/openapi.yml @@ -189,6 +189,54 @@ paths: application/json: schema: $ref: '#/components/schemas/MessageResponse' + /api/data-ingest/test-definitions: + post: + summary: Save test definitions + operationId: postTestDefinitions + tags: + - data-ingest + security: + - apiKeyAuth: [ ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddTestDefinitionsPayload' + application/protobuf: + schema: + $ref: '#/components/schemas/AddTestDefinitionsPayload' + responses: + '200': + description: Test definitions saved + content: + application/json: + schema: + $ref: '#/components/schemas/MessageResponse' + /api/data-ingest/test-launches: + post: + summary: Save test launches + operationId: postTestLaunches + tags: + - data-ingest + security: + - apiKeyAuth: [ ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AddTestLaunchesPayload' + application/protobuf: + schema: + $ref: '#/components/schemas/AddTestLaunchesPayload' + responses: + '200': + description: Test launches saved + content: + application/json: + schema: + $ref: '#/components/schemas/MessageResponse' /api/data-ingest/sessions: put: summary: Save test sessions @@ -1644,6 +1692,78 @@ components: - SKIPPED - SMART_SKIPPED - UNKNOWN + AddTestDefinitionsPayload: + type: object + required: + - groupId + - definitions + properties: + groupId: + type: string + definitions: + type: array + items: + $ref: '#/components/schemas/TestDefinitionPayload' + TestDefinitionPayload: + type: object + required: + - id + - runner + - name + properties: + id: + type: string + runner: + type: string + name: + type: string + type: + type: string + nullable: true + path: + type: string + nullable: true + tags: + type: array + items: + type: string + default: [ ] + metadata: + type: object + nullable: true + additionalProperties: true + AddTestLaunchesPayload: + type: object + required: + - groupId + - testSessionId + - launches + properties: + groupId: + type: string + testSessionId: + type: string + launches: + type: array + items: + $ref: '#/components/schemas/TestLaunchPayload' + TestLaunchPayload: + type: object + required: + - id + - testDefinitionId + properties: + id: + type: string + testDefinitionId: + type: string + result: + type: string + nullable: true + duration: + type: integer + format: int32 + nullable: true SessionPayload: type: object properties: