All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.avito.reportviewer.internal.ReportsAddApiImpl.kt Maven / Gradle / Ivy

Go to download

Collection of infrastructure libraries and gradle plugins of Avito Android project

There is a newer version: 2023.22
Show newest version
package com.avito.reportviewer.internal

import com.avito.android.Result
import com.avito.android.test.annotations.TestCaseBehavior
import com.avito.android.test.annotations.TestCasePriority
import com.avito.jsonrpc.JsonRpcClient
import com.avito.jsonrpc.RfcRpcRequest
import com.avito.jsonrpc.RpcResult
import com.avito.report.model.AndroidTest
import com.avito.report.model.FileAddress
import com.avito.report.model.Flakiness
import com.avito.report.model.TestStatus
import com.avito.reportviewer.ReportsAddApi
import com.avito.reportviewer.internal.model.Incident
import com.avito.reportviewer.internal.model.IncidentElement
import com.avito.reportviewer.internal.model.ReportViewerStatus
import com.avito.reportviewer.internal.model.Step
import com.avito.reportviewer.internal.model.Video
import com.avito.reportviewer.model.ReportCoordinates
import com.avito.reportviewer.model.team

internal class ReportsAddApiImpl(private val client: JsonRpcClient) : ReportsAddApi {

    /**
     * status
     * buildId null означает локальную сборку, значения в create недостаточно, потому что новые билды могут писать
     *         в тот же отчет и нужно сохранить знание о новых  buildId
     *         todo продумать способ не гонять лишние байты с каждым тестом
     *
     * @return todo id вместо string
     */
    override fun addTests(
        reportCoordinates: ReportCoordinates,
        buildId: String?,
        tests: Collection
    ): Result> {
        return Result.tryCatch {
            val requests = tests.map { test ->
                when (test) {
                    is AndroidTest.Skipped -> createAddFullRequest(
                        reportCoordinates = reportCoordinates,
                        buildId = buildId,
                        test = test,
                        status = TestStatus.Skipped(test.skipReason),
                        logcat = "",
                        incident = null,
                        video = null,
                        startTime = null,
                        endTime = null,
                        dataSetData = null,
                        preconditionList = emptyList(), // todo в теории можем достать проанализировав код теста,
                        stepList = emptyList() // todo в теории можем достать проанализировав код теста
                    )
                    is AndroidTest.Lost -> createAddFullRequest(
                        reportCoordinates = reportCoordinates,
                        buildId = buildId,
                        test = test,
                        // if incident available let backend decide status based on incident type
                        status = if (test.incident == null) TestStatus.Lost else null,
                        logcat = test.logcat,
                        incident = test.incident?.toInternal(),
                        video = null,
                        startTime = test.startTime,
                        endTime = test.lastSignalTime,
                        dataSetData = null,
                        preconditionList = emptyList(), // todo в теории можем достать проанализировав код теста,
                        stepList = emptyList() // todo в теории можем достать проанализировав код теста
                    )
                    is AndroidTest.Completed -> createAddFullRequest(
                        reportCoordinates = reportCoordinates,
                        buildId = buildId,
                        test = test,
                        // определяется на бэке для success/fail по наличию incident,
                        // отправляем остальные статусы самостоятельно
                        status = null,
                        logcat = test.logcat,
                        incident = test.incident?.toInternal(),
                        video = test.video?.let { Video(it.fileAddress, it.format) },
                        startTime = test.startTime,
                        endTime = test.endTime,
                        dataSetData = test.dataSetData,
                        preconditionList = test.preconditions.map { it.toInternal() },
                        stepList = test.steps.map { it.toInternal() }
                    )
                }
            }

            when {
                requests.size == 1 -> listOf(client.jsonRpcRequest>(requests[0]).result)
                requests.size > 1 -> client.batchRequest>>(requests).map { it.result }
                else -> emptyList()
            }
        }
    }

    override fun addTest(reportCoordinates: ReportCoordinates, buildId: String?, test: AndroidTest): Result {
        return addTests(reportCoordinates, buildId, listOf(test)).map { it.first() }
    }

    private fun createAddFullRequest(
        reportCoordinates: ReportCoordinates,
        buildId: String?,
        test: AndroidTest,
        status: TestStatus?,
        logcat: String,
        incident: Incident?,
        video: Video?,
        startTime: Long?,
        endTime: Long?,
        dataSetData: Map?,
        preconditionList: List,
        stepList: List
    ): RfcRpcRequest {
        val preparedData = mutableMapOf()
        val messageList = mutableListOf()
        val groupList = mutableListOf(test.name.team.name)
        val reportData = mutableMapOf()

        val kind = test.kind
        groupList.add(kind.tmsId)

        val report = mutableMapOf(
            "test_class" to test.name.className,
            "test_name" to if (test.dataSetNumber != null) {
                "${test.name.methodName}#${test.dataSetNumber}"
            } else {
                test.name.methodName
            },
            "message_list" to messageList,
            // собираем из всех нужных данных список лейблов(тэгов), по которым можно будет фильтровать тесты в RV
            "group_list" to groupList,
            "precondition_step_list" to preconditionList,
            "test_case_step_list" to stepList
        )

        if (test.testCaseId != null) report["test_case_id"] = test.testCaseId.toString()

        val description = test.description
        if (!description.isNullOrBlank()) report["description"] = description

        if (video != null) {
            when (val fileAddress = video.link) {
                is FileAddress.URL -> report["video"] = video
                is FileAddress.Error -> messageList.add("Не удалось загрузить видео: ${fileAddress.error.message}")
                is FileAddress.File -> messageList.add("Не удалось загрузить видео")
            }
        }
        if (startTime != null) report["start_time"] = startTime
        if (endTime != null) report["end_time"] = endTime

        /**
         * Обычно тесты с датасетами группируются в ReportViewer по testCaseId,
         * но также нужно группировать тесты с датасетами без testCaseId
         * Для них введено специальное поле grouping_key, в андроиде это всегда название класса
         */
        if (test.dataSetNumber != null) {
            if (test.testCaseId == null) {
                report["grouping_key"] = test.name.className
            }
            // Для консистентности можно также посылать здесь testCaseId, но бэкенд умеет обрабатывать это
        }

        require(!(test.dataSetNumber == null && !dataSetData.isNullOrEmpty())) {
            "DataSet data without DataSetNumber doesn't make sense!"
        }

        if (test.dataSetNumber != null) {
            report["data_set_number"] = test.dataSetNumber.toString()
            if (!dataSetData.isNullOrEmpty()) {
                report["data_set"] = dataSetData
            }
        }

        if (!buildId.isNullOrBlank()) preparedData["tc_build"] = buildId

        val externalId = test.externalId
        if (!externalId.isNullOrBlank()) {
            preparedData["external_id"] = if (test.dataSetNumber != null) {
                "${externalId}_${test.dataSetNumber}"
            } else {
                externalId
            }
        }
        if (test.tagIds.isNotEmpty()) preparedData["tag_id"] = test.tagIds
        if (test.featureIds.isNotEmpty()) preparedData["feature_id"] = test.featureIds

        val priority = test.priority
        preparedData["priority_id"] = priority?.tmsValue ?: TestCasePriority.NORMAL.tmsValue

        val behavior = test.behavior
        preparedData["behavior_id"] = behavior?.tmsValue ?: TestCaseBehavior.UNDEFINED.tmsValue

        if (status is TestStatus.Skipped) {
            // посылаем в 2 места skipReason, message_list нужен для отображения,
            // а prepared_data, чтобы получить с помощью RunTest.List
            preparedData["skip_reason"] = status.reason
            messageList.add(status.reason)
        }

        if (!buildId.isNullOrBlank()) {
            reportData["build_id_set"] = mapOf("\$fillSet" to listOf(buildId))
        }

        when (val flakiness = test.flakiness) {
            is Flakiness.Flaky -> {
                preparedData["is_flaky"] = true
                preparedData["flaky_reason"] = flakiness.reason
            }
            is Flakiness.Stable ->
                preparedData["is_flaky"] = false
        }

        val params = mutableMapOf(
            "plan_slug" to reportCoordinates.planSlug,
            "job_slug" to reportCoordinates.jobSlug,
            "run_id" to reportCoordinates.runId,
            "environment" to test.device.name,
            "report" to report,
            // тут происходит магия "с помощью оператора добавляется в массив новое значение билда"
            "report_data" to reportData,
            // todo onlyIf present
            "console" to mapOf(
                "stdout" to logcat,
                "stderr" to ""
            ),
            "prepared_data" to preparedData,
            "kind" to kind.tmsId
        )

        if (incident != null) params["incident"] = incident
        if (status != null) params["status"] = serializeStatus(status).intValue

        return RfcRpcRequest(
            method = "RunTest.AddFull",
            params = params
        )
    }

    private fun serializeStatus(status: TestStatus): ReportViewerStatus {
        return when (status) {
            TestStatus.Success,
            is TestStatus.Failure -> throw IllegalArgumentException(
                "Should not send Success/Failure test status explicitly, " +
                    "it will be calculated on report backend"
            )
            is TestStatus.Skipped -> ReportViewerStatus.SKIP
            TestStatus.Manual -> ReportViewerStatus.MANUAL
            TestStatus.Lost -> ReportViewerStatus.LOST
        }
    }

    private fun com.avito.report.model.Incident.toInternal(): Incident {
        return Incident(
            type = type.toInternal(),
            timestamp = timestamp,
            trace = trace,
            chain = chain.map { it.toInternal() },
            entryList = entryList
        )
    }

    private fun com.avito.report.model.Incident.Type.toInternal(): Incident.Type {
        return when (this) {
            com.avito.report.model.Incident.Type.INFRASTRUCTURE_ERROR -> Incident.Type.INFRASTRUCTURE_ERROR
            com.avito.report.model.Incident.Type.ASSERTION_FAILED -> Incident.Type.ASSERTION_FAILED
        }
    }

    private fun com.avito.report.model.IncidentElement.toInternal(): IncidentElement {
        return IncidentElement(
            message = message,
            code = code,
            type = type,
            origin = origin,
            className = className,
        )
    }

    private fun com.avito.report.model.Step.toInternal(): Step {
        return Step(
            timestamp = timestamp,
            number = number,
            title = title,
            entryList = entryList
        )
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy