Skip to content

Commit 497d2a3

Browse files
author
Sergey Chelombitko
committed
Gracefully handle build cancellation
1 parent e6aa69a commit 497d2a3

File tree

11 files changed

+89
-115
lines changed

11 files changed

+89
-115
lines changed

core/src/main/kotlin/com/malinskiy/marathon/cache/test/TestCacheLoader.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ class TestCacheLoader(
3434

3535
private lateinit var cacheCheckCompleted: Deferred<Unit>
3636

37-
fun initialize(scope: CoroutineScope) = with(scope) {
38-
cacheCheckCompleted = async {
37+
fun initialize(scope: CoroutineScope) {
38+
cacheCheckCompleted = scope.async {
3939
// TODO: check concurrently
4040
for (test in testsToCheck) {
4141
var result: CacheResult? = null

core/src/main/kotlin/com/malinskiy/marathon/cache/test/TestCacheSaver.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ class TestCacheSaver(
2121
private val tasks: Channel<SaveTask> = unboundedChannel()
2222
private lateinit var completableDeferred: Deferred<Unit>
2323

24-
fun initialize(scope: CoroutineScope) = with(scope) {
25-
completableDeferred = async {
24+
fun initialize(scope: CoroutineScope) {
25+
completableDeferred = scope.async {
2626
for (task in tasks) {
2727
val cacheKey = testCacheKeyProvider.getCacheKey(task.poolId, task.result.test)
2828
cache.store(cacheKey, task.result)

core/src/main/kotlin/com/malinskiy/marathon/device/Device.kt

-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,4 @@ interface Device {
2525
)
2626

2727
suspend fun prepare(configuration: Configuration)
28-
fun dispose()
2928
}
30-

core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class Scheduler(
5757

5858
private val logger = MarathonLogging.logger("Scheduler")
5959

60-
private val scope: CoroutineScope = CoroutineScope(context)
60+
private val scope = CoroutineScope(context)
6161

6262
suspend fun initialize() {
6363
logger.debug { "Initializing scheduler" }
@@ -124,8 +124,8 @@ class Scheduler(
124124
}
125125
}
126126

127-
private fun subscribeOnDevices(job: Job): Job {
128-
return scope.launch {
127+
private fun subscribeOnDevices(job: Job) {
128+
scope.launch {
129129
logger.debug { "Reading messages from device provider" }
130130

131131
for (msg in deviceProvider.subscribe()) {

core/src/test/kotlin/com/malinskiy/marathon/device/DeviceStub.kt

-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,4 @@ class DeviceStub(
2626
}
2727

2828
override suspend fun prepare(configuration: Configuration) {}
29-
30-
override fun dispose() {}
3129
}

vendor/vendor-android/base/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt

+1-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import com.malinskiy.marathon.device.Device
77
import java.awt.image.BufferedImage
88
import java.util.concurrent.TimeUnit
99

10-
interface AndroidDevice : Device {
10+
interface AndroidDevice : Device, AutoCloseable {
1111
val apiLevel: Int
1212
val version: AndroidVersion
1313

@@ -27,6 +27,4 @@ interface AndroidDevice : Device {
2727
remoteFilePath: String,
2828
options: ScreenRecorderOptions
2929
)
30-
31-
fun waitForAsyncWork()
3230
}

vendor/vendor-android/base/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt

+9-16
Original file line numberDiff line numberDiff line change
@@ -9,44 +9,37 @@ import com.malinskiy.marathon.report.attachment.AttachmentProvider
99
import com.malinskiy.marathon.test.Test
1010
import com.malinskiy.marathon.test.toSimpleSafeTestName
1111
import kotlinx.coroutines.CoroutineScope
12-
import kotlinx.coroutines.DelicateCoroutinesApi
12+
import kotlinx.coroutines.Dispatchers
1313
import kotlinx.coroutines.Job
1414
import kotlinx.coroutines.async
15-
import kotlinx.coroutines.newFixedThreadPoolContext
16-
import kotlin.coroutines.CoroutineContext
1715

1816
class ScreenCapturerTestRunListener(
1917
private val attachmentManager: AttachmentManager,
20-
private val device: AndroidDevice
21-
) : TestRunListener, CoroutineScope, AttachmentProvider {
18+
private val device: AndroidDevice,
19+
private val coroutineScope: CoroutineScope
20+
) : TestRunListener, AttachmentProvider {
2221

2322
private val attachmentListeners = mutableListOf<AttachmentListener>()
24-
25-
override fun registerListener(listener: AttachmentListener) {
26-
attachmentListeners.add(listener)
27-
}
28-
2923
private var screenCapturerJob: Job? = null
3024
private var screenCapturer: ScreenCapturer? = null
3125
private val logger = MarathonLogging.logger(ScreenCapturerTestRunListener::class.java.simpleName)
26+
private val dispatcher = Dispatchers.IO.limitedParallelism(1)
3227

33-
@OptIn(DelicateCoroutinesApi::class)
34-
private val threadPoolDispatcher = newFixedThreadPoolContext(1, "ScreenCapturer - ${device.serialNumber}")
35-
override val coroutineContext: CoroutineContext
36-
get() = threadPoolDispatcher
28+
override fun registerListener(listener: AttachmentListener) {
29+
attachmentListeners.add(listener)
30+
}
3731

3832
override fun testStarted(test: Test) {
3933
logger.debug { "Starting recording for ${test.toSimpleSafeTestName()}" }
4034
screenCapturer = ScreenCapturer(device, attachmentManager, test)
41-
screenCapturerJob = async {
35+
screenCapturerJob = coroutineScope.async(dispatcher) {
4236
screenCapturer?.start()
4337
}
4438
}
4539

4640
override fun testEnded(test: Test, testMetrics: Map<String, String>) {
4741
logger.debug { "Finished recording for ${test.toSimpleSafeTestName()}" }
4842
screenCapturerJob?.cancel()
49-
threadPoolDispatcher.close()
5043

5144
screenCapturer?.attachment?.let { attachment ->
5245
attachmentListeners.forEach {

vendor/vendor-android/ddmlib/src/main/kotlin/com/malinskiy/marathon/android/ddmlib/DdmlibAndroidDevice.kt

+24-39
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,16 @@ import com.malinskiy.marathon.test.TestBatch
5353
import com.malinskiy.marathon.time.Timer
5454
import kotlinx.coroutines.CompletableDeferred
5555
import kotlinx.coroutines.CoroutineScope
56-
import kotlinx.coroutines.DelicateCoroutinesApi
56+
import kotlinx.coroutines.Dispatchers
5757
import kotlinx.coroutines.Job
58+
import kotlinx.coroutines.SupervisorJob
5859
import kotlinx.coroutines.async
59-
import kotlinx.coroutines.newFixedThreadPoolContext
60-
import kotlinx.coroutines.runBlocking
60+
import kotlinx.coroutines.cancel
6161
import java.awt.image.BufferedImage
6262
import java.io.File
6363
import java.io.IOException
6464
import java.util.UUID
6565
import java.util.concurrent.TimeUnit
66-
import kotlin.coroutines.CoroutineContext
6766

6867
class DdmlibAndroidDevice(
6968
val ddmsDevice: IDevice,
@@ -75,15 +74,18 @@ class DdmlibAndroidDevice(
7574
private val reportsFileManager: FileManager,
7675
private val serialStrategy: SerialStrategy,
7776
private val logcatListener: LogcatListener,
78-
private val strictRunChecker: StrictRunChecker
79-
) : Device, CoroutineScope, AndroidDevice {
77+
private val strictRunChecker: StrictRunChecker,
78+
parentJob: Job = Job(),
79+
) : Device, AndroidDevice {
8080
override val fileManager = RemoteFileManager(this)
8181

8282
override val version: AndroidVersion by lazy { ddmsDevice.version }
83-
private val nullOutputReceiver = NullOutputReceiver()
84-
private val parentJob: Job = Job()
8583

86-
private var logcatReceiver: CliLogcatReceiver? = null
84+
private val dispatcher = Dispatchers.IO.limitedParallelism(1)
85+
private val job = SupervisorJob(parentJob)
86+
private val coroutineScope = CoroutineScope(job + dispatcher)
87+
private val logger = MarathonLogging.logger(DdmlibAndroidDevice::class.java.simpleName)
88+
8789
private val logMessagesListener: (List<LogCatMessage>) -> Unit = {
8890
it.forEach { msg ->
8991
logcatListener.onMessage(this, msg.toMarathonLogcatMessage())
@@ -102,7 +104,7 @@ class DdmlibAndroidDevice(
102104

103105
override fun executeCommand(command: String, errorMessage: String) {
104106
try {
105-
ddmsDevice.safeExecuteShellCommand(command, nullOutputReceiver)
107+
ddmsDevice.safeExecuteShellCommand(command, NullOutputReceiver())
106108
} catch (e: TimeoutException) {
107109
logger.error("$errorMessage while executing $command", e)
108110
} catch (e: AdbCommandRejectedException) {
@@ -147,14 +149,6 @@ class DdmlibAndroidDevice(
147149
)
148150
}
149151

150-
override fun waitForAsyncWork() {
151-
runBlocking(context = coroutineContext) {
152-
parentJob.children.forEach {
153-
it.join()
154-
}
155-
}
156-
}
157-
158152
private fun bufferedImageFrom(rawImage: RawImage): BufferedImage {
159153
val image = BufferedImage(rawImage.width, rawImage.height, BufferedImage.TYPE_INT_ARGB)
160154

@@ -169,15 +163,6 @@ class DdmlibAndroidDevice(
169163
return image
170164
}
171165

172-
@OptIn(DelicateCoroutinesApi::class)
173-
private val dispatcher by lazy {
174-
newFixedThreadPoolContext(1, "AndroidDevice - execution - ${ddmsDevice.serialNumber}")
175-
}
176-
177-
override val coroutineContext: CoroutineContext = dispatcher
178-
179-
private val logger = MarathonLogging.logger(DdmlibAndroidDevice::class.java.simpleName)
180-
181166
override val abi: String by lazy {
182167
ddmsDevice.getProperty("ro.product.cpu.abi") ?: "Unknown"
183168
}
@@ -231,6 +216,7 @@ class DdmlibAndroidDevice(
231216
?: serialNumber.takeIf { it.isNotEmpty() }
232217
?: UUID.randomUUID().toString()
233218
}
219+
234220
SerialStrategy.MARATHON_PROPERTY -> marathonSerialProp
235221
SerialStrategy.BOOT_PROPERTY -> serialProp
236222
SerialStrategy.HOSTNAME -> hostName
@@ -276,15 +262,15 @@ class DdmlibAndroidDevice(
276262
val androidComponentInfo = testBatch.componentInfo as AndroidComponentInfo
277263

278264
try {
279-
async { ensureInstalled(androidComponentInfo) }.await()
265+
coroutineScope.async { ensureInstalled(androidComponentInfo) }.await()
280266
} catch (@Suppress("TooGenericExceptionCaught") e: Throwable) {
281267
logger.error(e) { "Terminating device $serialNumber due to installation failures" }
282268
throw DeviceLostException(e)
283269
}
284270

285271
safePrintToLogcat(SERVICE_LOGS_TAG, "\"batch_started: {${testBatch.id}}\"")
286272

287-
val deferredResult = async {
273+
val deferredResult = coroutineScope.async {
288274
val listeners = createListeners(configuration, devicePoolId, testBatch, deferred, progressReporter)
289275
val listener = DdmlibTestRunListener(testBatch.componentInfo, listeners)
290276
AndroidDeviceTestRunner(this@DdmlibAndroidDevice).execute(configuration, testBatch, listener)
@@ -334,21 +320,16 @@ class DdmlibAndroidDevice(
334320

335321
override suspend fun prepare(configuration: Configuration) {
336322
track.trackDevicePreparing(this) {
337-
val deferred = async {
323+
val logcatReceiver = CliLogcatReceiver(adbPath, reportsFileManager, ddmsDevice, logMessagesListener)
324+
val deferred = coroutineScope.async {
338325
clearLogcat(ddmsDevice)
339-
340-
logcatReceiver = CliLogcatReceiver(adbPath, reportsFileManager, ddmsDevice, logMessagesListener)
341-
logcatReceiver?.start()
326+
logcatReceiver.start()
342327
}
328+
job.invokeOnCompletion { logcatReceiver.close() }
343329
deferred.await()
344330
}
345331
}
346332

347-
override fun dispose() {
348-
logcatReceiver?.dispose()
349-
dispatcher.close()
350-
}
351-
352333
private fun selectRecorderType(preferred: DeviceFeature?, features: Collection<DeviceFeature>) = when {
353334
features.contains(preferred) -> preferred
354335
features.contains(DeviceFeature.VIDEO) -> DeviceFeature.VIDEO
@@ -370,6 +351,10 @@ class DdmlibAndroidDevice(
370351
return receiver.output()
371352
}
372353

354+
override fun close() {
355+
coroutineScope.cancel()
356+
}
357+
373358
private fun prepareRecorderListener(
374359
feature: DeviceFeature,
375360
attachmentProviders: MutableList<AttachmentProvider>
@@ -381,7 +366,7 @@ class DdmlibAndroidDevice(
381366
}
382367

383368
DeviceFeature.SCREENSHOT -> {
384-
ScreenCapturerTestRunListener(attachmentManager, this)
369+
ScreenCapturerTestRunListener(attachmentManager, this, coroutineScope)
385370
.also { attachmentProviders.add(it) }
386371
}
387372
}

0 commit comments

Comments
 (0)