1 package com.android.ndkports
2 
3 import kotlinx.coroutines.Dispatchers
4 import kotlinx.coroutines.channels.Channel
5 import kotlinx.coroutines.channels.SendChannel
6 import kotlinx.coroutines.channels.toList
7 import kotlinx.coroutines.coroutineScope
8 import kotlinx.coroutines.launch
9 import kotlinx.coroutines.runBlocking
10 import org.gradle.api.DefaultTask
11 import org.gradle.api.file.DirectoryProperty
12 import org.gradle.api.provider.Property
13 import org.gradle.api.tasks.Input
14 import org.gradle.api.tasks.InputDirectory
15 import org.gradle.api.tasks.TaskAction
16 import java.io.File
17 
18 sealed class TestResult(val name: String, val abi: Abi) {
19     class Success(name: String, abi: Abi) : TestResult(name, abi) {
toStringnull20         override fun toString(): String = "PASS $abi $name"
21     }
22 
23     class Failure(name: String, abi: Abi, private val output: String) :
24         TestResult(name, abi) {
25         override fun toString(): String = "FAIL $abi $name: $output"
26     }
27 }
28 
29 private val BASE_DEVICE_DIRECTORY = File("/data/local/tmp/ndkports")
30 
31 data class PushSpec(val src: File, val dest: File)
32 
33 class PushBuilder(val abi: Abi, val toolchain: Toolchain) {
34     val pushSpecs = mutableListOf<PushSpec>()
35 
pushnull36     fun push(src: File, dest: File) = pushSpecs.add(PushSpec(src, dest))
37 }
38 
39 data class ShellTestSpec(val name: String, val cmd: Iterable<String>)
40 
41 class ShellTestBuilder(val deviceDirectory: File, val abi: Abi) {
42     val runSpecs = mutableListOf<ShellTestSpec>()
43 
44     fun shellTest(name: String, cmd: Iterable<String>) =
45         runSpecs.add(ShellTestSpec(name, cmd))
46 }
47 
48 abstract class AndroidExecutableTestTask : DefaultTask() {
49     @get:InputDirectory
50     abstract val ndkPath: DirectoryProperty
51 
52     private val ndk: Ndk
53         get() = Ndk(ndkPath.asFile.get())
54 
55     @get:Input
56     abstract val minSdkVersion: Property<Int>
57 
58     @get:Input
59     abstract val push: Property<PushBuilder.() -> Unit>
60 
pushnull61     fun push(block: PushBuilder.() -> Unit) = push.set(block)
62 
63     @get:Input
64     abstract val run: Property<ShellTestBuilder.() -> Unit>
65 
66     fun run(block: ShellTestBuilder.() -> Unit) = run.set(block)
67 
68     private fun deviceDirectoryForAbi(abi: Abi): File =
69         BASE_DEVICE_DIRECTORY.resolve(project.name).resolve(abi.toString())
70 
71     private suspend fun runTests(
72         device: Device, abi: Abi, resultChannel: SendChannel<TestResult>
73     ) = coroutineScope {
74         val deviceDirectory = deviceDirectoryForAbi(abi)
75 
76         val pushBlock = push.get()
77         val runBlock = run.get()
78 
79         val pushBuilder =
80             PushBuilder(abi, Toolchain(ndk, abi, minSdkVersion.get()))
81         pushBuilder.pushBlock()
82         coroutineScope {
83             pushBuilder.pushSpecs.forEach {
84                 launch(Dispatchers.IO) {
85                     device.push(
86                         it.src, deviceDirectory.resolve(it.dest)
87                     )
88                 }
89             }
90         }
91 
92         val runBuilder = ShellTestBuilder(deviceDirectory, abi)
93         runBuilder.runBlock()
94         runBuilder.runSpecs.forEach {
95             launch(Dispatchers.IO) {
96                 val result = try {
97                     device.shell(it.cmd)
98                     TestResult.Success(it.name, abi)
99                 } catch (ex: AdbException) {
100                     TestResult.Failure(it.name, abi, "${ex.cmd}\n${ex.output}")
101                 }
102 
103                 resultChannel.send(result)
104             }
105         }
106     }
107 
108     @Suppress("UnstableApiUsage")
109     @TaskAction
<lambda>null110     fun runTask() = runBlocking {
111         val fleet = DeviceFleet()
112         val warningChannel = Channel<String>(Channel.UNLIMITED)
113         val resultChannel = Channel<TestResult>(Channel.UNLIMITED)
114         coroutineScope {
115             for (abi in Abi.values()) {
116                 launch {
117                     val device = fleet.findDeviceFor(
118                         abi, abi.adjustMinSdkVersion(minSdkVersion.get())
119                     )
120                     if (device == null) {
121                         warningChannel.send(
122                             "No device capable of running tests for $abi " +
123                                     "minSdkVersion 21"
124                         )
125                         return@launch
126                     }
127                     device.shell(
128                         listOf(
129                             "rm", "-rf", deviceDirectoryForAbi(abi).toString()
130                         )
131                     )
132                     runTests(device, abi, resultChannel)
133                 }
134             }
135         }
136         warningChannel.close()
137         resultChannel.close()
138 
139         for (warning in warningChannel) {
140             logger.warn(warning)
141         }
142 
143         val failures =
144             resultChannel.toList().filterIsInstance<TestResult.Failure>()
145         if (failures.isNotEmpty()) {
146             throw RuntimeException(
147                 "Tests failed:\n${failures.joinToString("\n")}"
148             )
149         }
150     }
151 }