Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow blob, tilde and env vars in app paths #386

Merged
merged 15 commits into from
Nov 16, 2018
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -85,12 +86,14 @@ class AndroidArgs(
if (appApk.startsWith(FtlConstants.GCS_PREFIX)) {
assertGcsFileExists(appApk)
} else {
appApk = evaluateFilePath(appApk)
assertFileExists(appApk, "appApk")
}

if (testApk.startsWith(FtlConstants.GCS_PREFIX)) {
assertGcsFileExists(testApk)
} else {
testApk = evaluateFilePath(testApk)
assertFileExists(testApk, "testApk")
}

Expand Down
54 changes: 54 additions & 0 deletions test_runner/src/main/kotlin/ftl/args/ArgsFileVisitor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package ftl.args

import ftl.util.Utils.fatalError
import java.io.IOException
import java.nio.file.FileSystems
import java.nio.file.FileVisitOption
import java.nio.file.FileVisitResult
import java.nio.file.Files
import java.nio.file.LinkOption
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.SimpleFileVisitor
import java.nio.file.attribute.BasicFileAttributes
import java.util.EnumSet

class ArgsFileVisitor(glob: String) : SimpleFileVisitor<Path>() {
private val pathMatcher = FileSystems.getDefault().getPathMatcher(glob)
private val result: MutableList<Path> = mutableListOf()

@Throws(IOException::class)
override fun visitFile(path: Path, attrs: BasicFileAttributes): FileVisitResult {
if (pathMatcher.matches(path)) {
result.add(path)
}
return FileVisitResult.CONTINUE
}

override fun visitFileFailed(file: Path?, exc: IOException?): FileVisitResult {
fatalError("Failed to visit $file $exc")
return FileVisitResult.CONTINUE
}

companion object {
private const val RECURSE = "/**"
}

@Throws(java.nio.file.NoSuchFileException::class)
fun walk(searchPath: Path): List<Path> {
val searchString = searchPath.toString()
// /Users/tmp/code/flank/test_app/** => /Users/tmp/code/flank/test_app/
val beforeGlob = Paths.get(searchString.substringBefore(RECURSE))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably need more testing around paths with /**/ and /*/

i'm not sure splitting by /**/ works for all cases

// must not follow links when resolving paths or /tmp turns into /private/tmp
val realPath = beforeGlob.toRealPath(LinkOption.NOFOLLOW_LINKS)

val searchDepth = if (searchString.contains(RECURSE)) {
Integer.MAX_VALUE
} else {
1
}

Files.walkFileTree(realPath, EnumSet.of(FileVisitOption.FOLLOW_LINKS), searchDepth, this)
return this.result
}
}
44 changes: 43 additions & 1 deletion test_runner/src/main/kotlin/ftl/args/ArgsHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import java.io.File
import java.math.RoundingMode
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.regex.Pattern

object ArgsHelper {

Expand All @@ -45,6 +48,24 @@ object ArgsHelper {
}
}

fun evaluateFilePath(filePath: String): String {
var file = filePath.trim().replaceFirst("~", System.getProperty("user.home"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

~ should be replaced only if it's the first character in the string
https://stackoverflow.com/a/7163455

the regex should be ^~, we should add a JUnit test for this (for both the happy path and failure case).

~/tmp should be replaced
/tmp/~ should not be replaced

file = substituteEnvVars(file)
// avoid File(..).canonicalPath since that will resolve symlinks
file = Paths.get(file).toAbsolutePath().normalize().toString()

// Avoid walking the folder's parent dir if we know it exists already.
if (File(file).exists()) return file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer if we also printed this value to console. Something like - Resolved app file path - "$file".
Under a conditional. if (filePath != file).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be printed already when the args are printed to stdout at the start of a test run.


val filePaths = walkFileTree(file)
if (filePaths.size > 1) {
Utils.fatalError("'$file' ($filePath) matches multiple files: $filePaths")
} else if (filePaths.isEmpty()) {
Utils.fatalError("'$file' not found ($filePath)")
}
return filePaths[0].toAbsolutePath().toString()
}

fun assertGcsFileExists(uri: String) {
if (!uri.startsWith(GCS_PREFIX)) {
throw IllegalArgumentException("must start with $GCS_PREFIX uri: $uri")
Expand Down Expand Up @@ -135,7 +156,8 @@ object ArgsHelper {
return JsonObjectParser(JSON_FACTORY).parseAndClose(
Files.newInputStream(defaultCredentialPath),
Charsets.UTF_8,
GenericJson::class.java)["project_id"] as String
GenericJson::class.java
)["project_id"] as String
} catch (e: Exception) {
println("Parsing $defaultCredentialPath failed:")
println(e.printStackTrace())
Expand All @@ -150,4 +172,24 @@ object ArgsHelper {
// Allow users control over projectId by checking using Google's logic first before falling back to JSON.
return ServiceOptions.getDefaultProjectId() ?: serviceAccountProjectId()
}

// https://stackoverflow.com/a/2821201/2450315
private val envRegex = Pattern.compile("\\$([a-zA-Z_]+[a-zA-Z0-9_]*)")
private fun substituteEnvVars(text: String): String {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should check this works for duplicate environment variables via more JUnit tests. There are probably other edge cases as well. For example the ${HOME} syntax which we should probably document as unsupported.

$HOME/$HOME/tmp

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably WARN but not fail for duplicate env variables. The warning is to notify the user that he's duplicated an env variable. Not failing, in case its intentional.

Regardless, I don't think we should handle custom env vars. That would likely be a pain to maintain.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bash doesn't warn on duplicates. I'm using bash as the model for the parsing logic.

val buffer = StringBuffer()
val matcher = envRegex.matcher(text)
while (matcher.find()) {
val envName = matcher.group(1)
val envValue = System.getenv(envName) ?: ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we raise an error/warning here? Silently ignoring un-found env variables seems like a bug.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is standard behavior in Bash, that's how environment variables work sadly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discussed on slack, printing a warning makes sense.

matcher.appendReplacement(buffer, envValue)
}
matcher.appendTail(buffer)
return buffer.toString()
}

private fun walkFileTree(filePath: String): List<Path> {
val searchDir = Paths.get(filePath).parent

return ArgsFileVisitor("glob:$filePath").walk(searchDir)
}
}
7 changes: 5 additions & 2 deletions test_runner/src/main/kotlin/ftl/args/IosArgs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -70,8 +71,10 @@ class IosArgs(
if (xctestrunZip.startsWith(FtlConstants.GCS_PREFIX)) {
assertGcsFileExists(xctestrunZip)
} else {
xctestrunZip = evaluateFilePath(xctestrunZip)
assertFileExists(xctestrunZip, "xctestrunZip")
}
xctestrunFile = evaluateFilePath(xctestrunFile)
assertFileExists(xctestrunFile, "xctestrunFile")

devices.forEach { device -> assertDeviceSupported(device) }
Expand Down
7 changes: 5 additions & 2 deletions test_runner/src/test/kotlin/ftl/args/AndroidArgsFileTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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() {
Expand All @@ -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)
Expand Down
19 changes: 11 additions & 8 deletions test_runner/src/test/kotlin/ftl/args/AndroidArgsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ftl.args
import com.google.common.truth.Truth.assertThat
import ftl.config.Device
import ftl.test.util.FlankTestRunner
import ftl.test.util.TestHelper.absolutePath
import ftl.test.util.TestHelper.assert
import org.junit.Rule
import org.junit.Test
Expand All @@ -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 = appApk.absolutePath()
private val testApkAbsolutePath = testApk.absolutePath()

private val androidNonDefault = """
gcloud:
Expand Down Expand Up @@ -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"))
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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<String, String>())
Expand Down Expand Up @@ -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)
}
}
67 changes: 67 additions & 0 deletions test_runner/src/test/kotlin/ftl/args/ArgsHelperFilePathTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package ftl.args

import com.google.common.truth.Truth
import ftl.test.util.FlankTestRunner
import ftl.test.util.TestHelper.absolutePath
import org.junit.Rule
import org.junit.Test
import org.junit.contrib.java.lang.system.EnvironmentVariables
import org.junit.runner.RunWith
import java.io.File

@RunWith(FlankTestRunner::class)
class ArgsHelperFilePathTest {

@get:Rule
val environmentVariables = EnvironmentVariables()

@Test
fun evaluateBlobInFilePath() {
val testApkBlobPath = "../test_app/**/app-debug-*.apk"
val actual = ArgsHelper.evaluateFilePath(testApkBlobPath)

val testApkRelativePath = "../test_app/apks/app-debug-androidTest.apk"
val expected = testApkRelativePath.absolutePath()

Truth.assertThat(actual).isEqualTo(expected)
}

private fun makeTmpFile(filePath: String): String {
val file = File(filePath)
file.parentFile.mkdirs()

file.apply {
createNewFile()
deleteOnExit()
}

return file.absolutePath
}

@Test
fun evaluateTildeInFilePath() {
val expected = makeTmpFile("/tmp/random.xctestrun")

val inputPath = "~/../../tmp/random.xctestrun"
val actual = ArgsHelper.evaluateFilePath(inputPath)

Truth.assertThat(actual).isEqualTo(expected)
}

@Test
fun evaluateEnvVarInFilePath() {
environmentVariables.set("TEST_APK_DIR", "test_app/apks")
val testApkPath = "../\$TEST_APK_DIR/app-debug-androidTest.apk"
val actual = ArgsHelper.evaluateFilePath(testApkPath)

val expected = "../test_app/apks/app-debug-androidTest.apk".absolutePath()

Truth.assertThat(actual).isEqualTo(expected)
}

@Test(expected = java.nio.file.NoSuchFileException::class)
fun evaluateInvalidFilePath() {
val testApkPath = "~/flank_test_app/invalid_path/app-debug-*.xctestrun"
ArgsHelper.evaluateFilePath(testApkPath)
}
}
Loading