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