Skip to content

Commit

Permalink
Improved handling for test result flank failures (#46)
Browse files Browse the repository at this point in the history
  • Loading branch information
LachlanMcKee authored Oct 18, 2020
1 parent baff38d commit f7cccfb
Show file tree
Hide file tree
Showing 24 changed files with 500 additions and 181 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ internal class BitriseDataSourceImpl @Inject constructor(
return bitriseService.getBuildDetails(buildSlug)
}

override suspend fun getBuildLog(buildSlug: String): Result<String> {
return bitriseService.getBuildLog(buildSlug)
.mapCatching { bitriseService.getArtifactText(it.expiringRawLogUrl).getOrThrow() }
}

override suspend fun getArtifactDetails(buildSlug: String): Result<BitriseArtifactsListResponse> {
return bitriseService.getArtifactDetails(buildSlug)
}
Expand Down Expand Up @@ -54,7 +59,7 @@ internal class BitriseDataSourceImpl @Inject constructor(

override suspend fun getTestResults(buildSlug: String): Result<List<TestSuite>> {
return getArtifactDetails(buildSlug)
.map { artifactDetails ->
.mapCatching { artifactDetails ->
getArtifactText(artifactDetails, buildSlug, "JUnitReport.xml")
.getOrThrow()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ internal class BitriseServiceImpl(
.data
}

override suspend fun getBuildLog(buildSlug: String): Result<BuildLogResponse> =
kotlin.runCatching {
client
.get<BuildLogResponse>("${createAppUrl()}/builds/$buildSlug/log") {
auth()
}
}

override suspend fun getArtifactDetails(buildSlug: String): Result<BitriseArtifactsListResponse> =
kotlin.runCatching {
client
Expand Down
5 changes: 4 additions & 1 deletion core-test/src/main/resources/input/api/junit-report.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='UTF-8' ?>
<testsuites>
<testsuite name="Pixel2-28-en-portrait" tests="1" failures="0" flakes="0" errors="0" skipped="0" time="5.000"
<testsuite name="Pixel2-28-en-portrait" tests="2" failures="0" flakes="0" errors="0" skipped="1" time="5.000"
timestamp="2020-10-05T12:00:00" hostname="localhost">
<testcase name="testA"
classname="com.example.TestClass1"
Expand All @@ -9,6 +9,9 @@
https://console.firebase.google.com/project/fake-project-id/testlab/histories/history-id/matrices/matrices-id/executions/execution-1/testcases/1
</webLink>
</testcase>
<testcase name="testSkipped"
classname="com.example.TestClass1"
time="5.000"/>
</testsuite>
<testsuite name="Pixel2-28-en-portrait" tests="1" failures="1" flakes="0" errors="0" skipped="0" time="6.000"
timestamp="2020-10-05T12:00:00" hostname="localhost">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package net.lachlanmckee.bitrise.core

import kotlinx.coroutines.Deferred

suspend fun <T> Deferred<Result<T>>.awaitGetOrThrow(): T {
return await().getOrThrow()
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ interface BitriseDataSource {

suspend fun getBuildDetails(buildSlug: String): Result<BuildDataResponse>

suspend fun getBuildLog(buildSlug: String): Result<String>

suspend fun getArtifactDetails(buildSlug: String): Result<BitriseArtifactsListResponse>

suspend fun getArtifact(buildSlug: String, artifactSlug: String): Result<BitriseArtifactResponse>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ interface BitriseService {

suspend fun getBuildDetails(buildSlug: String): Result<BuildDataResponse>

suspend fun getBuildLog(buildSlug: String): Result<BuildLogResponse>

suspend fun getArtifactDetails(buildSlug: String): Result<BitriseArtifactsListResponse>

suspend fun getArtifact(buildSlug: String, artifactSlug: String): Result<BitriseArtifactResponse>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
@@ -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<TestSuiteModel>
)
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<TestModel>
)
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<TestSuiteModel>
) : 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<TestModel>
)

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
}
}
Original file line number Diff line number Diff line change
@@ -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<TestResultDetailModel> {
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<TestResultDetailModel> = 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<Result<String>> {
return GlobalScope.async {
bitriseDataSource
.getArtifactDetails(buildSlug)
.mapCatching { artifactDetails ->
bitriseDataSource
.getArtifactText(artifactDetails, buildSlug, "CostReport.txt")
.getOrThrow()
.let(testSuiteModelMapper::mapToTestSuiteModelList)
)
}
}
}
}

private fun getTestSuiteModelListAsync(
buildSlug: String
): Deferred<Result<List<TestSuiteModel>>> {
return GlobalScope.async {
bitriseDataSource.getTestResults(buildSlug)
.mapCatching(testSuiteModelMapper::mapToTestSuiteModelList)
}
}

private fun getFirebaseUrlAsync(
buildSlug: String
): Deferred<Result<String>> {
return GlobalScope.async {
bitriseDataSource.getBuildLog(buildSlug)
.mapCatching(firebaseUrlMapper::mapBuildLogToFirebaseUrl)
}
}
}
Original file line number Diff line number Diff line change
@@ -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/"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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(" ")
}
Loading

0 comments on commit f7cccfb

Please sign in to comment.