From 4fe4a149d8f6cc84043b36acd5ca444896f4166a Mon Sep 17 00:00:00 2001 From: Anton Malinskiy Date: Fri, 23 Nov 2018 02:27:04 +0700 Subject: [PATCH 01/14] Add support for taking screenshots on Android Screenshots are taken all the time independent of the test result --- buildSrc/src/main/kotlin/Versions.kt | 2 + .../com/malinskiy/marathon/io/FileType.kt | 3 +- .../marathon/report/html/HtmlReport.kt | 9 +- vendor-android/build.gradle.kts | 1 + .../executor/AndroidDeviceTestRunner.kt | 13 +- .../listeners/screenshot/GifSequenceWriter.kt | 195 ++++++++++++++++++ .../listeners/screenshot/ScreenCapturer.kt | 83 ++++++++ .../ScreenCapturerTestRunListener.kt | 34 +++ 8 files changed, 333 insertions(+), 7 deletions(-) create mode 100644 vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/GifSequenceWriter.kt create mode 100644 vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturer.kt create mode 100644 vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index c6741b69a..b1049f8b6 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -37,6 +37,7 @@ object Versions { val sshj = "0.26.0" val testContainers = "1.9.1" val jupiterEngine = "5.1.0" + val scalr = "4.2" } object BuildPlugins { @@ -70,6 +71,7 @@ object Libraries { val guava = "com.google.guava:guava:${Versions.guava}" val rsync4j = "com.github.fracpete:rsync4j-all:${Versions.rsync4j}" val sshj = "com.hierynomus:sshj:${Versions.sshj}" + val scalr = "org.imgscalr:imgscalr-lib:${Versions.scalr}" } object TestLibraries { diff --git a/core/src/main/kotlin/com/malinskiy/marathon/io/FileType.kt b/core/src/main/kotlin/com/malinskiy/marathon/io/FileType.kt index fb112fa94..03e0bb82a 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/io/FileType.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/io/FileType.kt @@ -6,6 +6,7 @@ enum class FileType(val dir: String, val suffix: String) { TEST_RESULT("test_result", "json"), LOG("logs", "log"), DEVICE_INFO("devices", "json"), - VIDEO("video", "mp4") + VIDEO("video", "mp4"), + SCREENSHOT("screenshot", "gif") } diff --git a/core/src/main/kotlin/com/malinskiy/marathon/report/html/HtmlReport.kt b/core/src/main/kotlin/com/malinskiy/marathon/report/html/HtmlReport.kt index e435b04b9..ee7f3cabf 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/report/html/HtmlReport.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/report/html/HtmlReport.kt @@ -157,12 +157,11 @@ fun TestResult.toHtmlFullTest(poolId: String) = HtmlFullTest( diagnosticVideo = device.deviceFeatures.contains(DeviceFeature.VIDEO), diagnosticScreenshots = device.deviceFeatures.contains(DeviceFeature.SCREENSHOT), stacktrace = stacktrace, - screenshot = "", - /*screenshot = when (device.deviceFeatures.contains(DeviceFeature.SCREENSHOT) && status != Status.Passed) { - true -> "../../../../animation/$poolId/${device.serialNumber}/${test.pkg}.${test.clazz}%23${test.method}.gif" + screenshot = when (device.deviceFeatures.contains(DeviceFeature.SCREENSHOT)) { + true -> "../../../../screenshot/$poolId/${device.serialNumber}/${test.pkg}.${test.clazz}%23${test.method}.gif" false -> "" - },*/ - video = when (device.deviceFeatures.contains(DeviceFeature.VIDEO) && status != Status.Passed) { + }, + video = when (device.deviceFeatures.contains(DeviceFeature.VIDEO)) { true -> "../../../../video/$poolId/${device.serialNumber}/${test.pkg}.${test.clazz}%23${test.method}.mp4" false -> "" }, diff --git a/vendor-android/build.gradle.kts b/vendor-android/build.gradle.kts index 50eedb17d..8828dfa4e 100644 --- a/vendor-android/build.gradle.kts +++ b/vendor-android/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(Libraries.dexTestParser) implementation(Libraries.axmlParser) implementation(Libraries.jacksonAnnotations) + implementation(Libraries.scalr) implementation(project(":core")) testImplementation(TestLibraries.kluent) testImplementation(TestLibraries.spekAPI) diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/AndroidDeviceTestRunner.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/AndroidDeviceTestRunner.kt index 513932362..b87d8b35e 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/AndroidDeviceTestRunner.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/AndroidDeviceTestRunner.kt @@ -10,9 +10,12 @@ import com.malinskiy.marathon.android.ApkParser import com.malinskiy.marathon.android.executor.listeners.CompositeTestRunListener import com.malinskiy.marathon.android.executor.listeners.DebugTestRunListener import com.malinskiy.marathon.android.executor.listeners.LogCatListener +import com.malinskiy.marathon.android.executor.listeners.NoOpTestRunListener import com.malinskiy.marathon.android.executor.listeners.ProgressTestRunListener import com.malinskiy.marathon.android.executor.listeners.video.ScreenRecorderTestRunListener import com.malinskiy.marathon.android.executor.listeners.TestRunResultsListener +import com.malinskiy.marathon.android.executor.listeners.screenshot.ScreenCapturerTestRunListener +import com.malinskiy.marathon.device.DeviceFeature import com.malinskiy.marathon.device.DevicePoolId import com.malinskiy.marathon.exceptions.TestBatchExecutionException import com.malinskiy.marathon.execution.Configuration @@ -51,10 +54,18 @@ class AndroidDeviceTestRunner(private val device: AndroidDevice) { runner.setClassNames(tests) val fileManager = FileManager(configuration.outputDir) + + val features = device.deviceFeatures + val recorderListener = when { + features.contains(DeviceFeature.VIDEO) -> ScreenRecorderTestRunListener(fileManager, devicePoolId, device) + features.contains(DeviceFeature.SCREENSHOT) -> ScreenCapturerTestRunListener(fileManager, devicePoolId, device) + else -> NoOpTestRunListener() + } + val listeners = CompositeTestRunListener( listOf( TestRunResultsListener(testBatch, device, deferred), - ScreenRecorderTestRunListener(fileManager, devicePoolId, device), + recorderListener, DebugTestRunListener(device), ProgressTestRunListener(device, devicePoolId, progressReporter), LogCatListener(device, devicePoolId, LogWriter(fileManager)) diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/GifSequenceWriter.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/GifSequenceWriter.kt new file mode 100644 index 000000000..743ca9cef --- /dev/null +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/GifSequenceWriter.kt @@ -0,0 +1,195 @@ +package com.malinskiy.marathon.android.executor.listeners.screenshot + +// +// GifSequenceWriter.java +// +// Created by Elliot Kroo on 2009-04-25. +// +// This work is licensed under the Creative Commons Attribution 3.0 Unported +// License. To view a copy of this license, visit +// http://creativecommons.org/licenses/by/3.0/ or send a letter to Creative +// Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. + + +import javax.imageio.* +import javax.imageio.metadata.* +import javax.imageio.stream.* +import java.awt.image.* +import java.io.* + +class GifSequenceWriter +/** + * Creates a new GifSequenceWriter + * + * @param outputStream the ImageOutputStream to be written to + * @param imageType one of the imageTypes specified in BufferedImage + * @param timeBetweenFramesMS the time between frames in miliseconds + * @param loopContinuously wether the gif should loop repeatedly + * @throws IIOException if no gif ImageWriters are found + * + * @author Elliot Kroo (elliot[at]kroo[dot]net) + */ +@Throws(IIOException::class, IOException::class) +constructor( + outputStream: ImageOutputStream, + imageType: Int, + timeBetweenFramesMS: Int, + loopContinuously: Boolean) { + protected var gifWriter: ImageWriter + protected var imageWriteParam: ImageWriteParam + protected var imageMetaData: IIOMetadata + + init { + // my method to create a writer + gifWriter = writer + imageWriteParam = gifWriter.defaultWriteParam + val imageTypeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(imageType) + + imageMetaData = gifWriter.getDefaultImageMetadata(imageTypeSpecifier, + imageWriteParam) + + val metaFormatName = imageMetaData.nativeMetadataFormatName + + val root = imageMetaData.getAsTree(metaFormatName) as IIOMetadataNode + + val graphicsControlExtensionNode = getNode( + root, + "GraphicControlExtension") + + graphicsControlExtensionNode.setAttribute("disposalMethod", "none") + graphicsControlExtensionNode.setAttribute("userInputFlag", "FALSE") + graphicsControlExtensionNode.setAttribute( + "transparentColorFlag", + "FALSE") + graphicsControlExtensionNode.setAttribute( + "delayTime", + Integer.toString(timeBetweenFramesMS / 10)) + graphicsControlExtensionNode.setAttribute( + "transparentColorIndex", + "0") + + val commentsNode = getNode(root, "CommentExtensions") + commentsNode.setAttribute("CommentExtension", "Created by MAH") + + val appEntensionsNode = getNode( + root, + "ApplicationExtensions") + + val child = IIOMetadataNode("ApplicationExtension") + + child.setAttribute("applicationID", "NETSCAPE") + child.setAttribute("authenticationCode", "2.0") + + val loop = if (loopContinuously) 0 else 1 + + child.userObject = byteArrayOf(0x1, (loop and 0xFF).toByte(), (loop shr 8 and 0xFF).toByte()) + appEntensionsNode.appendChild(child) + + imageMetaData.setFromTree(metaFormatName, root) + + gifWriter.output = outputStream + + gifWriter.prepareWriteSequence( + null) + } + + @Throws(IOException::class) + fun writeToSequence(img: RenderedImage) { + gifWriter.writeToSequence( + IIOImage( + img, null, + imageMetaData), + imageWriteParam) + } + + /** + * Close this GifSequenceWriter object. This does not close the underlying + * stream, just finishes off the GIF. + */ + @Throws(IOException::class) + fun close() { + gifWriter.endWriteSequence() + } + + companion object { + + /** + * Returns the first available GIF ImageWriter using + * ImageIO.getImageWritersBySuffix("gif"). + * + * @return a GIF ImageWriter object + * @throws IIOException if no GIF image writers are returned + */ + private val writer: ImageWriter + @Throws(IIOException::class) + get() { + val iter = ImageIO.getImageWritersBySuffix("gif") + return if (!iter.hasNext()) { + throw IIOException("No GIF Image Writers Exist") + } else { + iter.next() + } + } + + /** + * Returns an existing child node, or creates and returns a new child node (if + * the requested node does not exist). + * + * @param rootNode the IIOMetadataNode to search for the child node. + * @param nodeName the name of the child node. + * + * @return the child node, if found or a new node created with the given name. + */ + private fun getNode( + rootNode: IIOMetadataNode, + nodeName: String): IIOMetadataNode { + val nNodes = rootNode.length + for (i in 0 until nNodes) { + if (rootNode.item(i).nodeName.compareTo(nodeName, ignoreCase = true) == 0) { + return rootNode.item(i) as IIOMetadataNode + } + } + val node = IIOMetadataNode(nodeName) + rootNode.appendChild(node) + return node + } + + /** + * public GifSequenceWriter( + * BufferedOutputStream outputStream, + * int imageType, + * int timeBetweenFramesMS, + * boolean loopContinuously) { + * + */ + + @Throws(Exception::class) + @JvmStatic + fun main(args: Array) { + if (args.size > 1) { + // grab the output image type from the first image in the sequence + val firstImage = ImageIO.read(File(args[0])) + + // create a new BufferedOutputStream with the last argument + val output = FileImageOutputStream(File(args[args.size - 1])) + + // create a gif sequence with the type of the first image, 1 second + // between frames, which loops continuously + val writer = GifSequenceWriter(output, firstImage.type, 1, false) + + // write out the first image to our sequence... + writer.writeToSequence(firstImage) + for (i in 1 until args.size - 1) { + val nextImage = ImageIO.read(File(args[i])) + writer.writeToSequence(nextImage) + } + + writer.close() + output.close() + } else { + println( + "Usage: java GifSequenceWriter [list of gif files] [output file]") + } + } + } +} \ No newline at end of file diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturer.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturer.kt new file mode 100644 index 000000000..3a02bf360 --- /dev/null +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturer.kt @@ -0,0 +1,83 @@ +package com.malinskiy.marathon.android.executor.listeners.screenshot + +import com.android.ddmlib.AdbCommandRejectedException +import com.android.ddmlib.RawImage +import com.android.ddmlib.TimeoutException +import com.android.ddmlib.testrunner.TestIdentifier +import com.malinskiy.marathon.android.AndroidDevice +import com.malinskiy.marathon.android.toTest +import com.malinskiy.marathon.device.DevicePoolId +import com.malinskiy.marathon.io.FileManager +import com.malinskiy.marathon.io.FileType +import com.malinskiy.marathon.log.MarathonLogging +import kotlinx.coroutines.experimental.NonCancellable.isActive +import kotlinx.coroutines.experimental.delay +import org.imgscalr.Scalr +import java.awt.image.BufferedImage +import java.awt.image.BufferedImage.TYPE_INT_ARGB +import java.awt.image.RenderedImage +import java.io.IOException +import java.util.concurrent.TimeUnit +import javax.imageio.stream.FileImageOutputStream +import kotlin.system.measureTimeMillis + + +class ScreenCapturer(val device: AndroidDevice, + private val poolId: DevicePoolId, + private val fileManager: FileManager, + val test: TestIdentifier) { + + suspend fun start() { + val outputStream = FileImageOutputStream(fileManager.createFile(FileType.SCREENSHOT, poolId, device, test.toTest())) + val writer = GifSequenceWriter(outputStream, TYPE_INT_ARGB, DELAY, true) + while (isActive) { + val capturingTimeMillis = measureTimeMillis { + getScreenshot()?.let { writer.writeToSequence(it) } + } + val sleepTimeMillis = when { + (DELAY - capturingTimeMillis) < 0 -> 0 + else -> DELAY - capturingTimeMillis + } + delay(sleepTimeMillis) + } + writer.close() + outputStream.close() + } + + private fun getScreenshot(): RenderedImage? { + return try { + val screenshot = device.ddmsDevice.getScreenshot(TIMEOUT_MS, TimeUnit.MILLISECONDS) + val bufferedImage = bufferedImageFrom(rawImage = screenshot) + Scalr.resize(bufferedImage, Scalr.Method.SPEED, Scalr.Mode.AUTOMATIC, 1280, 720) + } catch (e: TimeoutException) { + logger.error(e) { "Timeout. Exiting" } + null + } catch (e: IOException) { + null + } catch (e: AdbCommandRejectedException) { + logger.error(e) { "Adb is not responding. Exiting" } + null + } + } + + private fun bufferedImageFrom(rawImage: RawImage): BufferedImage { + val image = BufferedImage(rawImage.width, rawImage.height, TYPE_INT_ARGB) + + var index = 0 + val bytesPerPixel = rawImage.bpp shr 3 + for (y in 0 until rawImage.height) { + for (x in 0 until rawImage.width) { + image.setRGB(x, y, rawImage.getARGB(index) or -0x1000000) + index += bytesPerPixel + } + } + return image + } + + + companion object { + const val DELAY = 500 + const val TIMEOUT_MS = 300L + val logger = MarathonLogging.logger(ScreenCapturer::class.java.simpleName) + } +} \ No newline at end of file diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt new file mode 100644 index 000000000..a6d93ecbd --- /dev/null +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt @@ -0,0 +1,34 @@ +package com.malinskiy.marathon.android.executor.listeners.screenshot + +import com.android.ddmlib.testrunner.TestIdentifier +import com.malinskiy.marathon.android.AndroidDevice +import com.malinskiy.marathon.android.executor.listeners.NoOpTestRunListener +import com.malinskiy.marathon.android.toTest +import com.malinskiy.marathon.device.DevicePoolId +import com.malinskiy.marathon.io.FileManager +import com.malinskiy.marathon.log.MarathonLogging +import com.malinskiy.marathon.test.toSimpleSafeTestName +import kotlinx.coroutines.experimental.Job +import kotlinx.coroutines.experimental.async + +class ScreenCapturerTestRunListener(private val fileManager: FileManager, + private val pool: DevicePoolId, + private val device: AndroidDevice) : NoOpTestRunListener() { + + private var screenCapturerJob: Job? = null + private val logger = MarathonLogging.logger(ScreenCapturerTestRunListener::class.java.simpleName) + + override fun testStarted(test: TestIdentifier) { + super.testStarted(test) + logger.debug { "Starting recording for ${test.toTest().toSimpleSafeTestName()}" } + screenCapturerJob = async { + ScreenCapturer(device, pool, fileManager, test).start() + } + } + + override fun testEnded(test: TestIdentifier, testMetrics: Map) { + super.testEnded(test, testMetrics) + logger.debug { "Finished recording for ${test.toTest().toSimpleSafeTestName()}" } + screenCapturerJob?.cancel() + } +} \ No newline at end of file From fceb52f54e389398e3e2f3097ff80ca65307b00c Mon Sep 17 00:00:00 2001 From: Anton Malinskiy Date: Fri, 23 Nov 2018 16:14:09 +0700 Subject: [PATCH 02/14] Since taking the screenshot actually blocks the thread separate it into different threadpool --- .../listeners/screenshot/ScreenCapturerTestRunListener.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt index a6d93ecbd..b7226b237 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt @@ -10,6 +10,7 @@ import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.test.toSimpleSafeTestName import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.async +import kotlinx.coroutines.experimental.newFixedThreadPoolContext class ScreenCapturerTestRunListener(private val fileManager: FileManager, private val pool: DevicePoolId, @@ -17,11 +18,12 @@ class ScreenCapturerTestRunListener(private val fileManager: FileManager, private var screenCapturerJob: Job? = null private val logger = MarathonLogging.logger(ScreenCapturerTestRunListener::class.java.simpleName) + private val threadPoolDispatcher = newFixedThreadPoolContext(1, "ScreenCapturer - ${device.serialNumber}") override fun testStarted(test: TestIdentifier) { super.testStarted(test) logger.debug { "Starting recording for ${test.toTest().toSimpleSafeTestName()}" } - screenCapturerJob = async { + screenCapturerJob = async(context = threadPoolDispatcher) { ScreenCapturer(device, pool, fileManager, test).start() } } From 7548f0afc42e0dab5f0ee35d381388f855f88c73 Mon Sep 17 00:00:00 2001 From: Anton Malinskiy Date: Mon, 26 Nov 2018 10:54:55 +0700 Subject: [PATCH 03/14] AndroidDeviceProvider should pass only booted devices as connected --- .../marathon/android/AndroidDevice.kt | 34 +++----- .../marathon/android/AndroidDeviceProvider.kt | 82 ++++++++++++++----- 2 files changed, 74 insertions(+), 42 deletions(-) diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt index 54d3abe0e..d6ca9269e 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt @@ -2,7 +2,6 @@ package com.malinskiy.marathon.android import com.android.ddmlib.IDevice import com.android.ddmlib.NullOutputReceiver -import com.android.ddmlib.TimeoutException import com.malinskiy.marathon.android.executor.AndroidAppInstaller import com.malinskiy.marathon.android.executor.AndroidDeviceTestRunner import com.malinskiy.marathon.device.Device @@ -53,7 +52,10 @@ class AndroidDevice(val ddmsDevice: IDevice) : Device { return features } - override val serialNumber: String by lazy { + /** + * We can only call this after the device finished booting + */ + private val realSerialNumber: String by lazy { val marathonSerialProp: String = ddmsDevice.getProperty("marathon.serialno") ?: "" val serialProp: String = ddmsDevice.getProperty("ro.boot.serialno") ?: "" val hostName: String = ddmsDevice.getProperty("net.hostname") ?: "" @@ -66,6 +68,14 @@ class AndroidDevice(val ddmsDevice: IDevice) : Device { ?: UUID.randomUUID().toString() } + val booted: Boolean + get() = ddmsDevice.getProperty("sys.boot_completed") != null + + override val serialNumber: String = when { + booted -> realSerialNumber + else -> ddmsDevice.serialNumber + } + override val operatingSystem: OperatingSystem by lazy { OperatingSystem(ddmsDevice.version.apiString) } @@ -91,32 +101,12 @@ class AndroidDevice(val ddmsDevice: IDevice) : Device { } override fun prepare(configuration: Configuration) { - if (!waitForBoot()) throw TimeoutException("Timeout waiting for device $serialNumber to boot") - AndroidAppInstaller(configuration).prepareInstallation(this) RemoteFileManager.removeRemoteDirectory(ddmsDevice) RemoteFileManager.createRemoteDirectory(ddmsDevice) clearLogcat(ddmsDevice) } - private fun waitForBoot(): Boolean { - var booted = false - for (i in 1..30) { - if (ddmsDevice.getProperty("sys.boot_completed") == null) { - Thread.sleep(1000) - logger.debug { "Device $serialNumber is still booting..." } - } else { - logger.debug { "Device $serialNumber booted!" } - booted = true - break - } - - if (Thread.interrupted()) return true - } - - return booted - } - override fun dispose() {} private fun clearLogcat(device: IDevice) { diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt index df9c8d0cb..01faefe15 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt @@ -12,8 +12,10 @@ import com.malinskiy.marathon.device.DeviceProvider.DeviceEvent.DeviceDisconnect import com.malinskiy.marathon.exceptions.NoDevicesException import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.vendor.VendorConfiguration +import kotlinx.coroutines.experimental.NonCancellable.isActive import kotlinx.coroutines.experimental.channels.Channel import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.newFixedThreadPoolContext import java.nio.file.Paths import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap @@ -29,6 +31,7 @@ class AndroidDeviceProvider : DeviceProvider { private val channel: Channel = unboundedChannel() private val devices: ConcurrentMap = ConcurrentHashMap() + private val bootWaitContext = newFixedThreadPoolContext(4, "AndroidDeviceProvider-BootWait") override fun initialize(vendorConfiguration: VendorConfiguration) { if (vendorConfiguration !is AndroidConfiguration) { @@ -42,27 +45,35 @@ class AndroidDeviceProvider : DeviceProvider { val listener = object : AndroidDebugBridge.IDeviceChangeListener { override fun deviceChanged(device: IDevice?, changeMask: Int) { device?.let { - val androidDevice = getDeviceOrCreate(it) - val healthy = androidDevice.healthy - - logger.debug { "Device ${device.serialNumber} changed state. Healthy = $healthy" } - if (healthy) { - notifyConnected(androidDevice) - } else { - //This shouldn't have any side effects even if device was previously removed - notifyDisconnected(androidDevice) + launch(context = bootWaitContext) { + val maybeNewAndroidDevice = AndroidDevice(it) + val healthy = maybeNewAndroidDevice.healthy + + logger.debug { "Device ${device.serialNumber} changed state. Healthy = $healthy" } + if (healthy) { + verifyBooted(maybeNewAndroidDevice) + val androidDevice = getDeviceOrPut(maybeNewAndroidDevice) + notifyConnected(androidDevice) + } else { + //This shouldn't have any side effects even if device was previously removed + notifyDisconnected(maybeNewAndroidDevice) + } } } } override fun deviceConnected(device: IDevice?) { device?.let { - val androidDevice = getDeviceOrCreate(it) - val healthy = androidDevice.healthy - logger.debug { "Device ${device.serialNumber} connected channel.isFull = ${channel.isFull}. Healthy = $healthy" } - - if (healthy) { - notifyConnected(androidDevice) + launch(context = bootWaitContext) { + val maybeNewAndroidDevice = AndroidDevice(it) + val healthy = maybeNewAndroidDevice.healthy + logger.debug { "Device ${maybeNewAndroidDevice.serialNumber} connected channel.isFull = ${channel.isFull}. Healthy = $healthy" } + + if (healthy) { + verifyBooted(maybeNewAndroidDevice) + val androidDevice = getDeviceOrPut(maybeNewAndroidDevice) + notifyConnected(androidDevice) + } } } } @@ -70,9 +81,32 @@ class AndroidDeviceProvider : DeviceProvider { override fun deviceDisconnected(device: IDevice?) { device?.let { logger.debug { "Device ${device.serialNumber} disconnected" } - val androidDevice = getDeviceOrCreate(it) - notifyDisconnected(androidDevice) + matchDdmsToDevice(it)?.let { + notifyDisconnected(it) + } + } + } + + private fun verifyBooted(device: AndroidDevice) { + if (!waitForBoot(device)) throw TimeoutException("Timeout waiting for device ${device.serialNumber} to boot") + } + + private fun waitForBoot(device: AndroidDevice): Boolean { + var booted = false + for (i in 1..30) { + if (device.booted) { + logger.debug { "Device ${device.serialNumber} booted!" } + booted = true + break + } else { + Thread.sleep(1000) + logger.debug { "Device ${device.serialNumber} is still booting..." } + } + + if (Thread.interrupted() || !isActive) return true } + + return booted } private fun notifyConnected(device: AndroidDevice) { @@ -105,9 +139,17 @@ class AndroidDeviceProvider : DeviceProvider { } } - private fun getDeviceOrCreate(ddmlibDevice: IDevice): AndroidDevice { - return devices.getOrPut(ddmlibDevice.serialNumber) { - AndroidDevice(ddmlibDevice) + private fun getDeviceOrPut(androidDevice: AndroidDevice): AndroidDevice { + return devices.getOrPut(androidDevice.serialNumber) { + androidDevice + } + } + + private fun matchDdmsToDevice(device: IDevice): AndroidDevice? { + val observedDevices = devices.values + return observedDevices.findLast { + device == it.ddmsDevice || + device.serialNumber == it.ddmsDevice.serialNumber } } From 052c34bcc7d59743e093e7b9c59e7a5eb3645ab7 Mon Sep 17 00:00:00 2001 From: Anton Malinskiy Date: Mon, 26 Nov 2018 18:57:19 +0700 Subject: [PATCH 04/14] Fix kotlin api version in build scripts Start of testing dsl for marathon core --- cli/build.gradle.kts | 6 +-- core/build.gradle.kts | 2 + .../exceptions/DeviceLostException.kt | 9 ++++ .../marathon/execution/device/DeviceActor.kt | 5 ++ .../com/malinskiy/marathon/CalculatorSpec.kt | 24 --------- .../marathon/scenario/SuccessScenarios.kt | 40 +++++++++++++++ execution-timeline/build.gradle.kts | 6 +-- marathon-html-report/build.gradle.kts | 6 +-- settings.gradle.kts | 1 + vendor-android/build.gradle.kts | 6 +-- .../executor/AndroidDeviceTestRunner.kt | 7 ++- vendor-ios/build.gradle.kts | 1 + .../com/malinskiy/marathon/ios/Mocks.kt | 9 ++++ vendor-test/build.gradle.kts | 26 ++++++++++ .../marathon/test/MarathonFactory.kt | 26 ++++++++++ .../com/malinskiy/marathon/test/Mocks.kt | 48 ++++++++++++++++++ .../com/malinskiy/marathon/test/StubDevice.kt | 50 +++++++++++++++++++ .../marathon/test/StubDeviceProvider.kt | 37 ++++++++++++++ .../marathon/test/StubExecutionResults.kt | 4 ++ .../marathon/test/TestBodyExtensions.kt | 9 ++++ 20 files changed, 281 insertions(+), 41 deletions(-) create mode 100644 core/src/main/kotlin/com/malinskiy/marathon/exceptions/DeviceLostException.kt delete mode 100644 core/src/test/kotlin/com/malinskiy/marathon/CalculatorSpec.kt create mode 100644 core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt create mode 100644 vendor-test/build.gradle.kts create mode 100644 vendor-test/src/main/kotlin/com/malinskiy/marathon/test/MarathonFactory.kt create mode 100644 vendor-test/src/main/kotlin/com/malinskiy/marathon/test/Mocks.kt create mode 100644 vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt create mode 100644 vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt create mode 100644 vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubExecutionResults.kt create mode 100644 vendor-test/src/main/kotlin/com/malinskiy/marathon/test/TestBodyExtensions.kt diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index f935129df..ebe679136 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -49,11 +49,9 @@ dependencies { Deployment.initialize(project) -val compileKotlin by tasks.getting(KotlinCompile::class) { - kotlinOptions.jvmTarget = "1.8" -} -val compileTestKotlin by tasks.getting(KotlinCompile::class) { +tasks.withType { kotlinOptions.jvmTarget = "1.8" + kotlinOptions.apiVersion = "1.3" } buildConfig { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index f76a3ec95..f5087f56f 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(Libraries.slf4jAPI) implementation(Libraries.logbackClassic) implementation(Libraries.influxDbClient) + testCompile(project(":vendor-test")) testCompile(TestLibraries.kluent) testCompile(TestLibraries.spekAPI) testRuntime(TestLibraries.spekJUnitPlatformEngine) @@ -73,6 +74,7 @@ Deployment.initialize(project) tasks.withType { kotlinOptions.jvmTarget = "1.8" + kotlinOptions.apiVersion = "1.3" } junitPlatform { diff --git a/core/src/main/kotlin/com/malinskiy/marathon/exceptions/DeviceLostException.kt b/core/src/main/kotlin/com/malinskiy/marathon/exceptions/DeviceLostException.kt new file mode 100644 index 000000000..ffc941956 --- /dev/null +++ b/core/src/main/kotlin/com/malinskiy/marathon/exceptions/DeviceLostException.kt @@ -0,0 +1,9 @@ +package com.malinskiy.marathon.exceptions + +/** + * Indicates that the execution device is no longer available + */ +class DeviceLostException: RuntimeException { + constructor(cause: Throwable): super(cause) + constructor(message: String): super(message) +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt index dc5f20f9f..bed46b684 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt @@ -4,6 +4,7 @@ import com.malinskiy.marathon.actor.Actor import com.malinskiy.marathon.actor.StateMachine import com.malinskiy.marathon.device.Device import com.malinskiy.marathon.device.DevicePoolId +import com.malinskiy.marathon.exceptions.DeviceLostException import com.malinskiy.marathon.exceptions.TestBatchExecutionException import com.malinskiy.marathon.execution.Configuration import com.malinskiy.marathon.execution.DevicePoolMessage @@ -172,6 +173,10 @@ class DeviceActor(private val devicePoolId: DevicePoolId, job = async(context, parent = deviceJob) { try { device.execute(configuration, devicePoolId, batch, result, progressReporter) + } catch (e: DeviceLostException) { + logger.error(e) { "Critical error during execution" } + returnBatch(batch) +// terminate() } catch (e: TestBatchExecutionException) { returnBatch(batch) } diff --git a/core/src/test/kotlin/com/malinskiy/marathon/CalculatorSpec.kt b/core/src/test/kotlin/com/malinskiy/marathon/CalculatorSpec.kt deleted file mode 100644 index b1c0b435f..000000000 --- a/core/src/test/kotlin/com/malinskiy/marathon/CalculatorSpec.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.malinskiy.marathon - -import org.amshove.kluent.shouldEqual -import org.jetbrains.spek.api.Spek -import org.jetbrains.spek.api.dsl.given -import org.jetbrains.spek.api.dsl.it -import org.jetbrains.spek.api.dsl.on - -object CalculatorSpec: Spek({ - given("a calculator") { - on("addition") { - val sum = 2 + 4 - it("should return the result of adding the first number to the second number") { - sum shouldEqual 6 - } - } - on("subtraction") { - val subtract = 4 - 2 - it("should return the result of subtracting the second number from the first number") { - subtract shouldEqual 2 - } - } - } -}) diff --git a/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt b/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt new file mode 100644 index 000000000..ad9b516f1 --- /dev/null +++ b/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt @@ -0,0 +1,40 @@ +package com.malinskiy.marathon.scenario + +import com.malinskiy.marathon.device.DeviceProvider +import com.malinskiy.marathon.execution.TestStatus +import com.malinskiy.marathon.execution.queue.TestState +import com.malinskiy.marathon.test.Mocks +import com.malinskiy.marathon.test.StubDevice +import com.malinskiy.marathon.test.Test +import com.malinskiy.marathon.test.setupMarathon +import kotlinx.coroutines.experimental.delay +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on + +class SuccessScenarios : Spek({ + given("one healthy device") { + on("execution of one test") { + it("should pass") { + val marathon = setupMarathon { + val test = Test("test", "SimpleTest", "test", emptySet()) + val device = StubDevice() + + configuration = Mocks.Configuration.DEFAULT + tests = listOf(test) + provideDevices { + delay(1000) + it.send(DeviceProvider.DeviceEvent.DeviceConnected(device)) + } + device.executionResults = mapOf( + test to arrayOf(TestStatus.PASSED) + ) + } + + marathon.run() + } + } + } +}) + diff --git a/execution-timeline/build.gradle.kts b/execution-timeline/build.gradle.kts index 9b1d14884..3fc67f8fd 100644 --- a/execution-timeline/build.gradle.kts +++ b/execution-timeline/build.gradle.kts @@ -25,11 +25,9 @@ dependencies { Deployment.initialize(project) -val compileKotlin by tasks.getting(KotlinCompile::class) { - kotlinOptions.jvmTarget = "1.8" -} -val compileTestKotlin by tasks.getting(KotlinCompile::class) { +tasks.withType { kotlinOptions.jvmTarget = "1.8" + kotlinOptions.apiVersion = "1.3" } junitPlatform { diff --git a/marathon-html-report/build.gradle.kts b/marathon-html-report/build.gradle.kts index 9b1d14884..3fc67f8fd 100644 --- a/marathon-html-report/build.gradle.kts +++ b/marathon-html-report/build.gradle.kts @@ -25,11 +25,9 @@ dependencies { Deployment.initialize(project) -val compileKotlin by tasks.getting(KotlinCompile::class) { - kotlinOptions.jvmTarget = "1.8" -} -val compileTestKotlin by tasks.getting(KotlinCompile::class) { +tasks.withType { kotlinOptions.jvmTarget = "1.8" + kotlinOptions.apiVersion = "1.3" } junitPlatform { diff --git a/settings.gradle.kts b/settings.gradle.kts index 46d7a36b8..b7dd69fb0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,6 +17,7 @@ rootProject.name = "marathon" include("core") include("vendor-android") include("vendor-ios") +include("vendor-test") include("marathon-gradle-plugin") include("marathon-html-report") include("execution-timeline") diff --git a/vendor-android/build.gradle.kts b/vendor-android/build.gradle.kts index 8828dfa4e..e8a21476b 100644 --- a/vendor-android/build.gradle.kts +++ b/vendor-android/build.gradle.kts @@ -29,11 +29,9 @@ dependencies { Deployment.initialize(project) -val compileKotlin by tasks.getting(KotlinCompile::class) { - kotlinOptions.jvmTarget = "1.8" -} -val compileTestKotlin by tasks.getting(KotlinCompile::class) { +tasks.withType { kotlinOptions.jvmTarget = "1.8" + kotlinOptions.apiVersion = "1.3" } junitPlatform { diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/AndroidDeviceTestRunner.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/AndroidDeviceTestRunner.kt index b87d8b35e..b4e96a606 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/AndroidDeviceTestRunner.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/AndroidDeviceTestRunner.kt @@ -17,6 +17,7 @@ import com.malinskiy.marathon.android.executor.listeners.TestRunResultsListener import com.malinskiy.marathon.android.executor.listeners.screenshot.ScreenCapturerTestRunListener import com.malinskiy.marathon.device.DeviceFeature import com.malinskiy.marathon.device.DevicePoolId +import com.malinskiy.marathon.exceptions.DeviceLostException import com.malinskiy.marathon.exceptions.TestBatchExecutionException import com.malinskiy.marathon.execution.Configuration import com.malinskiy.marathon.execution.TestBatchResults @@ -81,7 +82,11 @@ class AndroidDeviceTestRunner(private val device: AndroidDevice) { throw TestBatchExecutionException(e) } catch (e: AdbCommandRejectedException) { logger.error(e) { "adb error while running tests ${testBatch.tests.map { it.toTestName() }}" } - throw TestBatchExecutionException(e) + if (e.isDeviceOffline) { + throw DeviceLostException(e) + } else { + throw TestBatchExecutionException(e) + } } catch (e: IOException) { logger.error(e) { "Error while running tests ${testBatch.tests.map { it.toTestName() }}" } throw TestBatchExecutionException(e) diff --git a/vendor-ios/build.gradle.kts b/vendor-ios/build.gradle.kts index c74f51609..0e5c13896 100644 --- a/vendor-ios/build.gradle.kts +++ b/vendor-ios/build.gradle.kts @@ -36,6 +36,7 @@ Deployment.initialize(project) tasks.withType { kotlinOptions.jvmTarget = "1.8" + kotlinOptions.apiVersion = "1.3" } junitPlatform { diff --git a/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/Mocks.kt b/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/Mocks.kt index f5d15ede5..da25c80b0 100644 --- a/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/Mocks.kt +++ b/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/Mocks.kt @@ -1,13 +1,21 @@ package com.malinskiy.marathon.ios +import com.google.common.io.Files import com.google.gson.GsonBuilder import com.malinskiy.marathon.device.DevicePoolId +import com.malinskiy.marathon.device.DeviceProvider +import com.malinskiy.marathon.execution.AnalyticsConfiguration +import com.malinskiy.marathon.execution.Configuration +import com.malinskiy.marathon.execution.TestParser +import com.malinskiy.marathon.execution.strategy.impl.batching.IsolateBatchingStrategy import com.malinskiy.marathon.ios.cmd.remote.CommandExecutor import com.malinskiy.marathon.ios.cmd.remote.CommandResult import com.malinskiy.marathon.ios.simctl.model.SimctlDeviceList import com.malinskiy.marathon.ios.simctl.model.SimctlDeviceListDeserializer +import com.malinskiy.marathon.vendor.VendorConfiguration import net.schmizz.sshj.connection.channel.direct.Session import org.amshove.kluent.mock +import java.io.File class Mocks { class CommandExecutor { @@ -18,6 +26,7 @@ class Mocks { override fun exec(command: String, timeout: Long): CommandResult { TODO("not implemented") } + override fun disconnect() { TODO("not implemented") } diff --git a/vendor-test/build.gradle.kts b/vendor-test/build.gradle.kts new file mode 100644 index 000000000..869b3efc8 --- /dev/null +++ b/vendor-test/build.gradle.kts @@ -0,0 +1,26 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.jetbrains.kotlin.gradle.dsl.Coroutines +import org.junit.platform.gradle.plugin.EnginesExtension +import org.junit.platform.gradle.plugin.FiltersExtension +import org.junit.platform.gradle.plugin.JUnitPlatformExtension + +plugins { + `java-library` + id("org.jetbrains.kotlin.jvm") +} + +kotlin.experimental.coroutines = Coroutines.ENABLE + +dependencies { + implementation(Libraries.kotlinStdLib) + implementation(Libraries.kotlinCoroutines) + implementation(Libraries.kotlinLogging) + implementation(TestLibraries.spekAPI) + implementation(TestLibraries.kluent) + implementation(project(":core")) +} + +tasks.withType { + kotlinOptions.jvmTarget = "1.8" + kotlinOptions.apiVersion = "1.3" +} \ No newline at end of file diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/MarathonFactory.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/MarathonFactory.kt new file mode 100644 index 000000000..ccd9d86ca --- /dev/null +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/MarathonFactory.kt @@ -0,0 +1,26 @@ +package com.malinskiy.marathon.test + +import com.malinskiy.marathon.Marathon +import com.malinskiy.marathon.device.DeviceProvider +import com.malinskiy.marathon.execution.Configuration +import kotlinx.coroutines.experimental.channels.Channel +import org.amshove.kluent.When +import org.amshove.kluent.`it returns` +import org.amshove.kluent.calling + +class MarathonFactory { + var configuration: Configuration = Mocks.Configuration.DEFAULT + var tests: List + set(value) { + val testParser = configuration.vendorConfiguration.testParser()!! + When calling testParser.extract(configuration) `it returns` (value) + } + get() = TODO("Not implemented") + + fun provideDevices(f: suspend (Channel) -> Unit) { + val stubDeviceProvider = configuration.vendorConfiguration.deviceProvider() as StubDeviceProvider + stubDeviceProvider.providingLogic = f + } + + fun build() = Marathon(configuration) +} \ No newline at end of file diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/Mocks.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/Mocks.kt new file mode 100644 index 000000000..e95d20f0a --- /dev/null +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/Mocks.kt @@ -0,0 +1,48 @@ +package com.malinskiy.marathon.test + +import com.malinskiy.marathon.vendor.VendorConfiguration +import org.amshove.kluent.mock +import java.nio.file.Files + +class Mocks { + class TestParser { + companion object { + val DEFAULT = mock(com.malinskiy.marathon.execution.TestParser::class) + } + } + + class DeviceProvider { + companion object { + val DEFAULT = StubDeviceProvider() + } + } + + class Configuration { + companion object { + val DEFAULT = com.malinskiy.marathon.execution.Configuration( + name = "DEFAULT_TEST_CONFIG", + outputDir = Files.createTempDirectory("test-run").toFile(), + vendorConfiguration = object : VendorConfiguration { + override fun testParser(): com.malinskiy.marathon.execution.TestParser? = TestParser.DEFAULT + override fun deviceProvider(): com.malinskiy.marathon.device.DeviceProvider? = DeviceProvider.DEFAULT + }, + debug = null, + batchingStrategy = null, + analyticsConfiguration = null, + excludeSerialRegexes = null, + fallbackToScreenshots = null, + filteringConfiguration = null, + flakinessStrategy = null, + ignoreFailures = null, + includeSerialRegexes = null, + isCodeCoverageEnabled = null, + poolingStrategy = null, + retryStrategy = null, + shardingStrategy = null, + sortingStrategy = null, + testClassRegexes = null, + testOutputTimeoutMillis = null + ) + } + } +} \ No newline at end of file diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt new file mode 100644 index 000000000..c68e91ea2 --- /dev/null +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt @@ -0,0 +1,50 @@ +package com.malinskiy.marathon.test + +import com.malinskiy.marathon.device.Device +import com.malinskiy.marathon.device.DeviceFeature +import com.malinskiy.marathon.device.DevicePoolId +import com.malinskiy.marathon.device.NetworkState +import com.malinskiy.marathon.device.OperatingSystem +import com.malinskiy.marathon.device.toDeviceInfo +import com.malinskiy.marathon.execution.Configuration +import com.malinskiy.marathon.execution.TestBatchResults +import com.malinskiy.marathon.execution.TestResult +import com.malinskiy.marathon.execution.TestStatus +import com.malinskiy.marathon.execution.progress.ProgressReporter +import kotlinx.coroutines.experimental.CompletableDeferred + +class StubDevice(override val operatingSystem: OperatingSystem = OperatingSystem("25"), + override val model: String = "test", + override val manufacturer: String = "test", + override val networkState: NetworkState = NetworkState.CONNECTED, + override val deviceFeatures: Collection = listOf(), + override val abi: String = "test", + override val serialNumber: String = "serial-1", + override val healthy: Boolean = true) : Device { + + lateinit var executionResults: Map> + var executionIndexMap: MutableMap = mutableMapOf() + + override fun execute(configuration: Configuration, devicePoolId: DevicePoolId, testBatch: TestBatch, deferred: CompletableDeferred, progressReporter: ProgressReporter) { + val results = testBatch.tests.map { + val i = executionIndexMap.getOrDefault(it, 0) + val result = executionResults[it]!![i] + executionIndexMap[it] = i + 1 + TestResult(it, toDeviceInfo(), result, 0, 1, null) + } + + deferred.complete( + TestBatchResults(this, + results.filter { it.isSuccess }, + results.filter { !it.isSuccess }, + emptySet() + ) + ) + } + + override fun prepare(configuration: Configuration) { + } + + override fun dispose() { + } +} \ No newline at end of file diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt new file mode 100644 index 000000000..c7b87dbc8 --- /dev/null +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt @@ -0,0 +1,37 @@ +package com.malinskiy.marathon.test + +import com.malinskiy.marathon.actor.unboundedChannel +import com.malinskiy.marathon.device.Device +import com.malinskiy.marathon.device.DeviceProvider +import com.malinskiy.marathon.vendor.VendorConfiguration +import kotlinx.coroutines.experimental.channels.Channel +import kotlinx.coroutines.experimental.launch + +class StubDeviceProvider : DeviceProvider { + private val channel: Channel = unboundedChannel() + var providingLogic: (suspend (Channel) -> Unit)? = null + + override fun initialize(vendorConfiguration: VendorConfiguration) { + } + + override fun subscribe(): Channel { + providingLogic?.let { + launch { + providingLogic?.invoke(channel) + } + } + + return channel + } + + override fun lockDevice(device: Device): Boolean { + TODO("Not implemented") + } + + override fun unlockDevice(device: Device): Boolean { + TODO("Not implemented") + } + + override fun terminate() { + } +} \ No newline at end of file diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubExecutionResults.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubExecutionResults.kt new file mode 100644 index 000000000..fea9ab00c --- /dev/null +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubExecutionResults.kt @@ -0,0 +1,4 @@ +package com.malinskiy.marathon.test + +class StubExecutionResults() { +} \ No newline at end of file diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/TestBodyExtensions.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/TestBodyExtensions.kt new file mode 100644 index 000000000..5d24612bb --- /dev/null +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/TestBodyExtensions.kt @@ -0,0 +1,9 @@ +package com.malinskiy.marathon.test + +import com.malinskiy.marathon.Marathon +import org.jetbrains.spek.api.dsl.TestBody + +fun TestBody.setupMarathon(f: MarathonFactory.() -> Unit): Marathon { + val marathonFactory = MarathonFactory() + return marathonFactory.apply(f).build() +} \ No newline at end of file From 17ea8090dea4c2baf1fe571b457366f9942868f7 Mon Sep 17 00:00:00 2001 From: Anton Malinskiy Date: Mon, 26 Nov 2018 21:41:38 +0700 Subject: [PATCH 05/14] Force kotlin API 1.2 --- cli/build.gradle.kts | 2 +- core/build.gradle.kts | 2 +- .../kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt | 2 -- execution-timeline/build.gradle.kts | 2 +- marathon-html-report/build.gradle.kts | 2 +- vendor-android/build.gradle.kts | 2 +- vendor-ios/build.gradle.kts | 2 +- vendor-test/build.gradle.kts | 2 +- 8 files changed, 7 insertions(+), 9 deletions(-) diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index ebe679136..b816d5b67 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -51,7 +51,7 @@ Deployment.initialize(project) tasks.withType { kotlinOptions.jvmTarget = "1.8" - kotlinOptions.apiVersion = "1.3" + kotlinOptions.apiVersion = "1.2" } buildConfig { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index f5087f56f..ffcfe0ff7 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -74,7 +74,7 @@ Deployment.initialize(project) tasks.withType { kotlinOptions.jvmTarget = "1.8" - kotlinOptions.apiVersion = "1.3" + kotlinOptions.apiVersion = "1.2" } junitPlatform { diff --git a/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt b/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt index ad9b516f1..b1a80f3c7 100644 --- a/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt +++ b/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt @@ -2,7 +2,6 @@ package com.malinskiy.marathon.scenario import com.malinskiy.marathon.device.DeviceProvider import com.malinskiy.marathon.execution.TestStatus -import com.malinskiy.marathon.execution.queue.TestState import com.malinskiy.marathon.test.Mocks import com.malinskiy.marathon.test.StubDevice import com.malinskiy.marathon.test.Test @@ -37,4 +36,3 @@ class SuccessScenarios : Spek({ } } }) - diff --git a/execution-timeline/build.gradle.kts b/execution-timeline/build.gradle.kts index 3fc67f8fd..150294fca 100644 --- a/execution-timeline/build.gradle.kts +++ b/execution-timeline/build.gradle.kts @@ -27,7 +27,7 @@ Deployment.initialize(project) tasks.withType { kotlinOptions.jvmTarget = "1.8" - kotlinOptions.apiVersion = "1.3" + kotlinOptions.apiVersion = "1.2" } junitPlatform { diff --git a/marathon-html-report/build.gradle.kts b/marathon-html-report/build.gradle.kts index 3fc67f8fd..150294fca 100644 --- a/marathon-html-report/build.gradle.kts +++ b/marathon-html-report/build.gradle.kts @@ -27,7 +27,7 @@ Deployment.initialize(project) tasks.withType { kotlinOptions.jvmTarget = "1.8" - kotlinOptions.apiVersion = "1.3" + kotlinOptions.apiVersion = "1.2" } junitPlatform { diff --git a/vendor-android/build.gradle.kts b/vendor-android/build.gradle.kts index e8a21476b..949bd7423 100644 --- a/vendor-android/build.gradle.kts +++ b/vendor-android/build.gradle.kts @@ -31,7 +31,7 @@ Deployment.initialize(project) tasks.withType { kotlinOptions.jvmTarget = "1.8" - kotlinOptions.apiVersion = "1.3" + kotlinOptions.apiVersion = "1.2" } junitPlatform { diff --git a/vendor-ios/build.gradle.kts b/vendor-ios/build.gradle.kts index 0e5c13896..ef8d0aa79 100644 --- a/vendor-ios/build.gradle.kts +++ b/vendor-ios/build.gradle.kts @@ -36,7 +36,7 @@ Deployment.initialize(project) tasks.withType { kotlinOptions.jvmTarget = "1.8" - kotlinOptions.apiVersion = "1.3" + kotlinOptions.apiVersion = "1.2" } junitPlatform { diff --git a/vendor-test/build.gradle.kts b/vendor-test/build.gradle.kts index 869b3efc8..8296ef8bd 100644 --- a/vendor-test/build.gradle.kts +++ b/vendor-test/build.gradle.kts @@ -22,5 +22,5 @@ dependencies { tasks.withType { kotlinOptions.jvmTarget = "1.8" - kotlinOptions.apiVersion = "1.3" + kotlinOptions.apiVersion = "1.2" } \ No newline at end of file From 89e1c2283c6c4d931b44ead9d3798a2fdc2153a2 Mon Sep 17 00:00:00 2001 From: Anton Malinskiy Date: Tue, 27 Nov 2018 09:57:54 +0700 Subject: [PATCH 06/14] Use suspend for verifyBooted in Android --- .../malinskiy/marathon/android/AndroidDeviceProvider.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt index 01faefe15..2309e6079 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt @@ -14,6 +14,7 @@ import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.vendor.VendorConfiguration import kotlinx.coroutines.experimental.NonCancellable.isActive import kotlinx.coroutines.experimental.channels.Channel +import kotlinx.coroutines.experimental.delay import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.newFixedThreadPoolContext import java.nio.file.Paths @@ -87,11 +88,11 @@ class AndroidDeviceProvider : DeviceProvider { } } - private fun verifyBooted(device: AndroidDevice) { + private suspend fun verifyBooted(device: AndroidDevice) { if (!waitForBoot(device)) throw TimeoutException("Timeout waiting for device ${device.serialNumber} to boot") } - private fun waitForBoot(device: AndroidDevice): Boolean { + private suspend fun waitForBoot(device: AndroidDevice): Boolean { var booted = false for (i in 1..30) { if (device.booted) { @@ -99,7 +100,7 @@ class AndroidDeviceProvider : DeviceProvider { booted = true break } else { - Thread.sleep(1000) + delay(1000) logger.debug { "Device ${device.serialNumber} is still booting..." } } From 320254ce4a0f104bdf8ebd8a1bbed3bc09a92f56 Mon Sep 17 00:00:00 2001 From: Anton Malinskiy Date: Wed, 28 Nov 2018 14:00:51 +0700 Subject: [PATCH 07/14] Rework code for testing --- .../kotlin/com/malinskiy/marathon/Marathon.kt | 18 ++++- .../com/malinskiy/marathon/actor/Actor.kt | 11 +-- .../marathon/device/DeviceProvider.kt | 2 - .../marathon/execution/DevicePoolActor.kt | 9 ++- .../malinskiy/marathon/execution/Scheduler.kt | 16 +++-- .../marathon/execution/device/DeviceActor.kt | 35 +++++----- .../marathon/execution/queue/QueueActor.kt | 5 +- .../marathon/scenario/SuccessScenarios.kt | 39 +++++++++-- .../output/raw/success_scenario_1.json | 1 + .../marathon/android/AndroidDeviceProvider.kt | 17 ++--- .../marathon/ios/IOSDeviceProvider.kt | 8 --- .../marathon/test/MarathonFactory.kt | 26 ------- .../com/malinskiy/marathon/test/Mocks.kt | 37 ---------- .../com/malinskiy/marathon/test/StubDevice.kt | 5 ++ .../marathon/test/StubDeviceProvider.kt | 17 ++--- .../marathon/test/TestBodyExtensions.kt | 1 + .../marathon/test/TestVendorConfiguration.kt | 9 +++ .../marathon/test/assert/FileExtensions.kt | 6 ++ .../test/factory/ConfigurationFactory.kt | 68 +++++++++++++++++++ .../marathon/test/factory/MarathonFactory.kt | 11 +++ 20 files changed, 204 insertions(+), 137 deletions(-) create mode 100644 core/src/test/resources/output/raw/success_scenario_1.json delete mode 100644 vendor-test/src/main/kotlin/com/malinskiy/marathon/test/MarathonFactory.kt create mode 100644 vendor-test/src/main/kotlin/com/malinskiy/marathon/test/TestVendorConfiguration.kt create mode 100644 vendor-test/src/main/kotlin/com/malinskiy/marathon/test/assert/FileExtensions.kt create mode 100644 vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/ConfigurationFactory.kt create mode 100644 vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/MarathonFactory.kt diff --git a/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt b/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt index 2c9adb902..ce269a65c 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt @@ -20,9 +20,14 @@ import com.malinskiy.marathon.report.internal.TestResultReporter import com.malinskiy.marathon.test.Test import com.malinskiy.marathon.test.toTestName import com.malinskiy.marathon.vendor.VendorConfiguration +import kotlinx.coroutines.experimental.IO +import kotlinx.coroutines.experimental.delay +import kotlinx.coroutines.experimental.newFixedThreadPoolContext import kotlinx.coroutines.experimental.runBlocking +import kotlinx.coroutines.experimental.withContext import java.util.ServiceLoader import java.util.concurrent.TimeUnit +import kotlin.coroutines.experimental.coroutineContext import kotlin.system.measureTimeMillis private val log = MarathonLogging.logger {} @@ -68,7 +73,12 @@ class Marathon(val configuration: Configuration) { return loader.first() } - fun run(): Boolean = runBlocking { + fun run() = runBlocking { + runAsync() + log.debug { "Return from runAsync" } + } + + suspend fun runAsync() { MarathonLogging.debug = configuration.debug val testParser = loadTestParser(configuration.vendorConfiguration) @@ -81,7 +91,8 @@ class Marathon(val configuration: Configuration) { log.info("Scheduling ${tests.size} tests") log.debug(tests.map { it.toTestName() }.joinToString(", ")) val progressReporter = ProgressReporter() - val scheduler = Scheduler(deviceProvider, analytics, configuration, tests, progressReporter) + val currentCoroutineContext = coroutineContext + val scheduler = Scheduler(deviceProvider, analytics, configuration, tests, progressReporter, currentCoroutineContext) if (configuration.outputDir.exists()) { log.info { "Output ${configuration.outputDir} already exists" } @@ -108,7 +119,8 @@ class Marathon(val configuration: Configuration) { analytics.terminate() analytics.close() deviceProvider.terminate() - progressReporter.aggregateResult() + val result = progressReporter.aggregateResult() + log.debug { "Result is $result" } } private fun applyTestFilters(parsedTests: List): List { diff --git a/core/src/main/kotlin/com/malinskiy/marathon/actor/Actor.kt b/core/src/main/kotlin/com/malinskiy/marathon/actor/Actor.kt index 547136961..134184e1d 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/actor/Actor.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/actor/Actor.kt @@ -5,14 +5,17 @@ import kotlinx.coroutines.experimental.channels.Channel import kotlinx.coroutines.experimental.channels.SendChannel import kotlinx.coroutines.experimental.channels.actor import kotlinx.coroutines.experimental.selects.SelectClause2 +import kotlin.coroutines.experimental.CoroutineContext -abstract class Actor(parent: Job? = null) : SendChannel { - +abstract class Actor(parent: Job? = null, + context: CoroutineContext) : SendChannel { protected abstract suspend fun receive(msg: T) + private val actorJob = Job(parent) private val delegate = actor( capacity = Channel.UNLIMITED, - parent = parent + parent = actorJob, + context = context ) { for (msg in channel) { receive(msg) @@ -30,7 +33,7 @@ abstract class Actor(parent: Job? = null) : SendChannel { delegate.invokeOnClose(handler) } - override fun close(cause: Throwable?): Boolean = delegate.close(cause) + override fun close(cause: Throwable?): Boolean = actorJob.cancel() override fun offer(element: T): Boolean = delegate.offer(element) diff --git a/core/src/main/kotlin/com/malinskiy/marathon/device/DeviceProvider.kt b/core/src/main/kotlin/com/malinskiy/marathon/device/DeviceProvider.kt index 4f438d58f..9b06cf1f5 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/device/DeviceProvider.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/device/DeviceProvider.kt @@ -11,7 +11,5 @@ interface DeviceProvider { fun initialize(vendorConfiguration: VendorConfiguration) fun subscribe() : Channel - fun lockDevice(device: Device) : Boolean - fun unlockDevice(device: Device) : Boolean fun terminate() } diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt index 759aa1f28..674880b6d 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt @@ -14,13 +14,16 @@ import com.malinskiy.marathon.test.Test import com.malinskiy.marathon.test.TestBatch import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.channels.SendChannel +import kotlin.coroutines.experimental.CoroutineContext class DevicePoolActor(private val poolId: DevicePoolId, private val configuration: Configuration, analytics: Analytics, tests: Collection, private val progressReporter: ProgressReporter, - parent: Job) : Actor(parent = parent) { + parent: Job, + private val context: CoroutineContext) : + Actor(parent = parent, context = context) { private val logger = MarathonLogging.logger("DevicePoolActor[${poolId.name}]") @@ -44,7 +47,7 @@ class DevicePoolActor(private val poolId: DevicePoolId, private val flakinessShard = configuration.flakinessStrategy private val shard = flakinessShard.process(shardingStrategy.createShard(tests), analytics) - private val queue: QueueActor = QueueActor(configuration, shard, analytics, this, poolId, progressReporter, poolJob) + private val queue: QueueActor = QueueActor(configuration, shard, analytics, this, poolId, progressReporter, poolJob, context) private val devices = mutableMapOf>() @@ -111,7 +114,7 @@ class DevicePoolActor(private val poolId: DevicePoolId, } logger.debug { "add device ${device.serialNumber}" } - val actor = DeviceActor(poolId, this, configuration, device, progressReporter, poolJob) + val actor = DeviceActor(poolId, this, configuration, device, progressReporter, poolJob, context) devices[device.serialNumber] = actor actor.send(DeviceEvent.Initialize) } diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt index 32962e807..06d34267e 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.withTimeout import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit +import kotlin.coroutines.experimental.CoroutineContext /** * The logic of scheduler: @@ -30,7 +31,8 @@ class Scheduler(private val deviceProvider: DeviceProvider, private val analytics: Analytics, private val configuration: Configuration, private val tests: Collection, - private val progressReporter: ProgressReporter) { + private val progressReporter: ProgressReporter, + private val coroutineContext: CoroutineContext) { private val pools = ConcurrentHashMap>() private val poolingStrategy = configuration.poolingStrategy @@ -56,12 +58,12 @@ class Scheduler(private val deviceProvider: DeviceProvider, return pools.keys.toList() } - private fun subscribeOnDevices(job: Job) { - launch { + private fun subscribeOnDevices(job: Job): Job { + return launch(coroutineContext) { for (msg in deviceProvider.subscribe()) { when (msg) { is DeviceProvider.DeviceEvent.DeviceConnected -> { - onDeviceConnected(msg, job) + onDeviceConnected(msg, job, coroutineContext) } is DeviceProvider.DeviceEvent.DeviceDisconnected -> { onDeviceDisconnected(msg) @@ -78,13 +80,15 @@ class Scheduler(private val deviceProvider: DeviceProvider, } } - private suspend fun onDeviceConnected(item: DeviceProvider.DeviceEvent.DeviceConnected, parent: Job) { + private suspend fun onDeviceConnected(item: DeviceProvider.DeviceEvent.DeviceConnected, + parent: Job, + context: CoroutineContext) { val device = item.device val poolId = poolingStrategy.associate(device) logger.debug { "device ${device.serialNumber} associated with poolId ${poolId.name}" } pools.computeIfAbsent(poolId) { id -> logger.debug { "pool actor ${id.name} is being created" } - DevicePoolActor(id, configuration, analytics, tests, progressReporter, parent) + DevicePoolActor(id, configuration, analytics, tests, progressReporter, parent, context) } pools[poolId]?.send(AddDevice(device)) ?: logger.debug { "not sending the AddDevice event " + "to device pool for ${device.serialNumber}" } diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt index bed46b684..f7a5fb33a 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.async import kotlinx.coroutines.experimental.channels.SendChannel import kotlinx.coroutines.experimental.launch -import kotlinx.coroutines.experimental.newSingleThreadContext +import kotlin.coroutines.experimental.CoroutineContext import kotlin.properties.Delegates class DeviceActor(private val devicePoolId: DevicePoolId, @@ -27,7 +27,9 @@ class DeviceActor(private val devicePoolId: DevicePoolId, private val configuration: Configuration, private val device: Device, private val progressReporter: ProgressReporter, - parent: Job) : Actor(parent = parent) { + parent: Job, + private val coroutineContext: CoroutineContext) : + Actor(parent = parent, context = coroutineContext) { private val deviceJob = Job(parent) @@ -128,7 +130,7 @@ class DeviceActor(private val devicePoolId: DevicePoolId, } private fun requestNextBatch(result: CompletableDeferred?) { - launch(parent = deviceJob) { + launch(parent = deviceJob, context = coroutineContext) { if (result != null) { val testResults = result.await() pool.send(DevicePoolMessage.FromDevice.CompletedTestBatch(device, testResults)) @@ -138,8 +140,6 @@ class DeviceActor(private val devicePoolId: DevicePoolId, } } - private val context = newSingleThreadContext(device.toString()) - private var job by Delegates.observable(null) { _, _, newValue -> newValue?.invokeOnCompletion { if (it == null) { @@ -155,28 +155,28 @@ class DeviceActor(private val devicePoolId: DevicePoolId, private fun initialize() { logger.debug { "initialize ${device.serialNumber}" } - job = async(context, parent = deviceJob) { - withRetry(30, 10000) { - if(!isActive) return@async - try { - device.prepare(configuration) - } catch (e: Exception) { - logger.debug { "device ${device.serialNumber} initialization failed. Retrying" } - throw e + job = launch(context = coroutineContext, parent = deviceJob) { +// withRetry(30, 10000) { + if(isActive) { + try { + device.prepare(configuration) + } catch (e: Exception) { + logger.debug { "device ${device.serialNumber} initialization failed. Retrying" } + throw e + } } - } +// } } } private fun executeBatch(batch: TestBatch, result: CompletableDeferred) { logger.debug { "executeBatch ${device.serialNumber}" } - job = async(context, parent = deviceJob) { + job = async(coroutineContext, parent = deviceJob) { try { device.execute(configuration, devicePoolId, batch, result, progressReporter) } catch (e: DeviceLostException) { logger.error(e) { "Critical error during execution" } returnBatch(batch) -// terminate() } catch (e: TestBatchExecutionException) { returnBatch(batch) } @@ -184,7 +184,7 @@ class DeviceActor(private val devicePoolId: DevicePoolId, } private fun returnBatch(batch: TestBatch): Job { - return launch(parent = deviceJob) { + return launch(parent = deviceJob, context = coroutineContext) { pool.send(DevicePoolMessage.FromDevice.ReturnTestBatch(device, batch)) } } @@ -192,7 +192,6 @@ class DeviceActor(private val devicePoolId: DevicePoolId, private fun terminate() { logger.debug { "terminate ${device.serialNumber}" } job?.cancel() - context.close() deviceJob.cancel() close() } diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/queue/QueueActor.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/queue/QueueActor.kt index 1540fe526..70de079e9 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/queue/QueueActor.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/queue/QueueActor.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.experimental.CompletableDeferred import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.channels.SendChannel import java.util.* +import kotlin.coroutines.experimental.CoroutineContext class QueueActor(configuration: Configuration, private val testShard: TestShard, @@ -25,7 +26,9 @@ class QueueActor(configuration: Configuration, private val pool: SendChannel, private val poolId: DevicePoolId, private val progressReporter: ProgressReporter, - poolJob: Job) : Actor(parent = poolJob) { + poolJob: Job, + private val coroutineContext: CoroutineContext) : + Actor(parent = poolJob, context = coroutineContext) { private val logger = MarathonLogging.logger("QueueActor[$poolId]") diff --git a/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt b/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt index b1a80f3c7..5e8610e9b 100644 --- a/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt +++ b/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt @@ -2,36 +2,61 @@ package com.malinskiy.marathon.scenario import com.malinskiy.marathon.device.DeviceProvider import com.malinskiy.marathon.execution.TestStatus -import com.malinskiy.marathon.test.Mocks import com.malinskiy.marathon.test.StubDevice import com.malinskiy.marathon.test.Test +import com.malinskiy.marathon.test.assert.shouldBeEqualTo import com.malinskiy.marathon.test.setupMarathon +import kotlinx.coroutines.experimental.CoroutineExceptionHandler import kotlinx.coroutines.experimental.delay +import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.runBlocking +import kotlinx.coroutines.experimental.test.TestCoroutineContext import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it import org.jetbrains.spek.api.dsl.on +import java.io.File +import java.util.concurrent.TimeUnit class SuccessScenarios : Spek({ given("one healthy device") { on("execution of one test") { it("should pass") { + var output: File? = null + val context = TestCoroutineContext("testing context") + val marathon = setupMarathon { val test = Test("test", "SimpleTest", "test", emptySet()) val device = StubDevice() - configuration = Mocks.Configuration.DEFAULT - tests = listOf(test) - provideDevices { - delay(1000) - it.send(DeviceProvider.DeviceEvent.DeviceConnected(device)) + configuration { + output = outputDir + + tests { + listOf(test) + } + + vendorConfiguration.deviceProvider.coroutineContext = context + + devices { + delay(1000) + it.send(DeviceProvider.DeviceEvent.DeviceConnected(device)) + } } + device.executionResults = mapOf( test to arrayOf(TestStatus.PASSED) ) } - marathon.run() + launch(context = context) { + marathon.runAsync() + } + + context.advanceTimeBy(2, TimeUnit.SECONDS) + + File(output!!.absolutePath + "/test_result", "raw.json") + .shouldBeEqualTo(File(javaClass.getResource("/output/raw/success_scenario_1.json").file)) } } } diff --git a/core/src/test/resources/output/raw/success_scenario_1.json b/core/src/test/resources/output/raw/success_scenario_1.json new file mode 100644 index 000000000..ed307085b --- /dev/null +++ b/core/src/test/resources/output/raw/success_scenario_1.json @@ -0,0 +1 @@ +[{"package":"test","class":"SimpleTest","method":"test","deviceSerial":"serial-1","ignored":false,"success":true,"timestamp":0,"duration":1}] \ No newline at end of file diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt index 2309e6079..4da44a853 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt @@ -5,7 +5,6 @@ import com.android.ddmlib.DdmPreferences import com.android.ddmlib.IDevice import com.android.ddmlib.TimeoutException import com.malinskiy.marathon.actor.unboundedChannel -import com.malinskiy.marathon.device.Device import com.malinskiy.marathon.device.DeviceProvider import com.malinskiy.marathon.device.DeviceProvider.DeviceEvent.DeviceConnected import com.malinskiy.marathon.device.DeviceProvider.DeviceEvent.DeviceDisconnected @@ -81,9 +80,11 @@ class AndroidDeviceProvider : DeviceProvider { override fun deviceDisconnected(device: IDevice?) { device?.let { - logger.debug { "Device ${device.serialNumber} disconnected" } - matchDdmsToDevice(it)?.let { - notifyDisconnected(it) + launch(context = bootWaitContext) { + logger.debug { "Device ${device.serialNumber} disconnected" } + matchDdmsToDevice(it)?.let { + notifyDisconnected(it) + } } } } @@ -159,15 +160,9 @@ class AndroidDeviceProvider : DeviceProvider { override fun terminate() { AndroidDebugBridge.disconnectBridge() AndroidDebugBridge.terminate() + bootWaitContext.close() } override fun subscribe() = channel - override fun lockDevice(device: Device): Boolean { - TODO("not implemented") - } - - override fun unlockDevice(device: Device): Boolean { - TODO("not implemented") - } } diff --git a/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt b/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt index 23b82de4f..a2f96628c 100644 --- a/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt +++ b/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt @@ -6,7 +6,6 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator import com.fasterxml.jackson.module.kotlin.KotlinModule import com.google.gson.GsonBuilder import com.malinskiy.marathon.actor.unboundedChannel -import com.malinskiy.marathon.device.Device import com.malinskiy.marathon.device.DeviceProvider import com.malinskiy.marathon.ios.device.LocalListSimulatorProvider import com.malinskiy.marathon.ios.device.SimulatorProvider @@ -49,11 +48,4 @@ class IOSDeviceProvider : DeviceProvider { private val channel: Channel = unboundedChannel() override fun subscribe() = channel - override fun lockDevice(device: Device): Boolean { - return false - } - - override fun unlockDevice(device: Device): Boolean { - return false - } } diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/MarathonFactory.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/MarathonFactory.kt deleted file mode 100644 index ccd9d86ca..000000000 --- a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/MarathonFactory.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.malinskiy.marathon.test - -import com.malinskiy.marathon.Marathon -import com.malinskiy.marathon.device.DeviceProvider -import com.malinskiy.marathon.execution.Configuration -import kotlinx.coroutines.experimental.channels.Channel -import org.amshove.kluent.When -import org.amshove.kluent.`it returns` -import org.amshove.kluent.calling - -class MarathonFactory { - var configuration: Configuration = Mocks.Configuration.DEFAULT - var tests: List - set(value) { - val testParser = configuration.vendorConfiguration.testParser()!! - When calling testParser.extract(configuration) `it returns` (value) - } - get() = TODO("Not implemented") - - fun provideDevices(f: suspend (Channel) -> Unit) { - val stubDeviceProvider = configuration.vendorConfiguration.deviceProvider() as StubDeviceProvider - stubDeviceProvider.providingLogic = f - } - - fun build() = Marathon(configuration) -} \ No newline at end of file diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/Mocks.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/Mocks.kt index e95d20f0a..cfd84cf20 100644 --- a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/Mocks.kt +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/Mocks.kt @@ -1,8 +1,6 @@ package com.malinskiy.marathon.test -import com.malinskiy.marathon.vendor.VendorConfiguration import org.amshove.kluent.mock -import java.nio.file.Files class Mocks { class TestParser { @@ -10,39 +8,4 @@ class Mocks { val DEFAULT = mock(com.malinskiy.marathon.execution.TestParser::class) } } - - class DeviceProvider { - companion object { - val DEFAULT = StubDeviceProvider() - } - } - - class Configuration { - companion object { - val DEFAULT = com.malinskiy.marathon.execution.Configuration( - name = "DEFAULT_TEST_CONFIG", - outputDir = Files.createTempDirectory("test-run").toFile(), - vendorConfiguration = object : VendorConfiguration { - override fun testParser(): com.malinskiy.marathon.execution.TestParser? = TestParser.DEFAULT - override fun deviceProvider(): com.malinskiy.marathon.device.DeviceProvider? = DeviceProvider.DEFAULT - }, - debug = null, - batchingStrategy = null, - analyticsConfiguration = null, - excludeSerialRegexes = null, - fallbackToScreenshots = null, - filteringConfiguration = null, - flakinessStrategy = null, - ignoreFailures = null, - includeSerialRegexes = null, - isCodeCoverageEnabled = null, - poolingStrategy = null, - retryStrategy = null, - shardingStrategy = null, - sortingStrategy = null, - testClassRegexes = null, - testOutputTimeoutMillis = null - ) - } - } } \ No newline at end of file diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt index c68e91ea2..fe4c0d530 100644 --- a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt @@ -11,6 +11,7 @@ import com.malinskiy.marathon.execution.TestBatchResults import com.malinskiy.marathon.execution.TestResult import com.malinskiy.marathon.execution.TestStatus import com.malinskiy.marathon.execution.progress.ProgressReporter +import com.malinskiy.marathon.log.MarathonLogging import kotlinx.coroutines.experimental.CompletableDeferred class StubDevice(override val operatingSystem: OperatingSystem = OperatingSystem("25"), @@ -22,6 +23,8 @@ class StubDevice(override val operatingSystem: OperatingSystem = OperatingSystem override val serialNumber: String = "serial-1", override val healthy: Boolean = true) : Device { + val logger = MarathonLogging.logger(StubDevice::class.java.simpleName) + lateinit var executionResults: Map> var executionIndexMap: MutableMap = mutableMapOf() @@ -43,8 +46,10 @@ class StubDevice(override val operatingSystem: OperatingSystem = OperatingSystem } override fun prepare(configuration: Configuration) { + logger.debug { "Preparing" } } override fun dispose() { + logger.debug { "Disposing" } } } \ No newline at end of file diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt index c7b87dbc8..1e7fe5d68 100644 --- a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt @@ -1,13 +1,15 @@ package com.malinskiy.marathon.test import com.malinskiy.marathon.actor.unboundedChannel -import com.malinskiy.marathon.device.Device import com.malinskiy.marathon.device.DeviceProvider import com.malinskiy.marathon.vendor.VendorConfiguration import kotlinx.coroutines.experimental.channels.Channel import kotlinx.coroutines.experimental.launch +import kotlin.coroutines.experimental.CoroutineContext + +class StubDeviceProvider() : DeviceProvider { + lateinit var coroutineContext: CoroutineContext -class StubDeviceProvider : DeviceProvider { private val channel: Channel = unboundedChannel() var providingLogic: (suspend (Channel) -> Unit)? = null @@ -16,7 +18,7 @@ class StubDeviceProvider : DeviceProvider { override fun subscribe(): Channel { providingLogic?.let { - launch { + launch(context = coroutineContext) { providingLogic?.invoke(channel) } } @@ -24,14 +26,7 @@ class StubDeviceProvider : DeviceProvider { return channel } - override fun lockDevice(device: Device): Boolean { - TODO("Not implemented") - } - - override fun unlockDevice(device: Device): Boolean { - TODO("Not implemented") - } - override fun terminate() { + channel.close() } } \ No newline at end of file diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/TestBodyExtensions.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/TestBodyExtensions.kt index 5d24612bb..683b7a713 100644 --- a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/TestBodyExtensions.kt +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/TestBodyExtensions.kt @@ -1,6 +1,7 @@ package com.malinskiy.marathon.test import com.malinskiy.marathon.Marathon +import com.malinskiy.marathon.test.factory.MarathonFactory import org.jetbrains.spek.api.dsl.TestBody fun TestBody.setupMarathon(f: MarathonFactory.() -> Unit): Marathon { diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/TestVendorConfiguration.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/TestVendorConfiguration.kt new file mode 100644 index 000000000..f407c3e06 --- /dev/null +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/TestVendorConfiguration.kt @@ -0,0 +1,9 @@ +package com.malinskiy.marathon.test + +import com.malinskiy.marathon.execution.TestParser +import com.malinskiy.marathon.vendor.VendorConfiguration + +class TestVendorConfiguration(var testParser: TestParser, var deviceProvider: StubDeviceProvider) : VendorConfiguration { + override fun testParser() = testParser + override fun deviceProvider() = deviceProvider +} \ No newline at end of file diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/assert/FileExtensions.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/assert/FileExtensions.kt new file mode 100644 index 000000000..d00cc38d5 --- /dev/null +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/assert/FileExtensions.kt @@ -0,0 +1,6 @@ +package com.malinskiy.marathon.test.assert + +import org.amshove.kluent.shouldBeEqualTo +import java.io.File + +fun File.shouldBeEqualTo(expected: File) = readText().shouldBeEqualTo(expected.readText()) \ No newline at end of file diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/ConfigurationFactory.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/ConfigurationFactory.kt new file mode 100644 index 000000000..2e5108d95 --- /dev/null +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/ConfigurationFactory.kt @@ -0,0 +1,68 @@ +package com.malinskiy.marathon.test.factory + +import com.malinskiy.marathon.device.DeviceProvider +import com.malinskiy.marathon.execution.Configuration +import com.malinskiy.marathon.test.Mocks +import com.malinskiy.marathon.test.StubDeviceProvider +import com.malinskiy.marathon.test.Test +import com.malinskiy.marathon.test.TestVendorConfiguration +import com.malinskiy.marathon.vendor.VendorConfiguration +import kotlinx.coroutines.experimental.channels.Channel +import org.amshove.kluent.When +import org.amshove.kluent.`it returns` +import org.amshove.kluent.any +import org.amshove.kluent.calling +import java.nio.file.Files + +class ConfigurationFactory { + var name = "DEFAULT_TEST_CONFIG" + var outputDir = Files.createTempDirectory("test-run").toFile() + var vendorConfiguration = TestVendorConfiguration(Mocks.TestParser.DEFAULT, StubDeviceProvider()) + var debug = null + var batchingStrategy = null + var analyticsConfiguration = null + var excludeSerialRegexes = null + var fallbackToScreenshots = null + var filteringConfiguration = null + var flakinessStrategy = null + var ignoreFailures = null + var includeSerialRegexes = null + var isCodeCoverageEnabled = null + var poolingStrategy = null + var retryStrategy = null + var shardingStrategy = null + var sortingStrategy = null + var testClassRegexes = null + var testOutputTimeoutMillis = null + + fun tests(block: () -> List) { + val testParser = vendorConfiguration.testParser()!! + When calling testParser.extract(any()) `it returns` (block.invoke()) + } + + fun devices(f: suspend (Channel) -> Unit) { + val stubDeviceProvider = vendorConfiguration.deviceProvider() as StubDeviceProvider + stubDeviceProvider.providingLogic = f + } + + fun build(): Configuration = + Configuration(name, + outputDir, + analyticsConfiguration, + poolingStrategy, + shardingStrategy, + sortingStrategy, + batchingStrategy, + flakinessStrategy, + retryStrategy, + filteringConfiguration, + ignoreFailures, + isCodeCoverageEnabled, + fallbackToScreenshots, + testClassRegexes, + includeSerialRegexes, + excludeSerialRegexes, + testOutputTimeoutMillis, + debug, + vendorConfiguration) +} \ No newline at end of file diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/MarathonFactory.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/MarathonFactory.kt new file mode 100644 index 000000000..f832061d8 --- /dev/null +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/MarathonFactory.kt @@ -0,0 +1,11 @@ +package com.malinskiy.marathon.test.factory + +import com.malinskiy.marathon.Marathon + +class MarathonFactory { + val configurationFactory: ConfigurationFactory = ConfigurationFactory() + + fun configuration(block: ConfigurationFactory.() -> Unit) = configurationFactory.apply(block) + + fun build() = Marathon(configurationFactory.build()) +} \ No newline at end of file From 3277cd59a2beb063149ed69b7e693a77e1ee527b Mon Sep 17 00:00:00 2001 From: Anton Malinskiy Date: Wed, 28 Nov 2018 16:54:56 +0700 Subject: [PATCH 08/14] Add suspend modifiers to execute and prepare methods Threading should be handled in the vendor module --- .../kotlin/com/malinskiy/marathon/Marathon.kt | 7 +- .../com/malinskiy/marathon/device/Device.kt | 4 +- .../com/malinskiy/marathon/execution/Retry.kt | 6 +- .../malinskiy/marathon/execution/Scheduler.kt | 6 +- .../marathon/execution/device/DeviceActor.kt | 4 +- .../malinskiy/marathon/device/DeviceStub.kt | 4 +- vendor-android/build.gradle.kts | 1 + .../marathon/android/AndroidDevice.kt | 35 +-- .../marathon/android/AndroidDeviceProvider.kt | 2 + .../android/executor/AndroidAppInstaller.kt | 6 +- .../android/AndroidDeviceProviderSpek.kt | 22 ++ .../com/malinskiy/marathon/ios/IOSDevice.kt | 211 +++++++++--------- .../marathon/ios/IOSDeviceProvider.kt | 1 + .../marathon/ios/IOSDeviceProviderSpek.kt | 22 ++ .../com/malinskiy/marathon/ios/Mocks.kt | 10 - .../com/malinskiy/marathon/test/StubDevice.kt | 4 +- 16 files changed, 201 insertions(+), 144 deletions(-) create mode 100644 vendor-android/src/test/kotlin/com/malinskiy/marathon/android/AndroidDeviceProviderSpek.kt create mode 100644 vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/IOSDeviceProviderSpek.kt diff --git a/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt b/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt index ce269a65c..c369371f5 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt @@ -22,6 +22,7 @@ import com.malinskiy.marathon.test.toTestName import com.malinskiy.marathon.vendor.VendorConfiguration import kotlinx.coroutines.experimental.IO import kotlinx.coroutines.experimental.delay +import kotlinx.coroutines.experimental.launch import kotlinx.coroutines.experimental.newFixedThreadPoolContext import kotlinx.coroutines.experimental.runBlocking import kotlinx.coroutines.experimental.withContext @@ -75,10 +76,9 @@ class Marathon(val configuration: Configuration) { fun run() = runBlocking { runAsync() - log.debug { "Return from runAsync" } } - suspend fun runAsync() { + suspend fun runAsync(): Boolean { MarathonLogging.debug = configuration.debug val testParser = loadTestParser(configuration.vendorConfiguration) @@ -119,8 +119,7 @@ class Marathon(val configuration: Configuration) { analytics.terminate() analytics.close() deviceProvider.terminate() - val result = progressReporter.aggregateResult() - log.debug { "Result is $result" } + return progressReporter.aggregateResult() } private fun applyTestFilters(parsedTests: List): List { diff --git a/core/src/main/kotlin/com/malinskiy/marathon/device/Device.kt b/core/src/main/kotlin/com/malinskiy/marathon/device/Device.kt index 44aca6f48..abc9e0fa9 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/device/Device.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/device/Device.kt @@ -16,13 +16,13 @@ interface Device { val healthy: Boolean val abi: String - fun execute(configuration: Configuration, + suspend fun execute(configuration: Configuration, devicePoolId: DevicePoolId, testBatch: TestBatch, deferred: CompletableDeferred, progressReporter: ProgressReporter) - fun prepare(configuration: Configuration) + suspend fun prepare(configuration: Configuration) fun dispose() } diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/Retry.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/Retry.kt index 8ba54ac4b..53a6d963d 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/Retry.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/Retry.kt @@ -1,7 +1,9 @@ package com.malinskiy.marathon.execution +import kotlinx.coroutines.experimental.delay + @Suppress("TooGenericExceptionCaught") -inline fun withRetry(attempts: Int, delay: Long = 0, f: () -> Unit) { +suspend fun withRetry(attempts: Int, delayTime: Long = 0, f: suspend () -> Unit) { var attempt = 1 while (true) { try { @@ -11,7 +13,7 @@ inline fun withRetry(attempts: Int, delay: Long = 0, f: () -> Unit) { if (attempt == attempts) { throw th } else { - Thread.sleep(delay) + delay(delayTime) } } ++attempt diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt index 06d34267e..f78376f09 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt @@ -90,8 +90,10 @@ class Scheduler(private val deviceProvider: DeviceProvider, logger.debug { "pool actor ${id.name} is being created" } DevicePoolActor(id, configuration, analytics, tests, progressReporter, parent, context) } - pools[poolId]?.send(AddDevice(device)) ?: logger.debug { "not sending the AddDevice event " + - "to device pool for ${device.serialNumber}" } + pools[poolId]?.send(AddDevice(device)) ?: logger.debug { + "not sending the AddDevice event " + + "to device pool for ${device.serialNumber}" + } analytics.trackDeviceConnected(poolId, device) } } diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt index f7a5fb33a..ee9179843 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt @@ -156,7 +156,7 @@ class DeviceActor(private val devicePoolId: DevicePoolId, private fun initialize() { logger.debug { "initialize ${device.serialNumber}" } job = launch(context = coroutineContext, parent = deviceJob) { -// withRetry(30, 10000) { + withRetry(30, 10000) { if(isActive) { try { device.prepare(configuration) @@ -165,7 +165,7 @@ class DeviceActor(private val devicePoolId: DevicePoolId, throw e } } -// } + } } } diff --git a/core/src/test/kotlin/com/malinskiy/marathon/device/DeviceStub.kt b/core/src/test/kotlin/com/malinskiy/marathon/device/DeviceStub.kt index f795496dd..b99b2bfe5 100644 --- a/core/src/test/kotlin/com/malinskiy/marathon/device/DeviceStub.kt +++ b/core/src/test/kotlin/com/malinskiy/marathon/device/DeviceStub.kt @@ -14,9 +14,9 @@ class DeviceStub(override var operatingSystem: OperatingSystem = OperatingSystem override val model: String = "model", override val manufacturer: String = "manufacturer", override val deviceFeatures: Collection = emptyList()) : Device { - override fun execute(configuration: Configuration, devicePoolId: DevicePoolId, testBatch: TestBatch, deferred: CompletableDeferred, progressReporter: ProgressReporter) {} + override suspend fun execute(configuration: Configuration, devicePoolId: DevicePoolId, testBatch: TestBatch, deferred: CompletableDeferred, progressReporter: ProgressReporter) {} - override fun prepare(configuration: Configuration) {} + override suspend fun prepare(configuration: Configuration) {} override fun dispose() {} } diff --git a/vendor-android/build.gradle.kts b/vendor-android/build.gradle.kts index 949bd7423..5ca9dec26 100644 --- a/vendor-android/build.gradle.kts +++ b/vendor-android/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(Libraries.jacksonAnnotations) implementation(Libraries.scalr) implementation(project(":core")) + testImplementation(project(":vendor-test")) testImplementation(TestLibraries.kluent) testImplementation(TestLibraries.spekAPI) testRuntime(TestLibraries.spekJUnitPlatformEngine) diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt index d6ca9269e..707c3e6ae 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt @@ -15,11 +15,14 @@ import com.malinskiy.marathon.execution.progress.ProgressReporter import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.test.TestBatch import kotlinx.coroutines.experimental.CompletableDeferred +import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.newFixedThreadPoolContext import java.util.UUID class AndroidDevice(val ddmsDevice: IDevice) : Device { val logger = MarathonLogging.logger(AndroidDevice::class.java.simpleName) + private val deviceContext = newFixedThreadPoolContext(1, ddmsDevice.serialNumber) override val abi: String by lazy { ddmsDevice.getProperty("ro.product.cpu.abi") ?: "Unknown" @@ -37,7 +40,6 @@ class AndroidDevice(val ddmsDevice: IDevice) : Device { ddmsDevice.getProperty("ro.product.manufacturer") ?: "Unknown" } - override val deviceFeatures: Collection get() { val videoSupport = ddmsDevice.supportsFeature(IDevice.Feature.SCREEN_RECORD) && @@ -92,22 +94,29 @@ class AndroidDevice(val ddmsDevice: IDevice) : Device { else -> false } - override fun execute(configuration: Configuration, - devicePoolId: DevicePoolId, - testBatch: TestBatch, - deferred: CompletableDeferred, - progressReporter: ProgressReporter) { - AndroidDeviceTestRunner(this@AndroidDevice).execute(configuration, devicePoolId, testBatch, deferred, progressReporter) + override suspend fun execute(configuration: Configuration, + devicePoolId: DevicePoolId, + testBatch: TestBatch, + deferred: CompletableDeferred, + progressReporter: ProgressReporter) { + + launch(deviceContext) { + AndroidDeviceTestRunner(this@AndroidDevice).execute(configuration, devicePoolId, testBatch, deferred, progressReporter) + } } - override fun prepare(configuration: Configuration) { - AndroidAppInstaller(configuration).prepareInstallation(this) - RemoteFileManager.removeRemoteDirectory(ddmsDevice) - RemoteFileManager.createRemoteDirectory(ddmsDevice) - clearLogcat(ddmsDevice) + override suspend fun prepare(configuration: Configuration) { + launch(deviceContext) { + AndroidAppInstaller(configuration).prepareInstallation(this@AndroidDevice) + RemoteFileManager.removeRemoteDirectory(ddmsDevice) + RemoteFileManager.createRemoteDirectory(ddmsDevice) + clearLogcat(ddmsDevice) + } } - override fun dispose() {} + override fun dispose() { + deviceContext.close() + } private fun clearLogcat(device: IDevice) { val logger = MarathonLogging.logger("AndroidDevice.clearLogcat") diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt index 4da44a853..fe468ab69 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt @@ -84,6 +84,7 @@ class AndroidDeviceProvider : DeviceProvider { logger.debug { "Device ${device.serialNumber} disconnected" } matchDdmsToDevice(it)?.let { notifyDisconnected(it) + it.dispose() } } } @@ -161,6 +162,7 @@ class AndroidDeviceProvider : DeviceProvider { AndroidDebugBridge.disconnectBridge() AndroidDebugBridge.terminate() bootWaitContext.close() + channel.close() } override fun subscribe() = channel diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/AndroidAppInstaller.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/AndroidAppInstaller.kt index 9ca15d9c2..dc4f323af 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/AndroidAppInstaller.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/AndroidAppInstaller.kt @@ -22,7 +22,7 @@ class AndroidAppInstaller(configuration: Configuration) { private val logger = MarathonLogging.logger("AndroidAppInstaller") private val androidConfiguration = configuration.vendorConfiguration as AndroidConfiguration - fun prepareInstallation(device: AndroidDevice) { + suspend fun prepareInstallation(device: AndroidDevice) { val applicationInfo = ApkParser().parseInstrumentationInfo(androidConfiguration.testApplicationOutput) logger.debug { "Installing application output to ${device.serialNumber}" } androidConfiguration.applicationOutput?.let { @@ -34,10 +34,10 @@ class AndroidAppInstaller(configuration: Configuration) { } @Suppress("TooGenericExceptionThrown") - private fun reinstall(device: AndroidDevice, appPackage: String, appApk: File) { + private suspend fun reinstall(device: AndroidDevice, appPackage: String, appApk: File) { val ddmsDevice = device.ddmsDevice - withRetry(attempts = MAX_RETIRES, delay = 1000) { + withRetry(attempts = MAX_RETIRES, delayTime = 1000) { try { logger.info("Uninstalling $appPackage from ${device.serialNumber}") val uninstallMessage = ddmsDevice.safeUninstallPackage(appPackage) diff --git a/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/AndroidDeviceProviderSpek.kt b/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/AndroidDeviceProviderSpek.kt new file mode 100644 index 000000000..3ca16dd6a --- /dev/null +++ b/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/AndroidDeviceProviderSpek.kt @@ -0,0 +1,22 @@ +package com.malinskiy.marathon.android + +import org.amshove.kluent.shouldEqual +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on + +class AndroidDeviceProviderSpek: Spek({ + given("A provider") { + val provider = AndroidDeviceProvider() + + on("terminate") { + it("should close the channel") { + provider.terminate() + + provider.subscribe().isClosedForReceive shouldEqual true + provider.subscribe().isClosedForSend shouldEqual true + } + } + } +}) \ No newline at end of file diff --git a/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDevice.kt b/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDevice.kt index b5f1f0f5c..9d3c1b879 100644 --- a/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDevice.kt +++ b/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDevice.kt @@ -26,10 +26,11 @@ import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.test.TestBatch import com.malinskiy.marathon.time.SystemTimer import kotlinx.coroutines.experimental.CompletableDeferred +import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.newFixedThreadPoolContext import net.schmizz.sshj.connection.ConnectionException import net.schmizz.sshj.transport.TransportException import java.io.File -import java.io.FileNotFoundException import java.io.IOException import java.util.concurrent.TimeUnit @@ -45,6 +46,8 @@ class IOSDevice(val udid: String, private val runtime: String? private val deviceType: String? + private val deviceContext = newFixedThreadPoolContext(1, udid) + init { val device = simctl.list(this, gson).find { it.udid == udid } runtime = device?.runtime @@ -70,112 +73,116 @@ class IOSDevice(val udid: String, override val abi: String get() = "Simulator" - override fun execute(configuration: Configuration, - devicePoolId: DevicePoolId, - testBatch: TestBatch, - deferred: CompletableDeferred, - progressReporter: ProgressReporter) { - - val iosConfiguration = configuration.vendorConfiguration as IOSConfiguration - val fileManager = FileManager(configuration.outputDir) - val testLogListener = TestLogListener() - - val remoteXcresultPath = RemoteFileManager.remoteXcresultFile(this) - val remoteXctestrunFile = RemoteFileManager.remoteXctestrunFile(this) - val remoteDir = remoteXctestrunFile.parent - - logger.debug { "remote xctestrun = $remoteXctestrunFile" } - - val xctestrun = Xctestrun(iosConfiguration.xctestrunPath) - val packageNameFormatter = TestLogPackageNameFormatter(xctestrun.productModuleName, xctestrun.targetName) - - val logParser = CompositeLogParser(listOf( - //Order matters here: first grab the log with log listener, - //then use this log to insert into the test report - testLogListener, - TestRunProgressParser(SystemTimer(), - packageNameFormatter, - listOf( - ProgressReportingListener( - this, - devicePoolId, - progressReporter, - deferred, - testBatch, + override suspend fun execute(configuration: Configuration, + devicePoolId: DevicePoolId, + testBatch: TestBatch, + deferred: CompletableDeferred, + progressReporter: ProgressReporter) { + + launch(deviceContext) { + val iosConfiguration = configuration.vendorConfiguration as IOSConfiguration + val fileManager = FileManager(configuration.outputDir) + val testLogListener = TestLogListener() + + val remoteXcresultPath = RemoteFileManager.remoteXcresultFile(this@IOSDevice) + val remoteXctestrunFile = RemoteFileManager.remoteXctestrunFile(this@IOSDevice) + val remoteDir = remoteXctestrunFile.parent + + logger.debug { "remote xctestrun = $remoteXctestrunFile" } + + val xctestrun = Xctestrun(iosConfiguration.xctestrunPath) + val packageNameFormatter = TestLogPackageNameFormatter(xctestrun.productModuleName, xctestrun.targetName) + + val logParser = CompositeLogParser(listOf( + //Order matters here: first grab the log with log listener, + //then use this log to insert into the test report + testLogListener, + TestRunProgressParser(SystemTimer(), + packageNameFormatter, + listOf( + ProgressReportingListener( + this@IOSDevice, + devicePoolId, + progressReporter, + deferred, + testBatch, + testLogListener + ), testLogListener - ), - testLogListener - ) - ), - DebugLoggingParser() - )) - - val tests = testBatch.tests.map { - "${it.pkg}.${it.clazz}#${it.method}" - }.toTypedArray() - - logger.debug { "tests = ${tests.toList()}" } - - val testBatchToArguments = testBatch.tests - .map { "-only-testing:\"${it.pkg}/${it.clazz}/${it.method}\"" } - .joinToString(separator = " ") - - val remoteCommand = - listOf("cd '$remoteDir' &&", - "NSUnbufferedIO=YES", - "xcodebuild test-without-building", - "-xctestrun ${remoteXctestrunFile.path}", - // "-resultBundlePath ${remoteXcresultPath.canonicalPath} ", - testBatchToArguments, - "-destination 'platform=iOS simulator,id=$udid' ;", - "exit") - .joinToString(" ") - .also { logger.debug(it) } - val session = hostCommandExecutor.startSession() - try { - val command = session.exec(remoteCommand) - - command.inputStream.reader().forEachLine { logParser.onLine(it) } - command.errorStream.reader().forEachLine { logger.error(it) } - - command.join(configuration.testOutputTimeoutMillis, TimeUnit.MILLISECONDS) - } catch(e: ConnectionException) { - logger.error("Ssh exception: ${e}") - } catch(e: TransportException) { - logger.error("Ssh exception: ${e}") - } finally { - logParser.close() - - if (session.isOpen) { - try { - session.close() - } catch (e: IOException) { } + ) + ), + DebugLoggingParser() + )) + + val tests = testBatch.tests.map { + "${it.pkg}.${it.clazz}#${it.method}" + }.toTypedArray() + + logger.debug { "tests = ${tests.toList()}" } + + val testBatchToArguments = testBatch.tests + .map { "-only-testing:\"${it.pkg}/${it.clazz}/${it.method}\"" } + .joinToString(separator = " ") + + val remoteCommand = + listOf("cd '$remoteDir' &&", + "NSUnbufferedIO=YES", + "xcodebuild test-without-building", + "-xctestrun ${remoteXctestrunFile.path}", + // "-resultBundlePath ${remoteXcresultPath.canonicalPath} ", + testBatchToArguments, + "-destination 'platform=iOS simulator,id=$udid' ;", + "exit") + .joinToString(" ") + .also { logger.debug(it) } + val session = hostCommandExecutor.startSession() + try { + val command = session.exec(remoteCommand) + + command.inputStream.reader().forEachLine { logParser.onLine(it) } + command.errorStream.reader().forEachLine { logger.error(it) } + + command.join(configuration.testOutputTimeoutMillis, TimeUnit.MILLISECONDS) + } catch(e: ConnectionException) { + logger.error("Ssh exception: ${e}") + } catch(e: TransportException) { + logger.error("Ssh exception: ${e}") + } finally { + logParser.close() + + if (session.isOpen) { + try { + session.close() + } catch (e: IOException) { } + } } } } - override fun prepare(configuration: Configuration) { - RemoteFileManager.createRemoteDirectory(this) - - val sshjCommandExecutor = hostCommandExecutor as SshjCommandExecutor - val derivedDataManager = DerivedDataManager(configuration) - - val remoteXctestrunFile = RemoteFileManager.remoteXctestrunFile(this) - val xctestrunFile = prepareXctestrunFile(derivedDataManager, remoteXctestrunFile) - - derivedDataManager.sendSynchronized( - localPath = xctestrunFile, - remotePath = remoteXctestrunFile.absolutePath, - hostName = sshjCommandExecutor.hostAddress.hostName, - port = sshjCommandExecutor.port - ) - - derivedDataManager.sendSynchronized( - localPath = derivedDataManager.productsDir, - remotePath = RemoteFileManager.remoteDirectory(this).path, - hostName = sshjCommandExecutor.hostAddress.hostName, - port = sshjCommandExecutor.port - ) + override suspend fun prepare(configuration: Configuration) { + launch(deviceContext) { + RemoteFileManager.createRemoteDirectory(this@IOSDevice) + + val sshjCommandExecutor = hostCommandExecutor as SshjCommandExecutor + val derivedDataManager = DerivedDataManager(configuration) + + val remoteXctestrunFile = RemoteFileManager.remoteXctestrunFile(this@IOSDevice) + val xctestrunFile = prepareXctestrunFile(derivedDataManager, remoteXctestrunFile) + + derivedDataManager.sendSynchronized( + localPath = xctestrunFile, + remotePath = remoteXctestrunFile.absolutePath, + hostName = sshjCommandExecutor.hostAddress.hostName, + port = sshjCommandExecutor.port + ) + + derivedDataManager.sendSynchronized( + localPath = derivedDataManager.productsDir, + remotePath = RemoteFileManager.remoteDirectory(this@IOSDevice).path, + hostName = sshjCommandExecutor.hostAddress.hostName, + port = sshjCommandExecutor.port + ) + } } private fun prepareXctestrunFile(derivedDataManager: DerivedDataManager, remoteXctestrunFile: File): File { diff --git a/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt b/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt index a2f96628c..15e40ceff 100644 --- a/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt +++ b/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt @@ -24,6 +24,7 @@ class IOSDeviceProvider : DeviceProvider { override fun terminate() { logger.debug { "Terminating IOS device provider" } simulatorProvider.stop() + channel.close() } override fun initialize(vendorConfiguration: VendorConfiguration) { diff --git a/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/IOSDeviceProviderSpek.kt b/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/IOSDeviceProviderSpek.kt new file mode 100644 index 000000000..2f0c24828 --- /dev/null +++ b/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/IOSDeviceProviderSpek.kt @@ -0,0 +1,22 @@ +package com.malinskiy.marathon.ios + +import org.amshove.kluent.shouldEqual +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on + +class IOSDeviceProviderSpek: Spek({ + given("A provider") { + val provider = IOSDeviceProvider() + + on("terminate") { + it("should close the channel") { + provider.terminate() + + provider.subscribe().isClosedForReceive shouldEqual true + provider.subscribe().isClosedForSend shouldEqual true + } + } + } +}) \ No newline at end of file diff --git a/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/Mocks.kt b/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/Mocks.kt index da25c80b0..7c43d4cbb 100644 --- a/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/Mocks.kt +++ b/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/Mocks.kt @@ -1,21 +1,11 @@ package com.malinskiy.marathon.ios -import com.google.common.io.Files import com.google.gson.GsonBuilder -import com.malinskiy.marathon.device.DevicePoolId -import com.malinskiy.marathon.device.DeviceProvider -import com.malinskiy.marathon.execution.AnalyticsConfiguration -import com.malinskiy.marathon.execution.Configuration -import com.malinskiy.marathon.execution.TestParser -import com.malinskiy.marathon.execution.strategy.impl.batching.IsolateBatchingStrategy -import com.malinskiy.marathon.ios.cmd.remote.CommandExecutor import com.malinskiy.marathon.ios.cmd.remote.CommandResult import com.malinskiy.marathon.ios.simctl.model.SimctlDeviceList import com.malinskiy.marathon.ios.simctl.model.SimctlDeviceListDeserializer -import com.malinskiy.marathon.vendor.VendorConfiguration import net.schmizz.sshj.connection.channel.direct.Session import org.amshove.kluent.mock -import java.io.File class Mocks { class CommandExecutor { diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt index fe4c0d530..c92bf7c92 100644 --- a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt @@ -28,7 +28,7 @@ class StubDevice(override val operatingSystem: OperatingSystem = OperatingSystem lateinit var executionResults: Map> var executionIndexMap: MutableMap = mutableMapOf() - override fun execute(configuration: Configuration, devicePoolId: DevicePoolId, testBatch: TestBatch, deferred: CompletableDeferred, progressReporter: ProgressReporter) { + override suspend fun execute(configuration: Configuration, devicePoolId: DevicePoolId, testBatch: TestBatch, deferred: CompletableDeferred, progressReporter: ProgressReporter) { val results = testBatch.tests.map { val i = executionIndexMap.getOrDefault(it, 0) val result = executionResults[it]!![i] @@ -45,7 +45,7 @@ class StubDevice(override val operatingSystem: OperatingSystem = OperatingSystem ) } - override fun prepare(configuration: Configuration) { + override suspend fun prepare(configuration: Configuration) { logger.debug { "Preparing" } } From bab6fd2ac232ae8c3d52023fa56e192b7a6c9757 Mon Sep 17 00:00:00 2001 From: Anton Malinskiy Date: Thu, 29 Nov 2018 14:11:43 +0700 Subject: [PATCH 09/14] Android tests hang on device disconnected --- .../marathon/execution/device/DeviceActor.kt | 2 +- .../scenario/DisconnectingScenarios.kt | 75 +++++++++++++++++++ .../marathon/scenario/SuccessScenarios.kt | 8 +- .../output/raw/disconnecting_scenario_1.json | 1 + .../marathon/android/AndroidDevice.kt | 12 +-- .../marathon/android/AndroidDeviceProvider.kt | 7 +- .../executor/listeners/LogCatListener.kt | 4 +- .../ScreenCapturerTestRunListener.kt | 3 +- .../com/malinskiy/marathon/test/StubDevice.kt | 13 +++- .../marathon/test/StubDeviceProvider.kt | 2 +- 10 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 core/src/test/kotlin/com/malinskiy/marathon/scenario/DisconnectingScenarios.kt create mode 100644 core/src/test/resources/output/raw/disconnecting_scenario_1.json diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt index ee9179843..f49d4cc56 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt @@ -176,7 +176,7 @@ class DeviceActor(private val devicePoolId: DevicePoolId, device.execute(configuration, devicePoolId, batch, result, progressReporter) } catch (e: DeviceLostException) { logger.error(e) { "Critical error during execution" } - returnBatch(batch) + state.transition(DeviceEvent.Terminate) } catch (e: TestBatchExecutionException) { returnBatch(batch) } diff --git a/core/src/test/kotlin/com/malinskiy/marathon/scenario/DisconnectingScenarios.kt b/core/src/test/kotlin/com/malinskiy/marathon/scenario/DisconnectingScenarios.kt new file mode 100644 index 000000000..eaa47c9cb --- /dev/null +++ b/core/src/test/kotlin/com/malinskiy/marathon/scenario/DisconnectingScenarios.kt @@ -0,0 +1,75 @@ +package com.malinskiy.marathon.scenario + +import com.malinskiy.marathon.device.DeviceProvider +import com.malinskiy.marathon.execution.TestStatus +import com.malinskiy.marathon.test.StubDevice +import com.malinskiy.marathon.test.Test +import com.malinskiy.marathon.test.assert.shouldBeEqualTo +import com.malinskiy.marathon.test.setupMarathon +import kotlinx.coroutines.experimental.delay +import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.test.TestCoroutineContext +import org.amshove.kluent.shouldBe +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import org.jetbrains.spek.api.dsl.on +import java.io.File +import java.util.concurrent.TimeUnit + +class DisconnectingScenarios : Spek({ + given("two healthy devices") { + on("execution of two tests while one device disconnects") { + it("should pass") { + var output: File? = null + val context = TestCoroutineContext("testing context") + + val marathon = setupMarathon { + val test1 = Test("test", "SimpleTest", "test1", emptySet()) + val test2 = Test("test", "SimpleTest", "test2", emptySet()) + val device1 = StubDevice(serialNumber = "serial-1") + val device2 = StubDevice(serialNumber = "serial-2") + + configuration { + output = outputDir + + tests { + listOf(test1, test2) + } + + vendorConfiguration.deviceProvider.coroutineContext = context + + devices { + delay(1000) + it.send(DeviceProvider.DeviceEvent.DeviceConnected(device1)) + delay(100) + it.send(DeviceProvider.DeviceEvent.DeviceConnected(device2)) + delay(5000) + it.send(DeviceProvider.DeviceEvent.DeviceDisconnected(device1)) + } + } + + device1.executionResults = mapOf( + test1 to arrayOf(TestStatus.INCOMPLETE), + test2 to arrayOf(TestStatus.INCOMPLETE) + ) + device2.executionResults = mapOf( + test1 to arrayOf(TestStatus.PASSED), + test2 to arrayOf(TestStatus.PASSED) + ) + } + + val job = launch(context = context) { + marathon.runAsync() + } + + context.advanceTimeBy(20, TimeUnit.SECONDS) + + job.isCompleted shouldBe true + + File(output!!.absolutePath + "/test_result", "raw.json") + .shouldBeEqualTo(File(javaClass.getResource("/output/raw/disconnecting_scenario_1.json").file)) + } + } + } +}) diff --git a/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt b/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt index 5e8610e9b..df164e083 100644 --- a/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt +++ b/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt @@ -6,11 +6,10 @@ import com.malinskiy.marathon.test.StubDevice import com.malinskiy.marathon.test.Test import com.malinskiy.marathon.test.assert.shouldBeEqualTo import com.malinskiy.marathon.test.setupMarathon -import kotlinx.coroutines.experimental.CoroutineExceptionHandler import kotlinx.coroutines.experimental.delay import kotlinx.coroutines.experimental.launch -import kotlinx.coroutines.experimental.runBlocking import kotlinx.coroutines.experimental.test.TestCoroutineContext +import org.amshove.kluent.shouldBe import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it @@ -49,12 +48,13 @@ class SuccessScenarios : Spek({ ) } - launch(context = context) { + val job = launch(context = context) { marathon.runAsync() } - context.advanceTimeBy(2, TimeUnit.SECONDS) + context.advanceTimeBy(20, TimeUnit.SECONDS) + job.isCompleted shouldBe true File(output!!.absolutePath + "/test_result", "raw.json") .shouldBeEqualTo(File(javaClass.getResource("/output/raw/success_scenario_1.json").file)) } diff --git a/core/src/test/resources/output/raw/disconnecting_scenario_1.json b/core/src/test/resources/output/raw/disconnecting_scenario_1.json new file mode 100644 index 000000000..7ac512797 --- /dev/null +++ b/core/src/test/resources/output/raw/disconnecting_scenario_1.json @@ -0,0 +1 @@ +[{"package":"test","class":"SimpleTest","method":"test2","deviceSerial":"serial-2","ignored":false,"success":true,"timestamp":0,"duration":1},{"package":"test","class":"SimpleTest","method":"test1","deviceSerial":"serial-2","ignored":false,"success":true,"timestamp":1,"duration":1}] \ No newline at end of file diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt index 707c3e6ae..f8368ce1f 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt @@ -15,14 +15,14 @@ import com.malinskiy.marathon.execution.progress.ProgressReporter import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.test.TestBatch import kotlinx.coroutines.experimental.CompletableDeferred -import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.async import kotlinx.coroutines.experimental.newFixedThreadPoolContext -import java.util.UUID +import java.util.* class AndroidDevice(val ddmsDevice: IDevice) : Device { val logger = MarathonLogging.logger(AndroidDevice::class.java.simpleName) - private val deviceContext = newFixedThreadPoolContext(1, ddmsDevice.serialNumber) + private val deviceContext = newFixedThreadPoolContext(1, "AndroidDevice - execution - ${ddmsDevice.serialNumber}") override val abi: String by lazy { ddmsDevice.getProperty("ro.product.cpu.abi") ?: "Unknown" @@ -100,18 +100,20 @@ class AndroidDevice(val ddmsDevice: IDevice) : Device { deferred: CompletableDeferred, progressReporter: ProgressReporter) { - launch(deviceContext) { + val deferredResult = async(deviceContext) { AndroidDeviceTestRunner(this@AndroidDevice).execute(configuration, devicePoolId, testBatch, deferred, progressReporter) } + deferredResult.await() } override suspend fun prepare(configuration: Configuration) { - launch(deviceContext) { + val deferred = async(deviceContext) { AndroidAppInstaller(configuration).prepareInstallation(this@AndroidDevice) RemoteFileManager.removeRemoteDirectory(ddmsDevice) RemoteFileManager.createRemoteDirectory(ddmsDevice) clearLogcat(ddmsDevice) } + deferred.await() } override fun dispose() { diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt index fe468ab69..7e39a9a2c 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt @@ -143,9 +143,14 @@ class AndroidDeviceProvider : DeviceProvider { } private fun getDeviceOrPut(androidDevice: AndroidDevice): AndroidDevice { - return devices.getOrPut(androidDevice.serialNumber) { + val newAndroidDevice = devices.getOrPut(androidDevice.serialNumber) { androidDevice } + if (newAndroidDevice != androidDevice) { + androidDevice.dispose() + } + + return newAndroidDevice } private fun matchDdmsToDevice(device: IDevice): AndroidDevice? { diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/LogCatListener.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/LogCatListener.kt index f5aee3950..f2b3c7394 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/LogCatListener.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/LogCatListener.kt @@ -16,6 +16,7 @@ class LogCatListener(private val device: AndroidDevice, private val receiver = LogCatReceiverTask(device.ddmsDevice) private val ref = AtomicReference>(mutableListOf()) + private var thread: Thread? = null private val listener: (MutableList) -> Unit = { ref.get().addAll(it) @@ -23,7 +24,7 @@ class LogCatListener(private val device: AndroidDevice, override fun testRunStarted(runName: String, testCount: Int) { receiver.addLogCatListener(listener) - thread(name = "LogCatLogger-$runName-${device.serialNumber}") { + thread = thread(name = "LogCatLogger-$runName-${device.serialNumber}") { receiver.run() } } @@ -38,5 +39,6 @@ class LogCatListener(private val device: AndroidDevice, override fun testRunEnded(elapsedTime: Long, runMetrics: Map) { receiver.stop() receiver.removeLogCatListener(listener) + thread?.interrupt() } } diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt index b7226b237..b399449d2 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt @@ -23,7 +23,7 @@ class ScreenCapturerTestRunListener(private val fileManager: FileManager, override fun testStarted(test: TestIdentifier) { super.testStarted(test) logger.debug { "Starting recording for ${test.toTest().toSimpleSafeTestName()}" } - screenCapturerJob = async(context = threadPoolDispatcher) { + screenCapturerJob = async (context = threadPoolDispatcher) { ScreenCapturer(device, pool, fileManager, test).start() } } @@ -32,5 +32,6 @@ class ScreenCapturerTestRunListener(private val fileManager: FileManager, super.testEnded(test, testMetrics) logger.debug { "Finished recording for ${test.toTest().toSimpleSafeTestName()}" } screenCapturerJob?.cancel() + threadPoolDispatcher.close() } } \ No newline at end of file diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt index c92bf7c92..7984c6764 100644 --- a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt @@ -13,8 +13,11 @@ import com.malinskiy.marathon.execution.TestStatus import com.malinskiy.marathon.execution.progress.ProgressReporter import com.malinskiy.marathon.log.MarathonLogging import kotlinx.coroutines.experimental.CompletableDeferred +import kotlinx.coroutines.experimental.delay -class StubDevice(override val operatingSystem: OperatingSystem = OperatingSystem("25"), +class StubDevice(private val prepareTimeMillis: Long = 5000L, + private val testTimeMillis: Long = 5000L, + override val operatingSystem: OperatingSystem = OperatingSystem("25"), override val model: String = "test", override val manufacturer: String = "test", override val networkState: NetworkState = NetworkState.CONNECTED, @@ -27,13 +30,18 @@ class StubDevice(override val operatingSystem: OperatingSystem = OperatingSystem lateinit var executionResults: Map> var executionIndexMap: MutableMap = mutableMapOf() + var timeCounter: Long = 0 override suspend fun execute(configuration: Configuration, devicePoolId: DevicePoolId, testBatch: TestBatch, deferred: CompletableDeferred, progressReporter: ProgressReporter) { + delay(testTimeMillis) + val results = testBatch.tests.map { val i = executionIndexMap.getOrDefault(it, 0) val result = executionResults[it]!![i] executionIndexMap[it] = i + 1 - TestResult(it, toDeviceInfo(), result, 0, 1, null) + val testResult = TestResult(it, toDeviceInfo(), result, timeCounter, timeCounter + 1, null) + timeCounter += 1 + testResult } deferred.complete( @@ -47,6 +55,7 @@ class StubDevice(override val operatingSystem: OperatingSystem = OperatingSystem override suspend fun prepare(configuration: Configuration) { logger.debug { "Preparing" } + delay(prepareTimeMillis) } override fun dispose() { diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt index 1e7fe5d68..a4c7b94dc 100644 --- a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.experimental.channels.Channel import kotlinx.coroutines.experimental.launch import kotlin.coroutines.experimental.CoroutineContext -class StubDeviceProvider() : DeviceProvider { +class StubDeviceProvider : DeviceProvider { lateinit var coroutineContext: CoroutineContext private val channel: Channel = unboundedChannel() From a8562228fb96d4158c0ff6c7f1e29819bfab131f Mon Sep 17 00:00:00 2001 From: Anton Malinskiy Date: Fri, 30 Nov 2018 21:39:18 +0700 Subject: [PATCH 10/14] Gradle 5, Kotlin 1.3.10, Coroutines 1.0.1 --- buildSrc/build.gradle.kts | 8 +++++ buildSrc/src/main/kotlin/Versions.kt | 6 ++-- cli/build.gradle.kts | 20 ++++++++---- .../marathon/cli/config/ConfigFactorySpec.kt | 3 +- core/build.gradle.kts | 5 ++- .../kotlin/com/malinskiy/marathon/Marathon.kt | 9 ++---- .../com/malinskiy/marathon/actor/Actor.kt | 27 ++++++++++------ .../malinskiy/marathon/actor/extensions.kt | 2 +- .../com/malinskiy/marathon/device/Device.kt | 2 +- .../marathon/device/DeviceProvider.kt | 2 +- .../marathon/execution/DevicePoolActor.kt | 10 +++--- .../com/malinskiy/marathon/execution/Retry.kt | 2 +- .../malinskiy/marathon/execution/Scheduler.kt | 29 +++++++++-------- .../marathon/execution/device/DeviceAction.kt | 2 +- .../marathon/execution/device/DeviceActor.kt | 32 +++++++++---------- .../marathon/execution/device/DeviceEvent.kt | 2 +- .../marathon/execution/device/DeviceState.kt | 2 +- .../marathon/execution/queue/QueueActor.kt | 10 +++--- .../tracker/DelegatingTrackerSpek.kt | 6 ++-- .../analytics/tracker/NoOpTrackerSpek.kt | 2 +- .../malinskiy/marathon/device/DeviceStub.kt | 2 +- .../scenario/DisconnectingScenarios.kt | 11 ++++--- .../marathon/scenario/SuccessScenarios.kt | 11 ++++--- execution-timeline/build.gradle.kts | 4 +-- gradle/wrapper/gradle-wrapper.properties | 2 +- kotlin-version | 2 +- marathon-html-report/build.gradle.kts | 4 +-- vendor-android/build.gradle.kts | 5 ++- .../marathon/android/AndroidDevice.kt | 22 ++++++++----- .../marathon/android/AndroidDeviceProvider.kt | 21 ++++++------ .../executor/AndroidDeviceTestRunner.kt | 2 +- .../listeners/TestRunResultsListener.kt | 2 +- .../listeners/screenshot/ScreenCapturer.kt | 7 ++-- .../ScreenCapturerTestRunListener.kt | 14 +++++--- .../marathon/android/AndroidDeviceSpek.kt | 2 +- vendor-ios/build.gradle.kts | 6 ++-- .../com/malinskiy/marathon/ios/IOSDevice.kt | 16 ++++++---- .../marathon/ios/IOSDeviceProvider.kt | 2 +- .../ios/device/LocalListSimulatorProvider.kt | 11 +++++-- .../listener/ProgressReportingListener.kt | 2 +- .../marathon/ios/DerivedDataManagerSpek.kt | 4 +-- .../ios/logparser/ProgressParserSpek.kt | 6 ++-- vendor-test/build.gradle.kts | 6 ++-- .../com/malinskiy/marathon/test/StubDevice.kt | 4 +-- .../marathon/test/StubDeviceProvider.kt | 14 +++++--- .../test/factory/ConfigurationFactory.kt | 3 +- 46 files changed, 202 insertions(+), 164 deletions(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 3d7a9541b..05e0f87e9 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -2,6 +2,14 @@ plugins { `kotlin-dsl` } +kotlinDslPluginOptions { + experimentalWarning.set(false) +} + repositories { jcenter() } + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.10") +} diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index b1049f8b6..c784dae5a 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -1,8 +1,8 @@ object Versions { val marathon = System.getenv("DEPLOY_VERSION_OVERRIDE") ?: "0.2.2" - val kotlin = "1.2.61" - val coroutines = "0.25.0" + val kotlin = "1.3.10" + val coroutines = "1.0.1" val ddmlib = "26.2.0" val dexTestParser = "2.0.0" @@ -38,6 +38,7 @@ object Versions { val testContainers = "1.9.1" val jupiterEngine = "5.1.0" val scalr = "4.2" + val mockitoKotlin = "2.0.0" } object BuildPlugins { @@ -87,6 +88,7 @@ object TestLibraries { val espressoContrib = "com.android.support.test.espresso:espresso-contrib:${Versions.espresso}" val espressoIntents = "com.android.support.test.espresso:espresso-intents:${Versions.espresso}" val junit = "junit:junit:${Versions.junit}" + val mockitoKotlin = "com.nhaarman.mockitokotlin2:mockito-kotlin:${Versions.mockitoKotlin}" val jupiterEngine = "org.junit.jupiter:junit-jupiter-engine:${Versions.jupiterEngine}" val testContainers = "org.testcontainers:testcontainers:${Versions.testContainers}" diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index b816d5b67..8fc238027 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -13,10 +13,16 @@ plugins { id("de.fuerstenau.buildconfig") version "1.1.8" } +val debugCoroutines = true +val coroutinesJvmOptions = when(debugCoroutines) { + true -> "-Dkotlinx.coroutines.debug" + else -> "" +} + application { mainClassName = "com.malinskiy.marathon.cli.ApplicationViewKt" applicationName = "marathon" - applicationDefaultJvmArgs = listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044") + applicationDefaultJvmArgs = listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044", coroutinesJvmOptions) } distributions { @@ -25,7 +31,10 @@ distributions { } } -kotlin.experimental.coroutines = Coroutines.ENABLE +tasks.withType { + kotlinOptions.jvmTarget = "1.8" + kotlinOptions.apiVersion = "1.3" +} dependencies { implementation(project(":core")) @@ -34,6 +43,7 @@ dependencies { implementation(Libraries.kotlinStdLib) implementation(Libraries.kotlinCoroutines) implementation(Libraries.kotlinLogging) + implementation(Libraries.kotlinReflect) implementation(Libraries.slf4jAPI) implementation(Libraries.logbackClassic) implementation(Libraries.argParser) @@ -43,17 +53,13 @@ dependencies { implementation(Libraries.jacksonYaml) implementation(Libraries.jacksonJSR310) testCompile(TestLibraries.kluent) + testCompile(TestLibraries.mockitoKotlin) testCompile(TestLibraries.spekAPI) testRuntime(TestLibraries.spekJUnitPlatformEngine) } Deployment.initialize(project) -tasks.withType { - kotlinOptions.jvmTarget = "1.8" - kotlinOptions.apiVersion = "1.2" -} - buildConfig { appName = project.name version = Versions.marathon diff --git a/cli/src/test/kotlin/com/malinskiy/marathon/cli/config/ConfigFactorySpec.kt b/cli/src/test/kotlin/com/malinskiy/marathon/cli/config/ConfigFactorySpec.kt index d9e7444fa..d160de721 100644 --- a/cli/src/test/kotlin/com/malinskiy/marathon/cli/config/ConfigFactorySpec.kt +++ b/cli/src/test/kotlin/com/malinskiy/marathon/cli/config/ConfigFactorySpec.kt @@ -34,13 +34,12 @@ import com.malinskiy.marathon.execution.strategy.impl.sorting.ExecutionTimeSorti import com.malinskiy.marathon.execution.strategy.impl.sorting.NoSortingStrategy import com.malinskiy.marathon.execution.strategy.impl.sorting.SuccessRateSortingStrategy import com.malinskiy.marathon.ios.IOSConfiguration -import com.nhaarman.mockito_kotlin.whenever +import com.nhaarman.mockitokotlin2.whenever import org.amshove.kluent.`it returns` import org.amshove.kluent.`should be instance of` import org.amshove.kluent.mock import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeEmpty -import org.amshove.kluent.shouldBeInRange import org.amshove.kluent.shouldContainAll import org.amshove.kluent.shouldEqual import org.amshove.kluent.shouldNotThrow diff --git a/core/build.gradle.kts b/core/build.gradle.kts index ffcfe0ff7..8426ea768 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -13,8 +13,6 @@ plugins { id("org.junit.platform.gradle.plugin") } -kotlin.experimental.coroutines = Coroutines.ENABLE - sourceSets { create("integrationTest") { compileClasspath += sourceSets["main"].output @@ -50,6 +48,7 @@ dependencies { testRuntime(TestLibraries.jupiterEngine) testCompile(TestLibraries.testContainers) testCompile(TestLibraries.testContainersInflux) + testImplementation(TestLibraries.mockitoKotlin) } @@ -74,7 +73,7 @@ Deployment.initialize(project) tasks.withType { kotlinOptions.jvmTarget = "1.8" - kotlinOptions.apiVersion = "1.2" + kotlinOptions.apiVersion = "1.3" } junitPlatform { diff --git a/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt b/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt index c369371f5..89280780d 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt @@ -20,15 +20,10 @@ import com.malinskiy.marathon.report.internal.TestResultReporter import com.malinskiy.marathon.test.Test import com.malinskiy.marathon.test.toTestName import com.malinskiy.marathon.vendor.VendorConfiguration -import kotlinx.coroutines.experimental.IO -import kotlinx.coroutines.experimental.delay -import kotlinx.coroutines.experimental.launch -import kotlinx.coroutines.experimental.newFixedThreadPoolContext -import kotlinx.coroutines.experimental.runBlocking -import kotlinx.coroutines.experimental.withContext +import kotlinx.coroutines.runBlocking import java.util.ServiceLoader import java.util.concurrent.TimeUnit -import kotlin.coroutines.experimental.coroutineContext +import kotlin.coroutines.coroutineContext import kotlin.system.measureTimeMillis private val log = MarathonLogging.logger {} diff --git a/core/src/main/kotlin/com/malinskiy/marathon/actor/Actor.kt b/core/src/main/kotlin/com/malinskiy/marathon/actor/Actor.kt index 134184e1d..ddea5c676 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/actor/Actor.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/actor/Actor.kt @@ -1,21 +1,25 @@ package com.malinskiy.marathon.actor -import kotlinx.coroutines.experimental.Job -import kotlinx.coroutines.experimental.channels.Channel -import kotlinx.coroutines.experimental.channels.SendChannel -import kotlinx.coroutines.experimental.channels.actor -import kotlinx.coroutines.experimental.selects.SelectClause2 -import kotlin.coroutines.experimental.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.channels.actor +import kotlinx.coroutines.selects.SelectClause2 +import kotlin.coroutines.CoroutineContext abstract class Actor(parent: Job? = null, - context: CoroutineContext) : SendChannel { + val context: CoroutineContext) : SendChannel, CoroutineScope { + protected abstract suspend fun receive(msg: T) + override val coroutineContext: CoroutineContext + get() = context + actorJob + private val actorJob = Job(parent) private val delegate = actor( capacity = Channel.UNLIMITED, - parent = actorJob, - context = context + context = coroutineContext ) { for (msg in channel) { receive(msg) @@ -33,7 +37,10 @@ abstract class Actor(parent: Job? = null, delegate.invokeOnClose(handler) } - override fun close(cause: Throwable?): Boolean = actorJob.cancel() + override fun close(cause: Throwable?): Boolean { + actorJob.cancel() + return true + } override fun offer(element: T): Boolean = delegate.offer(element) diff --git a/core/src/main/kotlin/com/malinskiy/marathon/actor/extensions.kt b/core/src/main/kotlin/com/malinskiy/marathon/actor/extensions.kt index 95b39eeb6..31065f2b0 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/actor/extensions.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/actor/extensions.kt @@ -1,5 +1,5 @@ package com.malinskiy.marathon.actor -import kotlinx.coroutines.experimental.channels.Channel +import kotlinx.coroutines.channels.Channel fun unboundedChannel() = Channel(Channel.UNLIMITED) diff --git a/core/src/main/kotlin/com/malinskiy/marathon/device/Device.kt b/core/src/main/kotlin/com/malinskiy/marathon/device/Device.kt index abc9e0fa9..1926845b1 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/device/Device.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/device/Device.kt @@ -4,7 +4,7 @@ import com.malinskiy.marathon.execution.Configuration import com.malinskiy.marathon.execution.TestBatchResults import com.malinskiy.marathon.execution.progress.ProgressReporter import com.malinskiy.marathon.test.TestBatch -import kotlinx.coroutines.experimental.CompletableDeferred +import kotlinx.coroutines.CompletableDeferred interface Device { val operatingSystem: OperatingSystem diff --git a/core/src/main/kotlin/com/malinskiy/marathon/device/DeviceProvider.kt b/core/src/main/kotlin/com/malinskiy/marathon/device/DeviceProvider.kt index 9b06cf1f5..bd1ecf8e4 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/device/DeviceProvider.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/device/DeviceProvider.kt @@ -1,7 +1,7 @@ package com.malinskiy.marathon.device import com.malinskiy.marathon.vendor.VendorConfiguration -import kotlinx.coroutines.experimental.channels.Channel +import kotlinx.coroutines.channels.Channel interface DeviceProvider { sealed class DeviceEvent { diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt index 674880b6d..9d5595074 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt @@ -12,9 +12,9 @@ import com.malinskiy.marathon.execution.queue.QueueMessage import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.test.Test import com.malinskiy.marathon.test.TestBatch -import kotlinx.coroutines.experimental.Job -import kotlinx.coroutines.experimental.channels.SendChannel -import kotlin.coroutines.experimental.CoroutineContext +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.SendChannel +import kotlin.coroutines.CoroutineContext class DevicePoolActor(private val poolId: DevicePoolId, private val configuration: Configuration, @@ -22,7 +22,7 @@ class DevicePoolActor(private val poolId: DevicePoolId, tests: Collection, private val progressReporter: ProgressReporter, parent: Job, - private val context: CoroutineContext) : + context: CoroutineContext) : Actor(parent = parent, context = context) { private val logger = MarathonLogging.logger("DevicePoolActor[${poolId.name}]") @@ -114,7 +114,7 @@ class DevicePoolActor(private val poolId: DevicePoolId, } logger.debug { "add device ${device.serialNumber}" } - val actor = DeviceActor(poolId, this, configuration, device, progressReporter, poolJob, context) + val actor = DeviceActor(poolId, this, configuration, device, progressReporter, poolJob, coroutineContext) devices[device.serialNumber] = actor actor.send(DeviceEvent.Initialize) } diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/Retry.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/Retry.kt index 53a6d963d..8dd908490 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/Retry.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/Retry.kt @@ -1,6 +1,6 @@ package com.malinskiy.marathon.execution -import kotlinx.coroutines.experimental.delay +import kotlinx.coroutines.delay @Suppress("TooGenericExceptionCaught") suspend fun withRetry(attempts: Int, delayTime: Long = 0, f: suspend () -> Unit) { diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt index f78376f09..137f77132 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt @@ -10,16 +10,15 @@ import com.malinskiy.marathon.execution.DevicePoolMessage.FromScheduler.RemoveDe import com.malinskiy.marathon.execution.progress.ProgressReporter import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.test.Test -import kotlinx.coroutines.experimental.Job -import kotlinx.coroutines.experimental.TimeoutCancellationException -import kotlinx.coroutines.experimental.channels.SendChannel -import kotlinx.coroutines.experimental.delay -import kotlinx.coroutines.experimental.joinChildren -import kotlinx.coroutines.experimental.launch -import kotlinx.coroutines.experimental.withTimeout +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.TimeUnit -import kotlin.coroutines.experimental.CoroutineContext +import kotlin.coroutines.CoroutineContext /** * The logic of scheduler: @@ -32,18 +31,18 @@ class Scheduler(private val deviceProvider: DeviceProvider, private val configuration: Configuration, private val tests: Collection, private val progressReporter: ProgressReporter, - private val coroutineContext: CoroutineContext) { + override val coroutineContext: CoroutineContext) : CoroutineScope { + private val job = Job() private val pools = ConcurrentHashMap>() private val poolingStrategy = configuration.poolingStrategy private val logger = MarathonLogging.logger("Scheduler") suspend fun execute() { - val job = Job() subscribeOnDevices(job) try { - withTimeout(60, TimeUnit.SECONDS) { + withTimeout(60000) { while (pools.isEmpty()) { delay(100) } @@ -51,7 +50,9 @@ class Scheduler(private val deviceProvider: DeviceProvider, } catch (e: TimeoutCancellationException) { throw NoDevicesException("") } - job.joinChildren() + for(child in job.children) { + child.join() + } } fun getPools(): List { @@ -59,7 +60,7 @@ class Scheduler(private val deviceProvider: DeviceProvider, } private fun subscribeOnDevices(job: Job): Job { - return launch(coroutineContext) { + return launch { for (msg in deviceProvider.subscribe()) { when (msg) { is DeviceProvider.DeviceEvent.DeviceConnected -> { diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceAction.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceAction.kt index 2619215d7..a4ebf4c37 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceAction.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceAction.kt @@ -2,7 +2,7 @@ package com.malinskiy.marathon.execution.device import com.malinskiy.marathon.execution.TestBatchResults import com.malinskiy.marathon.test.TestBatch -import kotlinx.coroutines.experimental.CompletableDeferred +import kotlinx.coroutines.CompletableDeferred sealed class DeviceAction { object Initialize : DeviceAction() diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt index f49d4cc56..c88dee4f8 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceActor.kt @@ -14,12 +14,13 @@ import com.malinskiy.marathon.execution.progress.ProgressReporter import com.malinskiy.marathon.execution.withRetry import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.test.TestBatch -import kotlinx.coroutines.experimental.CompletableDeferred -import kotlinx.coroutines.experimental.Job -import kotlinx.coroutines.experimental.async -import kotlinx.coroutines.experimental.channels.SendChannel -import kotlinx.coroutines.experimental.launch -import kotlin.coroutines.experimental.CoroutineContext +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext import kotlin.properties.Delegates class DeviceActor(private val devicePoolId: DevicePoolId, @@ -28,10 +29,8 @@ class DeviceActor(private val devicePoolId: DevicePoolId, private val device: Device, private val progressReporter: ProgressReporter, parent: Job, - private val coroutineContext: CoroutineContext) : - Actor(parent = parent, context = coroutineContext) { - - private val deviceJob = Job(parent) + context: CoroutineContext) : + Actor(parent = parent, context = context) { private val state = StateMachine.create { initialState(DeviceState.Connected) @@ -105,7 +104,7 @@ class DeviceActor(private val devicePoolId: DevicePoolId, } is DeviceAction.Terminate -> { val batch = sideEffect.batch - if(batch == null) { + if (batch == null) { terminate() } else { returnBatch(batch).invokeOnCompletion { @@ -130,7 +129,7 @@ class DeviceActor(private val devicePoolId: DevicePoolId, } private fun requestNextBatch(result: CompletableDeferred?) { - launch(parent = deviceJob, context = coroutineContext) { + launch { if (result != null) { val testResults = result.await() pool.send(DevicePoolMessage.FromDevice.CompletedTestBatch(device, testResults)) @@ -155,9 +154,9 @@ class DeviceActor(private val devicePoolId: DevicePoolId, private fun initialize() { logger.debug { "initialize ${device.serialNumber}" } - job = launch(context = coroutineContext, parent = deviceJob) { + job = launch { withRetry(30, 10000) { - if(isActive) { + if (isActive) { try { device.prepare(configuration) } catch (e: Exception) { @@ -171,7 +170,7 @@ class DeviceActor(private val devicePoolId: DevicePoolId, private fun executeBatch(batch: TestBatch, result: CompletableDeferred) { logger.debug { "executeBatch ${device.serialNumber}" } - job = async(coroutineContext, parent = deviceJob) { + job = async { try { device.execute(configuration, devicePoolId, batch, result, progressReporter) } catch (e: DeviceLostException) { @@ -184,7 +183,7 @@ class DeviceActor(private val devicePoolId: DevicePoolId, } private fun returnBatch(batch: TestBatch): Job { - return launch(parent = deviceJob, context = coroutineContext) { + return launch { pool.send(DevicePoolMessage.FromDevice.ReturnTestBatch(device, batch)) } } @@ -192,7 +191,6 @@ class DeviceActor(private val devicePoolId: DevicePoolId, private fun terminate() { logger.debug { "terminate ${device.serialNumber}" } job?.cancel() - deviceJob.cancel() close() } } diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceEvent.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceEvent.kt index c48b905a1..290f8332c 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceEvent.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceEvent.kt @@ -1,7 +1,7 @@ package com.malinskiy.marathon.execution.device import com.malinskiy.marathon.test.TestBatch -import kotlinx.coroutines.experimental.CompletableDeferred +import kotlinx.coroutines.CompletableDeferred sealed class DeviceEvent { data class Execute(val batch: TestBatch) : DeviceEvent() diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceState.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceState.kt index 7723d963b..b03ee9810 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceState.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/device/DeviceState.kt @@ -2,7 +2,7 @@ package com.malinskiy.marathon.execution.device import com.malinskiy.marathon.execution.TestBatchResults import com.malinskiy.marathon.test.TestBatch -import kotlinx.coroutines.experimental.CompletableDeferred +import kotlinx.coroutines.CompletableDeferred sealed class DeviceState { object Connected : DeviceState() diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/queue/QueueActor.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/queue/QueueActor.kt index 70de079e9..35ef54584 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/queue/QueueActor.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/queue/QueueActor.kt @@ -14,11 +14,11 @@ import com.malinskiy.marathon.execution.progress.ProgressReporter import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.test.Test import com.malinskiy.marathon.test.TestBatch -import kotlinx.coroutines.experimental.CompletableDeferred -import kotlinx.coroutines.experimental.Job -import kotlinx.coroutines.experimental.channels.SendChannel +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.SendChannel import java.util.* -import kotlin.coroutines.experimental.CoroutineContext +import kotlin.coroutines.CoroutineContext class QueueActor(configuration: Configuration, private val testShard: TestShard, @@ -27,7 +27,7 @@ class QueueActor(configuration: Configuration, private val poolId: DevicePoolId, private val progressReporter: ProgressReporter, poolJob: Job, - private val coroutineContext: CoroutineContext) : + coroutineContext: CoroutineContext) : Actor(parent = poolJob, context = coroutineContext) { private val logger = MarathonLogging.logger("QueueActor[$poolId]") diff --git a/core/src/test/kotlin/com/malinskiy/marathon/analytics/tracker/DelegatingTrackerSpek.kt b/core/src/test/kotlin/com/malinskiy/marathon/analytics/tracker/DelegatingTrackerSpek.kt index 939635a8e..52cfdd1c7 100644 --- a/core/src/test/kotlin/com/malinskiy/marathon/analytics/tracker/DelegatingTrackerSpek.kt +++ b/core/src/test/kotlin/com/malinskiy/marathon/analytics/tracker/DelegatingTrackerSpek.kt @@ -6,9 +6,9 @@ import com.malinskiy.marathon.device.DevicePoolId import com.malinskiy.marathon.execution.queue.TestAction import com.malinskiy.marathon.execution.queue.TestEvent import com.malinskiy.marathon.execution.queue.TestState -import com.nhaarman.mockito_kotlin.eq -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockitokotlin2.eq +import com.nhaarman.mockitokotlin2.verify +import org.amshove.kluent.mock import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.describe import org.jetbrains.spek.api.dsl.it diff --git a/core/src/test/kotlin/com/malinskiy/marathon/analytics/tracker/NoOpTrackerSpek.kt b/core/src/test/kotlin/com/malinskiy/marathon/analytics/tracker/NoOpTrackerSpek.kt index 55c94c849..77170a279 100644 --- a/core/src/test/kotlin/com/malinskiy/marathon/analytics/tracker/NoOpTrackerSpek.kt +++ b/core/src/test/kotlin/com/malinskiy/marathon/analytics/tracker/NoOpTrackerSpek.kt @@ -6,7 +6,7 @@ import com.malinskiy.marathon.device.DevicePoolId import com.malinskiy.marathon.execution.queue.TestAction import com.malinskiy.marathon.execution.queue.TestEvent import com.malinskiy.marathon.execution.queue.TestState -import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockitokotlin2.mock import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.describe import org.jetbrains.spek.api.dsl.it diff --git a/core/src/test/kotlin/com/malinskiy/marathon/device/DeviceStub.kt b/core/src/test/kotlin/com/malinskiy/marathon/device/DeviceStub.kt index b99b2bfe5..20cf9674e 100644 --- a/core/src/test/kotlin/com/malinskiy/marathon/device/DeviceStub.kt +++ b/core/src/test/kotlin/com/malinskiy/marathon/device/DeviceStub.kt @@ -4,7 +4,7 @@ import com.malinskiy.marathon.execution.Configuration import com.malinskiy.marathon.execution.TestBatchResults import com.malinskiy.marathon.execution.progress.ProgressReporter import com.malinskiy.marathon.test.TestBatch -import kotlinx.coroutines.experimental.CompletableDeferred +import kotlinx.coroutines.CompletableDeferred class DeviceStub(override var operatingSystem: OperatingSystem = OperatingSystem("25"), override var serialNumber: String = "serialNumber", diff --git a/core/src/test/kotlin/com/malinskiy/marathon/scenario/DisconnectingScenarios.kt b/core/src/test/kotlin/com/malinskiy/marathon/scenario/DisconnectingScenarios.kt index eaa47c9cb..bb02d8951 100644 --- a/core/src/test/kotlin/com/malinskiy/marathon/scenario/DisconnectingScenarios.kt +++ b/core/src/test/kotlin/com/malinskiy/marathon/scenario/DisconnectingScenarios.kt @@ -6,9 +6,10 @@ import com.malinskiy.marathon.test.StubDevice import com.malinskiy.marathon.test.Test import com.malinskiy.marathon.test.assert.shouldBeEqualTo import com.malinskiy.marathon.test.setupMarathon -import kotlinx.coroutines.experimental.delay -import kotlinx.coroutines.experimental.launch -import kotlinx.coroutines.experimental.test.TestCoroutineContext +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestCoroutineContext import org.amshove.kluent.shouldBe import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given @@ -37,7 +38,7 @@ class DisconnectingScenarios : Spek({ listOf(test1, test2) } - vendorConfiguration.deviceProvider.coroutineContext = context + vendorConfiguration.deviceProvider.context = context devices { delay(1000) @@ -59,7 +60,7 @@ class DisconnectingScenarios : Spek({ ) } - val job = launch(context = context) { + val job = GlobalScope.launch(context = context) { marathon.runAsync() } diff --git a/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt b/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt index df164e083..4615aad20 100644 --- a/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt +++ b/core/src/test/kotlin/com/malinskiy/marathon/scenario/SuccessScenarios.kt @@ -6,9 +6,10 @@ import com.malinskiy.marathon.test.StubDevice import com.malinskiy.marathon.test.Test import com.malinskiy.marathon.test.assert.shouldBeEqualTo import com.malinskiy.marathon.test.setupMarathon -import kotlinx.coroutines.experimental.delay -import kotlinx.coroutines.experimental.launch -import kotlinx.coroutines.experimental.test.TestCoroutineContext +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestCoroutineContext import org.amshove.kluent.shouldBe import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given @@ -35,7 +36,7 @@ class SuccessScenarios : Spek({ listOf(test) } - vendorConfiguration.deviceProvider.coroutineContext = context + vendorConfiguration.deviceProvider.context = context devices { delay(1000) @@ -48,7 +49,7 @@ class SuccessScenarios : Spek({ ) } - val job = launch(context = context) { + val job = GlobalScope.launch(context = context) { marathon.runAsync() } diff --git a/execution-timeline/build.gradle.kts b/execution-timeline/build.gradle.kts index 150294fca..4d911a015 100644 --- a/execution-timeline/build.gradle.kts +++ b/execution-timeline/build.gradle.kts @@ -11,8 +11,6 @@ plugins { id("org.junit.platform.gradle.plugin") } -kotlin.experimental.coroutines = Coroutines.ENABLE - dependencies { implementation(Libraries.gson) implementation(Libraries.kotlinStdLib) @@ -27,7 +25,7 @@ Deployment.initialize(project) tasks.withType { kotlinOptions.jvmTarget = "1.8" - kotlinOptions.apiVersion = "1.2" + kotlinOptions.apiVersion = "1.3" } junitPlatform { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d76b502e2..ee671127f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/kotlin-version b/kotlin-version index 903077985..7c2791e8e 100644 --- a/kotlin-version +++ b/kotlin-version @@ -1 +1 @@ -1.2.51 \ No newline at end of file +1.3.10 \ No newline at end of file diff --git a/marathon-html-report/build.gradle.kts b/marathon-html-report/build.gradle.kts index 150294fca..4d911a015 100644 --- a/marathon-html-report/build.gradle.kts +++ b/marathon-html-report/build.gradle.kts @@ -11,8 +11,6 @@ plugins { id("org.junit.platform.gradle.plugin") } -kotlin.experimental.coroutines = Coroutines.ENABLE - dependencies { implementation(Libraries.gson) implementation(Libraries.kotlinStdLib) @@ -27,7 +25,7 @@ Deployment.initialize(project) tasks.withType { kotlinOptions.jvmTarget = "1.8" - kotlinOptions.apiVersion = "1.2" + kotlinOptions.apiVersion = "1.3" } junitPlatform { diff --git a/vendor-android/build.gradle.kts b/vendor-android/build.gradle.kts index 5ca9dec26..ff9417840 100644 --- a/vendor-android/build.gradle.kts +++ b/vendor-android/build.gradle.kts @@ -10,8 +10,6 @@ plugins { id("org.junit.platform.gradle.plugin") } -kotlin.experimental.coroutines = Coroutines.ENABLE - dependencies { implementation(Libraries.kotlinStdLib) implementation(Libraries.kotlinCoroutines) @@ -24,6 +22,7 @@ dependencies { implementation(project(":core")) testImplementation(project(":vendor-test")) testImplementation(TestLibraries.kluent) + testImplementation(TestLibraries.mockitoKotlin) testImplementation(TestLibraries.spekAPI) testRuntime(TestLibraries.spekJUnitPlatformEngine) } @@ -32,7 +31,7 @@ Deployment.initialize(project) tasks.withType { kotlinOptions.jvmTarget = "1.8" - kotlinOptions.apiVersion = "1.2" + kotlinOptions.apiVersion = "1.3" } junitPlatform { diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt index f8368ce1f..e72d5a54c 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt @@ -14,15 +14,21 @@ import com.malinskiy.marathon.execution.TestBatchResults import com.malinskiy.marathon.execution.progress.ProgressReporter import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.test.TestBatch -import kotlinx.coroutines.experimental.CompletableDeferred -import kotlinx.coroutines.experimental.async -import kotlinx.coroutines.experimental.newFixedThreadPoolContext +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.newFixedThreadPoolContext import java.util.* +import kotlin.coroutines.CoroutineContext -class AndroidDevice(val ddmsDevice: IDevice) : Device { +class AndroidDevice(val ddmsDevice: IDevice) : Device, CoroutineScope { + private val dispatcher by lazy { + newFixedThreadPoolContext(1, "AndroidDevice - execution - ${ddmsDevice.serialNumber}") + } + + override val coroutineContext: CoroutineContext = dispatcher val logger = MarathonLogging.logger(AndroidDevice::class.java.simpleName) - private val deviceContext = newFixedThreadPoolContext(1, "AndroidDevice - execution - ${ddmsDevice.serialNumber}") override val abi: String by lazy { ddmsDevice.getProperty("ro.product.cpu.abi") ?: "Unknown" @@ -100,14 +106,14 @@ class AndroidDevice(val ddmsDevice: IDevice) : Device { deferred: CompletableDeferred, progressReporter: ProgressReporter) { - val deferredResult = async(deviceContext) { + val deferredResult = async { AndroidDeviceTestRunner(this@AndroidDevice).execute(configuration, devicePoolId, testBatch, deferred, progressReporter) } deferredResult.await() } override suspend fun prepare(configuration: Configuration) { - val deferred = async(deviceContext) { + val deferred = async { AndroidAppInstaller(configuration).prepareInstallation(this@AndroidDevice) RemoteFileManager.removeRemoteDirectory(ddmsDevice) RemoteFileManager.createRemoteDirectory(ddmsDevice) @@ -117,7 +123,7 @@ class AndroidDevice(val ddmsDevice: IDevice) : Device { } override fun dispose() { - deviceContext.close() + dispatcher.close() } private fun clearLogcat(device: IDevice) { diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt index 7e39a9a2c..6e68c2bdb 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDeviceProvider.kt @@ -11,20 +11,21 @@ import com.malinskiy.marathon.device.DeviceProvider.DeviceEvent.DeviceDisconnect import com.malinskiy.marathon.exceptions.NoDevicesException import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.vendor.VendorConfiguration -import kotlinx.coroutines.experimental.NonCancellable.isActive -import kotlinx.coroutines.experimental.channels.Channel -import kotlinx.coroutines.experimental.delay -import kotlinx.coroutines.experimental.launch -import kotlinx.coroutines.experimental.newFixedThreadPoolContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.newFixedThreadPoolContext import java.nio.file.Paths import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap +import kotlin.coroutines.CoroutineContext private const val DEFAULT_DDM_LIB_TIMEOUT = 30000 private const val DEFAULT_DDM_LIB_SLEEP_TIME = 500 -class AndroidDeviceProvider : DeviceProvider { - +class AndroidDeviceProvider : DeviceProvider, CoroutineScope { private val logger = MarathonLogging.logger("AndroidDeviceProvider") private lateinit var adb: AndroidDebugBridge @@ -32,6 +33,8 @@ class AndroidDeviceProvider : DeviceProvider { private val channel: Channel = unboundedChannel() private val devices: ConcurrentMap = ConcurrentHashMap() private val bootWaitContext = newFixedThreadPoolContext(4, "AndroidDeviceProvider-BootWait") + override val coroutineContext: CoroutineContext + get() = bootWaitContext override fun initialize(vendorConfiguration: VendorConfiguration) { if (vendorConfiguration !is AndroidConfiguration) { @@ -64,7 +67,7 @@ class AndroidDeviceProvider : DeviceProvider { override fun deviceConnected(device: IDevice?) { device?.let { - launch(context = bootWaitContext) { + launch { val maybeNewAndroidDevice = AndroidDevice(it) val healthy = maybeNewAndroidDevice.healthy logger.debug { "Device ${maybeNewAndroidDevice.serialNumber} connected channel.isFull = ${channel.isFull}. Healthy = $healthy" } @@ -80,7 +83,7 @@ class AndroidDeviceProvider : DeviceProvider { override fun deviceDisconnected(device: IDevice?) { device?.let { - launch(context = bootWaitContext) { + launch { logger.debug { "Device ${device.serialNumber} disconnected" } matchDdmsToDevice(it)?.let { notifyDisconnected(it) diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/AndroidDeviceTestRunner.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/AndroidDeviceTestRunner.kt index b4e96a606..97aa58819 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/AndroidDeviceTestRunner.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/AndroidDeviceTestRunner.kt @@ -27,7 +27,7 @@ import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.report.logs.LogWriter import com.malinskiy.marathon.test.TestBatch import com.malinskiy.marathon.test.toTestName -import kotlinx.coroutines.experimental.CompletableDeferred +import kotlinx.coroutines.CompletableDeferred import java.io.IOException import java.util.concurrent.TimeUnit diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/TestRunResultsListener.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/TestRunResultsListener.kt index cac0a26aa..68fd6b689 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/TestRunResultsListener.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/TestRunResultsListener.kt @@ -11,7 +11,7 @@ import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.test.Test import com.malinskiy.marathon.test.TestBatch import com.malinskiy.marathon.test.toTestName -import kotlinx.coroutines.experimental.CompletableDeferred +import kotlinx.coroutines.CompletableDeferred import com.android.ddmlib.testrunner.TestResult as DdmLibTestResult import com.android.ddmlib.testrunner.TestRunResult as DdmLibTestRunResult diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturer.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturer.kt index 3a02bf360..67b887297 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturer.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturer.kt @@ -10,8 +10,9 @@ import com.malinskiy.marathon.device.DevicePoolId import com.malinskiy.marathon.io.FileManager import com.malinskiy.marathon.io.FileType import com.malinskiy.marathon.log.MarathonLogging -import kotlinx.coroutines.experimental.NonCancellable.isActive -import kotlinx.coroutines.experimental.delay +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive import org.imgscalr.Scalr import java.awt.image.BufferedImage import java.awt.image.BufferedImage.TYPE_INT_ARGB @@ -27,7 +28,7 @@ class ScreenCapturer(val device: AndroidDevice, private val fileManager: FileManager, val test: TestIdentifier) { - suspend fun start() { + suspend fun start() = coroutineScope { val outputStream = FileImageOutputStream(fileManager.createFile(FileType.SCREENSHOT, poolId, device, test.toTest())) val writer = GifSequenceWriter(outputStream, TYPE_INT_ARGB, DELAY, true) while (isActive) { diff --git a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt index b399449d2..acd71458d 100644 --- a/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt +++ b/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/executor/listeners/screenshot/ScreenCapturerTestRunListener.kt @@ -8,22 +8,26 @@ import com.malinskiy.marathon.device.DevicePoolId import com.malinskiy.marathon.io.FileManager import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.test.toSimpleSafeTestName -import kotlinx.coroutines.experimental.Job -import kotlinx.coroutines.experimental.async -import kotlinx.coroutines.experimental.newFixedThreadPoolContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.newFixedThreadPoolContext +import kotlin.coroutines.CoroutineContext class ScreenCapturerTestRunListener(private val fileManager: FileManager, private val pool: DevicePoolId, - private val device: AndroidDevice) : NoOpTestRunListener() { + private val device: AndroidDevice) : NoOpTestRunListener(), CoroutineScope { private var screenCapturerJob: Job? = null private val logger = MarathonLogging.logger(ScreenCapturerTestRunListener::class.java.simpleName) private val threadPoolDispatcher = newFixedThreadPoolContext(1, "ScreenCapturer - ${device.serialNumber}") + override val coroutineContext: CoroutineContext + get() = threadPoolDispatcher override fun testStarted(test: TestIdentifier) { super.testStarted(test) logger.debug { "Starting recording for ${test.toTest().toSimpleSafeTestName()}" } - screenCapturerJob = async (context = threadPoolDispatcher) { + screenCapturerJob = async { ScreenCapturer(device, pool, fileManager, test).start() } } diff --git a/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/AndroidDeviceSpek.kt b/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/AndroidDeviceSpek.kt index 6a5b8c900..8100f03ea 100644 --- a/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/AndroidDeviceSpek.kt +++ b/vendor-android/src/test/kotlin/com/malinskiy/marathon/android/AndroidDeviceSpek.kt @@ -2,7 +2,7 @@ package com.malinskiy.marathon.android import com.android.ddmlib.IDevice import com.android.sdklib.AndroidVersion -import com.nhaarman.mockito_kotlin.whenever +import com.nhaarman.mockitokotlin2.whenever import org.amshove.kluent.mock import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeEqualTo diff --git a/vendor-ios/build.gradle.kts b/vendor-ios/build.gradle.kts index ef8d0aa79..ed82e9cd3 100644 --- a/vendor-ios/build.gradle.kts +++ b/vendor-ios/build.gradle.kts @@ -10,12 +10,11 @@ plugins { id("org.junit.platform.gradle.plugin") } -kotlin.experimental.coroutines = Coroutines.ENABLE - dependencies { implementation(Libraries.kotlinStdLib) implementation(Libraries.kotlinCoroutines) implementation(Libraries.kotlinLogging) + implementation(Libraries.kotlinReflect) implementation(Libraries.slf4jAPI) implementation(Libraries.logbackClassic) implementation(Libraries.ddPlist) @@ -27,6 +26,7 @@ dependencies { implementation(Libraries.jacksonYaml) implementation(project(":core")) testImplementation(TestLibraries.kluent) + testImplementation(TestLibraries.mockitoKotlin) testImplementation(TestLibraries.spekAPI) testRuntime(TestLibraries.spekJUnitPlatformEngine) testImplementation(TestLibraries.testContainers) @@ -36,7 +36,7 @@ Deployment.initialize(project) tasks.withType { kotlinOptions.jvmTarget = "1.8" - kotlinOptions.apiVersion = "1.2" + kotlinOptions.apiVersion = "1.3" } junitPlatform { diff --git a/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDevice.kt b/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDevice.kt index 9d3c1b879..d582fd275 100644 --- a/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDevice.kt +++ b/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDevice.kt @@ -25,20 +25,22 @@ import com.malinskiy.marathon.ios.xctestrun.Xctestrun import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.test.TestBatch import com.malinskiy.marathon.time.SystemTimer -import kotlinx.coroutines.experimental.CompletableDeferred -import kotlinx.coroutines.experimental.launch -import kotlinx.coroutines.experimental.newFixedThreadPoolContext +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.newFixedThreadPoolContext import net.schmizz.sshj.connection.ConnectionException import net.schmizz.sshj.transport.TransportException import java.io.File import java.io.IOException import java.util.concurrent.TimeUnit +import kotlin.coroutines.CoroutineContext private const val HOSTNAME = "localhost" class IOSDevice(val udid: String, val hostCommandExecutor: CommandExecutor, - val gson: Gson) : Device { + val gson: Gson) : Device, CoroutineScope { val logger = MarathonLogging.logger("${javaClass.simpleName}($udid)") val simctl = Simctl() @@ -47,6 +49,8 @@ class IOSDevice(val udid: String, private val deviceType: String? private val deviceContext = newFixedThreadPoolContext(1, udid) + override val coroutineContext: CoroutineContext + get() = deviceContext init { val device = simctl.list(this, gson).find { it.udid == udid } @@ -79,7 +83,7 @@ class IOSDevice(val udid: String, deferred: CompletableDeferred, progressReporter: ProgressReporter) { - launch(deviceContext) { + launch { val iosConfiguration = configuration.vendorConfiguration as IOSConfiguration val fileManager = FileManager(configuration.outputDir) val testLogListener = TestLogListener() @@ -160,7 +164,7 @@ class IOSDevice(val udid: String, } override suspend fun prepare(configuration: Configuration) { - launch(deviceContext) { + launch { RemoteFileManager.createRemoteDirectory(this@IOSDevice) val sshjCommandExecutor = hostCommandExecutor as SshjCommandExecutor diff --git a/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt b/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt index 15e40ceff..d4af86584 100644 --- a/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt +++ b/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt @@ -13,7 +13,7 @@ import com.malinskiy.marathon.ios.simctl.model.SimctlDeviceList import com.malinskiy.marathon.ios.simctl.model.SimctlDeviceListDeserializer import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.vendor.VendorConfiguration -import kotlinx.coroutines.experimental.channels.Channel +import kotlinx.coroutines.channels.Channel class IOSDeviceProvider : DeviceProvider { diff --git a/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/device/LocalListSimulatorProvider.kt b/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/device/LocalListSimulatorProvider.kt index e9272ef94..a9cacd84b 100644 --- a/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/device/LocalListSimulatorProvider.kt +++ b/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/device/LocalListSimulatorProvider.kt @@ -9,15 +9,20 @@ import com.malinskiy.marathon.ios.IOSConfiguration import com.malinskiy.marathon.ios.IOSDevice import com.malinskiy.marathon.ios.cmd.remote.SshjCommandExecutor import com.malinskiy.marathon.log.MarathonLogging -import kotlinx.coroutines.experimental.channels.Channel -import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.coroutines.newFixedThreadPoolContext import java.io.File import java.net.InetAddress +import kotlin.coroutines.CoroutineContext class LocalListSimulatorProvider(private val channel: Channel, private val configuration: IOSConfiguration, yamlObjectMapper: ObjectMapper, - private val gson: Gson) : SimulatorProvider { + private val gson: Gson) : SimulatorProvider, CoroutineScope { + + override val coroutineContext: CoroutineContext by lazy { newFixedThreadPoolContext(1, "LocalListSimulatorProvider") } private val logger = MarathonLogging.logger(LocalListSimulatorProvider::class.java.simpleName) diff --git a/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/logparser/listener/ProgressReportingListener.kt b/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/logparser/listener/ProgressReportingListener.kt index 10bc24d64..e8b80cc0a 100644 --- a/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/logparser/listener/ProgressReportingListener.kt +++ b/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/logparser/listener/ProgressReportingListener.kt @@ -10,7 +10,7 @@ import com.malinskiy.marathon.execution.progress.ProgressReporter import com.malinskiy.marathon.test.Test import com.malinskiy.marathon.test.TestBatch import com.malinskiy.marathon.test.toSafeTestName -import kotlinx.coroutines.experimental.CompletableDeferred +import kotlinx.coroutines.CompletableDeferred class ProgressReportingListener(private val device: Device, private val poolId: DevicePoolId, diff --git a/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/DerivedDataManagerSpek.kt b/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/DerivedDataManagerSpek.kt index 63f4228d9..985fdbad4 100644 --- a/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/DerivedDataManagerSpek.kt +++ b/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/DerivedDataManagerSpek.kt @@ -3,8 +3,8 @@ package com.malinskiy.marathon.ios import com.malinskiy.marathon.execution.Configuration import com.malinskiy.marathon.log.MarathonLogging import com.malinskiy.marathon.report.html.relativePathTo -import com.nhaarman.mockito_kotlin.mock -import com.nhaarman.mockito_kotlin.whenever +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.whenever import org.amshove.kluent.shouldEqual import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.describe diff --git a/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/logparser/ProgressParserSpek.kt b/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/logparser/ProgressParserSpek.kt index e3bd89a39..05df87317 100644 --- a/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/logparser/ProgressParserSpek.kt +++ b/vendor-ios/src/test/kotlin/com/malinskiy/marathon/ios/logparser/ProgressParserSpek.kt @@ -4,9 +4,9 @@ import com.malinskiy.marathon.ios.logparser.formatter.PackageNameFormatter import com.malinskiy.marathon.ios.logparser.listener.TestRunListener import com.malinskiy.marathon.test.Test import com.malinskiy.marathon.time.Timer -import com.nhaarman.mockito_kotlin.atLeastOnce -import com.nhaarman.mockito_kotlin.reset -import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockitokotlin2.atLeastOnce +import com.nhaarman.mockitokotlin2.reset +import com.nhaarman.mockitokotlin2.verify import org.amshove.kluent.Verify import org.amshove.kluent.When import org.amshove.kluent.any diff --git a/vendor-test/build.gradle.kts b/vendor-test/build.gradle.kts index 8296ef8bd..ef6d84af8 100644 --- a/vendor-test/build.gradle.kts +++ b/vendor-test/build.gradle.kts @@ -9,18 +9,18 @@ plugins { id("org.jetbrains.kotlin.jvm") } -kotlin.experimental.coroutines = Coroutines.ENABLE - dependencies { implementation(Libraries.kotlinStdLib) implementation(Libraries.kotlinCoroutines) implementation(Libraries.kotlinLogging) + implementation(Libraries.kotlinReflect) implementation(TestLibraries.spekAPI) implementation(TestLibraries.kluent) + implementation(TestLibraries.mockitoKotlin) implementation(project(":core")) } tasks.withType { kotlinOptions.jvmTarget = "1.8" - kotlinOptions.apiVersion = "1.2" + kotlinOptions.apiVersion = "1.3" } \ No newline at end of file diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt index 7984c6764..af33df0a0 100644 --- a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDevice.kt @@ -12,8 +12,8 @@ import com.malinskiy.marathon.execution.TestResult import com.malinskiy.marathon.execution.TestStatus import com.malinskiy.marathon.execution.progress.ProgressReporter import com.malinskiy.marathon.log.MarathonLogging -import kotlinx.coroutines.experimental.CompletableDeferred -import kotlinx.coroutines.experimental.delay +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.delay class StubDevice(private val prepareTimeMillis: Long = 5000L, private val testTimeMillis: Long = 5000L, diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt index a4c7b94dc..d032db85f 100644 --- a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/StubDeviceProvider.kt @@ -3,12 +3,16 @@ package com.malinskiy.marathon.test import com.malinskiy.marathon.actor.unboundedChannel import com.malinskiy.marathon.device.DeviceProvider import com.malinskiy.marathon.vendor.VendorConfiguration -import kotlinx.coroutines.experimental.channels.Channel -import kotlinx.coroutines.experimental.launch -import kotlin.coroutines.experimental.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext -class StubDeviceProvider : DeviceProvider { - lateinit var coroutineContext: CoroutineContext +class StubDeviceProvider : DeviceProvider, CoroutineScope { + lateinit var context: CoroutineContext + + override val coroutineContext: kotlin.coroutines.CoroutineContext + get() = context private val channel: Channel = unboundedChannel() var providingLogic: (suspend (Channel) -> Unit)? = null diff --git a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/ConfigurationFactory.kt b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/ConfigurationFactory.kt index 2e5108d95..8e1815365 100644 --- a/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/ConfigurationFactory.kt +++ b/vendor-test/src/main/kotlin/com/malinskiy/marathon/test/factory/ConfigurationFactory.kt @@ -6,8 +6,7 @@ import com.malinskiy.marathon.test.Mocks import com.malinskiy.marathon.test.StubDeviceProvider import com.malinskiy.marathon.test.Test import com.malinskiy.marathon.test.TestVendorConfiguration -import com.malinskiy.marathon.vendor.VendorConfiguration -import kotlinx.coroutines.experimental.channels.Channel +import kotlinx.coroutines.channels.Channel import org.amshove.kluent.When import org.amshove.kluent.`it returns` import org.amshove.kluent.any From cecc5b1157f3549010d7ca5d857bb1e407ff777b Mon Sep 17 00:00:00 2001 From: Anton Malinskiy Date: Fri, 30 Nov 2018 22:43:30 +0700 Subject: [PATCH 11/14] Fixed the freeze when disconnecting device --- cli/build.gradle.kts | 4 ++-- .../kotlin/com/malinskiy/marathon/actor/extensions.kt | 6 ++++++ .../malinskiy/marathon/execution/DevicePoolActor.kt | 11 ++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index 8fc238027..a5adb818e 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -13,9 +13,9 @@ plugins { id("de.fuerstenau.buildconfig") version "1.1.8" } -val debugCoroutines = true +val debugCoroutines = false val coroutinesJvmOptions = when(debugCoroutines) { - true -> "-Dkotlinx.coroutines.debug" + true -> "-Dkotlinx.coroutines.debug=on" else -> "" } diff --git a/core/src/main/kotlin/com/malinskiy/marathon/actor/extensions.kt b/core/src/main/kotlin/com/malinskiy/marathon/actor/extensions.kt index 31065f2b0..2efdbb2b6 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/actor/extensions.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/actor/extensions.kt @@ -1,5 +1,11 @@ package com.malinskiy.marathon.actor import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.SendChannel fun unboundedChannel() = Channel(Channel.UNLIMITED) + +suspend fun SendChannel.safeSend(element: T) { + if(isClosedForSend) return + send(element) +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt index 9d5595074..9b35dbe25 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt @@ -1,6 +1,7 @@ package com.malinskiy.marathon.execution import com.malinskiy.marathon.actor.Actor +import com.malinskiy.marathon.actor.safeSend import com.malinskiy.marathon.analytics.Analytics import com.malinskiy.marathon.device.Device import com.malinskiy.marathon.device.DevicePoolId @@ -56,7 +57,7 @@ class DevicePoolActor(private val poolId: DevicePoolId, devices.filter { !it.value.isClosedForSend }.forEach { - it.value.send(DeviceEvent.WakeUp) + it.value.safeSend(DeviceEvent.WakeUp) } } @@ -64,7 +65,7 @@ class DevicePoolActor(private val poolId: DevicePoolId, devices.filterValues { !it.isClosedForSend }.forEach { - it.value.send(DeviceEvent.Terminate) + it.value.safeSend(DeviceEvent.Terminate) } terminate() } @@ -84,7 +85,7 @@ class DevicePoolActor(private val poolId: DevicePoolId, private suspend fun executeBatch(device: Device, batch: TestBatch) { devices[device.serialNumber]?.run { if (!isClosedForSend) { - send(DeviceEvent.Execute(batch)) + safeSend(DeviceEvent.Execute(batch)) } } } @@ -97,7 +98,7 @@ class DevicePoolActor(private val poolId: DevicePoolId, private suspend fun removeDevice(device: Device) { logger.debug { "remove device ${device.serialNumber}" } val actor = devices.remove(device.serialNumber) - actor?.send(DeviceEvent.Terminate) + actor?.safeSend(DeviceEvent.Terminate) logger.debug { "devices.size = ${devices.size}" } if (noActiveDevices()) { //TODO check if we still have tests and timeout if nothing available @@ -116,6 +117,6 @@ class DevicePoolActor(private val poolId: DevicePoolId, logger.debug { "add device ${device.serialNumber}" } val actor = DeviceActor(poolId, this, configuration, device, progressReporter, poolJob, coroutineContext) devices[device.serialNumber] = actor - actor.send(DeviceEvent.Initialize) + actor.safeSend(DeviceEvent.Initialize) } } From baca67229d546ba987c4197ca1549424d844699c Mon Sep 17 00:00:00 2001 From: Anton Malinskiy Date: Fri, 30 Nov 2018 23:02:17 +0700 Subject: [PATCH 12/14] Fix comments --- .../marathon/execution/DevicePoolActor.kt | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt index 9b35dbe25..2f472c0fb 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/DevicePoolActor.kt @@ -54,18 +54,14 @@ class DevicePoolActor(private val poolId: DevicePoolId, private suspend fun notifyDevices() { logger.debug { "Notify devices" } - devices.filter { - !it.value.isClosedForSend - }.forEach { - it.value.safeSend(DeviceEvent.WakeUp) + devices.values.forEach { + it.safeSend(DeviceEvent.WakeUp) } } private suspend fun onQueueTerminated() { - devices.filterValues { - !it.isClosedForSend - }.forEach { - it.value.safeSend(DeviceEvent.Terminate) + devices.values.forEach { + it.safeSend(DeviceEvent.Terminate) } terminate() } @@ -84,9 +80,7 @@ class DevicePoolActor(private val poolId: DevicePoolId, private suspend fun executeBatch(device: Device, batch: TestBatch) { devices[device.serialNumber]?.run { - if (!isClosedForSend) { - safeSend(DeviceEvent.Execute(batch)) - } + safeSend(DeviceEvent.Execute(batch)) } } From d712ecb96d4b511fb0c9c57389fc8dbaea9b6a7a Mon Sep 17 00:00:00 2001 From: Anton Malinskiy Date: Tue, 4 Dec 2018 23:20:16 +0700 Subject: [PATCH 13/14] Fix no debug coroutines mode for CLI --- cli/build.gradle.kts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index a5adb818e..1b9c30659 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -14,15 +14,14 @@ plugins { } val debugCoroutines = false -val coroutinesJvmOptions = when(debugCoroutines) { - true -> "-Dkotlinx.coroutines.debug=on" - else -> "" -} application { mainClassName = "com.malinskiy.marathon.cli.ApplicationViewKt" applicationName = "marathon" - applicationDefaultJvmArgs = listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044", coroutinesJvmOptions) + applicationDefaultJvmArgs = when(debugCoroutines) { + true -> listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044", "-Dkotlinx.coroutines.debug=on") + else -> listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1044") + } } distributions { From 4310cdef02517a6abc0f7789bad3f61787f89496 Mon Sep 17 00:00:00 2001 From: "Panforov, Yurii (Agoda)" Date: Tue, 11 Dec 2018 18:58:30 +0700 Subject: [PATCH 14/14] Verify lateinit initialization --- .../kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt b/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt index d4af86584..0fdbdc02b 100644 --- a/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt +++ b/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSDeviceProvider.kt @@ -23,7 +23,9 @@ class IOSDeviceProvider : DeviceProvider { override fun terminate() { logger.debug { "Terminating IOS device provider" } - simulatorProvider.stop() + if (::simulatorProvider.isInitialized) { + simulatorProvider.stop() + } channel.close() }