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 }