Skip to content
This repository has been archived by the owner on Dec 7, 2019. It is now read-only.

Commit

Permalink
Handle app process crash. (#95)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmitry-novikov authored and yunikkk committed Aug 9, 2017
1 parent b4d2532 commit beae497
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 82 deletions.
54 changes: 30 additions & 24 deletions composer/src/main/kotlin/com/gojuno/composer/Instrumentation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import rx.Observable
import java.io.File

data class InstrumentationTest(
val index: Int,
val total: Int,
val className: String,
val testName: String,
val status: Status,
Expand Down Expand Up @@ -54,19 +56,26 @@ private fun String.substringBetween(first: String, second: String): String {
return substring(startIndex, endIndex)
}

private fun String.parseInstrumentationStatusValue(key: String): String = substringBetween("INSTRUMENTATION_STATUS: $key=", "INSTRUMENTATION_STATUS")
private fun String.parseInstrumentationStatusValue(key: String): String = this
.substringBetween("INSTRUMENTATION_STATUS: $key=", "INSTRUMENTATION_STATUS")
.trim()

private fun parseUnableToFindInstrumentationInfo(str: String, output: File): Exception? = when (str.contains("INSTRUMENTATION_STATUS: Error=Unable to find instrumentation info for")) {
false -> null
true -> {
val runner = str.substringBetween("ComponentInfo{", "}").substringAfter("/")
Exception("Instrumentation was unable to run tests using runner $runner.\n" +
private fun String.throwIfError(output: File) = when {
contains("INSTRUMENTATION_RESULT: shortMsg=Process crashed") -> {
throw Exception("Application process crashed. Check Logcat output for more details.")
}

contains("INSTRUMENTATION_STATUS: Error=Unable to find instrumentation info for") -> {
val runner = substringBetween("ComponentInfo{", "}").substringAfter("/")
throw Exception(
"Instrumentation was unable to run tests using runner $runner.\n" +
"Most likely you forgot to declare test runner in AndroidManifest.xml or build.gradle.\n" +
"Detailed log can be found in ${output.path} or Logcat output.\n" +
"See https://github.com/gojuno/composer/issues/79 for more info."
)
}

else -> this
}

private fun parseInstrumentationEntry(str: String): InstrumentationEntry =
Expand Down Expand Up @@ -99,8 +108,11 @@ fun readInstrumentationOutput(output: File): Observable<InstrumentationEntry> {

return tail(output)
.map(String::trim)
// `INSTRUMENTATION_CODE: -1` is last line printed by instrumentation, even if 0 tests were run.
.takeWhile { !it.startsWith("INSTRUMENTATION_CODE") }
.map { it.throwIfError(output) }
.takeWhile {
// `INSTRUMENTATION_CODE: <code>` is the last line printed by instrumentation, even if 0 tests were run.
!it.startsWith("INSTRUMENTATION_CODE")
}
.scan(result()) { previousResult, newLine ->
val buffer = when (previousResult.readyForProcessing) {
true -> newLine
Expand All @@ -111,14 +123,6 @@ fun readInstrumentationOutput(output: File): Observable<InstrumentationEntry> {
}
.filter { it.readyForProcessing }
.map { it.buffer }
.map {
val unableToFindInstrumentationInfo = parseUnableToFindInstrumentationInfo(it, output)

when (unableToFindInstrumentationInfo) {
null -> it
else -> throw unableToFindInstrumentationInfo
}
}
.map(::parseInstrumentationEntry)
}

Expand All @@ -133,27 +137,29 @@ fun Observable<InstrumentationEntry>.asTests(): Observable<InstrumentationTest>
val second = entries
.subList(index + 1, entries.size)
.firstOrNull {
first.clazz == it.clazz
&&
first.test == it.test
&&
first.current == it.current
&&
first.statusCode != it.statusCode
first.clazz == it.clazz &&
first.test == it.test &&
first.current == it.current &&
first.statusCode != it.statusCode
}

if (second == null) null else first to second
}
.filterNotNull()
.map { (first, second) ->
InstrumentationTest(
index = first.current,
total = first.numTests,
className = first.clazz,
testName = first.test,
status = when (second.statusCode) {
StatusCode.Ok -> Passed
StatusCode.Ignored -> Ignored
StatusCode.Failure, StatusCode.AssumptionFailure -> Failed(stacktrace = second.stack)
StatusCode.Start -> throw IllegalStateException("Unexpected status code [${second.statusCode}] in second entry, please report that to Composer maintainers ($first, $second)")
StatusCode.Start -> throw IllegalStateException(
"Unexpected status code [Start] in second entry, " +
"please report that to Composer maintainers ($first, $second)"
)
},
durationNanos = second.timestampNanos - first.timestampNanos
)
Expand Down
122 changes: 64 additions & 58 deletions composer/src/main/kotlin/com/gojuno/composer/TestRun.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package com.gojuno.composer

import com.gojuno.commander.android.AdbDevice
import com.gojuno.commander.android.adb
import com.gojuno.commander.android.log
import com.gojuno.commander.android.pullFolder
import com.gojuno.commander.android.redirectLogcatToFile
import com.gojuno.commander.android.*
import com.gojuno.commander.os.Notification
import com.gojuno.commander.os.nanosToHumanReadableTime
import com.gojuno.commander.os.process
Expand Down Expand Up @@ -49,6 +45,7 @@ fun AdbDevice.runTests(
outputDir: File,
verboseOutput: Boolean
): Single<AdbDeviceTestRun> {

val adbDevice = this
val logsDir = File(File(outputDir, "logs"), adbDevice.id)
val instrumentationOutputFile = File(logsDir, "instrumentation.output")
Expand All @@ -63,61 +60,68 @@ fun AdbDevice.runTests(
redirectOutputTo = instrumentationOutputFile
).share()

@Suppress("destructure")
val runningTests = runTests
.ofType(Notification.Start::class.java)
.flatMap { readInstrumentationOutput(it.output) }
.asTests()
.share()

val adbDeviceTestRun = Observable.zip(
Observable.fromCallable { System.nanoTime() },
runningTests
.doOnNext { instrumentationTest ->
val status = when (instrumentationTest.status) {
InstrumentationTest.Status.Passed -> "passed"
InstrumentationTest.Status.Ignored -> "ignored"
is InstrumentationTest.Status.Failed -> "failed"
}
.doOnNext { test ->
val status = when (test.status) {
is InstrumentationTest.Status.Passed -> "passed"
is InstrumentationTest.Status.Ignored -> "ignored"
is InstrumentationTest.Status.Failed -> "failed"
}

adbDevice.log("Test $status in ${instrumentationTest.durationNanos.nanosToHumanReadableTime()}: ${instrumentationTest.className}.${instrumentationTest.testName}")
}
.flatMap { test ->
pullTestFiles(adbDevice, test, outputDir, verboseOutput)
.toObservable()
.subscribeOn(Schedulers.io())
.map { pulledFiles -> test to pulledFiles }
}
.toList()
) { startTimeNanos, testsWithPulledFiles ->
val tests = testsWithPulledFiles.map { it.first }

AdbDeviceTestRun(
adbDevice = adbDevice,
tests = testsWithPulledFiles.map { (test, pulledFiles) ->
AdbDeviceTest(
adbDevice = adbDevice,
className = test.className,
testName = test.testName,
status = when (test.status) {
InstrumentationTest.Status.Passed -> AdbDeviceTest.Status.Passed
InstrumentationTest.Status.Ignored -> AdbDeviceTest.Status.Ignored
is InstrumentationTest.Status.Failed -> AdbDeviceTest.Status.Failed(test.status.stacktrace)
},
durationNanos = test.durationNanos,
logcat = logcatFileForTest(logsDir, test.className, test.testName),
files = pulledFiles.files.sortedBy { it.name },
screenshots = pulledFiles.screenshots.sortedBy { it.name }
)
},
passedCount = tests.count { it.status is InstrumentationTest.Status.Passed },
ignoredCount = tests.count { it.status is InstrumentationTest.Status.Ignored },
failedCount = tests.count { it.status is InstrumentationTest.Status.Failed },
durationNanos = System.nanoTime() - startTimeNanos,
timestampMillis = System.currentTimeMillis(),
logcat = logcatFileForDevice(logsDir),
instrumentationOutput = instrumentationOutputFile
)
}
adbDevice.log(
"Test ${test.index}/${test.total} $status in " +
"${test.durationNanos.nanosToHumanReadableTime()}: " +
"${test.className}.${test.testName}"
)
}
.flatMap { test ->
pullTestFiles(adbDevice, test, outputDir, verboseOutput)
.toObservable()
.subscribeOn(Schedulers.io())
.map { pulledFiles -> test to pulledFiles }
}
.toList()

val adbDeviceTestRun = Observable
.zip(
Observable.fromCallable { System.nanoTime() },
runningTests,
{ time, tests -> time to tests }
)
.map { (startTimeNanos, testsWithPulledFiles) ->
val tests = testsWithPulledFiles.map { it.first }

AdbDeviceTestRun(
adbDevice = adbDevice,
tests = testsWithPulledFiles.map { (test, pulledFiles) ->
AdbDeviceTest(
adbDevice = adbDevice,
className = test.className,
testName = test.testName,
status = when (test.status) {
is InstrumentationTest.Status.Passed -> AdbDeviceTest.Status.Passed
is InstrumentationTest.Status.Ignored -> AdbDeviceTest.Status.Ignored
is InstrumentationTest.Status.Failed -> AdbDeviceTest.Status.Failed(test.status.stacktrace)
},
durationNanos = test.durationNanos,
logcat = logcatFileForTest(logsDir, test.className, test.testName),
files = pulledFiles.files.sortedBy { it.name },
screenshots = pulledFiles.screenshots.sortedBy { it.name }
)
},
passedCount = tests.count { it.status is InstrumentationTest.Status.Passed },
ignoredCount = tests.count { it.status is InstrumentationTest.Status.Ignored },
failedCount = tests.count { it.status is InstrumentationTest.Status.Failed },
durationNanos = System.nanoTime() - startTimeNanos,
timestampMillis = System.currentTimeMillis(),
logcat = logcatFileForDevice(logsDir),
instrumentationOutput = instrumentationOutputFile
)
}

val testRunFinish = runTests.ofType(Notification.Exit::class.java).cache()

Expand All @@ -131,13 +135,15 @@ fun AdbDevice.runTests(
return Observable
.zip(adbDeviceTestRun, saveLogcat, testRunFinish) { suite, _, _ -> suite }
.doOnSubscribe { adbDevice.log("Starting tests...") }
.doOnNext { adbDeviceTestRun ->
.doOnNext { testRun ->
adbDevice.log(
"Test run finished, ${adbDeviceTestRun.passedCount} passed, ${adbDeviceTestRun.failedCount} failed, took ${adbDeviceTestRun.durationNanos.nanosToHumanReadableTime()}."
"Test run finished, " +
"${testRun.passedCount} passed, " +
"${testRun.failedCount} failed, took " +
"${testRun.durationNanos.nanosToHumanReadableTime()}."
)
}
.doOnError { adbDevice.log("Error during tests run: $it") }
.take(1)
.toSingle()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ at android.app.Instrumentation.InstrumentationThread.run(Instrumentation.java:19
// We have no control over system time in tests.
assertThat(testsSubscriber.onNextEvents.map { it.copy(durationNanos = 0) }).isEqualTo(listOf(
InstrumentationTest(
index = 1,
total = 4,
className = "com.example.test.TestClass",
testName = "test1",
status = Failed(stacktrace = """java.net.UnknownHostException: Test Exception
Expand Down Expand Up @@ -253,18 +255,24 @@ at android.app.Instrumentation.InstrumentationThread.run(Instrumentation.java:19
durationNanos = 0
),
InstrumentationTest(
index = 2,
total = 4,
className = "com.example.test.TestClass",
testName = "test2",
status = Passed,
durationNanos = 0
),
InstrumentationTest(
index = 3,
total = 4,
className = "com.example.test.TestClass",
testName = "test3",
status = Passed,
durationNanos = 0
),
InstrumentationTest(
index = 4,
total = 4,
className = "com.example.test.TestClass",
testName = "test4",
status = Passed,
Expand Down Expand Up @@ -430,18 +438,24 @@ at android.app.Instrumentation.InstrumentationThread.run(Instrumentation.java:19
it("emits expected tests") {
assertThat(testsSubscriber.onNextEvents.map { it.copy(durationNanos = 0) }).isEqualTo(listOf(
InstrumentationTest(
index = 1,
total = 3,
className = "com.example.test.TestClass",
testName = "test1",
status = Passed,
durationNanos = 0L
),
InstrumentationTest(
index = 2,
total = 3,
className = "com.example.test.TestClass",
testName = "test2",
status = Passed,
durationNanos = 0L
),
InstrumentationTest(
index = 3,
total = 3,
className = "com.example.test.TestClass",
testName = "test3",
status = Passed,
Expand Down Expand Up @@ -540,12 +554,16 @@ at android.app.Instrumentation.InstrumentationThread.run(Instrumentation.java:19
it("emits expected tests") {
assertThat(testsSubscriber.onNextEvents.map { it.copy(durationNanos = 0) }).isEqualTo(listOf(
InstrumentationTest(
index = 1,
total = 2,
className = "com.example.test.TestClass",
testName = "test1",
status = Passed,
durationNanos = 0L
),
InstrumentationTest(
index = 2,
total = 2,
className = "com.example.test.TestClass",
testName = "test2",
status = Ignored,
Expand Down

0 comments on commit beae497

Please sign in to comment.