forked from Flank/flank
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathArgsHelper.kt
More file actions
195 lines (163 loc) · 7.19 KB
/
ArgsHelper.kt
File metadata and controls
195 lines (163 loc) · 7.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
package ftl.args
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.google.api.client.json.GenericJson
import com.google.api.client.json.JsonObjectParser
import com.google.api.client.util.Charsets
import com.google.cloud.ServiceOptions
import com.google.cloud.storage.BucketInfo
import com.google.cloud.storage.Storage
import com.google.cloud.storage.StorageClass
import com.google.cloud.storage.StorageOptions
import com.google.common.math.IntMath
import ftl.args.yml.IYmlMap
import ftl.config.FtlConstants
import ftl.config.FtlConstants.GCS_PREFIX
import ftl.config.FtlConstants.JSON_FACTORY
import ftl.config.FtlConstants.defaultCredentialPath
import ftl.gc.GcStorage
import ftl.util.Utils
import java.io.File
import java.math.RoundingMode
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.regex.Pattern
object ArgsHelper {
val yamlMapper: ObjectMapper by lazy { ObjectMapper(YAMLFactory()).registerModule(KotlinModule()) }
fun mergeYmlMaps(vararg ymlMaps: IYmlMap): Map<String, List<String>> {
val result = mutableMapOf<String, List<String>>()
ymlMaps.map { it.map }
.forEach { map ->
map.forEach { (k, v) ->
result.merge(k, v) { a, b -> a + b }
}
}
return result
}
fun assertFileExists(file: String, name: String) {
if (!File(file).exists()) {
Utils.fatalError("'$file' $name doesn't exist")
}
}
fun evaluateFilePath(filePath: String): String {
var file = filePath.trim().replaceFirst("~", System.getProperty("user.home"))
file = substituteEnvVars(file)
// avoid File(..).canonicalPath since that will resolve symlinks
file = Paths.get(file).toAbsolutePath().normalize().toString()
// Avoid walking the folder's parent dir if we know it exists already.
if (File(file).exists()) return file
val filePaths = walkFileTree(file)
if (filePaths.size > 1) {
Utils.fatalError("'$file' ($filePath) matches multiple files: $filePaths")
} else if (filePaths.isEmpty()) {
Utils.fatalError("'$file' not found ($filePath)")
}
return filePaths[0].toAbsolutePath().toString()
}
fun assertGcsFileExists(uri: String) {
if (!uri.startsWith(GCS_PREFIX)) {
throw IllegalArgumentException("must start with $GCS_PREFIX uri: $uri")
}
val gcsURI = URI.create(uri)
val bucket = gcsURI.authority
val path = gcsURI.path.drop(1) // Drop leading slash
val blob = GcStorage.storage.get(bucket, path)
if (blob == null) {
Utils.fatalError("The file at '$uri' does not exist")
}
}
fun validateTestMethods(
testTargets: List<String>,
validTestMethods: Collection<String>,
from: String,
skipValidation: Boolean = FtlConstants.useMock
) {
val missingMethods = testTargets - validTestMethods
if (!skipValidation && missingMethods.isNotEmpty()) Utils.fatalError("$from is missing methods: $missingMethods.\nValid methods:\n$validTestMethods")
if (validTestMethods.isEmpty()) Utils.fatalError("$from has no tests")
}
fun calculateShards(
testMethodsToShard: Collection<String>,
testMethodsAlwaysRun: Collection<String>,
testShards: Int
): List<List<String>> {
val testShardMethods = testMethodsToShard.distinct().toMutableList()
testShardMethods.removeAll(testMethodsAlwaysRun)
val oneTestPerChunk = testShards == -1
var chunkSize = IntMath.divide(testShardMethods.size, testShards, RoundingMode.UP)
if (oneTestPerChunk || chunkSize < 1) {
chunkSize = 1
}
val testShardChunks = testShardMethods.asSequence()
.chunked(chunkSize)
.map { testMethodsAlwaysRun + it }
.toList()
// Ensure we don't create more VMs than requested. VM count per run should be <= testShards
if (!oneTestPerChunk && testShardChunks.size > testShards) {
Utils.fatalError("Calculated chunks $testShardChunks is > requested $testShards testShards.")
}
if (testShardChunks.isEmpty()) Utils.fatalError("Failed to populate test shard chunks")
return testShardChunks
}
fun getGcsBucket(projectId: String, resultsBucket: String): String {
// com.google.cloud.storage.contrib.nio.testing.FakeStorageRpc doesn't support list
// when testing, use a hard coded results bucket instead.
if (FtlConstants.useMock) return resultsBucket
// test lab supports using a special free storage bucket
// because we don't have access to the root account, it won't show up in the storage list.
if (resultsBucket.startsWith("test-lab-")) return resultsBucket
val storage = StorageOptions.newBuilder().setProjectId(projectId).build().service
val bucketLabel = mapOf(Pair("flank", ""))
val storageLocation = "us-central1"
val bucketListOption = Storage.BucketListOption.prefix(resultsBucket)
val storageList = storage.list(bucketListOption).values?.map { it.name } ?: emptyList()
val bucket = storageList.find { it == resultsBucket }
if (bucket != null) return bucket
return storage.create(
BucketInfo.newBuilder(resultsBucket)
.setStorageClass(StorageClass.REGIONAL)
.setLocation(storageLocation)
.setLabels(bucketLabel)
.build()
).name
}
private fun serviceAccountProjectId(): String? {
try {
if (!defaultCredentialPath.toFile().exists()) return null
return JsonObjectParser(JSON_FACTORY).parseAndClose(
Files.newInputStream(defaultCredentialPath),
Charsets.UTF_8,
GenericJson::class.java
)["project_id"] as String
} catch (e: Exception) {
println("Parsing $defaultCredentialPath failed:")
println(e.printStackTrace())
}
return null
}
fun getDefaultProjectId(): String? {
if (FtlConstants.useMock) return "mockProjectId"
// Allow users control over projectId by checking using Google's logic first before falling back to JSON.
return ServiceOptions.getDefaultProjectId() ?: serviceAccountProjectId()
}
// https://stackoverflow.com/a/2821201/2450315
private val envRegex = Pattern.compile("\\$([a-zA-Z_]+[a-zA-Z0-9_]*)")
private fun substituteEnvVars(text: String): String {
val buffer = StringBuffer()
val matcher = envRegex.matcher(text)
while (matcher.find()) {
val envName = matcher.group(1)
val envValue = System.getenv(envName) ?: ""
matcher.appendReplacement(buffer, envValue)
}
matcher.appendTail(buffer)
return buffer.toString()
}
private fun walkFileTree(filePath: String): List<Path> {
val searchDir = Paths.get(filePath).parent
return ArgsFileVisitor("glob:$filePath").walk(searchDir)
}
}