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