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 android.tools.traces.io 18 19 import android.tools.Scenario 20 import android.tools.io.Artifact 21 import android.tools.io.BUFFER_SIZE 22 import android.tools.io.FLICKER_IO_TAG 23 import android.tools.io.ResultArtifactDescriptor 24 import android.tools.io.RunStatus 25 import android.tools.traces.deleteIfExists 26 import android.tools.withTracing 27 import android.util.Log 28 import java.io.BufferedInputStream 29 import java.io.BufferedOutputStream 30 import java.io.ByteArrayOutputStream 31 import java.io.File 32 import java.io.FileInputStream 33 import java.io.FileNotFoundException 34 import java.io.IOException 35 import java.util.zip.ZipEntry 36 import java.util.zip.ZipInputStream 37 38 class FileArtifact 39 internal constructor(private val scenario: Scenario, artifactFile: File, private val counter: Int) : 40 Artifact { 41 var file: File = artifactFile 42 private set 43 44 init { <lambda>null45 require(!scenario.isEmpty) { "Scenario shouldn't be empty" } 46 } 47 48 override val runStatus: RunStatus 49 get() = 50 RunStatus.fromFileName(file.name) 51 ?: error("Failed to get RunStatus from file name ${file.name}") 52 53 override val absolutePath: String 54 get() = file.absolutePath 55 56 override val fileName: String 57 get() = file.name 58 59 override val stableId: String = "$scenario$counter" 60 updateStatusnull61 override fun updateStatus(newStatus: RunStatus) { 62 val currFile = file 63 val newFile = getNewFilePath(newStatus) 64 if (currFile != newFile) { 65 withTracing("${this::class.simpleName}#updateStatus") { 66 IoUtils.moveFile(currFile, newFile) 67 file = newFile 68 } 69 } 70 } 71 readBytesnull72 override fun readBytes(): ByteArray = file.readBytes() 73 74 override fun deleteIfExists() { 75 file.deleteIfExists() 76 } 77 hasTracenull78 override fun hasTrace(descriptor: ResultArtifactDescriptor): Boolean { 79 var found = false 80 forEachFileInZip { found = found || (it.name == descriptor.fileNameInArtifact) } 81 return found 82 } 83 traceCountnull84 override fun traceCount(): Int { 85 var count = 0 86 forEachFileInZip { count++ } 87 return count 88 } 89 toStringnull90 override fun toString(): String = fileName 91 92 override fun equals(other: Any?): Boolean { 93 if (this === other) return true 94 if (other !is Artifact) return false 95 96 if (absolutePath != other.absolutePath) return false 97 98 return true 99 } 100 hashCodenull101 override fun hashCode(): Int { 102 return absolutePath.hashCode() 103 } 104 105 /** updates the artifact status to [newStatus] */ getNewFilePathnull106 private fun getNewFilePath(newStatus: RunStatus): File { 107 return file.resolveSibling(newStatus.generateArchiveNameFor(scenario, counter)) 108 } 109 110 @Throws(IOException::class) readBytesnull111 override fun readBytes(descriptor: ResultArtifactDescriptor): ByteArray? { 112 Log.d(FLICKER_IO_TAG, "Reading descriptor=$descriptor from $this") 113 114 var foundFile = false 115 val outByteArray = ByteArrayOutputStream() 116 val tmpBuffer = ByteArray(BUFFER_SIZE) 117 withZipFile { 118 var zipEntry: ZipEntry? = it.nextEntry 119 while (zipEntry != null) { 120 if (zipEntry.name == descriptor.fileNameInArtifact) { 121 val outputStream = BufferedOutputStream(outByteArray, BUFFER_SIZE) 122 try { 123 var size = it.read(tmpBuffer, 0, BUFFER_SIZE) 124 while (size > 0) { 125 outputStream.write(tmpBuffer, 0, size) 126 size = it.read(tmpBuffer, 0, BUFFER_SIZE) 127 } 128 it.closeEntry() 129 } finally { 130 outputStream.flush() 131 outputStream.close() 132 } 133 foundFile = true 134 break 135 } 136 zipEntry = it.nextEntry 137 } 138 } 139 140 return if (foundFile) outByteArray.toByteArray() else null 141 } 142 withZipFilenull143 private fun withZipFile(predicate: (ZipInputStream) -> Unit) { 144 if (!file.exists()) { 145 val directory = file.parentFile 146 val files = 147 try { 148 directory?.listFiles()?.filterNot { it.isDirectory }?.map { it.absolutePath } 149 } catch (e: Throwable) { 150 null 151 } 152 throw FileNotFoundException( 153 buildString { 154 append(file) 155 appendLine(" could not be found!") 156 append("Found ") 157 append(files?.joinToString()?.ifEmpty { "no files" }) 158 append(" in ") 159 append(directory?.absolutePath) 160 } 161 ) 162 } 163 164 val zipInputStream = ZipInputStream(BufferedInputStream(FileInputStream(file), BUFFER_SIZE)) 165 try { 166 predicate(zipInputStream) 167 } finally { 168 zipInputStream.closeEntry() 169 zipInputStream.close() 170 } 171 } 172 forEachFileInZipnull173 private fun forEachFileInZip(predicate: (ZipEntry) -> Unit) { 174 withZipFile { 175 var zipEntry: ZipEntry? = it.nextEntry 176 while (zipEntry != null) { 177 predicate(zipEntry) 178 zipEntry = it.nextEntry 179 } 180 } 181 } 182 } 183