-
-
Notifications
You must be signed in to change notification settings - Fork 371
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Part of Android Studio Integration: Standardize test output (#4334)
## Motivation As part of our testing to integrate Mill with Android Studio, we are experimenting with BSP integration. The stubbed test method is not compatible with the expected BSP interfaces. ## Provided in this PR The raw output provided in ADB instrumented testing is translated to TestResults as expected from the test module interface. ## Not provided in this PR ( will be done in subsequent PRs ) - Handling parallelization - Selectors ( e.g for use in test only ) - Test failure scenarios ( with examples ) - Stack traces and test output
- Loading branch information
Showing
5 changed files
with
241 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
scalalib/src/mill/javalib/android/InstrumentationOutput.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
package mill.javalib.android | ||
|
||
import mill.api.Logger | ||
import mill.testrunner.TestResult | ||
|
||
import java.io.BufferedReader | ||
import java.util.concurrent.TimeUnit | ||
import scala.concurrent.duration.FiniteDuration | ||
import scala.jdk.CollectionConverters.IteratorHasAsScala | ||
|
||
private[android] sealed trait InstrumentationOutput | ||
|
||
private[android] object InstrumentationOutput { | ||
|
||
case class TestClassName(className: String) extends InstrumentationOutput | ||
|
||
case class TestMethodName(methodName: String) extends InstrumentationOutput | ||
|
||
case object StatusStarted extends InstrumentationOutput | ||
|
||
case object StatusOk extends InstrumentationOutput | ||
|
||
case object StatusFailure extends InstrumentationOutput | ||
|
||
case object StatusError extends InstrumentationOutput | ||
|
||
case class Ignored(line: String) extends InstrumentationOutput | ||
|
||
private case class TimeResultState( | ||
started: Long, | ||
currentTestResult: TestResult, | ||
testResults: Seq[TestResult] | ||
) | ||
private val testResultStarted = TestResult("", "", 0L, "") | ||
|
||
/* Inspiration from: | ||
https://android.googlesource.com/platform/development/+/52d4c30ca52320ec92d1d1ddc8db3f07f69c4f98/tools/ddms/libs/ddmlib/src/com/android/ddmlib/testrunner/InstrumentationResultParser.java | ||
*/ | ||
|
||
def parseTestOutputStream(outputReader: BufferedReader)(logger: Logger) | ||
: (String, Seq[TestResult]) = { | ||
val state = outputReader.lines().iterator().asScala.foldLeft(TimeResultState( | ||
0L, | ||
testResultStarted, | ||
Seq.empty | ||
)) { | ||
case (state, nextLine) => | ||
logger.debug(nextLine) | ||
parseLine(nextLine) match { | ||
case InstrumentationOutput.TestClassName(className) => | ||
logger.debug(s"TestClassName=$className") | ||
TimeResultState( | ||
state.started, | ||
state.currentTestResult.copy(fullyQualifiedName = className), | ||
state.testResults | ||
) | ||
case InstrumentationOutput.TestMethodName(methodName) => | ||
logger.debug(s"TestMethodName=$methodName") | ||
val fullyQualifiedNAme = s"${state.currentTestResult.fullyQualifiedName}.${methodName}" | ||
state.copy(currentTestResult = | ||
state.currentTestResult.copy( | ||
fullyQualifiedName = fullyQualifiedNAme, | ||
selector = fullyQualifiedNAme | ||
) | ||
) | ||
case InstrumentationOutput.StatusStarted => | ||
logger.debug(s"StatusStarted") | ||
state.copy(started = System.currentTimeMillis()) | ||
case InstrumentationOutput.StatusOk => | ||
logger.debug(s"StatusOk") | ||
val ended = System.currentTimeMillis() | ||
val duration = FiniteDuration.apply(ended - state.started, TimeUnit.MILLISECONDS) | ||
val testResult = | ||
state.currentTestResult.copy(duration = duration.toSeconds, status = "Success") | ||
TimeResultState(state.started, testResultStarted, state.testResults :+ testResult) | ||
case InstrumentationOutput.StatusFailure => | ||
logger.debug(s"StatusFailure") | ||
val ended = System.currentTimeMillis() | ||
val duration = FiniteDuration.apply(ended - state.started, TimeUnit.MILLISECONDS) | ||
val testResult = | ||
state.currentTestResult.copy(duration = duration.toSeconds, status = "Failure") | ||
TimeResultState(state.started, testResultStarted, state.testResults :+ testResult) | ||
case InstrumentationOutput.StatusError => | ||
logger.debug(s"StatusError") | ||
val ended = System.currentTimeMillis() | ||
val duration = FiniteDuration.apply(ended - state.started, TimeUnit.MILLISECONDS) | ||
val testResult = | ||
state.currentTestResult.copy(duration = duration.toSeconds, status = "Error") | ||
TimeResultState(state.started, testResultStarted, state.testResults :+ testResult) | ||
case InstrumentationOutput.Ignored(line) => | ||
// todo handle stream and stack | ||
logger.debug(s"Message ${line}, ignored") | ||
state | ||
} | ||
} | ||
("", state.testResults) | ||
} | ||
|
||
def parseLine(line: String): InstrumentationOutput = { | ||
if (line.contains("class=")) { | ||
val parts = line.split("class=") | ||
TestClassName(parts(1)) | ||
} else if (line.contains("test=")) { | ||
val parts = line.split("test=") | ||
TestMethodName(parts(1)) | ||
} else if (line.contains("INSTRUMENTATION_STATUS_CODE:")) { | ||
val parts = line.split(" ") | ||
parts(1).trim() match { | ||
case "1" => StatusStarted | ||
case "0" => StatusOk | ||
case "-1" => StatusError | ||
case "-2" => StatusFailure | ||
case _ => Ignored(line) | ||
} | ||
} else { | ||
Ignored(line) | ||
} | ||
} | ||
} |
82 changes: 82 additions & 0 deletions
82
scalalib/test/src/mill/javalib/android/InstrumentalOutputReportTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package mill.javalib.android | ||
|
||
import mill.scalalib.AssemblyTestUtils | ||
import utest._ | ||
import mill.javalib.android.InstrumentationOutput._ | ||
import mill.util.DummyLogger | ||
|
||
import java.io.{BufferedReader, StringReader} | ||
|
||
object InstrumentalOutputReportTest extends TestSuite with AssemblyTestUtils { | ||
|
||
def tests: Tests = Tests { | ||
test("parseLine should parse class names correctly") { | ||
val line = "INSTRUMENTATION_STATUS: class=com.example.MyClass" | ||
val result = parseLine(line) | ||
assert(result == TestClassName("com.example.MyClass")) | ||
} | ||
|
||
test("parseLine should parse method names correctly") { | ||
val line = "INSTRUMENTATION_STATUS: test=myTestMethod" | ||
val result = parseLine(line) | ||
assert(result == TestMethodName("myTestMethod")) | ||
} | ||
|
||
test("parseLine should parse status codes correctly") { | ||
assert(parseLine("INSTRUMENTATION_STATUS_CODE: 1") == StatusStarted) | ||
assert(parseLine("INSTRUMENTATION_STATUS_CODE: 0") == StatusOk) | ||
assert(parseLine("INSTRUMENTATION_STATUS_CODE: -1") == StatusError) | ||
assert(parseLine("INSTRUMENTATION_STATUS_CODE: -2") == StatusFailure) | ||
} | ||
|
||
test("parseLine should handle unrecognized lines gracefully") { | ||
val line = "unrelated log line" | ||
val result = parseLine(line) | ||
assert(result == Ignored("unrelated log line")) | ||
} | ||
|
||
test("parse test stream into test result") { | ||
val testOutput = | ||
s""" | ||
INSTRUMENTATION_STATUS: class=com.helloworld.app.ExampleInstrumentedTest | ||
|INSTRUMENTATION_STATUS: current=1 | ||
|INSTRUMENTATION_STATUS: id=AndroidJUnitRunner | ||
|INSTRUMENTATION_STATUS: numtests=1 | ||
|INSTRUMENTATION_STATUS: stream= | ||
|com.helloworld.app.ExampleInstrumentedTest: | ||
|INSTRUMENTATION_STATUS: test=useAppContext | ||
|INSTRUMENTATION_STATUS_CODE: 1 | ||
|INSTRUMENTATION_STATUS: class=com.helloworld.app.ExampleInstrumentedTest | ||
|INSTRUMENTATION_STATUS: current=1 | ||
|INSTRUMENTATION_STATUS: id=AndroidJUnitRunner | ||
|INSTRUMENTATION_STATUS: numtests=1 | ||
|INSTRUMENTATION_STATUS: stream=. | ||
|INSTRUMENTATION_STATUS: test=useAppContext | ||
|INSTRUMENTATION_STATUS_CODE: 0 | ||
|INSTRUMENTATION_RESULT: stream= | ||
| | ||
|Time: 0.005 | ||
| | ||
|OK (1 test) | ||
| | ||
| | ||
|INSTRUMENTATION_CODE: -1 | ||
""".stripMargin | ||
|
||
val reader = new BufferedReader(new StringReader(testOutput)) | ||
|
||
val (_, testResults) = parseTestOutputStream(reader)(DummyLogger) | ||
|
||
assert(testResults.size == 1) | ||
assert( | ||
testResults.head.fullyQualifiedName == "com.helloworld.app.ExampleInstrumentedTest.useAppContext" | ||
) | ||
assert(testResults.head.duration == 0L) | ||
assert(testResults.head.status == "Success") | ||
assert( | ||
testResults.head.selector == "com.helloworld.app.ExampleInstrumentedTest.useAppContext" | ||
) | ||
|
||
} | ||
} | ||
} |