diff --git a/admin-app/src/main/resources/openapi.yml b/admin-app/src/main/resources/openapi.yml index 818882c6a..cfb3b185f 100644 --- a/admin-app/src/main/resources/openapi.yml +++ b/admin-app/src/main/resources/openapi.yml @@ -644,6 +644,16 @@ paths: in: query schema: type: string + - name: testSessionId + in: query + description: Filter coverage by test session ID. When specified, only coverage data from the given test session is included. + schema: + type: string + - name: testDefinitionId + in: query + description: Filter coverage by test definition ID. Requires testSessionId to be specified. When provided, only coverage data from the specific test definition is included. + schema: + type: string responses: '200': description: Coverage treemap data diff --git a/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/models/MethodCriteria.kt b/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/models/MethodCriteria.kt index 5b4f9a88c..bffdcc0c3 100644 --- a/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/models/MethodCriteria.kt +++ b/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/models/MethodCriteria.kt @@ -29,13 +29,13 @@ open class MethodCriteria( object NONE : MethodCriteria() open val packageNamePattern: String? - get() = packageName?.let { it.removeSuffix("/") + "/" + "%" } + get() = packageName?.let { "$it%" } open val signaturePattern: String? get() = listOf( - className ?: "%", - methodName ?: "%", - methodParams ?: "%", - returnType ?: "%" + className?.takeIf { it.isNotEmpty() } ?: "%", + methodName?.takeIf { it.isNotEmpty() } ?: "%", + methodParams?.takeIf { it.isNotEmpty() } ?: "%", + returnType?.takeIf { it.isNotEmpty() } ?: "%" ).joinToString(":").takeIf { it != "%:%:%:%" } } \ No newline at end of file diff --git a/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/repository/MetricsRepository.kt b/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/repository/MetricsRepository.kt index 7a5b5d08b..a8a40fbfc 100644 --- a/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/repository/MetricsRepository.kt +++ b/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/repository/MetricsRepository.kt @@ -46,6 +46,25 @@ interface MetricsRepository { offset: Int? = null, limit: Int? = null ): List> + suspend fun getMethodsWithCoverageByTestSession( + buildId: String, + testSessionId: String, + testTags: List = emptyList(), + packageNamePattern: String? = null, + methodSignaturePattern: String? = null, + coverageAppEnvIds: List = emptyList(), + ): List> + + suspend fun getMethodsWithCoverageByTestDefinition( + buildId: String, + testSessionId: String, + testDefinitionId: String, + packageNamePattern: String? = null, + methodSignaturePattern: String? = null, + coverageAppEnvIds: List = emptyList(), + ): List> + + suspend fun getMethodsCount( buildId: String, packageNamePattern: String? = null, diff --git a/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/repository/impl/MetricsRepositoryImpl.kt b/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/repository/impl/MetricsRepositoryImpl.kt index d57486b4d..0ab1161f7 100644 --- a/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/repository/impl/MetricsRepositoryImpl.kt +++ b/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/repository/impl/MetricsRepositoryImpl.kt @@ -144,6 +144,87 @@ class MetricsRepositoryImpl : MetricsRepository { } } + override suspend fun getMethodsWithCoverageByTestSession( + buildId: String, + testSessionId: String, + testTags: List, + packageNamePattern: String?, + methodSignaturePattern: String?, + coverageAppEnvIds: List, + ): List> = transaction { + executeQueryReturnMap { + append( + """ + SELECT + signature, + class_name, + method_name, + method_params, + return_type, + probes_count, + covered_probes AS isolated_covered_probes, + covered_probes AS aggregated_covered_probes, + probes_coverage_ratio AS isolated_probes_coverage_ratio, + probes_coverage_ratio AS aggregated_probes_coverage_ratio + FROM metrics.get_methods_with_coverage_by_test_session( + input_build_id => ?, + input_test_session_id => ? + """.trimIndent(), buildId, testSessionId + ) + appendOptional(", input_coverage_test_tags => ?", testTags) + appendOptional(", input_package_name_pattern => ?", packageNamePattern) { "$it%" } + appendOptional(", input_signature_pattern => ?", methodSignaturePattern) + appendOptional(", input_coverage_app_env_ids => ?", coverageAppEnvIds) + append( + """ + ) + ORDER BY signature + """.trimIndent() + ) + } + } + + override suspend fun getMethodsWithCoverageByTestDefinition( + buildId: String, + testSessionId: String, + testDefinitionId: String, + packageNamePattern: String?, + methodSignaturePattern: String?, + coverageAppEnvIds: List, + ): List> = transaction { + executeQueryReturnMap { + append( + """ + SELECT + signature, + class_name, + method_name, + method_params, + return_type, + probes_count, + covered_probes AS isolated_covered_probes, + covered_probes AS aggregated_covered_probes, + probes_coverage_ratio AS isolated_probes_coverage_ratio, + probes_coverage_ratio AS aggregated_probes_coverage_ratio + FROM metrics.get_methods_with_coverage_by_test_definition( + input_build_id => ?, + input_test_session_id => ?, + input_test_definition_id => ? + """.trimIndent(), buildId, testSessionId, testDefinitionId + ) + appendOptional(", input_package_name_pattern => ?", packageNamePattern) { "$it%" } + appendOptional(", input_signature_pattern => ?", methodSignaturePattern) + appendOptional(", input_coverage_app_env_ids => ?", coverageAppEnvIds) + append( + """ + ) + ORDER BY signature + """.trimIndent() + ) + } + } + + override suspend fun getChangesWithCoverage( buildId: String, baselineBuildId: String?, diff --git a/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/route/MetricRoutes.kt b/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/route/MetricRoutes.kt index 37f7d0e3d..3b9b0e6e0 100644 --- a/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/route/MetricRoutes.kt +++ b/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/route/MetricRoutes.kt @@ -77,7 +77,9 @@ class Metrics { val branch: String? = null, val packageNamePattern: String? = null, val classNamePattern: String? = null, - val rootId: String? = null + val rootId: String? = null, + val testSessionId: String? = null, + val testDefinitionId: String? = null ) @Resource("/changes-coverage-treemap") @@ -327,7 +329,9 @@ fun Route.getCoverageTreemap() { params.branch, params.packageNamePattern, params.classNamePattern, - params.rootId + params.rootId, + params.testSessionId, + params.testDefinitionId ) this.call.respond(HttpStatusCode.OK, ApiResponse(treemap)) } diff --git a/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/service/MetricsService.kt b/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/service/MetricsService.kt index 4b16b2eaf..50713f35b 100644 --- a/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/service/MetricsService.kt +++ b/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/service/MetricsService.kt @@ -45,6 +45,8 @@ interface MetricsService { packageNamePattern: String?, classNamePattern: String?, rootId: String?, + testSessionId: String? = null, + testDefinitionId: String? = null, ): List suspend fun getChangesCoverageTreemap( diff --git a/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/service/impl/MetricsServiceImpl.kt b/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/service/impl/MetricsServiceImpl.kt index e75807373..8ceba9e2f 100644 --- a/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/service/impl/MetricsServiceImpl.kt +++ b/admin-metrics/src/main/kotlin/com/epam/drill/admin/metrics/service/impl/MetricsServiceImpl.kt @@ -114,21 +114,53 @@ class MetricsServiceImpl( branch: String?, packageNamePattern: String?, classNamePattern: String?, - rootId: String? + rootId: String?, + testSessionId: String?, + testDefinitionId: String?, ): List { if (!metricsRepository.buildExists(buildId)) { throw BuildNotFound("Build info not found for $buildId") } - - val data = metricsRepository.getMethodsWithCoverage( - buildId = buildId, - coverageTestTag = testTag?.takeIf { it.isNotBlank() }, - coverageEnvId = envId?.takeIf { it.isNotBlank() }, - coverageBranch = branch?.takeIf { it.isNotBlank() }, - packageName = packageNamePattern?.takeIf { it.isNotBlank() }, - className = classNamePattern?.takeIf { it.isNotBlank() } + val methodCriteria = MethodCriteria( + packageName = packageNamePattern, + className = classNamePattern ) + val data = when { + testDefinitionId != null -> { + val resolvedTestSessionId = testSessionId + ?: throw IllegalArgumentException("testSessionId is required when testDefinitionId is specified") + metricsRepository.getMethodsWithCoverageByTestDefinition( + buildId = buildId, + testSessionId = resolvedTestSessionId, + testDefinitionId = testDefinitionId, + packageNamePattern = methodCriteria.packageNamePattern, + methodSignaturePattern = methodCriteria.signaturePattern, + coverageAppEnvIds = envId?.takeIf { it.isNotBlank() }?.let { listOf(it) } ?: emptyList(), + ) + } + testSessionId != null -> { + metricsRepository.getMethodsWithCoverageByTestSession( + buildId = buildId, + testSessionId = testSessionId, + packageNamePattern = methodCriteria.packageNamePattern, + methodSignaturePattern = methodCriteria.signaturePattern, + coverageAppEnvIds = envId?.takeIf { it.isNotBlank() }?.let { listOf(it) } ?: emptyList(), + testTags = testTag?.takeIf { it.isNotBlank() }?.let { listOf(it) } ?: emptyList(), + ) + } + else -> { + metricsRepository.getMethodsWithCoverage( + buildId = buildId, + coverageTestTag = testTag?.takeIf { it.isNotBlank() }, + coverageEnvId = envId?.takeIf { it.isNotBlank() }, + coverageBranch = branch?.takeIf { it.isNotBlank() }, + packageName = packageNamePattern?.takeIf { it.isNotBlank() }, + className = classNamePattern?.takeIf { it.isNotBlank() } + ) + } + } + return buildTree(data, rootId) } diff --git a/admin-metrics/src/main/resources/metrics/db/migration/R__2_Views.sql b/admin-metrics/src/main/resources/metrics/db/migration/R__2_Views.sql index d386e2ce5..e77f43181 100644 --- a/admin-metrics/src/main/resources/metrics/db/migration/R__2_Views.sql +++ b/admin-metrics/src/main/resources/metrics/db/migration/R__2_Views.sql @@ -131,3 +131,34 @@ SELECT FROM metrics.test_definitions td LEFT JOIN metrics.test_launches tl ON tl.group_id = td.group_id AND tl.test_definition_id = td.test_definition_id GROUP BY td.group_id, td.test_path, tl.test_session_id; + +------------------------------------------------------------------- +-- Create a view of test session definitions with information about +-- the number of test launches, total duration, and overall results +------------------------------------------------------------------- +DROP VIEW IF EXISTS metrics.test_session_definitions CASCADE; +CREATE OR REPLACE VIEW metrics.test_session_definitions AS +SELECT + ts.test_session_id, + td.test_definition_id, + COUNT(*) AS test_launches, + SUM(tl.test_duration) AS test_duration_sum, + raw_data.format_duration(SUM(tl.test_duration)::bigint) AS test_duration_sum_formatted, + CASE + WHEN SUM(tl.failed) > 0 THEN 'FAILED' + WHEN SUM(tl.passed) > 0 THEN 'PASSED' + WHEN SUM(tl.smart_skipped) > 0 THEN 'SMART_SKIPPED' + WHEN SUM(tl.skipped) > 0 THEN 'SKIPPED' + ELSE 'UNKNOWN' + END AS test_result, + MIN(td.test_name) AS test_name, + MIN(td.test_path) AS test_path, + MIN(td.test_runner) AS test_runner, + MIN(td.test_tags) AS test_tags, + array_to_string(MIN(td.test_tags), ', ') AS test_tags_formatted +FROM metrics.test_sessions ts +JOIN metrics.test_launches tl ON tl.test_session_id = ts.test_session_id AND tl.group_id = ts.group_id +JOIN metrics.test_definitions td ON td.group_id = tl.group_id AND td.test_definition_id = tl.test_definition_id +GROUP BY + ts.test_session_id, + td.test_definition_id; diff --git a/admin-metrics/src/test/kotlin/com/epam/drill/admin/metrics/CoverageTreemapTest.kt b/admin-metrics/src/test/kotlin/com/epam/drill/admin/metrics/CoverageTreemapTest.kt index 7d69ff266..539575ccb 100644 --- a/admin-metrics/src/test/kotlin/com/epam/drill/admin/metrics/CoverageTreemapTest.kt +++ b/admin-metrics/src/test/kotlin/com/epam/drill/admin/metrics/CoverageTreemapTest.kt @@ -20,9 +20,10 @@ import com.epam.drill.admin.test.DatabaseTests import com.epam.drill.admin.test.withTransaction import com.epam.drill.admin.writer.rawdata.config.RawDataWriterDatabaseConfig import com.epam.drill.admin.writer.rawdata.route.payload.BuildPayload +import com.epam.drill.admin.writer.rawdata.route.payload.InstancePayload +import com.epam.drill.admin.writer.rawdata.route.payload.SingleMethodPayload import com.epam.drill.admin.writer.rawdata.table.* import io.ktor.client.request.* -import io.ktor.client.statement.* import org.jetbrains.exposed.sql.deleteAll import org.junit.jupiter.api.AfterEach import kotlin.test.Test @@ -70,6 +71,204 @@ class CoverageTreemapTest : DatabaseTests({ } } + @Test + fun `given build with coverage from multiple sessions, coverage-treemap filtered by testSessionId should return only that session coverage`() = havingData { + build1 has listOf(method1, method2) + test1 of session1 covers method1 with probesOf(1, 1) on build1 + test2 of session2 covers method2 with probesOf(1, 1, 1) on build1 + }.expectThat { + // Without filter - all coverage + client.get("/metrics/coverage-treemap") { + parameter("buildId", "${build1.groupId}:${build1.appId}:${build1.buildVersion}") + }.returns { data -> + assertTrue(data.isNotEmpty()) + assertTrue(data.any { it["name"].toString().startsWith(method1.name) && it["covered_probes"] == 2 }) + assertTrue(data.any { it["name"].toString().startsWith(method2.name) && it["covered_probes"] == 3 }) + } + // Filter by session1 - only method1 coverage + client.get("/metrics/coverage-treemap") { + parameter("buildId", "${build1.groupId}:${build1.appId}:${build1.buildVersion}") + parameter("testSessionId", session1.id) + }.returns { data -> + assertTrue(data.isNotEmpty()) + assertTrue(data.any { it["name"].toString().startsWith(method1.name) && it["covered_probes"] == 2 }) + assertTrue(data.any { it["name"].toString().startsWith(method2.name) && it["covered_probes"] == 0 }) + } + // Filter by session2 - only method2 coverage + client.get("/metrics/coverage-treemap") { + parameter("buildId", "${build1.groupId}:${build1.appId}:${build1.buildVersion}") + parameter("testSessionId", session2.id) + }.returns { data -> + assertTrue(data.isNotEmpty()) + assertTrue(data.any { it["name"].toString().startsWith(method1.name) && it["covered_probes"] == 0 }) + assertTrue(data.any { it["name"].toString().startsWith(method2.name) && it["covered_probes"] == 3 }) + } + } + + @Test + fun `given build with coverage, coverage-treemap filtered by testDefinitionId should return only that test definition coverage`() { + havingData { + build1 has listOf(method1, method2) + test1 of session1 covers method1 with probesOf(1, 1) on build1 + test2 of session1 covers method2 with probesOf(1, 1, 1) on build1 + test2 of session2 covers method2 with probesOf(1, 0, 0) on build1 + }.expectThat { + // Filter by test1 definition (test1 covering method1) - requires testSessionId + client.get("/metrics/coverage-treemap") { + parameter("buildId", "${build1.groupId}:${build1.appId}:${build1.buildVersion}") + parameter("testSessionId", session1.id) + parameter("testDefinitionId", test1.definitionId) + }.returns { data -> + assertTrue(data.isNotEmpty()) + assertTrue(data.any { it["name"].toString().startsWith(method1.name) && it["covered_probes"] == 2 }) + assertTrue(data.any { it["name"].toString().startsWith(method2.name) && it["covered_probes"] == 0 }) + } + // Filter by test2 definition (test2 covering method2) - requires testSessionId + client.get("/metrics/coverage-treemap") { + parameter("buildId", "${build1.groupId}:${build1.appId}:${build1.buildVersion}") + parameter("testSessionId", session1.id) + parameter("testDefinitionId", test2.definitionId) + }.returns { data -> + assertTrue(data.isNotEmpty()) + assertTrue(data.any { it["name"].toString().startsWith(method1.name) && it["covered_probes"] == 0 }) + assertTrue(data.any { it["name"].toString().startsWith(method2.name) && it["covered_probes"] == 3 }) + } + } + } + + @Test + fun `coverage-treemap filtered by packageNamePattern should return only matching methods`() { + val methodA = SingleMethodPayload( + classname = "com.example.foo.ClassA", + name = "methodA", params = "()", returnType = "void", + probesCount = 2, probesStartPos = 0, bodyChecksum = "A00" + ) + val methodB = SingleMethodPayload( + classname = "com.other.bar.ClassB", + name = "methodB", params = "()", returnType = "void", + probesCount = 3, probesStartPos = 2, bodyChecksum = "B00" + ) + havingData { + build1 has listOf(methodA, methodB) + test1 of session1 covers methodA with probesOf(1, 1) on build1 + test1 of session1 covers methodB with probesOf(1, 1, 1) on build1 + }.expectThat { + fun onlyMatchingMethods(data: List>) { + assertTrue(data.isNotEmpty()) + assertTrue(data.all { it["name"].toString().contains("com.example.foo") }) + assertTrue(data.none { it["name"].toString().contains("com.other.bar") }) + } + // Get coverage treemap by buildId + client.get("/metrics/coverage-treemap") { + parameter("buildId", "${build1.groupId}:${build1.appId}:${build1.buildVersion}") + parameter("packageNamePattern", "com.example.foo") + }.returns { onlyMatchingMethods(it) } + // Get coverage treemap by buildId + testSessionId + client.get("/metrics/coverage-treemap") { + parameter("buildId", "${build1.groupId}:${build1.appId}:${build1.buildVersion}") + parameter("testSessionId", session1.id) + parameter("packageNamePattern", "com.example.foo") + }.returns { onlyMatchingMethods(it) } + // Get coverage treemap by buildId + testSessionId + testDefinitionId + client.get("/metrics/coverage-treemap") { + parameter("buildId", "${build1.groupId}:${build1.appId}:${build1.buildVersion}") + parameter("testSessionId", session1.id) + parameter("testDefinitionId", test1.definitionId) + parameter("packageNamePattern", "com.example.foo") + }.returns { onlyMatchingMethods(it) } + } + } + + @Test + fun `coverage-treemap filtered by classNamePattern should return only matching methods`() { + val methodA = SingleMethodPayload( + classname = "com.example.foo.ClassA", + name = "methodA", params = "()", returnType = "void", + probesCount = 2, probesStartPos = 0, bodyChecksum = "A00" + ) + val methodB = SingleMethodPayload( + classname = "com.example.foo.ClassB", + name = "methodB", params = "()", returnType = "void", + probesCount = 3, probesStartPos = 2, bodyChecksum = "B00" + ) + havingData { + build1 has listOf(methodA, methodB) + test1 of session1 covers methodA with probesOf(1, 1) on build1 + test1 of session1 covers methodB with probesOf(1, 1, 1) on build1 + }.expectThat { + fun onlyMatchingMethods(data: List>) { + assertTrue(data.isNotEmpty()) + assertTrue(data.all { it["name"].toString().contains("com.example.foo.ClassA") }) + assertTrue(data.none { it["name"].toString().contains("com.example.foo.ClassB") }) + } + // Get coverage treemap by buildId + client.get("/metrics/coverage-treemap") { + parameter("buildId", "${build1.groupId}:${build1.appId}:${build1.buildVersion}") + parameter("classNamePattern", "com.example.foo.ClassA") + }.returns { onlyMatchingMethods(it) } + // Get coverage treemap by buildId + testSessionId + client.get("/metrics/coverage-treemap") { + parameter("buildId", "${build1.groupId}:${build1.appId}:${build1.buildVersion}") + parameter("testSessionId", session1.id) + parameter("classNamePattern", "com.example.foo.ClassA") + }.returns { onlyMatchingMethods(it) } + // Get coverage treemap by buildId + testSessionId + testDefinitionId + client.get("/metrics/coverage-treemap") { + parameter("buildId", "${build1.groupId}:${build1.appId}:${build1.buildVersion}") + parameter("testSessionId", session1.id) + parameter("testDefinitionId", test1.definitionId) + parameter("classNamePattern", "com.example.foo.ClassA") + }.returns { onlyMatchingMethods(it) } + } + } + + @Test + fun `coverage-treemap filtered by envId should return only matching environments`() { + havingData { + val envA = InstancePayload( + groupId = testGroup, + appId = testApp, + instanceId = "instance-A", + buildVersion = "1.0.0", + envId = "env-A" + ) + val envB = InstancePayload( + groupId = testGroup, + appId = testApp, + instanceId = "instance-B", + buildVersion = "1.0.0", + envId = "env-B" + ) + envA has listOf(method1) + envB has listOf(method1) + test1 of session1 covers method1 with probesOf(1, 0) on envA //1 of 2 probes covered on env-A + test1 of session1 covers method1 with probesOf(1, 1) on envB + }.expectThat { + fun onlyMatchingEnvironments(data: List>) { + assertTrue(data.isNotEmpty()) + assertTrue(data.filter { it["name"] == method1.name }.all { it["isolated_covered_probes"].toString() == "1" }) + } + // Get coverage treemap by buildId + client.get("/metrics/coverage-treemap") { + parameter("buildId", "${build1.groupId}:${build1.appId}:${build1.buildVersion}") + parameter("envId", "env-A") + }.returns { onlyMatchingEnvironments(it) } + // Get coverage treemap by buildId + testSessionId + client.get("/metrics/coverage-treemap") { + parameter("buildId", "${build1.groupId}:${build1.appId}:${build1.buildVersion}") + parameter("testSessionId", session1.id) + parameter("envId", "env-A") + }.returns { onlyMatchingEnvironments(it) } + // Get coverage treemap by buildId + testSessionId + testDefinitionId + client.get("/metrics/coverage-treemap") { + parameter("buildId", "${build1.groupId}:${build1.appId}:${build1.buildVersion}") + parameter("testSessionId", session1.id) + parameter("testDefinitionId", test1.definitionId) + parameter("envId", "env-A") + }.returns { onlyMatchingEnvironments(it) } + } + } + @AfterEach fun clearAll() = withTransaction { MethodCoverageTable.deleteAll()