diff --git a/test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt b/test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt index ba267f7228..504b5b0eb2 100644 --- a/test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt +++ b/test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt @@ -10,6 +10,7 @@ import ftl.android.UnsupportedVersionId import ftl.args.ArgsHelper.assertFileExists import ftl.args.ArgsHelper.assertGcsFileExists import ftl.args.ArgsHelper.calculateShards +import ftl.args.ArgsHelper.evaluateFilePath import ftl.args.ArgsHelper.getGcsBucket import ftl.args.ArgsHelper.mergeYmlMaps import ftl.args.ArgsHelper.yamlMapper @@ -45,8 +46,8 @@ class AndroidArgs( override val resultsHistoryName = gcloud.resultsHistoryName private val androidGcloud = androidGcloudYml.gcloud - val appApk = androidGcloud.app - val testApk = androidGcloud.test + var appApk = androidGcloud.app + var testApk = androidGcloud.test val autoGoogleLogin = androidGcloud.autoGoogleLogin val useOrchestrator = androidGcloud.useOrchestrator val environmentVariables = androidGcloud.environmentVariables @@ -85,12 +86,14 @@ class AndroidArgs( if (appApk.startsWith(FtlConstants.GCS_PREFIX)) { assertGcsFileExists(appApk) } else { + appApk = evaluateFilePath(appApk, "appApk") assertFileExists(appApk, "appApk") } if (testApk.startsWith(FtlConstants.GCS_PREFIX)) { assertGcsFileExists(testApk) } else { + testApk = evaluateFilePath(testApk, "testApk") assertFileExists(testApk, "testApk") } diff --git a/test_runner/src/main/kotlin/ftl/args/ArgsHelper.kt b/test_runner/src/main/kotlin/ftl/args/ArgsHelper.kt index 66523352ed..612067ba23 100644 --- a/test_runner/src/main/kotlin/ftl/args/ArgsHelper.kt +++ b/test_runner/src/main/kotlin/ftl/args/ArgsHelper.kt @@ -20,9 +20,18 @@ import ftl.config.FtlConstants.defaultCredentialPath import ftl.gc.GcStorage import ftl.util.Utils import java.io.File +import java.io.IOException import java.math.RoundingMode import java.net.URI +import java.nio.file.FileVisitResult import java.nio.file.Files +import java.nio.file.FileSystems +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file.Path +import java.nio.file.Paths +import java.util.HashSet +import java.util.regex.Pattern object ArgsHelper { @@ -45,6 +54,19 @@ object ArgsHelper { } } + fun evaluateFilePath(fileRegEx: String, name: String): String { + var file = fileRegEx.trim().replaceFirst("~", System.getProperty("user.home")) + file = substituteEnvVars(file) + val searchDirectoryPath = getSearchDirectoryPath(file) + val filePaths = getAbsoluteFilePaths(searchDirectoryPath, file) + if (filePaths.size > 1) { + Utils.fatalError("$name multiple files found with expression: `$fileRegEx`: $filePaths") + } else if (filePaths.isEmpty()) { + Utils.fatalError("'$fileRegEx' $name doesn't exist") + } + return filePaths[0].toAbsolutePath().toString() + } + fun assertGcsFileExists(uri: String) { if (!uri.startsWith(GCS_PREFIX)) { throw IllegalArgumentException("must start with $GCS_PREFIX uri: $uri") @@ -150,4 +172,53 @@ object ArgsHelper { // Allow users control over projectId by checking using Google's logic first before falling back to JSON. return ServiceOptions.getDefaultProjectId() ?: serviceAccountProjectId() } + + private fun getSearchDirectoryPath(path: String): String { + var searchDirectoryPath = String() + val pattern = "([^*]*/)" + val matcher = Pattern.compile(pattern).matcher(path) + if (matcher.find()) { + searchDirectoryPath = matcher.group(1) + } + return searchDirectoryPath + } + + private fun substituteEnvVars(text: String): String { + val sb = StringBuffer() + // https://stackoverflow.com/a/2821201/2450315 + val pattern = "\\$([a-zA-Z_]{1,}[a-zA-Z0-9_]{0,})" + val matcher = Pattern.compile(pattern).matcher(text) + while (matcher.find()) { + val varname = matcher.group(1) + val envValue: String = System.getenv(varname) ?: "" + matcher.appendReplacement(sb, envValue) + } + matcher.appendTail(sb) + return sb.toString() + } + + private fun getAbsoluteFilePaths(searchDir: String, globPath: String): List { + val maxDepth = if (globPath.contains("/*")) Integer.MAX_VALUE else 1 + val glob = "glob:$globPath" + val paths = ArrayList() + val pathMatcher = FileSystems.getDefault().getPathMatcher(glob) + + Files.walkFileTree(Paths.get(searchDir), HashSet(), maxDepth, object : SimpleFileVisitor() { + + @Throws(IOException::class) + override fun visitFile(path: Path, attrs: BasicFileAttributes): FileVisitResult { + if (pathMatcher.matches(path)) { + paths.add(path) + } + return FileVisitResult.CONTINUE + } + + @Throws(IOException::class) + override fun visitFileFailed(file: Path, exc: IOException?): FileVisitResult { + Utils.fatalError("'$file' doesn't exist") + return FileVisitResult.CONTINUE + } + }) + return paths + } } diff --git a/test_runner/src/main/kotlin/ftl/args/IosArgs.kt b/test_runner/src/main/kotlin/ftl/args/IosArgs.kt index cb380c76b7..b8458b08e5 100644 --- a/test_runner/src/main/kotlin/ftl/args/IosArgs.kt +++ b/test_runner/src/main/kotlin/ftl/args/IosArgs.kt @@ -2,6 +2,7 @@ package ftl.args import ftl.args.ArgsHelper.assertFileExists import ftl.args.ArgsHelper.assertGcsFileExists +import ftl.args.ArgsHelper.evaluateFilePath import ftl.args.ArgsHelper.mergeYmlMaps import ftl.args.ArgsHelper.validateTestMethods import ftl.args.ArgsHelper.yamlMapper @@ -36,8 +37,8 @@ class IosArgs( override val resultsHistoryName = gcloud.resultsHistoryName private val iosGcloud = iosGcloudYml.gcloud - val xctestrunZip = iosGcloud.test - val xctestrunFile = iosGcloud.xctestrunFile + var xctestrunZip = iosGcloud.test + var xctestrunFile = iosGcloud.xctestrunFile val xcodeVersion = iosGcloud.xcodeVersion val devices = iosGcloud.device @@ -70,8 +71,10 @@ class IosArgs( if (xctestrunZip.startsWith(FtlConstants.GCS_PREFIX)) { assertGcsFileExists(xctestrunZip) } else { + xctestrunZip = evaluateFilePath(xctestrunZip, "xctestrunZip") assertFileExists(xctestrunZip, "xctestrunZip") } + xctestrunFile = evaluateFilePath(xctestrunFile, "xctestrunFile") assertFileExists(xctestrunFile, "xctestrunFile") devices.forEach { device -> assertDeviceSupported(device) } diff --git a/test_runner/src/test/kotlin/ftl/args/AndroidArgsFileTest.kt b/test_runner/src/test/kotlin/ftl/args/AndroidArgsFileTest.kt index 065e5cec92..e56158b6e4 100644 --- a/test_runner/src/test/kotlin/ftl/args/AndroidArgsFileTest.kt +++ b/test_runner/src/test/kotlin/ftl/args/AndroidArgsFileTest.kt @@ -17,6 +17,7 @@ import org.junit.contrib.java.lang.system.SystemErrRule import org.junit.contrib.java.lang.system.SystemOutRule import org.junit.rules.ExpectedException import org.junit.runner.RunWith +import java.io.File @RunWith(FlankTestRunner::class) class AndroidArgsFileTest { @@ -42,6 +43,8 @@ class AndroidArgsFileTest { private val testName = "class com.example.app.ExampleUiTest#testPasses" private val directoryToPull = "/sdcard/screenshots" + private val appApkAbsolutePath = File(appApkLocal).absolutePath + private val testApkAbsolutePath = File(testApkLocal).absolutePath // NOTE: Change working dir to '%MODULE_WORKING_DIR%' in IntelliJ to match gradle for this test to pass. @Test fun localConfigLoadsSuccessfully() { @@ -58,10 +61,10 @@ class AndroidArgsFileTest { private fun checkConfig(args: AndroidArgs, local: Boolean) { with(args) { - if (local) assert(getString(testApk), testApkLocal) + if (local) assert(getString(testApk), getString(testApkAbsolutePath)) else assert(testApk, testApkGcs) - if (local) assert(getString(appApk), appApkLocal) + if (local) assert(getString(appApk), getString(appApkAbsolutePath)) else assert(appApk, appApkGcs) assert(autoGoogleLogin, true) diff --git a/test_runner/src/test/kotlin/ftl/args/AndroidArgsTest.kt b/test_runner/src/test/kotlin/ftl/args/AndroidArgsTest.kt index 9f7b68b645..6142513437 100644 --- a/test_runner/src/test/kotlin/ftl/args/AndroidArgsTest.kt +++ b/test_runner/src/test/kotlin/ftl/args/AndroidArgsTest.kt @@ -8,6 +8,7 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.ExpectedException import org.junit.runner.RunWith +import java.io.File @RunWith(FlankTestRunner::class) class AndroidArgsTest { @@ -15,6 +16,8 @@ class AndroidArgsTest { private val appApk = "../test_app/apks/app-debug.apk" private val testApk = "../test_app/apks/app-debug-androidTest.apk" private val testErrorApk = "../test_app/apks/error-androidTest.apk" + private val appApkAbsolutePath = File(appApk).absolutePath + private val testApkAbsolutePath = File(testApk).absolutePath private val androidNonDefault = """ gcloud: @@ -123,8 +126,8 @@ class AndroidArgsTest { assert(resultsHistoryName ?: "", "android-history") // AndroidGcloudYml - assert(appApk, appApk) - assert(testApk, testApk) + assert(appApk, appApkAbsolutePath) + assert(testApk, testApkAbsolutePath) assert(autoGoogleLogin, false) assert(useOrchestrator, false) assert(environmentVariables, linkedMapOf("clearPackageData" to "true", "randomEnvVar" to "false")) @@ -170,8 +173,8 @@ AndroidArgs project: projectFoo results-history-name: android-history # Android gcloud - app: ../test_app/apks/app-debug.apk - test: ../test_app/apks/app-debug-androidTest.apk + app: $appApkAbsolutePath + test: $testApkAbsolutePath auto-google-login: false use-orchestrator: false environment-variables: @@ -223,8 +226,8 @@ AndroidArgs assert(projectId, "mockProjectId") // AndroidGcloudYml - assert(appApk, appApk) - assert(testApk, testApk) + assert(appApk, appApkAbsolutePath) + assert(testApk, testApkAbsolutePath) assert(autoGoogleLogin, true) assert(useOrchestrator, true) assert(environmentVariables, emptyMap()) @@ -272,7 +275,7 @@ AndroidArgs """ ) - assertThat(androidArgs.appApk).isEqualTo(appApk) - assertThat(androidArgs.testApk).isEqualTo(testApk) + assertThat(androidArgs.appApk).isEqualTo(appApkAbsolutePath) + assertThat(androidArgs.testApk).isEqualTo(testApkAbsolutePath) } } diff --git a/test_runner/src/test/kotlin/ftl/args/ArgsHelperTest.kt b/test_runner/src/test/kotlin/ftl/args/ArgsHelperTest.kt index 7e5a5d8361..3e98363f4d 100644 --- a/test_runner/src/test/kotlin/ftl/args/ArgsHelperTest.kt +++ b/test_runner/src/test/kotlin/ftl/args/ArgsHelperTest.kt @@ -15,6 +15,9 @@ import org.junit.Test import org.junit.contrib.java.lang.system.SystemErrRule import org.junit.rules.ExpectedException import org.junit.runner.RunWith +import java.io.File +import org.junit.contrib.java.lang.system.EnvironmentVariables +import java.lang.RuntimeException @RunWith(FlankTestRunner::class) class ArgsHelperTest { @@ -27,6 +30,9 @@ class ArgsHelperTest { @JvmField val systemErrRule = SystemErrRule().muteForSuccessfulTests()!! + @get:Rule + val environmentVariables = EnvironmentVariables() + @Test fun mergeYmlMaps_succeeds() { val merged = mergeYmlMaps(GcloudYml, IosGcloudYml) @@ -161,4 +167,43 @@ class ArgsHelperTest { assertThat(ArgsHelper.getDefaultProjectId()) .isEqualTo("mockProjectId") } + + @Test + fun evaluateBlobInFilePath() { + val testApkRelativePath = "../test_app/apks/app-debug-androidTest.apk" + val testApkBlobPath = "../test_app/**/app-debug-*.apk" + + assertThat(File(testApkRelativePath).absolutePath) + .isEqualTo(ArgsHelper.evaluateFilePath(testApkBlobPath, "test")) + } + + @Test + fun evaluateTildeInFilePath() { + val testApkPath = "~/flank_test_app/random.xctestrun" + val file = File(testApkPath.replaceFirst("~", System.getProperty("user.home"))) + file.getParentFile().mkdirs() + file.createNewFile() + + assertThat(file.absolutePath) + .isEqualTo(ArgsHelper.evaluateFilePath(testApkPath, "xctestrun-file")) + } + + @Test + fun evaluateEnvVarInFilePath() { + environmentVariables.set("TEST_APK_DIR", "test_app/apks") + val testApkPath = "../\$TEST_APK_DIR/app-debug-androidTest.apk" + val expectedPath = File("../test_app/apks/app-debug-androidTest.apk").absolutePath + val file = File(testApkPath.replaceFirst("~", System.getProperty("user.home"))) + file.getParentFile().mkdirs() + file.createNewFile() + + assertThat(expectedPath) + .isEqualTo(ArgsHelper.evaluateFilePath(testApkPath, "test")) + } + + @Test(expected = RuntimeException::class) + fun evaluateInvalidFilePath() { + val testApkPath = "~/flank_test_app/invalid_path/app-debug-*.xctestrun" + ArgsHelper.evaluateFilePath(testApkPath, "test") + } } diff --git a/test_runner/src/test/kotlin/ftl/args/IosArgsTest.kt b/test_runner/src/test/kotlin/ftl/args/IosArgsTest.kt index 26a7f6e52f..0e1c716640 100644 --- a/test_runner/src/test/kotlin/ftl/args/IosArgsTest.kt +++ b/test_runner/src/test/kotlin/ftl/args/IosArgsTest.kt @@ -14,6 +14,7 @@ import org.junit.Test import org.junit.contrib.java.lang.system.SystemErrRule import org.junit.rules.ExpectedException import org.junit.runner.RunWith +import java.io.File @RunWith(FlankTestRunner::class) class IosArgsTest { @@ -21,6 +22,8 @@ class IosArgsTest { private val testPath = "./src/test/kotlin/ftl/fixtures/tmp/EarlGreyExample.zip" private val xctestrunFile = "./src/test/kotlin/ftl/fixtures/tmp/EarlGreyExampleSwiftTests_iphoneos12.1-arm64e.xctestrun" + val xctestrunFileAbsolutePath = File(xctestrunFile).absolutePath + val testAbsolutePath = File(testPath).absolutePath private val iosNonDefault = """ gcloud: results-bucket: mockBucket @@ -101,8 +104,8 @@ class IosArgsTest { assert(resultsHistoryName ?: "", "ios-history") // IosGcloudYml - assert(xctestrunZip, testPath) - assert(xctestrunFile, xctestrunFile) + assert(xctestrunZip, testAbsolutePath) + assert(xctestrunFile, xctestrunFileAbsolutePath) val device = Device("iphone8", "11.2", "c", "d") assert(xcodeVersion ?: "", "9.2") assert(devices, listOf(device, device)) @@ -131,8 +134,8 @@ IosArgs project: projectFoo results-history-name: ios-history # iOS gcloud - test: $testPath - xctestrun-file: $xctestrunFile + test: $testAbsolutePath + xctestrun-file: $xctestrunFileAbsolutePath xcode-version: 9.2 device: - model: iphone8 @@ -177,8 +180,8 @@ IosArgs assert(projectId, "mockProjectId") // IosGcloudYml - assert(xctestrunZip, testPath) - assert(xctestrunFile, xctestrunFile) + assert(xctestrunZip, testAbsolutePath) + assert(xctestrunFile, xctestrunFileAbsolutePath) assert(devices, listOf(Device("iphone8", "11.2"))) // FlankYml @@ -224,8 +227,8 @@ IosArgs ) with(iosArgs) { - assertThat(xctestrunZip).isEqualTo(testPath) - assertThat(xctestrunFile).isEqualTo(xctestrunFile) + assertThat(xctestrunZip).isEqualTo(testAbsolutePath) + assertThat(xctestrunFile).isEqualTo(xctestrunFileAbsolutePath) } } }