1 /*
<lambda>null2 * Copyright 2020 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 androidx.build.testConfiguration
18
19 import com.google.gson.GsonBuilder
20 import groovy.xml.XmlUtil
21
22 class ConfigBuilder {
23 lateinit var configName: String
24 lateinit var configType: TestConfigType
25 var appApksModel: AppApksModel? = null
26 lateinit var applicationId: String
27 var isMicrobenchmark: Boolean = false
28 var isMacrobenchmark: Boolean = false
29 var isPostsubmit: Boolean = true
30 lateinit var minSdk: String
31 val tags = mutableListOf<String>()
32 lateinit var testApkName: String
33 lateinit var testApkSha256: String
34 lateinit var testRunner: String
35 val additionalApkKeys = mutableListOf<String>()
36 val instrumentationArgsMap = mutableMapOf<String, String>()
37
38 fun configName(configName: String) = apply { this.configName = configName }
39
40 fun configType(configType: TestConfigType) = apply { this.configType = configType }
41
42 fun appApksModel(appApksModel: AppApksModel) = apply { this.appApksModel = appApksModel }
43
44 fun applicationId(applicationId: String) = apply { this.applicationId = applicationId }
45
46 fun isMicrobenchmark(isMicrobenchmark: Boolean) = apply {
47 this.isMicrobenchmark = isMicrobenchmark
48 }
49
50 fun isMacrobenchmark(isMacrobenchmark: Boolean) = apply {
51 this.isMacrobenchmark = isMacrobenchmark
52 }
53
54 fun isPostsubmit(isPostsubmit: Boolean) = apply { this.isPostsubmit = isPostsubmit }
55
56 fun minSdk(minSdk: String) = apply { this.minSdk = minSdk }
57
58 fun tag(tag: String) = apply { this.tags.add(tag) }
59
60 fun additionalApkKeys(keys: List<String>) = apply { additionalApkKeys.addAll(keys) }
61
62 fun testApkName(testApkName: String) = apply { this.testApkName = testApkName }
63
64 fun testApkSha256(testApkSha256: String) = apply { this.testApkSha256 = testApkSha256 }
65
66 fun testRunner(testRunner: String) = apply { this.testRunner = testRunner }
67
68 fun buildJson(): String {
69 val gson = GsonBuilder().setPrettyPrinting().create()
70 val instrumentationArgsList = mutableListOf<InstrumentationArg>()
71 instrumentationArgsMap
72 .filter { it.key !in INST_ARG_BLOCKLIST }
73 .forEach { (key, value) -> instrumentationArgsList.add(InstrumentationArg(key, value)) }
74 instrumentationArgsList.addAll(
75 if (isMicrobenchmark && !isPostsubmit) {
76 listOf(
77 InstrumentationArg("notAnnotation", "androidx.test.filters.FlakyTest"),
78 InstrumentationArg("androidx.benchmark.dryRunMode.enable", "true"),
79 )
80 } else {
81 listOf(InstrumentationArg("notAnnotation", "androidx.test.filters.FlakyTest"))
82 }
83 )
84 if (configType.isAddedToInstrumentationArgs()) {
85 instrumentationArgsList.add(
86 InstrumentationArg("androidx.testConfigType", configType.toString())
87 )
88 }
89 val appApk = singleAppApk()
90 val values =
91 mapOf(
92 "name" to configName,
93 "minSdkVersion" to minSdk,
94 "testSuiteTags" to tags,
95 "testApk" to testApkName,
96 "testApkSha256" to testApkSha256,
97 "appApk" to appApk?.name,
98 "appApkSha256" to appApk?.sha256,
99 "instrumentationArgs" to instrumentationArgsList,
100 "additionalApkKeys" to additionalApkKeys
101 )
102 return gson.toJson(values)
103 }
104
105 fun buildXml(): String {
106 val sb = StringBuilder()
107 sb.append(XML_HEADER_AND_LICENSE)
108 sb.append(CONFIGURATION_OPEN)
109 .append(MIN_API_LEVEL_CONTROLLER_OBJECT.replace("MIN_SDK", minSdk))
110 tags.forEach { tag -> sb.append(TEST_SUITE_TAG_OPTION.replace("TEST_SUITE_TAG", tag)) }
111 sb.append(MODULE_METADATA_TAG_OPTION.replace("APPLICATION_ID", applicationId))
112 .append(WIFI_DISABLE_OPTION)
113 .append(FLAKY_TEST_OPTION)
114 if (!isPostsubmit && (isMicrobenchmark || isMacrobenchmark)) {
115 sb.append(BENCHMARK_PRESUBMIT_INST_ARGS)
116 }
117 val instrumentationArgsList = mutableListOf<InstrumentationArg>()
118 instrumentationArgsMap
119 .filter { it.key !in INST_ARG_BLOCKLIST }
120 .forEach { (key, value) -> instrumentationArgsList.add(InstrumentationArg(key, value)) }
121 if (isMicrobenchmark || isMacrobenchmark) {
122 instrumentationArgsList.add(
123 InstrumentationArg("androidx.benchmark.output.payload.testApkSha256", testApkSha256)
124 )
125 if (isMacrobenchmark) {
126 instrumentationArgsList.addAll(
127 listOf(
128 InstrumentationArg(
129 "androidx.benchmark.output.payload.appApkSha256",
130 checkNotNull(appApksModel?.sha256()) {
131 "app apk sha should be provided for macrobenchmarks."
132 }
133 ),
134 // suppress BaselineProfileRule in CI to save time
135 InstrumentationArg("androidx.benchmark.enabledRules", "Macrobenchmark")
136 )
137 )
138 }
139 }
140 if (configType.isAddedToInstrumentationArgs()) {
141 instrumentationArgsList.add(
142 InstrumentationArg("androidx.testConfigType", configType.toString())
143 )
144 }
145 instrumentationArgsList.forEach { (key, value) ->
146 sb.append(
147 """
148 <option name="instrumentation-arg" key="${XmlUtil.escapeXml(key)}" value="${XmlUtil.escapeXml(value)}" />
149
150 """
151 .trimIndent()
152 )
153 }
154 sb.append(SETUP_INCLUDE).append(TARGET_PREPARER_OPEN.replace("CLEANUP_APKS", "true"))
155 sb.append(APK_INSTALL_OPTION.replace("APK_NAME", testApkName))
156 appApksModel?.apkGroups?.forEach { group ->
157 if (group.isUsingApkSplits()) {
158 val apkList = group.apks.map(ApkFile::name).joinToString(",")
159 sb.append(APK_WITH_SPLITS_INSTALL_OPTION.replace("APK_LIST", apkList))
160 } else {
161 sb.append(APK_INSTALL_OPTION.replace("APK_NAME", group.apks.single().name))
162 }
163 }
164 sb.append(TARGET_PREPARER_CLOSE)
165 // Post install commands after SuiteApkInstaller is declared
166 if (isMicrobenchmark) {
167 sb.append(benchmarkPostInstallCommandOption(applicationId))
168 }
169 if (configType == TestConfigType.PRIVACY_SANDBOX_MAIN) {
170 sb.append(PRIVACY_SANDBOX_ENABLE_PREPARER)
171 }
172 sb.append(TEST_BLOCK_OPEN)
173 .append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
174 .append(PACKAGE_OPTION.replace("APPLICATION_ID", applicationId))
175 .apply {
176 if (isPostsubmit) {
177 // These listeners should be unified eventually (b/331974955)
178 if (isMicrobenchmark) {
179 sb.append(MICROBENCHMARK_POSTSUBMIT_LISTENERS)
180 } else if (isMacrobenchmark) {
181 sb.append(MACROBENCHMARK_POSTSUBMIT_LISTENERS)
182 }
183 }
184 }
185 .append(TEST_BLOCK_CLOSE)
186 sb.append(CONFIGURATION_CLOSE)
187 return sb.toString()
188 }
189
190 private fun singleAppApk(): ApkFile? {
191 val apkGroups = appApksModel?.apkGroups
192 if (apkGroups.isNullOrEmpty()) {
193 return null
194 }
195 return apkGroups.single().apks.single()
196 }
197 }
198
mediaInstrumentationArgsForJsonnull199 private fun mediaInstrumentationArgsForJson(
200 isClientPrevious: Boolean,
201 isServicePrevious: Boolean
202 ): List<InstrumentationArg> {
203 return listOf(
204 if (isClientPrevious) {
205 InstrumentationArg(key = "client_version", value = "previous")
206 } else {
207 InstrumentationArg(key = "client_version", value = "tot")
208 },
209 if (isServicePrevious) {
210 InstrumentationArg(key = "service_version", value = "previous")
211 } else {
212 InstrumentationArg(key = "service_version", value = "tot")
213 }
214 )
215 }
216
buildMediaJsonnull217 fun buildMediaJson(
218 configName: String,
219 forClient: Boolean,
220 clientApkName: String,
221 clientApkSha256: String,
222 isClientPrevious: Boolean,
223 isServicePrevious: Boolean,
224 minSdk: String,
225 serviceApkName: String,
226 serviceApkSha256: String,
227 tags: List<String>,
228 ): String {
229 val gson = GsonBuilder().setPrettyPrinting().create()
230 val instrumentationArgs =
231 listOf(InstrumentationArg("notAnnotation", "androidx.test.filters.FlakyTest")) +
232 mediaInstrumentationArgsForJson(
233 isClientPrevious = isClientPrevious,
234 isServicePrevious = isServicePrevious
235 )
236 val values =
237 mapOf(
238 "name" to configName,
239 "minSdkVersion" to minSdk,
240 "testSuiteTags" to tags,
241 "testApk" to if (forClient) clientApkName else serviceApkName,
242 "testApkSha256" to if (forClient) clientApkSha256 else serviceApkSha256,
243 "appApk" to if (forClient) serviceApkName else clientApkName,
244 "appApkSha256" to if (forClient) serviceApkSha256 else clientApkSha256,
245 "instrumentationArgs" to instrumentationArgs,
246 "additionalApkKeys" to listOf<String>()
247 )
248 return gson.toJson(values)
249 }
250
251 private data class InstrumentationArg(val key: String, val value: String)
252
253 /**
254 * These constants are the building blocks of the xml configs, but they aren't very readable as
255 * separate chunks. Look to the golden examples at the bottom of {@link
256 * androidx.build.testConfiguration.XmlTestConfigVerificationTest} for examples of what the full xml
257 * will look like.
258 */
259 private val XML_HEADER_AND_LICENSE =
260 """
261 <?xml version="1.0" encoding="utf-8"?>
262 <!-- Copyright (C) 2020 The Android Open Source Project
263 Licensed under the Apache License, Version 2.0 (the "License")
264 you may not use this file except in compliance with the License.
265 You may obtain a copy of the License at
266 http://www.apache.org/licenses/LICENSE-2.0
267 Unless required by applicable law or agreed to in writing, software
268 distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
269 WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
270 See the License for the specific language governing permissions
271 and limitations under the License.-->
272
273 """
274 .trimIndent()
275
276 private val CONFIGURATION_OPEN =
277 """
278 <configuration description="Runs tests for the module">
279
280 """
281 .trimIndent()
282
283 private val CONFIGURATION_CLOSE =
284 """
285 </configuration>
286 """
287 .trimIndent()
288
289 private val MIN_API_LEVEL_CONTROLLER_OBJECT =
290 """
291 <object type="module_controller" class="com.android.tradefed.testtype.suite.module.MinApiLevelModuleController">
292 <option name="min-api-level" value="MIN_SDK" />
293 </object>
294
295 """
296 .trimIndent()
297
298 private val TEST_SUITE_TAG_OPTION =
299 """
300 <option name="test-suite-tag" value="TEST_SUITE_TAG" />
301
302 """
303 .trimIndent()
304
305 private val MODULE_METADATA_TAG_OPTION =
306 """
307 <option name="config-descriptor:metadata" key="applicationId" value="APPLICATION_ID" />
308
309 """
310 .trimIndent()
311
312 private val WIFI_DISABLE_OPTION =
313 """
314 <option name="wifi:disable" value="true" />
315
316 """
317 .trimIndent()
318
benchmarkPostInstallCommandOptionnull319 private fun benchmarkPostInstallCommandOption(packageName: String) =
320 """
321 <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
322 <option name="run-command" value="${benchmarkPostInstallCommand(packageName)}" />
323 <option name="run-command-timeout" value="240000" />
324 </target_preparer>
325
326 """
327 .trimIndent()
328
329 private fun benchmarkPostInstallCommand(packageName: String): String {
330 return "cmd package compile -f -m speed $packageName"
331 }
332
333 private val SETUP_INCLUDE =
334 """
335 <include name="google/unbundled/common/setup" />
336
337 """
338 .trimIndent()
339
340 /**
341 * Specify the following options on the APK installer:
342 * - Pass the -t argument when installing APKs. This allows testonly APKs to be installed, which
343 * includes all APKs built against a pre-release SDK. See b/205571374.
344 */
345 private val TARGET_PREPARER_OPEN =
346 """
347 <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
348 <option name="cleanup-apks" value="CLEANUP_APKS" />
349 <option name="install-arg" value="-t" />
350
351 """
352 .trimIndent()
353
354 private val TARGET_PREPARER_CLOSE =
355 """
356 </target_preparer>
357
358 """
359 .trimIndent()
360
361 private val APK_INSTALL_OPTION =
362 """
363 <option name="test-file-name" value="APK_NAME" />
364
365 """
366 .trimIndent()
367
368 private val APK_WITH_SPLITS_INSTALL_OPTION =
369 """
370 <option name="split-apk-file-names" value="APK_LIST" />
371
372 """
373 .trimIndent()
374
375 private val TEST_BLOCK_OPEN =
376 """
377 <test class="com.android.tradefed.testtype.AndroidJUnitTest">
378
379 """
380 .trimIndent()
381
382 private val TEST_BLOCK_CLOSE =
383 """
384 </test>
385
386 """
387 .trimIndent()
388
389 private val RUNNER_OPTION =
390 """
391 <option name="runner" value="TEST_RUNNER"/>
392
393 """
394 .trimIndent()
395
396 private val PACKAGE_OPTION =
397 """
398 <option name="package" value="APPLICATION_ID" />
399
400 """
401 .trimIndent()
402
403 private val BENCHMARK_PRESUBMIT_INST_ARGS =
404 """
405 <option name="instrumentation-arg" key="androidx.benchmark.dryRunMode.enable" value="true" />
406
407 """
408 .trimIndent()
409
410 /** These args may never be passed in CI, even if they are set per module */
411 private val INST_ARG_BLOCKLIST = listOf("androidx.benchmark.profiling.skipWhenDurationRisksAnr")
412
413 private val MICROBENCHMARK_POSTSUBMIT_LISTENERS =
414 """
415 <option name="device-listeners" value="androidx.benchmark.junit4.InstrumentationResultsRunListener" />
416 <option name="device-listeners" value="androidx.benchmark.junit4.SideEffectRunListener" />
417
418 """
419 .trimIndent()
420
421 // NOTE: listeners are duplicated in macro package due to no common module w/ junit dependency
422 // See b/331974955
423 private val MACROBENCHMARK_POSTSUBMIT_LISTENERS =
424 """
425 <option name="device-listeners" value="androidx.benchmark.macro.junit4.InstrumentationResultsRunListener" />
426 <option name="device-listeners" value="androidx.benchmark.macro.junit4.SideEffectRunListener" />
427
428 """
429 .trimIndent()
430
431 private val FLAKY_TEST_OPTION =
432 """
433 <option name="instrumentation-arg" key="notAnnotation" value="androidx.test.filters.FlakyTest" />
434
435 """
436 .trimIndent()
437
438 private val PRIVACY_SANDBOX_ENABLE_PREPARER =
439 """
440 <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
441 <option name="run-command" value="cmd sdk_sandbox set-state --enabled"/>
442 <option name="run-command" value="device_config set_sync_disabled_for_tests persistent" />
443 <option name="teardown-command" value="cmd sdk_sandbox set-state --reset"/>
444 <option name="teardown-command" value="device_config set_sync_disabled_for_tests none" />
445 </target_preparer>
446
447 """
448 .trimIndent()
449