1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package platform.test.screenshot.report 18 19 import android.graphics.Bitmap 20 import android.os.Bundle 21 import androidx.test.platform.app.InstrumentationRegistry 22 import java.io.File 23 import java.io.FileOutputStream 24 import java.io.IOException 25 import platform.test.screenshot.GoldenPathManager 26 import platform.test.screenshot.proto.ScreenshotResultProto 27 28 /** 29 * Writes bitmap diff results to the local test device, for Trade Federation to pick them up and 30 * upload to Scuba. 31 * 32 * TODO(b/322324387) Cleanup code - this is copied with only minor modifications (`testIdentifier` 33 * is now an argument rather than a member) from http://shortn/_7AMZiumx0f for reviewability. 34 */ 35 class ExportToScubaStrategy( 36 private val goldenPathManager: GoldenPathManager, 37 ) : DiffResultExportStrategy { 38 private val imageExtension = ".png" 39 private val resultBinaryProtoFileSuffix = "goldResult.pb" 40 41 // This is used in CI to identify the files. 42 private val resultProtoFileSuffix = "goldResult.textproto" 43 44 // Magic number for an in-progress status report 45 private val bundleStatusInProgress = 2 46 private val bundleKeyPrefix = "platform_screenshots_" 47 reportResultnull48 override fun reportResult( 49 testIdentifier: String, 50 goldenIdentifier: String, 51 actual: Bitmap, 52 status: ScreenshotResultProto.DiffResult.Status, 53 comparisonStatistics: ScreenshotResultProto.DiffResult.ComparisonStatistics?, 54 expected: Bitmap?, 55 diff: Bitmap? 56 ) { 57 val resultProto = 58 ScreenshotResultProto.DiffResult.newBuilder() 59 .setResultType(status) 60 .addMetadata( 61 ScreenshotResultProto.Metadata.newBuilder() 62 .setKey("repoRootPath") 63 .setValue(goldenPathManager.deviceLocalPath) 64 ) 65 66 if (comparisonStatistics != null) { 67 resultProto.comparisonStatistics = comparisonStatistics 68 } 69 70 val pathRelativeToAssets = goldenPathManager.goldenImageIdentifierResolver(goldenIdentifier) 71 resultProto.imageLocationGolden = 72 "${goldenPathManager.assetsPathRelativeToBuildRoot}/$pathRelativeToAssets" 73 74 val report = Bundle() 75 76 actual.writeToDevice(OutputFileType.IMAGE_ACTUAL, goldenIdentifier, testIdentifier).also { 77 resultProto.imageLocationTest = it.name 78 report.putString(bundleKeyPrefix + OutputFileType.IMAGE_ACTUAL, it.absolutePath) 79 } 80 diff?.run { 81 writeToDevice(OutputFileType.IMAGE_DIFF, goldenIdentifier, testIdentifier).also { 82 resultProto.imageLocationDiff = it.name 83 report.putString(bundleKeyPrefix + OutputFileType.IMAGE_DIFF, it.absolutePath) 84 } 85 } 86 expected?.run { 87 writeToDevice(OutputFileType.IMAGE_EXPECTED, goldenIdentifier, testIdentifier).also { 88 resultProto.imageLocationReference = it.name 89 report.putString(bundleKeyPrefix + OutputFileType.IMAGE_EXPECTED, it.absolutePath) 90 } 91 } 92 93 writeToDevice(OutputFileType.RESULT_PROTO, goldenIdentifier, testIdentifier) { 94 it.write(resultProto.build().toString().toByteArray()) 95 } 96 .also { 97 report.putString(bundleKeyPrefix + OutputFileType.RESULT_PROTO, it.absolutePath) 98 } 99 100 writeToDevice(OutputFileType.RESULT_BIN_PROTO, goldenIdentifier, testIdentifier) { 101 it.write(resultProto.build().toByteArray()) 102 } 103 .also { 104 report.putString(bundleKeyPrefix + OutputFileType.RESULT_BIN_PROTO, it.absolutePath) 105 } 106 107 InstrumentationRegistry.getInstrumentation().sendStatus(bundleStatusInProgress, report) 108 } 109 getPathOnDeviceFornull110 internal fun getPathOnDeviceFor( 111 fileType: OutputFileType, 112 goldenIdentifier: String, 113 testIdentifier: String, 114 ): File { 115 val imageSuffix = getOnDeviceImageSuffix(goldenIdentifier) 116 val protoSuffix = getOnDeviceArtifactsSuffix(goldenIdentifier, resultProtoFileSuffix) 117 val binProtoSuffix = 118 getOnDeviceArtifactsSuffix(goldenIdentifier, resultBinaryProtoFileSuffix) 119 val succinctTestIdentifier = getSuccinctTestIdentifier(testIdentifier) 120 val fileName = 121 when (fileType) { 122 OutputFileType.IMAGE_ACTUAL -> "${succinctTestIdentifier}_actual_$imageSuffix" 123 OutputFileType.IMAGE_EXPECTED -> "${succinctTestIdentifier}_expected_$imageSuffix" 124 OutputFileType.IMAGE_DIFF -> "${succinctTestIdentifier}_diff_$imageSuffix" 125 OutputFileType.RESULT_PROTO -> "${succinctTestIdentifier}_$protoSuffix" 126 OutputFileType.RESULT_BIN_PROTO -> "${succinctTestIdentifier}_$binProtoSuffix" 127 } 128 return File(goldenPathManager.deviceLocalPath, fileName) 129 } 130 getOnDeviceImageSuffixnull131 private fun getOnDeviceImageSuffix(goldenIdentifier: String): String { 132 val resolvedGoldenIdentifier = 133 goldenPathManager 134 .goldenImageIdentifierResolver(goldenIdentifier) 135 .replace('/', '_') 136 .replace(imageExtension, "") 137 return "$resolvedGoldenIdentifier$imageExtension" 138 } 139 getOnDeviceArtifactsSuffixnull140 private fun getOnDeviceArtifactsSuffix(goldenIdentifier: String, suffix: String): String { 141 val resolvedGoldenIdentifier = 142 goldenPathManager 143 .goldenImageIdentifierResolver(goldenIdentifier) 144 .replace('/', '_') 145 .replace(imageExtension, "") 146 return "${resolvedGoldenIdentifier}_$suffix" 147 } 148 getSuccinctTestIdentifiernull149 private fun getSuccinctTestIdentifier(identifier: String): String { 150 val pattern = Regex("\\[([A-Za-z0-9_]+)\\]") 151 return pattern.replace(identifier, "") 152 } 153 Bitmapnull154 private fun Bitmap.writeToDevice( 155 fileType: OutputFileType, 156 goldenIdentifier: String, 157 testIdentifier: String, 158 ): File { 159 return writeToDevice(fileType, goldenIdentifier, testIdentifier) { 160 compress(Bitmap.CompressFormat.PNG, 0 /*ignored for png*/, it) 161 } 162 } 163 writeToDevicenull164 private fun writeToDevice( 165 fileType: OutputFileType, 166 goldenIdentifier: String, 167 testIdentifier: String, 168 writeAction: (FileOutputStream) -> Unit 169 ): File { 170 val fileGolden = File(goldenPathManager.deviceLocalPath) 171 if (!fileGolden.exists() && !fileGolden.mkdirs()) { 172 throw IOException("Could not create folder $fileGolden.") 173 } 174 175 val file = getPathOnDeviceFor(fileType, goldenIdentifier, testIdentifier) 176 if (!file.exists()) { 177 // file typically exists when in one test, the same golden image was repeatedly 178 // compared with. In this scenario, multiple actual/expected/diff images with same 179 // names will be attempted to write to the device. 180 try { 181 FileOutputStream(file).use { writeAction(it) } 182 } catch (e: Exception) { 183 throw IOException( 184 "Could not write file to storage (path: ${file.absolutePath}). ", 185 e 186 ) 187 } 188 } 189 190 return file 191 } 192 } 193 194 /** Type of file that can be produced by the [ExportToScubaStrategy]. */ 195 internal enum class OutputFileType { 196 IMAGE_ACTUAL, 197 IMAGE_EXPECTED, 198 IMAGE_DIFF, 199 RESULT_PROTO, 200 RESULT_BIN_PROTO 201 } 202