diff --git a/backend/src/main/resources/styles.css b/backend/src/main/resources/styles.css index 51d6c89..07d25a2 100644 --- a/backend/src/main/resources/styles.css +++ b/backend/src/main/resources/styles.css @@ -41,13 +41,36 @@ form { } .test-success { color: white; - background-color: green; + background-color: darkgreen; padding: 12px; + border: 2px solid black; + border-radius: 5px; } .test-failure { color: white; - background-color: red; + background-color: darkred; padding: 12px; + border: 2px solid black; + border-radius: 5px; +} +.test-in-progress { + color: white; + background-color: #CCCC00; + padding: 12px; + border: 2px solid black; + border-radius: 5px; +} + +.test-success a { + color: white; +} + +.test-failure a { + color: white; +} + +.test-in-progress a { + color: white; } p { diff --git a/core-impl/src/main/kotlin/net/lachlanmckee/bitrise/core/domain/mapper/BuildsMapperImpl.kt b/core-impl/src/main/kotlin/net/lachlanmckee/bitrise/core/domain/mapper/BuildsMapperImpl.kt index 75d2b6c..a3c07ff 100644 --- a/core-impl/src/main/kotlin/net/lachlanmckee/bitrise/core/domain/mapper/BuildsMapperImpl.kt +++ b/core-impl/src/main/kotlin/net/lachlanmckee/bitrise/core/domain/mapper/BuildsMapperImpl.kt @@ -30,7 +30,12 @@ internal class BuildsMapperImpl @Inject constructor() : BuildsMapper { } }, buildNumber = it.buildNumber, - buildSlug = it.slug + buildSlug = it.slug, + triggeredAt = it.triggeredAt, + finishedAt = it.finishedAt, + originalEnvironmentValueList = it.originalEnvironmentValueList.map { env -> + BuildsData.EnvironmentValue(env.mappedTo, env.value) + } ) } ) diff --git a/core-impl/src/test/kotlin/net/lachlanmckee/bitrise/domain/mapper/BuildsMapperTest.kt b/core-impl/src/test/kotlin/net/lachlanmckee/bitrise/domain/mapper/BuildsMapperTest.kt index 1003fcc..2d2d544 100644 --- a/core-impl/src/test/kotlin/net/lachlanmckee/bitrise/domain/mapper/BuildsMapperTest.kt +++ b/core-impl/src/test/kotlin/net/lachlanmckee/bitrise/domain/mapper/BuildsMapperTest.kt @@ -28,7 +28,12 @@ class BuildsMapperTest { commitHash = "commit-hash-dev-1", commitMessage = "commit-message-dev-1", buildNumber = 1, - slug = "slug-dev-1" + slug = "slug-dev-1", + triggeredAt = "2020-09-01T16:00:00Z", + finishedAt = "2020-09-01T17:00:00Z", + originalEnvironmentValueList = listOf( + BuildsResponse.EnvironmentValue("ENV1", "VALUE1") + ) ), BuildsResponse.BuildData( branch = "dev", @@ -36,7 +41,10 @@ class BuildsMapperTest { commitHash = "commit-hash-dev-2", commitMessage = "commit-message-dev-2\nSecond Line", buildNumber = 2, - slug = "slug-dev-2" + slug = "slug-dev-2", + triggeredAt = "2020-09-01T16:00:00Z", + finishedAt = "2020-09-01T17:00:00Z", + originalEnvironmentValueList = emptyList() ), BuildsResponse.BuildData( branch = "dev", @@ -44,7 +52,10 @@ class BuildsMapperTest { commitHash = "commit-hash-dev-3", commitMessage = null, buildNumber = 3, - slug = "slug-dev-3" + slug = "slug-dev-3", + triggeredAt = "2020-09-01T16:00:00Z", + finishedAt = "2020-09-01T17:00:00Z", + originalEnvironmentValueList = emptyList() ), BuildsResponse.BuildData( branch = "feature1", @@ -52,7 +63,10 @@ class BuildsMapperTest { commitHash = "commit-hash-feature-1", commitMessage = "commit-message-feature-1-that-will-exceed-50-characters", buildNumber = 3, - slug = "slug-feature-1" + slug = "slug-feature-1", + triggeredAt = "2020-09-01T16:00:00Z", + finishedAt = "2020-09-01T17:00:00Z", + originalEnvironmentValueList = emptyList() ) ), BuildsData( @@ -64,21 +78,32 @@ class BuildsMapperTest { commitHash = "commit-hash-dev-3", commitMessage = null, buildNumber = 3, - buildSlug = "slug-dev-3" + buildSlug = "slug-dev-3", + triggeredAt = "2020-09-01T16:00:00Z", + finishedAt = "2020-09-01T17:00:00Z", + originalEnvironmentValueList = emptyList() ), BuildsData.Build( status = "status-text-dev-2", commitHash = "commit-hash-dev-2", commitMessage = "commit-message-dev-2", buildNumber = 2, - buildSlug = "slug-dev-2" + buildSlug = "slug-dev-2", + triggeredAt = "2020-09-01T16:00:00Z", + finishedAt = "2020-09-01T17:00:00Z", + originalEnvironmentValueList = emptyList() ), BuildsData.Build( status = "status-text-dev-1", commitHash = "commit-hash-dev-1", commitMessage = "commit-message-dev-1", buildNumber = 1, - buildSlug = "slug-dev-1" + buildSlug = "slug-dev-1", + triggeredAt = "2020-09-01T16:00:00Z", + finishedAt = "2020-09-01T17:00:00Z", + originalEnvironmentValueList = listOf( + BuildsData.EnvironmentValue("ENV1", "VALUE1") + ) ) ), "feature1" to listOf( @@ -87,7 +112,10 @@ class BuildsMapperTest { commitHash = "commit-hash-feature-1", commitMessage = "commit-message-feature-1-that-will-exceed-50-chara...", buildNumber = 3, - buildSlug = "slug-feature-1" + buildSlug = "slug-feature-1", + triggeredAt = "2020-09-01T16:00:00Z", + finishedAt = "2020-09-01T17:00:00Z", + originalEnvironmentValueList = emptyList() ) ) ) diff --git a/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/entity/BuildsData.kt b/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/entity/BuildsData.kt index df3a474..5106858 100644 --- a/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/entity/BuildsData.kt +++ b/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/entity/BuildsData.kt @@ -9,6 +9,14 @@ data class BuildsData( val commitHash: String, val commitMessage: String?, val buildNumber: Int, - val buildSlug: String + val buildSlug: String, + val triggeredAt: String, + val finishedAt: String, + val originalEnvironmentValueList: List + ) + + data class EnvironmentValue( + val name: String, + val value: String ) } diff --git a/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/entity/BuildsResponse.kt b/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/entity/BuildsResponse.kt index 3d91c63..45277d8 100644 --- a/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/entity/BuildsResponse.kt +++ b/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/entity/BuildsResponse.kt @@ -1,6 +1,7 @@ package net.lachlanmckee.bitrise.core.data.entity import com.google.gson.FieldNamingPolicy +import com.google.gson.annotations.SerializedName import gsonpath.GsonResultList import gsonpath.annotation.AutoGsonAdapter @@ -16,7 +17,11 @@ data class BuildsResponse( val commitHash: String, val commitMessage: String?, val buildNumber: Int, - val slug: String + val slug: String, + val triggeredAt: String, + val finishedAt: String, + @SerializedName("original_build_params.environments") + val originalEnvironmentValueList: List ) @AutoGsonAdapter(fieldNamingPolicy = [FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES]) @@ -25,4 +30,10 @@ data class BuildsResponse( val pageItemLimit: Int, val next: String ) + + @AutoGsonAdapter(fieldNamingPolicy = [FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES]) + data class EnvironmentValue( + val mappedTo: String, + val value: String + ) } diff --git a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/entity/TestResultDetailModel.kt b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/entity/TestResultDetailModel.kt new file mode 100644 index 0000000..23a4daa --- /dev/null +++ b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/entity/TestResultDetailModel.kt @@ -0,0 +1,7 @@ +package net.lachlanmckee.bitrise.results.domain.entity + +internal data class TestResultDetailModel( + val cost: String, + val testSuites: TestSuites, + val matrixIds: String +) diff --git a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/entity/TestResultModel.kt b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/entity/TestResultModel.kt index 5b4f518..47a0055 100644 --- a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/entity/TestResultModel.kt +++ b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/entity/TestResultModel.kt @@ -1,7 +1,11 @@ package net.lachlanmckee.bitrise.results.domain.entity internal data class TestResultModel( - val cost: String, - val testSuites: TestSuites, - val matrixIds: String + val branch: String, + val status: String, + val commitHash: String, + val triggeredAt: String, + val finishedAt: String, + val buildSlug: String, + val jobName: String? ) diff --git a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/entity/TestSuites.kt b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/entity/TestSuites.kt index b003223..2e8cdb2 100644 --- a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/entity/TestSuites.kt +++ b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/entity/TestSuites.kt @@ -21,6 +21,6 @@ internal data class TestCase( val name: String, val classname: String, val time: String, - val webLink: String, + val webLink: String?, val failure: String? ) diff --git a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/interactor/TestResultInteractor.kt b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/interactor/TestResultInteractor.kt index f4ec728..9e583c1 100644 --- a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/interactor/TestResultInteractor.kt +++ b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/interactor/TestResultInteractor.kt @@ -2,21 +2,26 @@ package net.lachlanmckee.bitrise.results.domain.interactor import net.lachlanmckee.bitrise.core.data.datasource.remote.BitriseDataSource import net.lachlanmckee.bitrise.core.data.entity.BitriseArtifactsListResponse -import net.lachlanmckee.bitrise.results.domain.entity.TestResultModel +import net.lachlanmckee.bitrise.results.domain.entity.TestResultDetailModel import net.lachlanmckee.bitrise.results.domain.mapper.TestSuitesMapper +import java.lang.IllegalStateException import javax.inject.Inject internal class TestResultInteractor @Inject constructor( private val bitriseDataSource: BitriseDataSource, private val testSuitesMapper: TestSuitesMapper ) { - suspend fun execute(buildSlug: String): Result { + suspend fun execute(buildSlug: String): Result { return bitriseDataSource .getArtifactDetails(buildSlug) .mapCatching { artifactDetails -> println(artifactDetails) - TestResultModel( + if (artifactDetails.data.isEmpty()) { + throw IllegalStateException("No artifacts found. Perhaps the tests did not run?") + } + + TestResultDetailModel( cost = getArtifactText(artifactDetails, buildSlug, "CostReport.txt"), testSuites = testSuitesMapper.mapTestSuites( getArtifactText( @@ -37,7 +42,8 @@ internal class TestResultInteractor @Inject constructor( ): String { val artifactDetail = artifactDetails .data - .first { it.title == fileName } + .firstOrNull { it.title == fileName } + ?: throw IllegalStateException("Unable to find artifact with file name: $fileName") val artifact = bitriseDataSource .getArtifact(buildSlug, artifactDetail.slug) diff --git a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/interactor/TestResultsListInteractor.kt b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/interactor/TestResultsListInteractor.kt index 857de31..91265aa 100644 --- a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/interactor/TestResultsListInteractor.kt +++ b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/interactor/TestResultsListInteractor.kt @@ -2,18 +2,21 @@ package net.lachlanmckee.bitrise.results.domain.interactor import net.lachlanmckee.bitrise.core.data.datasource.local.ConfigDataSource import net.lachlanmckee.bitrise.core.data.datasource.remote.BitriseDataSource -import net.lachlanmckee.bitrise.core.data.entity.BuildsData import net.lachlanmckee.bitrise.core.domain.mapper.BuildsMapper +import net.lachlanmckee.bitrise.results.domain.entity.TestResultModel +import net.lachlanmckee.bitrise.results.domain.mapper.TestResultsListMapper import javax.inject.Inject internal class TestResultsListInteractor @Inject constructor( private val bitriseDataSource: BitriseDataSource, private val configDataSource: ConfigDataSource, - private val buildsMapper: BuildsMapper + private val buildsMapper: BuildsMapper, + private val testResultsListMapper: TestResultsListMapper ) { - suspend fun execute(): Result { + suspend fun execute(): Result> { return bitriseDataSource .getBuilds(configDataSource.getConfig().bitrise.testTriggerWorkflow) .mapCatching(buildsMapper::mapBuilds) + .mapCatching(testResultsListMapper::mapToTestResultsList) } } diff --git a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/TestResultsListMapper.kt b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/TestResultsListMapper.kt new file mode 100644 index 0000000..c571b4e --- /dev/null +++ b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/TestResultsListMapper.kt @@ -0,0 +1,25 @@ +package net.lachlanmckee.bitrise.results.domain.mapper + +import net.lachlanmckee.bitrise.core.data.entity.BuildsData +import net.lachlanmckee.bitrise.results.domain.entity.TestResultModel +import javax.inject.Inject + +internal class TestResultsListMapper @Inject constructor() { + fun mapToTestResultsList(buildsData: BuildsData): List { + return buildsData.branchBuilds.entries.flatMap { (branch, builds) -> + builds.map { build -> + TestResultModel( + branch = branch, + status = build.status, + commitHash = build.commitHash, + triggeredAt = build.triggeredAt, + finishedAt = build.finishedAt, + buildSlug = build.buildSlug, + jobName = build.originalEnvironmentValueList + .find { envValue -> envValue.name == "JOB_NAME" } + ?.value + ) + } + } + } +} diff --git a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/presentation/TestResultScreen.kt b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/presentation/TestResultScreen.kt index daada92..cdc6f12 100644 --- a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/presentation/TestResultScreen.kt +++ b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/presentation/TestResultScreen.kt @@ -4,7 +4,7 @@ import io.ktor.application.ApplicationCall import io.ktor.html.respondHtml import kotlinx.html.* import net.lachlanmckee.bitrise.core.presentation.ErrorScreenFactory -import net.lachlanmckee.bitrise.results.domain.entity.TestResultModel +import net.lachlanmckee.bitrise.results.domain.entity.TestResultDetailModel import net.lachlanmckee.bitrise.results.domain.interactor.TestResultInteractor internal class TestResultScreen( @@ -20,7 +20,7 @@ internal class TestResultScreen( private suspend fun render( call: ApplicationCall, - resultModel: TestResultModel + resultDetailModel: TestResultDetailModel ) { call.respondHtml { head { @@ -35,11 +35,11 @@ internal class TestResultScreen( span { classes = setOf("content") b { - text(resultModel.cost) + text(resultDetailModel.cost) } } } - resultModel.testSuites.testsuite.forEach { testSuite -> + resultDetailModel.testSuites.testsuite.forEach { testSuite -> div { p { classes = setOf("heading") @@ -62,16 +62,24 @@ internal class TestResultScreen( } } span { - classes = if (testCase.failure != null) { - setOf("content", "test-failure") - } else { - setOf("content", "test-success") + classes = when { + testCase.failure != null -> { + setOf("content", "test-failure") + } + testCase.webLink == null -> { + setOf("content", "test-in-progress") + } + else -> { + setOf("content", "test-success") + } } text("${testCase.classname}#${testCase.name}") br() - a(href = testCase.webLink) { - target = "_blank" - text("Open in Firebase") + if (testCase.webLink != null) { + a(href = testCase.webLink) { + target = "_blank" + text("Open in Firebase") + } } } } diff --git a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/presentation/TestResultsListScreen.kt b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/presentation/TestResultsListScreen.kt index 6ca75ef..6b59c3c 100644 --- a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/presentation/TestResultsListScreen.kt +++ b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/presentation/TestResultsListScreen.kt @@ -3,8 +3,8 @@ package net.lachlanmckee.bitrise.results.presentation import io.ktor.application.ApplicationCall import io.ktor.html.respondHtml import kotlinx.html.* -import net.lachlanmckee.bitrise.core.data.entity.BuildsData import net.lachlanmckee.bitrise.core.presentation.ErrorScreenFactory +import net.lachlanmckee.bitrise.results.domain.entity.TestResultModel import net.lachlanmckee.bitrise.results.domain.interactor.TestResultsListInteractor internal class TestResultsListScreen( @@ -20,7 +20,7 @@ internal class TestResultsListScreen( private suspend fun render( call: ApplicationCall, - buildsData: BuildsData + testResultModelList: List ) { call.respondHtml { head { @@ -29,24 +29,36 @@ internal class TestResultsListScreen( body { h1 { +"Bitrise Test Results" } div { - p { - classes = setOf("heading") - } - buildsData.branchBuilds.entries.forEach { (branch, builds) -> - builds.forEach { build -> - p { - classes = if (build.status == "error") { - setOf("content", "test-failure") - } else { - setOf("content", "test-success") - } - text("Branch: $branch") - br() - text("Result: $build") + testResultModelList.forEach { build -> + span { + classes = setOf("heading") + } + span { + classes = when (build.status) { + "success" -> setOf("content", "test-success") + "in-progress" -> setOf("content", "test-in-progress") + else -> setOf("content", "test-failure") + } + b { + text("${build.branch} [${build.commitHash}]") + } + br() + br() + + if (!build.jobName.isNullOrBlank()) { + text(build.jobName) br() + } + + text("${build.triggeredAt} - ${build.finishedAt}") + + br() + br() + + if (build.status != "in-progress") { a(href = "/test-results/${build.buildSlug}") { target = "_blank" - text("Details") + text("Test Results") } } }