From 2b92fec60259256975a58757ecf927ffb4e91a38 Mon Sep 17 00:00:00 2001 From: Pawel Pasterz Date: Sat, 7 Mar 2020 18:05:17 +0100 Subject: [PATCH 1/7] Make Utils class more Kotlinish --- test_runner/src/main/kotlin/ftl/Main.kt | 4 +- .../src/main/kotlin/ftl/args/AndroidArgs.kt | 2 +- .../main/kotlin/ftl/args/AndroidTestShard.kt | 6 +- .../main/kotlin/ftl/args/ArgsFileVisitor.kt | 2 +- .../src/main/kotlin/ftl/args/ArgsHelper.kt | 27 +-- .../src/main/kotlin/ftl/args/IosArgs.kt | 2 +- .../kotlin/ftl/args/yml/YamlDeprecated.kt | 6 +- .../src/main/kotlin/ftl/config/Device.kt | 2 +- .../main/kotlin/ftl/config/FtlConstants.kt | 4 +- .../main/kotlin/ftl/gc/GcAndroidTestMatrix.kt | 4 +- .../src/main/kotlin/ftl/gc/GcIosTestMatrix.kt | 4 +- .../src/main/kotlin/ftl/gc/GcStorage.kt | 4 +- .../src/main/kotlin/ftl/gc/GcTestMatrix.kt | 2 +- test_runner/src/main/kotlin/ftl/ios/Parse.kt | 2 +- .../src/main/kotlin/ftl/reports/CostReport.kt | 4 +- .../kotlin/ftl/reports/HtmlErrorReport.kt | 2 +- .../main/kotlin/ftl/reports/JUnitReport.kt | 2 +- .../kotlin/ftl/reports/MatrixResultsReport.kt | 4 +- .../main/kotlin/ftl/run/GenericTestRunner.kt | 4 +- .../src/main/kotlin/ftl/run/TestRunner.kt | 6 +- .../src/main/kotlin/ftl/shard/Shard.kt | 2 +- .../src/main/kotlin/ftl/util/MatrixUtil.kt | 1 - test_runner/src/main/kotlin/ftl/util/Utils.kt | 207 +++++++++--------- test_runner/src/test/kotlin/Debug.kt | 2 +- .../kotlin/ftl/run/GenericTestRunnerTest.kt | 2 +- .../src/test/kotlin/ftl/util/UtilsTest.kt | 6 +- 26 files changed, 155 insertions(+), 158 deletions(-) diff --git a/test_runner/src/main/kotlin/ftl/Main.kt b/test_runner/src/main/kotlin/ftl/Main.kt index 544e470a40..6e7093dc9d 100644 --- a/test_runner/src/main/kotlin/ftl/Main.kt +++ b/test_runner/src/main/kotlin/ftl/Main.kt @@ -7,8 +7,8 @@ import ftl.cli.firebase.RefreshCommand import ftl.cli.firebase.test.AndroidCommand import ftl.cli.firebase.test.IosCommand import ftl.log.setDebugLogging -import ftl.util.Utils.readRevision -import ftl.util.Utils.readVersion +import ftl.util.readRevision +import ftl.util.readVersion import picocli.CommandLine @CommandLine.Command( diff --git a/test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt b/test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt index 3a5cece0fd..9500696f4c 100644 --- a/test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt +++ b/test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt @@ -25,7 +25,7 @@ import ftl.args.yml.YamlDeprecated import ftl.cli.firebase.test.android.AndroidRunCommand import ftl.config.Device import ftl.config.FtlConstants -import ftl.util.Utils.fatalError +import ftl.util.fatalError import java.nio.file.Files import java.nio.file.Path diff --git a/test_runner/src/main/kotlin/ftl/args/AndroidTestShard.kt b/test_runner/src/main/kotlin/ftl/args/AndroidTestShard.kt index b6f2de0396..17db1fcb96 100644 --- a/test_runner/src/main/kotlin/ftl/args/AndroidTestShard.kt +++ b/test_runner/src/main/kotlin/ftl/args/AndroidTestShard.kt @@ -7,7 +7,7 @@ import ftl.filter.TestFilter import ftl.filter.TestFilters import ftl.gc.GcStorage import ftl.util.FlankTestMethod -import ftl.util.Utils +import ftl.util.fatalError import kotlinx.coroutines.runBlocking object AndroidTestShard { @@ -32,7 +32,7 @@ object AndroidTestShard { val shouldThrowErrorIfMissingTests = allTestMethods.isEmpty() && !args.disableSharding when { shouldIgnoreMissingTests -> return mutableListOf() - shouldThrowErrorIfMissingTests -> throw IllegalStateException(Utils.fatalError("Test APK has no tests")) + shouldThrowErrorIfMissingTests -> throw IllegalStateException(fatalError("Test APK has no tests")) } val testFilter = TestFilters.fromTestTargets(args.testTargets) return allTestMethods filterWith testFilter @@ -44,7 +44,7 @@ object AndroidTestShard { .map { FlankTestMethod("class ${it.testName}", it.isIgnored) } .toList() .also { - require(FtlConstants.useMock || it.isNotEmpty()) { Utils.fatalError("All tests filtered out") } + require(FtlConstants.useMock || it.isNotEmpty()) { fatalError("All tests filtered out") } } } diff --git a/test_runner/src/main/kotlin/ftl/args/ArgsFileVisitor.kt b/test_runner/src/main/kotlin/ftl/args/ArgsFileVisitor.kt index d1b256d56a..087abba920 100644 --- a/test_runner/src/main/kotlin/ftl/args/ArgsFileVisitor.kt +++ b/test_runner/src/main/kotlin/ftl/args/ArgsFileVisitor.kt @@ -1,6 +1,6 @@ package ftl.args -import ftl.util.Utils.fatalError +import ftl.util.fatalError import java.io.IOException import java.lang.RuntimeException import java.nio.file.FileSystems diff --git a/test_runner/src/main/kotlin/ftl/args/ArgsHelper.kt b/test_runner/src/main/kotlin/ftl/args/ArgsHelper.kt index 5afec4e20a..83079d60cb 100644 --- a/test_runner/src/main/kotlin/ftl/args/ArgsHelper.kt +++ b/test_runner/src/main/kotlin/ftl/args/ArgsHelper.kt @@ -24,7 +24,8 @@ import ftl.shard.Shard import ftl.shard.StringShards import ftl.shard.stringShards import ftl.util.FlankTestMethod -import ftl.util.Utils +import ftl.util.assertNotEmpty +import ftl.util.fatalError import java.io.File import java.net.URI import java.nio.file.Files @@ -49,29 +50,29 @@ object ArgsHelper { fun assertFileExists(file: String, name: String) { if (!File(file).exists()) { - Utils.fatalError("'$file' $name doesn't exist") + fatalError("'$file' $name doesn't exist") } } fun assertCommonProps(args: IArgs) { - Utils.assertNotEmpty( + assertNotEmpty( args.project, "The project is not set. Define GOOGLE_CLOUD_PROJECT, set project in flank.yml\n" + "or save service account credential to ${defaultCredentialPath}\n" + " See https://github.com/GoogleCloudPlatform/google-cloud-java#specifying-a-project-id" ) if (args.maxTestShards !in IArgs.AVAILABLE_SHARD_COUNT_RANGE && args.maxTestShards != -1) - Utils.fatalError("max-test-shards must be >= 1 and <= 50, or -1. But current is ${args.maxTestShards}") + fatalError("max-test-shards must be >= 1 and <= 50, or -1. But current is ${args.maxTestShards}") - if (args.shardTime <= 0 && args.shardTime != -1) Utils.fatalError("shard-time must be >= 1 or -1") - if (args.repeatTests < 1) Utils.fatalError("num-test-runs must be >= 1") + if (args.shardTime <= 0 && args.shardTime != -1) fatalError("shard-time must be >= 1 or -1") + if (args.repeatTests < 1) fatalError("num-test-runs must be >= 1") if (args.smartFlankGcsPath.isNotEmpty()) { if (!args.smartFlankGcsPath.startsWith(GCS_PREFIX)) { - Utils.fatalError("smart-flank-gcs-path must start with gs://") + fatalError("smart-flank-gcs-path must start with gs://") } if (args.smartFlankGcsPath.count { it == '/' } <= 2 || !args.smartFlankGcsPath.endsWith(".xml")) { - Utils.fatalError("smart-flank-gcs-path must be in the format gs://bucket/foo.xml") + fatalError("smart-flank-gcs-path must be in the format gs://bucket/foo.xml") } } } @@ -87,9 +88,9 @@ object ArgsHelper { val filePaths = walkFileTree(file) if (filePaths.size > 1) { - Utils.fatalError("'$file' ($filePath) matches multiple files: $filePaths") + fatalError("'$file' ($filePath) matches multiple files: $filePaths") } else if (filePaths.isEmpty()) { - Utils.fatalError("'$file' not found ($filePath)") + fatalError("'$file' not found ($filePath)") } return filePaths.first().toAbsolutePath().normalize().toString() @@ -107,7 +108,7 @@ object ArgsHelper { val blob = GcStorage.storage.get(bucket, path) if (blob == null) { - Utils.fatalError("The file at '$uri' does not exist") + fatalError("The file at '$uri' does not exist") } } @@ -119,8 +120,8 @@ object ArgsHelper { ) { val missingMethods = testTargets - validTestMethods - if (!skipValidation && missingMethods.isNotEmpty()) Utils.fatalError("$from is missing methods: $missingMethods.\nValid methods:\n$validTestMethods") - if (validTestMethods.isEmpty()) Utils.fatalError("$from has no tests") + if (!skipValidation && missingMethods.isNotEmpty()) fatalError("$from is missing methods: $missingMethods.\nValid methods:\n$validTestMethods") + if (validTestMethods.isEmpty()) fatalError("$from has no tests") } fun createJunitBucket(projectId: String, junitGcsPath: String) { diff --git a/test_runner/src/main/kotlin/ftl/args/IosArgs.kt b/test_runner/src/main/kotlin/ftl/args/IosArgs.kt index 3ad4e2b258..471755baaf 100644 --- a/test_runner/src/main/kotlin/ftl/args/IosArgs.kt +++ b/test_runner/src/main/kotlin/ftl/args/IosArgs.kt @@ -22,7 +22,7 @@ import ftl.config.FtlConstants import ftl.ios.IosCatalog import ftl.ios.Xctestrun import ftl.util.FlankTestMethod -import ftl.util.Utils.fatalError +import ftl.util.fatalError import java.nio.file.Files import java.nio.file.Path diff --git a/test_runner/src/main/kotlin/ftl/args/yml/YamlDeprecated.kt b/test_runner/src/main/kotlin/ftl/args/yml/YamlDeprecated.kt index b3d718b1f3..a364d8591e 100644 --- a/test_runner/src/main/kotlin/ftl/args/yml/YamlDeprecated.kt +++ b/test_runner/src/main/kotlin/ftl/args/yml/YamlDeprecated.kt @@ -5,9 +5,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.MissingNode import com.fasterxml.jackson.databind.node.ObjectNode import ftl.args.ArgsHelper.yamlMapper -import ftl.args.yml.YamlDeprecated.replace -import ftl.util.Utils -import ftl.util.Utils.fatalError +import ftl.util.fatalError import java.nio.file.Files import java.nio.file.Path @@ -140,7 +138,7 @@ object YamlDeprecated { if (error) { val platform = if (android) "android" else "ios" - Utils.fatalError("Invalid keys detected! Auto fix with: flank $platform doctor --fix") + fatalError("Invalid keys detected! Auto fix with: flank $platform doctor --fix") } return data diff --git a/test_runner/src/main/kotlin/ftl/config/Device.kt b/test_runner/src/main/kotlin/ftl/config/Device.kt index 860b8aa1b1..d7b7771348 100644 --- a/test_runner/src/main/kotlin/ftl/config/Device.kt +++ b/test_runner/src/main/kotlin/ftl/config/Device.kt @@ -2,7 +2,7 @@ package ftl.config import ftl.config.FtlConstants.defaultLocale import ftl.config.FtlConstants.defaultOrientation -import ftl.util.Utils.trimStartLine +import ftl.util.trimStartLine data class Device( val model: String, diff --git a/test_runner/src/main/kotlin/ftl/config/FtlConstants.kt b/test_runner/src/main/kotlin/ftl/config/FtlConstants.kt index 4d19c6b0de..4e81d9b6e4 100644 --- a/test_runner/src/main/kotlin/ftl/config/FtlConstants.kt +++ b/test_runner/src/main/kotlin/ftl/config/FtlConstants.kt @@ -15,8 +15,8 @@ import ftl.args.IArgs import ftl.args.IosArgs import ftl.gc.UserAuth import ftl.http.HttpTimeoutIncrease -import ftl.util.Utils.fatalError -import ftl.util.Utils.readRevision +import ftl.util.fatalError +import ftl.util.readRevision import java.io.IOException import java.nio.file.Path import java.nio.file.Paths diff --git a/test_runner/src/main/kotlin/ftl/gc/GcAndroidTestMatrix.kt b/test_runner/src/main/kotlin/ftl/gc/GcAndroidTestMatrix.kt index 03d2c50bf2..e3a466bf64 100644 --- a/test_runner/src/main/kotlin/ftl/gc/GcAndroidTestMatrix.kt +++ b/test_runner/src/main/kotlin/ftl/gc/GcAndroidTestMatrix.kt @@ -19,8 +19,8 @@ import com.google.api.services.testing.model.TestSpecification import com.google.api.services.testing.model.TestTargetsForShard import com.google.api.services.testing.model.ToolResultsHistory import ftl.args.AndroidArgs -import ftl.util.Utils.fatalError -import ftl.util.Utils.join +import ftl.util.fatalError +import ftl.util.join import ftl.util.testTimeoutToSeconds object GcAndroidTestMatrix { diff --git a/test_runner/src/main/kotlin/ftl/gc/GcIosTestMatrix.kt b/test_runner/src/main/kotlin/ftl/gc/GcIosTestMatrix.kt index fbe372d8b9..69037b3031 100644 --- a/test_runner/src/main/kotlin/ftl/gc/GcIosTestMatrix.kt +++ b/test_runner/src/main/kotlin/ftl/gc/GcIosTestMatrix.kt @@ -17,8 +17,8 @@ import ftl.args.IosArgs import ftl.ios.Xctestrun import ftl.ios.Xctestrun.toByteArray import ftl.util.ShardCounter -import ftl.util.Utils.fatalError -import ftl.util.Utils.join +import ftl.util.fatalError +import ftl.util.join import ftl.util.testTimeoutToSeconds object GcIosTestMatrix { diff --git a/test_runner/src/main/kotlin/ftl/gc/GcStorage.kt b/test_runner/src/main/kotlin/ftl/gc/GcStorage.kt index 4608de2e08..c7779b937e 100644 --- a/test_runner/src/main/kotlin/ftl/gc/GcStorage.kt +++ b/test_runner/src/main/kotlin/ftl/gc/GcStorage.kt @@ -13,8 +13,8 @@ import ftl.reports.xml.model.JUnitTestResult import ftl.reports.xml.parseAllSuitesXml import ftl.reports.xml.xmlToString import ftl.util.ProgressBar -import ftl.util.Utils.fatalError -import ftl.util.Utils.join +import ftl.util.fatalError +import ftl.util.join import java.io.File import java.io.FileOutputStream import java.net.URI diff --git a/test_runner/src/main/kotlin/ftl/gc/GcTestMatrix.kt b/test_runner/src/main/kotlin/ftl/gc/GcTestMatrix.kt index 1ed00fc6cd..e384f0698b 100644 --- a/test_runner/src/main/kotlin/ftl/gc/GcTestMatrix.kt +++ b/test_runner/src/main/kotlin/ftl/gc/GcTestMatrix.kt @@ -4,7 +4,7 @@ import com.google.api.services.testing.model.CancelTestMatrixResponse import com.google.api.services.testing.model.TestMatrix import ftl.args.IArgs import ftl.http.executeWithRetry -import ftl.util.Utils.sleep +import ftl.util.sleep import java.time.Duration.ofHours object GcTestMatrix { diff --git a/test_runner/src/main/kotlin/ftl/ios/Parse.kt b/test_runner/src/main/kotlin/ftl/ios/Parse.kt index 31c5c96f23..3acad0d3fb 100644 --- a/test_runner/src/main/kotlin/ftl/ios/Parse.kt +++ b/test_runner/src/main/kotlin/ftl/ios/Parse.kt @@ -2,7 +2,7 @@ package ftl.ios import ftl.config.FtlConstants.isMacOS import ftl.util.Bash -import ftl.util.Utils.copyBinaryResource +import ftl.util.copyBinaryResource import java.io.File object Parse { diff --git a/test_runner/src/main/kotlin/ftl/reports/CostReport.kt b/test_runner/src/main/kotlin/ftl/reports/CostReport.kt index 66b35d6ad5..78aaf06fcb 100644 --- a/test_runner/src/main/kotlin/ftl/reports/CostReport.kt +++ b/test_runner/src/main/kotlin/ftl/reports/CostReport.kt @@ -6,8 +6,8 @@ import ftl.json.MatrixMap import ftl.reports.util.IReport import ftl.reports.xml.model.JUnitTestResult import ftl.util.Billing -import ftl.util.Utils.println -import ftl.util.Utils.write +import ftl.util.println +import ftl.util.write import java.io.StringWriter /** Calculates cost based on the matrix map. Always run. */ diff --git a/test_runner/src/main/kotlin/ftl/reports/HtmlErrorReport.kt b/test_runner/src/main/kotlin/ftl/reports/HtmlErrorReport.kt index a25a3823f0..4ad4f2b47a 100644 --- a/test_runner/src/main/kotlin/ftl/reports/HtmlErrorReport.kt +++ b/test_runner/src/main/kotlin/ftl/reports/HtmlErrorReport.kt @@ -6,7 +6,7 @@ import ftl.json.MatrixMap import ftl.reports.util.IReport import ftl.reports.xml.model.JUnitTestCase import ftl.reports.xml.model.JUnitTestResult -import ftl.util.Utils.readTextResource +import ftl.util.readTextResource import java.nio.file.Files import java.nio.file.Paths diff --git a/test_runner/src/main/kotlin/ftl/reports/JUnitReport.kt b/test_runner/src/main/kotlin/ftl/reports/JUnitReport.kt index 688373c57d..ae19f82a98 100644 --- a/test_runner/src/main/kotlin/ftl/reports/JUnitReport.kt +++ b/test_runner/src/main/kotlin/ftl/reports/JUnitReport.kt @@ -5,7 +5,7 @@ import ftl.json.MatrixMap import ftl.reports.util.IReport import ftl.reports.xml.model.JUnitTestResult import ftl.reports.xml.xmlToString -import ftl.util.Utils.write +import ftl.util.write /** Calculates cost based on the matrix map. Always run. */ object JUnitReport : IReport { diff --git a/test_runner/src/main/kotlin/ftl/reports/MatrixResultsReport.kt b/test_runner/src/main/kotlin/ftl/reports/MatrixResultsReport.kt index 3dc7a1f4a6..92f9ee5b66 100644 --- a/test_runner/src/main/kotlin/ftl/reports/MatrixResultsReport.kt +++ b/test_runner/src/main/kotlin/ftl/reports/MatrixResultsReport.kt @@ -5,8 +5,8 @@ import ftl.config.FtlConstants.indent import ftl.json.MatrixMap import ftl.reports.util.IReport import ftl.reports.xml.model.JUnitTestResult -import ftl.util.Utils.println -import ftl.util.Utils.write +import ftl.util.println +import ftl.util.write import java.io.StringWriter import java.text.DecimalFormat diff --git a/test_runner/src/main/kotlin/ftl/run/GenericTestRunner.kt b/test_runner/src/main/kotlin/ftl/run/GenericTestRunner.kt index 468f5bcb8a..84891dc00e 100644 --- a/test_runner/src/main/kotlin/ftl/run/GenericTestRunner.kt +++ b/test_runner/src/main/kotlin/ftl/run/GenericTestRunner.kt @@ -7,7 +7,7 @@ import ftl.config.FtlConstants.useMock import ftl.json.MatrixMap import ftl.json.SavedMatrix import ftl.util.StopWatch -import ftl.util.Utils +import ftl.util.uniqueObjectName import java.io.File object GenericTestRunner { @@ -16,7 +16,7 @@ object GenericTestRunner { val stopwatch = StopWatch().start() TestRunner.assertMockUrl() - val runGcsPath = args.resultsDir ?: Utils.uniqueObjectName() + val runGcsPath = args.resultsDir ?: uniqueObjectName() // Avoid spamming the results/ dir with temporary files from running the test suite. if (useMock) { diff --git a/test_runner/src/main/kotlin/ftl/run/TestRunner.kt b/test_runner/src/main/kotlin/ftl/run/TestRunner.kt index ff76ef8d15..1694a62ebf 100644 --- a/test_runner/src/main/kotlin/ftl/run/TestRunner.kt +++ b/test_runner/src/main/kotlin/ftl/run/TestRunner.kt @@ -25,9 +25,9 @@ import ftl.util.MatrixState import ftl.util.ObjPath import ftl.util.StopWatch import ftl.util.StopWatchMatrix -import ftl.util.Utils -import ftl.util.Utils.fatalError import ftl.util.completed +import ftl.util.fatalError +import ftl.util.sleep import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -310,7 +310,7 @@ object TestRunner { // GetTestMatrix is not designed to handle many requests per second. // Sleep to avoid overloading the system. - Utils.sleep(5) + sleep(5) refreshedMatrix = GcTestMatrix.refresh(matrixId, args) } diff --git a/test_runner/src/main/kotlin/ftl/shard/Shard.kt b/test_runner/src/main/kotlin/ftl/shard/Shard.kt index 8ccb89ca7c..2101fb3f41 100644 --- a/test_runner/src/main/kotlin/ftl/shard/Shard.kt +++ b/test_runner/src/main/kotlin/ftl/shard/Shard.kt @@ -7,7 +7,7 @@ import ftl.args.IosArgs import ftl.reports.xml.model.JUnitTestCase import ftl.reports.xml.model.JUnitTestResult import ftl.util.FlankTestMethod -import ftl.util.Utils.fatalError +import ftl.util.fatalError import kotlin.math.ceil import kotlin.math.min import kotlin.math.roundToInt diff --git a/test_runner/src/main/kotlin/ftl/util/MatrixUtil.kt b/test_runner/src/main/kotlin/ftl/util/MatrixUtil.kt index 35f1f10707..5f5869848c 100644 --- a/test_runner/src/main/kotlin/ftl/util/MatrixUtil.kt +++ b/test_runner/src/main/kotlin/ftl/util/MatrixUtil.kt @@ -2,7 +2,6 @@ package ftl.util import ftl.args.IArgs import ftl.json.MatrixMap -import ftl.util.Utils.join import java.io.File import java.util.concurrent.TimeUnit diff --git a/test_runner/src/main/kotlin/ftl/util/Utils.kt b/test_runner/src/main/kotlin/ftl/util/Utils.kt index bb765caa7e..354bb5ea27 100644 --- a/test_runner/src/main/kotlin/ftl/util/Utils.kt +++ b/test_runner/src/main/kotlin/ftl/util/Utils.kt @@ -1,3 +1,5 @@ +@file:JvmName("Utils") + package ftl.util import ftl.config.FtlConstants @@ -12,135 +14,132 @@ import java.time.format.DateTimeFormatter import java.util.Random import kotlin.system.exitProcess -object Utils { +fun String.trimStartLine(): String { + return this.split("\n").drop(1).joinToString("\n") +} - fun String.trimStartLine(): String { - return this.split("\n").drop(1).joinToString("\n") - } +fun StringWriter.println(msg: String = "") { + this.append(msg + "\n") +} - fun StringWriter.println(msg: String = "") { - this.append(msg + "\n") - } +fun String.write(data: String) { + Files.write(Paths.get(this), data.toByteArray()) +} - fun String.write(data: String) { - Files.write(Paths.get(this), data.toByteArray()) - } +fun join(first: String, vararg more: String): String { + // Note: Paths.get(...) does not work for joining because the path separator + // will be '\' on Windows which is invalid for a URI + return listOf(first, *more) + .joinToString("/") + .replace("\\", "/") + .replace(regex = Regex("/+"), replacement = "/") +} - fun join(first: String, vararg more: String): String { - // Note: Paths.get(...) does not work for joining because the path separator - // will be '\' on Windows which is invalid for a URI - return listOf(first, *more) - .joinToString("/") - .replace("\\", "/") - .replace(regex = Regex("/+"), replacement = "/") +fun sleep(seconds: Long) { + try { + Thread.sleep(ofSeconds(seconds).toMillis()) + } catch (e: Exception) { + System.err.println(e) } +} - fun sleep(seconds: Long) { - try { - Thread.sleep(ofSeconds(seconds).toMillis()) - } catch (e: Exception) { - System.err.println(e) - } - } +// marked as inline to make JaCoCo happy +@Suppress("NOTHING_TO_INLINE") +inline fun fatalError(e: Exception, message: String? = null) { + throw RuntimeException(message ?: e.toString(), e) +} - // marked as inline to make JaCoCo happy - @Suppress("NOTHING_TO_INLINE") - inline fun fatalError(e: Exception, message: String? = null) { - throw RuntimeException(message ?: e.toString(), e) +@Suppress("NOTHING_TO_INLINE") +inline fun fatalError(e: String): String { + if (FtlConstants.useMock) { + throw RuntimeException(e) } + System.err.println(e) + exitProcess(3) +} - @Suppress("NOTHING_TO_INLINE") - inline fun fatalError(e: String): String { - if (FtlConstants.useMock) { - throw RuntimeException(e) - } - System.err.println(e) - exitProcess(3) +fun assertNotEmpty(str: String, e: String) { + if (str.isEmpty()) { + fatalError(e) } +} - fun assertNotEmpty(str: String, e: String) { - if (str.isEmpty()) { - fatalError(e) - } +// Match _GenerateUniqueGcsObjectName from api_lib/firebase/test/arg_validate.py +// +// Example: 2017-05-31_17:19:36.431540_hRJD +// +// https://cloud.google.com/storage/docs/naming +fun uniqueObjectName(): String { + val bucketName = StringBuilder() + val instant = Instant.now() + + bucketName.append( + DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss.") + .withZone(ZoneOffset.UTC) + .format(instant) + ) + + val nanoseconds = instant.nano.toString() + + if (nanoseconds.length >= 6) { + bucketName.append(nanoseconds.substring(0, 6)) + } else { + bucketName.append(nanoseconds.substring(0, nanoseconds.length - 1)) } - // Match _GenerateUniqueGcsObjectName from api_lib/firebase/test/arg_validate.py - // - // Example: 2017-05-31_17:19:36.431540_hRJD - // - // https://cloud.google.com/storage/docs/naming - fun uniqueObjectName(): String { - val bucketName = StringBuilder() - val instant = Instant.now() - - bucketName.append( - DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss.") - .withZone(ZoneOffset.UTC) - .format(instant) - ) - - val nanoseconds = instant.nano.toString() - - if (nanoseconds.length >= 6) { - bucketName.append(nanoseconds.substring(0, 6)) - } else { - bucketName.append(nanoseconds.substring(0, nanoseconds.length - 1)) - } - - bucketName.append("_") + bucketName.append("_") - val random = Random() - // a-z: 97 - 122 - // A-Z: 65 - 90 - repeat(4) { - val ascii = random.nextInt(26) - var letter = (ascii + 'a'.toInt()).toChar() + val random = Random() + // a-z: 97 - 122 + // A-Z: 65 - 90 + repeat(4) { + val ascii = random.nextInt(26) + var letter = (ascii + 'a'.toInt()).toChar() - if (ascii % 2 == 0) { - letter -= 32 // upcase - } - - bucketName.append(letter) + if (ascii % 2 == 0) { + letter -= 32 // upcase } - bucketName.append("/") - - return bucketName.toString() + bucketName.append(letter) } - private val classLoader = Thread.currentThread().contextClassLoader + bucketName.append("/") - private fun getResource(name: String): InputStream { - return classLoader.getResourceAsStream(name) - ?: throw RuntimeException("Unable to find resource: $name") - } + return bucketName.toString() +} - // app version: flank_snapshot - fun readVersion(): String { - return readTextResource("version.txt").trim() - } +private val classLoader = Thread.currentThread().contextClassLoader - // git commit name: 5b0d23215e3bd90e5f9c1c57149320634aad8008 - fun readRevision(): String { - return readTextResource("revision.txt").trim() - } +private fun getResource(name: String): InputStream { + return classLoader.getResourceAsStream(name) + ?: throw RuntimeException("Unable to find resource: $name") +} - fun readTextResource(name: String): String { - return getResource(name).bufferedReader().use { it.readText() } - } +// app version: flank_snapshot +fun readVersion(): String { + return readTextResource("version.txt").trim() +} + +// git commit name: 5b0d23215e3bd90e5f9c1c57149320634aad8008 +fun readRevision(): String { + return readTextResource("revision.txt").trim() +} + +fun readTextResource(name: String): String { + return getResource(name).bufferedReader().use { it.readText() } +} - private val userHome = System.getProperty("user.home") +private val userHome = System.getProperty("user.home") - fun copyBinaryResource(name: String) { - val destinationPath = Paths.get(userHome, ".flank", name) - val destinationFile = destinationPath.toFile() +fun copyBinaryResource(name: String) { + val destinationPath = Paths.get(userHome, ".flank", name) + val destinationFile = destinationPath.toFile() - if (destinationFile.exists()) return - destinationPath.parent.toFile().mkdirs() + if (destinationFile.exists()) return + destinationPath.parent.toFile().mkdirs() - // "binaries/" folder prefix is required for Linux to find the resource. - val bytes = getResource("binaries/$name").use { it.readBytes() } - Files.write(destinationPath, bytes) - destinationFile.setExecutable(true) - } + // "binaries/" folder prefix is required for Linux to find the resource. + val bytes = getResource("binaries/$name").use { it.readBytes() } + Files.write(destinationPath, bytes) + destinationFile.setExecutable(true) } diff --git a/test_runner/src/test/kotlin/Debug.kt b/test_runner/src/test/kotlin/Debug.kt index a796254ee2..860142539b 100644 --- a/test_runner/src/test/kotlin/Debug.kt +++ b/test_runner/src/test/kotlin/Debug.kt @@ -16,7 +16,7 @@ fun main() { "--debug", "firebase", "test", "android", "run", -// "--dry", + "--dry", "-c=src/test/kotlin/ftl/fixtures/test_app_cases/flank-$quantity-$type.yml", "--project=$projectId" ) diff --git a/test_runner/src/test/kotlin/ftl/run/GenericTestRunnerTest.kt b/test_runner/src/test/kotlin/ftl/run/GenericTestRunnerTest.kt index 0cbd25b251..ba643ada4d 100644 --- a/test_runner/src/test/kotlin/ftl/run/GenericTestRunnerTest.kt +++ b/test_runner/src/test/kotlin/ftl/run/GenericTestRunnerTest.kt @@ -4,12 +4,12 @@ import ftl.args.IArgs import ftl.run.GenericTestRunner.beforeRunMessage import ftl.test.util.FlankTestRunner import ftl.test.util.TestHelper.assert -import ftl.util.Utils.trimStartLine import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.`when` import org.mockito.Mockito.mock import ftl.test.util.TestHelper.normalizeLineEnding +import ftl.util.trimStartLine @RunWith(FlankTestRunner::class) class GenericTestRunnerTest { diff --git a/test_runner/src/test/kotlin/ftl/util/UtilsTest.kt b/test_runner/src/test/kotlin/ftl/util/UtilsTest.kt index 632923738f..db0636ed35 100644 --- a/test_runner/src/test/kotlin/ftl/util/UtilsTest.kt +++ b/test_runner/src/test/kotlin/ftl/util/UtilsTest.kt @@ -15,17 +15,17 @@ class UtilsTest { @Test(expected = RuntimeException::class) fun `readTextResource errors`() { - Utils.readTextResource("does not exist") + readTextResource("does not exist") } @Test fun `readTextResource succeeds`() { - assertThat(Utils.readTextResource("version.txt")).isNotNull() + assertThat(readTextResource("version.txt")).isNotNull() } @Test fun `uniqueObjectName verifyPattern`() { - val randomName = Utils.uniqueObjectName() + val randomName = uniqueObjectName() assertThat(randomName.length).isEqualTo(32) assertThat(randomName[4]).isEqualTo('-') assertThat(randomName[7]).isEqualTo('-') From 67090f5dfe686895227e4f4eb99f282b5dc0aea1 Mon Sep 17 00:00:00 2001 From: Pawel Pasterz Date: Sun, 8 Mar 2020 22:34:09 +0100 Subject: [PATCH 2/7] Implement fix to shut down jvm when main thread is terminated and handle Bugsnag bug --- test_runner/src/main/kotlin/ftl/Main.kt | 3 ++- test_runner/src/test/kotlin/Debug.kt | 5 +++-- test_runner/src/test/kotlin/ftl/MainTest.kt | 21 ++++++++++++++++++- .../src/test/kotlin/ftl/fixtures/invalid.yml | 2 ++ 4 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 test_runner/src/test/kotlin/ftl/fixtures/invalid.yml diff --git a/test_runner/src/main/kotlin/ftl/Main.kt b/test_runner/src/main/kotlin/ftl/Main.kt index 6e7093dc9d..d424069829 100644 --- a/test_runner/src/main/kotlin/ftl/Main.kt +++ b/test_runner/src/main/kotlin/ftl/Main.kt @@ -10,6 +10,7 @@ import ftl.log.setDebugLogging import ftl.util.readRevision import ftl.util.readVersion import picocli.CommandLine +import kotlin.system.exitProcess @CommandLine.Command( name = "flank.jar\n", @@ -45,7 +46,7 @@ class Main : Runnable { companion object { @JvmStatic fun main(args: Array) { - CommandLine(Main()).execute(*args) + exitProcess(CommandLine(Main()).execute(*args)) } } } diff --git a/test_runner/src/test/kotlin/Debug.kt b/test_runner/src/test/kotlin/Debug.kt index 860142539b..c1aa540105 100644 --- a/test_runner/src/test/kotlin/Debug.kt +++ b/test_runner/src/test/kotlin/Debug.kt @@ -1,5 +1,6 @@ import ftl.Main import picocli.CommandLine +import kotlin.system.exitProcess fun main() { // GoogleApiLogger.logAllToStdout() @@ -12,12 +13,12 @@ fun main() { val quantity = "single" val type = "success" - CommandLine(Main()).execute( + exitProcess(CommandLine(Main()).execute( "--debug", "firebase", "test", "android", "run", "--dry", "-c=src/test/kotlin/ftl/fixtures/test_app_cases/flank-$quantity-$type.yml", "--project=$projectId" - ) + )) } diff --git a/test_runner/src/test/kotlin/ftl/MainTest.kt b/test_runner/src/test/kotlin/ftl/MainTest.kt index 56c4fca117..e0b960504b 100644 --- a/test_runner/src/test/kotlin/ftl/MainTest.kt +++ b/test_runner/src/test/kotlin/ftl/MainTest.kt @@ -9,6 +9,7 @@ import org.junit.contrib.java.lang.system.SystemOutRule import org.junit.runner.RunWith import picocli.CommandLine import ftl.test.util.TestHelper.normalizeLineEnding +import org.junit.contrib.java.lang.system.ExpectedSystemExit @RunWith(FlankTestRunner::class) class MainTest { @@ -21,6 +22,10 @@ class MainTest { @JvmField val systemErrRule: SystemErrRule = SystemErrRule().enableLog().muteForSuccessfulTests() + @Rule + @JvmField + val systemExit = ExpectedSystemExit.none()!! + private fun assertMainHelpStrings(output: String) { assertThat(output.normalizeLineEnding()).contains( "flank.jar\n" + @@ -77,8 +82,22 @@ class MainTest { } @Test - fun mainStaticEntrypoint() { + fun `should exit with status code 0 if no args provided`() { + systemExit.expectSystemExitWithStatus(0) Main.main(emptyArray()) assertMainHelpStrings(systemOutRule.log) } + + @Test + fun `should terminate jvm with exit status 1 if yml parsing error occurs`() { + systemExit.expectSystemExitWithStatus(1) + Main.main(arrayOf( + "firebase", + "test", + "android", + "run", + "--dry", + "-c=./src/test/kotlin/ftl/fixtures/invalid.yml" + )) + } } diff --git a/test_runner/src/test/kotlin/ftl/fixtures/invalid.yml b/test_runner/src/test/kotlin/ftl/fixtures/invalid.yml new file mode 100644 index 0000000000..bfd8e34a2c --- /dev/null +++ b/test_runner/src/test/kotlin/ftl/fixtures/invalid.yml @@ -0,0 +1,2 @@ +gcloud: + invalid config From 2a0f8f5986e4b56521766fdc0b2fdb029c833c78 Mon Sep 17 00:00:00 2001 From: Pawel Pasterz Date: Sun, 8 Mar 2020 23:18:38 +0100 Subject: [PATCH 3/7] Update release notes --- release_notes.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/release_notes.md b/release_notes.md index a2426efe31..22adc79473 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,7 +1,8 @@ ## next (unreleased) -- [#654](https://github.com/Flank/flank/pull/654) Fix test filters when using both notPackage and notClass. ([jan-gogo](https://github.com/jan-gogo)) -- [#648](https://github.com/Flank/flank/pull/648) Include @Ignore JUnit tests in JUnit XML. ([pawelpasterz](https://github.com/pawelpasterz)) +- [#657](https://github.com/Flank/flank/pull/657) Fix execution hangs. ([pawelpasterz](https://github.com/pawelpasterz)) +- [#654](https://github.com/Flank/flank/pull/654) Fix test filters when using both notPackage and notClass. ([jan-gogo](https://github.com/jan-gogo)) +- [#648](https://github.com/Flank/flank/pull/648) Include @Ignore JUnit tests in JUnit XML. ([pawelpasterz](https://github.com/pawelpasterz)) - [#646](https://github.com/Flank/flank/pull/646) Adopt kotlin-logging as a logging framework. ([jan-gogo](https://github.com/jan-gogo)) - [#644](https://github.com/Flank/flank/pull/644) Use high performance options by default. Video, login, and perf metrics are now disabled by default. ([pawelpasterz](https://github.com/pawelpasterz)) - [#643](https://github.com/Flank/flank/pull/643) Add --dry option to android run & ios run. ([jan-gogo](https://github.com/jan-gogo)) From 6c70271d071d7a424b44e86a8647b0ab28b3876b Mon Sep 17 00:00:00 2001 From: bootstraponline Date: Sun, 8 Mar 2020 16:04:02 -0700 Subject: [PATCH 4/7] Update Main.kt --- test_runner/src/main/kotlin/ftl/Main.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test_runner/src/main/kotlin/ftl/Main.kt b/test_runner/src/main/kotlin/ftl/Main.kt index d424069829..bb9b581855 100644 --- a/test_runner/src/main/kotlin/ftl/Main.kt +++ b/test_runner/src/main/kotlin/ftl/Main.kt @@ -46,6 +46,9 @@ class Main : Runnable { companion object { @JvmStatic fun main(args: Array) { + // BugSnag opens a non-daemon thread which will keep the JVM process alive. + // Flank must invoke exitProcess to exit cleanly. + // https://github.com/bugsnag/bugsnag-java/issues/151 exitProcess(CommandLine(Main()).execute(*args)) } } From cc3d31e89b8b90d8ca6af743c10818d57d9d2f2b Mon Sep 17 00:00:00 2001 From: bootstraponline Date: Sun, 8 Mar 2020 16:05:07 -0700 Subject: [PATCH 5/7] Update Debug.kt --- test_runner/src/test/kotlin/Debug.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test_runner/src/test/kotlin/Debug.kt b/test_runner/src/test/kotlin/Debug.kt index c1aa540105..d69a5794ea 100644 --- a/test_runner/src/test/kotlin/Debug.kt +++ b/test_runner/src/test/kotlin/Debug.kt @@ -13,6 +13,8 @@ fun main() { val quantity = "single" val type = "success" + // Bugsnag keeps the process alive so we must call exitProcess + // https://github.com/bugsnag/bugsnag-java/issues/151 exitProcess(CommandLine(Main()).execute( "--debug", "firebase", "test", From b9ce72749aea41d66409fd299c5646313e093bf5 Mon Sep 17 00:00:00 2001 From: Pawel Pasterz Date: Mon, 9 Mar 2020 01:17:27 +0100 Subject: [PATCH 6/7] After review changes --- test_runner/src/main/kotlin/ftl/Main.kt | 7 ++++++- test_runner/src/test/kotlin/Debug.kt | 23 +++++++++++++++-------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/test_runner/src/main/kotlin/ftl/Main.kt b/test_runner/src/main/kotlin/ftl/Main.kt index bb9b581855..00a3855da7 100644 --- a/test_runner/src/main/kotlin/ftl/Main.kt +++ b/test_runner/src/main/kotlin/ftl/Main.kt @@ -49,7 +49,12 @@ class Main : Runnable { // BugSnag opens a non-daemon thread which will keep the JVM process alive. // Flank must invoke exitProcess to exit cleanly. // https://github.com/bugsnag/bugsnag-java/issues/151 - exitProcess(CommandLine(Main()).execute(*args)) + try { + exitProcess(CommandLine(Main()).execute(*args)) + } catch (t: Throwable) { + t.printStackTrace() + exitProcess(CommandLine.ExitCode.SOFTWARE) + } } } } diff --git a/test_runner/src/test/kotlin/Debug.kt b/test_runner/src/test/kotlin/Debug.kt index d69a5794ea..f7dce3a83b 100644 --- a/test_runner/src/test/kotlin/Debug.kt +++ b/test_runner/src/test/kotlin/Debug.kt @@ -15,12 +15,19 @@ fun main() { // Bugsnag keeps the process alive so we must call exitProcess // https://github.com/bugsnag/bugsnag-java/issues/151 - exitProcess(CommandLine(Main()).execute( - "--debug", - "firebase", "test", - "android", "run", - "--dry", - "-c=src/test/kotlin/ftl/fixtures/test_app_cases/flank-$quantity-$type.yml", - "--project=$projectId" - )) + try { + exitProcess( + CommandLine(Main()).execute( + "--debug", + "firebase", "test", + "android", "run", + "--dry", + "-c=src/test/kotlin/ftl/fixtures/test_app_cases/flank-$quantity-$type.yml", + "--project=$projectId" + ) + ) + } catch (t: Throwable) { + t.printStackTrace() + exitProcess(CommandLine.ExitCode.SOFTWARE) + } } From adb4e71eade4d48343740969ce98e851e49b8993 Mon Sep 17 00:00:00 2001 From: Pawel Pasterz Date: Mon, 9 Mar 2020 14:26:12 +0100 Subject: [PATCH 7/7] Add test to verify if flank will terminate if non-daemon thread throws an error --- test_runner/src/main/kotlin/ftl/Main.kt | 9 +-- test_runner/src/main/kotlin/ftl/util/Utils.kt | 12 ++++ test_runner/src/test/kotlin/Debug.kt | 25 +++----- test_runner/src/test/kotlin/ftl/MainTest.kt | 4 +- .../src/test/kotlin/ftl/util/UtilsTest.kt | 63 +++++++++++++++++++ 5 files changed, 88 insertions(+), 25 deletions(-) diff --git a/test_runner/src/main/kotlin/ftl/Main.kt b/test_runner/src/main/kotlin/ftl/Main.kt index 00a3855da7..1e49d5197d 100644 --- a/test_runner/src/main/kotlin/ftl/Main.kt +++ b/test_runner/src/main/kotlin/ftl/Main.kt @@ -9,8 +9,8 @@ import ftl.cli.firebase.test.IosCommand import ftl.log.setDebugLogging import ftl.util.readRevision import ftl.util.readVersion +import ftl.util.jvmHangingSafe import picocli.CommandLine -import kotlin.system.exitProcess @CommandLine.Command( name = "flank.jar\n", @@ -49,12 +49,7 @@ class Main : Runnable { // BugSnag opens a non-daemon thread which will keep the JVM process alive. // Flank must invoke exitProcess to exit cleanly. // https://github.com/bugsnag/bugsnag-java/issues/151 - try { - exitProcess(CommandLine(Main()).execute(*args)) - } catch (t: Throwable) { - t.printStackTrace() - exitProcess(CommandLine.ExitCode.SOFTWARE) - } + jvmHangingSafe { CommandLine(Main()).execute(*args) } } } } diff --git a/test_runner/src/main/kotlin/ftl/util/Utils.kt b/test_runner/src/main/kotlin/ftl/util/Utils.kt index 354bb5ea27..e52f5fdce8 100644 --- a/test_runner/src/main/kotlin/ftl/util/Utils.kt +++ b/test_runner/src/main/kotlin/ftl/util/Utils.kt @@ -3,6 +3,7 @@ package ftl.util import ftl.config.FtlConstants +import picocli.CommandLine import java.io.InputStream import java.io.StringWriter import java.nio.file.Files @@ -143,3 +144,14 @@ fun copyBinaryResource(name: String) { Files.write(destinationPath, bytes) destinationFile.setExecutable(true) } + +// We need to cover the case where some component in the call stack starts a non-daemon +// thread, and then throws an Error that kills the main thread. This is extra safe implementation +fun jvmHangingSafe(block: () -> Int) { + try { + exitProcess(block()) + } catch (t: Throwable) { + t.printStackTrace() + exitProcess(CommandLine.ExitCode.SOFTWARE) + } +} diff --git a/test_runner/src/test/kotlin/Debug.kt b/test_runner/src/test/kotlin/Debug.kt index f7dce3a83b..470b74439d 100644 --- a/test_runner/src/test/kotlin/Debug.kt +++ b/test_runner/src/test/kotlin/Debug.kt @@ -1,6 +1,6 @@ import ftl.Main +import ftl.util.jvmHangingSafe import picocli.CommandLine -import kotlin.system.exitProcess fun main() { // GoogleApiLogger.logAllToStdout() @@ -15,19 +15,12 @@ fun main() { // Bugsnag keeps the process alive so we must call exitProcess // https://github.com/bugsnag/bugsnag-java/issues/151 - try { - exitProcess( - CommandLine(Main()).execute( - "--debug", - "firebase", "test", - "android", "run", - "--dry", - "-c=src/test/kotlin/ftl/fixtures/test_app_cases/flank-$quantity-$type.yml", - "--project=$projectId" - ) - ) - } catch (t: Throwable) { - t.printStackTrace() - exitProcess(CommandLine.ExitCode.SOFTWARE) - } + jvmHangingSafe { CommandLine(Main()).execute( + "--debug", + "firebase", "test", + "android", "run", + "--dry", + "-c=src/test/kotlin/ftl/fixtures/test_app_cases/flank-$quantity-$type.yml", + "--project=$projectId" + ) } } diff --git a/test_runner/src/test/kotlin/ftl/MainTest.kt b/test_runner/src/test/kotlin/ftl/MainTest.kt index e0b960504b..9f6aaa6b1a 100644 --- a/test_runner/src/test/kotlin/ftl/MainTest.kt +++ b/test_runner/src/test/kotlin/ftl/MainTest.kt @@ -2,14 +2,14 @@ package ftl import com.google.common.truth.Truth.assertThat import ftl.test.util.FlankTestRunner +import ftl.test.util.TestHelper.normalizeLineEnding import org.junit.Rule import org.junit.Test +import org.junit.contrib.java.lang.system.ExpectedSystemExit import org.junit.contrib.java.lang.system.SystemErrRule import org.junit.contrib.java.lang.system.SystemOutRule import org.junit.runner.RunWith import picocli.CommandLine -import ftl.test.util.TestHelper.normalizeLineEnding -import org.junit.contrib.java.lang.system.ExpectedSystemExit @RunWith(FlankTestRunner::class) class MainTest { diff --git a/test_runner/src/test/kotlin/ftl/util/UtilsTest.kt b/test_runner/src/test/kotlin/ftl/util/UtilsTest.kt index db0636ed35..576ac9e34f 100644 --- a/test_runner/src/test/kotlin/ftl/util/UtilsTest.kt +++ b/test_runner/src/test/kotlin/ftl/util/UtilsTest.kt @@ -7,8 +7,13 @@ import ftl.json.SavedMatrix import ftl.json.SavedMatrixTest.Companion.createResultsStorage import ftl.json.SavedMatrixTest.Companion.createStepExecution import ftl.test.util.FlankTestRunner +import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith +import picocli.CommandLine +import java.util.concurrent.CountDownLatch +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger @RunWith(FlankTestRunner::class) class UtilsTest { @@ -97,4 +102,62 @@ class UtilsTest { val matrixMap = MatrixMap(mutableMapOf("errorMatrix" to errorMatrix), "MockPath") assertThat(matrixMap.exitCode()).isEqualTo(2) } + + @CommandLine.Command(name = "whosbad") + private class Malicious : Runnable { + override fun run() { + val isThreadRunning = CountDownLatch(1) + val forever: Thread = object : Thread("forever") { + override fun run() { + try { + isThreadRunning.countDown() + sleep(Long.MAX_VALUE) + } catch (ignored: InterruptedException) { + } + } + } + forever.isDaemon = false + forever.start() + try { + isThreadRunning.await() + } catch (ignored: InterruptedException) { + } + throw Error("Killing the calling thread...") + } + } + + internal object HangingApp { + @JvmStatic + fun main(args: Array) { + jvmHangingSafe { CommandLine(Malicious()).execute(*args) } + } + } + + @Test + @Throws(Exception::class) + fun `should terminate if non-daemon thread launched from main thread throws an error`() { + val processStarted = CountDownLatch(1) + val completed = AtomicBoolean(false) + val exitCode = AtomicInteger(Int.MIN_VALUE) + val simulatedMain: Thread = object : Thread("simulated-main") { + override fun run() { + val pb = ProcessBuilder( + "java", "-cp", "classpath...picocli-4.2.0.jar", "ftl.util.UtilsTest\$HangingApp" + ) + try { + val process = pb.start() + processStarted.countDown() + exitCode.set(process.waitFor()) + completed.set(true) + } catch (ex: Exception) { + ex.printStackTrace() + } + } + } + simulatedMain.start() + processStarted.await() + simulatedMain.join(3 * 1000L) + Assert.assertTrue("Our simulated main thread should have completed but instead it hung...", completed.get()) + Assert.assertEquals(CommandLine.ExitCode.SOFTWARE, exitCode.get()) + } }