1 /*
<lambda>null2  * Copyright 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 package bench.flame.diff.command
17 
18 import bench.flame.diff.config.Paths
19 import bench.flame.diff.interop.isValidFileName
20 import bench.flame.diff.ui.promptOverwriteFile
21 import bench.flame.diff.ui.promptProvideFile
22 import com.github.ajalt.clikt.core.CliktCommand
23 import com.github.ajalt.clikt.core.terminal
24 import com.github.ajalt.clikt.parameters.groups.mutuallyExclusiveOptions
25 import com.github.ajalt.clikt.parameters.groups.single
26 import com.github.ajalt.clikt.parameters.options.check
27 import com.github.ajalt.clikt.parameters.options.default
28 import com.github.ajalt.clikt.parameters.options.option
29 import com.github.ajalt.clikt.parameters.types.file
30 import com.github.ajalt.mordant.terminal.ConversionResult
31 import java.io.File
32 
33 class Save : CliktCommand(help = "Save a trace file for future comparison.") {
34     private val src by mutuallyExclusiveOptions(
35         option("--src-file", help = "Path to a trace file.")
36             .file(mustExist = true, canBeDir = false),
37         option("--src-dir", help = "Path to a directory containing trace files.")
38             .file(mustExist = true, canBeFile = false),
39         name = "Trace source",
40     ).single()
41 
42     private val dst by option(help = "Name for the saved trace file.")
43         .check { it.isValidFileName() }
44 
45     private val pattern by option("--pattern", help = "Trace file name regex.")
46         .default(Paths.traceFileNamePattern)
47 
48     override fun run() {
49         // determine the source trace file location
50         val src = src // allows for smart casts
51         val srcTraceFile: File = when {
52             src != null && src.isFile -> src
53             else -> promptProvideFile(
54                 "Provide trace source", pattern,
55                 excludePattern = null, src, Paths.outDir.toFile()
56             )
57         }
58         check(srcTraceFile.exists() && srcTraceFile.isFile)
59 
60         // copy the source trace file to the destination
61         val dstTraceFile: File =
62             Paths.savedTracesDir.resolve(dst ?: promptDestinationName(srcTraceFile.name)).toFile()
63         if (dstTraceFile.exists()) promptOverwriteFile(dstTraceFile).let { overwrite ->
64             if (!overwrite) return
65         }
66         dstTraceFile.parentFile.mkdirs() // ensure destination dir is present
67         srcTraceFile.copyTo(dstTraceFile, overwrite = true)
68 
69         // Determine the test result file corresponding to the trace (if present). JSON file will
70         // have the trace name in profileResults section. Trace files are guaranteed unique.
71         val srcTestResultFile = srcTraceFile.parentFile.walkTopDown().filter {
72             it.name.lowercase().endsWith("json") && it.readText().contains(srcTraceFile.name)
73         }.singleOrNull()
74 
75         // copy the test result file to the destination
76         srcTestResultFile?.let {
77             // adding the extension (.json) helps us avoid a conflict with the trace file name
78             val dstTestResultFileName = dstTraceFile.nameWithoutExtension + ".json"
79             val dstTestResultFile = dstTraceFile.parentFile.resolve(dstTestResultFileName)
80             // If we are changing the name of the trace file, we also need to reflect that in JSON.
81             val updatedJsonFile = it.readText().replace(srcTraceFile.name, dstTraceFile.name)
82             dstTestResultFile.writeText(updatedJsonFile)
83         }
84     }
85 
86     private fun promptDestinationName(default: String? = null): String =
87         terminal.prompt("Provide destination file name", default) {
88             when {
89                 it.isValidFileName() -> ConversionResult.Valid(it)
90                 else -> ConversionResult.Invalid("Invalid value")
91             }
92         }!!
93 }
94