diff --git a/core-impl/src/main/kotlin/net/lachlanmckee/bitrise/core/data/datasource/remote/BitriseDataSourceImpl.kt b/core-impl/src/main/kotlin/net/lachlanmckee/bitrise/core/data/datasource/remote/BitriseDataSourceImpl.kt index 56dfe2e..ef0250a 100644 --- a/core-impl/src/main/kotlin/net/lachlanmckee/bitrise/core/data/datasource/remote/BitriseDataSourceImpl.kt +++ b/core-impl/src/main/kotlin/net/lachlanmckee/bitrise/core/data/datasource/remote/BitriseDataSourceImpl.kt @@ -19,6 +19,11 @@ internal class BitriseDataSourceImpl @Inject constructor( return bitriseService.getBuildDetails(buildSlug) } + override suspend fun getBuildLog(buildSlug: String): Result { + return bitriseService.getBuildLog(buildSlug) + .mapCatching { bitriseService.getArtifactText(it.expiringRawLogUrl).getOrThrow() } + } + override suspend fun getArtifactDetails(buildSlug: String): Result { return bitriseService.getArtifactDetails(buildSlug) } @@ -54,7 +59,7 @@ internal class BitriseDataSourceImpl @Inject constructor( override suspend fun getTestResults(buildSlug: String): Result> { return getArtifactDetails(buildSlug) - .map { artifactDetails -> + .mapCatching { artifactDetails -> getArtifactText(artifactDetails, buildSlug, "JUnitReport.xml") .getOrThrow() } diff --git a/core-impl/src/main/kotlin/net/lachlanmckee/bitrise/core/data/datasource/remote/BitriseServiceImpl.kt b/core-impl/src/main/kotlin/net/lachlanmckee/bitrise/core/data/datasource/remote/BitriseServiceImpl.kt index 50f85bf..d543480 100644 --- a/core-impl/src/main/kotlin/net/lachlanmckee/bitrise/core/data/datasource/remote/BitriseServiceImpl.kt +++ b/core-impl/src/main/kotlin/net/lachlanmckee/bitrise/core/data/datasource/remote/BitriseServiceImpl.kt @@ -46,6 +46,14 @@ internal class BitriseServiceImpl( .data } + override suspend fun getBuildLog(buildSlug: String): Result = + kotlin.runCatching { + client + .get("${createAppUrl()}/builds/$buildSlug/log") { + auth() + } + } + override suspend fun getArtifactDetails(buildSlug: String): Result = kotlin.runCatching { client diff --git a/core-test/src/main/resources/input/api/junit-report.xml b/core-test/src/main/resources/input/api/junit-report.xml index c1db10a..a79899b 100644 --- a/core-test/src/main/resources/input/api/junit-report.xml +++ b/core-test/src/main/resources/input/api/junit-report.xml @@ -1,6 +1,6 @@ - + diff --git a/core/src/main/kotlin/net/lachlanmckee/bitrise/core/startApplication.kt b/core/src/main/kotlin/net/lachlanmckee/bitrise/core/ApplicationExt.kt similarity index 100% rename from core/src/main/kotlin/net/lachlanmckee/bitrise/core/startApplication.kt rename to core/src/main/kotlin/net/lachlanmckee/bitrise/core/ApplicationExt.kt diff --git a/core/src/main/kotlin/net/lachlanmckee/bitrise/core/CoroutinesExt.kt b/core/src/main/kotlin/net/lachlanmckee/bitrise/core/CoroutinesExt.kt new file mode 100644 index 0000000..1a1582b --- /dev/null +++ b/core/src/main/kotlin/net/lachlanmckee/bitrise/core/CoroutinesExt.kt @@ -0,0 +1,7 @@ +package net.lachlanmckee.bitrise.core + +import kotlinx.coroutines.Deferred + +suspend fun Deferred>.awaitGetOrThrow(): T { + return await().getOrThrow() +} diff --git a/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/datasource/remote/BitriseDataSource.kt b/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/datasource/remote/BitriseDataSource.kt index e567e16..5b66a8d 100644 --- a/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/datasource/remote/BitriseDataSource.kt +++ b/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/datasource/remote/BitriseDataSource.kt @@ -7,6 +7,8 @@ interface BitriseDataSource { suspend fun getBuildDetails(buildSlug: String): Result + suspend fun getBuildLog(buildSlug: String): Result + suspend fun getArtifactDetails(buildSlug: String): Result suspend fun getArtifact(buildSlug: String, artifactSlug: String): Result diff --git a/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/datasource/remote/BitriseService.kt b/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/datasource/remote/BitriseService.kt index 98a08d2..0549bdb 100644 --- a/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/datasource/remote/BitriseService.kt +++ b/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/datasource/remote/BitriseService.kt @@ -8,6 +8,8 @@ interface BitriseService { suspend fun getBuildDetails(buildSlug: String): Result + suspend fun getBuildLog(buildSlug: String): Result + suspend fun getArtifactDetails(buildSlug: String): Result suspend fun getArtifact(buildSlug: String, artifactSlug: String): Result diff --git a/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/entity/BuildLogResponse.kt b/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/entity/BuildLogResponse.kt new file mode 100644 index 0000000..7ecb430 --- /dev/null +++ b/core/src/main/kotlin/net/lachlanmckee/bitrise/core/data/entity/BuildLogResponse.kt @@ -0,0 +1,9 @@ +package net.lachlanmckee.bitrise.core.data.entity + +import com.google.gson.FieldNamingPolicy +import gsonpath.annotation.AutoGsonAdapter + +@AutoGsonAdapter(fieldNamingPolicy = [FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES]) +data class BuildLogResponse( + val expiringRawLogUrl: 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 index 08e880d..c0b48c3 100644 --- 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 @@ -1,27 +1,44 @@ package net.lachlanmckee.bitrise.results.domain.entity -internal data class TestResultDetailModel( - val buildSlug: String, - val bitriseUrl: String, - val cost: String, - val testSuiteModelList: List -) +internal sealed class TestResultDetailModel { + abstract val buildSlug: String + abstract val bitriseUrl: String + abstract val firebaseUrl: String + abstract val totalFailures: Int -internal data class TestSuiteModel( - val name: String, - val totalTests: Int, - val successfulTestCount: Int, - val time: String, - val resultType: TestResultType, - val testCases: List -) + internal data class WithResults( + override val buildSlug: String, + override val bitriseUrl: String, + override val firebaseUrl: String, + override val totalFailures: Int, + val cost: String?, + val testSuiteModelList: List + ) : TestResultDetailModel() { -internal data class TestModel( - val path: String, - val webLink: String?, - val time: String -) + internal data class TestSuiteModel( + val name: String, + val totalTests: Int, + val time: String, + val resultType: TestResultType, + val testCases: List + ) -internal enum class TestResultType { - FAILURE, SKIPPED, SUCCESS + internal data class TestModel( + val path: String, + val webLink: String?, + val time: String + ) + + internal enum class TestResultType { + FAILURE, SKIPPED, SUCCESS + } + } + + internal data class NoResults( + override val buildSlug: String, + override val bitriseUrl: String, + override val firebaseUrl: String + ) : TestResultDetailModel() { + override val totalFailures: Int = 0 + } } 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 17c9ba3..2ede8d7 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 @@ -1,36 +1,91 @@ package net.lachlanmckee.bitrise.results.domain.interactor +import kotlinx.coroutines.* +import net.lachlanmckee.bitrise.core.awaitGetOrThrow import net.lachlanmckee.bitrise.core.data.datasource.remote.BitriseDataSource import net.lachlanmckee.bitrise.results.domain.entity.TestResultDetailModel +import net.lachlanmckee.bitrise.results.domain.entity.TestResultDetailModel.WithResults.TestResultType +import net.lachlanmckee.bitrise.results.domain.entity.TestResultDetailModel.WithResults.TestSuiteModel +import net.lachlanmckee.bitrise.results.domain.mapper.FirebaseUrlMapper import net.lachlanmckee.bitrise.results.domain.mapper.TestSuiteModelMapper import javax.inject.Inject internal class TestResultInteractor @Inject constructor( private val bitriseDataSource: BitriseDataSource, - private val testSuiteModelMapper: TestSuiteModelMapper + private val testSuiteModelMapper: TestSuiteModelMapper, + private val firebaseUrlMapper: FirebaseUrlMapper ) { - suspend fun execute(buildSlug: String): Result { - return bitriseDataSource - .getArtifactDetails(buildSlug) - .mapCatching { artifactDetails -> - println(artifactDetails) - - if (artifactDetails.data.isEmpty()) { - throw IllegalStateException("No artifacts found. Perhaps the tests did not run?") - } + suspend fun execute(buildSlug: String): Result = kotlin.runCatching { + createTestResultModel(buildSlug) + } - TestResultDetailModel( - buildSlug = buildSlug, - bitriseUrl = "https://app.bitrise.io/build/$buildSlug", - cost = bitriseDataSource - .getArtifactText(artifactDetails, buildSlug, "CostReport.txt") - .getOrThrow(), + private suspend fun createTestResultModel( + buildSlug: String + ): TestResultDetailModel { + val costResponse = getCostAsync(buildSlug) + val testSuiteModelListResponse = getTestSuiteModelListAsync(buildSlug) + val firebaseUrlResponse = getFirebaseUrlAsync(buildSlug) + + val costResult = costResponse.await() + val testSuiteModelListResult = testSuiteModelListResponse.await() + val firebaseUrl = firebaseUrlResponse.awaitGetOrThrow() + val bitriseUrl = "https://app.bitrise.io/build/$buildSlug" + + return if (testSuiteModelListResult.isSuccess) { + val testSuiteModelList = testSuiteModelListResult.getOrThrow() + + TestResultDetailModel.WithResults( + buildSlug = buildSlug, + bitriseUrl = bitriseUrl, + firebaseUrl = firebaseUrl, + totalFailures = testSuiteModelList.sumBy { suiteModel -> + if (suiteModel.resultType == TestResultType.FAILURE) { + suiteModel.totalTests + } else { + 0 + } + }, + cost = costResult.getOrNull(), + testSuiteModelList = testSuiteModelList + ) + } else { + TestResultDetailModel.NoResults( + buildSlug = buildSlug, + bitriseUrl = bitriseUrl, + firebaseUrl = firebaseUrl + ) + } + } - testSuiteModelList = bitriseDataSource - .getTestResults(buildSlug) + private fun getCostAsync( + buildSlug: String + ): Deferred> { + return GlobalScope.async { + bitriseDataSource + .getArtifactDetails(buildSlug) + .mapCatching { artifactDetails -> + bitriseDataSource + .getArtifactText(artifactDetails, buildSlug, "CostReport.txt") .getOrThrow() - .let(testSuiteModelMapper::mapToTestSuiteModelList) - ) - } + } + } + } + + private fun getTestSuiteModelListAsync( + buildSlug: String + ): Deferred>> { + return GlobalScope.async { + bitriseDataSource.getTestResults(buildSlug) + .mapCatching(testSuiteModelMapper::mapToTestSuiteModelList) + } + } + + private fun getFirebaseUrlAsync( + buildSlug: String + ): Deferred> { + return GlobalScope.async { + bitriseDataSource.getBuildLog(buildSlug) + .mapCatching(firebaseUrlMapper::mapBuildLogToFirebaseUrl) + } } } diff --git a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/FirebaseUrlMapper.kt b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/FirebaseUrlMapper.kt new file mode 100644 index 0000000..92eaf77 --- /dev/null +++ b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/FirebaseUrlMapper.kt @@ -0,0 +1,16 @@ +package net.lachlanmckee.bitrise.results.domain.mapper + +import javax.inject.Inject + +internal class FirebaseUrlMapper @Inject constructor() { + fun mapBuildLogToFirebaseUrl(buildLog: String): String { + return buildLog + .lineSequence() + .first { it.contains(FIREBASE_CONSOLE_PREFIX) } + .let { FIREBASE_CONSOLE_PREFIX + it.substringAfter(FIREBASE_CONSOLE_PREFIX) } + } + + private companion object { + private const val FIREBASE_CONSOLE_PREFIX = "https://console.firebase.google.com/project/" + } +} diff --git a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/TestSuiteModelMapper.kt b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/TestSuiteModelMapper.kt index d55ea4c..ce73813 100644 --- a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/TestSuiteModelMapper.kt +++ b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/TestSuiteModelMapper.kt @@ -2,9 +2,7 @@ package net.lachlanmckee.bitrise.results.domain.mapper import net.lachlanmckee.bitrise.core.data.entity.TestCase import net.lachlanmckee.bitrise.core.data.entity.TestSuite -import net.lachlanmckee.bitrise.results.domain.entity.TestModel -import net.lachlanmckee.bitrise.results.domain.entity.TestResultType -import net.lachlanmckee.bitrise.results.domain.entity.TestSuiteModel +import net.lachlanmckee.bitrise.results.domain.entity.TestResultDetailModel.WithResults.* import javax.inject.Inject internal class TestSuiteModelMapper @Inject constructor() { @@ -30,9 +28,6 @@ internal class TestSuiteModelMapper @Inject constructor() { TestSuiteModel( name = testGroup.key.name, totalTests = testGroup.value.count(), - successfulTestCount = testGroup.value.count { - it.failure == null && it.webLink != null - }, time = String.format("%.2f", testGroup.value.sumByDouble { it.time.toDouble() }), resultType = testGroup.key.resultType, testCases = testGroup.value diff --git a/results/src/main/kotlin/net/lachlanmckee/bitrise/results/presentation/HtmlElements.kt b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/presentation/HtmlElements.kt new file mode 100644 index 0000000..7d4a317 --- /dev/null +++ b/results/src/main/kotlin/net/lachlanmckee/bitrise/results/presentation/HtmlElements.kt @@ -0,0 +1,14 @@ +package net.lachlanmckee.bitrise.results.presentation + +import kotlinx.html.BODY +import kotlinx.html.a +import kotlinx.html.classes + +fun BODY.button(label: String, url: String) { + a(href = url) { + classes = setOf("mdl-button mdl-button--colored", "mdl-js-button", "mdl-js-ripple-effect", "gray-button") + target = "_blank" + text(label) + } + text(" ") +} 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 62c3670..dd30540 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 @@ -5,7 +5,9 @@ import io.ktor.html.* import kotlinx.html.* import net.lachlanmckee.bitrise.core.presentation.ErrorScreenFactory import net.lachlanmckee.bitrise.results.domain.entity.TestResultDetailModel -import net.lachlanmckee.bitrise.results.domain.entity.TestResultType +import net.lachlanmckee.bitrise.results.domain.entity.TestResultDetailModel.WithResults.TestModel +import net.lachlanmckee.bitrise.results.domain.entity.TestResultDetailModel.WithResults.TestResultType +import net.lachlanmckee.bitrise.results.domain.entity.TestResultDetailModel.WithResults.TestSuiteModel import net.lachlanmckee.bitrise.results.domain.interactor.TestResultInteractor internal class TestResultScreen( @@ -15,13 +17,60 @@ internal class TestResultScreen( suspend fun respondHtml(call: ApplicationCall, buildSlug: String) { testResultInteractor .execute(buildSlug) - .onSuccess { render(call, it) } + .onSuccess { testResultModel -> + when (testResultModel) { + is TestResultDetailModel.WithResults -> { + renderWithResults(call, testResultModel) + } + is TestResultDetailModel.NoResults -> { + renderNoResults(call, testResultModel) + } + } + } .onFailure { errorScreenFactory.respondHtml(call, "Failed to parse content", it.message!!) } } - private suspend fun render( + private suspend fun renderWithResults( call: ApplicationCall, - resultDetailModel: TestResultDetailModel + resultDetailModel: TestResultDetailModel.WithResults + ) { + renderTemplate(call, resultDetailModel) { + div { + span { + classes = setOf("content") + b { + if (resultDetailModel.cost != null) { + text(resultDetailModel.cost) + } else { + text("Failed to fetch cost") + } + } + } + } + resultDetailModel.testSuiteModelList.forEach { testSuite -> testSuiteElement(testSuite) } + } + } + + private suspend fun renderNoResults( + call: ApplicationCall, + resultDetailModel: TestResultDetailModel.NoResults + ) { + renderTemplate(call, resultDetailModel) { + div { + span { + classes = setOf("content") + b { + text("Failed to fetch test data. Perhaps Flank failed?") + } + } + } + } + } + + private suspend fun renderTemplate( + call: ApplicationCall, + resultDetailModel: TestResultDetailModel, + extraContentFunc: BODY.() -> Unit ) { call.respondHtml { head { @@ -35,120 +84,98 @@ internal class TestResultScreen( body { h1 { +"Bitrise Test Result" } - val totalFailures = resultDetailModel.testSuiteModelList - .sumBy { it.totalTests - it.successfulTestCount } - - if (totalFailures > 0) { - div { - span { - classes = setOf("heading") - text("Total Failures") - } - span { - classes = setOf("content") - text("$totalFailures (") - a(href = "/test-rerun?build-slug=${resultDetailModel.buildSlug}") { - target = "_blank" - text("Rerun Failures") - } - text(")") - } - } - } div { - span { - classes = setOf("content") - a(href = resultDetailModel.bitriseUrl) { - target = "_blank" - text("Bitrise") - } + this@body.button("Bitrise", resultDetailModel.bitriseUrl) + this@body.button("Firebase", resultDetailModel.firebaseUrl) + + if (resultDetailModel.totalFailures > 0) { + this@body.button( + "Rerun ${resultDetailModel.totalFailures} failures", + "/test-rerun?build-slug=${resultDetailModel.buildSlug}" + ) } } br() - div { - span { - classes = setOf("content") - b { - text(resultDetailModel.cost) - } + extraContentFunc() + } + } + } + + private fun BODY.testSuiteElement(testSuite: TestSuiteModel) { + div { + p { + classes = setOf("heading") + } + p { + classes = setOf("content") + b { + text("${testSuite.name}. Tests: ${testSuite.totalTests}. Total Duration: ${testSuite.time}") + } + } + } + table { + classes = setOf("mdl-data-table", "mdl-js-data-table", "mdl-data-table", "mdl-shadow--2dp") + thead { + tr { + th { + classes = setOf("mdl-data-table__cell--non-numeric") + text("Result") + } + th { + classes = setOf("mdl-data-table__cell--non-numeric") + text("Duration") + } + th { + classes = setOf("mdl-data-table__cell--non-numeric") + text("Test") } } - resultDetailModel.testSuiteModelList.forEach { testSuite -> - div { - p { - classes = setOf("heading") - } - p { - classes = setOf("content") - b { - text("${testSuite.name}. Tests: ${testSuite.totalTests}. Total Duration: ${testSuite.time}") - } + } + tbody { + testSuite.testCases.forEach { testCase -> this@table.testCaseElement(testSuite, testCase) } + } + } + } + + private fun TABLE.testCaseElement(testSuite: TestSuiteModel, testCase: TestModel) { + tr { + classes = when (testSuite.resultType) { + TestResultType.FAILURE -> { + setOf("test-failure") + } + TestResultType.SKIPPED -> { + setOf("test-in-progress") + } + TestResultType.SUCCESS -> { + setOf("test-success") + } + } + td { + text( + when (testSuite.resultType) { + TestResultType.FAILURE -> { + "Failure" } - } - table { - classes = setOf("mdl-data-table", "mdl-js-data-table", "mdl-data-table", "mdl-shadow--2dp") - thead { - tr { - th { - classes = setOf("mdl-data-table__cell--non-numeric") - text("Result") - } - th { - classes = setOf("mdl-data-table__cell--non-numeric") - text("Duration") - } - th { - classes = setOf("mdl-data-table__cell--non-numeric") - text("Test") - } - } + TestResultType.SKIPPED -> { + "Skipped" } - tbody { - testSuite.testCases.forEach { testCase -> - tr { - classes = when (testSuite.resultType) { - TestResultType.FAILURE -> { - setOf("test-failure") - } - TestResultType.SKIPPED -> { - setOf("test-in-progress") - } - TestResultType.SUCCESS -> { - setOf("test-success") - } - } - td { - text( - when (testSuite.resultType) { - TestResultType.FAILURE -> { - "Failure" - } - TestResultType.SKIPPED -> { - "Skipped" - } - TestResultType.SUCCESS -> { - "Success" - } - } - ) - } - td { - text(testCase.time) - } - td { - text(testCase.path) - br() - if (testCase.webLink != null) { - a(href = testCase.webLink) { - target = "_blank" - text("Open in Firebase") - } - } - } - } - } + TestResultType.SUCCESS -> { + "Success" } } + ) + } + td { + text(testCase.time) + } + td { + text(testCase.path) + br() + 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 651f2df..00f2bbf 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 @@ -74,22 +74,12 @@ internal class TestResultsListScreen( if (build.status != "in-progress") { classes = setOf("mdl-card__actions mdl-card--border") this@testResult.button("Test Results", "/test-results/${build.buildSlug}") - text(" ") } this@testResult.button("Bitrise", build.bitriseUrl) if (build.status == "error") { - text(" ") this@testResult.button("Rerun Failures", "/test-rerun?build-slug=${build.buildSlug}") } } } } - - private fun BODY.button(label: String, url: String) { - a(href = url) { - classes = setOf("mdl-button mdl-button--colored", "mdl-js-button", "mdl-js-ripple-effect", "gray-button") - target = "_blank" - text(label) - } - } } diff --git a/results/src/test/kotlin/net/lachlanmckee/bitrise/results/domain/interactor/TestResultInteractorTest.kt b/results/src/test/kotlin/net/lachlanmckee/bitrise/results/domain/interactor/TestResultInteractorTest.kt new file mode 100644 index 0000000..a68bbf8 --- /dev/null +++ b/results/src/test/kotlin/net/lachlanmckee/bitrise/results/domain/interactor/TestResultInteractorTest.kt @@ -0,0 +1,73 @@ +package net.lachlanmckee.bitrise.results.domain.interactor + +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import net.lachlanmckee.bitrise.core.data.datasource.remote.BitriseDataSource +import net.lachlanmckee.bitrise.core.data.entity.BitriseArtifactsListResponse +import net.lachlanmckee.bitrise.core.data.entity.TestSuite +import net.lachlanmckee.bitrise.results.domain.entity.TestResultDetailModel +import net.lachlanmckee.bitrise.results.domain.entity.TestResultDetailModel.WithResults.TestResultType +import net.lachlanmckee.bitrise.results.domain.entity.TestResultDetailModel.WithResults.TestSuiteModel +import net.lachlanmckee.bitrise.results.domain.mapper.FirebaseUrlMapper +import net.lachlanmckee.bitrise.results.domain.mapper.TestSuiteModelMapper +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +internal class TestResultInteractorTest { + private val bitriseDataSource: BitriseDataSource = mockk() + private val testSuiteModelMapper: TestSuiteModelMapper = mockk() + private val firebaseUrlMapper: FirebaseUrlMapper = mockk() + + @Test + fun givenAllResponsesSucceed_whenExecute_thenExpectTestResultDetailModel() = runBlocking { + val artifactsResponse = BitriseArtifactsListResponse(listOf()) + coEvery { bitriseDataSource.getArtifactDetails("buildSlug") } returns Result.success(artifactsResponse) + coEvery { + bitriseDataSource.getArtifactText( + artifactsResponse, + "buildSlug", + "CostReport.txt" + ) + } returns Result.success("cost") + coEvery { bitriseDataSource.getBuildLog("buildSlug") } returns Result.success("buildLog") + + val testResults = listOf() + val testSuiteModelList = listOf( + TestSuiteModel( + name = "suite", + totalTests = 1, + time = "5.00", + resultType = TestResultType.FAILURE, + testCases = listOf(mockk()) + ) + ) + coEvery { bitriseDataSource.getTestResults("buildSlug") } returns Result.success(testResults) + coEvery { testSuiteModelMapper.mapToTestSuiteModelList(testResults) } returns testSuiteModelList + coEvery { firebaseUrlMapper.mapBuildLogToFirebaseUrl("buildLog") } returns "firebaseUrl" + + val result = TestResultInteractor(bitriseDataSource, testSuiteModelMapper, firebaseUrlMapper) + .execute("buildSlug") + + coVerify { + bitriseDataSource.getArtifactDetails("buildSlug") + bitriseDataSource.getTestResults("buildSlug") + bitriseDataSource.getArtifactText(artifactsResponse, "buildSlug", "CostReport.txt") + bitriseDataSource.getBuildLog("buildSlug") + testSuiteModelMapper.mapToTestSuiteModelList(testResults) + } + + assertEquals( + TestResultDetailModel.WithResults( + buildSlug = "buildSlug", + bitriseUrl = "https://app.bitrise.io/build/buildSlug", + cost = "cost", + firebaseUrl = "firebaseUrl", + totalFailures = 1, + testSuiteModelList = testSuiteModelList + ), + result.getOrThrow() + ) + } +} diff --git a/results/src/test/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/FirebaseUrlMapperTest.kt b/results/src/test/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/FirebaseUrlMapperTest.kt new file mode 100644 index 0000000..f7111b5 --- /dev/null +++ b/results/src/test/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/FirebaseUrlMapperTest.kt @@ -0,0 +1,28 @@ +package net.lachlanmckee.bitrise.results.domain.mapper + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class FirebaseUrlMapperTest { + @Test + fun givenNoFirebaseConsoleUrl_whenMap_thenExpectException() { + Assertions.assertThrows(NoSuchElementException::class.java) { + FirebaseUrlMapper().mapBuildLogToFirebaseUrl("No console URL") + } + } + + @Test + fun givenFirebaseConsoleUrlExists_whenMap_thenExpectUrl() { + val firebaseUrl = FirebaseUrlMapper() + .mapBuildLogToFirebaseUrl( + """ + Log line 1 + https://console.firebase.google.com/project/foobar + Log line 2 + """.trimIndent() + ) + + assertEquals("https://console.firebase.google.com/project/foobar", firebaseUrl) + } +} diff --git a/results/src/test/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/TestSuiteModelMapperTest.kt b/results/src/test/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/TestSuiteModelMapperTest.kt index 1ea93da..ca18e41 100644 --- a/results/src/test/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/TestSuiteModelMapperTest.kt +++ b/results/src/test/kotlin/net/lachlanmckee/bitrise/results/domain/mapper/TestSuiteModelMapperTest.kt @@ -2,9 +2,7 @@ package net.lachlanmckee.bitrise.results.domain.mapper import net.lachlanmckee.bitrise.core.data.entity.TestCase import net.lachlanmckee.bitrise.core.data.entity.TestSuite -import net.lachlanmckee.bitrise.results.domain.entity.TestModel -import net.lachlanmckee.bitrise.results.domain.entity.TestResultType -import net.lachlanmckee.bitrise.results.domain.entity.TestSuiteModel +import net.lachlanmckee.bitrise.results.domain.entity.TestResultDetailModel.WithResults.* import org.junit.jupiter.api.Test import kotlin.test.assertEquals @@ -85,7 +83,6 @@ internal class TestSuiteModelMapperTest { TestSuiteModel( name = "Device1", totalTests = 1, - successfulTestCount = 0, time = "5.00", resultType = TestResultType.FAILURE, testCases = listOf( @@ -99,7 +96,6 @@ internal class TestSuiteModelMapperTest { TestSuiteModel( name = "Device2", totalTests = 1, - successfulTestCount = 0, time = "5.00", resultType = TestResultType.FAILURE, testCases = listOf( @@ -113,7 +109,6 @@ internal class TestSuiteModelMapperTest { TestSuiteModel( name = "Device1", totalTests = 2, - successfulTestCount = 2, time = "10.00", resultType = TestResultType.SUCCESS, testCases = listOf( @@ -132,7 +127,6 @@ internal class TestSuiteModelMapperTest { TestSuiteModel( name = "Device2", totalTests = 2, - successfulTestCount = 2, time = "10.00", resultType = TestResultType.SUCCESS, testCases = listOf( diff --git a/results/src/test/kotlin/net/lachlanmckee/bitrise/results/integration/ResultsApplicationTest.kt b/results/src/test/kotlin/net/lachlanmckee/bitrise/results/integration/ResultsApplicationTest.kt index 7e59289..380e41b 100644 --- a/results/src/test/kotlin/net/lachlanmckee/bitrise/results/integration/ResultsApplicationTest.kt +++ b/results/src/test/kotlin/net/lachlanmckee/bitrise/results/integration/ResultsApplicationTest.kt @@ -1,5 +1,6 @@ package net.lachlanmckee.bitrise.results.integration +import io.ktor.client.engine.mock.* import io.ktor.http.* import io.ktor.server.testing.* import net.lachlanmckee.bitrise.* @@ -27,7 +28,7 @@ internal class ResultsApplicationTest { } @Test - fun testResult() = withTestApplication( + fun testResultWithValidContent() = withTestApplication( createTestHttpClientFactory { request -> when (request.url.fullPath) { "/v0.1/apps/APP_ID/builds/BUILD_SLUG/artifacts" -> { @@ -45,13 +46,42 @@ internal class ResultsApplicationTest { "/builds/BUILD_SLUG/artifacts/ARTIFACT_SLUG_1/JUnitReport.xml" -> { respondJson("input/api/junit-report.xml") } + "/v0.1/apps/APP_ID/builds/BUILD_SLUG/log" -> { + respondJson("input/api/build-log-response.json") + } + "/build-logs-v2/BUILD_LOG_1" -> { + respondJson("input/api/build-log.txt") + } + else -> error("Unhandled ${request.url.fullPath}") + } + } + ) { + with(handleRequest(HttpMethod.Get, "/test-results/BUILD_SLUG")) { + assertEquals(HttpStatusCode.OK, response.status()) + assertContentEquals(response, "output/test-result/success.html") + } + } + + @Test + fun testResultWithFailureToFetchTests() = withTestApplication( + createTestHttpClientFactory { request -> + when (request.url.fullPath) { + "/v0.1/apps/APP_ID/builds/BUILD_SLUG/artifacts" -> { + respondError(HttpStatusCode.InternalServerError) + } + "/v0.1/apps/APP_ID/builds/BUILD_SLUG/log" -> { + respondJson("input/api/build-log-response.json") + } + "/build-logs-v2/BUILD_LOG_1" -> { + respondJson("input/api/build-log.txt") + } else -> error("Unhandled ${request.url.fullPath}") } } ) { with(handleRequest(HttpMethod.Get, "/test-results/BUILD_SLUG")) { assertEquals(HttpStatusCode.OK, response.status()) - assertContentEquals(response, "output/test-result/expected.html") + assertContentEquals(response, "output/test-result/failure-no-tests-found.html") } } diff --git a/results/src/test/resources/input/api/build-log-response.json b/results/src/test/resources/input/api/build-log-response.json new file mode 100644 index 0000000..9face00 --- /dev/null +++ b/results/src/test/resources/input/api/build-log-response.json @@ -0,0 +1,7 @@ +{ + "expiring_raw_log_url": "https://bitrise-build-log-archives-production.s3.amazonaws.com/build-logs-v2/BUILD_LOG_1", + "generated_log_chunks_num": 0, + "is_archived": true, + "log_chunks": [], + "timestamp": null +} diff --git a/results/src/test/resources/input/api/build-log.txt b/results/src/test/resources/input/api/build-log.txt new file mode 100644 index 0000000..ad2fb71 --- /dev/null +++ b/results/src/test/resources/input/api/build-log.txt @@ -0,0 +1,3 @@ +Log line 1 +https://console.firebase.google.com/project/foobar +Log line 2 diff --git a/results/src/test/resources/output/test-result/failure-no-tests-found.html b/results/src/test/resources/output/test-result/failure-no-tests-found.html new file mode 100644 index 0000000..791902b --- /dev/null +++ b/results/src/test/resources/output/test-result/failure-no-tests-found.html @@ -0,0 +1,15 @@ + + + + + + + + + +

Bitrise Test Result

+ +
+
Failed to fetch test data. Perhaps Flank failed?
+ + diff --git a/results/src/test/resources/output/test-result/expected.html b/results/src/test/resources/output/test-result/success.html similarity index 70% rename from results/src/test/resources/output/test-result/expected.html rename to results/src/test/resources/output/test-result/success.html index 7502f18..2d1fa3a 100644 --- a/results/src/test/resources/output/test-result/expected.html +++ b/results/src/test/resources/output/test-result/success.html @@ -8,8 +8,7 @@

Bitrise Test Result

-
Total Failures3 (Rerun Failures)
- +
Virtual devices $0.40 for 20m @@ -48,6 +47,26 @@

Bitrise Test Result

Pixel2-28-en-portrait. Tests: 1. Total Duration: 5.00

+ + + + + + + + + + + + + + + +
ResultDurationTest
Skipped5.000com.example.TestClass1#testSkipped
+
+

+

Pixel2-28-en-portrait. Tests: 1. Total Duration: 5.00

+
diff --git a/results/src/test/resources/output/test-results-list/expected.html b/results/src/test/resources/output/test-results-list/expected.html index de06f9d..7678819 100644 --- a/results/src/test/resources/output/test-results-list/expected.html +++ b/results/src/test/resources/output/test-results-list/expected.html @@ -15,14 +15,14 @@

Bitrise Test Results

BRANCH_1 [HASH_1]

2020-10-05T12:00:00Z - 2020-10-05T12:00:00Z
- +

BRANCH_2 [HASH_2]

custom_job_name
2020-10-05T12:00:00Z - 2020-10-05T12:00:00Z
- +