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 deleteIfExistsnull72 override fun deleteIfExists() { 73 file.deleteIfExists() 74 } 75 hasTracenull76 override fun hasTrace(descriptor: ResultArtifactDescriptor): Boolean { 77 var found = false 78 forEachFileInZip { found = found || (it.name == descriptor.fileNameInArtifact) } 79 return found 80 } 81 traceCountnull82 override fun traceCount(): Int { 83 var count = 0 84 forEachFileInZip { count++ } 85 return count 86 } 87 toStringnull88 override fun toString(): String = fileName 89 90 override fun equals(other: Any?): Boolean { 91 if (this === other) return true 92 if (other !is Artifact) return false 93 94 if (absolutePath != other.absolutePath) return false 95 96 return true 97 } 98 hashCodenull99 override fun hashCode(): Int { 100 return absolutePath.hashCode() 101 } 102 103 /** updates the artifact status to [newStatus] */ getNewFilePathnull104 private fun getNewFilePath(newStatus: RunStatus): File { 105 return file.resolveSibling(newStatus.generateArchiveNameFor(scenario, counter)) 106 } 107 108 @Throws(IOException::class) readBytesnull109 override fun readBytes(descriptor: ResultArtifactDescriptor): ByteArray? { 110 Log.d(FLICKER_IO_TAG, "Reading descriptor=$descriptor from $this") 111 112 var foundFile = false 113 val outByteArray = ByteArrayOutputStream() 114 val tmpBuffer = ByteArray(BUFFER_SIZE) 115 withZipFile { 116 var zipEntry: ZipEntry? = it.nextEntry 117 while (zipEntry != null) { 118 if (zipEntry.name == descriptor.fileNameInArtifact) { 119 val outputStream = BufferedOutputStream(outByteArray, BUFFER_SIZE) 120 try { 121 var size = it.read(tmpBuffer, 0, BUFFER_SIZE) 122 while (size > 0) { 123 outputStream.write(tmpBuffer, 0, size) 124 size = it.read(tmpBuffer, 0, BUFFER_SIZE) 125 } 126 it.closeEntry() 127 } finally { 128 outputStream.flush() 129 outputStream.close() 130 } 131 foundFile = true 132 break 133 } 134 zipEntry = it.nextEntry 135 } 136 } 137 138 return if (foundFile) outByteArray.toByteArray() else null 139 } 140 withZipFilenull141 private fun withZipFile(predicate: (ZipInputStream) -> Unit) { 142 if (!file.exists()) { 143 val directory = file.parentFile 144 val files = 145 try { 146 directory?.listFiles()?.filterNot { it.isDirectory }?.map { it.absolutePath } 147 } catch (e: Throwable) { 148 null 149 } 150 throw FileNotFoundException( 151 buildString { 152 append(file) 153 appendLine(" could not be found!") 154 append("Found ") 155 append(files?.joinToString()?.ifEmpty { "no files" }) 156 append(" in ") 157 append(directory?.absolutePath) 158 } 159 ) 160 } 161 162 val zipInputStream = ZipInputStream(BufferedInputStream(FileInputStream(file), BUFFER_SIZE)) 163 try { 164 predicate(zipInputStream) 165 } finally { 166 zipInputStream.closeEntry() 167 zipInputStream.close() 168 } 169 } 170 forEachFileInZipnull171 private fun forEachFileInZip(predicate: (ZipEntry) -> Unit) { 172 withZipFile { 173 var zipEntry: ZipEntry? = it.nextEntry 174 while (zipEntry != null) { 175 predicate(zipEntry) 176 zipEntry = it.nextEntry 177 } 178 } 179 } 180 } 181