diff --git a/gradle.properties b/gradle.properties index 4508084cc..99e5153c9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ retrofitVersion=2.9.0 auth0_version=4.4.0 jacksonDatabindVersion=2.15.3 -jacksonVersion=2.15.3 +jacksonVersion=2.16.2 postgresDriverVersion=42.7.3 diff --git a/server/server-api/src/main/kotlin/projektor/server/api/repository/RepositoryTestRunSummaries.kt b/server/server-api/src/main/kotlin/projektor/server/api/repository/RepositoryTestRunSummaries.kt new file mode 100644 index 000000000..8cdc610ba --- /dev/null +++ b/server/server-api/src/main/kotlin/projektor/server/api/repository/RepositoryTestRunSummaries.kt @@ -0,0 +1,5 @@ +package projektor.server.api.repository + +import projektor.server.api.TestRunSummary + +data class RepositoryTestRunSummaries(val testRuns: List) diff --git a/server/server-app/gradle.properties b/server/server-app/gradle.properties index 3074fbb56..a9648783a 100644 --- a/server/server-app/gradle.properties +++ b/server/server-app/gradle.properties @@ -2,4 +2,4 @@ ktor_version=2.3.9 koin_version=3.5.1 koin_test_version=3.5.4 -opentelemetry_version=1.28.0 \ No newline at end of file +opentelemetry_version=1.36.0 diff --git a/server/server-app/opentelemetry/.version b/server/server-app/opentelemetry/.version index e4264e984..ccbccc3dc 100644 --- a/server/server-app/opentelemetry/.version +++ b/server/server-app/opentelemetry/.version @@ -1 +1 @@ -1.21.0 \ No newline at end of file +2.2.0 diff --git a/server/server-app/opentelemetry/opentelemetry-javaagent.jar b/server/server-app/opentelemetry/opentelemetry-javaagent.jar index 8ad6fa490..04f1353cd 100644 Binary files a/server/server-app/opentelemetry/opentelemetry-javaagent.jar and b/server/server-app/opentelemetry/opentelemetry-javaagent.jar differ diff --git a/server/server-app/src/main/kotlin/projektor/repository/testrun/RepositoryTestRunDatabaseRepository.kt b/server/server-app/src/main/kotlin/projektor/repository/testrun/RepositoryTestRunDatabaseRepository.kt index 1d5a3ccbb..e4326548c 100644 --- a/server/server-app/src/main/kotlin/projektor/repository/testrun/RepositoryTestRunDatabaseRepository.kt +++ b/server/server-app/src/main/kotlin/projektor/repository/testrun/RepositoryTestRunDatabaseRepository.kt @@ -1,5 +1,6 @@ package projektor.repository.testrun +import io.opentelemetry.api.GlobalOpenTelemetry import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.jooq.Condition @@ -14,14 +15,18 @@ import projektor.database.generated.Tables.TEST_CASE import projektor.database.generated.Tables.TEST_RUN import projektor.server.api.PublicId import projektor.server.api.TestCase +import projektor.server.api.TestRunSummary import projektor.server.api.repository.BranchType import projektor.server.api.repository.RepositoryTestRunTimeline import projektor.server.api.repository.RepositoryTestRunTimelineEntry +import projektor.telemetry.startSpanWithParent import projektor.testcase.TestCaseDatabaseRepository import projektor.testcase.TestCaseDatabaseRepository.Companion.selectTestCase import kotlin.streams.toList class RepositoryTestRunDatabaseRepository(private val dslContext: DSLContext) : RepositoryTestRunRepository { + private val tracer = GlobalOpenTelemetry.getTracer("projektor.RepositoryTestRunDatabaseRepository") + private val timelineEntryMapper = JdbcMapperFactory.newInstance() .addKeys("public_id") .ignorePropertyNotFound() @@ -51,6 +56,29 @@ class RepositoryTestRunDatabaseRepository(private val dslContext: DSLContext) : if (timelineEntries.isNotEmpty()) RepositoryTestRunTimeline(timelineEntries) else null } + override suspend fun fetchRepositoryTestRunSummaries(repoName: String, projectName: String?, limit: Int): List = + withContext(Dispatchers.IO) { + val span = tracer.startSpanWithParent("projektor.fetchRepositoryTestRunSummaries") + + val testRunSummaries = dslContext + .select(TEST_RUN.PUBLIC_ID.`as`("id")) + .select(TEST_RUN.fields().filterNot { it.name == "id" }.toList()) + .from(TEST_RUN) + .innerJoin(RESULTS_METADATA).on(TEST_RUN.ID.eq(RESULTS_METADATA.TEST_RUN_ID)) + .innerJoin(GIT_METADATA).on(TEST_RUN.ID.eq(GIT_METADATA.TEST_RUN_ID)) + .where( + runInCIFromRepo(repoName, projectName) + .and(withBranchType(BranchType.MAINLINE)) + ) + .orderBy(TEST_RUN.CREATED_TIMESTAMP.desc()) + .limit(limit) + .fetchInto(TestRunSummary::class.java) + + span.end() + + testRunSummaries + } + override suspend fun fetchRepositoryFailingTestCases( repoName: String, projectName: String?, diff --git a/server/server-app/src/main/kotlin/projektor/repository/testrun/RepositoryTestRunRepository.kt b/server/server-app/src/main/kotlin/projektor/repository/testrun/RepositoryTestRunRepository.kt index d6e7d9c73..685086903 100644 --- a/server/server-app/src/main/kotlin/projektor/repository/testrun/RepositoryTestRunRepository.kt +++ b/server/server-app/src/main/kotlin/projektor/repository/testrun/RepositoryTestRunRepository.kt @@ -2,12 +2,15 @@ package projektor.repository.testrun import projektor.server.api.PublicId import projektor.server.api.TestCase +import projektor.server.api.TestRunSummary import projektor.server.api.repository.BranchType import projektor.server.api.repository.RepositoryTestRunTimeline interface RepositoryTestRunRepository { suspend fun fetchRepositoryTestRunTimeline(repoName: String, projectName: String?): RepositoryTestRunTimeline? + suspend fun fetchRepositoryTestRunSummaries(repoName: String, projectName: String?, limit: Int): List + suspend fun fetchRepositoryFailingTestCases(repoName: String, projectName: String?, maxRuns: Int, branchType: BranchType): List suspend fun fetchRecentTestRunPublicIds(repoName: String, projectName: String?, maxRuns: Int): List diff --git a/server/server-app/src/main/kotlin/projektor/repository/testrun/RepositoryTestRunService.kt b/server/server-app/src/main/kotlin/projektor/repository/testrun/RepositoryTestRunService.kt index b06a33f58..354c26899 100644 --- a/server/server-app/src/main/kotlin/projektor/repository/testrun/RepositoryTestRunService.kt +++ b/server/server-app/src/main/kotlin/projektor/repository/testrun/RepositoryTestRunService.kt @@ -1,5 +1,6 @@ package projektor.repository.testrun +import projektor.server.api.TestRunSummary import projektor.server.api.repository.BranchType import projektor.server.api.repository.RepositoryFlakyTest import kotlin.math.min @@ -23,4 +24,7 @@ class RepositoryTestRunService(private val repositoryTestRunRepository: Reposito return flakyTestCalculator.calculateFlakyTests(failingTestCases, flakyFailureThreshold, testRunCount) } + + suspend fun fetchRepositoryTestRunSummaries(repoName: String, projectName: String?, limit: Int): List = + repositoryTestRunRepository.fetchRepositoryTestRunSummaries(repoName, projectName, limit) } diff --git a/server/server-app/src/main/kotlin/projektor/route/ApiRoutes.kt b/server/server-app/src/main/kotlin/projektor/route/ApiRoutes.kt index efafbe9cf..5f6a3e3ab 100644 --- a/server/server-app/src/main/kotlin/projektor/route/ApiRoutes.kt +++ b/server/server-app/src/main/kotlin/projektor/route/ApiRoutes.kt @@ -11,6 +11,8 @@ import projektor.repository.testrun.RepositoryTestRunService import projektor.server.api.repository.BranchSearch import projektor.server.api.repository.BranchType import projektor.server.api.repository.RepositoryFlakyTests +import projektor.server.api.repository.RepositoryTestRunSummaries +import kotlin.math.min fun Route.api( organizationCoverageService: OrganizationCoverageService, @@ -84,4 +86,17 @@ fun Route.api( call.respond(HttpStatusCode.NoContent) } } + + get("/api/v1/repo/{orgPart}/{repoPart}/tests/runs/summaries") { + val orgPart = call.parameters.getOrFail("orgPart") + val repoPart = call.parameters.getOrFail("repoPart") + val projectName = call.request.queryParameters["project"] + val limit = min(call.request.queryParameters["limit"]?.toInt() ?: 10, 100) // Defaults to 10 with a max of 100 + + val fullRepoName = "$orgPart/$repoPart" + + val testRunSummaries = repositoryTestRunService.fetchRepositoryTestRunSummaries(fullRepoName, projectName, limit) + + call.respond(HttpStatusCode.OK, RepositoryTestRunSummaries(testRuns = testRunSummaries)) + } } diff --git a/server/server-app/src/test/kotlin/projektor/api/ApiRepositoryApplicationTest.kt b/server/server-app/src/test/kotlin/projektor/api/ApiRepositoryApplicationTest.kt index 28342867a..8959a39b6 100644 --- a/server/server-app/src/test/kotlin/projektor/api/ApiRepositoryApplicationTest.kt +++ b/server/server-app/src/test/kotlin/projektor/api/ApiRepositoryApplicationTest.kt @@ -3,12 +3,12 @@ package projektor.api import com.fasterxml.jackson.module.kotlin.readValue import io.ktor.http.* import io.ktor.server.testing.* -import org.apache.commons.lang3.RandomStringUtils import org.junit.jupiter.api.Test import projektor.ApplicationTestCase import projektor.incomingresults.randomPublicId import projektor.server.api.repository.coverage.RepositoryCurrentCoverage import projektor.server.example.coverage.JacocoXmlLoader +import projektor.util.randomFullRepoName import strikt.api.expectThat import strikt.assertions.isEqualTo import java.time.ZoneOffset @@ -17,8 +17,7 @@ import kotlin.test.assertNotNull class ApiRepositoryApplicationTest : ApplicationTestCase() { @Test fun `should fetch current coverage for repository without project name`() { - val orgName = RandomStringUtils.randomAlphabetic(12) - val repoName = "$orgName/repo" + val repoName = randomFullRepoName() val firstRunPublicId = randomPublicId() val secondRunPublicId = randomPublicId() @@ -85,8 +84,7 @@ class ApiRepositoryApplicationTest : ApplicationTestCase() { @Test fun `should fetch current coverage for repository with project name`() { - val orgName = RandomStringUtils.randomAlphabetic(12) - val repoName = "$orgName/repo" + val repoName = randomFullRepoName() val firstRunPublicId = randomPublicId() val secondRunPublicId = randomPublicId() @@ -154,8 +152,7 @@ class ApiRepositoryApplicationTest : ApplicationTestCase() { @Test fun `should fetch current coverage for repository with branch name`() { - val orgName = RandomStringUtils.randomAlphabetic(12) - val repoName = "$orgName/repo" + val repoName = randomFullRepoName() val firstRunPublicId = randomPublicId() val secondRunPublicId = randomPublicId() @@ -224,8 +221,7 @@ class ApiRepositoryApplicationTest : ApplicationTestCase() { @Test fun `when no branch specific should fetch current coverage from mainline branch`() { - val orgName = RandomStringUtils.randomAlphabetic(12) - val repoName = "$orgName/repo" + val repoName = randomFullRepoName() val otherBranchRunPublicId1 = randomPublicId() val mainRunPublicId = randomPublicId() diff --git a/server/server-app/src/test/kotlin/projektor/api/ApiRepositoryFlakyTestsApplicationTest.kt b/server/server-app/src/test/kotlin/projektor/api/ApiRepositoryFlakyTestsApplicationTest.kt index 4340db36b..4707c9852 100644 --- a/server/server-app/src/test/kotlin/projektor/api/ApiRepositoryFlakyTestsApplicationTest.kt +++ b/server/server-app/src/test/kotlin/projektor/api/ApiRepositoryFlakyTestsApplicationTest.kt @@ -2,12 +2,12 @@ package projektor.api import io.ktor.http.* import io.ktor.server.testing.* -import org.apache.commons.lang3.RandomStringUtils import org.junit.jupiter.api.Test import projektor.ApplicationTestCase import projektor.TestSuiteData import projektor.incomingresults.randomPublicId import projektor.server.api.repository.RepositoryFlakyTests +import projektor.util.randomFullRepoName import strikt.api.expectThat import strikt.assertions.any import strikt.assertions.contains @@ -18,8 +18,7 @@ import kotlin.test.assertNotNull class ApiRepositoryFlakyTestsApplicationTest : ApplicationTestCase() { @Test fun `when flaky tests without a project name should return them`() { - val orgName = RandomStringUtils.randomAlphabetic(12) - val repoName = "$orgName/repo" + val repoName = randomFullRepoName() val projectName = null val publicIds = (1..5).map { randomPublicId() } @@ -60,8 +59,7 @@ class ApiRepositoryFlakyTestsApplicationTest : ApplicationTestCase() { @Test fun `when flaky tests within specified max runs and threshold should find them`() { - val orgName = RandomStringUtils.randomAlphabetic(12) - val repoName = "$orgName/repo" + val repoName = randomFullRepoName() val projectName = null val failingPublicIds = (1..3).map { randomPublicId() } @@ -111,8 +109,7 @@ class ApiRepositoryFlakyTestsApplicationTest : ApplicationTestCase() { @Test fun `when flaky tests with a project name should return them`() { - val orgName = RandomStringUtils.randomAlphabetic(12) - val repoName = "$orgName/repo" + val repoName = randomFullRepoName() val projectName = "my-project" val publicIds = (1..5).map { randomPublicId() } @@ -153,8 +150,7 @@ class ApiRepositoryFlakyTestsApplicationTest : ApplicationTestCase() { @Test fun `should find flaky tests in mainline only`() { - val orgName = RandomStringUtils.randomAlphabetic(12) - val repoName = "$orgName/repo" + val repoName = randomFullRepoName() val projectName = null val failingMainlinePublicIds = (1..3).map { randomPublicId() } diff --git a/server/server-app/src/test/kotlin/projektor/api/ApiRepositoryTestRunSummariesApplicationTest.kt b/server/server-app/src/test/kotlin/projektor/api/ApiRepositoryTestRunSummariesApplicationTest.kt new file mode 100644 index 000000000..e6c162d35 --- /dev/null +++ b/server/server-app/src/test/kotlin/projektor/api/ApiRepositoryTestRunSummariesApplicationTest.kt @@ -0,0 +1,158 @@ +package projektor.api + +import io.ktor.http.* +import io.ktor.server.testing.* +import org.junit.jupiter.api.Test +import projektor.ApplicationTestCase +import projektor.createTestRun +import projektor.incomingresults.randomPublicId +import projektor.server.api.repository.RepositoryTestRunSummaries +import projektor.util.randomFullRepoName +import strikt.api.expectThat +import strikt.assertions.hasSize +import strikt.assertions.isEqualTo +import java.math.BigDecimal +import kotlin.test.assertNotNull + +class ApiRepositoryTestRunSummariesApplicationTest : ApplicationTestCase() { + @Test + fun `when no project should get test run summaries for repo`() { + val repoName = randomFullRepoName() + + val firstRunPublicId = randomPublicId() + val secondRunPublicId = randomPublicId() + val thirdRunPublicId = randomPublicId() + + val otherBranchRunPublicId = randomPublicId() + + withTestApplication(::createTestApplication) { + handleRequest(HttpMethod.Get, "/api/v1/repo/$repoName/tests/runs/summaries") { + val firstTestRun = createTestRun(firstRunPublicId, 20, 0, BigDecimal("10.001")) + testRunDao.insert(firstTestRun) + testRunDBGenerator.addGitMetadata(firstTestRun, repoName, true, "main", null, null, null) + testRunDBGenerator.addResultsMetadata(firstTestRun, true) + + val secondTestRun = createTestRun(secondRunPublicId, 30, 0, BigDecimal("15.001")) + testRunDao.insert(secondTestRun) + testRunDBGenerator.addGitMetadata(secondTestRun, repoName, true, "main", null, null, null) + testRunDBGenerator.addResultsMetadata(secondTestRun, true) + + val thirdTestRun = createTestRun(thirdRunPublicId, 45, 0, BigDecimal("25.001")) + testRunDao.insert(thirdTestRun) + testRunDBGenerator.addGitMetadata(thirdTestRun, repoName, true, "main", null, null, null) + testRunDBGenerator.addResultsMetadata(thirdTestRun, true) + + val otherBranchRunPublicId = createTestRun(otherBranchRunPublicId, 50, 0, BigDecimal("30.001")) + testRunDao.insert(otherBranchRunPublicId) + testRunDBGenerator.addGitMetadata(otherBranchRunPublicId, repoName, false, "feature-branch", null, null, null) + testRunDBGenerator.addResultsMetadata(otherBranchRunPublicId, true) + }.apply { + expectThat(response.status()).isEqualTo(HttpStatusCode.OK) + + val repositoryTestRunSummaries = objectMapper.readValue(response.content, RepositoryTestRunSummaries::class.java) + assertNotNull(repositoryTestRunSummaries) + + expectThat(repositoryTestRunSummaries.testRuns).hasSize(3) + + // should sort in descending order of date created + expectThat(repositoryTestRunSummaries.testRuns.map { it.id }[0]).isEqualTo(thirdRunPublicId.id) + expectThat(repositoryTestRunSummaries.testRuns.map { it.id }[1]).isEqualTo(secondRunPublicId.id) + expectThat(repositoryTestRunSummaries.testRuns.map { it.id }[2]).isEqualTo(firstRunPublicId.id) + } + } + } + + @Test + fun `when no project and limit should get test run summaries for repo within limit`() { + val repoName = randomFullRepoName() + + val firstRunPublicId = randomPublicId() + val secondRunPublicId = randomPublicId() + val thirdRunPublicId = randomPublicId() + + val otherBranchRunPublicId = randomPublicId() + + withTestApplication(::createTestApplication) { + handleRequest(HttpMethod.Get, "/api/v1/repo/$repoName/tests/runs/summaries?limit=2") { + val firstTestRun = createTestRun(firstRunPublicId, 20, 0, BigDecimal("10.001")) + testRunDao.insert(firstTestRun) + testRunDBGenerator.addGitMetadata(firstTestRun, repoName, true, "main", null, null, null) + testRunDBGenerator.addResultsMetadata(firstTestRun, true) + + val secondTestRun = createTestRun(secondRunPublicId, 30, 0, BigDecimal("15.001")) + testRunDao.insert(secondTestRun) + testRunDBGenerator.addGitMetadata(secondTestRun, repoName, true, "main", null, null, null) + testRunDBGenerator.addResultsMetadata(secondTestRun, true) + + val thirdTestRun = createTestRun(thirdRunPublicId, 45, 0, BigDecimal("25.001")) + testRunDao.insert(thirdTestRun) + testRunDBGenerator.addGitMetadata(thirdTestRun, repoName, true, "main", null, null, null) + testRunDBGenerator.addResultsMetadata(thirdTestRun, true) + + val otherBranchRunPublicId = createTestRun(otherBranchRunPublicId, 50, 0, BigDecimal("30.001")) + testRunDao.insert(otherBranchRunPublicId) + testRunDBGenerator.addGitMetadata(otherBranchRunPublicId, repoName, false, "feature-branch", null, null, null) + testRunDBGenerator.addResultsMetadata(otherBranchRunPublicId, true) + }.apply { + expectThat(response.status()).isEqualTo(HttpStatusCode.OK) + + val repositoryTestRunSummaries = objectMapper.readValue(response.content, RepositoryTestRunSummaries::class.java) + assertNotNull(repositoryTestRunSummaries) + + expectThat(repositoryTestRunSummaries.testRuns).hasSize(2) + + // should sort in descending order of date created + expectThat(repositoryTestRunSummaries.testRuns.map { it.id }[0]).isEqualTo(thirdRunPublicId.id) + expectThat(repositoryTestRunSummaries.testRuns.map { it.id }[1]).isEqualTo(secondRunPublicId.id) + } + } + } + + @Test + fun `when project name should get test run summaries for repo and project`() { + val repoName = randomFullRepoName() + val projectName = "my-proj" + + val firstRunPublicId = randomPublicId() + val secondRunPublicId = randomPublicId() + val thirdRunPublicId = randomPublicId() + + val otherBranchRunPublicId = randomPublicId() + + withTestApplication(::createTestApplication) { + handleRequest(HttpMethod.Get, "/api/v1/repo/$repoName/tests/runs/summaries?project=$projectName") { + val firstTestRun = createTestRun(firstRunPublicId, 20, 0, BigDecimal("10.001")) + testRunDao.insert(firstTestRun) + testRunDBGenerator.addGitMetadata(firstTestRun, repoName, true, "main", projectName, null, null) + testRunDBGenerator.addResultsMetadata(firstTestRun, true) + + val secondTestRun = createTestRun(secondRunPublicId, 30, 0, BigDecimal("15.001")) + testRunDao.insert(secondTestRun) + testRunDBGenerator.addGitMetadata(secondTestRun, repoName, true, "main", projectName, null, null) + testRunDBGenerator.addResultsMetadata(secondTestRun, true) + + val thirdTestRun = createTestRun(thirdRunPublicId, 45, 0, BigDecimal("25.001")) + testRunDao.insert(thirdTestRun) + testRunDBGenerator.addGitMetadata(thirdTestRun, repoName, true, "main", projectName, null, null) + testRunDBGenerator.addResultsMetadata(thirdTestRun, true) + + val otherBranchRunPublicId = createTestRun(otherBranchRunPublicId, 50, 0, BigDecimal("30.001")) + testRunDao.insert(otherBranchRunPublicId) + testRunDBGenerator.addGitMetadata(otherBranchRunPublicId, repoName, false, "feature-branch", projectName, null, null) + testRunDBGenerator.addResultsMetadata(otherBranchRunPublicId, true) + }.apply { + expectThat(response.status()).isEqualTo(HttpStatusCode.OK) + + val repositoryTestRunSummaries = objectMapper.readValue(response.content, RepositoryTestRunSummaries::class.java) + assertNotNull(repositoryTestRunSummaries) + + expectThat(repositoryTestRunSummaries.testRuns).hasSize(3) + + // should sort in descending order of date created + expectThat(repositoryTestRunSummaries.testRuns.map { it.id }[0]).isEqualTo(thirdRunPublicId.id) + expectThat(repositoryTestRunSummaries.testRuns.map { it.id }[1]).isEqualTo(secondRunPublicId.id) + expectThat(repositoryTestRunSummaries.testRuns.map { it.id }[2]).isEqualTo(firstRunPublicId.id) + } + } + } +} diff --git a/server/server-app/src/test/kotlin/projektor/util/RepositoryUtil.kt b/server/server-app/src/test/kotlin/projektor/util/RepositoryUtil.kt new file mode 100644 index 000000000..4af551ca2 --- /dev/null +++ b/server/server-app/src/test/kotlin/projektor/util/RepositoryUtil.kt @@ -0,0 +1,9 @@ +package projektor.util + +import org.apache.commons.lang3.RandomStringUtils + +fun randomFullRepoName(): String { + val orgName = RandomStringUtils.randomAlphabetic(12) + + return "$orgName/repo" +}